/>"
@@ -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
@@ -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
diff --git a/src/options_types/jsx.rs b/src/options_types/jsx.rs
index 0cda8d160553..3f63e0c591db 100644
--- a/src/options_types/jsx.rs
+++ b/src/options_types/jsx.rs
@@ -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:
@@ -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,
@@ -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.
@@ -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;
diff --git a/src/resolver/tsconfig_json.rs b/src/resolver/tsconfig_json.rs
index aab471671832..ba42f9051963 100644
--- a/src/resolver/tsconfig_json.rs
+++ b/src/resolver/tsconfig_json.rs
@@ -241,6 +241,7 @@ impl TSConfigJSON {
if self.jsx_flags.contains(JsxField::ImportSource) {
out.import_source = self.jsx.import_source.clone();
+ out.package_name = self.jsx.package_name.clone();
}
if self.jsx_flags.contains(JsxField::Runtime) {
diff --git a/src/runtime/api/js_bundle_completion_task.rs b/src/runtime/api/js_bundle_completion_task.rs
index b57a80b54751..027e54102cba 100644
--- a/src/runtime/api/js_bundle_completion_task.rs
+++ b/src/runtime/api/js_bundle_completion_task.rs
@@ -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 {
From 02006ca4a05c4535a2f45f4a92d1b02c89fde5ed Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Wed, 28 Oct 2026 12:53:07 +0000
Subject: [PATCH 19/23] Bun.Transpiler: scan and scanImports use the logLevel
of the transpiler
Both built a fresh Log at the default level, so logLevel: "error" did
not stop a warning from throwing. transform and transformSync already
used the configured level.
---
src/bundler/cache.rs | 1 +
src/runtime/api/JSTranspiler.rs | 2 ++
2 files changed, 3 insertions(+)
diff --git a/src/bundler/cache.rs b/src/bundler/cache.rs
index d65b7ad84426..cda055d978e8 100644
--- a/src/bundler/cache.rs
+++ b/src/bundler/cache.rs
@@ -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.
diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs
index 8c615a29d48b..454f4f2f97ed 100644
--- a/src/runtime/api/JSTranspiler.rs
+++ b/src/runtime/api/JSTranspiler.rs
@@ -1240,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.
@@ -1534,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.
From d2632a6f10ee464eeb4560ccfeb5c0c1455e3c75 Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Wed, 28 Oct 2026 12:53:08 +0000
Subject: [PATCH 20/23] test: scan records and tsconfig package for the
createElement fallback
---
test/bundler/transpiler/transpiler.test.js | 60 ++++++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js
index 79abe3fe1e8c..ef92608a8f5c 100644
--- a/test/bundler/transpiler/transpiler.test.js
+++ b/test/bundler/transpiler/transpiler.test.js
@@ -2960,6 +2960,17 @@ console.log();`),
expect(out).toMatch(/^import { createElement as (\w+) } from "react";\nexport default \1\("div",/);
});
+ it("key-after-spread imports createElement from the tsconfig jsxImportSource", () => {
+ const out = new Bun.Transpiler({
+ loader: "tsx",
+ tsconfig: { compilerOptions: { jsx: "react-jsx", jsxImportSource: "preact" } },
+ logLevel: "error",
+ }).transformSync(`export default
;`);
+ expect(out).toMatch(
+ /^import { jsx as (\w+) } from "preact\/jsx-runtime";\nimport { createElement as (\w+) } from "preact";\nexport default \2\("div",/,
+ );
+ });
+
it("does not affect the classic runtime", () => {
const out = new Bun.Transpiler({
loader: "tsx",
@@ -2988,6 +2999,17 @@ console.log();`),
expect(new Bun.Transpiler(opts).scan("export const x = 1;").imports).toEqual([]);
});
+ it(".scan() and .scanImports() honour logLevel like .transformSync()", () => {
+ const src = `export default ;`;
+ const loud = new Bun.Transpiler({ loader: "tsx" });
+ expect(() => loud.scan(src)).toThrow('"key" prop after a {...spread} is deprecated in JSX');
+ expect(() => loud.scanImports(src)).toThrow('"key" prop after a {...spread} is deprecated in JSX');
+
+ const quiet = new Bun.Transpiler({ loader: "tsx", logLevel: "error" });
+ expect(quiet.scan(src).imports).toEqual([{ kind: "import-statement", path: "react" }]);
+ expect(quiet.scanImports(src)).toEqual([{ kind: "import-statement", path: "react" }]);
+ });
+
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" }];
@@ -3034,6 +3056,44 @@ console.log();`),
[{ kind: "import-statement", path: "preact/jsx-dev-runtime" }],
],
["no JSX", dev, "export const x = 1;", []],
+ // A key after a spread falls back to createElement from the package.
+ [
+ "createElement fallback alone",
+ { ...dev, logLevel: "error" },
+ `export default ;`,
+ [{ kind: "import-statement", path: "react" }],
+ ],
+ [
+ "createElement fallback with a child element",
+ { ...dev, logLevel: "error" },
+ `export default
;`,
+ [...jsxDevRuntime, { kind: "import-statement", path: "react" }],
+ ],
+ [
+ "createElement fallback with jsxImportSource",
+ {
+ loader: "tsx",
+ tsconfig: { compilerOptions: { jsx: "react-jsx", jsxImportSource: "preact" } },
+ logLevel: "error",
+ },
+ `export default
;`,
+ [
+ { kind: "import-statement", path: "preact/jsx-runtime" },
+ { kind: "import-statement", path: "preact" },
+ ],
+ ],
+ [
+ "createElement fallback with @jsxImportSource pragma",
+ { ...dev, logLevel: "error" },
+ `/** @jsxImportSource preact */\nexport default ;`,
+ [{ kind: "import-statement", path: "preact" }],
+ ],
+ [
+ "createElement fallback under the classic runtime",
+ { loader: "tsx", tsconfig: { compilerOptions: { jsx: "react" } }, logLevel: "error" },
+ `export default ;`,
+ [],
+ ],
])("%s", (_, opts, src, expected) => {
const t = new Bun.Transpiler(opts);
expect(t.scanImports(src)).toEqual(expected);
From 0ce60616b01a11a89b67f4727801571df8f4bfd6 Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Wed, 28 Oct 2026 15:39:00 +0000
Subject: [PATCH 21/23] js_parser: one apply_jsx_pragmas for the visit pass and
the scan pass
The scan pass had a trimmed copy of the pragma code. It skipped the
factory and fragment pragmas and the warning for an unsupported
runtime, so scanImports accepted "@jsxRuntime preserve" while
transformSync and scan rejected it.
---
src/js_parser/p.rs | 104 +++++++++++++++--------------
src/js_parser/parse/parse_entry.rs | 14 +---
2 files changed, 56 insertions(+), 62 deletions(-)
diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs
index 9625fbdd470a..45a18483601d 100644
--- a/src/js_parser/p.rs
+++ b/src/js_parser/p.rs
@@ -3190,55 +3190,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}
- pub(crate) fn prepare_for_visit_pass(&mut self) -> Result<(), crate::Error> {
- {
- // The wrapper stores only the arena and a non-capturing
- // fn-pointer trampoline; the `*mut P` context is supplied *at call
- // time* (see `Binding::to_expr`) so the raw pointer's provenance is
- // a child of the live `&mut P` at the call site rather than a stale
- // tag captured here. The transposer shims need no wiring at all —
- // call sites invoke `P::maybe_transpose_if_*` etc. directly.
- self.to_expr_wrapper_namespace =
- bun_ast::binding::ToExprWrapper::new(self.arena, |ctx, loc, ref_| {
- // SAFETY: `ctx` was derived from the caller's live `&mut P`
- // immediately before `Binding::to_expr`; no other `&mut P`
- // borrow is active for the duration of this call.
- let p = unsafe { &mut *ctx.cast::>() };
- p.wrap_identifier_namespace(loc, ref_)
- });
- self.to_expr_wrapper_hoisted =
- bun_ast::binding::ToExprWrapper::new(self.arena, |ctx, loc, ref_| {
- // SAFETY: same as above.
- let p = unsafe { &mut *ctx.cast::
>() };
- p.wrap_identifier_hoisting(loc, ref_)
- });
- }
-
- {
- // Compact `scopes_in_order` (parse pass leaves None holes from
- // popAndDiscardScope) into a dense bump-slice for the visit pass.
- let mut buf =
- BumpVec::>::with_capacity_in(self.scopes_in_order.len(), self.arena);
- for item_ in self.scopes_in_order.iter().flatten() {
- buf.push(*item_);
- }
- // `into_bump_slice()` leaks the BumpVec into the arena and returns
- // a `&'a [T]` for that allocation.
- self.scope_order_to_visit = buf.into_bump_slice();
- }
-
- self.is_file_considered_to_have_esm_exports = !self.top_level_await_keyword.is_empty()
- || !self.esm_export_keyword.is_empty()
- || self.options.module_type == options::ModuleType::Esm;
-
- self.push_scope_for_visit_pass(js_ast::scope::Kind::Entry, loc_module_scope)?;
- self.fn_or_arrow_data_visit.is_outside_fn_or_arrow = true;
- self.module_scope = self.current_scope;
- self.has_es_module_syntax = self.has_es_module_syntax
- || self.esm_import_keyword.len > 0
- || self.esm_export_keyword.len > 0
- || self.top_level_await_keyword.len > 0;
-
+ /// Applies the `@jsx*` pragmas of the file to `options.jsx`. The visit pass
+ /// and the scan pass both call it, 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();
@@ -3295,6 +3249,58 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
);
}
}
+ }
+
+ pub(crate) fn prepare_for_visit_pass(&mut self) -> Result<(), crate::Error> {
+ {
+ // The wrapper stores only the arena and a non-capturing
+ // fn-pointer trampoline; the `*mut P` context is supplied *at call
+ // time* (see `Binding::to_expr`) so the raw pointer's provenance is
+ // a child of the live `&mut P` at the call site rather than a stale
+ // tag captured here. The transposer shims need no wiring at all —
+ // call sites invoke `P::maybe_transpose_if_*` etc. directly.
+ self.to_expr_wrapper_namespace =
+ bun_ast::binding::ToExprWrapper::new(self.arena, |ctx, loc, ref_| {
+ // SAFETY: `ctx` was derived from the caller's live `&mut P`
+ // immediately before `Binding::to_expr`; no other `&mut P`
+ // borrow is active for the duration of this call.
+ let p = unsafe { &mut *ctx.cast::>() };
+ p.wrap_identifier_namespace(loc, ref_)
+ });
+ self.to_expr_wrapper_hoisted =
+ bun_ast::binding::ToExprWrapper::new(self.arena, |ctx, loc, ref_| {
+ // SAFETY: same as above.
+ let p = unsafe { &mut *ctx.cast::
>() };
+ p.wrap_identifier_hoisting(loc, ref_)
+ });
+ }
+
+ {
+ // Compact `scopes_in_order` (parse pass leaves None holes from
+ // popAndDiscardScope) into a dense bump-slice for the visit pass.
+ let mut buf =
+ BumpVec::>::with_capacity_in(self.scopes_in_order.len(), self.arena);
+ for item_ in self.scopes_in_order.iter().flatten() {
+ buf.push(*item_);
+ }
+ // `into_bump_slice()` leaks the BumpVec into the arena and returns
+ // a `&'a [T]` for that allocation.
+ self.scope_order_to_visit = buf.into_bump_slice();
+ }
+
+ self.is_file_considered_to_have_esm_exports = !self.top_level_await_keyword.is_empty()
+ || !self.esm_export_keyword.is_empty()
+ || self.options.module_type == options::ModuleType::Esm;
+
+ self.push_scope_for_visit_pass(js_ast::scope::Kind::Entry, loc_module_scope)?;
+ self.fn_or_arrow_data_visit.is_outside_fn_or_arrow = true;
+ self.module_scope = self.current_scope;
+ self.has_es_module_syntax = self.has_es_module_syntax
+ || self.esm_import_keyword.len > 0
+ || self.esm_export_keyword.len > 0
+ || self.top_level_await_keyword.len > 0;
+
+ 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 (!).
diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs
index d08436c5128e..c4fe2efc97be 100644
--- a/src/js_parser/parse/parse_entry.rs
+++ b/src/js_parser/parse/parse_entry.rs
@@ -649,19 +649,7 @@ impl<'a> Parser<'a> {
}
}
- if let Some(import_source) = p.lexer.jsx_pragma.jsx_import_source() {
- let text = import_source.text.slice();
- p.options.jsx.package_name = text.to_vec().into();
- 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;
- }
- }
+ p.apply_jsx_pragmas();
// Symbol use counts are unavailable, so "any JSX parsed?" is the proxy.
// The records follow the order of the imports that the visit pass emits.
if p.options.jsx.parse
From 1d049c2623993dc0ff65c2bfe371a0dad8547793 Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Wed, 28 Oct 2026 15:39:03 +0000
Subject: [PATCH 22/23] test: scan records match the imports of transformSync
for every row
---
test/bundler/transpiler/transpiler.test.js | 24 +++++++++++++++++-----
1 file changed, 19 insertions(+), 5 deletions(-)
diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js
index ef92608a8f5c..b7aa3fd42cc6 100644
--- a/test/bundler/transpiler/transpiler.test.js
+++ b/test/bundler/transpiler/transpiler.test.js
@@ -2999,13 +2999,11 @@ console.log();`),
expect(new Bun.Transpiler(opts).scan("export const x = 1;").imports).toEqual([]);
});
- it(".scan() and .scanImports() honour logLevel like .transformSync()", () => {
+ it(".scan() and .scanImports() use logLevel like .transformSync()", () => {
+ // The key after the spread logs a warning. logLevel: "error" drops it.
const src = `export default ;`;
- const loud = new Bun.Transpiler({ loader: "tsx" });
- expect(() => loud.scan(src)).toThrow('"key" prop after a {...spread} is deprecated in JSX');
- expect(() => loud.scanImports(src)).toThrow('"key" prop after a {...spread} is deprecated in JSX');
-
const quiet = new Bun.Transpiler({ loader: "tsx", logLevel: "error" });
+ expect(quiet.transformSync(src)).toContain('from "react";');
expect(quiet.scan(src).imports).toEqual([{ kind: "import-statement", path: "react" }]);
expect(quiet.scanImports(src)).toEqual([{ kind: "import-statement", path: "react" }]);
});
@@ -3055,6 +3053,19 @@ console.log();`),
"/** @jsxImportSource preact */\nexport default ;",
[{ kind: "import-statement", path: "preact/jsx-dev-runtime" }],
],
+ [
+ "@jsxImportSource pragma over tsconfig jsxImportSource",
+ { loader: "tsx", tsconfig: { compilerOptions: { jsx: "react-jsx", jsxImportSource: "preact" } } },
+ "/** @jsxImportSource solid-js */\nexport default ;",
+ [{ kind: "import-statement", path: "solid-js/jsx-runtime" }],
+ ],
+ // "preserve" is not supported. Both passes warn and keep the configured runtime.
+ [
+ "@jsxRuntime preserve",
+ { ...dev, logLevel: "error" },
+ "// @jsxRuntime preserve\nexport default ;",
+ jsxDevRuntime,
+ ],
["no JSX", dev, "export const x = 1;", []],
// A key after a spread falls back to createElement from the package.
[
@@ -3098,6 +3109,9 @@ console.log();`),
const t = new Bun.Transpiler(opts);
expect(t.scanImports(src)).toEqual(expected);
expect(t.scan(src).imports).toEqual(expected);
+ // The scan records name the modules that the output imports, in order.
+ const imported = [...t.transformSync(src).matchAll(/^import .* from "([^"]+)";$/gm)].map(m => m[1]);
+ expect(imported).toEqual(expected.map(r => r.path));
});
});
});
From 28b0e3069e63f1829904e3e4b2a1459b54e4dedf Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Wed, 28 Oct 2026 18:32:17 +0000
Subject: [PATCH 23/23] js_parser: place apply_jsx_pragmas after
prepare_for_visit_pass, shorter comments
---
src/js_parser/p.rs | 124 ++++++++++++++---------------
src/js_parser/parse/parse_entry.rs | 3 +-
2 files changed, 62 insertions(+), 65 deletions(-)
diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs
index 45a18483601d..fe44208b3723 100644
--- a/src/js_parser/p.rs
+++ b/src/js_parser/p.rs
@@ -490,8 +490,7 @@ 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` prop after a `{...spread}` makes the visit pass
- // call `createElement` from the package instead of the runtime module.
+ // 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
@@ -3190,67 +3189,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}
- /// Applies the `@jsx*` pragmas of the file to `options.jsx`. The visit pass
- /// and the scan pass both call it, 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)),
- );
- }
- }
- }
-
pub(crate) fn prepare_for_visit_pass(&mut self) -> Result<(), crate::Error> {
{
// The wrapper stores only the arena and a non-capturing
@@ -3403,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;
diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs
index c4fe2efc97be..181650f8c545 100644
--- a/src/js_parser/parse/parse_entry.rs
+++ b/src/js_parser/parse/parse_entry.rs
@@ -650,8 +650,7 @@ impl<'a> Parser<'a> {
}
p.apply_jsx_pragmas();
- // Symbol use counts are unavailable, so "any JSX parsed?" is the proxy.
- // The records follow the order of the imports that the visit pass emits.
+ // 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