bundler: create the HTML import manifest on the plugin and in-memory resolution paths - #38605
bundler: create the HTML import manifest on the plugin and in-memory resolution paths#38605robobun wants to merge 5 commits into
Conversation
…resolution paths A server-side import of an HTML file only became a manifest module plus a browser entry point when the import record was resolved by the bulk pass in resolve_import_records / process_resolve_queue. Records resolved one at a time (an onResolve plugin declining the import, an onResolve plugin returning the HTML file's path) and imports of in-memory `files:` entries bundled the HTML as an ordinary module of the server graph instead, so the import either failed with 'No matching export in "page.html" for import "default"' or, for require() and in-memory files, built into output that never had a manifest. Factor the decision into is_server_html_import, make generate_server_html_module return the manifest module's index, and add enqueue_server_html_import for the one-at-a-time paths: it creates the manifest module in the importing graph and parses the HTML file as a browser entry point unless the browser graph already has it.
|
Warning Review limit reached
Next review available in: 40 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Status: fix and tests pushed (latest aa0a705), waiting on CI. Reproduced on bun 1.4.0 with a Since the first push: the |
|
I was working on the One thing that may be worth folding in here, or doing as a follow-up: the Branch with an alternative shape of the |
|
Heads up from #38621, which fixes the This branch adds the same assignment to |
…disk
Bun.build({ files }) resolved an import that hits the in-memory file map in
a separate branch of resolve_import_records that duplicated the tail of the
disk resolution and had drifted from it. Two things it did not do:
- importing an .html file into a server build did not turn the import into
an HTML import manifest: the output kept a literal import of the virtual
path and the page's scripts were bundled for the server target
- a url asset referenced from an in-memory .html file (for example
<link rel="manifest" href="./manifest.json">) kept its parsed loader
instead of being copied to the output as a file
A file map hit now only substitutes for the resolver's result and then goes
through the shared steps, keeping the two things that are specific to
virtual files: the jsx options come from the transpiler (there is no
tsconfig.json to consult) and the display path is the map key as-is.
|
Updated 1:05 PM PT - Aug 14th, 2026
❌ @robobun, your commit 70af9ef has some failures in 🧪 To try this PR locally: bunx bun-pr 38605That installs a local version of the PR into your bun-38605 --bun |
…side generate_server_html_module The two call sites that reserve their map slot with get_or_put now fill it the same way their non-HTML siblings do, and the two that use get-then-put call put; generate_server_html_module only creates the module.
|
#38635 now carries the restructure mentioned above (the |
…nifest module The linker wraps a module (or gives it its own chunk) based on the Require / Dynamic kind of the records importing it, and the printer emits the import from the same kind, so replacing the kind with HtmlManifest is what breaks import() and require() of an HTML file. The manifest wiring does not depend on it: the module is found through html_imports.server_source_indices and the browser graph's path map. Do not introduce the rewrite on the paths added here; the existing one on the bulk path is removed separately.
|
Re the Re #38621: thanks, that matches what I saw when probing ( |
Problem
import page from "./page.html"in atarget: "bun"(ornode) build works, but the same build fails withNo matching export in "page.html" for import "default"as soon as any onResolve plugin matches the import, even one that returnsundefinedfor everything. A plugin that returns the path of an.htmlfile fails the same way.require("./page.html")on those paths builds without an error but evaluates to an empty module, and an HTML file supplied through thefiles:option is never turned into a manifest either: the output keeps a literalimport page from "/page.html" with { type: "html" }and the page's scripts are bundled for the server target.html_manifest) existed only on the bulk resolution path for files on disk, inresolve_import_recordsandprocess_resolve_queue(src/bundler/bundle_v2.rs). Every other place that binds a resolved import record to a module bundled the HTML file as an ordinary module of the server graph, which exports nothing:on_resolveNoMatch->run_resolver, both its disk branch and itsfiles:branch,on_resolveSuccess(plugin returned a path),files:branch ofresolve_import_records, a copy of the disk branch's tail that had drifted from it (it also skipped the loader override that copies url assets referenced from an HTML file, so an in-memory page with<link rel="manifest" href="./manifest.json">bundled the JSON instead of emitting the asset).Fix
is_server_html_importis the one definition of "this import binds to a manifest module" (HTML loader, server-side target, no dev server); the bulk path's two inline copies use it too.generate_server_html_modulenow only creates the manifest module and returns its source index. Each call site registers that index in its graph's path map and binds the record the same way it does for any other module (puton the get-then-put sites, the reservedget_or_putslot on the other two), so the record can live insidegraph.ast, as it does on the plugin paths.enqueue_server_html_importis the one-at-a-time counterpart of the bulk path, used byrun_resolver(both branches) andon_resolve'sSuccessarm: create the manifest module, and parse the HTML file as aTarget::Browserentry point unless the browser graph already has it (a second importer never gets here, it finds the manifest in the server graph's map; an HTML file that is also a user entry point is parsed once). The callers bind the record to it and leave the record's kind alone: rewriting it toHtmlManifest, as the bulk path currently does, is what breaksimport()andrequire()of HTML files (the linker wraps a module and the printer emits the import based on the record'sDynamic/Requirekind), and the manifest wiring does not depend on it. bundler: keep the import kind of server-side HTML imports so import() and require() of the manifest link #38621 removes that rewrite on the bulk path; this PR does not add it to the new ones. TheSuccessarm skips all of this forEntryPointBuildrecords, which are entry points rather than imports (bundler: bundle HTML entry points for the browser on every entry path of a server build #38594 handles those).resolve_import_records, afiles:hit now stands in for the resolver's result and goes through the shared tail instead of its own copy of it; the only virtual-file specifics left are thatjsxcomes from the transpiler (no tsconfig.json to consult) and that the display path is the map key as-is. This is what makes the in-memory HTML import a manifest and also fixes the url-asset case above. (Taken from a parallel attempt at thefiles:half of this bug, see the comment below.)process_html_import_files,HTMLImportManifest) needs the manifest module inhtml_imports.server_source_indicesand the HTML file under the same path in the browser graph's map, and nothing else; every path now produces exactly that. Building the same inputs with no plugin, with a declining plugin and with a path-returning plugin yields byte-identical outputs; the metafiles differ only in the import's kind label (html_manifeston the bulk path until bundler: keep the import kind of server-side HTML imports so import() and require() of the manifest link #38621 lands, the real kind on the new paths), andimport()of an HTML file already works on the new paths.test/bundler/html-import-manifest.test.ts(onresolve-fallthrough, -require, -returns-html-path, -html-is-also-an-entry-point, plus a guard thatwith { type: "file" }still wins on the fall-through path) andtest/bundler/bundler_files.test.ts("HTML imports": bulk path, importer on disk, one manifest per page however often it is imported, declining plugin, path-returning plugin, url asset; plus a guard for the jsx options of in-memory files). Everything except the two guards fails on bun 1.4.0 and passes with this branch. Existinghtml-import-manifest,bundler_files,bundler_html,bundler_html_server,bundler_plugin,css/doesnt_crash(the other user offiles:),bun-serve-html-manifest,bake/dev/htmlandbake/dev/pluginstests pass.Background
index,files[]with paths, loaders and headers);Bun.serveroutes consume it. Internally the import binds to a generated "manifest module" whose AST is a lazy export of a placeholder string that the linker replaces once output paths are known.BundleV2keeps one path -> source index map per target graph. For an HTML import, the server graph's map holds the manifest module and the browser graph's map holds the real HTML file under the same path;process_html_import_filesjoins the two by that path.resolve_import_records, thenprocess_resolve_queuecreates the parse tasks). A record that matches an onResolve filter is handed to the plugin instead and comes back later throughon_resolve, which either uses the plugin's answer (Success) or runs the regular resolver itself (NoMatch->run_resolver); both create the module for the record directly.files:(FileMap) is the in-memory file map ofBun.build({ files }), consulted before the resolver on both kinds of path.Probes and adjacent findings
Repro on 1.4.0 (
server.tsimports./page.html,target: "bun"):With this branch all of the above build, and comparing the three disk builds (no plugin, declining plugin, path-returning plugin) gives identical output files (one manifest per import, the page's script chunk compiled for the browser, i.e. without the
// @bunpragma); the metafiles differ only in the kind label described above.Related, pre-existing on the plugin-less path as well, and handled elsewhere: HTML entry points (not imports) bundled under the server target when a plugin or
files:is involved (#38594);files:entry points failing when a plugin declines them (#38600; thefiles:tests here use a filter that does not match the entry point for that reason);import("./page.html")from server code on the bulk path emitting a call to an undefinedrequire_pagewrapper (#38621, whose analysis is why the new paths here do not rewrite the kind); the import attribute loader being ignored when a plugin returns a path (#37476, which is also why theSuccessarm here still derives the loader from the extension); and the manifest'sindexreading././page.htmlunder an explicit[dir]/[name].[ext]entry naming, which is why the tests compare the basename ofindex.