Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c82b0a4
Bun.Transpiler: default autoImportJSX to true for the automatic runtime
robobun Jul 25, 2026
9046fc6
Address review: cover .transform()/.scan(), update docs, trim comment
robobun Jul 25, 2026
e8aff3d
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 25, 2026
70b1006
Drop multi-line comment per review; add no-arg constructor coverage
robobun Jul 25, 2026
d30e0de
ci: retrigger
robobun Jul 25, 2026
68a560d
docs: clarify autoImportJSX only imports used bindings
robobun Jul 25, 2026
c9e5956
Clarify autoImportJSX JSDoc; make no-arg test NODE_ENV-agnostic
robobun Jul 25, 2026
c3c0717
scanImports: mirror the auto-import gate so it agrees with scan()
robobun Jul 27, 2026
3be402e
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 27, 2026
b541580
Trim multi-line comments; drop bug-history narration in tests
robobun Jul 27, 2026
3e1b0ea
scanImports: honor // @jsxRuntime pragma when deciding whether to inj…
robobun Jul 27, 2026
8643622
scanImports: also apply the @jsxRuntime pragma's development flag; pi…
robobun Jul 27, 2026
a1f3775
Move the needs_jsx_import comment back next to the gate it annotates
robobun Jul 27, 2026
57c7ff1
Merge branch 'main' into farm/5470f44d/transpiler-auto-import-jsx-def…
Jarred-Sumner Aug 7, 2026
2f2125e
scanImports: apply the @jsxImportSource pragma; generalize the mdx op…
robobun Aug 7, 2026
af465bd
Merge branch 'main' into farm/5470f44d/transpiler-auto-import-jsx-def…
robobun Oct 27, 2026
ab74d9b
test: pin autoImportJSX: false where a define test asserts the output…
robobun Oct 27, 2026
06e5a38
Merge remote-tracking branch 'origin/main' into farm/5470f44d/transpi…
robobun Oct 28, 2026
6b4af7d
js_parser: copy the JSX import paths only when an import is emitted
robobun Oct 28, 2026
c22b04a
docs: the JSX runtime import is added, its position is not part of th…
robobun Oct 28, 2026
b6ea63e
js_parser: report the createElement fallback from the scan pass
robobun Oct 28, 2026
02006ca
Bun.Transpiler: scan and scanImports use the logLevel of the transpiler
robobun Oct 28, 2026
d2632a6
test: scan records and tsconfig package for the createElement fallback
robobun Oct 28, 2026
0ce6061
js_parser: one apply_jsx_pragmas for the visit pass and the scan pass
robobun Oct 28, 2026
1d049c2
test: scan records match the imports of transformSync for every row
robobun Oct 28, 2026
28b0e30
js_parser: place apply_jsx_pragmas after prepare_for_visit_pass, shor…
robobun Oct 28, 2026
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
8 changes: 8 additions & 0 deletions docs/runtime/transpiler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const result = transpiler.transformSync(code);
````

