Repository navigation
Conversation
…ugin loads or that only types use The type checker got the text of a `files` entry from the bundle graph. For an entry that an onLoad plugin loads, that is the plugin's output, or the file on disk that the entry overrides. An entry that the bundle does not load, such as the target of `import type`, was missing. `BundleV2::type_check` now takes the text of a plugin-loaded entry from the file map, with the loader of its name. It also hands over the entries that the bundle does not load and that are not on disk.
|
Updated 6:01 PM PT - Oct 21st, 2026
✅ @robobun, your commit 191fd684c8c1ab91e4cb758074394c0b3930da46 passed in 🧪 To try this PR locally: bunx bun-pr 46320That installs a local version of the PR into your bun-46320 --bun |
StatusReproduced on main with a debug build. The layout:
A second layout needs no plugin:
PR: #46320 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughType checking now selects source contents from the graph, ChangesBuild type checking
Suggested reviewers: Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to
|
|
The review left one case unverified: a type-only import through a relative
This PR does not change that case. The Notes list it under "Not changed", and #38650 is about what a relative key names. |
…hat isn't TypeScript or JavaScript by name A plugin-loaded key of `files` like `virtual`, `App.svelte` or `config.yaml` has no source text that the type checker can read. The check takes the plugin's output for it, as for a file that is in neither place. A key of `files` that the bundle does not load is not given to the type checker if it has a `.`, `..` or empty part, or if it is in `node_modules`. For such a key, the bundler and the type checker can name different files.
Problem
filesentry that only types use (import type) is not in the bundle.Bun.build({ check: true })reports a false TS2307 for it.filesentry that anonLoadplugin loads, the checker gets the plugin's output or the disk file. Afilestext with TS2322 passes.BundleV2::type_check(src/bundler/bundle_v2.rs:2586) takes each text from the graph and never readsself.file_map.Fix
type_checkhands over each TS or JSfilesentry that the bundle does not load as code, if it is not on disk. Its key must be a plain absolute path outsidenode_modules.fileskey with such a name, it takes thefilestext and the name's loader.test/bundler/bundler_files.test.ts(41 new tests, 20 fail on main). Alsocheck.test.ts,entry-points.test.ts.Background
filesmaps paths to in-memory texts that override the disk (ParseTask.rs:1477).type_checkfillsalready_read, which the checker reads before the disk.TypeChecked::filesfield (a second files-before-disk rule).Downsides
filestext the source when anonLoadplugin loads the path? This PR says yes, so a plugin-filled placeholder fails (TS2305), as on disk.access()calls, 1,013,760 copied bytes, and 990 more loaded files underinclude. Binary: +2,560 bytes of text.filesentry that is on disk too and that only types use. The check reads the disk (Notes).Notes
How it was found. By a probe during the work on #45693. There is no user report and no issue.
The rule in this PR.
onLoadplugin loads, and whose path is a key offiles: if the name is TypeScript or JavaScript (Path::loader, which also reads theloaderoption), the check reads the text offiles, with the loader of the name. The loader in the graph is the plugin's. For every other name (App.svelte,config.yaml,data.json, a name without an extension), the rule of main applies: the check reads what the plugin returns if the file is not on disk.filesthat no file of the bundle gives to the checker (the bundle does not load it, or loads it as text): the checker gets it if its name is TypeScript or JavaScript, its key is an absolute path with no.,..or empty part, it is not innode_modules, and no file is on disk at the key.Results that change. Each one has a test.
filesand that only types use: no TS2307 (import type, an import that only a type uses,export type,with { type: "text" }of a TypeScript file) and no TS2304 (an ambient.d.ts). Such a file is now a file of the program ifincludehas it: an error in it is reported when a checked file imports it, and its ambient declarations apply.onLoadfor a TypeScript or JavaScript key offiles: thefilestext is checked. Main checks the plugin's output (file not on disk) or the disk file (file on disk too). So:filestext is reported.filesthat the plugin fills fails with the errors of the placeholder (TS2305 in the test). The same placeholder on disk fails on main too.Not changed. These are as on main. The first five have a test.
filesentry that the bundle does not load and that is on disk too (an override of a file that only types use): the check reads the disk text. A rule intype_checkis not safe there, for two reasons. Where names fold case (macOS, Windows), the resolver names a file by the directory as the importer spelled it. So the bundle can hold the disk text of that file under another spelling than the key, and the checker, which folds case, keeps one of the two texts. And a module that only a macro import reaches is not in the graph, but the macro runs the text on disk..,..or empty part (/app/missing/../lib.ts,/app/lib.ts/). A relative import reaches a key only in joined form.access()on the other spellings can fail while the joined path is a file on disk.node_modules. The checker takes the real path of a file that it finds there through a package name. Under a linked package directory, a key that is only infilesthen fails with TS5083.filesentry with a relative key that the bundle does not load: TS2307. What a relative key names is the subject of bundler: resolve relative Bun.build files keys against the cwd #38650.fileswhose name is not TypeScript or JavaScript: the check reads what the plugin returns if the file is not on disk, and nothing of the plugin if it is.filesthat the bundle does not load, which includestsconfig.jsonand a file that it extends: the bundler's resolver reads configuration from disk, and so does the check. A file thattsconfig.jsonextends can have any.jsonname, so no rule by name separates it from a JSON module. The false TS2307 for a JSON module that only types use stays.files, and that a server module imports: the scripts of the page on disk are checked too.onBeforeParseplugin: the check reads what the plugin returns, on disk and infiles.onLoadreturns with a loader that is not code is not checked.files.//server/share/...) has an empty part, so the checker does not get it.A difference that stays. The check finds a file that is only in
filesthe way TypeScript finds a module: also without its extension, as a directory index, and as.tsfor.js. The bundler finds afilesentry only by its exact path. So a type-only import can pass the check where the same import of a value fails in the bundler.The question, in full.
onLoaddoes not get thefilestext. A plugin that answers for a path replaces the source before the bundler readsfiles, so the bundle never has thefilestext of that path. Main checks the source text of a file, not what a plugin returns for it, and it takes the source text from disk. This PR takes it fromfilesfirst. That is worse than main in two cases: a placeholder infilesthat a plugin fills, and a plugin that reads the file on disk whilefilesoverrides it (the check readsfiles, the bundle has the disk text). If the answer is no, the plugin clause goes (theif is_of_pluginblock ofchecked_source, with its tests), and the rest stays.Measured. Linux x64.
straceandperfare not installed. The tools are anLD_PRELOADcounter ofaccess()andrealpath(), an inotifyIN_OPENwatcher, a temporary log line incheck_for_build(not in this PR), andsize.check:type_checkreturns on its first line, and the load path has no hunk.access()calls that name the project in an unchecked build with 8 plugin-loaded files: 0 on main, 0 here.access()calls in a checked build with 8 plugin-loaded files, 4 of them keys offiles: 8 on main, 4 here. With nofiles: 8 and 8. Withfilesthat the bundle loads and no plugin: 0 and 0.bun build --check: 0 and 0.realpath()calls: no difference in any of these.filesof 1 KB each, none on disk, 10 of them imported:access()calls 0 on main, 990 here. The checker gets 11 texts (10,666 bytes) on main, and 1,001 texts (1,024,426 bytes) here. It loads 12 files on main. Here it loads 1,002 files ifincludehas the 1,000, and 12 if not.access()calls 0 on main, 990 here. The checker gets the same texts as on main.include. Keys not on disk: median 67 and 60 ms on main (two series), 92 ms here. Keys on disk too: 141 and 132 ms on main, 149 ms here.filesoverrides: 1 on main, 0 here.existscall. The path of that file in the graph has backslashes, and the lookup of the key compares bytes. The result does not change.scripts/runner.node.tswith the debug build (67 pass, 2 skip, exit 0).bun check,bun --checkandbun test --checkdo not reach the changed code:check_command.rsand the checker host have no hunk.Other pull requests.
git merge-treeof this head with it reports no conflict in the files of this PR. An earlier revision of this change, with the same plugin clause, was merged with bundler: type-check what a plugin-loaded file imports, where the project has the file #45693 on ea6a2c7 without a conflict. On that buildplugin-imports.test.ts(84 tests) andbundler_files.test.tspassed.bun build --compile --aot#45631 (draft) moves theexistsclause intochecked_sourceand keeps it for builds withoutanalyze. This change ports there under the same guard.filesagainst the working directory. After it, the absolute-key condition here can go.Tests. The tests are in
test/bundler/bundler_files.test.ts, the file of thefilesoption. The other build tests ofcheckare intest/cli/check/check.test.ts. That file does not pass in a debug build on this machine: on unchanged main, 220 of its 641 tests reach the 5 s limit. The new tests runBun.buildin the test process withnoLib. Two tests run a build in a child process, because a relative key needs the project as the working directory. Two tests run only on a case-insensitive file system. I did not run them. They passed in CI at 5566c61 on macOS and Windows.The new tests run one at a time. Each type check uses every core, so checks that run at once in one process slow each other down: on a release build here, 20 of these builds at once take about 5 s, and one alone takes 30 ms. With
describe.concurrent, 30 to 36 of the tests reach the 5 s limit on that build.Also run with this change:
bun run rust:check-all x86_64-pc-windows-msvc aarch64-apple-darwin(2 ok),cargo clippy -p bun_bundler(no warning), andtest/integration/bun-types/bun-types.test.ts(24 pass). On a release build of this change on ea6a2c7:bundler_files.test.ts(67 pass, 2 skip),entry-points.test.ts(473 pass), andcheck.test.tswith the same failures as the release build of ea6a2c7 without the change (those tests are newer than that base).Self-review. Two reviews, 65 concerns, 59 addressed.
The first revision also gave the checker a page of
files, plain.jsonentries, and a rule for the spelling of a key. The first review found that the page rule turned three green builds red (TS6053), that a.jsonentry thattsconfig.jsonextends changed the options of the check, and that the key rule contradicted #38650. All three are removed. It also found a test that leaked under LeakSanitizer (a native plugin), which is removed with the native case. Not taken: one classification of a graph row thatentry_pointsuses too (that is the change of #45693), a stack on #38650, and a disk-as-oracle test helper.The second review read the answer to the code review of 5566c61. That answer gave the checker an unloaded key that is on disk too, if the key was the real path of the file. The review showed by experiment the two cases under "Not changed" where the check then reads a text that the bundle did not use, so that rule is removed. It also found the keys with a
..part, the keys in a linked package, and the names likeconfig.yamlthat the plugin clause took fromfilesalthough the checker cannot read them. Each has a condition and a test now. Not taken:type_checkbefore it readsfiles, for a build in which no entry point has types. Such a build withcheckis rare, and the pass costs about 1 ms for 1,000 keys.filesentry the way it finds a file on disk.[human-review] gate passed · iteration 0 · 5 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