Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/swc-jsx-development-mode.md
Original file line number Diff line number Diff line change
@@ -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 `<importSource>/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.
2 changes: 2 additions & 0 deletions packages/repack/src/utils/getJsTransformRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};
Expand Down
18 changes: 15 additions & 3 deletions packages/repack/src/utils/getSwcLoaderOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand All @@ -148,6 +159,7 @@ export function getSwcLoaderOptions({
jsx,
externalHelpers = true,
jsxRuntime = 'automatic',
development,
disableImportExportTransform = false,
importSource = 'react',
lazyImports = false,
Expand All @@ -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),
};
Expand Down
98 changes: 98 additions & 0 deletions tests/integration/src/loaders/swc-jsx-development.test.ts
Original file line number Diff line number Diff line change
@@ -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<Parameters<typeof getJsTransformRules>[0]>['swc']
) {
const virtualPlugin = await createVirtualModulePlugin({
'App.jsx': 'export const App = () => <view title="hello" />;',
});

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);
});
}
);
1 change: 1 addition & 0 deletions website/src/latest/api/utils/get-js-transform-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface GetJsTransformRulesOptions {
externalHelpers?: boolean;
importSource?: string;
jsxRuntime?: "automatic" | "classic";
development?: boolean;
lazyImports?: boolean | string[];
};
flow?: {
Expand Down
10 changes: 10 additions & 0 deletions website/src/latest/api/utils/get-swc-loader-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface MakeSwcLoaderConfigOptions {
jsx: boolean;
externalHelpers?: boolean;
jsxRuntime?: "automatic" | "classic";
development?: boolean;
disableImportExportTransform?: boolean;
importSource?: string;
lazyImports?: boolean | string[];
Expand Down Expand Up @@ -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 `<importSource>/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`
Expand Down
Loading