Skip to content

bundler: keep the import kind of server-side HTML imports so import() and require() of the manifest link - #38621

Open
robobun wants to merge 12 commits into
mainfrom
farm/2af2873b/html-import-dynamic-require-wrap
Open

robobun wants to merge 12 commits into
mainfrom
farm/2af2873b/html-import-dynamic-require-wrap

Conversation

@robobun

@robobun robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A --target=bun or node bundle dies on import("./page.html") or require("./page.html"): ReferenceError: require_page is not defined. In a CommonJS file, require() gives { default: manifest }.
  • With a static importer in another file, one tree gives two bundles: 70 of 240 fail.
  • Cause: resolve_import_records (src/bundler/bundle_v2.rs:7316) overwrote the record kind of the first importer to finish parsing with ImportKind::HtmlManifest. The linker wraps a module only for Dynamic/Require records (scanImportsAndExports.rs:359-411).

Fix

  • Keep the record's real kind. Delete ImportKind::HtmlManifest.
  • Give the manifest module the server target and the js loader. Keep it out of --check.
  • Correct because the manifest module links like a .json file: one tree gives one bundle.
  • Verified: test/bundler/html-import-manifest.test.ts (10 new tests, 9 fail without the fix) and test/cli/check/entry-points.test.ts. Also bundler_html, bundler_html_server, metafile.

Background

  • A server-side .html import evaluates to a JSON manifest of the page's browser build. A generated "manifest module" holds it: export default, or a __commonJS wrapper for require() and import().
  • Considered a linker arm for HtmlManifest: that kind does not say which syntax made the import.

Downsides

  • A bundled require("./page.html").default was the page and is undefined now. Bun.serve skips such a route: GET / answers 404 with no error if a fetch handler or another route exists. Write import page from "./page.html" or (m => m.default ?? m)(require("./page.html")).
  • Unbundled, that .default is the page only if an import loaded the page first: bun run serves the page and this PR's bundle answers 404.
  • The metafile reports the real kind. A tool that matches html_manifest must change.
Notes

Two importers in two files, measured on linux x64. Static.js has import page from "./page.html". Lazy.js has import("./page.html"). Legacy.js (CommonJS) has exports.viaLegacy = require("./page.html"). Each entry imports Static.js and one of the other two, in the order shown. Each entry was built 60 times with bun build --target=bun from one unchanged tree, and each bundle was run once. A debug build logs [bundle] onParse(<index>, <path>) when the bundle thread takes a parse result, so the log gives the arrival order of the two importers.

entry imports main 91e7e3b, debug build this PR, debug build
Static.js, Lazy.js static first 46: works. lazy first 14: ReferenceError static first 42, lazy first 18. One bundle, works 60 of 60
Lazy.js, Static.js static first 42: works. lazy first 18: ReferenceError static first 45, lazy first 15. One bundle, works 60 of 60
Static.js, Legacy.js static first 40: works. legacy first 20: ReferenceError static first 29, legacy first 31. One bundle, works 60 of 60
Legacy.js, Static.js static first 42: works. legacy first 18: ReferenceError static first 30, legacy first 30. One bundle, works 60 of 60
  • On main the bundle follows the arrival order in 240 of 240 builds. Every build exits 0. Each entry has exactly two bundles by checksum.
  • With this PR both arrival orders still occur, and each entry has one bundle. It is byte-identical to the working bundle of main.
  • Release 1.4.3 shows the same split. Of 40 builds each, 29 and 11 fail for the two Lazy.js entries (bun build). With Bun.build and metafile: true, one importer reports html_manifest in each build, and the bundle fails exactly when that importer is Lazy.js or Legacy.js (240 of 240 builds).
  • The mechanism: on_parse_task_complete (bundle_v2.rs:7702 on main) runs once per parse result, in arrival order, and calls resolve_import_records for that file. The first record that misses path_to_source_index_map for the page created the manifest module and had its kind overwritten. Every later importer hit the early return and kept its kind.
  • The two static-import-and-*-in-two-files tests build the same tree five times. They expect each record to keep its own kind in the metafile, one bundle from all five builds, and the same manifest object from both importers. Without the fix they fail in every run on the kinds: the output shows html_manifest move between the two files within the five builds.
  • bun build --compile of the two-file trees also runs with this PR.
  • Loader of the manifest module: InputFile::default() gave it Loader::File. With --splitting and import(), the chunk's BuildArtifact.loader was "file" and chunk_gets_bytecode skipped it. With Loader::Js the artifact is kind: "chunk", loader: "js" (asserted in dynamic-import-with-splitting). require() with --splitting stays in the requiring chunk, like require() of a JSON file: require() splits only an ES module (require-with-splitting). On target: "node" the entry and the lazy chunk carry no // @bun pragma (dynamic-import-target-node).
  • type_check (bundle_v2.rs) hands the checker every input with a JS-like loader at a file path. The manifest module passed that filter once its loader was Js: with allowJs the checker resolved ./page.html to the empty module and reported TS2306: File 'page.html' is not a module. Without allowJs the *.html declaration still won. type_check now skips html_imports.server_source_indices. The new check test builds with allowJs and expects no diagnostics.

