Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
6 changes: 6 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,11 @@ interface TranspilerOptions {
// Default: false
trimUnusedImports?: boolean,

// Whether to prepend the automatic JSX runtime import
// (`import { jsx } from "react/jsx-runtime"`) when JSX is used
// Default: true
autoImportJSX?: boolean,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// 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 @@ -2462,6 +2462,15 @@ declare module "bun" {
*/
macro?: MacroMap;

/**
* When the automatic JSX runtime is in use, prepend 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
* suppress the import.
*
* @default true
*/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
autoImportJSX?: boolean;
allowBunRuntime?: boolean;
exports?: {
Expand Down
23 changes: 9 additions & 14 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,28 +480,23 @@
}
}

// 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 {
// Symbol use counts are unavailable, so "any JSX parsed?" is the proxy.
// Mirror the full-parse auto-import gate so scanImports() and scan()
// agree on the injected JSX runtime import.
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated
if p.options.jsx.parse
&& p.needs_jsx_import
&& p.options.features.auto_import_jsx
&& p.options.jsx.runtime == options::JSX::Runtime::Automatic
{
// `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::ImportKind::Stmt,
bun_ast::Loc { start: 0 },
import_source,
);

Check warning on line 499 in src/js_parser/parse/parse_entry.rs

View check run for this annotation

Claude / Claude Code Review

scanImports() misses the createElement ("react") record for key-after-spread JSX, disagreeing with scan()

Dropping the second `add_import_record(.., classic_import_source)` also drops coverage for the key-after-spread `createElement` fallback: for `export default <div {...obj} key="after" />;` under the automatic runtime, `.scan().imports` → `[{path:"react"}]` (per this PR's own "key-after-spread emits createElement with an import" test) while `.scanImports()` → `[{path:"react/jsx-dev-runtime"}]` — adding that case to the new `.scanImports() agrees with .scan()` `it.each` matrix would fail. Since `c
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// 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,
);
}

scan_pass.approximate_newline_count = p.lexer.approximate_newline_count;
Expand Down
2 changes: 2 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,
..Default::default()
},
tree_shaking: false,
Expand Down Expand Up @@ -1743,6 +1744,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
121 changes: 121 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2086,6 +2086,7 @@ export default <>hi</>
"process.env.NODE_ENV": JSON.stringify("development"),
},
logLevel: "error",
autoImportJSX: false,
});

