From 4013c2e4a631c0ad173d79bac8b086a2bf456607 Mon Sep 17 00:00:00 2001 From: Amir Saifutdinov Date: Fri, 9 Oct 2026 23:23:19 +0300 Subject: [PATCH 1/5] fix(repack): make SWC JSX development transforms follow the build mode getSwcLoaderOptions tied jsc.transform.react.development to the JSX runtime, so development builds with the default automatic runtime emitted `jsx` instead of `jsxDEV` and lost __source/__self, accurate component stacks and "open in editor", while classic production builds kept them. Config helpers don't know the mode, so RepackPlugin now sets `development` from `mode` for every builtin:swc-loader rule that doesn't set it explicitly. getSwcLoaderOptions and getJsTransformRules accept a `development` option to override it. Part of #1422 --- .changeset/swc-jsx-development-mode.md | 5 ++ packages/repack/src/plugins/RepackPlugin.ts | 6 ++ .../src/plugins/utils/setSwcJsxDevelopment.ts | 73 +++++++++++++++++++ .../repack/src/utils/getJsTransformRules.ts | 2 + .../repack/src/utils/getSwcLoaderOptions.ts | 18 ++++- .../src/plugins/RepackPlugin.test.ts | 73 +++++++++++++++++++ website/src/latest/api/plugins/repack.md | 2 + .../api/utils/get-js-transform-rules.md | 2 + .../api/utils/get-swc-loader-options.md | 10 +++ 9 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 .changeset/swc-jsx-development-mode.md create mode 100644 packages/repack/src/plugins/utils/setSwcJsxDevelopment.ts create mode 100644 tests/integration/src/plugins/RepackPlugin.test.ts diff --git a/.changeset/swc-jsx-development-mode.md b/.changeset/swc-jsx-development-mode.md new file mode 100644 index 000000000..4d3a83e6e --- /dev/null +++ b/.changeset/swc-jsx-development-mode.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Make SWC JSX development transforms follow the build mode. `getSwcLoaderOptions` (and `getJsTransformRules`) tied `jsc.transform.react.development` to the JSX runtime, so development builds with the default `automatic` runtime emitted `jsx` instead of `jsxDEV` and lost `__source` / `__self`, accurate component stacks and "open in editor", while `classic` production builds kept them. `RepackPlugin` now sets it from `mode` for every `builtin:swc-loader` rule that doesn't set it explicitly, and both helpers accept a `development` option to override it. diff --git a/packages/repack/src/plugins/RepackPlugin.ts b/packages/repack/src/plugins/RepackPlugin.ts index 9d6c7db00..61b31331d 100644 --- a/packages/repack/src/plugins/RepackPlugin.ts +++ b/packages/repack/src/plugins/RepackPlugin.ts @@ -7,6 +7,7 @@ import { NativeEntryPlugin } from './NativeEntryPlugin/index.js'; import { OutputPlugin, type OutputPluginConfig } from './OutputPlugin/index.js'; import { RepackTargetPlugin } from './RepackTargetPlugin/index.js'; import { SourceMapPlugin } from './SourceMapPlugin.js'; +import { setSwcJsxDevelopment } from './utils/setSwcJsxDevelopment.js'; /** * {@link RepackPlugin} configuration options. @@ -107,6 +108,11 @@ export class RepackPlugin { __DEV__: JSON.stringify(compiler.options.mode === 'development'), }).apply(compiler); + setSwcJsxDevelopment( + compiler.options.module.rules, + compiler.options.mode === 'development' + ); + new BabelPlugin().apply(compiler); new OutputPlugin({ diff --git a/packages/repack/src/plugins/utils/setSwcJsxDevelopment.ts b/packages/repack/src/plugins/utils/setSwcJsxDevelopment.ts new file mode 100644 index 000000000..6d5567de5 --- /dev/null +++ b/packages/repack/src/plugins/utils/setSwcJsxDevelopment.ts @@ -0,0 +1,73 @@ +type SwcLoaderOptions = { + jsc?: { transform?: { react?: { development?: boolean } } }; +}; + +type RuleLike = { + loader?: string; + options?: unknown; + use?: unknown; + oneOf?: unknown; + rules?: unknown; +}; + +function isSwcLoader(item: unknown): item is RuleLike { + return ( + typeof item === 'object' && + item !== null && + (item as RuleLike).loader === 'builtin:swc-loader' + ); +} + +function withDevelopment(options: unknown, development: boolean) { + const swcOptions = options as SwcLoaderOptions | undefined; + const react = swcOptions?.jsc?.transform?.react; + if (typeof react !== 'object' || react.development !== undefined) { + return options; + } + + // nested option objects can be shared between rules, so copy them + return { + ...swcOptions, + jsc: { + ...swcOptions!.jsc, + transform: { + ...swcOptions!.jsc!.transform, + react: { ...react, development }, + }, + }, + }; +} + +/** + * Makes `builtin:swc-loader` JSX development transforms (`jsxDEV`, `__source`, + * `__self`) follow the build mode. Config helpers like `getSwcLoaderOptions` + * don't know the mode, so only an explicit `jsc.transform.react.development` + * is left untouched. + */ +export function setSwcJsxDevelopment(rules: unknown, development: boolean) { + if (!Array.isArray(rules)) return; + + for (const rule of rules as RuleLike[]) { + if (!rule || typeof rule !== 'object') continue; + + if (isSwcLoader(rule)) { + rule.options = withDevelopment(rule.options, development); + } + + if (Array.isArray(rule.use)) { + rule.use = rule.use.map((item) => + isSwcLoader(item) + ? { ...item, options: withDevelopment(item.options, development) } + : item + ); + } else if (isSwcLoader(rule.use)) { + rule.use = { + ...rule.use, + options: withDevelopment(rule.use.options, development), + }; + } + + setSwcJsxDevelopment(rule.oneOf, development); + setSwcJsxDevelopment(rule.rules, development); + } +} diff --git a/packages/repack/src/utils/getJsTransformRules.ts b/packages/repack/src/utils/getJsTransformRules.ts index 480d34754..a93bbdfa9 100644 --- a/packages/repack/src/utils/getJsTransformRules.ts +++ b/packages/repack/src/utils/getJsTransformRules.ts @@ -16,6 +16,8 @@ interface GetJsTransformRulesOptions { importSource?: string; /** The JSX runtime to use ('automatic' for React 17+ new JSX transform or 'classic' for traditional JSX transform) */ jsxRuntime?: 'automatic' | 'classic'; + /** Whether to apply JSX development transforms (defaults to the build `mode`, set by `RepackPlugin`) */ + development?: boolean; /** Enable lazy loading for all imports or specific modules */ lazyImports?: boolean | string[]; }; diff --git a/packages/repack/src/utils/getSwcLoaderOptions.ts b/packages/repack/src/utils/getSwcLoaderOptions.ts index d337f8a31..1df970419 100644 --- a/packages/repack/src/utils/getSwcLoaderOptions.ts +++ b/packages/repack/src/utils/getSwcLoaderOptions.ts @@ -52,12 +52,14 @@ function getParserOptions(syntax: 'js' | 'ts', jsx: boolean) { function getJSCTransformOptions( jsxRuntime: 'automatic' | 'classic', - importSource: string + importSource: string, + development?: boolean ) { return { react: { runtime: jsxRuntime, - development: jsxRuntime === 'classic', + // when omitted, `RepackPlugin` sets it from the build mode + ...(development !== undefined && { development }), importSource, }, }; @@ -109,6 +111,14 @@ interface MakeSwcLoaderConfigOptions { */ jsxRuntime?: 'automatic' | 'classic'; + /** + * Whether to apply JSX development transforms (`jsxDEV`, `__source`, `__self`). + * Defaults to the build `mode`, set by `RepackPlugin`. + * See SWC `jsc.transform.react.development`: + * https://swc.rs/docs/configuration/compilation#jsctransformreactdevelopment + */ + development?: boolean; + /** * Whether to disable transformation of import/export statements. */ @@ -137,6 +147,7 @@ interface MakeSwcLoaderConfigOptions { * @param options.jsx Whether to enable JSX parsing and transformation * @param options.externalHelpers Whether to use external helpers for transformations (equivalent of `@babel/runtime`) * @param options.jsxRuntime The JSX runtime to use ('automatic' for React 17+ new JSX transform or 'classic' for traditional JSX transform) + * @param options.development Whether to apply JSX development transforms (defaults to the build `mode`, set by `RepackPlugin`) * @param options.disableImportExportTransform Whether to disable transformation of import/export statements * @param options.importSource The source module for JSX runtime imports (defaults to 'react') * @param options.lazyImports Enable lazy loading for all imports or specific modules @@ -148,6 +159,7 @@ export function getSwcLoaderOptions({ jsx, externalHelpers = true, jsxRuntime = 'automatic', + development, disableImportExportTransform = false, importSource = 'react', lazyImports = false, @@ -158,7 +170,7 @@ export function getSwcLoaderOptions({ assumptions: getCompilerAssumptions(), externalHelpers: externalHelpers, parser: getParserOptions(syntax, jsx), - transform: getJSCTransformOptions(jsxRuntime, importSource), + transform: getJSCTransformOptions(jsxRuntime, importSource, development), }, module: getModuleOptions(disableImportExportTransform, lazyImports), }; diff --git a/tests/integration/src/plugins/RepackPlugin.test.ts b/tests/integration/src/plugins/RepackPlugin.test.ts new file mode 100644 index 000000000..f5e7527e9 --- /dev/null +++ b/tests/integration/src/plugins/RepackPlugin.test.ts @@ -0,0 +1,73 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getJsTransformRules, RepackPlugin } from '@callstack/repack'; +import type { RuleSetRule, RuleSetUseItem } from '@rspack/core'; +import { describe, expect, inject, it } from 'vitest'; +import { createCompiler } from '../helpers.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +type SwcOptions = { + jsc?: { transform?: { react?: { development?: boolean } } }; +}; + +function collectSwcOptions(rules: unknown[]): SwcOptions[] { + return rules.flatMap((rule) => { + if (!rule || typeof rule !== 'object') return []; + const { use, loader, options, oneOf, rules: nested } = rule as RuleSetRule; + const uses = [use].flat().filter(Boolean) as RuleSetUseItem[]; + return [ + ...(loader === 'builtin:swc-loader' ? [options as SwcOptions] : []), + ...uses + .filter((item) => typeof item === 'object') + .filter((item) => item.loader === 'builtin:swc-loader') + .map((item) => (item as { options: SwcOptions }).options), + ...collectSwcOptions(oneOf ?? []), + ...collectSwcOptions(nested ?? []), + ]; + }); +} + +async function getJsxDevelopmentFlags( + mode: 'development' | 'production', + rules: unknown[] +) { + const compiler = await createCompiler({ + context: __dirname, + mode, + entry: './index.js', + output: { path: '/out' }, + module: { rules: rules as RuleSetRule[] }, + plugins: [new RepackPlugin({ platform: 'ios' })], + }); + + return collectSwcOptions(compiler.options.module.rules).map( + (options) => options.jsc?.transform?.react?.development + ); +} + +// builtin:swc-loader only exists in Rspack +describe.runIf(inject('bundlerType') === 'rspack')('RepackPlugin', () => { + it.each([ + ['development', true], + ['production', false], + ] as const)( + 'sets SWC JSX development transforms for %s mode', + async (mode, development) => { + const flags = await getJsxDevelopmentFlags(mode, getJsTransformRules()); + + expect(flags.length).toBeGreaterThan(0); + expect(new Set(flags)).toEqual(new Set([development])); + } + ); + + it('keeps an explicit SWC JSX development option', async () => { + const flags = await getJsxDevelopmentFlags( + 'development', + getJsTransformRules({ swc: { development: false } }) + ); + + expect(flags.length).toBeGreaterThan(0); + expect(new Set(flags)).toEqual(new Set([false])); + }); +}); diff --git a/website/src/latest/api/plugins/repack.md b/website/src/latest/api/plugins/repack.md index 44b30f33a..bc3a259f4 100644 --- a/website/src/latest/api/plugins/repack.md +++ b/website/src/latest/api/plugins/repack.md @@ -2,6 +2,8 @@ This is the main plugin that enables React Native app development & bundling with Re.Pack and should be included in all of your configurations. It abstracts the configuration of other core internal plugins into one plugin. +It also makes JSX development transforms of `builtin:swc-loader` rules (e.g. from [`getJsTransformRules`](/api/utils/get-js-transform-rules)) follow `mode`, unless `jsc.transform.react.development` is set explicitly. + :::warning About internal plugins Plugins configured by the `RepackPlugin` are considered internal and there is no need to use or configure them directly. Their use is heavily discouraged and they are only included for the sake of completeness of the API. diff --git a/website/src/latest/api/utils/get-js-transform-rules.md b/website/src/latest/api/utils/get-js-transform-rules.md index 33bdeba7d..03d093207 100644 --- a/website/src/latest/api/utils/get-js-transform-rules.md +++ b/website/src/latest/api/utils/get-js-transform-rules.md @@ -26,6 +26,7 @@ interface GetJsTransformRulesOptions { externalHelpers?: boolean; importSource?: string; jsxRuntime?: "automatic" | "classic"; + development?: boolean; lazyImports?: boolean | string[]; }; flow?: { @@ -63,6 +64,7 @@ Configuration options for SWC transformations. For detailed documentation of the externalHelpers: true, importSource: 'react', jsxRuntime: 'automatic', + // development: defaults to the build `mode` lazyImports: false, } } diff --git a/website/src/latest/api/utils/get-swc-loader-options.md b/website/src/latest/api/utils/get-swc-loader-options.md index c4cd34742..c24b7ef15 100644 --- a/website/src/latest/api/utils/get-swc-loader-options.md +++ b/website/src/latest/api/utils/get-swc-loader-options.md @@ -14,6 +14,7 @@ interface MakeSwcLoaderConfigOptions { jsx: boolean; externalHelpers?: boolean; jsxRuntime?: "automatic" | "classic"; + development?: boolean; disableImportExportTransform?: boolean; importSource?: string; lazyImports?: boolean | string[]; @@ -58,6 +59,15 @@ The JSX runtime to use - 'automatic' for React 17+ new JSX transform or 'classic See [SWC `jsc.transform.react.runtime`](https://swc.rs/docs/configuration/compilation#jsctransformreactruntime). +### options.development + +- Type: `boolean` +- Default: the build `mode` + +Whether to apply JSX development transforms (`jsxDEV`, `__source`, `__self`), which give accurate component stacks and "open in editor" during development. When omitted, [`RepackPlugin`](/api/plugins/repack) sets it from `mode`: `true` in development, `false` in production. + +See [SWC `jsc.transform.react.development`](https://swc.rs/docs/configuration/compilation#jsctransformreactdevelopment). + ### options.disableImportExportTransform - Type: `boolean` From 010d01b9e81ea0cc99eb392818f606896d639218 Mon Sep 17 00:00:00 2001 From: Amir Saifutdinov Date: Fri, 9 Oct 2026 23:44:24 +0300 Subject: [PATCH 2/5] docs(repack): mention jsx-dev-runtime and how to opt out Development builds now import `/jsx-dev-runtime`; document the `development: false` override for import sources without it and that the mode-based default comes from RepackPlugin. Part of #1422 --- .changeset/swc-jsx-development-mode.md | 2 +- website/src/latest/api/utils/get-swc-loader-options.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/swc-jsx-development-mode.md b/.changeset/swc-jsx-development-mode.md index 4d3a83e6e..5188d7e62 100644 --- a/.changeset/swc-jsx-development-mode.md +++ b/.changeset/swc-jsx-development-mode.md @@ -2,4 +2,4 @@ "@callstack/repack": patch --- -Make SWC JSX development transforms follow the build mode. `getSwcLoaderOptions` (and `getJsTransformRules`) tied `jsc.transform.react.development` to the JSX runtime, so development builds with the default `automatic` runtime emitted `jsx` instead of `jsxDEV` and lost `__source` / `__self`, accurate component stacks and "open in editor", while `classic` production builds kept them. `RepackPlugin` now sets it from `mode` for every `builtin:swc-loader` rule that doesn't set it explicitly, and both helpers accept a `development` option to override it. +Make SWC JSX development transforms follow the build mode. `getSwcLoaderOptions` (and `getJsTransformRules`) tied `jsc.transform.react.development` to the JSX runtime, so development builds with the default `automatic` runtime emitted `jsx` instead of `jsxDEV` and lost `__source` / `__self`, accurate component stacks and "open in editor", while `classic` production builds kept them. `RepackPlugin` now sets it from `mode` for every `builtin:swc-loader` rule that doesn't set it explicitly, so development builds import `/jsx-dev-runtime`. Both helpers accept a `development` option to override it, e.g. `getJsTransformRules({ swc: { development: false } })` when a custom `importSource` doesn't ship a `jsx-dev-runtime` entry. diff --git a/website/src/latest/api/utils/get-swc-loader-options.md b/website/src/latest/api/utils/get-swc-loader-options.md index c24b7ef15..955b405ee 100644 --- a/website/src/latest/api/utils/get-swc-loader-options.md +++ b/website/src/latest/api/utils/get-swc-loader-options.md @@ -62,9 +62,9 @@ See [SWC `jsc.transform.react.runtime`](https://swc.rs/docs/configuration/compil ### options.development - Type: `boolean` -- Default: the build `mode` +- Default: the build `mode` (set by [`RepackPlugin`](/api/plugins/repack)), otherwise SWC's default `false` -Whether to apply JSX development transforms (`jsxDEV`, `__source`, `__self`), which give accurate component stacks and "open in editor" during development. When omitted, [`RepackPlugin`](/api/plugins/repack) sets it from `mode`: `true` in development, `false` in production. +Whether to apply JSX development transforms (`jsxDEV`, `__source`, `__self`), which give accurate component stacks and "open in editor" during development. When omitted, [`RepackPlugin`](/api/plugins/repack) sets it from `mode`: `true` in development, `false` in production. Development builds then import `/jsx-dev-runtime`; set it to `false` if your `importSource` doesn't provide one. See [SWC `jsc.transform.react.development`](https://swc.rs/docs/configuration/compilation#jsctransformreactdevelopment). From 626687809b06c4ee1bf166aad2612668c186040b Mon Sep 17 00:00:00 2001 From: Amir Saifutdinov Date: Sat, 10 Oct 2026 10:26:20 +0300 Subject: [PATCH 3/5] docs(repack): drop development placeholder from getJsTransformRules example --- website/src/latest/api/utils/get-js-transform-rules.md | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/latest/api/utils/get-js-transform-rules.md b/website/src/latest/api/utils/get-js-transform-rules.md index 03d093207..9f70fcda6 100644 --- a/website/src/latest/api/utils/get-js-transform-rules.md +++ b/website/src/latest/api/utils/get-js-transform-rules.md @@ -64,7 +64,6 @@ Configuration options for SWC transformations. For detailed documentation of the externalHelpers: true, importSource: 'react', jsxRuntime: 'automatic', - // development: defaults to the build `mode` lazyImports: false, } } From eed9d2bc626712268abc5206fafa3b5018f2970b Mon Sep 17 00:00:00 2001 From: Amir Saifutdinov Date: Thu, 15 Oct 2026 20:18:42 +0300 Subject: [PATCH 4/5] test(repack): enhance RepackPlugin tests for SWC JSX development transforms --- .../__tests__/setSwcJsxDevelopment.test.ts | 142 ++++++++++++++++++ .../src/plugins/RepackPlugin.test.ts | 37 +++-- 2 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 packages/repack/src/plugins/utils/__tests__/setSwcJsxDevelopment.test.ts diff --git a/packages/repack/src/plugins/utils/__tests__/setSwcJsxDevelopment.test.ts b/packages/repack/src/plugins/utils/__tests__/setSwcJsxDevelopment.test.ts new file mode 100644 index 000000000..b3649b860 --- /dev/null +++ b/packages/repack/src/plugins/utils/__tests__/setSwcJsxDevelopment.test.ts @@ -0,0 +1,142 @@ +import { setSwcJsxDevelopment } from '../setSwcJsxDevelopment.js'; + +const SWC_LOADER = 'builtin:swc-loader'; + +function swcOptions(development?: boolean) { + return { + sourceMaps: true, + jsc: { + parser: { syntax: 'ecmascript' }, + transform: { + legacyDecorator: true, + react: { + runtime: 'automatic', + importSource: 'react', + ...(development !== undefined && { development }), + }, + }, + }, + }; +} + +describe('setSwcJsxDevelopment', () => { + it.each([true, false])( + 'sets development to %s on a rule-level loader', + (development) => { + const rules = [{ loader: SWC_LOADER, options: swcOptions() }]; + + setSwcJsxDevelopment(rules, development); + + expect(rules).toEqual([ + { loader: SWC_LOADER, options: swcOptions(development) }, + ]); + } + ); + + it('sets development on a use object', () => { + const rules = [{ use: { loader: SWC_LOADER, options: swcOptions() } }]; + + setSwcJsxDevelopment(rules, true); + + expect(rules).toEqual([ + { use: { loader: SWC_LOADER, options: swcOptions(true) } }, + ]); + }); + + it('sets development only on swc loaders in a use array', () => { + const babelLoader = { loader: 'babel-loader', options: { presets: [] } }; + const rules = [ + { + use: [ + 'string-loader', + babelLoader, + { loader: SWC_LOADER, options: swcOptions() }, + ], + }, + ]; + + setSwcJsxDevelopment(rules, true); + + expect(rules).toEqual([ + { + use: [ + 'string-loader', + babelLoader, + { loader: SWC_LOADER, options: swcOptions(true) }, + ], + }, + ]); + expect(rules[0].use[1]).toBe(babelLoader); + }); + + it('walks nested oneOf and rules', () => { + const nestedRules = (options: object) => [ + { oneOf: [{ rules: [{ loader: SWC_LOADER, options }] }] }, + { rules: [{ oneOf: [{ use: { loader: SWC_LOADER, options } }] }] }, + ]; + const rules = nestedRules(swcOptions()); + + setSwcJsxDevelopment(rules, true); + + expect(rules).toEqual(nestedRules(swcOptions(true))); + }); + + it('skips empty rule and use entries', () => { + const rules = [null, undefined, false, { use: [null, undefined] }]; + + expect(() => setSwcJsxDevelopment(rules, true)).not.toThrow(); + }); + + it.each([true, false])( + 'keeps an explicit development: %s', + (explicitDevelopment) => { + const rules = [ + { loader: SWC_LOADER, options: swcOptions(explicitDevelopment) }, + ]; + + setSwcJsxDevelopment(rules, !explicitDevelopment); + + expect(rules).toEqual([ + { loader: SWC_LOADER, options: swcOptions(explicitDevelopment) }, + ]); + } + ); + + it('leaves swc options without a react transform untouched', () => { + const options = { jsc: { parser: { syntax: 'typescript' } } }; + const rules = [{ loader: SWC_LOADER, options }]; + + setSwcJsxDevelopment(rules, true); + + expect(rules[0].options).toBe(options); + expect(options).toEqual({ jsc: { parser: { syntax: 'typescript' } } }); + }); + + it('leaves other loaders untouched', () => { + const rules = [{ loader: 'other-loader', options: swcOptions() }]; + + setSwcJsxDevelopment(rules, true); + + expect(rules).toEqual([{ loader: 'other-loader', options: swcOptions() }]); + }); + + it('copies shared options and use entries instead of mutating them', () => { + const sharedOptions = swcOptions(); + const sharedLoader = { loader: SWC_LOADER, options: sharedOptions }; + const rules = [ + { loader: SWC_LOADER, options: sharedOptions }, + { use: sharedLoader }, + { use: [sharedLoader] }, + ]; + + setSwcJsxDevelopment(rules, true); + + expect(sharedOptions).toEqual(swcOptions()); + expect(sharedLoader.options).toBe(sharedOptions); + expect(rules).toEqual([ + { loader: SWC_LOADER, options: swcOptions(true) }, + { use: { loader: SWC_LOADER, options: swcOptions(true) } }, + { use: [{ loader: SWC_LOADER, options: swcOptions(true) }] }, + ]); + }); +}); diff --git a/tests/integration/src/plugins/RepackPlugin.test.ts b/tests/integration/src/plugins/RepackPlugin.test.ts index f5e7527e9..cc4536a59 100644 --- a/tests/integration/src/plugins/RepackPlugin.test.ts +++ b/tests/integration/src/plugins/RepackPlugin.test.ts @@ -49,25 +49,36 @@ async function getJsxDevelopmentFlags( // builtin:swc-loader only exists in Rspack describe.runIf(inject('bundlerType') === 'rspack')('RepackPlugin', () => { it.each([ - ['development', true], - ['production', false], + ['automatic', 'development', true], + ['automatic', 'production', false], + ['classic', 'development', true], + ['classic', 'production', false], ] as const)( - 'sets SWC JSX development transforms for %s mode', - async (mode, development) => { - const flags = await getJsxDevelopmentFlags(mode, getJsTransformRules()); + 'sets SWC JSX development transforms for %s runtime in %s mode', + async (jsxRuntime, mode, development) => { + const flags = await getJsxDevelopmentFlags( + mode, + getJsTransformRules({ swc: { jsxRuntime } }) + ); expect(flags.length).toBeGreaterThan(0); expect(new Set(flags)).toEqual(new Set([development])); } ); - it('keeps an explicit SWC JSX development option', async () => { - const flags = await getJsxDevelopmentFlags( - 'development', - getJsTransformRules({ swc: { development: false } }) - ); + it.each([ + ['development', false], + ['production', true], + ] as const)( + 'keeps an explicit SWC JSX development option in %s mode', + async (mode, development) => { + const flags = await getJsxDevelopmentFlags( + mode, + getJsTransformRules({ swc: { development } }) + ); - expect(flags.length).toBeGreaterThan(0); - expect(new Set(flags)).toEqual(new Set([false])); - }); + expect(flags.length).toBeGreaterThan(0); + expect(new Set(flags)).toEqual(new Set([development])); + } + ); }); From a7a63865f70694d55db151ce73ab33de5101dc1e Mon Sep 17 00:00:00 2001 From: Amir Saifutdinov Date: Fri, 16 Oct 2026 17:42:31 +0300 Subject: [PATCH 5/5] fix(repack-plugin): align SWC JSX development transforms with build mode and remove deprecated utility --- .changeset/swc-jsx-development-mode.md | 2 +- packages/repack/src/plugins/RepackPlugin.ts | 6 - .../__tests__/setSwcJsxDevelopment.test.ts | 142 ------------------ .../src/plugins/utils/setSwcJsxDevelopment.ts | 73 --------- .../repack/src/utils/getJsTransformRules.ts | 2 +- .../repack/src/utils/getSwcLoaderOptions.ts | 6 +- .../src/loaders/swc-jsx-development.test.ts | 98 ++++++++++++ .../src/plugins/RepackPlugin.test.ts | 84 ----------- website/src/latest/api/plugins/repack.md | 2 - .../api/utils/get-swc-loader-options.md | 4 +- 10 files changed, 105 insertions(+), 314 deletions(-) delete mode 100644 packages/repack/src/plugins/utils/__tests__/setSwcJsxDevelopment.test.ts delete mode 100644 packages/repack/src/plugins/utils/setSwcJsxDevelopment.ts create mode 100644 tests/integration/src/loaders/swc-jsx-development.test.ts delete mode 100644 tests/integration/src/plugins/RepackPlugin.test.ts diff --git a/.changeset/swc-jsx-development-mode.md b/.changeset/swc-jsx-development-mode.md index 5188d7e62..54e1691ec 100644 --- a/.changeset/swc-jsx-development-mode.md +++ b/.changeset/swc-jsx-development-mode.md @@ -2,4 +2,4 @@ "@callstack/repack": patch --- -Make SWC JSX development transforms follow the build mode. `getSwcLoaderOptions` (and `getJsTransformRules`) tied `jsc.transform.react.development` to the JSX runtime, so development builds with the default `automatic` runtime emitted `jsx` instead of `jsxDEV` and lost `__source` / `__self`, accurate component stacks and "open in editor", while `classic` production builds kept them. `RepackPlugin` now sets it from `mode` for every `builtin:swc-loader` rule that doesn't set it explicitly, so development builds import `/jsx-dev-runtime`. Both helpers accept a `development` option to override it, e.g. `getJsTransformRules({ swc: { development: false } })` when a custom `importSource` doesn't ship a `jsx-dev-runtime` entry. +Make SWC JSX development transforms follow the build mode. `getSwcLoaderOptions` (and `getJsTransformRules`) tied `jsc.transform.react.development` to the JSX runtime, so development builds with the default `automatic` runtime emitted `jsx` instead of `jsxDEV` and lost `__source` / `__self`, accurate component stacks and "open in editor", while `classic` production builds kept them. `getSwcLoaderOptions` no longer sets `jsc.transform.react.development` by default, so Rspack's `builtin:swc-loader` derives it from `mode`, and development builds with the `automatic` runtime now import `/jsx-dev-runtime`. Both helpers accept a `development` option to override it, e.g. `getJsTransformRules({ swc: { development: false } })` when a custom `importSource` doesn't ship a `jsx-dev-runtime` entry. diff --git a/packages/repack/src/plugins/RepackPlugin.ts b/packages/repack/src/plugins/RepackPlugin.ts index 61b31331d..9d6c7db00 100644 --- a/packages/repack/src/plugins/RepackPlugin.ts +++ b/packages/repack/src/plugins/RepackPlugin.ts @@ -7,7 +7,6 @@ import { NativeEntryPlugin } from './NativeEntryPlugin/index.js'; import { OutputPlugin, type OutputPluginConfig } from './OutputPlugin/index.js'; import { RepackTargetPlugin } from './RepackTargetPlugin/index.js'; import { SourceMapPlugin } from './SourceMapPlugin.js'; -import { setSwcJsxDevelopment } from './utils/setSwcJsxDevelopment.js'; /** * {@link RepackPlugin} configuration options. @@ -108,11 +107,6 @@ export class RepackPlugin { __DEV__: JSON.stringify(compiler.options.mode === 'development'), }).apply(compiler); - setSwcJsxDevelopment( - compiler.options.module.rules, - compiler.options.mode === 'development' - ); - new BabelPlugin().apply(compiler); new OutputPlugin({ diff --git a/packages/repack/src/plugins/utils/__tests__/setSwcJsxDevelopment.test.ts b/packages/repack/src/plugins/utils/__tests__/setSwcJsxDevelopment.test.ts deleted file mode 100644 index b3649b860..000000000 --- a/packages/repack/src/plugins/utils/__tests__/setSwcJsxDevelopment.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { setSwcJsxDevelopment } from '../setSwcJsxDevelopment.js'; - -const SWC_LOADER = 'builtin:swc-loader'; - -function swcOptions(development?: boolean) { - return { - sourceMaps: true, - jsc: { - parser: { syntax: 'ecmascript' }, - transform: { - legacyDecorator: true, - react: { - runtime: 'automatic', - importSource: 'react', - ...(development !== undefined && { development }), - }, - }, - }, - }; -} - -describe('setSwcJsxDevelopment', () => { - it.each([true, false])( - 'sets development to %s on a rule-level loader', - (development) => { - const rules = [{ loader: SWC_LOADER, options: swcOptions() }]; - - setSwcJsxDevelopment(rules, development); - - expect(rules).toEqual([ - { loader: SWC_LOADER, options: swcOptions(development) }, - ]); - } - ); - - it('sets development on a use object', () => { - const rules = [{ use: { loader: SWC_LOADER, options: swcOptions() } }]; - - setSwcJsxDevelopment(rules, true); - - expect(rules).toEqual([ - { use: { loader: SWC_LOADER, options: swcOptions(true) } }, - ]); - }); - - it('sets development only on swc loaders in a use array', () => { - const babelLoader = { loader: 'babel-loader', options: { presets: [] } }; - const rules = [ - { - use: [ - 'string-loader', - babelLoader, - { loader: SWC_LOADER, options: swcOptions() }, - ], - }, - ]; - - setSwcJsxDevelopment(rules, true); - - expect(rules).toEqual([ - { - use: [ - 'string-loader', - babelLoader, - { loader: SWC_LOADER, options: swcOptions(true) }, - ], - }, - ]); - expect(rules[0].use[1]).toBe(babelLoader); - }); - - it('walks nested oneOf and rules', () => { - const nestedRules = (options: object) => [ - { oneOf: [{ rules: [{ loader: SWC_LOADER, options }] }] }, - { rules: [{ oneOf: [{ use: { loader: SWC_LOADER, options } }] }] }, - ]; - const rules = nestedRules(swcOptions()); - - setSwcJsxDevelopment(rules, true); - - expect(rules).toEqual(nestedRules(swcOptions(true))); - }); - - it('skips empty rule and use entries', () => { - const rules = [null, undefined, false, { use: [null, undefined] }]; - - expect(() => setSwcJsxDevelopment(rules, true)).not.toThrow(); - }); - - it.each([true, false])( - 'keeps an explicit development: %s', - (explicitDevelopment) => { - const rules = [ - { loader: SWC_LOADER, options: swcOptions(explicitDevelopment) }, - ]; - - setSwcJsxDevelopment(rules, !explicitDevelopment); - - expect(rules).toEqual([ - { loader: SWC_LOADER, options: swcOptions(explicitDevelopment) }, - ]); - } - ); - - it('leaves swc options without a react transform untouched', () => { - const options = { jsc: { parser: { syntax: 'typescript' } } }; - const rules = [{ loader: SWC_LOADER, options }]; - - setSwcJsxDevelopment(rules, true); - - expect(rules[0].options).toBe(options); - expect(options).toEqual({ jsc: { parser: { syntax: 'typescript' } } }); - }); - - it('leaves other loaders untouched', () => { - const rules = [{ loader: 'other-loader', options: swcOptions() }]; - - setSwcJsxDevelopment(rules, true); - - expect(rules).toEqual([{ loader: 'other-loader', options: swcOptions() }]); - }); - - it('copies shared options and use entries instead of mutating them', () => { - const sharedOptions = swcOptions(); - const sharedLoader = { loader: SWC_LOADER, options: sharedOptions }; - const rules = [ - { loader: SWC_LOADER, options: sharedOptions }, - { use: sharedLoader }, - { use: [sharedLoader] }, - ]; - - setSwcJsxDevelopment(rules, true); - - expect(sharedOptions).toEqual(swcOptions()); - expect(sharedLoader.options).toBe(sharedOptions); - expect(rules).toEqual([ - { loader: SWC_LOADER, options: swcOptions(true) }, - { use: { loader: SWC_LOADER, options: swcOptions(true) } }, - { use: [{ loader: SWC_LOADER, options: swcOptions(true) }] }, - ]); - }); -}); diff --git a/packages/repack/src/plugins/utils/setSwcJsxDevelopment.ts b/packages/repack/src/plugins/utils/setSwcJsxDevelopment.ts deleted file mode 100644 index 6d5567de5..000000000 --- a/packages/repack/src/plugins/utils/setSwcJsxDevelopment.ts +++ /dev/null @@ -1,73 +0,0 @@ -type SwcLoaderOptions = { - jsc?: { transform?: { react?: { development?: boolean } } }; -}; - -type RuleLike = { - loader?: string; - options?: unknown; - use?: unknown; - oneOf?: unknown; - rules?: unknown; -}; - -function isSwcLoader(item: unknown): item is RuleLike { - return ( - typeof item === 'object' && - item !== null && - (item as RuleLike).loader === 'builtin:swc-loader' - ); -} - -function withDevelopment(options: unknown, development: boolean) { - const swcOptions = options as SwcLoaderOptions | undefined; - const react = swcOptions?.jsc?.transform?.react; - if (typeof react !== 'object' || react.development !== undefined) { - return options; - } - - // nested option objects can be shared between rules, so copy them - return { - ...swcOptions, - jsc: { - ...swcOptions!.jsc, - transform: { - ...swcOptions!.jsc!.transform, - react: { ...react, development }, - }, - }, - }; -} - -/** - * Makes `builtin:swc-loader` JSX development transforms (`jsxDEV`, `__source`, - * `__self`) follow the build mode. Config helpers like `getSwcLoaderOptions` - * don't know the mode, so only an explicit `jsc.transform.react.development` - * is left untouched. - */ -export function setSwcJsxDevelopment(rules: unknown, development: boolean) { - if (!Array.isArray(rules)) return; - - for (const rule of rules as RuleLike[]) { - if (!rule || typeof rule !== 'object') continue; - - if (isSwcLoader(rule)) { - rule.options = withDevelopment(rule.options, development); - } - - if (Array.isArray(rule.use)) { - rule.use = rule.use.map((item) => - isSwcLoader(item) - ? { ...item, options: withDevelopment(item.options, development) } - : item - ); - } else if (isSwcLoader(rule.use)) { - rule.use = { - ...rule.use, - options: withDevelopment(rule.use.options, development), - }; - } - - setSwcJsxDevelopment(rule.oneOf, development); - setSwcJsxDevelopment(rule.rules, development); - } -} diff --git a/packages/repack/src/utils/getJsTransformRules.ts b/packages/repack/src/utils/getJsTransformRules.ts index a93bbdfa9..f62000403 100644 --- a/packages/repack/src/utils/getJsTransformRules.ts +++ b/packages/repack/src/utils/getJsTransformRules.ts @@ -16,7 +16,7 @@ interface GetJsTransformRulesOptions { importSource?: string; /** The JSX runtime to use ('automatic' for React 17+ new JSX transform or 'classic' for traditional JSX transform) */ jsxRuntime?: 'automatic' | 'classic'; - /** Whether to apply JSX development transforms (defaults to the build `mode`, set by `RepackPlugin`) */ + /** Whether to apply JSX development transforms (defaults to the build `mode`, set by Rspack's `builtin:swc-loader`) */ development?: boolean; /** Enable lazy loading for all imports or specific modules */ lazyImports?: boolean | string[]; diff --git a/packages/repack/src/utils/getSwcLoaderOptions.ts b/packages/repack/src/utils/getSwcLoaderOptions.ts index 1df970419..d594f8810 100644 --- a/packages/repack/src/utils/getSwcLoaderOptions.ts +++ b/packages/repack/src/utils/getSwcLoaderOptions.ts @@ -58,7 +58,7 @@ function getJSCTransformOptions( return { react: { runtime: jsxRuntime, - // when omitted, `RepackPlugin` sets it from the build mode + // when omitted, Rspack's builtin:swc-loader sets it from `mode` ...(development !== undefined && { development }), importSource, }, @@ -113,7 +113,7 @@ interface MakeSwcLoaderConfigOptions { /** * Whether to apply JSX development transforms (`jsxDEV`, `__source`, `__self`). - * Defaults to the build `mode`, set by `RepackPlugin`. + * Defaults to the build `mode` (set by Rspack's `builtin:swc-loader` when omitted). * See SWC `jsc.transform.react.development`: * https://swc.rs/docs/configuration/compilation#jsctransformreactdevelopment */ @@ -147,7 +147,7 @@ interface MakeSwcLoaderConfigOptions { * @param options.jsx Whether to enable JSX parsing and transformation * @param options.externalHelpers Whether to use external helpers for transformations (equivalent of `@babel/runtime`) * @param options.jsxRuntime The JSX runtime to use ('automatic' for React 17+ new JSX transform or 'classic' for traditional JSX transform) - * @param options.development Whether to apply JSX development transforms (defaults to the build `mode`, set by `RepackPlugin`) + * @param options.development Whether to apply JSX development transforms (defaults to the build `mode`, set by Rspack's `builtin:swc-loader`) * @param options.disableImportExportTransform Whether to disable transformation of import/export statements * @param options.importSource The source module for JSX runtime imports (defaults to 'react') * @param options.lazyImports Enable lazy loading for all imports or specific modules diff --git a/tests/integration/src/loaders/swc-jsx-development.test.ts b/tests/integration/src/loaders/swc-jsx-development.test.ts new file mode 100644 index 000000000..a957fc7cc --- /dev/null +++ b/tests/integration/src/loaders/swc-jsx-development.test.ts @@ -0,0 +1,98 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getJsTransformRules } from '@callstack/repack'; +import { describe, expect, inject, it } from 'vitest'; +import { + compile, + createCompiler, + createVirtualModulePlugin, +} from '../helpers.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// JSX source info that development transforms add for the fixture +// (`jsxDEV` source argument for automatic runtime, `__source` prop for classic) +const SOURCE_INFO = /fileName: ".*App\.jsx"/; + +async function compileJsx( + mode: 'development' | 'production', + swc: NonNullable[0]>['swc'] +) { + const virtualPlugin = await createVirtualModulePlugin({ + 'App.jsx': 'export const App = () => ;', + }); + + const compiler = await createCompiler({ + context: __dirname, + mode, + devtool: false, + entry: './App.jsx', + output: { path: '/out' }, + optimization: { minimize: false }, + externals: { + react: 'commonjs react', + 'react/jsx-runtime': 'commonjs react/jsx-runtime', + 'react/jsx-dev-runtime': 'commonjs react/jsx-dev-runtime', + }, + module: { rules: getJsTransformRules({ swc }) }, + plugins: [virtualPlugin], + }); + + const { code } = await compile(compiler); + return code; +} + +// builtin:swc-loader only exists in Rspack +describe.runIf(inject('bundlerType') === 'rspack')( + 'SWC JSX development transforms', + () => { + it('emits jsxDEV with source info for automatic runtime in development', async () => { + const code = await compileJsx('development', { jsxRuntime: 'automatic' }); + + expect(code).toContain('react/jsx-dev-runtime'); + expect(code).toContain('jsxDEV'); + expect(code).toMatch(SOURCE_INFO); + }); + + it('emits jsx without source info for automatic runtime in production', async () => { + const code = await compileJsx('production', { jsxRuntime: 'automatic' }); + + expect(code).toContain('react/jsx-runtime'); + expect(code).not.toContain('jsx-dev-runtime'); + expect(code).not.toContain('jsxDEV'); + expect(code).not.toMatch(SOURCE_INFO); + }); + + it('adds __source for classic runtime in development', async () => { + const code = await compileJsx('development', { jsxRuntime: 'classic' }); + + expect(code).toContain('createElement'); + expect(code).toContain('__source'); + expect(code).toMatch(SOURCE_INFO); + }); + + it('omits __source for classic runtime in production', async () => { + const code = await compileJsx('production', { jsxRuntime: 'classic' }); + + expect(code).toContain('createElement'); + expect(code).not.toContain('__source'); + expect(code).not.toMatch(SOURCE_INFO); + }); + + it('respects an explicit development: false in development', async () => { + const code = await compileJsx('development', { development: false }); + + expect(code).toContain('react/jsx-runtime'); + expect(code).not.toContain('jsxDEV'); + expect(code).not.toMatch(SOURCE_INFO); + }); + + it('respects an explicit development: true in production', async () => { + const code = await compileJsx('production', { development: true }); + + expect(code).toContain('react/jsx-dev-runtime'); + expect(code).toContain('jsxDEV'); + expect(code).toMatch(SOURCE_INFO); + }); + } +); diff --git a/tests/integration/src/plugins/RepackPlugin.test.ts b/tests/integration/src/plugins/RepackPlugin.test.ts deleted file mode 100644 index cc4536a59..000000000 --- a/tests/integration/src/plugins/RepackPlugin.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { getJsTransformRules, RepackPlugin } from '@callstack/repack'; -import type { RuleSetRule, RuleSetUseItem } from '@rspack/core'; -import { describe, expect, inject, it } from 'vitest'; -import { createCompiler } from '../helpers.js'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -type SwcOptions = { - jsc?: { transform?: { react?: { development?: boolean } } }; -}; - -function collectSwcOptions(rules: unknown[]): SwcOptions[] { - return rules.flatMap((rule) => { - if (!rule || typeof rule !== 'object') return []; - const { use, loader, options, oneOf, rules: nested } = rule as RuleSetRule; - const uses = [use].flat().filter(Boolean) as RuleSetUseItem[]; - return [ - ...(loader === 'builtin:swc-loader' ? [options as SwcOptions] : []), - ...uses - .filter((item) => typeof item === 'object') - .filter((item) => item.loader === 'builtin:swc-loader') - .map((item) => (item as { options: SwcOptions }).options), - ...collectSwcOptions(oneOf ?? []), - ...collectSwcOptions(nested ?? []), - ]; - }); -} - -async function getJsxDevelopmentFlags( - mode: 'development' | 'production', - rules: unknown[] -) { - const compiler = await createCompiler({ - context: __dirname, - mode, - entry: './index.js', - output: { path: '/out' }, - module: { rules: rules as RuleSetRule[] }, - plugins: [new RepackPlugin({ platform: 'ios' })], - }); - - return collectSwcOptions(compiler.options.module.rules).map( - (options) => options.jsc?.transform?.react?.development - ); -} - -// builtin:swc-loader only exists in Rspack -describe.runIf(inject('bundlerType') === 'rspack')('RepackPlugin', () => { - it.each([ - ['automatic', 'development', true], - ['automatic', 'production', false], - ['classic', 'development', true], - ['classic', 'production', false], - ] as const)( - 'sets SWC JSX development transforms for %s runtime in %s mode', - async (jsxRuntime, mode, development) => { - const flags = await getJsxDevelopmentFlags( - mode, - getJsTransformRules({ swc: { jsxRuntime } }) - ); - - expect(flags.length).toBeGreaterThan(0); - expect(new Set(flags)).toEqual(new Set([development])); - } - ); - - it.each([ - ['development', false], - ['production', true], - ] as const)( - 'keeps an explicit SWC JSX development option in %s mode', - async (mode, development) => { - const flags = await getJsxDevelopmentFlags( - mode, - getJsTransformRules({ swc: { development } }) - ); - - expect(flags.length).toBeGreaterThan(0); - expect(new Set(flags)).toEqual(new Set([development])); - } - ); -}); diff --git a/website/src/latest/api/plugins/repack.md b/website/src/latest/api/plugins/repack.md index bc3a259f4..44b30f33a 100644 --- a/website/src/latest/api/plugins/repack.md +++ b/website/src/latest/api/plugins/repack.md @@ -2,8 +2,6 @@ This is the main plugin that enables React Native app development & bundling with Re.Pack and should be included in all of your configurations. It abstracts the configuration of other core internal plugins into one plugin. -It also makes JSX development transforms of `builtin:swc-loader` rules (e.g. from [`getJsTransformRules`](/api/utils/get-js-transform-rules)) follow `mode`, unless `jsc.transform.react.development` is set explicitly. - :::warning About internal plugins Plugins configured by the `RepackPlugin` are considered internal and there is no need to use or configure them directly. Their use is heavily discouraged and they are only included for the sake of completeness of the API. diff --git a/website/src/latest/api/utils/get-swc-loader-options.md b/website/src/latest/api/utils/get-swc-loader-options.md index 955b405ee..857783ea3 100644 --- a/website/src/latest/api/utils/get-swc-loader-options.md +++ b/website/src/latest/api/utils/get-swc-loader-options.md @@ -62,9 +62,9 @@ See [SWC `jsc.transform.react.runtime`](https://swc.rs/docs/configuration/compil ### options.development - Type: `boolean` -- Default: the build `mode` (set by [`RepackPlugin`](/api/plugins/repack)), otherwise SWC's default `false` +- Default: the build `mode` when used with Rspack's `builtin:swc-loader` -Whether to apply JSX development transforms (`jsxDEV`, `__source`, `__self`), which give accurate component stacks and "open in editor" during development. When omitted, [`RepackPlugin`](/api/plugins/repack) sets it from `mode`: `true` in development, `false` in production. Development builds then import `/jsx-dev-runtime`; set it to `false` if your `importSource` doesn't provide one. +Whether to apply JSX development transforms (`jsxDEV`, `__source`, `__self`), which give accurate component stacks and "open in editor" during development. When omitted, Rspack's `builtin:swc-loader` sets it from `mode`: `true` in development, `false` in production. With the `automatic` runtime, development builds then import `/jsx-dev-runtime`; set it to `false` if your `importSource` doesn't provide one. See [SWC `jsc.transform.react.development`](https://swc.rs/docs/configuration/compilation#jsctransformreactdevelopment).