diff --git a/docs/runtime/transpiler.mdx b/docs/runtime/transpiler.mdx index 02a3b3fccb08..c1e215cd75f4 100644 --- a/docs/runtime/transpiler.mdx +++ b/docs/runtime/transpiler.mdx @@ -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", { @@ -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 ..., diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 4c756a6d4c31..07e1688b0bc9 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -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 `"/jsx-runtime"`, or `jsxDEV` from + * `jsx-dev-runtime`) so the output can run standalone. Set `false` to + * suppress the import. + * + * @default true + */ autoImportJSX?: boolean; allowBunRuntime?: boolean; exports?: { diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 87c89136c8a7..f4daab207d95 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -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, ); - // 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; diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index 7c643d8aa462..d9fdb35cdb42 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -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, @@ -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()`. diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 75ff51849b6f..37a37ba85f39 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2204,6 +2204,7 @@ export default <>hi "process.env.NODE_ENV": JSON.stringify("development"), }, logLevel: "error", + autoImportJSX: false, }); expect(bun.transformSync("console.log(
{}} points={() => {}}>
);")).toBe( @@ -2311,6 +2312,139 @@ console.log(
);`), } }); + // 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
;")).toMatch( + /^import \{ jsx(?:DEV)? as (\w+) \} from "react\/jsx-(?:dev-)?runtime";\nexport default \1\("div",/, + ); + }); + + it("development", () => { + const out = new Bun.Transpiler({ + loader: "tsx", + define: { "process.env.NODE_ENV": JSON.stringify("development") }, + }).transformSync("export default function App() { return <>
hi
; }"); + 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
hi
;"); + 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
hi
;"); + 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
hi
;"); + 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
;`); + 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
hi
;"); + 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
hi
;"); + 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
;").imports).toEqual([ + { kind: "import-statement", path: "react/jsx-dev-runtime" }, + ]); + expect(new Bun.Transpiler({ ...opts, autoImportJSX: false }).scan("export default
;").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
;", jsxDevRuntime], + ["automatic + fragment", dev, "export default <>
;", jsxDevRuntime], + [ + "automatic (prod)", + { loader: "tsx", tsconfig: { compilerOptions: { jsx: "react-jsx" } } }, + "export default
;", + [{ kind: "import-statement", path: "react/jsx-runtime" }], + ], + [ + "automatic + jsxImportSource", + { loader: "tsx", tsconfig: { compilerOptions: { jsx: "react-jsx", jsxImportSource: "preact" } } }, + "export default
;", + [{ kind: "import-statement", path: "preact/jsx-runtime" }], + ], + ["autoImportJSX: false", { ...dev, autoImportJSX: false }, "export default
;", []], + [ + "classic runtime", + { loader: "tsx", tsconfig: { compilerOptions: { jsx: "react" } } }, + "export default
;", + [], + ], + [ + "@jsxRuntime automatic over classic tsconfig", + { ...dev, tsconfig: { compilerOptions: { jsx: "react" } } }, + "// @jsxRuntime automatic\nexport default
;", + jsxDevRuntime, + ], + ["@jsxRuntime classic over automatic", dev, "// @jsxRuntime classic\nexport default
;", []], + [ + "@jsxRuntime react-jsxdev over react-jsx tsconfig", + { loader: "tsx", tsconfig: { compilerOptions: { jsx: "react-jsx" } } }, + "// @jsxRuntime react-jsxdev\nexport default
;", + jsxDevRuntime, + ], + [ + "@jsxImportSource pragma", + dev, + "/** @jsxImportSource preact */\nexport default
;", + [{ 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: [ @@ -2321,6 +2455,7 @@ console.log(
);`), loader: "jsx", define: { "process.env.NODE_ENV": JSON.stringify("development") }, logLevel: "error", + autoImportJSX: false, }); process.stdout.write(t.transformSync('console.log(
);')); process.stdout.write(t.transformSync('console.log(
);')); @@ -2462,6 +2597,7 @@ console.log(
);`), define: { "process.env.NODE_ENV": JSON.stringify("development"), }, + autoImportJSX: false, }); expect(bun.transformSync("export var foo =
{...a}b
")).toBe( `export var foo = jsxDEV_7x81h0kn("div", { @@ -2489,6 +2625,7 @@ console.log(
);`), define: { "process.env.NODE_ENV": JSON.stringify("development"), }, + autoImportJSX: false, }); for (const [tag, expected] of [ ["Foo-Bar", `"Foo-Bar"`],