// The modules a given entry point's bundle may not contain, keyed by the // repo-relative entry point. // // ONE table, read by both layers that act on it: build.js asserts it against // esbuild's own metafile (the guarantee), and // script/lib/eslint/noStateSingletonInBackground.js reports the same // prohibition in the editor (fast feedback). It lives here because a second // literal copy of the path is exactly how a rename disarms one layer while the // other still looks enforced. // // src/shared/state.js holds a module-level `state` object, loaded once by // loadState() and mutated in place from then on. That is the popup's model: // one page, one load at boot, one lifetime. The MV3 service worker has no // "once" — it is killed when idle and revived by the next message, nothing // loads state at module scope, and an unpopulated read was answered out of // DEFAULT_STATE in silence. Five defects came from that, one of which // destroyed a wallet (https://git.eeqj.de/sneak/AutistMask/issues/324). The // background has its own per-call storage layer in src/background/state.js // instead. // // What build.js's assertion covers, measured rather than assumed: // // - Any import of a listed module, at any hop, in any specifier syntax, // however esbuild resolved it. The check reads the input list esbuild // reported for the emitted bundle, so it is the resolution the shipped // file was built from and not a model of it. Measured on a computed // specifier that esbuild constant-folds (`require("../shared/" + // "state")`), on a computed specifier it resolves as a glob // (`import("../shared/" + variable)`), and on a symlink to the module // (esbuild reports the real path): each is `make build` exit 2. // // - NOT covered: a COPY of a listed module at another path. The table is // keyed by path, so `cp src/shared/state.js src/shared/stateCopy.js` plus // a background require of the copy is `make build` exit 0 and `make lint` // exit 0 (measured). That is deliberate rather than an oversight: a copy // carries the singleton's own guard, so a background read of an unloaded // field throws StateNotLoadedError instead of being served DEFAULT_STATE // — loud, which is the opposite of the failure this table exists to // prevent. A newly WRITTEN singleton would carry no such backstop, and // nothing mechanical catches that one. // // - The table protects the entry points it names. A second background-side // entry point added later needs its own line here; the ESLint rule's // src/background/** glob would cover it, the build assertion would not. // // Both halves are checked for rot at the end of a build: a key no bundled // entry point matched, and a listed module this build bundled nowhere, each // fail the build rather than passing vacuously // (assertForbiddenTableCovered(), pinned by tests/buildForbiddenInputs.test.js). const FORBIDDEN_INPUTS = { "src/background/index.js": ["src/shared/state.js"], }; module.exports = { FORBIDDEN_INPUTS };