require("./page.html").default, measured on linux x64. A CommonJS server gives Bun.serve one route, "/", and a fetch handler that answers 404. The value of the route is one of three expressions. Each server runs unbundled, then as a bun build --target=bun bundle, and gets one GET / on the loopback. The binaries are release 1.4.3 and a debug build of this PR merged with main 449877f. Unbundled, the two binaries answer the same.

The require() is the first load of the page:

the value of the route unbundled bundled, 1.4.3 bundled, this PR
require("./page.html") 200, the page Bun.serve throws: 'routes' expects a Record<string, Response | HTMLBundle | ...> 200, the page
require("./page.html").default 404 200, the page 404
(m => m.default ?? m)(require("./page.html")) 200, the page 200, the page 200, the page

An import loaded the page first. The server file requires pages.mjs, which has import home from "./page.html", and then requires the page:

the value of the route unbundled bundled, 1.4.3 bundled, this PR
require("./page.html") Bun.serve throws, as above Bun.serve throws, as above 200, the page
require("./page.html").default 200, the page 200, the page 404
(m => m.default ?? m)(require("./page.html")) 200, the page 200, the page 200, the page
  • Unbundled, the value of require("./page.html") depends on the load order. As the first load of the page it is the HTMLBundle, which has no default. After an import of the page it is the module namespace, with the keys __esModule and default. require() of a .json file has the same two shapes unbundled: for {"a":1} the keys are a as the first load, and a, default after an import.
  • On 1.4.3 only the first record to reach the page has the overwritten kind. The bundle links that require() like a static import, so it evaluates to a namespace with default: (init_page(), exports_page), or __toESM(require_page()) if the bundle has a second require() of the page. That second require() gives the bare manifest. With the kind intact the manifest module is module.exports = manifest, and each require() gives the manifest in both load orders, as require() of a .json file does in a bundle.
  • So this PR's bundle answers as the unbundled program in each row of the first table, and differs from it in two rows of the second. The middle row of the second table is the one program here that serves the page unbundled and from a 1.4.3 bundle, and not from this PR's bundle.
  • import page from "./page.html" in an ES module, and the third row in a CommonJS file, serve the page in each column of both tables.
  • The 404 has no error because Bun.serve skips a route whose value is undefined (src/runtime/server/ServerConfig.rs:727, since Fix disabling HMR, add way to do /api/* and /* in static  #17333). If that route is the only one and the server has no fetch handler, Bun.serve throws at startup: Bun.serve() needs either: .... With another route and no fetch handler, GET / answers 404 with an empty body.
  • This PR does not keep the old form alive and does not add an error. Three GitHub code searches ("html\").default" "Bun.serve", the same with single quotes, and "require(\"./index.html\").default") found no Bun server that reads .default from require() of an HTML file. A default on the bundled manifest would be a property that the unbundled HTMLBundle and its type do not have, and console.log prints a non-enumerable own property (default: [Circular]). An error from Bun.serve for an undefined route would reverse the explicit skip from Fix disabling HMR, add way to do /api/* and /* in static  #17333.

Other facts.

  • Deleted: ImportKind::HtmlManifest (discriminant 10), its arms in label and error_label (src/ast/lib.rs), in import_kind_label and the resolve error code table (src/jsc/ResolveMessage.rs), and in the async dependency walk (src/bundler/LinkerGraph.rs). Internal keeps discriminant 11. No JS table, type declaration or doc listed html_manifest.
  • The metafile kind html_manifest was never in the declared ImportKind type, and only the first importer reported it.
  • Static-only builds: diff -r of the output directories of 1.4.3 and of this PR shows no difference for one entry, and for two entries with --splitting that share the manifest chunk. Two pages imported by two sibling files gave one set of output files in 30 builds for each of six variants (static, import(), mixed, each with and without --splitting).
  • A limit that stays: with --splitting, the manifest chunk keeps its [hash] name when the manifest changes, as shared chunks do on main. import() of a page did not work on main, so no working program gets worse. Reproduced: build await import("./page.html") with --splitting, change the page's script, build again. The manifest chunk is page-jzenhyes.js both times and its bytes differ. On main the same happens to the chunk that two entry points share (a-6f21sh1s.js). final_chunk_hashes (LinkerContext.rs:2654) does nothing for HtmlImport pieces (line 2707), so the spliced manifest does not reach the hash. That is a separate change.
  • splitting-shared-manifest-chunk failed on 1.4.0 and passes on main since bundler: list the chunks and assets reached through dynamic imports in the HTML import manifest #38347 changed how the manifest picks chunks. It stays as a guard. dynamic-import-with-splitting needs the target line: without it the manifest chunk is emitted as browser output (no // @bun pragma).
  • bundler: resolve files imports through the shared steps (HTML import manifest, url assets) #38605 adds the same kind = HtmlManifest assignment on the plugin and files: resolution paths. It must drop that assignment: the variant no longer exists. bundler: give the browser side of a server build its own copy of the runtime #38376 carries the same one-line target fix. The hunks are identical.
  • Suites run on this PR merged with main 449877f (debug build): html-import-manifest 20 pass, bundler_html_server 4, bun-serve-html-manifest 6, bundler_html 26, metafile 65, resolve-error 32, bundler_plugin 68. On release 1.4.3, 9 of the 10 new tests in html-import-manifest fail. The tenth, splitting-shared-manifest-chunk, is the guard named above.

[human-review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/html-import-manifest.test.ts
bun test v1.4.3 (f42e98025)

test/bundler/html-import-manifest.test.ts:
(pass) bundler > html-import/multiple-manifests [313.26ms]
(pass) bundler > html-import/html-referenced-assets-in-manifest [571.01ms]
(pass) bundler > html-import/manifest-with-metadata [1139.44ms]
(pass) bundler > html-import/splitting-lists-assets-of-lazy-chunks [196.16ms]
(pass) bundler > html-import/splitting-attributes-lazy-chunks-to-their-page [165.32ms]
(pass) bundler > html-import/shared-css-chunk-is-in-every-manifest [153.50ms]
(pass) bundler > html-import/splitting-more-than-127-entry-points [944.48ms]
800 | 
801 |     await buildServer(dir, ["server.ts"], { splitting: true });
802 | 
803 |     // With splitting, the manifest module is loaded lazily from a chunk of its own.
804 |     const serverCode = readFileSync(join(dir, "out", "server.js"), "utf8");
805 |     const lazyChunk = serverCode.match(/import\("(\.\/[^"]+)"\)/)![1];
                                                                      ^
TypeError: nul
... (truncated)

release without fix: 4 FAILED
bun test v1.4.3-canary.1 (f42e98025)

test/bundler/html-import-manifest.test.ts:
(pass) bundler > html-import/multiple-manifests [19.25ms]
(pass) bundler > html-import/manifest-with-metadata [37.99ms]
(pass) bundler > html-import/html-referenced-assets-in-manifest [28.61ms]
(pass) bundler > html-import/splitting-lists-assets-of-lazy-chunks [24.37ms]
(pass) bundler > html-import/splitting-attributes-lazy-chunks-to-their-page [20.59ms]
(pass) bundler > html-import/shared-css-chunk-is-in-every-manifest [15.78ms]
(pass) bundler > html-import/splitting-more-than-127-entry-points [68.83ms]
800 | 
801 |     await buildServer(dir, ["server.ts"], { splitting: true });
802 | 
803 |     // With splitting, the manifest module is loaded lazily from a chunk of its own.
804 |     const serverCode = readFileSync(join(dir, "out", "server.js"), "utf8");
805 |     const lazyChunk = serverCode.match(/import\("(\.\/[^"]+)"\)/)![1];
                                                                      ^
TypeError: null is not an object (evaluating 'readFileSync(join(dir, "out", "server.js"), "utf8").match(/import\("(\.\/[^"]+)"\)/)[1]')
      at <anonymous> (/workspace/bun/test/bundler/h
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/html-import-manifest.test.ts
bun test v1.4.3 (f42e98025)

test/bundler/html-import-manifest.test.ts:
(pass) bundler > html-import/multiple-manifests [293.19ms]
(pass) bundler > html-import/manifest-with-metadata [883.83ms]
(pass) bundler > html-import/html-referenced-assets-in-manifest [696.84ms]
(pass) bundler > html-import/splitting-lists-assets-of-lazy-chunks [216.28ms]
(pass) bundler > html-import/splitting-attributes-lazy-chunks-to-their-page [162.41ms]
(pass) bundler > html-import/shared-css-chunk-is-in-every-manifest [124.80ms]
(pass) bundler > html-import/splitting-more-than-127-entry-points [822.85ms]
(pass) bundler > html-import/etag-changes-with-referenced-chunks [350.66ms]
(pass) bundler > html-import/source-map-columns-after-manifest [332.63ms]
(pass) bundler > html-import/dynamic-import-with-splitting [402.51ms]
(pass) bundler > html-import/dynamic-import [540.02ms]
(pass) bundler > html-import/with-type-file-attribute [628.99ms]
(pass) bundler > html-import/require-from-esm [330.88ms]
(pass) bundler > html-im
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 814ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/4] cargo bun_runtime → libbun_runtime.a
^[[1m^[[92m   Compiling^[[0m bun_js_printer v0.0.0 (/workspace/bun/src/js_printer)
^[[1m^[[92m   Compiling^[[0m bun_bundler v0.0.0 (/workspace/bun/src/bundler)
^[[1m^[[92m   Compiling^[[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph)
^[[1m^[[92m   Compiling^[[0m bun_transpiler v0.0.0 (/workspace/bun/src/transpiler)
^[[1m^[[92m   Compiling^[[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig)
^[[1m^[[92m   Compiling^[[0m bun_install v0.0.0 (/workspace/bun/src/install)
^[[1m^[[92m   Compiling^[[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc)
^[[1m^[[92m   Compiling^[[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc)
^[[1m^[[92m   Compiling^[[0m bun_sql_jsc v0.0.0 (/workspace/bun/src/sql_jsc)
^[[1m^[[92m   Compiling^[[0m bun_css_jsc v0.0.0 (/workspace/bun/src/css_jsc)
^[[1m^[[92m   Compiling^[[0m bun_sys_jsc v0.0.0 (/workspace/bun/src/sys_jsc)
^[[1m^[[92m   Compiling^[[0m bun_ast_jsc v0.0.0 (/workspace/bun/src/ast_jsc)
^[[1m^[[92m   Compiling^[[0m bun_patch_jsc v0.0.0 (/workspa
... (truncated)
diff hotspot
src/bundler/bundle_v2.rs                  |  10 +--
 test/bundler/html-import-manifest.test.ts | 130 +++++++++++++++++++++++++++++-
 2 files changed, 133 insertions(+), 7 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                       reads  edits  tests
src/bundler/bundle_v2.rs                       0      0      1
test/bundler/html-import-manifest.test.ts      0      0      1

… and require() of the manifest link

resolve_import_records() overwrote the importing record's kind with
ImportKind::HtmlManifest when binding a server-side HTML import to its
generated manifest module. The linker and printer decide how to emit an
import from that kind: import() and require() records get the imported
module wrapped (or, with splitting, turned into a chunk of its own) and
are printed as a call to that wrapper. With the kind replaced, the
manifest module was never wrapped, and import("./page.html") or
require("./page.html") printed a call to an undefined require_page().
Only the record that created the manifest module was rewritten, so the
result also depended on which importer was resolved first.

The kind is now left alone; the only thing that read the rewritten kind
was the known_target of the HTML file's parse task, which uses the same
predicate directly.

The manifest module's AST also gets the server target it is evaluated
under. It defaulted to browser, so with splitting enabled the chunk it
lands in (a chunk of its own for import(), or a chunk shared between
server entry points for static imports) was flagged as a browser chunk
of the server build and listed in the manifest as one of the page's
files.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: bd924a37-956b-417e-b985-e9d43d9b4a97
📥 Commits

Reviewing files that changed from the base of the PR and between 91e7e3b and 1837152.

📒 Files selected for processing: 5
  • src/ast/lib.rs
  • src/bundler/LinkerGraph.rs
  • src/bundler/bundle_v2.rs
  • src/jsc/ResolveMessage.rs
  • test/bundler/html-import-manifest.test.ts
💤 Files with no reviewable changes: 1
  • src/ast/lib.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

HTML entry-point imports retain their original import kinds. Target selection now uses the HTML entry-point condition, and generated HTML ASTs receive the importing target. HTML manifest records also proceed through async-dependency traversal. New tests cover runtime imports, code splitting, and repeated builds.

Changes

HTML Manifest Imports

Layer / File(s) Summary
Import kinds and HTML targets
src/ast/lib.rs, src/jsc/ResolveMessage.rs, src/bundler/bundle_v2.rs, src/bundler/LinkerGraph.rs
The HtmlManifest import kind and its label mappings are removed. HTML entry-point imports retain their kinds. Target selection uses the HTML entry-point condition, generated HTML ASTs receive the importing target, and manifest records proceed through async-dependency traversal.
Manifest bundling and runtime tests
test/bundler/html-import-manifest.test.ts
Tests cover dynamic imports and require(), shared manifest chunks, browser asset lists, retained import kinds, repeated-build output, and manifest shape.

Suggested reviewers: jarred-sumner

Priority: Normal

Merge Risk: Minimal · up to 18371

No PR-introduced issue requiring a fix before merge was established.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly describes the main change: preserving the import kind for server-side HTML imports so import() and require() link the manifest correctly.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is thorough and covers both template sections. "What does this PR do?" is answered under Problem, Fix, Background, and Downsides: it explains that a server-side HTML import recorded th…
Full details: Description check

Explanation

The description is thorough and covers both template sections. "What does this PR do?" is answered under Problem, Fix, Background, and Downsides: it explains that a server-side HTML import recorded the wrong ImportKind, causing a missing require_page reference and bundle output that depended on parse arrival order. The fix removes ImportKind::HtmlManifest, keeps each record's real kind, and sets the manifest module to the server target with the js loader. "How did you verify your code works?" is answered with the named test file (10 new tests, 9 failing without the fix), related suites, and a measured table across 240 builds per arrival order. Downsides and related PRs are also covered. Minor gaps: the test counts in the description (10 new tests, 17 total) differ from the objectives (which cite 15 and 17), and the 'Verified' bullet does not name a platform beyond linux x64 in the notes. Neither gap blocks merge.

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on bun 1.4.3 and on main (91e7e3b) in two ways.

  1. One importer. A --target=bun or --target=node build of a file that does await import("./page.html") or require("./page.html") emits a call to require_page(), which nothing defines. Every build fails at load.
  2. Two importers in two files. One file has import page from "./page.html", the other has import("./page.html") or require("./page.html"). One unchanged source tree gives two different bundles. The bundle fails when the parse result of the import() or require() file arrives first: 70 of 240 builds on a debug build of main. With this PR both arrival orders still occur (seen in the bundler's onParse debug log), each tree gives one bundle, and 240 of 240 builds work.

test/bundler/html-import-manifest.test.ts: 9 of the 10 new tests fail on release 1.4.3, and all 20 tests pass with this PR merged with main 449877f (debug build). The PR body has the numbers under Notes.

One program that works on 1.4.3 stops with this PR. A CommonJS server has routes: { "/": require("./page.html").default } and a fetch handler, and is built with bun build --target=bun. GET / is the page from a 1.4.3 bundle and a 404 from a bundle of this PR, with no error. Unbundled, that server answers 404 on both binaries, unless an import loaded the page before the require(). import page from "./page.html" and (m => m.default ?? m)(require("./page.html")) serve the page in every case measured. Downsides in the PR body names the change, and Notes has the two tables.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this and didn't find any bugs. The fix is small and well-reasoned, but it changes load-bearing bundler linking behavior (import-record kind drives wrapping/chunking decisions) and has a user-observable side effect on metafile kind reporting, plus noted interactions with #38605 and #38347 — a maintainer familiar with the bundler should sign off.

Checked that the known_target rewrite from import_record.kind == HtmlManifest to is_html_entrypoint is equivalent (the removed assignment was the only setter, 20 lines above). Checked remaining HtmlManifest consumers: LinkerGraph.rs:947 (TLA propagation) now falls through for static-import manifest records, which is harmless — the manifest is a lazy-export with no TLA, same as a JSON import. Verified bunRun returns {stdout, stderr, exitCode} with trimmed strings, so the two-line stdout.split("\n") in the dynamic-import test is sound.

Extended reasoning...

Overview

Two changes in src/bundler/bundle_v2.rs: (1) stop overwriting import_record.kind with ImportKind::HtmlManifest in resolve_import_records() and use the local is_html_entrypoint predicate directly for known_target; (2) set the generated manifest module's AST target to the importing server target in generate_server_html_module() instead of leaving it at the Browser default. Five new tests in test/bundler/html-import-manifest.test.ts cover import(), import() + splitting, require() from ESM and CJS, and a shared manifest chunk under splitting.

Security risks

None. This is bundler output-shape logic; no untrusted input parsing, auth, or crypto is touched.

Level of scrutiny

High. The line count is tiny (~8 native lines), but import_record.kind is load-bearing across the linker (scanImportsAndExports.rs), chunk computation (bundle_v2.rs:1852), and printer (js_printer/lib.rs) as the PR description itself lays out. The change also alters user-observable metafile output (html_manifest → import-statement/dynamic-import/require-call). I audited every remaining reader of ImportKind::HtmlManifest — only LinkerGraph.rs:947 (TLA-propagation continue) is behaviorally affected, and the fallthrough for a static-import record just visits a lazy-export module with no async dependency, matching how JSON imports are already handled. The known_target rewrite is trivially equivalent since the deleted assignment was its only setter.

Other factors

The tests are well-constructed: they run the bundled output with bunRun, assert stderr and manifest shape before exitCode, use tempDir with await using, and pin the metafile kind. The PR description explicitly flags interactions with two open PRs (#38605 would reintroduce the same assignment on other paths; #38347 overlaps the splitting symptom) — a maintainer should coordinate the landing order. Given the subsystem, the observable metafile change, and the cross-PR coordination, this warrants a human sign-off despite finding no defects.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. bundler: give the browser side of a server build its own copy of the runtime #38376 - Contains the same ast_for_html_entrypoint.target = target; fix in generate_server_html_module byte-for-byte at the identical location, for the same stated reason.
  2. bundler: resolve files imports through the shared steps (HTML import manifest, url assets) #38605 - Rewrites the same generate_server_html_module function and factors out the same is_html_entrypoint predicate, but goes the opposite direction on the bug by adding two more import_record.kind = ImportKind::HtmlManifest assignments that this PR removes.
  3. bundler: list the chunks and assets reached through dynamic imports in the HTML import manifest #38347 - Replaces the manifest chunk-selection logic to address the same "wrong chunk listed in the manifest under splitting: true" symptom, via a different mechanism.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of any of the three; checked each:

The main change in this PR (not rewriting the record kind, which is what the require_page is not defined failures come from) is in none of them.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. The Rust change is small and the reasoning is thorough, but since it changes how HTML-manifest import records flow through the linker (with a user-visible metafile kind change) and overlaps with three open PRs that take different directions on the same code, a human look would still be worthwhile.

Checked: is_html_entrypoint is computed just above and is equivalent to the removed kind == HtmlManifest check; remaining ImportKind::HtmlManifest readers (LinkerGraph.rs:947 async-propagation walk, ResolveMessage.rs/ast/lib.rs label tables) are unaffected — the Stmt arm now visits the manifest module like any JSON import, which is harmless. No test in the tree asserts on "html_manifest" as a metafile kind. The new tests use bunRun/tempDir correctly and expectPageManifest asserts the exact object shape.

Extended reasoning...

Overview

The PR touches two files: a ~6-line net change in src/bundler/bundle_v2.rs (stop rewriting import_record.kind to ImportKind::HtmlManifest in resolve_import_records(), use the is_html_entrypoint local for known_target instead, and set the generated manifest module's AST target to the importing server target in generate_server_html_module()), plus five new tests in test/bundler/html-import-manifest.test.ts covering import(), require() (ESM/CJS), and splitting variants.

Security risks

None. This is bundler output-shape logic; no auth, crypto, untrusted-input parsing, or filesystem-escape surface is touched.

Level of scrutiny

Medium-high. The diff itself is tiny and mechanically sound — I traced is_html_entrypoint (defined at bundle_v2.rs:6548) and confirmed it's exactly the predicate the removed assignment encoded, and I audited every remaining reader of ImportKind::HtmlManifest (the async-dependency walk at LinkerGraph.rs:947 now falls through to Stmt for static imports, which just visits a synchronous lazy-export module — same as a JSON import — and still skips for Dynamic/Require; the other hits are label tables). But the change sits inside the bundler's link/print decision path, has a documented user-visible side effect (metafile kind for HTML imports changes from html_manifest to the real syntax kind), and the PR description itself notes it conflicts directionally with #38605 and overlaps with #38376/#38347/#38549. That coordination is a maintainer call.

Other factors

The PR description is unusually detailed with a proven mechanism (not just "crash goes away"), the five new tests each pin a distinct failure mode (wrapper-not-defined, chunk-not-emitted, wrong result shape, server chunk leaking into manifest), and existing tests in the same file are unchanged. bunRun trims stdout so stdout.split("\n") in the dynamic-import test yields the expected two lines. The duplicate-PR bot flagged three overlapping PRs, one of which (#38605) adds the very assignment this PR removes — a human should decide which direction wins before either lands.

@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:58 PM PT - Oct 21st, 2026

✅ @robobun, your commit 66782cc7757cbd537ed3e6e9f1dbd97468542c8e passed in Build #130184! 🎉


🧪   To try this PR locally:

bunx bun-pr 38621

That installs a local version of the PR into your bun-38621 executable, so you can run:

bun-38621 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked the one remaining reader of ImportKind::HtmlManifest, the continue in the async-dependency walk at src/bundler/LinkerGraph.rs:973: with the record now keeping Stmt, the walk descends into the manifest module, which has no import records and is not async, so nothing changes there.

Extended reasoning...

The change removes the kind overwrite in the import-record resolution loop of src/bundler/bundle_v2.rs, keys known_target off is_html_entrypoint, and sets the manifest module's AST target to the server target; the test file adds dynamic-import, splitting, and require (ESM/CJS) cases. No security-sensitive surface is touched. The LinkerGraph skip on the now-unset kind was traced and found to be behavior-neutral for the manifest module.

Comment thread src/bundler/bundle_v2.rs Resolved
Comment thread src/bundler/bundle_v2.rs Resolved
…import() or require()

The two importing files are parsed in parallel. The file whose parse
result arrives first creates the manifest module. The test builds the
same sources five times. It expects each import record to keep its own
kind, one bundle from all five builds, and the manifest from both
importers.
Nothing constructs the variant now that an import record of an HTML
file keeps its own kind. Delete the variant and its match arms: the two
label tables in src/ast/lib.rs, the label and error code tables in
src/jsc/ResolveMessage.rs, and the async dependency walk in
src/bundler/LinkerGraph.rs. `Internal` keeps discriminant 11.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked the gap the variant deletion leaves at discriminant 10 (src/ast/lib.rs:73): enum_map::Enum indexes by ordinal, nothing in src/ converts a u8 back into ImportKind, and the remaining discriminants are unchanged, so the kind as u8 handed to the plugin label table is unaffected by this change. The known_target rewrite at src/bundler/bundle_v2.rs:7243 is equivalent to the old check (the removed assignment was gated on the same is_html_entrypoint), and the removed arm in ResolveMessage::get_code was unreachable since the kind was only assigned after a successful resolve. The variant deletion from the earlier inline note is now in the diff.

Extended reasoning...

The change removes ImportKind::HtmlManifest and stops overwriting the import record kind for server-side HTML imports in resolve_import_records, sets the generated manifest module's AST target to the importing server target, and adds seven tests exercising import()/require() of an HTML page with and without splitting plus a five-build determinism check. It touches no security-sensitive surface. Inline findings already exist, so this body only records what else was examined and ruled out.

Comment thread src/bundler/bundle_v2.rs Resolved
Comment thread src/ast/lib.rs Resolved
With splitting, the manifest module is the entry point of its own chunk.
The chunk's reported loader and its bytecode decision read the entry
point's input loader, which defaulted to file.
Comment thread src/bundler/bundle_v2.rs Resolved
Comment thread src/bundler/bundle_v2.rs Resolved
The manifest module has the page's path and no text. With the js
loader it met the type checker's source filter, and with allowJs the
checker took it for the page and reported TS2306 on the import.

Match the split chunk artifact by basename in the test, as the path
has backslashes on Windows.
Comment thread src/bundler/bundle_v2.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

The latest commits address both issues from my previous review (the split-chunk artifact lookup now matches by basename, and --check excludes the manifest module via server_source_indices). Beyond the inline finding, I also checked that server_source_indices is filled before both type_check call sites (each runs after wait_for_parse) and holds indices into input_files, so the new is_manifest lookup is in-bounds; and that no remaining Rust, JS-table, or codegen reference to HtmlManifest/html_manifest exists after the variant removal, with Internal keeping discriminant 11.

Extended reasoning...

The change removes ImportKind::HtmlManifest and its match arms, keeps the real import kind for server-side HTML imports in bundle_v2.rs, sets the manifest AST target and Loader::Js, and excludes manifest modules from the --check sources; it touches no security-sensitive surface. The one inline finding is a pre-existing parse-order collision between manifest and with { type: "file" } imports of the same page, not introduced here, so this note only records what else was examined.

Comment thread src/bundler/bundle_v2.rs Resolved

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants