@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 ADDED
@@ -0,0 +1,146 @@
1
+ # @acusti/vite-plugin-react-compiler
2
+
3
+ [![latest version](https://img.shields.io/npm/v/@acusti/vite-plugin-react-compiler?style=for-the-badge)](https://www.npmjs.com/package/@acusti/vite-plugin-react-compiler)
4
+ [![maintenance status](https://img.shields.io/npms-io/maintenance-score/@acusti/vite-plugin-react-compiler?style=for-the-badge)](https://npms.io/search?q=%40acusti%2Fvite-plugin-react-compiler)
5
+ [![downloads per month](https://img.shields.io/npm/dm/@acusti/vite-plugin-react-compiler?style=for-the-badge)](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).
@@ -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
+ }