Merge: build-time IIFE guard (t-paliad-053)
This commit is contained in:
@@ -35,6 +35,26 @@ import { renderNotFound } from "./src/notfound";
|
||||
|
||||
const DIST = join(import.meta.dir, "dist");
|
||||
|
||||
// Bundle-scope isolation guard (t-paliad-043).
|
||||
//
|
||||
// All client bundles MUST be built with format: "iife" so each bundle's
|
||||
// top-level `var`/`function` declarations are wrapped in their own scope.
|
||||
// Without IIFE wrapping, minified identifiers leak to `window` and clobber
|
||||
// each other across bundles. On Apr 26, app.js's `var d = "patholo-sidebar-pinned"`
|
||||
// overwrote projects.js's `function d()` (applyTranslations), and the entire
|
||||
// authenticated surface crashed in initI18n with "TypeError: d is not a function".
|
||||
//
|
||||
// The constant below is the single source of truth for the bundle format;
|
||||
// the post-build inspection further down verifies that every emitted asset
|
||||
// actually starts with an IIFE prologue, so this guard survives future Bun
|
||||
// versions, refactors that drop the constant, or anyone trying to silence
|
||||
// the type system with `as "esm"`.
|
||||
const BUILD_FORMAT = "iife" as const;
|
||||
|
||||
// Bun emits IIFE bundles as either `(()=>{...})()` (arrow form, what we get
|
||||
// today with minify: true) or `(function(){...})()`. Match either prologue.
|
||||
const IIFE_PROLOGUE = /^(\(\(\)\s*=>\s*\{|\(function\s*\(\s*\)\s*\{)/;
|
||||
|
||||
async function build() {
|
||||
// Clean dist/
|
||||
await rm(DIST, { recursive: true, force: true });
|
||||
@@ -83,14 +103,10 @@ async function build() {
|
||||
outdir: join(DIST, "assets"),
|
||||
naming: "[name].js",
|
||||
minify: true,
|
||||
// IIFE wraps each bundle's top-level scope so per-page modules don't
|
||||
// leak globals into each other. Without this, app.js's minified
|
||||
// `var d = "patholo-sidebar-pinned"` (the legacy sidebar storage key)
|
||||
// clobbered projects.js's minified `function d()` (applyTranslations)
|
||||
// on the global object — projects.js then crashed with
|
||||
// "TypeError: d is not a function" inside its DOMContentLoaded init,
|
||||
// making /projects appear empty (t-paliad-043).
|
||||
format: "iife",
|
||||
// See BUILD_FORMAT comment at top of file — bundle-scope isolation
|
||||
// depends on IIFE wrapping. Reuses the single-source-of-truth constant
|
||||
// so the post-build guard below can detect a format swap.
|
||||
format: BUILD_FORMAT,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
@@ -101,6 +117,28 @@ async function build() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Bundle-scope isolation guard (t-paliad-043) — verify every emitted JS
|
||||
// bundle starts with an IIFE prologue. This catches the case where
|
||||
// BUILD_FORMAT is changed to "esm", `format` is dropped from the Bun.build
|
||||
// call, or a future Bun version emits a non-IIFE wrapper despite the
|
||||
// option. Without this, top-level identifier collisions between bundles
|
||||
// can take down the whole authenticated surface (see comment at top).
|
||||
const emittedAssets = await readdir(join(DIST, "assets"));
|
||||
for (const f of emittedAssets) {
|
||||
if (!f.endsWith(".js")) continue;
|
||||
const head = (await Bun.file(join(DIST, "assets", f)).text()).slice(0, 64);
|
||||
if (!IIFE_PROLOGUE.test(head)) {
|
||||
console.error(
|
||||
`Build aborted: dist/assets/${f} is not IIFE-wrapped ` +
|
||||
`(starts with ${JSON.stringify(head.slice(0, 32))}). ` +
|
||||
`All client bundles must be built with Bun.build({ format: "iife" }) — ` +
|
||||
`per-page bundles' top-level identifiers leak to window and clobber ` +
|
||||
`each other after minification (see t-paliad-043).`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy CSS
|
||||
await cp(
|
||||
join(import.meta.dir, "src/styles/global.css"),
|
||||
|
||||
Reference in New Issue
Block a user