Files
AutistMask/build.js
clawbot 9e209e27bd
All checks were successful
check / check (push) Successful in 32s
chore: repo policy compliance sweep — test rerun, npx, frozen lockfile, docs (closes #166)
- script/test runs the suite quietly, then reruns it with --verbose on
  failure and always exits 1 (REPO_POLICIES.md conditional verbose rerun
  pattern). New package.json script test:verbose is the -v form of the
  existing jest --forceExit invocation.
- build.js calls node_modules/.bin/tailwindcss instead of npx, which
  would fetch from the registry unpinned if the binary were absent.
- make install uses --frozen-lockfile, so a stale yarn.lock fails the
  target instead of being silently rewritten.
- README Getting Started uses make setup (which also installs the
  pre-commit hook); the Makefile-only targets (install, hooks, build,
  build-debug, clean, dev) are now documented in Entrypoints.
- .dockerignore records why .git is deliberately not excluded.
2026-08-11 12:34:02 +00:00

215 lines
7.5 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const esbuild = require("esbuild");
const DIST = path.join(__dirname, "dist");
const DIST_CHROME = path.join(DIST, "chrome");
const DIST_FIREFOX = path.join(DIST, "firefox");
const SRC = path.join(__dirname, "src");
// The module whose compiled DEBUG state script/verify-build asserts, and the
// manifest naming every emitted bundle that ends up containing it. The
// manifest is derived from esbuild's own dependency graph rather than from a
// hardcoded list, so it tracks the bundle layout instead of rotting with it.
const AUDITED_MODULE = "src/shared/constants.js";
const BUNDLE_MANIFEST = path.join(DIST, "constants-bundles.txt");
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
// Repo-relative, forward-slashed, so the manifest reads the same on every
// platform and can be consumed by a POSIX shell script without further work.
function repoRelative(p) {
return path.relative(__dirname, p).split(path.sep).join("/");
}
// Collect the outputs of one esbuild run that bundle AUDITED_MODULE. esbuild
// reports every input that contributed to an output in the metafile, which is
// the authoritative answer to "is constants.js in this bundle" — unlike
// searching the minified text, it does not depend on what survived minification.
function outputsContainingAuditedModule(metafile) {
return Object.entries(metafile.outputs)
.filter(([outFile, info]) => {
if (!outFile.endsWith(".js")) return false;
return Object.keys(info.inputs).some(
(input) => repoRelative(input) === AUDITED_MODULE,
);
})
.map(([outFile]) => repoRelative(outFile));
}
// DEBUG is a build-time flag, off unless explicitly requested. It is the only
// thing that makes the hardcoded test mnemonic reachable, so the opt-in must be
// exact: anything other than the literal "1" (unset, empty, "true", a typo)
// produces a release build. Failing towards the safe mode is deliberate.
function isDebugBuild() {
return process.env.AUTISTMASK_DEBUG === "1";
}
function getBuildInfo() {
const pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
);
let commitHash = "unknown";
try {
commitHash = execSync("git rev-parse --short HEAD", {
encoding: "utf8",
}).trim();
} catch (_) {
// not a git repo or git not available
}
let commitHashFull = "unknown";
try {
commitHashFull = execSync("git rev-parse HEAD", {
encoding: "utf8",
}).trim();
} catch (_) {
// not a git repo or git not available
}
return {
version: pkg.version,
license: pkg.license,
author: pkg.author,
commitHash,
commitHashFull,
buildDate: new Date().toISOString().slice(0, 10),
};
}
async function build() {
console.log("Building AutistMask extension...");
const buildInfo = getBuildInfo();
console.log("Build info:", buildInfo);
const debugBuild = isDebugBuild();
console.log(
debugBuild
? "Build mode: DEBUG (INSECURE - hardcoded test mnemonic, do not ship)"
: "Build mode: release (DEBUG off)",
);
const define = {
__BUILD_DEBUG__: JSON.stringify(debugBuild),
__BUILD_VERSION__: JSON.stringify(buildInfo.version),
__BUILD_LICENSE__: JSON.stringify(buildInfo.license),
__BUILD_AUTHOR__: JSON.stringify(buildInfo.author),
__BUILD_COMMIT__: JSON.stringify(buildInfo.commitHash),
__BUILD_COMMIT_FULL__: JSON.stringify(buildInfo.commitHashFull),
__BUILD_DATE__: JSON.stringify(buildInfo.buildDate),
};
// Emitted bundles that contain constants.js, accumulated across every
// esbuild run below and written out for script/verify-build.
const auditedBundles = [];
// compile tailwind CSS
console.log("Compiling Tailwind CSS...");
const tailwindInput = path.join(SRC, "popup", "styles", "main.css");
const tailwindOutput = path.join(DIST, "styles.css");
ensureDir(DIST);
// Drop any manifest from a previous build before emitting anything, so a
// build that never gets around to writing one cannot be verified against
// a stale list.
fs.rmSync(BUNDLE_MANIFEST, { force: true });
// The locally installed binary, not `npx` — npx silently fetches from the
// registry when the binary is absent, which is an unpinned network fetch
// in the middle of a build.
const tailwindBin = path.join(
__dirname,
"node_modules",
".bin",
"tailwindcss",
);
execSync(
`"${tailwindBin}" -i "${tailwindInput}" -o "${tailwindOutput}" --minify`,
{ stdio: "inherit" },
);
// Every bundle goes through here, so metafile collection cannot be
// forgotten when a new entry point is added.
async function bundle(entryPoint, outfile) {
const result = await esbuild.build({
entryPoints: [entryPoint],
bundle: true,
format: "iife",
outfile,
platform: "browser",
target: ["chrome110", "firefox110"],
minify: true,
metafile: true,
define,
});
auditedBundles.push(...outputsContainingAuditedModule(result.metafile));
}
for (const distDir of [DIST_CHROME, DIST_FIREFOX]) {
ensureDir(path.join(distDir, "src", "popup"));
ensureDir(path.join(distDir, "src", "background"));
ensureDir(path.join(distDir, "src", "content"));
// bundle popup JS with esbuild (inlines ethers, libsodium, etc.)
await bundle(
path.join(SRC, "popup", "index.js"),
path.join(distDir, "src", "popup", "index.js"),
);
// bundle background script
await bundle(
path.join(SRC, "background", "index.js"),
path.join(distDir, "src", "background", "index.js"),
);
// bundle content script
await bundle(
path.join(SRC, "content", "index.js"),
path.join(distDir, "src", "content", "index.js"),
);
// bundle inpage script (injected into page context, separate file)
await bundle(
path.join(SRC, "content", "inpage.js"),
path.join(distDir, "src", "content", "inpage.js"),
);
// copy popup HTML
fs.copyFileSync(
path.join(SRC, "popup", "index.html"),
path.join(distDir, "src", "popup", "index.html"),
);
// place compiled CSS next to popup HTML
fs.copyFileSync(
tailwindOutput,
path.join(distDir, "src", "popup", "styles.css"),
);
}
// copy manifests
fs.copyFileSync(
path.join(__dirname, "manifest", "chrome.json"),
path.join(DIST_CHROME, "manifest.json"),
);
fs.copyFileSync(
path.join(__dirname, "manifest", "firefox.json"),
path.join(DIST_FIREFOX, "manifest.json"),
);
// Written last so a build that died partway through leaves no manifest
// at all, which script/verify-build treats as a hard failure rather than
// as "nothing to check".
const manifest = [...new Set(auditedBundles)].sort();
fs.writeFileSync(BUNDLE_MANIFEST, manifest.map((p) => `${p}\n`).join(""));
console.log(
`Bundles containing ${AUDITED_MODULE}: ${manifest.length} ` +
`(listed in ${repoRelative(BUNDLE_MANIFEST)})`,
);
console.log("Build complete: dist/chrome/ and dist/firefox/");
}
build();