Skip to content

Commit 7bbbeb9

Browse files
committed
feat: check consistency of JSX between swc and tsconfig
1 parent 860dba6 commit 7bbbeb9

File tree

10 files changed

+149
-13
lines changed

10 files changed

+149
-13
lines changed

packages/core/src/check.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { RsbuildConfig, RsbuildPlugin } from '@rsbuild/core';
2+
import type { TsconfigCompilerOptions } from './types';
3+
import { color } from './utils/helper';
4+
import { logger } from './utils/logger';
5+
6+
type PluginReactOptions = {
7+
tsconfigCompilerOptions?: TsconfigCompilerOptions;
8+
};
9+
10+
const mapTsconfigJsxToSwcJsx = (jsx: string | undefined): string | null => {
11+
if (jsx === undefined) {
12+
// 'preserve' is the default value of tsconfig.compilerOptions.jsx
13+
return null;
14+
}
15+
16+
// Calculate a corresponding SWC JSX config if tsconfig.compilerOptions.jsx is set to React related option.
17+
// Return `null` stands for no need to check.
18+
switch (jsx) {
19+
case 'react-jsx':
20+
case 'react-jsxdev':
21+
return 'automatic';
22+
case 'react':
23+
return 'classic';
24+
case 'preserve':
25+
case 'react-native':
26+
// SWC JSX does not support `preserve` as of now.
27+
return null;
28+
default:
29+
return null;
30+
}
31+
};
32+
33+
const checkJsx = ({
34+
tsconfigCompilerOptions,
35+
}: PluginReactOptions): RsbuildPlugin => ({
36+
name: 'rsbuild:lib-check',
37+
setup(api) {
38+
api.onBeforeEnvironmentCompile(({ environment }) => {
39+
const config = api.getNormalizedConfig({
40+
environment: environment.name,
41+
});
42+
const swc = config.tools.swc;
43+
const tsconfigJsx = tsconfigCompilerOptions?.jsx;
44+
if (swc && !Array.isArray(swc) && typeof swc !== 'function') {
45+
const swcReactRuntime = swc?.jsc?.transform?.react?.runtime || null;
46+
const mapped = mapTsconfigJsxToSwcJsx(tsconfigJsx);
47+
if (mapped !== swcReactRuntime) {
48+
logger.warn(
49+
`JSX runtime is set to ${color.green(`${JSON.stringify(swcReactRuntime)}`)} in SWC, but got ${color.green(`${JSON.stringify(tsconfigJsx)}`)} in tsconfig.json. This may cause unexpected behavior, considering aligning them.`,
50+
);
51+
}
52+
}
53+
});
54+
},
55+
});
56+
57+
export const composeCheckConfig = (
58+
compilerOptions: TsconfigCompilerOptions,
59+
): RsbuildConfig => {
60+
return { plugins: [checkJsx({ tsconfigCompilerOptions: compilerOptions })] };
61+
};