expect(bun.transformSync("console.log(<div key={() => {}} points={() => {}}></div>);")).toBe(
Expand Down Expand Up @@ -2193,6 +2194,124 @@ console.log(<div {...obj} key="after" />);`),
}
});

// https://github.com/oven-sh/bun/issues/7499
describe("autoImportJSX defaults to true for the automatic runtime", () => {
it("no-arg constructor", () => {
expect(new Bun.Transpiler().transformSync("export default <div/>;")).toMatch(
/^import \{ jsx(?:DEV)? as (\w+) \} from "react\/jsx-(?:dev-)?runtime";\nexport default \1\("div",/,
);
});
Comment thread
claude[bot] marked this conversation as resolved.

it("development", () => {
const out = new Bun.Transpiler({
loader: "tsx",
define: { "process.env.NODE_ENV": JSON.stringify("development") },
}).transformSync("export default function App() { return <><div>hi</div></>; }");
expect(out).toMatch(
/^import { jsxDEV as (\w+), Fragment as (\w+) } from "react\/jsx-dev-runtime";\nexport default function App\(\) {\n return \1\(\2,/,
);
});

it("async .transform() also emits the import", async () => {
const out = await new Bun.Transpiler({
loader: "tsx",
define: { "process.env.NODE_ENV": JSON.stringify("development") },
}).transform("export default <div>hi</div>;");
expect(out).toMatch(/^import { jsxDEV as (\w+) } from "react\/jsx-dev-runtime";\nexport default \1\("div",/);
});

it("production", () => {
const out = new Bun.Transpiler({
loader: "tsx",
tsconfig: { compilerOptions: { jsx: "react-jsx" } },
}).transformSync("export default <div>hi</div>;");
expect(out).toMatch(/^import { jsx as (\w+) } from "react\/jsx-runtime";\nexport default \1\("div",/);
});

it("respects jsxImportSource", () => {
const out = new Bun.Transpiler({
loader: "tsx",
tsconfig: { compilerOptions: { jsx: "react-jsx", jsxImportSource: "preact" } },
}).transformSync("export default <div>hi</div>;");
expect(out).toMatch(/^import { jsx as (\w+) } from "preact\/jsx-runtime";\nexport default \1\("div",/);
});

it("key-after-spread emits createElement with an import", () => {
const out = new Bun.Transpiler({
loader: "tsx",
define: { "process.env.NODE_ENV": JSON.stringify("development") },
logLevel: "error",
}).transformSync(`export default <div {...obj} key="after" />;`);
expect(out).toMatch(/^import { createElement as (\w+) } from "react";\nexport default \1\("div",/);
});

it("does not affect the classic runtime", () => {
const out = new Bun.Transpiler({
loader: "tsx",
tsconfig: { compilerOptions: { jsx: "react" } },
}).transformSync("export default <div>hi</div>;");
expect(out).toBe('export default React.createElement("div", null, "hi");\n');
});

it("can still be disabled", () => {
const out = new Bun.Transpiler({
loader: "tsx",
define: { "process.env.NODE_ENV": JSON.stringify("development") },
autoImportJSX: false,
}).transformSync("export default <div>hi</div>;");
expect(out).not.toContain("import");
expect(out).toContain("jsxDEV");
});

it("surfaces the runtime import through .scan()", () => {
const opts = { loader: "tsx", define: { "process.env.NODE_ENV": JSON.stringify("development") } };

expect(new Bun.Transpiler(opts).scan("export default <div/>;").imports).toEqual([
{ kind: "import-statement", path: "react/jsx-dev-runtime" },
]);
expect(new Bun.Transpiler({ ...opts, autoImportJSX: false }).scan("export default <div/>;").imports).toEqual([]);
expect(new Bun.Transpiler(opts).scan("export const x = 1;").imports).toEqual([]);
});

// scanImports() used to unconditionally add a `require-call` record for
// `<importSource>/jsx-dev-runtime` *and* a second one for the classic
// source, regardless of auto_import_jsx or the configured JSX runtime.
// It now mirrors the full-parse gate so it agrees with scan().
Comment thread
robobun marked this conversation as resolved.
Outdated
describe(".scanImports() agrees with .scan() on the injected JSX runtime import", () => {
const dev = { loader: "tsx", define: { "process.env.NODE_ENV": JSON.stringify("development") } };
const jsxDevRuntime = [{ kind: "import-statement", path: "react/jsx-dev-runtime" }];

it.each([
["automatic (dev)", dev, "export default <div/>;", jsxDevRuntime],
["automatic + fragment", dev, "export default <><div/></>;", jsxDevRuntime],
[
"automatic (prod)",
{ loader: "tsx", tsconfig: { compilerOptions: { jsx: "react-jsx" } } },
"export default <div/>;",
[{ kind: "import-statement", path: "react/jsx-runtime" }],
],
[
"automatic + jsxImportSource",
{ loader: "tsx", tsconfig: { compilerOptions: { jsx: "react-jsx", jsxImportSource: "preact" } } },
"export default <div/>;",
[{ kind: "import-statement", path: "preact/jsx-runtime" }],
],
["autoImportJSX: false", { ...dev, autoImportJSX: false }, "export default <div/>;", []],
[
"classic runtime",
{ loader: "tsx", tsconfig: { compilerOptions: { jsx: "react" } } },
"export default <div/>;",
[],
],
["no JSX", dev, "export const x = 1;", []],
])("%s", (_, opts, src, expected) => {
const t = new Bun.Transpiler(opts);
expect(t.scanImports(src)).toEqual(expected);
expect(t.scan(src).imports).toEqual(expected);
});
});
});

it("JSX bare key prop followed by key with a value does not crash", async () => {
await using proc = Bun.spawn({
cmd: [
Expand All @@ -2203,6 +2322,7 @@ console.log(<div {...obj} key="after" />);`),
loader: "jsx",
define: { "process.env.NODE_ENV": JSON.stringify("development") },
logLevel: "error",
autoImportJSX: false,
});
process.stdout.write(t.transformSync('console.log(<div key key="duplicate"></div>);'));
process.stdout.write(t.transformSync('console.log(<div key className="x" key="duplicate"></div>);'));
Expand Down Expand Up @@ -2344,6 +2464,7 @@ console.log(<div {...obj} key="after" />);`),
define: {
"process.env.NODE_ENV": JSON.stringify("development"),
},
autoImportJSX: false,
});
expect(bun.transformSync("export var foo = <div>{...a}b</div>")).toBe(
`export var foo = jsxDEV_7x81h0kn("div", {
Expand Down
Loading