Repository navigation
Conversation
… 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.
|
No actionable comments were generated in the recent review. ℹ️ Recent review info
WalkthroughHTML 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. ChangesHTML Manifest Imports
Suggested reviewers: Priority: Merge Risk:
|
|
Status: reproduced on bun 1.4.3 and on main (91e7e3b) in two ways.
One program that works on 1.4.3 stops with this PR. A CommonJS server has |
There was a problem hiding this comment.
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.
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
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 |
There was a problem hiding this comment.
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.
|
Updated 1:58 PM PT - Oct 21st, 2026
✅ @robobun, your commit 66782cc7757cbd537ed3e6e9f1dbd97468542c8e passed in 🧪 To try this PR locally: bunx bun-pr 38621That installs a local version of the PR into your bun-38621 --bun |
There was a problem hiding this comment.
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.
…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.
There was a problem hiding this comment.
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.
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.
…port-dynamic-require-wrap
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.
There was a problem hiding this comment.
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.
Problem
--target=bunornodebundle dies onimport("./page.html")orrequire("./page.html"):ReferenceError: require_page is not defined. In a CommonJS file,require()gives{ default: manifest }.resolve_import_records(src/bundler/bundle_v2.rs:7316) overwrote the record kind of the first importer to finish parsing withImportKind::HtmlManifest. The linker wraps a module only forDynamic/Requirerecords (scanImportsAndExports.rs:359-411).Fix
ImportKind::HtmlManifest.jsloader. Keep it out of--check..jsonfile: one tree gives one bundle.test/bundler/html-import-manifest.test.ts(10 new tests, 9 fail without the fix) andtest/cli/check/entry-points.test.ts. Alsobundler_html,bundler_html_server,metafile.Background
.htmlimport evaluates to a JSON manifest of the page's browser build. A generated "manifest module" holds it:export default, or a__commonJSwrapper forrequire()andimport().HtmlManifest: that kind does not say which syntax made the import.Downsides
require("./page.html").defaultwas the page and isundefinednow.Bun.serveskips such a route:GET /answers 404 with no error if afetchhandler or another route exists. Writeimport page from "./page.html"or(m => m.default ?? m)(require("./page.html"))..defaultis the page only if animportloaded the page first:bun runserves the page and this PR's bundle answers 404.html_manifestmust change.Notes
Two importers in two files, measured on linux x64.
Static.jshasimport page from "./page.html".Lazy.jshasimport("./page.html").Legacy.js(CommonJS) hasexports.viaLegacy = require("./page.html"). Each entry importsStatic.jsand one of the other two, in the order shown. Each entry was built 60 times withbun build --target=bunfrom 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.Static.js,Lazy.jsReferenceErrorLazy.js,Static.jsReferenceErrorStatic.js,Legacy.jsReferenceErrorLegacy.js,Static.jsReferenceErrorLazy.jsentries (bun build). WithBun.buildandmetafile: true, one importer reportshtml_manifestin each build, and the bundle fails exactly when that importer isLazy.jsorLegacy.js(240 of 240 builds).on_parse_task_complete(bundle_v2.rs:7702on main) runs once per parse result, in arrival order, and callsresolve_import_recordsfor that file. The first record that missespath_to_source_index_mapfor the page created the manifest module and had its kind overwritten. Every later importer hit the early return and kept its kind.static-import-and-*-in-two-filestests 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 showshtml_manifestmove between the two files within the five builds.bun build --compileof the two-file trees also runs with this PR.InputFile::default()gave itLoader::File. With--splittingandimport(), the chunk'sBuildArtifact.loaderwas"file"andchunk_gets_bytecodeskipped it. WithLoader::Jsthe artifact iskind: "chunk", loader: "js"(asserted indynamic-import-with-splitting).require()with--splittingstays in the requiring chunk, likerequire()of a JSON file:require()splits only an ES module (require-with-splitting). Ontarget: "node"the entry and the lazy chunk carry no// @bunpragma (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 wasJs: withallowJsthe checker resolved./page.htmlto the empty module and reportedTS2306: File 'page.html' is not a module. WithoutallowJsthe*.htmldeclaration still won.type_checknow skipshtml_imports.server_source_indices. The new check test builds withallowJsand expects no diagnostics.require("./page.html").default, measured on linux x64. A CommonJS server givesBun.serveone route,"/", and afetchhandler that answers 404. The value of the route is one of three expressions. Each server runs unbundled, then as abun build --target=bunbundle, and gets oneGET /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:require("./page.html")Bun.servethrows:'routes' expects a Record<string, Response | HTMLBundle | ...>require("./page.html").default(m => m.default ?? m)(require("./page.html"))An
importloaded the page first. The server file requirespages.mjs, which hasimport home from "./page.html", and then requires the page:require("./page.html")Bun.servethrows, as aboveBun.servethrows, as aboverequire("./page.html").default(m => m.default ?? m)(require("./page.html"))require("./page.html")depends on the load order. As the first load of the page it is theHTMLBundle, which has nodefault. After animportof the page it is the module namespace, with the keys__esModuleanddefault.require()of a.jsonfile has the same two shapes unbundled: for{"a":1}the keys areaas the first load, anda,defaultafter animport.require()like a static import, so it evaluates to a namespace withdefault:(init_page(), exports_page), or__toESM(require_page())if the bundle has a secondrequire()of the page. That secondrequire()gives the bare manifest. With the kind intact the manifest module ismodule.exports = manifest, and eachrequire()gives the manifest in both load orders, asrequire()of a.jsonfile does in a 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.Bun.serveskips a route whose value isundefined(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 nofetchhandler,Bun.servethrows at startup:Bun.serve() needs either: .... With another route and nofetchhandler,GET /answers 404 with an empty body."html\").default" "Bun.serve", the same with single quotes, and"require(\"./index.html\").default") found no Bun server that reads.defaultfromrequire()of an HTML file. Adefaulton the bundled manifest would be a property that the unbundledHTMLBundleand its type do not have, andconsole.logprints a non-enumerable own property (default: [Circular]). An error fromBun.servefor anundefinedroute would reverse the explicit skip from Fix disabling HMR, add way to do /api/* and /* in static #17333.Other facts.
ImportKind::HtmlManifest(discriminant 10), its arms inlabelanderror_label(src/ast/lib.rs), inimport_kind_labeland the resolve error code table (src/jsc/ResolveMessage.rs), and in the async dependency walk (src/bundler/LinkerGraph.rs).Internalkeeps discriminant 11. No JS table, type declaration or doc listedhtml_manifest.html_manifestwas never in the declaredImportKindtype, and only the first importer reported it.diff -rof the output directories of 1.4.3 and of this PR shows no difference for one entry, and for two entries with--splittingthat 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).--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: buildawait import("./page.html")with--splitting, change the page's script, build again. The manifest chunk ispage-jzenhyes.jsboth 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 forHtmlImportpieces (line 2707), so the spliced manifest does not reach the hash. That is a separate change.splitting-shared-manifest-chunkfailed 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-splittingneeds the target line: without it the manifest chunk is emitted as browser output (no// @bunpragma).filesimports through the shared steps (HTML import manifest, url assets) #38605 adds the samekind = HtmlManifestassignment on the plugin andfiles: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.html-import-manifest20 pass,bundler_html_server4,bun-serve-html-manifest6,bundler_html26,metafile65,resolve-error32,bundler_plugin68. On release 1.4.3, 9 of the 10 new tests inhtml-import-manifestfail. 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)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 0
evidence per changed file