Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
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
36 changes: 36 additions & 0 deletions docs/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,42 @@ 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. 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.

- `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"`.
- Bun reads the option from the `tsconfig.json` nearest to each file. Bun does not apply the option to files in `node_modules` or 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.

The module of an import that Bun keeps must exist. `import { type Handler } from "aws-lambda"` fails when only `@types/aws-lambda` is installed. Write `import type` for an import that you need only for its types. [`bun check`](/runtime/check) reports a type that you import without `type`.

The two settings that `verbatimModuleSyntax` replaced still work: `"importsNotUsedAsValues": "preserve"` keeps the statement, and `"preserveValueImports": true` keeps the unused names.

To run a project without this behavior, point Bun at a `tsconfig.json` that turns the option off.

```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
```

## 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
5 changes: 5 additions & 0 deletions src/ast/s.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +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
/// A TypeScript source had a `{ ... }` clause here. Read only while `items` is
/// empty, where it tells `import {} from 'path'` (type-only to TypeScript, printed
/// with its braces when kept) from `import 'path'`. Never set for JavaScript.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub has_items_clause: bool, // = false
}

impl Default for Import {
Expand All @@ -226,6 +230,7 @@ impl Default for Import {
import_record_index: u32::MAX,
is_single_line: false,
phase_defer: false,
has_items_clause: false,
}
}
}
Expand Down
5 changes: 5 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 @@ -292,6 +293,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 +350,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 +637,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 +2552,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
22 changes: 22 additions & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@ impl<'a> BundleV2<'a> {
self.graph.path_to_source_index_map(target)
}

/// `ParseTask::unused_import_flags_ts` for a task that no resolver result describes: the
/// setting of the tsconfig.json of the top-level directory, and none inside node_modules.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) fn root_unused_import_flags_ts(
&mut self,
target: options::Target,
path: &crate::bun_fs::Path<'_>,
) -> options::TSUnusedImportFlags {
if path.is_node_module() {
return options::TSUnusedImportFlags::empty();
}
self.transpiler_for_target(target)
.options
.unused_import_flags_ts
}