```ts output
import { jsxDEV as jsxDEV_7x81h0kn } from "react/jsx-dev-runtime";
import * as whatever from "./whatever.ts";
export function Home(props) {
return jsxDEV_7x81h0kn("p", {
Expand Down Expand Up @@ -216,6 +217,13 @@ interface TranspilerOptions {
// Default: false
trimUnusedImports?: boolean,

// Whether to add the automatic JSX runtime import when JSX is used,
// e.g. `import { jsx } from "react/jsx-runtime"` in production or
// `import { jsxDEV } from "react/jsx-dev-runtime"` in development,
// resolved against the configured jsxImportSource
// Default: true
autoImportJSX?: boolean,

// Whether to enable a set of JSX optimizations
// jsxOptimizationInline ...,

Expand Down
9 changes: 9 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3024,6 +3024,15 @@ declare module "bun" {
*/
macro?: MacroMap;

/**
* When the automatic JSX runtime is in use, add an import for the
* bindings the transformed output actually uses (some of `jsx`, `jsxs`,
* `Fragment` from `"<jsxImportSource>/jsx-runtime"`, or `jsxDEV` from
* `jsx-dev-runtime`) so the output can run standalone. Set `false` to
* suppress the import.
*
* @default true
*/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
autoImportJSX?: boolean;
allowBunRuntime?: boolean;
exports?: {
Expand Down
1 change: 1 addition & 0 deletions src/bundler/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ impl JavaScript {
}

let mut temp_log = bun_ast::Log::init();
temp_log.level = log.level;
// scopeguard cannot capture &mut temp_log while it's used below;
// explicit `append_to` calls at each exit.

Expand Down
121 changes: 64 additions & 57 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,8 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
// "Did we parse anything that looked like JSX"?
// If yes, then automatically add the JSX import.
pub(crate) needs_jsx_import: NeedsJSXType,
// Scan pass only: a `key` after a `{...spread}` imports `createElement` from the package.
pub(crate) needs_jsx_create_element_import: NeedsJSXType,

// The parser does two passes and we need to pass the scope tree information
// from the first pass to the second pass. That's done by tracking the calls
Expand Down Expand Up @@ -3236,63 +3238,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
|| self.esm_export_keyword.len > 0
|| self.top_level_await_keyword.len > 0;

if let Some(factory) = self.lexer.jsx_pragma.jsx() {
// `Span.text` is a `StoreStr` into lexer-owned source; valid for 'a.
let text = factory.text.slice();
match options::JSX::Pragma::member_list_to_components_if_different(
&self.options.jsx.factory,
text,
) {
Some(members) => self.options.jsx.factory = members,
None => self.log().add_range_warning_fmt(
Some(self.source),
factory.range,
format_args!("Invalid JSX factory: \"{}\"", bstr::BStr::new(text)),
),
}
}

if let Some(fragment) = self.lexer.jsx_pragma.jsx_frag() {
// SAFETY: Span.text is `ArenaStr` valid for 'a.
let text = fragment.text.slice();
match options::JSX::Pragma::member_list_to_components_if_different(
&self.options.jsx.fragment,
text,
) {
Some(members) => self.options.jsx.fragment = members,
None => self.log().add_range_warning_fmt(
Some(self.source),
fragment.range,
format_args!("Invalid JSX fragment: \"{}\"", bstr::BStr::new(text)),
),
}
}

if let Some(import_source) = self.lexer.jsx_pragma.jsx_import_source() {
// SAFETY: Span.text is `ArenaStr` valid for 'a.
let text = import_source.text.slice();
self.options.jsx.classic_import_source = text.to_vec().into();
self.options.jsx.package_name = self.options.jsx.classic_import_source.clone();
self.options.jsx.set_import_source();
}

if let Some(runtime) = self.lexer.jsx_pragma.jsx_runtime() {
// SAFETY: Span.text is `ArenaStr` valid for 'a.
let text = runtime.text.slice();
if let Some(jsx_runtime) = options::JSX::RUNTIME_MAP.get(text) {
self.options.jsx.runtime = jsx_runtime.runtime;
if let Some(dev) = jsx_runtime.development {
self.options.jsx.development = dev;
}
} else {
// make this a warning instead of an error because we don't support "preserve" right now
self.log().add_range_warning_fmt(
Some(self.source),
runtime.range,
format_args!("Unsupported JSX runtime: \"{}\"", bstr::BStr::new(text)),
);
}
}
self.apply_jsx_pragmas();

// ECMAScript modules are always interpreted as strict mode. This has to be
// done before "hoistSymbols" because strict mode can alter hoisting (!).
Expand Down Expand Up @@ -3395,6 +3341,66 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
Ok(())
}

/// The visit pass and the scan pass both call this, so both see the same runtime and import source.
pub(crate) fn apply_jsx_pragmas(&mut self) {
if let Some(factory) = self.lexer.jsx_pragma.jsx() {
// `Span.text` is a `StoreStr` into lexer-owned source; valid for 'a.
let text = factory.text.slice();
match options::JSX::Pragma::member_list_to_components_if_different(
&self.options.jsx.factory,
text,
) {
Some(members) => self.options.jsx.factory = members,
None => self.log().add_range_warning_fmt(
Some(self.source),
factory.range,
format_args!("Invalid JSX factory: \"{}\"", bstr::BStr::new(text)),
),
}
}

if let Some(fragment) = self.lexer.jsx_pragma.jsx_frag() {
// SAFETY: Span.text is `ArenaStr` valid for 'a.
let text = fragment.text.slice();
match options::JSX::Pragma::member_list_to_components_if_different(
&self.options.jsx.fragment,
text,
) {
Some(members) => self.options.jsx.fragment = members,
None => self.log().add_range_warning_fmt(
Some(self.source),
fragment.range,
format_args!("Invalid JSX fragment: \"{}\"", bstr::BStr::new(text)),
),
}
}

if let Some(import_source) = self.lexer.jsx_pragma.jsx_import_source() {
// SAFETY: Span.text is `ArenaStr` valid for 'a.
let text = import_source.text.slice();
self.options.jsx.package_name = text.to_vec().into();
self.options.jsx.set_import_source();
}

if let Some(runtime) = self.lexer.jsx_pragma.jsx_runtime() {
// SAFETY: Span.text is `ArenaStr` valid for 'a.
let text = runtime.text.slice();
if let Some(jsx_runtime) = options::JSX::RUNTIME_MAP.get(text) {
self.options.jsx.runtime = jsx_runtime.runtime;
if let Some(dev) = jsx_runtime.development {
self.options.jsx.development = dev;
}
} else {
// make this a warning instead of an error because we don't support "preserve" right now
self.log().add_range_warning_fmt(
Some(self.source),
runtime.range,
format_args!("Unsupported JSX runtime: \"{}\"", bstr::BStr::new(text)),
);
}
}
}

fn ensure_require_symbol(&mut self) {
if !self.runtime_imports.__require.is_empty() {
return;
Expand Down Expand Up @@ -9690,6 +9696,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
module_scope: scope,
scopes_in_order: scope_order,
needs_jsx_import: false,
needs_jsx_create_element_import: false,
lexer,

commonjs_named_exports_deoptimized,
Expand Down
46 changes: 25 additions & 21 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,28 +649,31 @@ impl<'a> Parser<'a> {
}
}

// Symbol use counts are unavailable
// So we say "did we parse any JSX?"
// if yes, just automatically add the import so that .bun knows to include the file.
if p.options.jsx.parse && p.needs_jsx_import {
p.apply_jsx_pragmas();
// No symbol use counts here: one record per import the visit pass emits, in its order.
if p.options.jsx.parse
&& p.options.features.auto_import_jsx
&& p.options.jsx.runtime == options::JSX::Runtime::Automatic
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
{
// `add_import_record` requires `&'a [u8]`, but borrowing
// `p.options` would conflict with `&mut p`, so copy into the arena.
let arena = p.arena;
let import_source: &'a [u8] = arena.alloc_slice_copy(p.options.jsx.import_source());
let classic_import_source: &'a [u8] =
arena.alloc_slice_copy(&p.options.jsx.classic_import_source);
let _ = p.add_import_record(
bun_ast::ImportKind::Require,
bun_ast::Loc { start: 0 },
import_source,
);
// Ensure we have both classic and automatic
// This is to handle cases where they use fragments in the automatic runtime
let _ = p.add_import_record(
bun_ast::ImportKind::Require,
bun_ast::Loc { start: 0 },
classic_import_source,
);
if p.needs_jsx_import {
let import_source: &'a [u8] = arena.alloc_slice_copy(p.options.jsx.import_source());
let _ = p.add_import_record(
bun_ast::ImportKind::Stmt,
bun_ast::Loc { start: 0 },
import_source,
);
}
if p.needs_jsx_create_element_import {
let package_name: &'a [u8] = arena.alloc_slice_copy(&p.options.jsx.package_name);
let _ = p.add_import_record(
bun_ast::ImportKind::Stmt,
bun_ast::Loc { start: 0 },
package_name,
);
}
}

