Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/guides/runtime/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ Here is the full set of recommended `compilerOptions` for a Bun project. With th

---

`verbatimModuleSyntax` also changes which imports Bun runs. See [`verbatimModuleSyntax`](/typescript#verbatimmodulesyntax).

---

If you're using TypeScript 6.0 or later, you also need `"types": ["bun"]` in your `compilerOptions`. See [TypeScript 6 and 7](/typescript-6) for details.

---
Expand Down
2 changes: 1 addition & 1 deletion docs/lint/rules/import/no-empty-named-blocks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ typeAware: false
deprecated: false
---

Empty braces in an import are usually left over after someone deleted the last name. The import still loads and runs the module, but it doesn't say whether that's on purpose.
Empty braces in an import are usually left over after someone deleted the last name. In a JavaScript file, and in a TypeScript file whose `tsconfig.json` sets `verbatimModuleSyntax`, the import still loads and runs the module, but it doesn't say whether that's on purpose. In other TypeScript files Bun removes the import.

`--fix` removes the braces if the import also has a default import. An import that has nothing but `{}` isn't fixed, because deleting it would stop the module from being loaded. It comes with two suggestions instead: delete the import, or keep it as `import "./setup"`.

Expand Down
2 changes: 1 addition & 1 deletion docs/lint/rules/typescript/no-restricted-imports.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Each `paths` entry is a module name or an object:
| `importNamePattern` | Report only the names that match this regular expression. |
| `allowImportNamePattern` | Report every name except those that match this regular expression. |

An `import type` is erased when the file is compiled, so it doesn't load anything. With `{ name: "./server/db", allowTypeImports: true }`, these are allowed:
An `import type` is erased when the file is compiled, so it doesn't load anything. With `verbatimModuleSyntax`, `import { type Order }` is not erased: it still loads the module. With `{ name: "./server/db", allowTypeImports: true }`, these are allowed:

{/* good {"options": [{"paths": [{"name": "./server/db", "allowTypeImports": true}]}]} */}
```ts
Expand Down
2 changes: 1 addition & 1 deletion docs/lint/rules/unicorn/require-module-specifiers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ deprecated: false

`import {} from "./setup.js"` doesn't import any names. It's usually left over after the last name was removed. If you want the module to run for its side effects, `import "./setup.js"` says so.

`--fix` removes the empty braces after a default import. It removes `export {}` if the file has another `import` or `export`. Without one, that line is what makes the file a module: TypeScript rejects `declare global` in a script, and a script's top-level names are global. So only `--fix-dangerously` removes it there, and in a declaration file. No flag touches `import {} from` and `export {} from`, because they still load the module.
`--fix` removes the empty braces after a default import. It removes `export {}` if the file has another `import` or `export`. Without one, that line is what makes the file a module: TypeScript rejects `declare global` in a script, and a script's top-level names are global. So only `--fix-dangerously` removes it there, and in a declaration file. No flag touches `import {} from` and `export {} from`, because they still load the module in a JavaScript file, and in a TypeScript file whose `tsconfig.json` sets `verbatimModuleSyntax`.

{/* Generated by src/codegen/lint-rule-docs.ts. Do not edit this section. */}

Expand Down
1 change: 0 additions & 1 deletion docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ Build a minimal HTTP server with `Bun.serve`, run it locally, then evolve it by
"moduleDetection": "force",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true
}
}
Expand Down
2 changes: 2 additions & 0 deletions docs/runtime/transpiler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ interface TranspilerOptions {

// Whether to remove unused imports from transpiled file
// Default: false
// With tsconfig `verbatimModuleSyntax: true`, the unused names go
// and the statement stays as `import "module"`
trimUnusedImports?: boolean,

// Whether to enable a set of JSX optimizations
Expand Down
2 changes: 2 additions & 0 deletions docs/typescript-6.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ Here's the full recommended `tsconfig.json` for a Bun project using TypeScript 6
}
```

`verbatimModuleSyntax` also changes which imports Bun runs. See [`verbatimModuleSyntax`](/typescript#verbatimmodulesyntax).

## Does this apply to TypeScript 7?

Yes. TypeScript 7 carries forward the same default. If you're upgrading directly from TypeScript 5 to 7, the same fix applies — add `"types": ["bun"]` to your `compilerOptions`.
Expand Down
68 changes: 68 additions & 0 deletions docs/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,74 @@ Running `bun init` in a new directory generates this `tsconfig.json` for you.
bun init
```

## `verbatimModuleSyntax`

With `"verbatimModuleSyntax": true`, Bun keeps every import that has no `type` modifier on the statement. The imported module runs even when your code uses none of its names.

```ts index.ts icon="/icons/typescript.svg"
import { unused } from "./register-plugin"; // runs ./register-plugin
import { type Options } from "./options"; // runs ./options
import type { Config } from "./config"; // removed
```

Without the option, Bun removes all three imports, because no value uses them. Node.js runs a TypeScript file as if the option is set.

- `bun run`, `bun test`, and `bun build` keep the statement and remove the names that nothing uses. `import { unused } from "./register-plugin"` becomes `import "./register-plugin"`, so a type that you import without `type` still loads.
- `bun build --no-bundle` prints the unused names too, and prints `import { type Options } from "./options"` as `import {} from "./options"`. Each name that it prints must be a real export when the output runs.
- Bun does not apply the option to declaration files (`.d.ts`).
- `bun run` does not apply the option to a file that uses `module.exports`. That file runs as CommonJS, which cannot hold an `import` statement.

### What a kept import needs

A kept import is a request for its module, at its place in the file.

- The module must exist. `import { type Handler } from "aws-lambda"` fails when only `@types/aws-lambda` is installed. `import { type T } from "./types"` fails when only `types.d.ts` exists.
- The module runs where the statement is. It can run before a later `import "./setup"`, and an import cycle through it can fail with `Cannot access 'X' before initialization`.
- A bundle includes the module. In client code, `import { type AppType } from "./server"` puts `./server` in the browser bundle.
- `bun test --coverage` lists the module.

Write `import type` for an import that you need only for its types. This command rewrites `import { type Options }` to `import type { Options }`:

```sh terminal icon="terminal"
bun lint --rule '@typescript-eslint/no-import-type-side-effects: error' --fix
```

[`bun check`](/runtime/check) reports a type that you import with no `type` at all.

### Which `tsconfig.json` applies

Bun reads the option from the `tsconfig.json` or `jsconfig.json` nearest to the directory that each file is loaded from.

- Bun follows `extends` when it is a relative path that ends in `.json`. It does not follow `references`.
- With no `tsconfig.json`, the option is off.
- A file in `node_modules`, or in a package that [auto-install](/runtime/auto-install) fetched, takes no option. A workspace package that is linked into `node_modules` is loaded from its own directory, so its `tsconfig.json` applies.
- While auto-install is on, Bun does not keep an unused import of a package name. It would be a request to the registry.

`bun check` selects the `tsconfig.json` as `tsc` does. It can report `TS1484` for a file that the runtime does not apply the option to.

### Turn it off

Set `"verbatimModuleSyntax": false` in the `tsconfig.json` that applies to the files. A config that extends it inherits the value.

For a project with one `tsconfig.json`, `--tsconfig-override` also works. It replaces the `tsconfig.json` of every directory, with its `paths` and JSX settings, and the dev server of `Bun.serve` does not read it.

```json tsconfig.run.json icon="file-json"
{
"extends": "./tsconfig.json",
"compilerOptions": {
"verbatimModuleSyntax": false
}
}
```

```sh terminal icon="terminal"
bun --tsconfig-override=tsconfig.run.json index.ts
```

### Older settings

`"importsNotUsedAsValues": "preserve"` keeps the statement, and `"preserveValueImports": true` keeps the unused names. TypeScript 5.5 removed both settings, and `bun check` reports them as unknown options. The transpiler still reads them.

## Type checking

Bun runs TypeScript files without checking their types. Use [`bun check`](/runtime/check) to type check your project.
Expand Down
16 changes: 16 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2938,6 +2938,22 @@ declare module "bun" {
jsxFragmentFactory?: string;
jsxImportSource?: string;
useDefineForClassFields?: boolean;
/**
* Keep an import that no value uses, so that the module it names still runs. Only
* `import type` and `export type` are removed. With `trimUnusedImports` or
* `treeShaking`, `Bun.Transpiler` removes the unused names and keeps the statement
* as `import "module"`.
*/
verbatimModuleSyntax?: boolean;
/**
* Keep a value import that nothing uses.
* @deprecated Use `verbatimModuleSyntax`
*/
preserveValueImports?: boolean;
/**
* `"preserve"` and `"error"` keep an import that no value uses as `import "module"`.
* @deprecated Use `verbatimModuleSyntax`
*/
importsNotUsedAsValues?: "remove" | "preserve" | "error";
/** moduleSuffixes is not supported yet */
moduleSuffixes?: any;
Expand Down
3 changes: 3 additions & 0 deletions src/ast/import_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ bitflags::bitflags! {
/// `IS_UNUSED` for its own reasons (an unused TypeScript import, a macro
/// import), and those records must never be resolved.
const IS_BARREL_DEFERRED = 1 << 18;

/// No value uses this TypeScript import: its statement stays because tsconfig.json keeps it.
const KEPT_BY_TSCONFIG = 1 << 19;
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/ast/s.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ pub struct Import {
/// proposal. Only valid with a namespace import (`star_name_loc` is set,
/// `default_name`/`items` are empty).
pub phase_defer: bool, // = false
/// TypeScript only: a `{ ... }` clause was written, so empty `items` is `import {} from`.
pub has_items_clause: bool, // = false
}
const _: () = assert!(core::mem::size_of::<Import>() == 48);

impl Default for Import {
fn default() -> Self {
Expand All @@ -226,6 +229,7 @@ impl Default for Import {
import_record_index: u32::MAX,
is_single_line: false,
phase_defer: false,
has_items_clause: false,
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ pub struct ParseTask {

pub(crate) known_target: options::Target,
pub(crate) module_type: options::ModuleType,
pub(crate) unused_import_flags_ts: options::TSUnusedImportFlags,
pub(crate) emit_decorator_metadata: bool,
pub(crate) experimental_decorators: bool,
pub(crate) use_define_for_class_fields: bool,
Expand Down Expand Up @@ -209,6 +210,9 @@ pub(crate) struct Success {

/// The package name from package.json, used for barrel optimization.
pub(crate) package_name: ast::StoreStr,

/// What this parse took, for a second parse of the same file ("use client").
pub(crate) unused_import_flags_ts: options::TSUnusedImportFlags,
}

pub(crate) struct ResultError {
Expand Down Expand Up @@ -292,6 +296,7 @@ impl ParseTask {
jsx: resolve_result.jsx.clone(),
source_index,
module_type: resolve_result.module_type,
unused_import_flags_ts: resolve_result.flags.unused_import_flags_ts(),
emit_decorator_metadata: resolve_result.flags.emit_decorator_metadata(),
experimental_decorators: resolve_result.flags.experimental_decorators(),
use_define_for_class_fields: resolve_result.flags.use_define_for_class_fields(),
Expand Down Expand Up @@ -348,6 +353,7 @@ impl Default for ParseTask {
stage: ParseTaskStage::NeedsSourceCode,
known_target: options::Target::default(),
module_type: options::ModuleType::Unknown,
unused_import_flags_ts: options::TSUnusedImportFlags::empty(),
emit_decorator_metadata: false,
experimental_decorators: false,
use_define_for_class_fields: true,
Expand Down Expand Up @@ -634,6 +640,7 @@ pub mod parse_worker {
},
stage: ParseTaskStage::NeedsSourceCode,
module_type: options::ModuleType::Unknown,
unused_import_flags_ts: options::TSUnusedImportFlags::empty(),
emit_decorator_metadata: false,
experimental_decorators: false,
use_define_for_class_fields: true,
Expand Down Expand Up @@ -2548,6 +2555,7 @@ pub mod parse_worker {
|| !(task.experimental_decorators || task.emit_decorator_metadata);
opts.features.unwrap_commonjs_packages = topts.unwrap_commonjs_packages;
opts.features.no_macros = topts.no_macros;
opts.unused_import_flags_ts = task.unused_import_flags_ts;
// Modeled as
// `Option<Box<StringSet>>` on both sides, so we deep-clone (small —
// CLI-supplied flag set). PERF: retype
Expand Down Expand Up @@ -2786,6 +2794,7 @@ pub mod parse_worker {
side_effects: task.side_effects,
loader,
package_name: task.package_name,
unused_import_flags_ts: task.unused_import_flags_ts,

// Hash the files in here so that we do it in parallel.
content_hash_for_additional_file: unique_key_for_additional_file.content_hash,
Expand Down
1 change: 1 addition & 0 deletions src/bundler/ServerComponentParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ fn task_callback(
unique_key_for_additional_file: bun_ast::StoreStr::EMPTY,
content_hash_for_additional_file: 0,
package_name: bun_ast::StoreStr::EMPTY,
unused_import_flags_ts: crate::options::TSUnusedImportFlags::empty(),
})
}

Expand Down
Loading
Loading