diff --git a/.changeset/swc-jsx-development-mode.md b/.changeset/swc-jsx-development-mode.md new file mode 100644 index 000000000..54e1691ec --- /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. `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/utils/getJsTransformRules.ts b/packages/repack/src/utils/getJsTransformRules.ts index 480d34754..f62000403 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 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 d337f8a31..d594f8810 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, Rspack's builtin:swc-loader sets it from `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 Rspack's `builtin:swc-loader` when omitted). + * 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 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 @@ -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/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/website/src/latest/api/utils/get-js-transform-rules.md b/website/src/latest/api/utils/get-js-transform-rules.md index 33bdeba7d..9f70fcda6 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?: { 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..857783ea3 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` 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, 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). + ### options.disableImportExportTransform - Type: `boolean`