packages/core/src/config.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '@rsbuild/core';
1515
import { glob } from 'tinyglobby';
1616
import { composeAssetConfig } from './asset/assetConfig';
17+
import { composeCheckConfig } from './check';
1718
import {
1819
DEFAULT_CONFIG_EXTENSIONS,
1920
DEFAULT_CONFIG_NAME,
@@ -57,6 +58,7 @@ import type {
5758
RspackResolver,
5859
Shims,
5960
Syntax,
61+
TsconfigCompilerOptions,
6062
} from './types';
6163
import { getDefaultExtension } from './utils/extension';
6264
import {
@@ -434,7 +436,7 @@ export function composeBannerFooterConfig(
434436
}
435437

436438
export function composeDecoratorsConfig(
437-
compilerOptions?: Record<string, any>,
439+
compilerOptions?: TsconfigCompilerOptions,
438440
version?: NonNullable<
439441
NonNullable<EnvironmentConfig['source']>['decorators']
440442
>['version'],
@@ -1327,6 +1329,8 @@ async function composeLibRsbuildConfig(
13271329
rootPath,
13281330
config.source?.tsconfigPath,
13291331
);
1332+
1333+
const checkConfig = composeCheckConfig({ compilerOptions });
13301334
const cssModulesAuto = config.output?.cssModules?.auto ?? true;
13311335

13321336
const {
@@ -1438,6 +1442,7 @@ async function composeLibRsbuildConfig(
14381442
dtsConfig,
14391443
bannerFooterConfig,
14401444
decoratorsConfig,
1445+
checkConfig,
14411446
);
14421447
}
14431448

packages/core/src/types/config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,7 @@ export type RslibConfigExport =
305305
| RslibConfig
306306
| RslibConfigSyncFn
307307
| RslibConfigAsyncFn;
308+
309+
export type TsconfigCompilerOptions = Record<string, any> & {
310+
jsx?: 'react-jsx' | 'react-jsxdev' | 'react';
311+
};

packages/core/src/utils/helper.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,23 @@ export function omit<T extends object, U extends keyof T>(
167167
);
168168
}
169169

170-
export function isPluginIncluded(
170+
function findPlugin(pluginName: string, plugins?: RsbuildPlugins) {
171+
return plugins?.find((plugin) => {
172+
if (Array.isArray(plugin)) {
173+
return isPluginIncluded(pluginName, plugin);
174+
}
175+
if (typeof plugin === 'object' && plugin !== null && 'name' in plugin) {
176+
return plugin.name === pluginName;
177+
}
178+
return false;
179+
});
180+
}
181+
182+
function isPluginIncluded(
171183
pluginName: string,
172184
plugins?: RsbuildPlugins,
173185
): boolean {
174-
return Boolean(
175-
plugins?.some((plugin) => {
176-
if (Array.isArray(plugin)) {
177-
return isPluginIncluded(pluginName, plugin);
178-
}
179-
if (typeof plugin === 'object' && plugin !== null && 'name' in plugin) {
180-
return plugin.name === pluginName;
181-
}
182-
return false;
183-
}),
184-
);
186+
return Boolean(findPlugin(pluginName, plugins));
185187
}
186188

187189
export function checkMFPlugin(

pnpm-lock.yaml

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/check/index.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { join } from 'node:path';
2+
import stripAnsi from 'strip-ansi';
3+
import { buildAndGetResults, proxyConsole } from 'test-helper';
4+
import { expect, test } from 'vitest';
5+
6+
test('should receive JSX mismatch warning of SWC with tsconfig', async () => {
7+
const { logs, restore } = proxyConsole();
8+
const fixturePath = join(__dirname, 'jsx');
9+
await buildAndGetResults({ fixturePath });
10+
const logStrings = logs
11+
.map((log) => stripAnsi(log))
12+
.filter((log) => log.startsWith('warn'))
13+
.sort()
14+
.join('\n');
15+
16+
expect(logStrings).toMatchInlineSnapshot(`
17+
"warn JSX runtime is set to "automatic" in SWC, but got undefined in tsconfig.json. This may cause unexpected behavior, considering aligning them.
18+
warn JSX runtime is set to "automatic" in SWC, but got undefined in tsconfig.json. This may cause unexpected behavior, considering aligning them."
19+
`);
20+
21+
restore();
22+
});
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "check-jsx-test",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"devDependencies": {
7+
"@rsbuild/plugin-react": "^1.1.0",
8+
"@types/react": "^19.0.6",
9+
"react": "^19.0.0"
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { pluginReact } from '@rsbuild/plugin-react';
2+
import { defineConfig } from '@rslib/core';
3+
import { generateBundleCjsConfig, generateBundleEsmConfig } from 'test-helper';
4+
5+
export default defineConfig({
6+
lib: [generateBundleEsmConfig(), generateBundleCjsConfig()],
7+
plugins: [pluginReact()],
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import React from 'react';
2+
3+
export const Foo = <div>foo</div>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "@rslib/tsconfig/base",
3+
"compilerOptions": {
4+
"baseUrl": "./",
5+
"jsx": "react"
6+
},
7+
"include": ["src"]
8+
}

0 commit comments

Comments
 (0)