├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── .node-version
├── .npmignore
├── LICENSE
├── README.md
├── eslint.config.js
├── package.json
├── pnpm-lock.yaml
├── rollup.config.ts
├── src
├── index.ts
└── react-compiler-loader.ts
├── test
├── __snapshots__
│ └── index.ts.snap
├── fixtures
│ ├── cjk.tsx
│ ├── complex.tsx
│ ├── simple.jsx
│ └── simple.tsx
├── index.ts
└── utils
│ ├── compile.ts
│ ├── get-module-source.ts
│ ├── get-rspack-compiler.ts
│ ├── get-swc-loader.ts
│ └── get-webpack-compiler.ts
└── tsconfig.json
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Automatic Publish
2 | on:
3 | push:
4 | branches:
5 | - master
6 | tags-ignore:
7 | - '**'
8 | paths-ignore:
9 | - '**/*.md'
10 | - LICENSE
11 | - '**/*.gitignore'
12 | - .editorconfig
13 | - docs/**
14 | pull_request: null
15 | jobs:
16 | publish:
17 | name: Publish
18 | runs-on: ubuntu-latest
19 | permissions:
20 | contents: read
21 | id-token: write
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: pnpm/action-setup@v4
25 | - uses: actions/setup-node@v4
26 | with:
27 | node-version-file: '.node-version'
28 | check-latest: true
29 | cache: 'pnpm'
30 | registry-url: 'https://registry.npmjs.org'
31 | - run: npm install -g npm
32 | - run: pnpm install
33 | - run: pnpm run build
34 | - run: pnpm run lint
35 | - name: Publish
36 | run: |
37 | if git log -1 --pretty=%B | grep "^release: [0-9]\+\.[0-9]\+\.[0-9]\+$";
38 | then
39 | pnpm -r publish --provenance --access public
40 | elif git log -1 --pretty=%B | grep "^release: [0-9]\+\.[0-9]\+\.[0-9]\+";
41 | then
42 | pnpm -r publish --provenance --access public --tag next
43 | else
44 | echo "Not a release, skipping publish"
45 | fi
46 | env:
47 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
48 | NPM_CONFIG_PROVENANCE: true
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | types
3 | dist
4 | .temp
5 | .DS_Store
6 | *.log*
7 | Thumb.db
8 | tmp
9 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 22
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | eslint.config.js
2 | test
3 | tools
4 | tsconfig.json
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sukka
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Compiler for webpack
2 |
3 | A webpack loader, a [rspack](https://www.rspack.dev/) loader that brings [the official React Compiler](https://react.dev/learn/react-compiler) to your project.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | # npm
9 | npm i -D react-compiler-webpack
10 | # yarn
11 | yarn add -D react-compiler-webpack
12 | # pnpm
13 | pnpm add -D react-compiler-webpack
14 | ```
15 |
16 | `react-compiler-webpack` has already declares `babel-plugin-react-compiler` as its peer dependency and it will be installed automatically when you install `react-compiler-webpack` with most package managers. But you can also explictly specify the version you like by manually install `babel-plugin-react-compiler` in your project:
17 |
18 | ```bash
19 | # npm
20 | npm i -D babel-plugin-react-compiler
21 | # yarn
22 | yarn add -D babel-plugin-react-compiler
23 | # pnpm
24 | pnpm add -D babel-plugin-react-compiler
25 | ```
26 |
27 | ## Usage
28 |
29 | ### webpack/rspack
30 |
31 | ```js
32 | // webpack.config.js / rspack.config.js
33 |
34 | // You can leverage your IDE's Intellisense (autocompletion, type check, etc.) with the helper function `defineReactCompilerLoaderOption`:
35 | const { defineReactCompilerLoaderOption, reactCompilerLoader } = require('react-compiler-webpack');
36 |
37 | module.exports = {
38 | module: {
39 | rules: [
40 | {
41 | test: /\.[mc]?[jt]sx?$/i,
42 | exclude: /node_modules/,
43 | use: [
44 | // babel-loader, swc-loader, esbuild-loader, or anything you like to transpile JSX should go here.
45 | // If you are using rspack, the rspack's buiilt-in react transformation is sufficient.
46 | // { loader: 'swc-loader' },
47 | // Now add reactCompilerLoader
48 | {
49 | loader: reactCompilerLoader,
50 | options: defineReactCompilerLoaderOption({
51 | // React Compiler options goes here
52 | })
53 | }
54 | ]
55 | }
56 | ]
57 | }
58 | };
59 | ```
60 |
61 | ### Next.js
62 |
63 | Next.js has already integrated the React Compiler and can be enabled with the following configuration:
64 |
65 | ```js
66 | // next.config.js
67 | module.exports = {
68 | experimental: {
69 | reactCompiler: true // or React Compiler options
70 | }
71 | }
72 | ```
73 |
74 | Using Next.js built-in React Compiler integration is highly recommended. But if you insist on going with `react-compiler-webpack`, you can follow use the provided Next.js plugin:
75 |
76 | ```js
77 | // next.config.js
78 | const { withReactCompiler } = require('react-compiler-webpack');
79 |
80 | module.exports = withReactCompiler({
81 | // React Compiler options goes here
82 | })({
83 | // Next.js config goes here
84 | });
85 | ```
86 |
87 | ## Author
88 |
89 | **react-compiler-webpack** © [Sukka](https://github.com/SukkaW), Released under the [MIT](./LICENSE) License.
90 | Authored and maintained by Sukka with help from contributors ([list](https://github.com/SukkaW/react-compiler-webpack/graphs/contributors)).
91 |
92 | > [Personal Website](https://skk.moe) · [Blog](https://blog.skk.moe) · GitHub [@SukkaW](https://github.com/SukkaW) · Telegram Channel [@SukkaChannel](https://t.me/SukkaChannel) · Twitter [@isukkaw](https://twitter.com/isukkaw) · Mastodon [@sukka@acg.mn](https://acg.mn/@sukka) · Keybase [@sukka](https://keybase.io/sukka)
93 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('eslint-config-sukka').sukka({
4 | react: false
5 | });
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-compiler-webpack",
3 | "version": "0.2.0",
4 | "description": "The webpack/Next.js Plugin for React Compiler",
5 | "repository": "https://github.com/SukkaW/react-compiler-webpack",
6 | "main": "./dist/index.js",
7 | "module": "./dist/index.mjs",
8 | "types": "./dist/index.d.ts",
9 | "files": [
10 | "dist",
11 | "next.js"
12 | ],
13 | "scripts": {
14 | "lint": "eslint --format=sukka .",
15 | "build": "rollup -c rollup.config.ts --configPlugin swc3 --bundleConfigAsCjs",
16 | "prepublishOnly": "npm run build",
17 | "pretest": "npm run build",
18 | "test": "mocha -r @swc-node/register --require mocha-expect-snapshot 'test/index.ts'",
19 | "test:update": "UPDATE_SNAPSHOT=all mocha -r @swc-node/register --require mocha-expect-snapshot 'test/index.ts'",
20 | "prerelease": "pnpm run build && pnpm run lint",
21 | "release": "bumpp -r --all --commit \"release: %s\" --tag \"%s\""
22 | },
23 | "keywords": [
24 | "react",
25 | "react-forget",
26 | "react-compiler",
27 | "webpack-loader",
28 | "webpack",
29 | "nextjs",
30 | "app-router"
31 | ],
32 | "author": "",
33 | "license": "MIT",
34 | "dependencies": {
35 | "@babel/core": "^7.27.1",
36 | "@babel/plugin-syntax-jsx": "^7.27.1",
37 | "@babel/plugin-syntax-typescript": "^7.27.1",
38 | "expect": "^29.7.0",
39 | "loader-utils": "^3.3.1"
40 | },
41 | "devDependencies": {
42 | "@eslint-sukka/node": "^6.19.0",
43 | "@jest/expect": "^29.7.0",
44 | "@rspack/core": "^1.3.9",
45 | "@swc-node/register": "^1.10.10",
46 | "@swc/core": "^1.11.24",
47 | "@types/babel__core": "^7.20.5",
48 | "@types/loader-utils": "^2.0.6",
49 | "@types/mocha": "^10.0.10",
50 | "@types/node": "^22.15.17",
51 | "babel-plugin-react-compiler": "^19.1.0-rc.1",
52 | "browserslist": "^4.24.5",
53 | "bumpp": "^10.1.0",
54 | "eslint": "^9.26.0",
55 | "eslint-config-sukka": "^6.19.0",
56 | "eslint-formatter-sukka": "^6.19.0",
57 | "memfs": "^5.0.0-next.1",
58 | "mocha": "^11.2.2",
59 | "mocha-expect-snapshot": "^7.2.0",
60 | "next": "^15.3.2",
61 | "rimraf": "^6.0.1",
62 | "rollup": "^4.40.2",
63 | "rollup-plugin-dts": "^6.2.1",
64 | "rollup-plugin-swc3": "^0.12.1",
65 | "swc-loader": "^0.2.6",
66 | "typescript": "^5.8.3",
67 | "webpack": "^5.99.8"
68 | },
69 | "peerDependencies": {
70 | "babel-plugin-react-compiler": "*"
71 | },
72 | "packageManager": "pnpm@10.10.0",
73 | "pnpm": {
74 | "neverBuiltDependencies": []
75 | },
76 | "overrides": {
77 | "hasown": "npm:@nolyfill/hasown@latest"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/rollup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'rollup';
2 | import { swc } from 'rollup-plugin-swc3';
3 | import { dts } from 'rollup-plugin-dts';
4 |
5 | import pkgJson from './package.json';
6 | import { builtinModules } from 'node:module';
7 |
8 | const externalModules = Object.keys(pkgJson.dependencies)
9 | .concat(Object.keys(pkgJson.peerDependencies))
10 | .concat(builtinModules)
11 | .concat('next');
12 | const external = (id: string) => externalModules.some((name) => id === name || id.startsWith(`${name}/`));
13 |
14 | export default defineConfig([
15 | {
16 | input: 'src/index.ts',
17 | output: {
18 | file: 'dist/index.js',
19 | format: 'commonjs'
20 | },
21 | plugins: [
22 | swc()
23 | ],
24 | external
25 | },
26 | {
27 | input: 'src/index.ts',
28 | output: {
29 | file: 'dist/index.d.ts',
30 | format: 'commonjs'
31 | },
32 | plugins: [dts()]
33 | },
34 | {
35 | input: 'src/react-compiler-loader.ts',
36 | output: {
37 | file: 'dist/react-compiler-loader.js',
38 | format: 'commonjs'
39 | },
40 | plugins: [swc()],
41 | external
42 | }
43 | ]);
44 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export const reactCompilerLoader = require.resolve('./react-compiler-loader');
2 |
3 | export type { PluginOptions as ReactCompilerConfig } from 'babel-plugin-react-compiler';
4 | export type { ReactCompilerLoaderOption } from './react-compiler-loader';
5 |
6 | import type { ReactCompilerLoaderOption } from './react-compiler-loader';
7 | import type { NextConfig } from 'next/dist/server/config-shared';
8 | import type { RuleSetRule } from 'webpack';
9 |
10 | type RuleOptions = Omit;
11 |
12 | export const defineReactCompilerLoaderOption = (options: ReactCompilerLoaderOption) => options;
13 |
14 | export function withReactCompiler(pluginOptions?: ReactCompilerLoaderOption, ruleOptions: RuleOptions = {}) {
15 | return (nextConfig: NextConfig = {}) => {
16 | const $ruleOptions: RuleOptions = {
17 | test: /\.(mtsx|mjsx|tsx|jsx)$/,
18 | exclude: /node_modules/,
19 | ...ruleOptions
20 | };
21 |
22 | return {
23 | ...nextConfig,
24 | webpack(config, ctx) {
25 | if (typeof nextConfig.webpack === 'function') {
26 | config = nextConfig.webpack(config, ctx);
27 | }
28 |
29 | config.module.rules.push({
30 | ...$ruleOptions,
31 | use: [
32 | {
33 | loader: reactCompilerLoader,
34 | options: pluginOptions
35 | }
36 | ]
37 | } as RuleSetRule);
38 |
39 | return config;
40 | }
41 | } as NextConfig;
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/react-compiler-loader.ts:
--------------------------------------------------------------------------------
1 | import babel from '@babel/core';
2 | import BabelPluginReactCompiler from 'babel-plugin-react-compiler';
3 |
4 | import type Webpack from 'webpack';
5 | import type { PluginOptions as ReactCompilerConfig } from 'babel-plugin-react-compiler';
6 |
7 | export interface ReactCompilerLoaderOption extends Partial {
8 | babelTransFormOpt?: babel.TransformOptions
9 | }
10 |
11 | const defaultBabelParsePlugins: NonNullable['plugins']> = ['jsx', 'typescript'];
12 |
13 | export default async function reactCompilerLoader(this: Webpack.LoaderContext, input: string, _inputSourceMap: any) {
14 | const callback = this.async();
15 |
16 | // TODO: is it possible to bail out early if the input doesn't contain a react component?
17 | try {
18 | const { babelTransFormOpt, ...reactCompilerConfig } = this.getOptions();
19 |
20 | const result = await babel.transformAsync(input, {
21 | sourceFileName: this.resourcePath,
22 | filename: this.resourcePath,
23 | cloneInputAst: false,
24 | // user configured babel option
25 | ...babelTransFormOpt,
26 | // override babel plugins
27 | plugins: [
28 | [BabelPluginReactCompiler, reactCompilerConfig],
29 | ...(babelTransFormOpt?.plugins || [])
30 | ],
31 | // override babel parserOpts
32 | parserOpts: {
33 | ...babelTransFormOpt?.parserOpts,
34 | // override babel parserOpts plugins and add jsx
35 | plugins: [
36 | ...(babelTransFormOpt?.parserOpts?.plugins || []),
37 | ...defaultBabelParsePlugins
38 | ]
39 | },
40 | ast: false,
41 | sourceMaps: true,
42 | configFile: false,
43 | babelrc: false
44 | });
45 |
46 | if (!result) {
47 | throw new TypeError('babel.transformAsync with react compiler plugin returns null');
48 | }
49 |
50 | const { code, map } = result;
51 | callback(null, code ?? undefined, map ?? undefined);
52 | } catch (e) {
53 | callback(e as Error);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/test/__snapshots__/index.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`react-compiler-webpack (rspack) facebook/react issue #29120: react-compiler-webpack (rspack):cjk.tsx 1`] = `
4 | "import { jsx as _jsx } from "react/jsx-runtime";
5 | import { c as _c } from "react/compiler-runtime";
6 | // https://github.com/facebook/react/issues/29120
7 | export default function Comp() {
8 | const $ = _c(1);
9 | let t0;
10 | if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
11 | t0 = /*#__PURE__*/ _jsx("div", {
12 | "aria-label": "\\u6211\\u80FD\\u541E\\u4E0B\\u73BB\\u7483\\u800C\\u4E0D\\u4F24\\u8EAB\\u4F53",
13 | children: "\\u6211\\u80FD\\u541E\\u4E0B\\u73BB\\u7483\\u800C\\u4E0D\\u4F24\\u8EAB\\u4F53"
14 | });
15 | $[0] = t0;
16 | } else {
17 | t0 = $[0];
18 | }
19 | return t0;
20 | }
21 | "
22 | `;
23 |
24 | exports[`react-compiler-webpack (rspack) should optimize complex component: react-compiler-webpack (rspack):complex.tsx 1`] = `
25 | "import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
26 | import { useState, useRef, useCallback } from 'react';
27 | export default function Sukka() {
28 | const [count, setCount] = useState(0);
29 | const singletonRef = useRef(null);
30 | if (!singletonRef.current) {
31 | singletonRef.current = new AbortController();
32 | }
33 | const handleButtonClick = useCallback(()=>setCount((count_0)=>count_0 + 1), []);
34 | if (count > 10) return null;
35 | return /*#__PURE__*/ _jsxs("div", {
36 | children: [
37 | /*#__PURE__*/ _jsxs("p", {
38 | children: [
39 | "Count: ",
40 | count
41 | ]
42 | }),
43 | /*#__PURE__*/ _jsx("button", {
44 | onClick: handleButtonClick,
45 | children: "Increment"
46 | }),
47 | count > 10 && /*#__PURE__*/ _jsx("p", {
48 | children: "Count is higher than 10"
49 | }),
50 | /*#__PURE__*/ _jsx("button", {
51 | disabled: count > 0,
52 | onClick: ()=>setCount(0),
53 | children: "Reset Count"
54 | })
55 | ]
56 | });
57 | }
58 | "
59 | `;
60 |
61 | exports[`react-compiler-webpack (rspack) should work with tsx: react-compiler-webpack (rspack):simple.tsx 1`] = `
62 | "import { jsx as _jsx } from "react/jsx-runtime";
63 | import { c as _c } from "react/compiler-runtime";
64 | export default function Example() {
65 | const $ = _c(2);
66 | let t0;
67 | if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
68 | t0 = [
69 | 1,
70 | 2,
71 | 3,
72 | 4
73 | ];
74 | $[0] = t0;
75 | } else {
76 | t0 = $[0];
77 | }
78 | const value = t0;
79 | let t1;
80 | if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
81 | t1 = /*#__PURE__*/ _jsx("div", {
82 | children: value.map(_temp)
83 | });
84 | $[1] = t1;
85 | } else {
86 | t1 = $[1];
87 | }
88 | return t1;
89 | }
90 | function _temp(i) {
91 | return /*#__PURE__*/ _jsx("p", {
92 | children: i
93 | }, i);
94 | }
95 | export const AnotherExmaple = ()=>{
96 | const $ = _c(2);
97 | let t0;
98 | if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
99 | t0 = [
100 | 1,
101 | 2,
102 | 3,
103 | 4
104 | ];
105 | $[0] = t0;
106 | } else {
107 | t0 = $[0];
108 | }
109 | const value = t0;
110 | let t1;
111 | if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
112 | t1 = /*#__PURE__*/ _jsx("div", {
113 | children: value.map(_temp2)
114 | });
115 | $[1] = t1;
116 | } else {
117 | t1 = $[1];
118 | }
119 | return t1;
120 | };
121 | function _temp2(i) {
122 | return /*#__PURE__*/ _jsx("p", {
123 | children: i
124 | }, i);
125 | }
126 | "
127 | `;
128 |
129 | exports[`react-compiler-webpack (rspack) should work: react-compiler-webpack (rspack):simple.jsx 1`] = `
130 | "import { jsx as _jsx } from "react/jsx-runtime";
131 | import { c as _c } from "react/compiler-runtime";
132 | export default function Example() {
133 | const $ = _c(1);
134 | let t0;
135 | if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
136 | const value = [
137 | 1,
138 | 2,
139 | 3,
140 | 4
141 | ];
142 | t0 = /*#__PURE__*/ _jsx("div", {
143 | children: value.map(_temp)
144 | });
145 | $[0] = t0;
146 | } else {
147 | t0 = $[0];
148 | }
149 | return t0;
150 | }
151 | function _temp(i) {
152 | return /*#__PURE__*/ _jsx("p", {
153 | children: i
154 | }, i);
155 | }
156 | "
157 | `;
158 |
--------------------------------------------------------------------------------
/test/fixtures/cjk.tsx:
--------------------------------------------------------------------------------
1 | // https://github.com/facebook/react/issues/29120
2 | export default function Comp() {
3 | return (
4 |
5 | 我能吞下玻璃而不伤身体
6 |
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/test/fixtures/complex.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useCallback } from 'react';
2 |
3 | export default function Sukka() {
4 | const [count, setCount] = useState(0);
5 |
6 | const singletonRef = useRef(null);
7 |
8 | if (!singletonRef.current) {
9 | singletonRef.current = new AbortController();
10 | }
11 |
12 | const handleButtonClick = useCallback(() => setCount(count => count + 1), []);
13 |
14 | if (count > 10) return null;
15 |
16 | return (
17 |
18 |
Count: {count}
19 |
20 |
21 | {count > 10 && (
22 |
Count is higher than 10
23 | )}
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/test/fixtures/simple.jsx:
--------------------------------------------------------------------------------
1 | export default function Example() {
2 | const value = [1, 2, 3, 4];
3 |
4 | return (
5 |
6 | {value.map(i =>
{i}
)}
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/test/fixtures/simple.tsx:
--------------------------------------------------------------------------------
1 | export default function Example() {
2 | const value = [1, 2, 3, 4] as const;
3 |
4 | return (
5 |
6 | {value.map(i =>
{i}
)}
7 |
8 | );
9 | }
10 |
11 | export const AnotherExmaple = () => {
12 | const value = [1, 2, 3, 4] as const;
13 |
14 | return (
15 |
16 | {value.map(i =>
{i}
)}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/test/index.ts:
--------------------------------------------------------------------------------
1 | import { defineReactCompilerLoaderOption } from '../dist';
2 |
3 | import getWebpackCompiler from './utils/get-webpack-compiler';
4 | import getRspackCompiler from './utils/get-rspack-compiler';
5 |
6 | import compile from './utils/compile';
7 | import getModuleSource from './utils/get-module-source';
8 |
9 | import 'mocha-expect-snapshot';
10 |
11 | import { jestExpect as expect } from '@jest/expect';
12 |
13 | const defaultOption = defineReactCompilerLoaderOption({
14 | sources() { return true; }
15 | });
16 |
17 | ([
18 | ['react-compiler-webpack (webpack)', getWebpackCompiler],
19 | ['react-compiler-webpack (rspack)', getRspackCompiler]
20 | ] as const).forEach(([name, getCompiler]) => {
21 | describe(name, () => {
22 | it('defineReactCompilerLoaderOption', () => {
23 | const opt = {};
24 | const definedOpt = defineReactCompilerLoaderOption(opt);
25 |
26 | expect(definedOpt).toBe(opt);
27 | });
28 |
29 | it('should work', async () => {
30 | const [compiler, fs] = getCompiler('./simple.jsx', defaultOption);
31 | const stats = await compile(compiler);
32 |
33 | expect(getModuleSource('./simple.jsx', stats, fs)).toMatchSnapshot(name + ':simple.jsx');
34 | });
35 |
36 | it('should work with tsx', async () => {
37 | const [compiler, fs] = getCompiler('./simple.tsx', defaultOption);
38 | const stats = await compile(compiler);
39 |
40 | expect(getModuleSource('./simple.tsx', stats, fs)).toMatchSnapshot(name + ':simple.tsx');
41 | });
42 |
43 | it('should optimize complex component', async () => {
44 | const [compiler, fs] = getCompiler('./complex.tsx', defaultOption);
45 | const stats = await compile(compiler);
46 |
47 | expect(getModuleSource('./complex.tsx', stats, fs)).toMatchSnapshot(name + ':complex.tsx');
48 | });
49 |
50 | // https://github.com/facebook/react/issues/29120
51 | it('facebook/react issue #29120', async () => {
52 | const [compiler, fs] = getCompiler('./cjk.tsx', defaultOption);
53 | const stats = await compile(compiler);
54 |
55 | expect(getModuleSource('./cjk.tsx', stats, fs)).toMatchSnapshot(name + ':cjk.tsx');
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/test/utils/compile.ts:
--------------------------------------------------------------------------------
1 | import type { Compiler as RspackCompiler, Stats as RspackStats, MultiStats as RspackMultiStats } from '@rspack/core';
2 | import type Webpack from 'webpack';
3 |
4 | export default function compile(compiler: Webpack.Compiler | RspackCompiler): Promise {
5 | return new Promise((resolve, reject) => {
6 | compiler.run((error, stats) => {
7 | if (error) {
8 | return reject(error);
9 | }
10 |
11 | if (!stats) {
12 | return reject(new TypeError('stats from compiler is null'));
13 | }
14 |
15 | return resolve(stats);
16 | });
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/test/utils/get-module-source.ts:
--------------------------------------------------------------------------------
1 | import type Webpack from 'webpack';
2 | import type { Stats as RspackStats, MultiStats as RspackMultiStats } from '@rspack/core';
3 |
4 | export default function getModuleSource(id: string, stats: Webpack.Stats | RspackStats | RspackMultiStats, fs: typeof import('fs')) {
5 | const jsonStat = stats.toJson({ source: true });
6 | const $module = jsonStat.modules?.find((m) => m.name?.endsWith(id));
7 |
8 | if (!$module) {
9 | throw new TypeError(`Module ${id} not found`);
10 | }
11 |
12 | if ('source' in $module) return $module.source;
13 | return fs.readFileSync('/main.bundle.js', { encoding: 'utf-8' });
14 | }
15 |
--------------------------------------------------------------------------------
/test/utils/get-rspack-compiler.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import process from 'node:process';
3 |
4 | import { rspack } from '@rspack/core';
5 | import { createFsFromVolume, Volume } from 'memfs';
6 |
7 | import type { RspackOptions } from '@rspack/core';
8 | import { externalModules } from './get-webpack-compiler';
9 |
10 | import type { ReactCompilerLoaderOption } from '../../src';
11 |
12 | import { reactCompilerLoader } from '../../dist';
13 | import { useSwcLoader } from './get-swc-loader';
14 |
15 | export default (fixture: string, loaderOptions?: ReactCompilerLoaderOption, config: RspackOptions = {}) => {
16 | process.env.RSPACK_CONFIG_VALIDATE = 'loose';
17 | const fullConfig: RspackOptions = {
18 | mode: 'development',
19 | target: 'web',
20 | devtool: config.devtool || false,
21 | context: path.resolve(__dirname, '../fixtures'),
22 | entry: Array.isArray(fixture)
23 | ? fixture
24 | : path.resolve(__dirname, '../fixtures', fixture),
25 | output: {
26 | path: '/',
27 | filename: '[name].bundle.js',
28 | chunkFilename: '[name].chunk.js',
29 | publicPath: '/webpack/public/path/',
30 | assetModuleFilename: '[name][ext]'
31 | },
32 | module: {
33 | rules: [
34 | {
35 | test: /\.[cm]?jsx$/i,
36 | exclude: /node_modules/,
37 | use: [
38 | useSwcLoader(false, 'builtin:swc-loader'),
39 | {
40 | loader: reactCompilerLoader,
41 | options: loaderOptions
42 | }
43 | ]
44 | },
45 | {
46 | test: /\.[cm]?tsx$/i,
47 | exclude: /node_modules/,
48 | use: [
49 | useSwcLoader(true, 'builtin:swc-loader'),
50 | {
51 | loader: reactCompilerLoader,
52 | options: loaderOptions
53 | }
54 | ]
55 | },
56 | {
57 | test: /\.[cm]?[jt]sx$/i,
58 | exclude: /node_modules/,
59 | use: [
60 | {
61 | loader: reactCompilerLoader,
62 | options: loaderOptions
63 | }
64 | ]
65 | }]
66 | },
67 | optimization: { minimize: false },
68 | externals: externalModules,
69 | plugins: [],
70 | stats: {
71 | preset: 'verbose',
72 | all: true,
73 | modules: true,
74 | chunks: true
75 | },
76 | ...config
77 | };
78 |
79 | const compiler = rspack(fullConfig);
80 | const fs = createFsFromVolume(new Volume()) as unknown as typeof import('fs');
81 |
82 | compiler.outputFileSystem = fs;
83 |
84 | return [compiler, fs] as const;
85 | };
86 |
--------------------------------------------------------------------------------
/test/utils/get-swc-loader.ts:
--------------------------------------------------------------------------------
1 | import type { Options as SwcOptions } from '@swc/core';
2 |
3 | export function useSwcLoader(isTSX: boolean, loader = 'swc-loader') {
4 | return {
5 | loader,
6 | options: {
7 | jsc: {
8 | parser: {
9 | syntax: isTSX ? 'typescript' : 'ecmascript',
10 | ...(
11 | isTSX
12 | ? { tsx: true }
13 | : { jsx: true }
14 | )
15 | },
16 | target: 'esnext',
17 | transform: {
18 | react: {
19 | runtime: 'automatic',
20 | refresh: false,
21 | development: false
22 | }
23 | }
24 | }
25 | } satisfies SwcOptions
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/test/utils/get-webpack-compiler.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 |
3 | import type Webpack from 'webpack';
4 | import { webpack } from 'webpack';
5 | import { createFsFromVolume, Volume } from 'memfs';
6 |
7 | import type { ReactCompilerLoaderOption } from '../../src';
8 |
9 | import pkgJson from '../../package.json';
10 | import { builtinModules } from 'node:module';
11 | import { useSwcLoader } from './get-swc-loader';
12 |
13 | import { reactCompilerLoader } from '../../dist';
14 |
15 | export const externalModules = Object.keys(pkgJson.dependencies)
16 | .concat(Object.keys(pkgJson.peerDependencies))
17 | .concat(builtinModules)
18 | .concat(['react', 'react/jsx-runtime', 'forgetti', 'forgetti/runtime', 'preact/hooks', 'preact/compat', 'preact']);
19 | export default (fixture: string, loaderOptions?: ReactCompilerLoaderOption, config: Webpack.Configuration = {}) => {
20 | const fullConfig: Webpack.Configuration = {
21 | mode: 'development',
22 | target: 'web',
23 | devtool: config.devtool || false,
24 | context: path.resolve(__dirname, '../fixtures'),
25 | entry: Array.isArray(fixture)
26 | ? fixture
27 | : path.resolve(__dirname, '../fixtures', fixture),
28 | output: {
29 | path: '/',
30 | filename: '[name].bundle.js',
31 | chunkFilename: '[name].chunk.js',
32 | publicPath: '/webpack/public/path/',
33 | assetModuleFilename: '[name][ext]'
34 | },
35 | module: {
36 | rules: [
37 | {
38 | test: /\.[cm]?jsx$/i,
39 | exclude: /node_modules/,
40 | use: [
41 | useSwcLoader(false),
42 | {
43 | loader: reactCompilerLoader,
44 | options: loaderOptions
45 | }
46 | ]
47 | },
48 | {
49 | test: /\.[cm]?tsx$/i,
50 | exclude: /node_modules/,
51 | use: [
52 | useSwcLoader(true),
53 | {
54 | loader: reactCompilerLoader,
55 | options: loaderOptions
56 | }
57 | ]
58 | }
59 | // {
60 | // test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/i,
61 | // resourceQuery: /^(?!.*\?ignore-asset-modules).*$/,
62 | // type: 'asset/resource'
63 | // },
64 | // {
65 | // resourceQuery: /\?ignore-asset-modules$/,
66 | // type: 'javascript/auto'
67 | // }
68 | ]
69 | },
70 | optimization: { minimize: false },
71 | externals: externalModules,
72 | plugins: [],
73 | ...config
74 | };
75 |
76 | const compiler = webpack(fullConfig);
77 | const fs = createFsFromVolume(new Volume()) as unknown as typeof import('fs');
78 |
79 | compiler.outputFileSystem = fs;
80 |
81 | return [compiler, fs] as const;
82 | };
83 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "resolveJsonModule": true,
6 | "declaration": true,
7 | "outDir": "./dist",
8 | "isolatedModules": true,
9 | "esModuleInterop": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "strict": true
12 | },
13 | "include": ["src/**/*", "test/**/*", "next.d.ts"],
14 | "exclude": ["node_modules", "dist", "test/fixtures/**/*"]
15 | }
16 |
--------------------------------------------------------------------------------