├── .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 | --------------------------------------------------------------------------------