@acusti/vite-plugin-react-compiler 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @acusti/vite-plugin-react-compiler
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@acusti/vite-plugin-react-compiler)
|
|
4
|
+
[](https://npms.io/search?q=%40acusti%2Fvite-plugin-react-compiler)
|
|
5
|
+
[](https://www.npmjs.com/package/@acusti/vite-plugin-react-compiler)
|
|
6
|
+
|
|
7
|
+
A [Vite][] plugin that runs [React Compiler][] via [oxc-transform-react][],
|
|
8
|
+
the native Node bindings for the oxc project’s Rust port of the compiler —
|
|
9
|
+
a drop-in alternative to the Babel-based React Compiler build path.
|
|
10
|
+
|
|
11
|
+
It was extracted from the build tooling of [Outlyne][], where it runs in
|
|
12
|
+
production.
|
|
13
|
+
|
|
14
|
+
[vite]: https://vite.dev
|
|
15
|
+
[react compiler]: https://react.dev/learn/react-compiler
|
|
16
|
+
[oxc-transform-react]: https://www.npmjs.com/package/oxc-transform-react
|
|
17
|
+
[outlyne]: https://outlyne.io
|
|
18
|
+
|
|
19
|
+
## Why this exists
|
|
20
|
+
|
|
21
|
+
Vite/Rolldown decided against shipping the Rust React Compiler integration
|
|
22
|
+
in Vite core over the binary size it would add, so the official React
|
|
23
|
+
Compiler path for Vite remains Babel: @vitejs/plugin-react’s
|
|
24
|
+
`reactCompilerPreset` running babel-plugin-react-compiler via
|
|
25
|
+
[@rolldown/plugin-babel][]. That works, but it drags a full Babel
|
|
26
|
+
parse/transform/print pipeline through every source file of an otherwise
|
|
27
|
+
all-native oxc/rolldown build.
|
|
28
|
+
|
|
29
|
+
This plugin is the published native alternative: a `transform`-hook plugin
|
|
30
|
+
that calls [oxc-transform-react][], the Rust React Compiler bindings the
|
|
31
|
+
oxc project publishes. The compiler pass runs with `enforce: 'pre'` (the
|
|
32
|
+
same position the Babel pass occupies today) and emits JSX untouched
|
|
33
|
+
(`jsx: 'preserve'`), so Vite’s own oxc transform stays in charge of
|
|
34
|
+
JSX/refresh/TypeScript handling downstream — the plugin only adds the
|
|
35
|
+
compiler pass. (oxc-transform-react does strip TypeScript syntax, which
|
|
36
|
+
matches how the Babel path uses @babel/preset-typescript.)
|
|
37
|
+
|
|
38
|
+
[@rolldown/plugin-babel]:
|
|
39
|
+
https://www.npmjs.com/package/@rolldown/plugin-babel
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
npm install --save-dev @acusti/vite-plugin-react-compiler
|
|
45
|
+
# or
|
|
46
|
+
yarn add --dev @acusti/vite-plugin-react-compiler
|
|
47
|
+
# or
|
|
48
|
+
pnpm add --save-dev @acusti/vite-plugin-react-compiler
|
|
49
|
+
# or
|
|
50
|
+
bun add --dev @acusti/vite-plugin-react-compiler
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The plugin depends on an exact, tested version of [oxc-transform-react][]:
|
|
54
|
+
the bindings are on a fast-moving 0.x release train tracking React Compiler
|
|
55
|
+
main, and new bindings releases are close to 100% bug fixes (the Rust port
|
|
56
|
+
chases parity with the existing widely-used Babel plugin), so the plugin
|
|
57
|
+
pins the newest version that passes this repo’s test suite and ships it in
|
|
58
|
+
regular plugin releases. If you need a different version, use your package
|
|
59
|
+
manager’s `overrides` (npm, pnpm, bun) or `resolutions` (yarn) to force it.
|
|
60
|
+
|
|
61
|
+
Like [@acusti/vite-plugin-svg-react][], this plugin requires Vite ≥ 8, on
|
|
62
|
+
purpose: its reason for existing is completing the all-native oxc/rolldown
|
|
63
|
+
pipeline that Vite 8 introduced.
|
|
64
|
+
|
|
65
|
+
[@acusti/vite-plugin-svg-react]:
|
|
66
|
+
https://www.npmjs.com/package/@acusti/vite-plugin-svg-react
|
|
67
|
+
|
|
68
|
+
Add the plugin to your vite config (and remove any Babel-based React
|
|
69
|
+
Compiler wiring, e.g. `reactCompilerPreset` + @rolldown/plugin-babel):
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// vite.config.ts
|
|
73
|
+
import reactCompiler from '@acusti/vite-plugin-react-compiler';
|
|
74
|
+
import react from '@vitejs/plugin-react';
|
|
75
|
+
import { defineConfig } from 'vite';
|
|
76
|
+
|
|
77
|
+
export default defineConfig({
|
|
78
|
+
plugins: [react(), reactCompiler()],
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Options
|
|
83
|
+
|
|
84
|
+
The plugin takes an optional options object with four properties:
|
|
85
|
+
|
|
86
|
+
- **`include`** / **`exclude`**: standard Vite
|
|
87
|
+
[`createFilter`](https://vite.dev/guide/api-plugin#filtering-include-exclude-pattern)
|
|
88
|
+
patterns selecting which modules get the compiler pass. Defaults:
|
|
89
|
+
`include: /\.[jt]sx?$/`, `exclude: /[/\\]node_modules[/\\]/`.
|
|
90
|
+
- **`reactCompiler`**: options passed verbatim to React Compiler, using the
|
|
91
|
+
same names as [babel-plugin-react-compiler][compiler options]:
|
|
92
|
+
`compilationMode`, `panicThreshold` (defaults to `'none'`), `target`
|
|
93
|
+
(defaults to `'19'`), `environment` flags like
|
|
94
|
+
`enableTreatRefLikeIdentifiersAsRefs`, and so on. Callback-valued options
|
|
95
|
+
(e.g. `logger`, function-valued `sources`) don’t cross the native
|
|
96
|
+
boundary and aren’t supported; `sources` accepts an array of filename
|
|
97
|
+
substrings instead.
|
|
98
|
+
- **`memoize`**: cache transform results per module id, keyed by a content
|
|
99
|
+
hash (bounded to one entry per file). Multi-environment builds (e.g.
|
|
100
|
+
React Router apps building client + SSR environments) run every transform
|
|
101
|
+
once per environment; the cache makes the repeat passes free. Defaults to
|
|
102
|
+
`true`.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
reactCompiler({
|
|
106
|
+
exclude: [/node_modules/, /\.stories\./],
|
|
107
|
+
reactCompiler: {
|
|
108
|
+
environment: { enableTreatRefLikeIdentifiersAsRefs: true },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If the compiler reports a fatal error (a parse failure or rejected
|
|
114
|
+
options), the plugin fails the build via `this.error(...)`, just like a
|
|
115
|
+
Babel syntax error would. Nonfatal compiler bail-outs behave as they do
|
|
116
|
+
with the Babel plugin under `panicThreshold: 'none'`: the affected function
|
|
117
|
+
is left uncompiled and the build continues.
|
|
118
|
+
|
|
119
|
+
[compiler options]:
|
|
120
|
+
https://react.dev/reference/react-compiler/configuration
|
|
121
|
+
|
|
122
|
+
## Caveats, and results from production
|
|
123
|
+
|
|
124
|
+
The Rust port of React Compiler and the oxc-transform-react bindings are
|
|
125
|
+
experimental and explicitly labeled as such upstream. Measure and verify
|
|
126
|
+
against the Babel plugin before trusting it (the `__COMPILER_RUNTIME.c`
|
|
127
|
+
memo cache calls in output are easy to diff). Alongside the plugin’s own
|
|
128
|
+
tests, this repo runs an upstream-tracking suite against the pinned
|
|
129
|
+
bindings version: it pins the ways the Rust port already exceeds
|
|
130
|
+
babel-plugin-react-compiler@1.0.0 by tracking React Compiler main (computed
|
|
131
|
+
object keys and try/catch value blocks both compile), pins Babel-parity
|
|
132
|
+
fixes as they land (react-hooks lint suppressions opt functions out of
|
|
133
|
+
compilation since 0.144.0), and tracks known upstream divergences as they
|
|
134
|
+
arise, re-verified on every bindings bump before it ships in a plugin
|
|
135
|
+
release.
|
|
136
|
+
|
|
137
|
+
Verified results from the production Vite 8 + React Router app this plugin
|
|
138
|
+
was extracted from, compared against babel-plugin-react-compiler@1.0.0
|
|
139
|
+
across 987 source files:
|
|
140
|
+
|
|
141
|
+
- **Full memoization parity with zero regressions** — every function the
|
|
142
|
+
Babel plugin compiles, the Rust port compiles. It compiles a strict
|
|
143
|
+
superset, since the Rust port tracks React Compiler main rather than the
|
|
144
|
+
latest stable release.
|
|
145
|
+
- **The compiler pass is ~16x faster** than the Babel pass.
|
|
146
|
+
- **The full production build is ~2.5x faster** (53s → 21s).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ReactCompilerOptions } from 'oxc-transform-react';
|
|
2
|
+
import { type FilterPattern, type Plugin } from 'vite';
|
|
3
|
+
export type Options = {
|
|
4
|
+
/**
|
|
5
|
+
* Modules to exclude from the compiler pass (takes precedence over
|
|
6
|
+
* include).
|
|
7
|
+
* @default /[/\\]node_modules[/\\]/
|
|
8
|
+
*/
|
|
9
|
+
exclude?: FilterPattern;
|
|
10
|
+
/**
|
|
11
|
+
* Modules to run the compiler pass on.
|
|
12
|
+
* @default /\.[jt]sx?$/
|
|
13
|
+
*/
|
|
14
|
+
include?: FilterPattern;
|
|
15
|
+
/**
|
|
16
|
+
* Cache transform results by module id + content hash so repeated
|
|
17
|
+
* transforms of identical content (e.g. one per environment in
|
|
18
|
+
* multi-environment builds) only run the compiler once per file.
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
memoize?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Options passed verbatim to React Compiler, using the same names as
|
|
24
|
+
* babel-plugin-react-compiler (compilationMode, panicThreshold
|
|
25
|
+
* (defaults to 'none'), target (defaults to '19'), environment, etc.).
|
|
26
|
+
*/
|
|
27
|
+
reactCompiler?: ReactCompilerOptions;
|
|
28
|
+
};
|
|
29
|
+
export default function vitePluginReactCompiler(options?: Options): Plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { transform } from "oxc-transform-react";
|
|
4
|
+
import { createFilter } from "vite";
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
var defaultExclude = /[/\\]node_modules[/\\]/;
|
|
7
|
+
var defaultInclude = /\.[jt]sx?$/;
|
|
8
|
+
function vitePluginReactCompiler(options = {}) {
|
|
9
|
+
const filter = createFilter(options.include ?? defaultInclude, options.exclude ?? defaultExclude);
|
|
10
|
+
const isMemoizing = options.memoize !== false;
|
|
11
|
+
const cache = /* @__PURE__ */ new Map();
|
|
12
|
+
return {
|
|
13
|
+
enforce: "pre",
|
|
14
|
+
name: "vite-plugin-react-compiler",
|
|
15
|
+
transform(code, id) {
|
|
16
|
+
const [filepath] = id.split("?");
|
|
17
|
+
if (!filter(filepath)) return null;
|
|
18
|
+
const run = async () => {
|
|
19
|
+
const transformed = await transform(filepath, code, {
|
|
20
|
+
jsx: "preserve",
|
|
21
|
+
reactCompiler: options.reactCompiler ?? {},
|
|
22
|
+
sourcemap: true
|
|
23
|
+
});
|
|
24
|
+
if (transformed.fatal) {
|
|
25
|
+
const byteStart = transformed.errors[0]?.labels[0]?.start;
|
|
26
|
+
this.error(transformed.errors.map((error) => error.codeframe ? `${error.message}\n${error.codeframe}` : error.message).join("\n"), byteStart == null ? void 0 : Buffer.from(code, "utf8").subarray(0, byteStart).toString("utf8").length);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
code: transformed.code,
|
|
30
|
+
map: transformed.map
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
if (!isMemoizing) return run();
|
|
34
|
+
const hash = createHash("sha1").update(code).digest("hex");
|
|
35
|
+
const cached = cache.get(filepath);
|
|
36
|
+
if (cached?.hash === hash) return cached.result;
|
|
37
|
+
const result = run();
|
|
38
|
+
cache.set(filepath, {
|
|
39
|
+
hash,
|
|
40
|
+
result
|
|
41
|
+
});
|
|
42
|
+
result.catch(() => {
|
|
43
|
+
if (cache.get(filepath)?.result === result) cache.delete(filepath);
|
|
44
|
+
});
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { vitePluginReactCompiler as default };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport { createHash } from 'node:crypto';\n\nimport {\n type ReactCompilerOptions,\n type SourceMap,\n transform,\n} from 'oxc-transform-react';\nimport { createFilter, type FilterPattern, type Plugin } from 'vite';\n\nexport type Options = {\n /**\n * Modules to exclude from the compiler pass (takes precedence over\n * include).\n * @default /[/\\\\]node_modules[/\\\\]/\n */\n exclude?: FilterPattern;\n /**\n * Modules to run the compiler pass on.\n * @default /\\.[jt]sx?$/\n */\n include?: FilterPattern;\n /**\n * Cache transform results by module id + content hash so repeated\n * transforms of identical content (e.g. one per environment in\n * multi-environment builds) only run the compiler once per file.\n * @default true\n */\n memoize?: boolean;\n /**\n * Options passed verbatim to React Compiler, using the same names as\n * babel-plugin-react-compiler (compilationMode, panicThreshold\n * (defaults to 'none'), target (defaults to '19'), environment, etc.).\n */\n reactCompiler?: ReactCompilerOptions;\n};\n\ntype CachedTransform = {\n hash: string;\n result: Promise<TransformOutput>;\n};\n\ntype TransformOutput = { code: string; map?: SourceMap };\n\nconst defaultExclude = /[/\\\\]node_modules[/\\\\]/;\nconst defaultInclude = /\\.[jt]sx?$/;\n\nexport default function vitePluginReactCompiler(options: Options = {}): Plugin {\n const filter = createFilter(\n options.include ?? defaultInclude,\n options.exclude ?? defaultExclude,\n );\n const isMemoizing = options.memoize !== false;\n // one entry per module id, replaced whenever the content hash changes;\n // holds the promise so overlapping transforms of identical content\n // (e.g. concurrent client + SSR environments) share a single run\n const cache = new Map<string, CachedTransform>();\n\n return {\n enforce: 'pre',\n name: 'vite-plugin-react-compiler',\n transform(code, id) {\n // vite module ids can carry a query suffix (e.g. sub-request\n // queries from other plugins); filter, cache, and compile by\n // the underlying file path — like @vitejs/plugin-react — so\n // the cache stays one-entry-per-file and sourcemap sources\n // don’t include vite queries\n const [filepath] = id.split('?');\n if (!filter(filepath)) return null;\n\n const run = async (): Promise<TransformOutput> => {\n const transformed = await transform(filepath, code, {\n // leave JSX for vite’s own pipeline (refresh, dev runtime)\n jsx: 'preserve',\n reactCompiler: options.reactCompiler ?? {},\n sourcemap: true,\n });\n\n // fatal means a parse failure or rejected options: no code\n // was emitted, so break the build like a Babel syntax error\n // would, forwarding the first error’s position so vite can\n // report the location alongside oxc’s own code frames\n if (transformed.fatal) {\n // oxc label offsets are utf-8 byte offsets, but rollup’s\n // pos argument indexes the (utf-16) code string\n const byteStart = transformed.errors[0]?.labels[0]?.start;\n this.error(\n transformed.errors\n .map((error) =>\n error.codeframe\n ? `${error.message}\\n${error.codeframe}`\n : error.message,\n )\n .join('\\n'),\n byteStart == null\n ? undefined\n : Buffer.from(code, 'utf8')\n .subarray(0, byteStart)\n .toString('utf8').length,\n );\n }\n\n return { code: transformed.code, map: transformed.map };\n };\n\n if (!isMemoizing) return run();\n\n const hash = createHash('sha1').update(code).digest('hex');\n const cached = cache.get(filepath);\n if (cached?.hash === hash) return cached.result;\n\n const result = run();\n cache.set(filepath, { hash, result });\n // drop failed transforms so the next request retries them\n result.catch(() => {\n if (cache.get(filepath)?.result === result) cache.delete(filepath);\n });\n return result;\n },\n };\n}\n"],"mappings":";;;;;AA4CA,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AAEvB,SAAwB,wBAAwB,UAAmB,CAAC,GAAW;CAC3E,MAAM,SAAS,aACX,QAAQ,WAAW,gBACnB,QAAQ,WAAW,cACvB;CACA,MAAM,cAAc,QAAQ,YAAY;CAIxC,MAAM,wBAAQ,IAAI,IAA6B;CAE/C,OAAO;EACH,SAAS;EACT,MAAM;EACN,UAAU,MAAM,IAAI;GAMhB,MAAM,CAAC,YAAY,GAAG,MAAM,GAAG;GAC/B,IAAI,CAAC,OAAO,QAAQ,GAAG,OAAO;GAE9B,MAAM,MAAM,YAAsC;IAC9C,MAAM,cAAc,MAAM,UAAU,UAAU,MAAM;KAEhD,KAAK;KACL,eAAe,QAAQ,iBAAiB,CAAC;KACzC,WAAW;IACf,CAAC;IAMD,IAAI,YAAY,OAAO;KAGnB,MAAM,YAAY,YAAY,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE;KACpD,KAAK,MACD,YAAY,OACP,KAAK,UACF,MAAM,YACA,GAAG,MAAM,QAAQ,IAAI,MAAM,cAC3B,MAAM,OAChB,CAAC,CACA,KAAK,IAAI,GACd,aAAa,OACP,KAAA,IACA,OAAO,KAAK,MAAM,MAAM,CAAC,CACpB,SAAS,GAAG,SAAS,CAAC,CACtB,SAAS,MAAM,CAAC,CAAC,MAChC;IACJ;IAEA,OAAO;KAAE,MAAM,YAAY;KAAM,KAAK,YAAY;IAAI;GAC1D;GAEA,IAAI,CAAC,aAAa,OAAO,IAAI;GAE7B,MAAM,OAAO,WAAW,MAAM,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK;GACzD,MAAM,SAAS,MAAM,IAAI,QAAQ;GACjC,IAAI,QAAQ,SAAS,MAAM,OAAO,OAAO;GAEzC,MAAM,SAAS,IAAI;GACnB,MAAM,IAAI,UAAU;IAAE;IAAM;GAAO,CAAC;GAEpC,OAAO,YAAY;IACf,IAAI,MAAM,IAAI,QAAQ,CAAC,EAAE,WAAW,QAAQ,MAAM,OAAO,QAAQ;GACrE,CAAC;GACD,OAAO;EACX;CACJ;AACJ"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acusti/vite-plugin-react-compiler",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Vite plugin that runs React Compiler via oxc-transform-react, the native Rust port’s Node bindings, instead of Babel",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"memoization",
|
|
7
|
+
"oxc",
|
|
8
|
+
"react",
|
|
9
|
+
"react-compiler",
|
|
10
|
+
"rolldown",
|
|
11
|
+
"rust",
|
|
12
|
+
"ts",
|
|
13
|
+
"typescript",
|
|
14
|
+
"vite",
|
|
15
|
+
"vite-plugin"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/acusti/uikit/tree/main/packages/vite-plugin-react-compiler#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/acusti/uikit/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "Unlicense",
|
|
22
|
+
"author": "andrew patton <andrew@acusti.ca> (https://www.acusti.ca)",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/acusti/uikit.git",
|
|
26
|
+
"directory": "packages/vite-plugin-react-compiler"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "vitest",
|
|
40
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
41
|
+
"tsc": "tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"oxc-transform-react": "0.144.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^26.1.0",
|
|
48
|
+
"typescript": "^7",
|
|
49
|
+
"vite": "^8",
|
|
50
|
+
"vitest": "^4"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"vite": ">=8.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|