Skip to content

bundler: keep a .sqlite import external without the type attribute - #45502

Draft
robobun wants to merge 2 commits into
mainfrom
robobun/926ebeab/sqlite-no-attribute-external
Draft

robobun wants to merge 2 commits into
mainfrom
robobun/926ebeab/sqlite-no-attribute-external

Conversation

@robobun

@robobun robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun build --target=bun bundles import db from "./db.sqlite" (no type attribute) as import.meta.require("/abs/path/on/build/machine/db.sqlite", { type: "sqlite" }).db. The bundle opens the build folder's database.
  • resolve_import_records (src/bundler/bundle_v2.rs) keeps a sqlite import external only by its attribute, before resolution. A loader from the file name comes later, so get_ast (src/bundler/ParseTask.rs) prints source.path.text.

Fix

  • The rule also runs where the resolved loader is first known (four sites). A .sqlite import prints as written, and the build warns.
  • Correct because the runtime picks the sqlite loader for a .sqlite name: the bundle runs like the source.
  • Verified: test/bundler/bundler_bun.test.ts (10 of 19 new tests fail on main). More in Notes.
  • Self-reviewed: 48 concerns raised, 25 addressed. See Notes.

Background

Downsides

  • Default change, needs a maintainer decision. A bundle that is not next to its database (src/ to dist/) now fails at run time. Before, it opened the build path. The same sites can warn only.
  • 10 of 38 probed routes keep the absolute path (loader-map names, plugin routes, export *).
  • Cost without sqlite: +2 instructions per resolved import record, +12 per onResolve-declined record, +2,304 bytes of .text.
Notes

The decision this PR needs

  • "External, like the attribute form" is this PR. import db from "./db.sqlite" is printed as written and the build prints a located warning.
  • "Warn only" keeps today's output and prints the warning at the same four sites. It breaks no build and does not remove the absolute path. It is a small edit of this branch.
  • The attribute (with { type: "sqlite" }) and --external '*.sqlite' give the external output today.

