Conversation
|
Warning Review limit reached
Next review available in: 27 minutes 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 (6)
Comment |
|
Updated 9:57 AM PT - Aug 14th, 2026
✅ @robobun, your commit 53210a0823f3a7cbda6bce1cabebf3e41cad60d0 passed in 🧪 To try this PR locally: bunx bun-pr 38275That installs a local version of the PR into your bun-38275 --bun |
|
Status: ready for review (head 53210a0). Reproduced on release 1.4.0 and current main with With this branch all of those emit the attribute form's output. Changes since the first push, all from review: resolved loaders are no longer stored on import records (they leaked onto CI for this head: every lane that has run is green (177 jobs); the only things outstanding are the two |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes bundler output semantics (records that were previously bundled as modules are now left external) and adds a second argument to every external require()/import() printed for the bun target, a human look would still be worthwhile.
What was reviewed:
- Traced
resolve_import_recordsto confirmimport_record.pathstill holds the original specifier (not the resolved absolute path) at the newcontinue, and that user---externaland dev-server paths branch off before the loader is assigned. - Verified the
import_attribute_typerefactor is byte-identical to both replaced match tables, including theFP::Jsonspecial case. - Confirmed the new printer branches sit after the
record.source_index.is_valid()early return, so bundled records never reach them; only attribute-set or newly-externalized sqlite records carry a loader there. - Checked
__toESM(...)wrapping andws!minified output against the test assertions.
Extended reasoning...
Overview
This PR fixes bun build --target bun emitting the build machine's absolute path when the sqlite loader is selected by extension or loader map (rather than by with { type: "sqlite" }). Three files: src/bundler/bundle_v2.rs adds a post-resolution check that externalizes Loader::Sqlite records (mirroring the pre-resolution attribute check at line ~6087); src/js_printer/lib.rs extracts two duplicate loader→type-string match tables into a shared import_attribute_type helper and teaches the external require() and import() printer branches to emit the type argument (import statements already did this); test/bundler/bundler_bun.test.ts adds 9 tests covering the variant matrix.
Security risks
None identified. The change affects bundler output for a Bun-specific loader; no auth, crypto, or untrusted-input parsing is involved.
Level of scrutiny
High. resolve_import_records is the bundler's central import-resolution loop, and print_require_or_import_expr shapes every external require/import in bundled output. The bundle_v2 change alters which records enter the module graph — a semantic shift, even though it aligns loader-map behavior with the documented attribute behavior. The printer change is not sqlite-specific: any external record with record.loader set (attribute-carrying imports that were also matched by --external, for instance) now gets a second argument on require()/import() under --target bun. I traced that such records are rare (bundled records return early at source_index.is_valid(), and post-resolution non-sqlite records always get a source index), and the emitted form is what Bun's runtime already accepts, but this is the kind of broad-reaching output change a maintainer should sign off on.
Other factors
The change is well-executed: the new check mirrors the existing attribute case with tighter guards (target.is_bun(), dev_server.is_none(), ImportKind::Stmt|Require|Dynamic), the refactor deduplicates two byte-identical tables per the repo's own guidance, and test coverage hits import/require/dynamic-import × esm/cjs × minified, tree-shaking of unused imports, all three loader-map surfaces (CLI flag, bunfig, Bun.build), and the --target node error path. The build-time vs runtime database trick makes the tests prove which file the bundle actually opens. The PR description lists an extensive set of adjacent bundler suites that were run. Nothing here looks wrong to me — deferring purely because the blast radius (bundler resolution + printer output for all bun-target externals) warrants a maintainer's eyes.
…extension or loader map
A file whose loader is selected as "sqlite" by the loader map (--loader
.db:sqlite, bunfig [loader], Bun.build({ loader })) or by a .sqlite
extension was bundled as a module that called import.meta.require() with
the absolute path of the database on the build machine. The import
attribute form of the same loader keeps the import external instead.
Externalize the record in resolve_import_records for that case as well,
and have the printer re-attach the type attribute on external require()
and import() calls (it already did so for import statements), so cjs
output and the require()/import() forms load the file with the sqlite
loader regardless of its extension.
…rinted back
resolve_import_records stored the loader it picked for every resolved
file on the import record. The printer re-emits a record's loader as a
type attribute for target bun, so the two places where the linker later
turns such a record external (dynamic imports rewritten to chunks under
--splitting, and imports redirected through a `module.exports =
require(x)` shim to an external x) printed the bundled file's loader on
an import of something else. For import statements this was already
happening (`import * as x from "ext" with { type: "js" }`); with the
previous commit it would also have reached require() and import().
The record now only carries a loader set by an import attribute or by
the bundler when it deliberately leaves the import to the runtime; the
loader of a bundled file stays with the input file, which is the only
thing the rest of the bundler reads. The sqlite externalization moves
into one helper and also covers the plugin fallback resolver and
in-memory `files`, which pick loaders on their own paths.
c917ccd to
6aadb2d
Compare
There was a problem hiding this comment.
Beyond the inline findings, also checked: the two removed import_record.loader = Some(...) writes — remaining reads in src/bundler/ either unwrap_or_else to the path-based loader (6204/6566/2265/2499) or are on ParseTask/result objects, so no consumer loses the loader; the HTML-entrypoint guard at 6491 already treats None as HTML. An export ... from './x.sqlite' with the loader-map case was also examined and does not lose the type attribute in esm output.
Extended reasoning...
The two inline findings are a nit (onLoad bypass now matches the attribute form) and a pre-existing sibling under --splitting; neither is introduced by this PR. I separately grepped src/bundler/ for reads of import_record.loader to check the description's claim that nothing depended on the removed writes: all remaining reads fall back to path.loader(&options.loaders) when unset, and metafile/linker/input-file loader come from the parse result, not the record. This is still a semantic redefinition of a shared field across four resolution paths plus new printer output for external require()/import(), so a human pass on the bundler side is appropriate.
… externalizing the import An onLoad plugin whose filter matches the resolved file used to get the file through the parse task; keep that ahead of the sqlite externalization, as for any other loader. The decision moves into a method that checks the plugin filters, and marking the record is a separate helper.
…te import from being externalized Native plugins keep their filters in a separate list that the onLoad check does not see. Expose a match over that list and consult it too, so the file still reaches the parse task that runs the plugin.
There was a problem hiding this comment.
I reviewed the latest push (53210a0) and didn't find any bugs — the onBeforeParse gap from my last comment is addressed with a per-filter match (JSBundlerPlugin__anyOnBeforeParseMatches) and covered by two new tests in native-plugin.test.ts. Given the scope — redefining what ImportRecord::loader may hold, edits on four resolution paths in bundle_v2.rs, new C++ FFI, and overlap with #35692's approach to the shim-redirect leak — a human look is still worthwhile.
What was checked this run:
NativePluginListsharesfileNamespace/namespaces/group()withNamespaceList, so the templatedanyMatchesForNamespaceinstantiates;FilterRegExp::matchalready locks per call, same thread pattern as the existinghas_any_matches.filter_inputallocates freshBunStrings per call, so the two consecutive plugin checks each transfer their own copies.import_attribute_typeoutput is byte-identical to the two loader→string tables it replaces.- The outstanding comment-cop note on
bundle_v2.rs:5982is a two-line doc comment, not a workaround.
Extended reasoning...
Overview
The PR makes bundler-selected sqlite imports (via .sqlite extension, --loader, bunfig, or Bun.build({ loader })) print as external with { type: "sqlite" } imports instead of bundling a module holding the build machine's absolute path. It touches src/ast/import_record.rs (doc comment tightening the loader field's contract), src/bundler/bundle_v2.rs (new leaves_sqlite_import_to_runtime/mark_sqlite_import_external called on all four resolution paths; removes the two writes that stored a bundled file's resolved loader on the record), src/js_printer/lib.rs (adds { type } / { with: { type } } to external require()/import(); consolidates two loader→attribute tables into one import_attribute_type), src/jsc/bindings/JSBundlerPlugin.cpp (templated anyMatchesForNamespace + new JSBundlerPlugin__anyOnBeforeParseMatches FFI), and ~330 lines of new tests across bundler_bun.test.ts and native-plugin.test.ts.
Since my last inline comment at 11:14 UTC, commit 53210a0 added the native-plugin filter check I asked for — the tighter per-filter variant rather than the coarse has_on_before_parse_plugins() fallback — plus a shared filter_input helper on the Rust side and two tests exercising both the match and no-match cases.
Security risks
None identified. No auth, crypto, or untrusted-input parsing is touched. The new FFI (JSBundlerPlugin__anyOnBeforeParseMatches) reads plugin filter lists that are populated at setup time and follows the same locking pattern as the existing JSBundlerPlugin__anyMatches.
Level of scrutiny
High. This is bundler-core: it changes what ImportRecord::loader is allowed to hold (from "whatever loader was resolved" to "only a loader the runtime must apply to the printed import"), which is an invariant every future writer of that field must respect. The four resolution-path edits in bundle_v2.rs are structurally similar but not mechanical, and the printer now emits attributes on require()/import() where it previously did not. The change is well-argued and thoroughly tested (15 new cases, 11 fail on the released binary), but it is not the kind of simple/obvious change auto-approval is meant for.
Other factors
- The PR description notes #35692 fixes the shim-redirect symptom by copying the loader at the redirect, and says the two approaches are compatible. A maintainer may still want to decide whether both should land or whether this PR's at-source fix supersedes #35692.
- The one remaining unresolved bot comment (comment-cop on
bundle_v2.rs:5982) targets a two-line doc comment onleaves_sqlite_import_to_runtime; the author already pushed back on identical notes and I don't consider it blocking. - All three of my earlier inline concerns (onLoad precedence, the pre-existing user-options
--splittingsibling, and onBeforeParse precedence) have been addressed or explicitly deferred with a note in the description.
|
One more symptom that the bun -e 'const {Database}=require("bun:sqlite"); const db=new Database("my.db"); db.run("create table t(x)"); db.run("insert into t values (1)")'
printf 'import db from "./my.db" with { type: "sqlite" };\nconsole.log(db.query("select x from t").get());\n' > index.ts
bun build --target=bun --format=cjs ./index.ts --outfile out.cjs && bun out.cjsOn main ( A note on coverage: |
Problem
bun build --target bunwith the sqlite loader selected by extension or loader map (--loader .db:sqlite, bunfig[loader] ".db" = "sqlite",Bun.build({ loader: { ".db": "sqlite" } }), or importing a.sqlitefile without an attribute) emits the build machine's absolute path:--compileexecutable) opens that path wherever it runs, so it breaks once moved. The same import written asimport db from "./app.db" with { type: "sqlite" }is emitted unchanged and left external.resolve_import_records(src/bundler/bundle_v2.rs) only externalizes a record whose loader an attribute set, in a check that runs before resolution. A loader that comes from the file's extension is only known after resolution, so the database becomes a module in the graph and theLoader::Sqlitearm ofsrc/bundler/ParseTask.rsbuilds the module fromsource.path.text, which is absolute. The plugin fallback resolver (run_resolver, used when anonResolveplugin matched but returned nothing) and the in-memoryfilespath pick the loader separately and had the same gap.print_require_or_import_expr(src/js_printer/lib.rs) printed externalrequire()andimport()without the record's loader; only import statements gotwith { type }back. The cjs output format turns every import intorequire(), so even the attribute form printed__toESM(require("./app.db"))there, which loads a.dbfile as JavaScript.resolve_import_recordsstored the loader it resolved for every bundled file on the import record. Where the linker later turns such a record external, that loader was printed on an import of something else: an import redirected through amodule.exports = require(x)shim to an externalxprintedimport * as y from "x" with { type: "js" }(on main and 1.4.0;xresolving to a.tsfile at runtime then fails to parse), and with the require()/import() printing added here,--splittingwould have printedimport("./data-HASH.js", { with: { type: "json" } })on every cross-chunk dynamic import (found in review; a JS chunk then fails to parse as JSON).Fix
ImportRecord::loadernow means one thing: a loader the runtime must apply to the import as printed. It is set by the parser from atypeattribute (unchanged) and by the bundler when it leaves a sqlite import to the runtime;resolve_import_recordsno longer stores the loader it resolves for a bundled file there. Nothing else read that value: the linker, metafile, HTML output and dev server all take a bundled file's loader from the input file, and builds that go throughonResolveplugins already ran with it unset. This fixes the shim redirect leak and makes the--splittingchunk rewrite inert, without touching the linker.leaves_sqlite_import_to_runtime/mark_sqlite_import_external(bundle_v2.rs): after the resolved loader is known, aLoader::Sqliterecord from animportstatement,require()orimport()is flaggedIS_EXTERNAL_WITHOUT_SIDE_EFFECTS, givenloader = Sqlite, and skipped, exactly like the attribute case. It runs on all four resolution paths (disk andfiles, directly and via the plugin fallback). The record still holds the specifier as written, so the output reads./app.dbrelative to the bundle. Not applied when anonLoador nativeonBeforeParseplugin filter matches the resolved file (the plugin keeps receiving it, as for any other loader;src/jsc/bindings/JSBundlerPlugin.cppgainsJSBundlerPlugin__anyOnBeforeParseMatches, the existing filter matcher applied to the native plugin list, because theonLoadmatcher does not see those filters), for targets other than bun (the parse task still reportsTo use the "sqlite" loader, set target to "bun"), or in the dev server, which resolves imports itself and is unchanged.require()gets, { type: "x" }and an externalimport()without user options gets, { with: { type: "x" } }when the record carries a loader, for the bun platform only (the only consumer of these forms;import.meta.requireandimport()in Bun accept them, and the code this replaces already emitted therequireform). Import statements already did this; the three sites now share oneimport_attribute_typetable, replacing two copies with byte-identical output..db).sqlite_embeddedis untouched; unused imports are still dropped (the record is external without side effects, matching theNoSideEffectsPureDatathe bundled module used to get).__toESM(require("./db.sqlite", { type: "sqlite" }))), and a shim redirected to an external no longer prints the shim's loader. Output for records without an attribute is otherwise unchanged.onResolvereturned a path for, still goes through the parse task and prints the absolute path; there is no import specifier to keep in those cases.test/bundler/bundler_bun.test.ts. Thesqlite loader selected by extension or loader mapblock keeps a different database at build time and next to the bundle, so the program's output says which file the bundle opened:import(esm and cjs),require()(minified),import(), an unused import being dropped,--loader, bunfig[loader],Bun.build({ loader })alone and combined with a passthroughonResolveplugin and/orfiles, anonLoadplugin taking precedence, plus the--target nodeerror.test/bundler/native-plugin.test.tsgets two cases: a native plugin filtering.sqlitestill receives the file (its counter sees the contents), and one filtering.tsleaves the import external (fails on the released build with the absolute path). Thetype attributes on imports the linker makes externalblock pins the shim redirect (import statement,require(),import(), run against a TypeScript external) and--splittingcross-chunkimport()of.jsonand.tsfiles. 11 of the 15 new tests fail on the released binary (the sqlite ones with the absolute path, the redirect one withwith { type: "js" }); the file passes with the debug build.bundler_edgecase,bundler_plugin,bundler_plugin_chain,native-plugin,bundler_files,bundler_barrel,bundler_splitting,bundler_compile_splitting,bundler_cjs,bundler_cjs2esm,bundler_loader,bun-build-api,metafile,bundler_html,html-import-manifest,esbuild/{loader,default,importstar,splitting},bundler_compile -t sqlite,transpiler/transpiler.test.js,bake/dev/{bundle,plugins},test/js/bun/import-attributes;native-plugin(full file);cargo clippyonbun_ast,bun_js_printer,bun_bundler; clang-format on the C++ file. Manually confirmed a--compilebuild with--loader .db:sqliteopens the database next to the executable.Related
with { type: ... }on external hoisted frommodule.exports = require(...)#35692 fixes the shim redirect symptom for import statements by copying the loader at the redirect; this PR removes the stale loader at its source, which covers that case and the--splittingone, and the two are compatible.--splitting, a dynamic import the user wrote with an attribute (import("./x.json", { with: { type: "json" } })) keeps that attribute on the rewritten chunk import; that is the user's options object, a separate channel, and is tracked separately.import.meta.requirecall the parse task synthesizes for the embedded loader; this PR does not touch that arm.sqlite_embeddedselectable from the loader map; independent of this change.Background
--loader, bunfig[loader]andBun.build({ loader }).Path::loader()(src/resolver/lib.rs) consults it and then falls back toLoader::from_string(extension), which is why a.sqliteextension selects the sqlite loader with no configuration and.dbdoes not.sqlitevssqlite_embeddedloaders: both makeimport db from "./x"evaluate to abun:sqliteDatabase.sqliteopens the file at runtime and does not copy it;sqlite_embedded(with { type: "sqlite", embed: "true" }) copies it into the output directory or the compiled executable and references the copy.source_indexpoints at the bundled module once one exists; a record without one is printed back out as an import of itspath.IS_EXTERNAL_WITHOUT_SIDE_EFFECTSadditionally lets tree shaking drop it when nothing uses the binding. The linker repurposes records in two places: a cross-chunk dynamic import under--splittingis pointed at the chunk file, and an import of a one-linemodule.exports = require(x)shim is redirected tox.--target bunthe printer addswith { type: "x" }to import statements fromrecord.loader, so the runtime uses the loader the build decided on. The runtime accepts the same information onrequire(id, { type })andimport(id, { with: { type } }).resolve_import_recordshandles imports the bundler resolves itself; when anonResolveplugin's filter matches but the callback returns nothing,run_resolverdoes the same work after the round trip to the JS thread; both checkBun.build({ files })(in-memory files) before the disk resolver.Earlier version of this PR
The first push only added the externalization in
resolve_import_recordsand keyed the newrequire()/import()printing onrecord.loaderwhile resolution still stored every resolved loader there. Review found that--splittingoutput for target bun then carried{ with: { type: "json" } }on cross-chunk dynamic imports and failed at runtime, and that shim redirects leaked the shim's loader (already the case for import statements on main). The second commit changes what the field holds instead, moves the sqlite check into a helper that also covers the plugin fallback andfilespaths, and adds the redirect and splitting tests. Later commits make a matchingonLoadplugin, and then a matching nativeonBeforeParseplugin, take precedence over the externalization, which the first version skipped.no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/native-plugin.test.ts