Skip to content

Honor tsconfig verbatimModuleSyntax, preserveValueImports and importsNotUsedAsValues - #40822

Draft
robobun wants to merge 9 commits into
mainfrom
farm/20b869b2/ts-unused-import-flags
Draft

robobun wants to merge 9 commits into
mainfrom
farm/20b869b2/ts-unused-import-flags

Conversation

@robobun

@robobun robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #12744

Problem

  • tsconfig verbatimModuleSyntax, preserveValueImports and importsNotUsedAsValues do nothing: preserve_unused_imports_ts is false at every site. import { unused } from "./b" never runs ./b, unlike tsc and esbuild.
  • With no setting, import {} from "x" runs x, unlike tsc: scan_imports.rs cannot tell an empty clause from import "x".

Fix

  • Each TypeScript file takes esbuild's two flags (keep the statement, keep the unused names) from the tsconfig.json nearest to its real directory. An installed package takes none.
  • bun run, bun test and bun build link their output, so they keep import "./b" and drop the names. bun build --no-bundle prints them.
  • A clause that binds no value goes unless the statement is kept. ImportRecordFlags::KEPT_BY_TSCONFIG marks an import that only tsconfig keeps.
  • Verified: test/bundler/transpiler/runtime-transpiler.test.ts (67 of 83 new tests fail on main). Self-reviewed: 43 concerns raised, 32 addressed, 11 left to a maintainer (Notes).

Background

  • tsc removes an import that no value uses: it may name a type. verbatimModuleSyntax (set by bun init) keeps all but import type and type names.
  • Considered keeping the names at runtime: a type imported without type then fails to link.

Downsides

  • A verbatimModuleSyntax: true project now loads more modules. import { type Handler } from "aws-lambda" with only @types/aws-lambda installed fails with Cannot find package, as in Node.
  • With no option set: +43 instructions per TypeScript module, +22 per JavaScript module, +54 per TypeScript import statement, .text +10.8 KB. No new allocation or syscall.
  • Not covered: a TypeScript file that becomes CommonJS at runtime.
Notes

Status. Draft. It waits for a maintainer's answers to the questions below.

How this was found. By inspection and by comparison with tsc and esbuild, after a maintainer asked for esbuild's shape with no flag, the import {} from fix and the test matrix. No user report of a skipped side-effect import exists. #12744 asks for an error on a type imported without type. This PR does not add that error: the runtime and a bundle drop the unused name.

Questions for a maintainer.

  1. Default. verbatimModuleSyntax: true is in the tsconfig.json that bun init writes, so this PR changes which modules load in those projects with no flag. The section "What a kept import changes" lists the programs that run today and fail after it. If it must ship behind a switch, gate the verbatim_module_syntax == Some(true) arm of TSConfigJSON::unused_import_flags. That switch makes the three keys inert. It does not bring back the behavior of main for import {} from (question 2).
  2. No setting. With no tsconfig setting, a TypeScript import {} from "x", export {} from "x" and import { type as } from "x" no longer run x. This follows tsc and esbuild. node file.ts runs them, and so does Bun today. It was part of the request, and js_parser: drop a TypeScript import whose clause binds nothing #41542 (closed in favor of this PR) held the same change. It has no opt-out other than a setting that keeps the statement. It can move to its own PR.
  3. bun build --no-bundle prints the unused names, as tsc and esbuild do. A type that is imported without type is then a name with no export: the output fails when it runs (Export named 'SomeType' not found). bun run and a bundle drop such a name. The other choice is to drop the names in --no-bundle too.
  4. Installed packages. A file in node_modules, or in a package that auto-install fetched, takes no setting, also not from a tsconfig.json in its own tarball. A library that relies on the setting then behaves in its repo and not when installed. resolver: ignore tsconfig.json inside node_modules #36673 and Transpile each file with the tsconfig.json nearest to it at runtime #38719 propose rules for the other tsconfig settings.
  5. Auto-install. While auto-install is on, an import of a bare name that only tsconfig keeps is dropped, as without the setting. Otherwise import { type Handler } from "aws-lambda" downloads and runs the npm package of that name, which is not @types/aws-lambda.
  6. importsNotUsedAsValues and preserveValueImports. TypeScript 5.5 removed them and bun check reports them as unknown options. The transpiler reads them because esbuild does.
  7. Order. See "Order with open PRs".