What changes

  • import, import { db }, import * as, require(), import(), require.resolve() and export { x } from of a .sqlite file (any case, also through #imports, a package subpath, Bun.build({ files }), or after an onResolve plugin declined): printed as written, in ESM, CommonJS, --bytecode and --compile output.
  • The warning: "./db.sqlite" is not bundled: the program opens it at run time, relative to the bundle with the note add with { type: "sqlite" } to this import to state that, or with { type: "sqlite", embed: "true" } to embed the database. For --compile it says relative to the working directory. Bun.build() puts it in logs only.
  • Bundled require("./db.sqlite") returns { __esModule, db, default }, as the runtime does. It returned the Database. import { db } works (it failed the build).
  • All import kinds of one database share one module and one Database at run time, as they do without the bundler.
  • A static import is hoisted: a database that a lazily loaded module imports is opened when the bundle starts. The attribute form does the same.
  • A name the module does not export (import { users }) fails at run time, not at build time.
  • An embed import and a plain import of one database no longer share a module. The embed import copies the file and the plain import stays external, in either order. On main the first import decides for both, and across files the order is a race that can drop embed (the module key is the path only: bundler: key modules by (path, loader) so one file under two import attributes is two modules #41834, Carry import attributes through the parser, printer, bundler and runtime; add the bytes loader #40836).
  • The build no longer reads the database.

What does not change (still the absolute path, as on main)

  • A name from the loader map (--loader .db:sqlite, bunfig, Bun.build({ loader })). The runtime does not have the map, and the printer puts a type only on an import statement of ESM output.
  • A database that is an entry point, that an onResolve plugin returns a path for, or that an onLoad filter matches. A native onBeforeParse plugin keeps today's output for every database (the check is coarse).
  • export * from a database: a star re-export of an external in a module that is not the entry point hits bundler: name the namespace of an external export * in a non-entry module #41220.
  • A module.exports = require("./db.sqlite") shim: the redirect would print the loader of the shim (bundler: no spurious with { type: ... } on external hoisted from module.exports = require(...) #35692).
  • Framework builds (bun build --app, the dev server) and the bun test --changed scan. The prerender runs the bundle where it was built, and the scan needs the graph edge.
  • Targets other than bun: To use the "sqlite" loader, set target to "bun".
  • An explicit external, a plugin's external: true, another type attribute and a loader-map entry for .sqlite go first.

Known limits of "printed as written"

Measurements (release builds of main at 8ba82c3, without and with this change)

  • resolve_import_records: +2 instructions per resolved import record (cmp $0xf, je not taken), 0 calls added on that path (objdump). 1,801 hits on a 301-module fixture (gdb counter).
  • run_resolver, record an onResolve plugin declined: +12 instructions, 0 calls (objdump). Path::loader calls 29 -> 29 on an 84-record plugin fixture (gdb counter).
  • Plugin filter probes with no sqlite import: 115 base, 115 PR. One .sqlite import adds 2 on both.
  • Release binary: .text +2,304 bytes, .rodata +0 (size -A). File size unchanged. JSBundlerPlugin__ exports 15 -> 15. 0 host functions. No codegen change. get_ast is unchanged.
  • Per parsed file: 2 more fields in ResolveImportRecordCtx (a slice and a u32). Per build: 1 bool on BundleV2.
  • Outputs with no sqlite file: 53 of 53 files byte-identical between base and PR (sha256sum). 7 inputs (react, prettier, typescript, a module.exports = require("some-ext") shim, a 301-module fixture, scripts/build.ts, a test fixture) in esm, cjs and --splitting, for target bun and browser. The same 8 of 42 builds fail on both.
  • 128 MB database (133,357,568 bytes), no attribute: peak RSS 162.8 MB -> 16.1 MB (getrusage, median of 7). Bytes read 133,370,301 -> 12,727 (/proc/<pid>/io under gdb). Opens of db.sqlite 1 -> 0 (gdb catch syscall).
  • Route matrix, 38 routes: 20 now print the import as written (19 had the absolute path, 1 failed the build). 18 are unchanged, and 10 of those still have the absolute path. 0 builds fail that passed.
  • In-place layout: the build warns and the run fails with Cannot find module './db.sqlite'. It printed the build row before. A --compile binary started from the database's directory still works.
  • strace, perf, valgrind and bloaty are not installed here. The counts come from gdb, objdump, size and getrusage.

Self-review (48 concerns)

  • 25 addressed by this shape: only names the runtime loads as sqlite, no build error in the sqlite arm, no docs change, no change to the redirect loop or the printer, a one-sentence warning with a note, itBundled tests, tests for explicit configuration and for bun build --app.
  • 13 are the limits and behavior changes listed above.
  • 8 question the default change itself (no user report, the attribute already works, a patch release). That is the decision above.
  • 1 needs no change (Node and the specs have no rule for this).
  • 1 rejected: the two run_resolver sites stay. Without them a build with a catch-all onResolve plugin keeps the absolute path.

Credit

Suites run with the debug build

  • test/bundler/bundler_bun.test.ts and test/cli/test/test-changed.test.ts (52 pass). 10 of the 19 new bundler tests fail on main at 8ba82c3 and on 1.4.2. The other 9 are guards: they pin what stays bundled and what goes first.
  • Also bundler_plugin, bundler_loader, bundler_barrel, bundler_cjs2esm, bundler_edgecase, esbuild/importstar and bundler_compile: 673 pass. 1 test in bundler_compile (does not run a synchronous full GC) hit its 5 s timeout under the local ASAN debug build. It has no database.
  • bun build --app with a server page that imports ../db.sqlite without the attribute: exit 0 and the prerendered page has the row of the build, on main and on this branch. Run by hand: the bake production suite does not fit its 5 s default under the local debug build.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/test/test-changed.test.ts

@robobun

robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:34 AM PT - Oct 16th, 2026

✅ @robobun, your commit cfd71cc21274867055a98210c5fc7976bee696d2 passed in Build #126596! 🎉


🧪   To try this PR locally:

bunx bun-pr 45502

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

bun-45502 --bun

@robobun

robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: draft. One maintainer decision is open (see below).

Reproduced on 1.4.3 and on main. import db from "./db.sqlite" (no type attribute) with bun build --target=bun writes import.meta.require("/abs/path/of/the/build/db.sqlite", { type: "sqlite" }) into the output. The bundle then reads the database of the build folder and ignores a database next to the bundle. With this branch the import is printed as written, the build warns, and the bundle reads the database next to it.

The open decision: this changes a default. A bundle that is not next to its database (src/ to dist/, or a --compile binary started from another directory) worked on the build machine before and now fails with Cannot find module './db.sqlite'. The alternative is to keep the output and only print the warning at the same four sites. That is a small edit of this branch.

Related: #38275 is an older proposal for the same defect. It conflicts with main. It also covers names from the loader map (--loader .db:sqlite) and prints the type on an external require() and import(). This PR does not. The comparison of the two is in #38275 (comment). Only one of the two should merge.

The sqlite loader does not copy the database. An import that got the
loader from its .sqlite name became a module that holds the absolute
path of the database on the build machine.

The import now stays as written once its resolved loader is known, like
the attribute form, and the build warns. The rule runs at the four
sites that resolve an import record.
@robobun
robobun force-pushed the robobun/926ebeab/sqlite-no-attribute-external branch from 53b655f to 155abf5 Compare October 16, 2026 10:33
Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/bundler/bundle_v2.rs Outdated

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