pub(crate) fn transpiler_for_target(&mut self, target: options::Target) -> &mut Transpiler<'a> {
// SAFETY: all three pointers are live for `'a` (set in `init`); the
// `client_transpiler` arm is only reached when bake populated it.
Expand Down Expand Up @@ -4218,6 +4233,7 @@ pub mod bv2_impl {
} else {
self.transpiler_for_target(known_target).options.jsx.clone()
};
let unused_import_flags_ts = self.root_unused_import_flags_ts(known_target, &task_path);
// SAFETY: arena (`self.graph.heap`) outlives the bundle pass; coerce the
// `&mut ParseTask` to `*mut` immediately so the `&self` borrow from
// `arena()` ends before we take `&mut self` below.
Expand All @@ -4228,6 +4244,7 @@ pub mod bv2_impl {
jsx,
source_index: bun_ast::Index::init(source_index.get()),
module_type,
unused_import_flags_ts,
emit_decorator_metadata: false, // TODO
package_version: bun_ast::StoreStr::EMPTY,
loader: Some(loader),
Expand Down Expand Up @@ -5427,6 +5444,10 @@ pub mod bv2_impl {
..Default::default()
})
.expect("unreachable");
let unused_import_flags_ts = this.root_unused_import_flags_ts(
resolve.import_record.original_target,
&path,
);
let task_val = ParseTask {
// SAFETY: `from_mut(this)` is the live bundle (write provenance);
// outlives the task.
Expand All @@ -5450,6 +5471,7 @@ pub mod bv2_impl {
.clone(),
source_index: bun_ast::Index::init(source_index.get()),
module_type: options::ModuleType::Unknown,
unused_import_flags_ts,
loader: Some(loader),
known_target: resolve.import_record.original_target,
is_entry_point: resolve.import_record.kind
Expand Down
6 changes: 6 additions & 0 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub(crate) fn validate_path(
// the SAME nominal type and `ParseTask::run_with_source_code` can hand
// `&transpiler.options.allow_unresolved` straight through.
pub use bun_js_parser::options::AllowUnresolved;
pub use bun_js_parser::options::TSUnusedImportFlags;

// Canonical defs live in `bun_resolver::options` (lower tier; resolver is the
// runtime consumer of `.patterns`/`.abs_paths`/`.node_modules`). Re-export so
Expand Down Expand Up @@ -1220,6 +1221,9 @@ pub struct BundleOptions<'a> {
pub allow_runtime: bool,

pub trim_unused_imports: Option<bool>,
/// Of the tsconfig.json of the top-level directory, for a source that no directory holds.
/// A file takes `Resolver::unused_import_flags_ts` of its own directory.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub unused_import_flags_ts: TSUnusedImportFlags,
pub(crate) mark_builtins_as_external: bool,
pub server_components: bool,
pub hot_module_reloading: bool,
Expand Down Expand Up @@ -1477,6 +1481,7 @@ impl<'a> BundleOptions<'a> {
auto_import_jsx: self.auto_import_jsx,
allow_runtime: self.allow_runtime,
trim_unused_imports: self.trim_unused_imports,
unused_import_flags_ts: self.unused_import_flags_ts,
mark_builtins_as_external: self.mark_builtins_as_external,
server_components: self.server_components,
hot_module_reloading: self.hot_module_reloading,
Expand Down Expand Up @@ -1746,6 +1751,7 @@ impl<'a> BundleOptions<'a> {
auto_import_jsx: true,
allow_runtime: true,
trim_unused_imports: None,
unused_import_flags_ts: TSUnusedImportFlags::empty(),
mark_builtins_as_external: false,
server_components: false,
hot_module_reloading: false,
Expand Down
13 changes: 11 additions & 2 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,8 @@ impl<'a> Transpiler<'a> {
// Most of the time, this will already be cached
let top_level_dir = self.fs().top_level_dir();
if let Ok(Some(root_dir)) = self.resolver.read_dir_info(top_level_dir) {
self.options.unused_import_flags_ts =
self.resolver.unused_import_flags_ts(&root_dir);
if let Some(tsconfig) = self.resolver.tsconfig_json(&root_dir) {
// If we don't explicitly pass JSX, try to get it from the root tsconfig
if self.options.transform_options.jsx.is_none() {
Expand Down Expand Up @@ -881,13 +883,17 @@ pub struct ParseOptions<'a, 'b> {
pub replace_exports: bun_collections::StringArrayHashMap<bun_ast::runtime::ReplaceableExport>,
pub inject_jest_globals: bool,
pub set_breakpoint_on_first_line: bool,
/// `Resolver::unused_import_flags_ts` for the directory of `path`.
pub unused_import_flags_ts: options::TSUnusedImportFlags,
pub emit_decorator_metadata: bool,
pub experimental_decorators: bool,
pub use_define_for_class_fields: bool,
pub remove_cjs_module_wrapper: bool,

pub dont_bundle_twice: bool,
pub allow_commonjs: bool,
/// The output is text that Bun does not link (`bun build --no-bundle`).
pub transform_only: bool,
/// `"type"` from `package.json`. Used to make sure the parser defaults
/// to CommonJS or ESM based on what the package.json says, when it
/// doesn't otherwise know from reading the source code.
Expand Down Expand Up @@ -1467,7 +1473,7 @@ impl<'a> Transpiler<'a> {
keep_names: true,
ignore_dce_annotations: self.options.ignore_dce_annotations,
keep_directives: false,
preserve_unused_imports_ts: false,
unused_import_flags_ts: this_parse.unused_import_flags_ts,
use_define_for_class_fields: this_parse.use_define_for_class_fields,
suppress_warnings_about_weird_code: true,
features: js_ast::RuntimeFeatures::default(),
Expand All @@ -1481,7 +1487,7 @@ impl<'a> Transpiler<'a> {
module_type: to_parser_module_type(this_parse.module_type),
jsc_builtin_syntax: false,
output_format: p_opts::Format::Esm,
transform_only: self.options.transform_only,
transform_only: this_parse.transform_only,
import_meta_main_value: None,
lower_import_meta_main_for_node_js: false,
framework: None,
Expand Down Expand Up @@ -2838,6 +2844,7 @@ impl<'a> Transpiler<'a> {
// the option fields needed for `ParseOptions` first.
let jsx = jsx_pragma_from_resolver(&resolve_result.jsx);
let dirname_fd = resolve_result.dirname_fd;
let unused_import_flags_ts = resolve_result.flags.unused_import_flags_ts();
let emit_decorator_metadata = resolve_result.flags.emit_decorator_metadata();
let experimental_decorators = resolve_result.flags.experimental_decorators();
let use_define_for_class_fields =
Expand Down Expand Up @@ -2868,6 +2875,7 @@ impl<'a> Transpiler<'a> {
macro_remappings,
macro_js_ctx: default_macro_js_value(),
jsx,
unused_import_flags_ts,
emit_decorator_metadata,
experimental_decorators,
use_define_for_class_fields,
Expand All @@ -2878,6 +2886,7 @@ impl<'a> Transpiler<'a> {
remove_cjs_module_wrapper: false,
dont_bundle_twice: false,
allow_commonjs: false,
transform_only: self.options.transform_only,
module_type: options::ModuleType::Unknown,
runtime_transpiler_cache: None,
keep_json_and_toml_as_one_statement: false,
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/lower/lower_esm_exports_hmr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ impl<'a> ConvertESMExportsForHmr<'a> {
namespace_ref,
star_name_loc,
phase_defer: false,
has_items_clause: false,
},
loc,
));
Expand Down
15 changes: 15 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2490,6 +2490,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
default_name: None,
star_name_loc: bun_ast::Loc::EMPTY,
phase_defer: false,
has_items_clause: false,
},
bun_ast::Loc::default(),
);
Expand Down Expand Up @@ -2660,6 +2661,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
default_name: None,
star_name_loc: bun_ast::Loc::EMPTY,
phase_defer: false,
has_items_clause: false,
},
bun_ast::Loc::EMPTY,
)
Expand Down Expand Up @@ -4113,12 +4115,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// Only assert above; do not actually pop.
}

/// `starts_unused`: the record is created with `IS_UNUSED` set, and stays so unless
/// `ImportScanner::scan` keeps the statement.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) fn process_import_statement(
&mut self,
stmt_: S::Import,
path: ParsedPath<'a>,
loc: bun_ast::Loc,
was_originally_bare_import: bool,
starts_unused: bool,
) -> Result<Stmt, crate::Error> {
let is_macro =
Self::ALLOW_MACROS && (path.is_macro || crate::Macro::is_macro_path(path.text));
Expand Down Expand Up @@ -4251,6 +4256,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.import_records.items_mut()[stmt.import_record_index as usize]
.flags
.set(bun_ast::ImportRecordFlags::PHASE_DEFER, stmt.phase_defer);
self.import_records.items_mut()[stmt.import_record_index as usize]
.flags
.set(bun_ast::ImportRecordFlags::IS_UNUSED, starts_unused);

if let Some(star) = stmt.star_name_loc.to_nullable() {
let name = self.load_name_from_ref(stmt.namespace_ref);
Expand Down Expand Up @@ -9615,6 +9623,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
opts.features.emit_decorator_metadata = false;
}

// Every parse passes here, so this is where a declaration file loses the flags.
if !opts.unused_import_flags_ts.is_empty()
&& options::TSUnusedImportFlags::is_declaration_file(source.path.text)
{
opts.unused_import_flags_ts = options::TSUnusedImportFlags::empty();
}

let unwrap_all_requires = 'brk: {
if opts.bundle && opts.output_format != options::Format::Cjs {
// `bun_paths::fs::Path<'static>` is the
Expand Down
Loading
Loading