Ok(())
Expand Down Expand Up @@ -2328,14 +2331,14 @@ impl<'a> Parser<'a> {
// out via `take` (it is `Default`) to avoid an overlapping `&self.jsx_imports`
// borrow. The callee never reads `self.jsx_imports`, so the take/restore is
// semantically a no-op.
let import_source: &'a [u8] = p.arena.alloc_slice_copy(p.options.jsx.import_source());
let package_name: &'a [u8] = p.arena.alloc_slice_copy(&p.options.jsx.package_name);
let jsx_imports = core::mem::take(&mut p.jsx_imports);

let mut buf: [&'static [u8]; 3] = [b"", b"", b""];
let runtime_import_names = jsx_imports.runtime_import_names(&mut buf);

if !runtime_import_names.is_empty() {
let import_source: &'a [u8] =
p.arena.alloc_slice_copy(p.options.jsx.import_source());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
p.generate_import_stmt(
Comment thread
robobun marked this conversation as resolved.
Resolved
import_source,
runtime_import_names,
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
Expand All @@ -2351,6 +2354,7 @@ impl<'a> Parser<'a> {

let source_import_names = jsx_imports.source_import_names();
if !source_import_names.is_empty() {
let package_name: &'a [u8] = p.arena.alloc_slice_copy(&p.options.jsx.package_name);
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
p.generate_import_stmt(
package_name,
source_import_names,
Expand Down
13 changes: 9 additions & 4 deletions src/js_parser/parse/parse_jsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if !p.stack_check.is_safe_to_recurse() {
return Err(crate::Error::StackOverflow);
}
if SCAN_ONLY {
p.needs_jsx_import = true;
}

let tag = JSXTag::parse(p)?;

// The tag may have TypeScript type arguments: "<Foo<T>/>"
Expand Down Expand Up @@ -219,6 +215,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if is_key_after_spread {
flags.insert(flags::JSXElement::IsKeyAfterSpread);
}
if SCAN_ONLY {
if is_key_after_spread {
p.needs_jsx_create_element_import = true;
} else {
p.needs_jsx_import = true;
}
}
properties = G::PropertyList::move_from_list(props);
if is_key_after_spread
&& p.options.jsx.runtime == options::JSXRuntime::Automatic
Expand All @@ -231,6 +234,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
);
p.has_classic_runtime_warned = true;
}
} else if SCAN_ONLY {
p.needs_jsx_import = true;
}

// People sometimes try to use the output of "JSON.stringify()" as a JSX
Expand Down
4 changes: 0 additions & 4 deletions src/options_types/jsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ pub struct Pragma {
/// Facilitates automatic JSX importing
/// Set on a per file basis like this:
/// /** @jsxImportSource @emotion/core */
pub classic_import_source: Cow<'static, [u8]>,
pub package_name: Cow<'static, [u8]>,

/// Configuration Priority:
Expand All @@ -182,7 +181,6 @@ impl Default for Pragma {
fragment: MemberList::Static(defaults::FRAGMENT),
runtime: Runtime::Automatic,
import_source: ImportSource::default(),
classic_import_source: Cow::Borrowed(b"react"),
package_name: Cow::Borrowed(b"react"),
development: true,
parse: true,
Expand All @@ -203,7 +201,6 @@ impl Pragma {
}
hasher.update(&self.import_source.development);
hasher.update(&self.import_source.production);
hasher.update(&self.classic_import_source);
hasher.update(&self.package_name);
// `runtime` selects classic vs automatic emission; `development`
// selects `jsx` vs `jsxDEV`. Both shape transpiled output.
Expand Down Expand Up @@ -287,7 +284,6 @@ impl Pragma {
if !jsx.import_source.is_empty() {
pragma.package_name = Cow::Owned(jsx.import_source.into_vec());
pragma.set_import_source();
pragma.classic_import_source = pragma.package_name.clone();
}

pragma.development = jsx.development;
Expand Down
1 change: 1 addition & 0 deletions src/resolver/tsconfig_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@

if self.jsx_flags.contains(JsxField::ImportSource) {
out.import_source = self.jsx.import_source.clone();
out.package_name = self.jsx.package_name.clone();

Check failure on line 244 in src/resolver/tsconfig_json.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

assigning the result of `Clone::clone()` may be inefficient
}

if self.jsx_flags.contains(JsxField::Runtime) {
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/api/JSTranspiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ impl Default for Config {
log: bun_ast::Log::default(), // overwritten at construction
runtime: Runtime::Features {
top_level_await: true,
auto_import_jsx: true,
Comment thread
robobun marked this conversation as resolved.
Resolved
..Default::default()
},
tree_shaking: false,
Expand Down Expand Up @@ -1239,6 +1240,7 @@ impl JSTranspiler {

let arena = Arena::new();
let mut log = bun_ast::Log::init();
log.level = self.config.get().log.level;
// SAFETY: `arena` outlives every use through `self.transpiler` in this fn body;
// `_restore` (declared after `arena`/`log`, so dropped first) restores
// `prev_arena` and `&self.config.log` before either local drops.
Expand Down Expand Up @@ -1533,6 +1535,7 @@ impl JSTranspiler {

let arena = Arena::new();
let mut log = bun_ast::Log::init();
log.level = self.config.get().log.level;
// SAFETY: `arena` outlives every use through `self.transpiler` in this fn body;
// `_restore` (declared after `arena`/`log`, so dropped first) restores
// `prev_arena` and `&self.config.log` before either local drops.
Expand Down Expand Up @@ -1562,6 +1565,7 @@ impl JSTranspiler {
};

let mut opts = bun_js_parser::ParserOptions::init(jsx, loader);
opts.features.auto_import_jsx = self.transpiler.get().options.auto_import_jsx;
// SAFETY: see `transpiler_mut`. The `&mut Transpiler` is reborrowed
// disjointly for `macro_context` (stored in `opts`) and `options.define`
// (raw-addr read) below; both end when `opts` is consumed by `scan()`.
Expand Down
5 changes: 0 additions & 5 deletions src/runtime/api/js_bundle_completion_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,11 +1133,6 @@ impl CompletionStruct for JSBundleCompletionTask {
} else {
std::borrow::Cow::Borrowed(b"react".as_slice())
},
classic_import_source: if !jsx_import.is_empty() {
std::borrow::Cow::Owned(jsx_import.to_vec())
} else {
std::borrow::Cow::Borrowed(b"react".as_slice())
},
side_effects: config.jsx.side_effects,
parse: true,
import_source: options::jsx::ImportSource {
Expand Down
Loading
Loading