Skip to content
Open
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
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 prepend 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 @@ -2602,6 +2602,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
35 changes: 21 additions & 14 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,28 +485,35 @@ 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 {
if let Some(import_source) = p.lexer.jsx_pragma.jsx_import_source() {
let text = import_source.text.slice();
p.options.jsx.classic_import_source = text.to_vec().into();
p.options.jsx.package_name = p.options.jsx.classic_import_source.clone();
p.options.jsx.set_import_source();
}
if let Some(span) = p.lexer.jsx_pragma.jsx_runtime()
&& let Some(pair) = options::JSX::RUNTIME_MAP.get(span.text.slice())
{
p.options.jsx.runtime = pair.runtime;
if let Some(dev) = pair.development {
p.options.jsx.development = dev;
}
}
// Symbol use counts are unavailable, so "any JSX parsed?" is the proxy.
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,
);
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 @@ -96,6 +96,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 @@ -1754,6 +1755,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
137 changes: 137 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2204,6 +2204,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 @@ -2311,6 +2312,139 @@ 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([]);
});

describe(".scanImports() reports 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/>;",
[],
],
[
"@jsxRuntime automatic over classic tsconfig",
{ ...dev, tsconfig: { compilerOptions: { jsx: "react" } } },
"// @jsxRuntime automatic\nexport default <div/>;",
jsxDevRuntime,
],
Comment thread
robobun marked this conversation as resolved.
["@jsxRuntime classic over automatic", dev, "// @jsxRuntime classic\nexport default <div/>;", []],
[
"@jsxRuntime react-jsxdev over react-jsx tsconfig",
{ loader: "tsx", tsconfig: { compilerOptions: { jsx: "react-jsx" } } },
"// @jsxRuntime react-jsxdev\nexport default <div/>;",
jsxDevRuntime,
],
[
"@jsxImportSource pragma",
dev,
"/** @jsxImportSource preact */\nexport default <div/>;",
[{ kind: "import-statement", path: "preact/jsx-dev-runtime" }],
],
["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 @@ -2321,6 +2455,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 @@ -2462,6 +2597,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 Expand Up @@ -2489,6 +2625,7 @@ console.log(<div {...obj} key="after" />);`),
define: {
"process.env.NODE_ENV": JSON.stringify("development"),
},
autoImportJSX: false,
});
for (const [tag, expected] of [
["Foo-Bar", `"Foo-Bar"`],
Expand Down