fix: make dev rebuilds dist/ on source changes (closes #332)
make dev passed --watch to a build.js that read no arguments, so it built once and exited. build.js --watch now builds, then builds again after every change to a file under src/, manifest/ or icons/, until interrupted; any other argument fails. Each directory gets its own watcher, because Node's recursive watch on Linux loses a file that an editor saves by renaming a new copy over it. A watch build writes no build receipt and cannot be verified; README.md and the Makefile say so and point to make build. Model: opus-5-5
This commit is contained in:
@@ -15,6 +15,15 @@ const DIST_CHROME = path.join(DIST, "chrome");
|
||||
const DIST_FIREFOX = path.join(DIST, "firefox");
|
||||
const SRC = path.join(__dirname, "src");
|
||||
|
||||
// What `make dev` watches: the directories whose files build() bundles,
|
||||
// compiles or copies. A change anywhere else, package.json and build.js
|
||||
// included, starts no build; restart make dev after one.
|
||||
const WATCHED_DIRS = [
|
||||
SRC,
|
||||
path.join(__dirname, "manifest"),
|
||||
path.join(__dirname, "icons"),
|
||||
];
|
||||
|
||||
// The module whose compiled DEBUG state script/verify-build asserts. Which
|
||||
// bundles contain it is derived from esbuild's own dependency graph rather
|
||||
// than from a hardcoded list, so it tracks the bundle layout instead of
|
||||
@@ -441,6 +450,10 @@ function getBuildInfo() {
|
||||
async function build() {
|
||||
console.log("Building AutistMask extension...");
|
||||
|
||||
// Under make dev this runs once per rebuild in the same process, and each
|
||||
// build accounts only for what it emits itself.
|
||||
emittedFiles.length = 0;
|
||||
|
||||
const receiptPath = receiptTarget();
|
||||
if (!receiptPath) {
|
||||
console.warn(
|
||||
@@ -597,27 +610,94 @@ async function build() {
|
||||
console.log("Build complete: dist/chrome/ and dist/firefox/");
|
||||
}
|
||||
|
||||
// Run only as a program. Required as a module — which is how
|
||||
// tests/buildForbiddenInputs.test.js reaches the checks below — this file
|
||||
// builds nothing and writes nothing.
|
||||
if (require.main === module) {
|
||||
build().catch((err) => {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
// make dev: build, then build again after every change under `dirs`, until
|
||||
// interrupted. A failed build is reported and watching carries on, so a
|
||||
// half-finished edit does not end it. A change that arrives while a build is
|
||||
// running is not lost: it causes one more build as soon as that one finishes.
|
||||
// A directory created after this starts is not watched until a restart.
|
||||
//
|
||||
// make dev sets no AUTISTMASK_BUILD_RECEIPT, so these builds write no receipt
|
||||
// and nothing can verify what they leave in dist/.
|
||||
//
|
||||
// `rebuild` is build() everywhere but tests/buildWatch.test.js, which also
|
||||
// closes the returned watchers.
|
||||
function watch(dirs, rebuild) {
|
||||
let building = false;
|
||||
let changedAgain = false;
|
||||
|
||||
async function run() {
|
||||
if (building) {
|
||||
changedAgain = true;
|
||||
return;
|
||||
}
|
||||
building = true;
|
||||
do {
|
||||
// Let the rest of one save's events arrive first, so that one
|
||||
// save is one build.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
changedAgain = false;
|
||||
try {
|
||||
await rebuild();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
}
|
||||
} while (changedAgain);
|
||||
building = false;
|
||||
console.log("Watching for changes (Ctrl-C to stop)...");
|
||||
}
|
||||
|
||||
// One watcher per directory rather than fs.watch's recursive option: on
|
||||
// Linux, Node's recursive watch watches each file, and stops seeing one
|
||||
// that an editor saves by renaming a new copy over it. A directory's
|
||||
// watcher reports every change to the files in it, however they are saved.
|
||||
const subdirectories = dirs.flatMap((dir) =>
|
||||
fs
|
||||
.readdirSync(dir, { recursive: true, withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(entry.parentPath, entry.name)),
|
||||
);
|
||||
const watchers = [...dirs, ...subdirectories].map((dir) =>
|
||||
fs.watch(dir, run),
|
||||
);
|
||||
run();
|
||||
return watchers;
|
||||
}
|
||||
|
||||
// Exported for tests/buildForbiddenInputs.test.js only. The prohibition these
|
||||
// three functions enforce is the guarantee behind
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/324, and `make check` does not
|
||||
// run `make build` — so they are unit tested against synthetic metafiles
|
||||
// rather than being exercised only by CI, where "it ran" is not "it works".
|
||||
// Run only as a program. Required as a module — which is how
|
||||
// tests/buildForbiddenInputs.test.js and tests/buildWatch.test.js reach the
|
||||
// functions below — this file builds nothing and writes nothing.
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
// An argument this file does not know fails rather than being ignored.
|
||||
if (args.length > 1 || (args.length === 1 && args[0] !== "--watch")) {
|
||||
console.error("usage: node build.js [--watch]");
|
||||
process.exit(2);
|
||||
}
|
||||
if (args[0] === "--watch") {
|
||||
watch(WATCHED_DIRS, build);
|
||||
} else {
|
||||
build().catch((err) => {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for tests only: watch() for tests/buildWatch.test.js, the rest for
|
||||
// tests/buildForbiddenInputs.test.js. The prohibition those enforce is the
|
||||
// guarantee behind https://git.eeqj.de/sneak/AutistMask/issues/324, and
|
||||
// `make check` does not run `make build` — so they are unit tested against
|
||||
// synthetic metafiles rather than being exercised only by CI, where "it ran"
|
||||
// is not "it works".
|
||||
module.exports = {
|
||||
importChain,
|
||||
newForbiddenRecord,
|
||||
recordBundledInputs,
|
||||
assertNoForbiddenInputs,
|
||||
assertForbiddenTableCovered,
|
||||
watch,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user