Reference matrix. m.ts is one statement plus a console.log. "runs" means the module of the statement is evaluated. tsc 6.0.2 emit run in Node 26.3 and esbuild 0.21.5 agree on every cell. Main (179664e) matches 19 of 30 cells. This PR matches 30 of 30 in bun run and in bun build. Node's own type stripping (node m.ts) reads no tsconfig.json and runs the module in every row.

Statement no tsconfig, {}, verbatimModuleSyntax: false, isolatedModules: true verbatimModuleSyntax: true
import {} from "./p" does not run runs
export {} from "./p" does not run runs
import { type X } from "./p" does not run runs
import d, {} from "./p" does not run runs
import * as n from "./p" does not run runs
import "./p" runs runs

Which tsconfig.json applies.

  • The nearest tsconfig.json or jsconfig.json of the directory that the file is loaded from (its real path). bun run and bun build use one accessor, Resolver::unused_import_flags_ts. A workspace package that is linked into node_modules is loaded from its own directory, so it takes the setting in both. With --preserve-symlinks it is loaded from node_modules and takes none, as in esbuild.
  • --tsconfig-override and Bun.build({ tsconfig }) replace the tsconfig.json of every directory.
  • A source with no directory (a plugin's, a Bun.build({ files }) entry, bun repl) takes the tsconfig.json of the working directory, and none for a path in node_modules.
  • A declaration file and a statement inside a declare block ask for no module. On main bun build fails for declare module "m" { import { T } from "./x" } (Could not resolve). That is fixed with and without the setting.
  • A TypeScript file that becomes CommonJS at runtime (it uses module.exports) keeps nothing in bun run, as on main. A file with a kept import and require, __dirname or "use strict" is an ES module, as it is for import { T }.
  • bun check selects the tsconfig.json as tsc does (references, every extends form, on with no tsconfig.json). It can report TS1484 for a file that the runtime does not apply the setting to.

What a kept import changes. Each row runs on main under verbatimModuleSyntax: true and differs after this PR. tsc's output does the same in Node.

Program After this PR
import { type Handler } from "aws-lambda", only @types/aws-lambda installed Cannot find package 'aws-lambda', with a note that names import type. bun build: Could not resolve, with the note in place of Maybe you need to "bun install"?
import { type T } from "./types", only types.d.ts exists Cannot find module './types', with the note
import { type Config } from "./config" before import "./setup" ./config and its imports run before ./setup. No error
A class imported without type, used only as a type, in an import cycle Can fail with Cannot access 'Base' before initialization
Client code with import { type AppType } from "./server" The browser bundle includes ./server
bun test --coverage Lists each module that a kept import loads

A sample from the self-review, not repeated here: of 7 public projects that set the option, 5 have imports whose loading changes and 1 has an import that no longer resolves. The tests of elysia 1.4.28 pass with the option forced on.

Measurements. Release builds of main 179664e and of this PR (61e5fba, the source of the pushed head), linux-x64.

  • Bun__transpileFile before the transpile starts (gdb step count): +22 instructions per .js module (1382 to 1404), +43 per .ts module (1476 to 1519), +121 per .ts module under verbatimModuleSyntax (1481 to 1602).
  • Run of 400 modules: DirInfo lookups 1406 = 1406, openat 428 = 428, read 823 = 823 (main = PR).
  • bun build with 4 packages linked from an isolated store: DirInfo lookups 31 = 31, same list of opened paths, store directories opened 0 = 0.
  • size_of, main to PR: S::Import 48 to 48, ImportRecord 96 to 96, resolver::Result 296 to 296, ResultFlags 1 to 2, DirInfo 104 to 104, TSConfigJSON 304 to 304, ParseTask 408 to 408, ParseOptions 432 to 440, TranspilerJob 400 to 400.
  • Release binary: .text +10,752 bytes (82,164,617 to 82,175,369), file +8,192 bytes.
  • Parser, bun build --no-bundle on 20,000 import statements (instructions:u, 20 interleaved runs, sigma 0.02 per statement): +54.0 per TypeScript import statement (+48 for import "x"), +1.0 per JavaScript import statement (+2 for import "x"). A file with no import: 0. Main spends 6,600 instructions per import statement.
  • Transpiler cache 43 to 44: 201 of 201 entries rebuilt once, +51k instructions and +6 file syscalls per entry (5 KiB modules). A tsconfig flip rewrites 101 of 101 TypeScript entries and 0 of 100 JavaScript entries.
  • Output with no option set, bun build --no-bundle on each tracked file: 0 of 3,817 TypeScript files and 0 of 8,000 JavaScript files differ.
  • In-tree projects with verbatimModuleSyntax: true (136 TypeScript files): 8 files differ. 7 statements are newly kept and 1 name is newly printed. Each names a module that the program loads already (bun, react, svelte). scripts/build does not change.
  • bun build --no-bundle against esbuild 0.21.5, 11 statements x 4 settings: 0 of 44 texts differ (main 22), apart from two spellings that main already has (import"j", export { } from).
  • Bun.Transpiler: transformSync, scan and scanImports disagree in 0 of 132 cases (main 4).

Order with open PRs.

Not done here.

  • bun run of a file that becomes CommonJS keeps nothing. bun build keeps the import there, as esbuild does.
  • jsx and the decorator options still come from the tsconfig.json of the working directory at runtime (Transpile each file with the tsconfig.json nearest to it at runtime #38719).
  • Bun.build({ files }) with nested tsconfig.json files that disagree: a files entry takes the working directory's.
  • The dev server of Bun.serve does not read --tsconfig-override.
  • bun test caches no output for a file that imports vitest or @jest/globals, and now also not for a kept type-only import of them. bun run does cache it, so bun run of such a file and then bun test can load the package, as for a value import on main.
  • Declined from the self-review: a second elysia lane in CI with the option forced on, the bun init label and --minimal help text, the lint rule in the React templates, and test rows for --compile, --hot and HTML entries.

Tests. test/bundler/transpiler/runtime-transpiler.test.ts (the three settings in bun run, bun build, --no-bundle, with extends; each way to load a file, the dev server after a save; which tsconfig.json applies: nested, node_modules, override, plugin sources, files, a linked workspace package; CommonJS; the 6 x 5 statement matrix; declare blocks and declaration files; what a kept import asks for), test/bundler/transpiler/transpiler.test.js (Bun.Transpiler table with transform(), declare blocks, exports.eliminate, the type as grammar), test/bundler/esbuild/tsconfig.test.ts (esbuild's fixtures, now asserted, and the dev server module format), test/cli/run/transpiler-cache.test.ts (cache key, bun test then bun run), test/cli/run/run-autoinstall.test.ts (no request for a kept import, a package in the cache), test/integration/bun-types/fixture/bun.ts.

Also run on the debug build, all pass: test/bundler/esbuild/{ts,importstar_ts,importstar,dce,default}.test.ts, test/bundler/transpiler/{decorators,es-decorators,decorator-metadata,ts-use-define-for-class-fields,macro-test,assign-to-import,jsx-production}.test.ts, test/bundler/transpiler/export-default.test.js, test/bundler/{bundler_plugin,bundler_loader}.test.ts, test/js/bun/plugin/plugins.test.ts, test/bake/dev/{esm,bundle}.test.ts, test/js/bun/resolve/{resolve,resolve-error}.test.ts, test/cli/run/{run-autoinstall,autoinstall-cached-manifest}.test.ts, test/cli/test/bun-test.test.ts, test/js/bun/test/test-auto-import-jest-globals.test.js, test/internal/source-lints/. test/bundler/bun-build-api.test.ts passes but for one stress test that exceeds its 180 s limit on the debug build of this machine. That test takes 7.8 s on the release build and 7.6 s on main.


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

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 23 days. After that, they cost $0.25 per reviewed file.

Or wait 5 minutes for your next included review.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 43aeeef5-31db-43af-9feb-3146c1ee1739

📥 Commits

Reviewing files that changed from the base of the PR and between d578a8c and 8f10198.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • src/ast/s.rs
  • src/bundler/ParseTask.rs
  • src/bundler/options.rs
  • src/bundler/transpiler.rs
  • src/js_parser/lower/lower_esm_exports_hmr.rs
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_entry.rs
  • src/js_parser/parse/parse_import_export.rs
  • src/js_parser/parse/parse_stmt.rs
  • src/js_parser/parser.rs
  • src/js_parser/scan/scan_imports.rs
  • src/js_printer/lib.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • src/jsc/RuntimeTranspilerStore.rs
  • src/options_types/Cargo.toml
  • src/options_types/bundle_enums.rs
  • src/options_types/lib.rs
  • src/react_compiler/imports.rs
  • src/resolver/resolver.rs
  • src/resolver/result.rs
  • src/resolver/tsconfig_json.rs
  • src/runtime/api/JSTranspiler.rs
  • src/runtime/jsc_hooks.rs
  • test/bundler/esbuild/tsconfig.test.ts
  • test/bundler/transpiler/transpiler.test.js
  • test/bundler/transpiler/ts-unused-imports.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: draft. It waits for a maintainer's answers to the "Questions for a maintainer" in the description: the default with no flag, the change with no setting, the names that bun build --no-bundle prints, installed packages, auto-install, and the order with #46553.

Reproduced with bun 1.4.3 (367d939) and main 179664e, against tsc 6.0.2, esbuild 0.21.5 and Node 26.3:

  • With verbatimModuleSyntax: true, import { unused } from "./b" does not run ./b in bun run and in bun build. tsc and esbuild run it.
  • With no setting, import {} from "./p" and export {} from "./p" run ./p. tsc and esbuild do not.

The current head removes three defects of the push 86c20b3:

  • bun build kept or dropped the import of a workspace package linked into node_modules by the order of resolution. The flags now come from the real directory of the file, as in bun run.
  • bun test cached output with bun:test in place of vitest for a kept type-only import, which bun run then loaded.
  • With auto-install, a kept type-only import downloaded and ran the npm package of that name.

@robobun

robobun commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:42 AM PT - Oct 26th, 2026

✅ @robobun, your commit 147707b1efb247009a70012020e3a308ee5cdd54 passed in Build #132964! 🎉


🧪   To try this PR locally:

bunx bun-pr 40822

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

bun-40822 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it changes TypeScript import-elision semantics across the parser, printer, resolver, and all four transpile entry points — including a documented user-visible behavior change under verbatimModuleSyntax: true — a human look would still be worthwhile.

What was reviewed:

  • TSUnusedImportFlags threading: resolver → ResultFlags (u8→u16) → ParseTask/ParseOptions/BundleOptions → parser, plus Bun.Transpiler and the runtime store/hooks — every construction site sets the field.
  • tsconfig parsing and extends merge: the three settings are Option<bool> and merge base→child in the existing loop, so a child unset no longer clobbers an inherited "preserve"; verbatimModuleSyntax folds to both bits.
  • has_items_clause on S::Import: distinguishes import {} from bare import; printer round-trips import {}/export {} from and the minify-whitespace from spacing check was updated to match; scan_imports treats an empty clause as "found" and clears the flag when it empties the clause itself.
  • Cache correctness: version bumped 27→28 and the flags are hashed into hash_for_runtime_transpiler.
Extended reasoning...

Overview

This PR implements TypeScript's verbatimModuleSyntax, preserveValueImports, and importsNotUsedAsValues tsconfig settings, which control whether unused/type-only imports are elided. It introduces a TSUnusedImportFlags bitflags type and threads it from tsconfig parsing through the resolver (ResultFlags widened u8→u16), bundler ParseTask, parser Options, Bun.Transpiler, the runtime transpiler store, and jsc_hooks. The parser replaces the never-set preserve_unused_imports_ts boolean with the flags, drops had_type_only_imports bookkeeping from ImportClause, adds has_items_clause to S::Import so import {} from 'x' is distinguishable from import 'x', and updates scan_imports to honor KEEP_VALUES when not bundling/minifying-identifiers. The printer gains empty-brace round-tripping for both import {} and export {} from. The runtime transpiler cache version is bumped and the flags are added to the parser-options hash. 27 files, ~690 insertions.

Security risks

None identified. The change parses two additional boolean tsconfig fields and one already-parsed string enum, all of which gate compile-time elision decisions. No new untrusted-input parsing surface, no path handling, no allocation driven by external sizes, no auth/crypto.

Level of scrutiny

High. This is a semantic change to the TypeScript transpiler's import-elision behavior, ported from esbuild's model, and it applies across every transpile entry point (bun run, bun build, --no-bundle, Bun.Transpiler, scan-only). The PR itself calls out a user-visible behavior change: under verbatimModuleSyntax: true (which bun init emits), import { SomeType } from "./x" without type is now kept and will fail at runtime — correct per tsc (TS1484), but a change users will hit. Separately, import {} from 'x' and import { type T } from 'x' are now dropped in TypeScript by default where they were previously kept as bare imports. Both are correctness fixes toward tsc/esbuild parity, but they warrant a maintainer's sign-off on the compatibility trade-off. The extends-merge loop's variable naming (parent_config is actually the more-specific child being folded into the base accumulator mc) is pre-existing and the new lines follow the same pattern as use_define_for_class_fields, so the merge direction is correct.

Other factors

Test coverage is thorough: a new ts-unused-imports.test.ts matrix covers all four entry points × all three settings × extends inheritance, previously-dead esbuild tsconfig fixtures are moved above the early return and given real assertions, and transpiler.test.js gains a type as grammar table. The cache-key concern from REVIEW.md ("cache keys cover every input that shapes the output") is addressed — both the version bump and the hash addition are present. I checked that every S::Import construction site in the diff sets has_items_clause and that Default initializes it to false. No CODEOWNERS entries cover the changed paths. The bug hunt exited on dry_streak with no findings and no ruled-out candidates. Given the breadth (parser/printer/resolver/cache) and the intentional behavior change, this should get a human review rather than an automated approval.

@robobun

robobun commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Status: do not merge this PR as written. A rework is on the branch robobun/edf50f18/honor-verbatim-module-syntax. It is not ready for a PR, and it needs a maintainer decision first.

The bug is still on main

With "verbatimModuleSyntax": true (the value that bun init writes), import { unused } from "./b" is removed, so ./b never runs. I checked 1.4.2 and main 5c6d4e2, in bun run and in bun build. tsc 6.0.2 and esbuild 0.21.5 keep the import, and Node 26.3 runs ./b.

What is wrong with this PR

I rebased it onto main and ran it.

  • bun run keeps the unused names. A type that is imported without type then fails at link time: SyntaxError: Export named 'SomeType' not found in module. import { Mock } from "bun:test" fails the same way. Both run today.
  • bun run takes the option from the tsconfig.json of the working directory and applies it to every file, files inside node_modules included.
  • It also changes import {} from "x" when no option is set. js_parser: drop a TypeScript import whose clause binds nothing #41542 is about that question.

What the rework branch does

  • bun run, bun test and bun build keep the statement as import "./b" and drop the unused names. Only bun build --no-bundle prints the names.
  • Each file takes the option from the tsconfig.json nearest to it. A file inside node_modules takes none.
  • With no option set, the output and the runtime transpiler cache entries do not change.

Why it is not a PR yet

To honor the option changes which modules load in every bun init project. These programs run on main and fail on the branch (release builds of 5c6d4e2 and of the branch):

Program Result on the branch Kind
import { type Mock } from "bun:test" in a test that uses the global beforeEach ReferenceError: beforeEach is not defined Defect of the branch
A workspace package that is symlinked into node_modules bun run keeps its unused import, bun build drops it Defect of the branch
import "./globals.d.ts", where the declaration file imports ./models and only models.d.ts exists Cannot find module './models' Defect of the branch
import { type T } from "./x" inside declare module bun build does not resolve ./x Defect of the branch
import { unused } from "./effect"; module.exports = ... bun run skips ./effect, bun build runs it Defect of the branch
import { type Handler } from "aws-lambda" with only @types/aws-lambda installed Cannot find package 'aws-lambda' Same as the tsc output in Node
A class that is imported without type and used only as a type, in an import cycle ReferenceError: Cannot access 'Base' before initialization. Same as Node

The last two rows are what the option means. tsc accepts both programs, and Node fails on them in the same way.

Decisions for a maintainer

  1. The default. The choices are: honor the option behind a flag that is off in stable releases, honor it by default in a named release, or continue to ignore it.
  2. import { type T } from "x". tsc, esbuild and Node keep this statement as import {} from "x", so x runs. Bun erases it today. Most of the failures in the table come from this form.

The rework continues after that decision. It still needs:

  • one scope rule on the real path of a file, for bun run, bun build, plugin sources and in-memory files
  • a mark on an import record that only the option keeps, for the bun:test globals, the CommonJS detection and the resolve errors
  • no auto-install for such an import
  • no effect in declaration files and in declare blocks
  • a test for each row of the table

…NotUsedAsValues

A TypeScript import that no value uses is removed. tsconfig.json can ask
to keep it, and nothing read those settings. The parser now takes two
flags from the tsconfig.json that governs a file: KEEP_STMT
(`importsNotUsedAsValues: "preserve" | "error"`) keeps the statement as a
bare import, and KEEP_VALUES (`preserveValueImports`) keeps the unused
names. `verbatimModuleSyntax` sets both. The `extends` merge inherits each
setting.

The runtime and a bundle link what they print, so there the unused names
go and the statement stays. Only `bun build --no-bundle` prints the names.
Each file takes the flags of the tsconfig.json nearest to it. A file in
node_modules, a declaration file, and a file that becomes CommonJS at
runtime take none.

A TypeScript clause that binds no value (`import {} from "x"`,
`import { type T } from "x"`, `export {} from "x"`) is type-only and is
dropped unless KEEP_STMT is set. `S::Import::has_items_clause` records the
clause so that it prints as `import {} from "x"` when kept.
`parse_import_clause` follows esbuild's table for `{ type as ... }`.

The record of a statement that binds no value, or that sits in a `declare`
block, is created unused, and only the import scanner makes it live. The
bun:test global injection and the module type detection do not see it.
@robobun
robobun marked this pull request as draft October 25, 2026 19:44
@robobun
robobun force-pushed the farm/20b869b2/ts-unused-import-flags branch from 8f10198 to 64c765f Compare October 25, 2026 19:44
Comment thread src/ast/s.rs Resolved
Comment thread src/bundler/bundle_v2.rs Resolved
Comment thread src/bundler/options.rs Resolved
Comment thread src/js_parser/p.rs Resolved
Comment thread src/js_parser/parse/parse_entry.rs Resolved
Comment thread src/js_parser/parse/parse_entry.rs Resolved
Comment thread src/js_parser/parse/parse_entry.rs Resolved
Comment thread src/js_parser/parse/parse_entry.rs Resolved
Comment thread src/js_parser/parse/parse_entry.rs Resolved
Comment thread src/js_parser/parse/parse_stmt.rs Resolved
Comment thread src/js_parser/parse/parse_stmt.rs Resolved
Comment thread src/js_parser/parse/parse_stmt.rs Resolved
Comment thread src/js_parser/parse/parse_stmt.rs Resolved
Comment thread src/js_parser/parser.rs Resolved
Comment thread src/js_parser/parser.rs Resolved
Comment thread src/js_parser/scan/scan_imports.rs Resolved
Comment thread src/js_parser/scan/scan_imports.rs Resolved
Comment thread src/js_parser/scan/scan_imports.rs Resolved
Comment thread src/js_parser/scan/scan_imports.rs Resolved
Comment thread src/js_parser/scan/scan_imports.rs Resolved
Comment thread src/js_parser/scan/scan_imports.rs Resolved
Comment thread src/js_parser/scan/scan_imports.rs Resolved
Comment thread src/jsc/RuntimeTranspilerCache.rs Resolved
Comment thread src/resolver/resolver.rs Resolved
Comment thread src/resolver/tsconfig_json.rs Resolved
Comment thread src/resolver/tsconfig_json.rs Resolved
Comment thread src/resolver/tsconfig_json.rs Resolved
Comment thread src/runtime/api/JSTranspiler.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
A JavaScript parse did work for flags that only a TypeScript parse
reads: one record flag write per import statement, and the flag tests
in the import scanner. The const generic of the parser removes them.

- `process_import_statement` writes `IS_UNUSED` only for a TypeScript
  statement that starts unused.
- `ImportScanner::scan` takes the flags as empty for JavaScript and
  clears `IS_UNUSED` only for TypeScript.
- `S::Import` has a size assert: `has_items_clause` uses padding.

New tests: a dev server bundle, a plugin's source at runtime and in
`Bun.build`, and `bun build --tsconfig-override`.
…real directory

- `ImportRecordFlags::KEPT_BY_TSCONFIG` marks a TypeScript import that no
  value uses and that tsconfig.json keeps. It replaces the provisional
  `IS_UNUSED` of a kept clause with no value name. The `bun:test` global
  injection ignores such a record, and the transpiler cache is dropped
  for every `vitest` or `@jest/globals` record.
- A module statement inside a `declare` block creates no import record.
- The bundler reads the flags from the directory that a file is loaded
  from (its real path), through `Resolver::unused_import_flags_ts`, as
  the runtime does. A `Bun.build({ files })` entry and the second parse
  of a "use client" file take flags too.
- A package that auto-install put in the global cache takes no flags.
  With auto-install on, the flags keep no import of a bare specifier.
- A resolve error for a kept import has a note in place of the
  `bun install` hint.
- `replace_exports` drops a kept statement only when a deleted export
  used one of its names.
- `TSConfigJSON::unused_import_flags` computes the flags on read.
  `bun repl` takes the top-level flags.
- `bun test` caches no output for a kept type-only import of `vitest`,
  and each setting has its own cache key.
- A kept import makes a file an ES module in `bun run` and `bun build`.
- A workspace package linked into node_modules: `bun run`, one bundle
  from `bun build`, and `--preserve-symlinks`.
- `Bun.build({ files })`, `Bun.build({ tsconfig })`, an override that
  cannot be read, a package with two entry files, a module with no
  extension or no directory, a path that a plugin resolved.
- The dev server after a save, and its module format in `bun build`.
- `bun build --no-bundle` with `--minify-identifiers`, and its output
  when it runs.
- `Bun.Transpiler`: `transform()`, `declare` blocks, `exports.eliminate`.
- The evaluation order, the resolve errors and the note of `bun build`.
- Auto-install makes no request for a kept import, and a package in
  the cache takes no setting.
…urn it off

- typescript.mdx: the module of a kept import must exist and runs at
  the place of the statement, a browser bundle includes it, and
  `bun lint --fix` rewrites `import { type T }` to `import type`.
  The option comes from the tsconfig.json nearest to the directory
  that a file is loaded from. The way to turn it off is the
  tsconfig.json value. `--tsconfig-override` replaces every
  tsconfig.json, and the dev server does not read it.
- The two older settings are unknown options for `bun check`.
- quickstart.mdx no longer adds the option to an existing project.
- Three lint pages say when `import {} from` and `import { type T }`
  load the module.
`bun run` reports `Cannot find package 'x'` for an import that only
tsconfig.json keeps. When the importer is a TypeScript file under such
a tsconfig.json and only `@types/x` or `x.d.ts` exists, the error has a
note that says so and names `import type`.
They ran in test/cli/run/run-autoinstall.test.ts, a file of 300 tests. They now run in test/bundler/transpiler/runtime-transpiler.test.ts with the rest of the matrix.
@robobun

robobun commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

@dylan-conway this PR is a draft until you answer here. The description has the details under "Questions for a maintainer". The three that decide the shape:

  1. Default. Does verbatimModuleSyntax ship on with no flag? bun init writes "verbatimModuleSyntax": true, so those projects load more modules after this PR. Example: import { type Handler } from "aws-lambda" with only @types/aws-lambda installed runs today and fails with Cannot find package 'aws-lambda' after it, as tsc's output does in Node. The error now has a note that names import type.
  2. No setting. Does the change for a TypeScript import {} from "x", export {} from "x" and import { type as } from "x" (they no longer run x) stay in this PR? It follows tsc and esbuild. node file.ts and Bun today run x.
  3. Order. typescript: match tsc for experimentalDecorators and emitDecoratorMetadata #46553 and this PR both take transpiler cache version 44 and touch the same five files. Which one lands first? The second takes version 45.

This comment replaces the plan in #40822 (comment): the rework it names is now the head of this PR (0cf7e64), and it includes the change with no setting.

The keys of `Bun.build({ files })` have forward slashes on every
platform. `Path::is_node_module` looks for the platform separator, so on
Windows an entry under node_modules took the tsconfig.json flags of the
working directory. The check now looks for `/node_modules/` in the key.

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