From 1517649704f3f26dda8142c10e2855b944c81b7a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:48:30 +0000 Subject: [PATCH 1/5] bundler: run CommonJS and lazily-initialized scripts of an HTML entry point A + + + + + +`, + "/umd.js": ` +(function (root, factory) { + root.LIB = factory(); +})(this, function () { + return { v: 1 }; +}); +console.log("umd");`, + "/guarded.js": ` +console.log("guarded"); +if (typeof module === "object") module.exports = { guarded: true };`, + "/ext.cjs": `console.log("cjs extension");`, + "/requires.js": ` +const dep = require("./dep.js"); +console.log("requires " + dep);`, + "/dep.js": `module.exports = 7;`, + "/plain.js": `console.log("plain");`, + }, + entryPoints: ["/index.html"], + onAfterBundle(api) { + const js = api.readFile("out/index.html").match(/src="\.\/([^"]+\.js)"/)![1]; + api.writeFile("out/run.mjs", `import "./${js}";`); + }, + run: { file: "out/run.mjs", stdout: "umd\nguarded\ncjs extension\nrequires 7\nplain" }, + }); + + // Every `.js` file of a `"type": "commonjs"` package (the `npm init` default) + // is classified as CommonJS, so even a plain `console.log` script is wrapped. + itBundled("html/script-src-type-commonjs-package", { + outdir: "out/", + files: { + "/package.json": `{ "name": "app", "version": "1.0.0", "type": "commonjs" }`, + "/index.html": ``, + "/first.js": `console.log("first");`, + "/second.js": `console.log("second");`, + }, + entryPoints: ["/index.html"], + onAfterBundle(api) { + const js = api.readFile("out/index.html").match(/src="\.\/([^"]+\.js)"/)![1]; + api.writeFile("out/run.mjs", `import "./${js}";`); + }, + run: { file: "out/run.mjs", stdout: "first\nsecond" }, + }); + + // Same, when the script resolved to a lazily-initialized ES module (here + // because another script also `import()`s it): the page calls `init_foo()`, + // with `await` when the module uses top-level await. + itBundled("html/script-src-lazy-esm", { + outdir: "out/", + files: { + "/index.html": ` + + + + + + + +`, + "/sync.js": ` +export const sync = "sync"; +console.log(sync);`, + "/tla.js": ` +export const tla = await Promise.resolve("tla"); +console.log(tla);`, + "/main.js": ` +console.log("main"); +const { sync } = await import("./sync.js"); +const { tla } = await import("./tla.js"); +console.log("lazy " + sync + " " + tla);`, + }, + entryPoints: ["/index.html"], + onAfterBundle(api) { + const js = api.readFile("out/index.html").match(/src="\.\/([^"]+\.js)"/)![1]; + api.expectFile("out/" + js).toMatch(/init_sync\(\);\s*await init_tla\(\);/); + api.writeFile("out/run.mjs", `import "./${js}";`); + }, + run: { file: "out/run.mjs", stdout: "sync\ntla\nmain\nlazy sync tla" }, + }); + + // With code splitting the CommonJS script shared by two pages moves to its + // own chunk. Each page imports `require_lib` from it and still calls it. The + // page itself never reads the namespace, so it does not import `__toESM` + // (two.js does, for its own `import * as`). + itBundled("html/script-src-commonjs-splitting", { + outdir: "out/", + splitting: true, + files: { + "/one.html": ``, + "/two.html": ``, + "/lib.js": ` +console.log("lib"); +module.exports = { lib: true };`, + "/one.js": `console.log("one");`, + "/two.js": ` +import * as ns from "./lib.js"; +console.log("two", ns.default.lib);`, + }, + entryPoints: ["/one.html", "/two.html"], + onAfterBundle(api) { + for (const page of ["one", "two"]) { + const js = api.readFile(`out/${page}.html`).match(/src="\.\/([^"]+\.js)"/)![1]; + if (page === "one") api.expectFile("out/" + js).not.toContain("__toESM"); + api.writeFile(`out/run-${page}.mjs`, `import "./${js}";`); + } + }, + run: [ + { file: "out/run-one.mjs", stdout: "lib\none" }, + { file: "out/run-two.mjs", stdout: "lib\ntwo true" }, + ], + }); + // Test CSS imports itBundled("html/css-imports", { outdir: "out/", diff --git a/test/bundler/html-import-manifest.test.ts b/test/bundler/html-import-manifest.test.ts index 11b5454c9bf3..d8d4eadeb03a 100644 --- a/test/bundler/html-import-manifest.test.ts +++ b/test/bundler/html-import-manifest.test.ts @@ -141,11 +141,11 @@ console.log(favicon); "files": [ { "input": "client.html", - "path": "./client-qjznw47b.js", + "path": "./client-c9kjeh05.js", "loader": "js", "isEntry": true, "headers": { - "etag": "l5IfNVyE54s", + "etag": "7GmzUg4RQCU", "content-type": "text/javascript;charset=utf-8" } }, @@ -155,7 +155,7 @@ console.log(favicon); "loader": "html", "isEntry": true, "headers": { - "etag": "xh1kdn7wbmI", + "etag": "L1NGVitZslc", "content-type": "text/html;charset=utf-8" } }, @@ -325,17 +325,17 @@ console.log("About manifest:", aboutHtml); { "headers": { "content-type": "text/javascript;charset=utf-8", - "etag": "Dl7kT6q7eY4", + "etag": "Vr91Jdsy4Zc", }, "input": "home.html", "isEntry": true, "loader": "js", - "path": "./home-ey4favse.js", + "path": "./home-pzn5vj1q.js", }, { "headers": { "content-type": "text/html;charset=utf-8", - "etag": "IhvRaM9jGDU", + "etag": "bO9UOC8tMwQ", }, "input": "home.html", "isEntry": true, @@ -360,17 +360,17 @@ console.log("About manifest:", aboutHtml); { "headers": { "content-type": "text/javascript;charset=utf-8", - "etag": "RCRrF1EbBvo", + "etag": "xjiS27vgDE0", }, "input": "about.html", "isEntry": true, "loader": "js", - "path": "./about-44bqhv6t.js", + "path": "./about-6rjvv0cd.js", }, { "headers": { "content-type": "text/html;charset=utf-8", - "etag": "2OGqRD6vx54", + "etag": "yq0KRVKuKEY", }, "input": "about.html", "isEntry": true, diff --git a/test/bundler/standalone.test.ts b/test/bundler/standalone.test.ts index 20cbc916e2af..5369f5fad531 100644 --- a/test/bundler/standalone.test.ts +++ b/test/bundler/standalone.test.ts @@ -74,6 +74,42 @@ console.log(data);`, expect(html).toContain("await"); }); + // A script that bun classifies as CommonJS (UMD header with top-level `this`, + // or a guarded `module.exports`) is wrapped in `__commonJS`. The inlined + // module must still run it, in document order. + test("runs CommonJS scripts in document order", async () => { + using dir = tempDir("compile-browser-cjs-script", { + "index.html": ``, + "umd.js": `(function (root, factory) { root.LIB = factory(); })(this, function () { return { v: 1 }; }); +console.log("umd");`, + "guarded.js": `console.log("guarded"); +if (typeof module === "object") module.exports = { guarded: true };`, + "plain.js": `console.log("plain");`, + }); + + const result = await Bun.build({ + entrypoints: [`${dir}/index.html`], + compile: true, + target: "browser", + }); + + expect(result.success).toBe(true); + const html = await result.outputs[0].text(); + const script = html.match(/ in inlined JS", async () => { using dir = tempDir("compile-browser-escape-script", { "index.html": ``, diff --git a/test/js/bun/http/bun-serve-html.test.ts b/test/js/bun/http/bun-serve-html.test.ts index 45ab33d47126..5a7e85be8edb 100644 --- a/test/js/bun/http/bun-serve-html.test.ts +++ b/test/js/bun/http/bun-serve-html.test.ts @@ -224,7 +224,7 @@ console.log("How...dashing?"); "content-length": "316", "content-type": "text/javascript;charset=utf-8", "date": "", - "etag": ""f862dbeedf9b72bc"", + "etag": ""c2050d7cfe555369"", "sourcemap": "/chunk-HASH.js.map", } `); From 02c77083a492aeacd34b73c3a155f35802e4e503 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:46:28 +0000 Subject: [PATCH 2/5] test: cover a CommonJS + + + +`, + "/first.js": `console.log("first");`, + "/umd-badge.js": ` +(function (root, factory) { + if (typeof module === "object" && module.exports) module.exports = factory(); + else root.Badge = factory(); +})(globalThis, function () { + console.log("umd badge"); + return {}; +});`, + "/last.js": `console.log("last");`, + }, + entryPoints: ["/index.html"], + onAfterBundle(api) { + const js = api.readFile("out/index.html").match(/src="\.\/([^"]+\.js)"/)![1]; + api.expectFile("out/" + js).not.toContain("__toESM"); + api.writeFile("out/run.mjs", `import "./${js}";`); + }, + run: { file: "out/run.mjs", stdout: "first\numd badge\nlast" }, + }); + // Same, when the script resolved to a lazily-initialized ES module (here // because another script also `import()`s it): the page calls `init_foo()`, // with `await` when the module uses top-level await. From 4fb8cf5cdd89171090ee637788422d3cafa2d42e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:38:06 +0000 Subject: [PATCH 3/5] bundler: call a wrapped + `, "/first.js": `console.log("first");`, @@ -335,6 +337,7 @@ console.log("requires " + dep);`, return {}; });`, "/last.js": `console.log("last");`, + "/tail.cjs": `console.log("tail");`, }, entryPoints: ["/index.html"], onAfterBundle(api) { @@ -342,7 +345,37 @@ console.log("requires " + dep);`, api.expectFile("out/" + js).not.toContain("__toESM"); api.writeFile("out/run.mjs", `import "./${js}";`); }, - run: { file: "out/run.mjs", stdout: "first\numd badge\nlast" }, + run: { file: "out/run.mjs", stdout: "first\numd badge\nlast\ntail" }, + }); + + // A `` only refers to its file: the page calls the wrapper + // of a CommonJS ` + + +`, + "/worker.js": ` +console.log("worker"); +module.exports = {};`, + "/lib.cjs": `console.log("lib");`, + "/app.js": `console.log("app");`, + }, + entryPoints: ["/index.html"], + onAfterBundle(api) { + const js = api.readFile("out/index.html").match(/src="\.\/([^"]+\.js)"/)![1]; + api.writeFile("out/run.mjs", `import "./${js}";`); + }, + run: { file: "out/run.mjs", stdout: "lib\napp" }, }); // Same, when the script resolved to a lazily-initialized ES module (here diff --git a/test/bundler/html-import-manifest.test.ts b/test/bundler/html-import-manifest.test.ts index 5ce08a6ca37e..2698ff6936ef 100644 --- a/test/bundler/html-import-manifest.test.ts +++ b/test/bundler/html-import-manifest.test.ts @@ -141,11 +141,11 @@ console.log(favicon); "files": [ { "input": "client.html", - "path": "./client-c9kjeh05.js", + "path": "./client-qjznw47b.js", "loader": "js", "isEntry": true, "headers": { - "etag": "7GmzUg4RQCU", + "etag": "l5IfNVyE54s", "content-type": "text/javascript;charset=utf-8" } }, @@ -155,7 +155,7 @@ console.log(favicon); "loader": "html", "isEntry": true, "headers": { - "etag": "L1NGVitZslc", + "etag": "xh1kdn7wbmI", "content-type": "text/html;charset=utf-8" } }, @@ -325,17 +325,17 @@ console.log("About manifest:", aboutHtml); { "headers": { "content-type": "text/javascript;charset=utf-8", - "etag": "Vr91Jdsy4Zc", + "etag": "Dl7kT6q7eY4", }, "input": "home.html", "isEntry": true, "loader": "js", - "path": "./home-pzn5vj1q.js", + "path": "./home-ey4favse.js", }, { "headers": { "content-type": "text/html;charset=utf-8", - "etag": "bO9UOC8tMwQ", + "etag": "IhvRaM9jGDU", }, "input": "home.html", "isEntry": true, @@ -360,17 +360,17 @@ console.log("About manifest:", aboutHtml); { "headers": { "content-type": "text/javascript;charset=utf-8", - "etag": "xjiS27vgDE0", + "etag": "RCRrF1EbBvo", }, "input": "about.html", "isEntry": true, "loader": "js", - "path": "./about-6rjvv0cd.js", + "path": "./about-44bqhv6t.js", }, { "headers": { "content-type": "text/html;charset=utf-8", - "etag": "yq0KRVKuKEY", + "etag": "2OGqRD6vx54", }, "input": "about.html", "isEntry": true, diff --git a/test/js/bun/http/bun-serve-html.test.ts b/test/js/bun/http/bun-serve-html.test.ts index c5b3ff58bb59..32cbe93c1061 100644 --- a/test/js/bun/http/bun-serve-html.test.ts +++ b/test/js/bun/http/bun-serve-html.test.ts @@ -224,7 +224,7 @@ console.log("How...dashing?"); "content-length": "316", "content-type": "text/javascript;charset=utf-8", "date": "", - "etag": ""c2050d7cfe555369"", + "etag": ""f862dbeedf9b72bc"", "sourcemap": "/chunk-HASH.js.map", } `); From 187526cb1a91f1eeaf46bbb02dc8a0d78c10c85d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:12:02 +0000 Subject: [PATCH 4/5] bundler: keep the comments of the HTML script wrapper calls to one line each --- src/ast/import_record.rs | 3 +-- src/bundler/LinkerContext.rs | 12 ++++-------- .../linker_context/findAllImportedPartsInJSOrder.rs | 3 +-- src/bundler/linker_context/scanImportsAndExports.rs | 3 +-- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/ast/import_record.rs b/src/ast/import_record.rs index ceb54e23255a..0a77a0d7ec4e 100644 --- a/src/ast/import_record.rs +++ b/src/ast/import_record.rs @@ -108,8 +108,7 @@ bitflags::bitflags! { /// import), and those records must never be resolved. const IS_BARREL_DEFERRED = 1 << 18; - /// The `src` of a ` `, + "/pure.js": `export const pure = "pure";`, "/sync.js": ` export const sync = "sync"; console.log(sync);`, @@ -401,9 +404,10 @@ export const tla = await Promise.resolve("tla"); console.log(tla);`, "/main.js": ` console.log("main"); +const { pure } = await import("./pure.js"); const { sync } = await import("./sync.js"); const { tla } = await import("./tla.js"); -console.log("lazy " + sync + " " + tla);`, +console.log("lazy " + pure + " " + sync + " " + tla);`, }, entryPoints: ["/index.html"], onAfterBundle(api) { @@ -411,7 +415,55 @@ console.log("lazy " + sync + " " + tla);`, api.expectFile("out/" + js).toMatch(/init_sync\(\);\s*await init_tla\(\);/); api.writeFile("out/run.mjs", `import "./${js}";`); }, - run: { file: "out/run.mjs", stdout: "sync\ntla\nmain\nlazy sync tla" }, + run: { file: "out/run.mjs", stdout: "sync\ntla\nmain\nlazy pure sync tla" }, + }); + + // The call is a statement of the page, so each output format, the minifier + // and the source map path keep it between the scripts around its tag. + for (const [name, options] of [ + ["iife", { format: "iife" }], + ["cjs", { format: "cjs" }], + ["minify", { minifyIdentifiers: true, minifySyntax: true, minifyWhitespace: true }], + ["sourcemap", { sourceMap: "external" }], + ] as const) { + itBundled(`html/script-src-commonjs-${name}`, { + outdir: "out/", + ...options, + files: { + "/index.html": ``, + "/first.js": `console.log("first");`, + "/lib.cjs": `console.log("lib");`, + "/last.js": `console.log("last");`, + }, + entryPoints: ["/index.html"], + onAfterBundle(api) { + const js = api.readFile("out/index.html").match(/src="\.\/([^"]+\.js)"/)![1]; + if (name === "sourcemap") { + const sources: string[] = JSON.parse(api.readFile(`out/${js}.map`)).sources; + expect(sources.map(source => source.split("/").pop())).toEqual(["lib.cjs", "first.js", "last.js"]); + } + api.writeFile("out/run.mjs", `import "./${js}";`); + }, + run: { file: "out/run.mjs", stdout: "first\nlib\nlast" }, + }); + } + + // A page that a script loads with `import()` is a lazily-initialized module + // itself: its scripts run, in document order, when that module does. + itBundled("html/script-src-commonjs-in-imported-page", { + outdir: "out/", + target: "browser", + files: { + "/entry.js": ` +console.log("entry"); +await import("./page.html"); +console.log("loaded");`, + "/page.html": ``, + "/lib.cjs": `console.log("lib");`, + "/plain.js": `console.log("plain");`, + }, + entryPoints: ["/entry.js"], + run: { file: "out/entry.js", stdout: "entry\nlib\nplain\nloaded" }, }); // With code splitting the CommonJS script shared by two pages moves to its