@rangojs/router 0.12.0 → 0.12.2

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.
@@ -1,8 +1,21 @@
1
1
  // src/testing/vitest.ts
2
- import { fileURLToPath } from "node:url";
2
+ import { createRequire } from "node:module";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
4
  function here(relativeFromRoot) {
4
5
  return fileURLToPath(new URL(`../../${relativeFromRoot}`, import.meta.url));
5
6
  }
7
+ var RSD_SERVER_EDGE_SPEC = "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
8
+ var requireFromHere = createRequire(
9
+ import.meta.url
10
+ );
11
+ function resolveFromRouter(spec) {
12
+ try {
13
+ return requireFromHere.resolve(spec);
14
+ } catch {
15
+ return void 0;
16
+ }
17
+ }
18
+ var rsdServerEdgePath = resolveFromRouter(RSD_SERVER_EDGE_SPEC);
6
19
  function rangoTestAliases(opts = {}) {
7
20
  const aliases = [
8
21
  // Real impls (index.rsc.ts) for the bare specifier ONLY — exact regex so
@@ -18,6 +31,12 @@ function rangoTestAliases(opts = {}) {
18
31
  replacement: here("src/testing/vitest-stubs/plugin-rsc.ts")
19
32
  }
20
33
  ];
34
+ if (rsdServerEdgePath) {
35
+ aliases.push({
36
+ find: RSD_SERVER_EDGE_SPEC,
37
+ replacement: rsdServerEdgePath
38
+ });
39
+ }
21
40
  if (opts.preset === "cloudflare") {
22
41
  aliases.push(
23
42
  {
@@ -63,8 +82,9 @@ function rangoUseClientTransform() {
63
82
  });
64
83
  if (!result) return void 0;
65
84
  const { output } = result;
85
+ const rsdHref = rsdServerEdgePath ? pathToFileURL(rsdServerEdgePath).href : RSD_SERVER_EDGE_SPEC;
66
86
  output.prepend(
67
- `import * as $$RangoRSD from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
87
+ `import * as $$RangoRSD from ${JSON.stringify(rsdHref)};
68
88
  `
69
89
  );
70
90
  return {
@@ -28,9 +28,11 @@
28
28
  * type:module in scope the deployed (isolated) function loads them as
29
29
  * CommonJS and fails on the first `import`.
30
30
  *
31
- * The launcher is bundled with srvx (the Web->Node streaming bridge, a
32
- * @rangojs/router dependency) and @vercel/functions (resolved from the app)
33
- * inlined, keeping the RSC bundle a runtime-relative external.
31
+ * The launcher is bundled with rolldown (Vite 8's bundler a production
32
+ * dependency of `vite`, resolved through the app's vite install) so a
33
+ * standalone consumer does not need `esbuild`. srvx (the Web->Node streaming
34
+ * bridge, a @rangojs/router dependency) and @vercel/functions (resolved from
35
+ * the app) are inlined; the RSC bundle stays a runtime-relative external.
34
36
  *
35
37
  * Timing: this runs in the `buildApp` hook (order "post"), which fires once
36
38
  * after every environment has built, so dist/{client,rsc,ssr} all exist.
@@ -41,6 +43,23 @@
41
43
  */
42
44
  import type { Plugin } from "vite";
43
45
  import type { RangoVercelOptions, VercelPresetOptions } from "../plugin-types.js";
46
+ /**
47
+ * Resolve rolldown through the app's vite install (rolldown is a production
48
+ * dependency of Vite 8, unlike esbuild which is only an optional peer). Fall
49
+ * back to the app root, then the plugin's own vite, so a hoisted or
50
+ * workspace copy still works. Issue #785: the previous "esbuild ships with
51
+ * Vite" path broke standalone consumers on Vite 8.
52
+ */
53
+ export declare function resolveRolldownPath(root: string): string;
54
+ /**
55
+ * Bundle the Node launcher into `funcDir/index.mjs`: srvx + @vercel/functions
56
+ * inlined, `./rsc/index.js` left as a runtime-relative external.
57
+ */
58
+ export declare function bundleVercelLauncher(opts: {
59
+ root: string;
60
+ funcDir: string;
61
+ srvxNodePath: string;
62
+ }): Promise<void>;
44
63
  /**
45
64
  * Reject a non-Node runtime for the vercel preset. The preset only emits a Node
46
65
  * serverless function (launcherType "Nodejs", bundled Node APIs, response
@@ -3746,7 +3746,7 @@ import { resolve } from "node:path";
3746
3746
  // package.json
3747
3747
  var package_default = {
3748
3748
  name: "@rangojs/router",
3749
- version: "0.12.0",
3749
+ version: "0.12.2",
3750
3750
  description: "Django-inspired RSC router with composable URL patterns",
3751
3751
  keywords: [
3752
3752
  "react",
@@ -5682,6 +5682,7 @@ import { existsSync as existsSync5 } from "node:fs";
5682
5682
  import { resolve as resolve5, join as join4 } from "node:path";
5683
5683
  import { createRequire as createRequire2 } from "node:module";
5684
5684
  import { pathToFileURL } from "node:url";
5685
+ var VIRTUAL_LAUNCHER_ID = "\0rango-vercel-launcher";
5685
5686
  var LAUNCHER_SOURCE = `import { toNodeHandler } from "srvx/node";
5686
5687
  import { waitUntil } from "@vercel/functions";
5687
5688
  import rscHandler from "./rsc/index.js";
@@ -5701,6 +5702,99 @@ const fetchHandler = (request) =>
5701
5702
 
5702
5703
  export default toNodeHandler(fetchHandler);
5703
5704
  `;
5705
+ function resolveRolldownPath(root) {
5706
+ const appRequire = createRequire2(join4(root, "package.json"));
5707
+ const rangoRequire = createRequire2(import.meta.url);
5708
+ const attempts = [
5709
+ () => createRequire2(appRequire.resolve("vite")).resolve("rolldown"),
5710
+ () => appRequire.resolve("rolldown"),
5711
+ () => createRequire2(rangoRequire.resolve("vite")).resolve("rolldown"),
5712
+ () => rangoRequire.resolve("rolldown")
5713
+ ];
5714
+ for (const attempt of attempts) {
5715
+ try {
5716
+ return attempt();
5717
+ } catch {
5718
+ }
5719
+ }
5720
+ throw new Error(
5721
+ '[rango] preset "vercel" requires "rolldown" to bundle the function launcher. Vite 8 depends on it; reinstall dependencies.'
5722
+ );
5723
+ }
5724
+ async function bundleVercelLauncher(opts) {
5725
+ const { root, funcDir, srvxNodePath } = opts;
5726
+ const appRequire = createRequire2(join4(root, "package.json"));
5727
+ let vercelFunctionsPath;
5728
+ try {
5729
+ vercelFunctionsPath = appRequire.resolve("@vercel/functions");
5730
+ } catch {
5731
+ throw new Error(
5732
+ '[rango] preset "vercel": could not resolve "@vercel/functions". Add it to your app dependencies (it also backs VercelCacheStore).'
5733
+ );
5734
+ }
5735
+ let rolldownModule;
5736
+ try {
5737
+ rolldownModule = await import(pathToFileURL(resolveRolldownPath(root)).href);
5738
+ } catch {
5739
+ throw new Error(
5740
+ '[rango] preset "vercel" requires "rolldown" to bundle the function launcher. Vite 8 depends on it; reinstall dependencies.'
5741
+ );
5742
+ }
5743
+ const rolldownFn = rolldownModule.rolldown ?? rolldownModule.default?.rolldown;
5744
+ if (typeof rolldownFn !== "function") {
5745
+ throw new Error('[rango] preset "vercel": could not load rolldown().');
5746
+ }
5747
+ let bundle;
5748
+ try {
5749
+ bundle = await rolldownFn({
5750
+ input: VIRTUAL_LAUNCHER_ID,
5751
+ cwd: root,
5752
+ platform: "node",
5753
+ logLevel: "silent",
5754
+ resolve: {
5755
+ alias: {
5756
+ "srvx/node": srvxNodePath,
5757
+ "@vercel/functions": vercelFunctionsPath
5758
+ }
5759
+ },
5760
+ plugins: [
5761
+ {
5762
+ name: "rango-vercel-launcher",
5763
+ resolveId(id) {
5764
+ if (id === VIRTUAL_LAUNCHER_ID) return VIRTUAL_LAUNCHER_ID;
5765
+ if (id === "./rsc/index.js") {
5766
+ return { id: "./rsc/index.js", external: true };
5767
+ }
5768
+ return null;
5769
+ },
5770
+ load(id) {
5771
+ if (id === VIRTUAL_LAUNCHER_ID) return LAUNCHER_SOURCE;
5772
+ return null;
5773
+ }
5774
+ }
5775
+ ]
5776
+ });
5777
+ await bundle.write({
5778
+ file: join4(funcDir, "index.mjs"),
5779
+ format: "esm",
5780
+ exports: "default",
5781
+ // @vercel/functions (and srvx) may contain dynamic import(); the
5782
+ // launcher must stay a single index.mjs — Vercel's handler field
5783
+ // points at that one file.
5784
+ codeSplitting: false
5785
+ });
5786
+ } catch (error) {
5787
+ const message = error instanceof Error ? error.message : String(error);
5788
+ if (/@vercel\/functions/.test(message)) {
5789
+ throw new Error(
5790
+ '[rango] preset "vercel": could not resolve "@vercel/functions". Add it to your app dependencies (it also backs VercelCacheStore).\n' + message
5791
+ );
5792
+ }
5793
+ throw error;
5794
+ } finally {
5795
+ await bundle?.close();
5796
+ }
5797
+ }
5704
5798
  function assertVercelNodeRuntime(runtime2) {
5705
5799
  if (runtime2 != null && !runtime2.startsWith("nodejs")) {
5706
5800
  throw new Error(
@@ -5788,66 +5882,7 @@ async function assemble(root, options, assetsDir, publicDir) {
5788
5882
  '[rango] preset "vercel" requires "srvx" (a dependency of @rangojs/router). Reinstall dependencies.'
5789
5883
  );
5790
5884
  }
5791
- const appRequire = createRequire2(join4(root, "package.json"));
5792
- const resolveEsbuildPath = () => {
5793
- try {
5794
- const viteRequire = createRequire2(appRequire.resolve("vite"));
5795
- return viteRequire.resolve("esbuild");
5796
- } catch {
5797
- }
5798
- try {
5799
- return appRequire.resolve("esbuild");
5800
- } catch {
5801
- }
5802
- return rangoRequire.resolve("esbuild");
5803
- };
5804
- let esbuildModule;
5805
- try {
5806
- esbuildModule = await import(pathToFileURL(resolveEsbuildPath()).href);
5807
- } catch {
5808
- throw new Error(
5809
- '[rango] preset "vercel" requires "esbuild" to bundle the function launcher. It ships with Vite; reinstall dependencies (or add esbuild to your app dependencies).'
5810
- );
5811
- }
5812
- const esbuildBuild = esbuildModule.build ?? esbuildModule.default?.build;
5813
- if (typeof esbuildBuild !== "function") {
5814
- throw new Error('[rango] preset "vercel": could not load esbuild.build.');
5815
- }
5816
- try {
5817
- await esbuildBuild({
5818
- stdin: {
5819
- contents: LAUNCHER_SOURCE,
5820
- resolveDir: root,
5821
- sourcefile: "func-entry.mjs",
5822
- loader: "js"
5823
- },
5824
- outfile: join4(funcDir, "index.mjs"),
5825
- bundle: true,
5826
- format: "esm",
5827
- platform: "node",
5828
- target: "node18",
5829
- alias: { "srvx/node": srvxNodePath },
5830
- plugins: [
5831
- {
5832
- name: "external-rsc-entry",
5833
- setup(b) {
5834
- b.onResolve({ filter: /^\.\/rsc\/index\.js$/ }, () => ({
5835
- path: "./rsc/index.js",
5836
- external: true
5837
- }));
5838
- }
5839
- }
5840
- ]
5841
- });
5842
- } catch (error) {
5843
- const message = error instanceof Error ? error.message : String(error);
5844
- if (/@vercel\/functions/.test(message)) {
5845
- throw new Error(
5846
- '[rango] preset "vercel": could not resolve "@vercel/functions". Add it to your app dependencies (it also backs VercelCacheStore).\n' + message
5847
- );
5848
- }
5849
- throw error;
5850
- }
5885
+ await bundleVercelLauncher({ root, funcDir, srvxNodePath });
5851
5886
  await writeFile(
5852
5887
  join4(funcDir, "package.json"),
5853
5888
  JSON.stringify({ type: "module" }, null, 2) + "\n"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -197,7 +197,7 @@
197
197
  },
198
198
  {
199
199
  "name": "react-compiler",
200
- "description": "Enable the React Compiler in a Rango app the @vitejs/plugin-rsc way a separate @rolldown/plugin-babel running reactCompilerPreset(), ordered after react() and before the plugin that supplies @vitejs/plugin-rsc. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, or is unsure why server components aren't being compiled.",
200
+ "description": "Enable the React Compiler in a Rango app with @vitejs/plugin-react 6.1's native `compiler` option (oxc-transform-react) one flag, client components only, no Babel. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, sees an unmet-peer / ERESOLVE on oxc-transform-react, or is unsure why server components aren't being compiled.",
201
201
  "argumentHint": "",
202
202
  "path": "skills/react-compiler/SKILL.md"
203
203
  },
@@ -289,7 +289,7 @@ Grouped by concern — read when you need to…
289
289
  | `/view-transitions` | React View Transitions on layouts, routes, and parallel slots |
290
290
  | `/defer-hydration` | Full body HTML in the PPR shell + hydration off the critical path (gated Suspense boundary, content-as-fallback) |
291
291
  | `/breadcrumbs` | Built-in Breadcrumbs handle for breadcrumb navigation |
292
- | `/react-compiler` | Enable React Compiler (opt-in) the vite-rsc way; client-only scope |
292
+ | `/react-compiler` | Enable React Compiler (opt-in) via plugin-react's native `compiler` option; client-only scope |
293
293
 
294
294
  **Observability & production health**:
295
295
 
@@ -1,45 +1,49 @@
1
1
  ---
2
2
  name: react-compiler
3
- description: Enable the React Compiler in a Rango app the @vitejs/plugin-rsc way a separate @rolldown/plugin-babel running reactCompilerPreset(), ordered after react() and before the plugin that supplies @vitejs/plugin-rsc. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, or is unsure why server components aren't being compiled.
3
+ description: Enable the React Compiler in a Rango app with @vitejs/plugin-react 6.1's native `compiler` option (oxc-transform-react) one flag, client components only, no Babel. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, sees an unmet-peer / ERESOLVE on oxc-transform-react, or is unsure why server components aren't being compiled.
4
4
  argument-hint:
5
5
  ---
6
6
 
7
7
  # React Compiler
8
8
 
9
- React Compiler is **opt-in** in Rango. The plugin pipeline is fully compatible
10
- you just add one more plugin. The catch on a current Rango stack (Vite 8 +
11
- `@vitejs/plugin-react` v6) is that **v6 dropped its internal Babel for oxc**, so
12
- the way the React docs and most blog posts show it — `react({ babel: { plugins:
13
- [...] } })` silently does nothing. The compiler has to be its own top-level
14
- plugin.
9
+ React Compiler is **opt-in** in Rango, and it is one option on the plugin you
10
+ already have: `@vitejs/plugin-react` 6.1 ships a native React Compiler behind
11
+ `react({ compiler: true })`, backed by
12
+ [`oxc-transform-react`](https://www.npmjs.com/package/oxc-transform-react),
13
+ Oxc's Rust port of the compiler. No Babel, no extra plugin, no ordering rules.
14
+ Upstream marks the option experimental; the previous Babel wiring still works
15
+ and is kept below as the [fallback](#babel-fallback).
15
16
 
16
17
  ## The shape (read first)
17
18
 
18
- - The compiler is a **Babel** plugin, run via
19
- [`@rolldown/plugin-babel`](https://www.npmjs.com/package/@rolldown/plugin-babel)
20
- with `reactCompilerPreset()` from `@vitejs/plugin-react`.
21
- - **Ordering is load-bearing:** put `babel(...)` **after `react()`** and
22
- **before the plugin that supplies `@vitejs/plugin-rsc`**. In a default Rango
23
- app that plugin is `rango()` itself; in a Cloudflare app it is
24
- `@cloudflare/vite-plugin`.
25
- - **It is client-only.** `reactCompilerPreset()` gates itself to the client
26
- environment. Server/RSC components are not compiled, and that is the upstream
27
- example's behavior — not a Rango limitation. See
19
+ - **One option:** `react({ compiler: true })`. Needs `@vitejs/plugin-react` 6.1+
20
+ and its optional peer `oxc-transform-react`.
21
+ - **It is client-only.** plugin-react runs the compiler only in environments
22
+ whose `consumer` is not `"server"`. Server/RSC components are not compiled,
23
+ and that is plugin-react's contract — not a Rango limitation. See
28
24
  [What gets compiled](#what-gets-compiled-client-only).
29
- - **Rango's build-time prerender is unaffected.** You do not need to do anything
30
- special. See [Prerender](#interaction-with-build-time-prerender).
25
+ - **It owns JSX and Fast Refresh too.** With `compiler` on, plugin-react hands
26
+ TypeScript, JSX and Fast Refresh to the same native pass and disables Vite's
27
+ built-in refresh injection. Nothing to configure, and dev line numbers still
28
+ point at your source.
29
+ - **Rango's build-time prerender is unaffected.** See
30
+ [Prerender](#interaction-with-build-time-prerender).
31
31
 
32
32
  ## Step 1: Install
33
33
 
34
34
  ```bash
35
- pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler
36
- # TypeScript users also want the Babel core types:
37
- pnpm add -D @types/babel__core
35
+ pnpm add -D oxc-transform-react@^0.145.0
38
36
  ```
39
37
 
38
+ Take the range from `@vitejs/plugin-react`'s `peerDependencies` (`^0.145.0` for
39
+ 6.1.x) rather than npm's latest. `oxc-transform-react` cuts a new minor every
40
+ couple of weeks and plugin-react widens its range in its own releases, so a newer
41
+ binding shows up as an unmet-peer warning under pnpm and an `ERESOLVE` error
42
+ under npm (vitejs/vite-plugin-react#1437). Bump both together.
43
+
40
44
  React 19 ships `react/compiler-runtime` in-tree, so there is **no** extra runtime
41
- to install and **no** `target` option to set. Only pass `target: '17' | '18'` to
42
- `reactCompilerPreset()` if you are on an older React.
45
+ to install and **no** `target` option to set. Only pass `target: '17' | '18'` if
46
+ you are on an older React.
43
47
 
44
48
  ## Step 2: Wire it in
45
49
 
@@ -48,16 +52,11 @@ to install and **no** `target` option to set. Only pass `target: '17' | '18'` to
48
52
  ```ts
49
53
  // vite.config.ts
50
54
  import { defineConfig } from "vite";
51
- import react, { reactCompilerPreset } from "@vitejs/plugin-react";
52
- import babel from "@rolldown/plugin-babel";
55
+ import react from "@vitejs/plugin-react";
53
56
  import { rango } from "@rangojs/router/vite";
54
57
 
55
58
  export default defineConfig({
56
- plugins: [
57
- react(),
58
- babel({ presets: [reactCompilerPreset()] }),
59
- rango(), // supplies @vitejs/plugin-rsc
60
- ],
59
+ plugins: [react({ compiler: true }), rango()],
61
60
  });
62
61
  ```
63
62
 
@@ -66,29 +65,29 @@ export default defineConfig({
66
65
  ```ts
67
66
  // vite.config.ts
68
67
  import { cloudflare } from "@cloudflare/vite-plugin";
69
- import react, { reactCompilerPreset } from "@vitejs/plugin-react";
70
- import babel from "@rolldown/plugin-babel";
68
+ import react from "@vitejs/plugin-react";
71
69
  import { defineConfig } from "vite";
72
70
  import { rango } from "@rangojs/router/vite";
73
71
 
74
72
  export default defineConfig({
75
73
  plugins: [
76
- react(),
77
- babel({ presets: [reactCompilerPreset()] }),
74
+ react({ compiler: true }),
78
75
  rango({ preset: "cloudflare" }),
79
76
  cloudflare({
80
77
  /* ... */
81
- }), // supplies @vitejs/plugin-rsc
78
+ }),
82
79
  ],
83
80
  });
84
81
  ```
85
82
 
83
+ Both layouts keep `react()` ahead of `rango()` / `cloudflare()`, which is what
84
+ Rango's own e2e apps run.
85
+
86
86
  ## What gets compiled (client-only)
87
87
 
88
- `reactCompilerPreset()` carries
89
- `rolldown.applyToEnvironmentHook: (env) => env.config.consumer === "client"`, so
90
- even though the babel plugin is top-level, the transform runs **only in the
91
- `client` environment**:
88
+ plugin-react's `vite:react-compiler` transform runs in every environment, but
89
+ passes `reactCompiler: false` whenever `environment.config.consumer === "server"`.
90
+ Server environments only get the TypeScript/JSX pass:
92
91
 
93
92
  | Environment | `consumer` | Compiled? |
94
93
  | ----------- | ---------- | --------- |
@@ -96,43 +95,53 @@ even though the babel plugin is top-level, the transform runs **only in the
96
95
  | ssr | `server` | No |
97
96
  | rsc | `server` | No |
98
97
 
99
- This matches the upstream `@vitejs/plugin-rsc` example. If you genuinely need to
100
- compile **server** components, you would have to invoke
101
- `babel-plugin-react-compiler` yourself without the preset's
102
- `applyToEnvironmentHook` — that is outside what the example does and is not
98
+ If you genuinely need to compile **server** components, the native option cannot
99
+ do it; you would have to run `babel-plugin-react-compiler` yourself. That is not
103
100
  covered here.
104
101
 
105
102
  ## Options
106
103
 
107
- `reactCompilerPreset()` forwards to `babel-plugin-react-compiler`:
104
+ `compiler` takes `true` or the React Compiler configuration plus one plugin-level
105
+ flag:
106
+
107
+ | Option | Effect |
108
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
109
+ | `compilationMode: 'annotation'` | Compile only components marked with the `"use memo"` directive, not every eligible one |
110
+ | `target: '17' \| '18'` | Emit `react-compiler-runtime` calls for React < 19. Omit on React 19+. |
111
+ | `logDiagnostics: true` | Log recoverable compiler diagnostics (why a component was skipped) through Vite. Default `false`. Fatal diagnostics always fail the transform. |
108
112
 
109
- | Option | Effect |
110
- | ------------------------------- | -------------------------------------------------------------------------------------- |
111
- | `compilationMode: 'annotation'` | Compile only components marked with the `"use memo"` directive, not every eligible one |
112
- | `target: '17' \| '18'` | Emit `react-compiler-runtime` calls for React < 19. Omit on React 19+. |
113
+ `logDiagnostics` can only relay what the binding reports. `oxc-transform-react`
114
+ 0.145.x, the range plugin-react 6.1 declares, reports bail-out reasons. 0.148.0
115
+ dropped them (oxc-project/oxc#26318), so the flag goes silent once plugin-react's
116
+ peer range moves past it; 0.146 and 0.147 still report but need a later
117
+ plugin-react. Callback-valued Babel
118
+ options (`logger`, function-valued `sources`) do not exist on the native path;
119
+ `sources` takes an array of filename substrings.
113
120
 
114
121
  ## Interaction with build-time prerender
115
122
 
116
123
  Nothing to configure. Rango's discovery/prerender step runs a throwaway temp Vite
117
124
  server (`createTempRscServer`) that forwards only your **resolution** plugins
118
- (`resolveId` / `load`). A pure transform plugin like `@rolldown/plugin-babel` is
119
- intentionally **not** forwarded and that is correct: the temp runner only
120
- produces **data** (serialized Flight payloads + the route manifest), not shipped
121
- code, and React Compiler is a memoization-only transform that does not change
122
- rendered output. Your shipped client bundle still gets compiled, because the
123
- babel plugin lives in your app's top-level plugin array alongside `react()`.
125
+ (`resolveId` / `load`) and denies every `vite:*` plugin, so `vite:react-compiler`
126
+ never runs there. That is correct: the temp runner only produces **data**
127
+ (serialized Flight payloads + the route manifest), not shipped code, and React
128
+ Compiler is a memoization-only transform that does not change rendered output.
129
+ Your shipped client bundle is compiled by the `react({ compiler: true })` in your
130
+ app's own plugin array.
124
131
 
125
132
  ## Step 3: Verify the compiler actually ran
126
133
 
127
- A compiled module imports the cache allocator from `react/compiler-runtime` and
134
+ The native compiler emits the same shape as `babel-plugin-react-compiler`. A
135
+ compiled module imports the cache allocator from `react/compiler-runtime` and
128
136
  calls `_c(n)`. Those two appear in **every** compiled module, so they are the
129
- reliable per-module signal in dev:
137
+ reliable per-module signal in dev, and a compiled module also carries Fast
138
+ Refresh's `$RefreshReg$(...)` registration:
130
139
 
131
140
  ```bash
132
141
  pnpm dev
133
142
  # fetch any client component module straight from Vite and look for the markers:
134
143
  curl -s "http://localhost:5173/src/components/SomeClientComponent.tsx" \
135
- | grep -E "compiler-runtime|_c\("
144
+ | grep -E "compiler-runtime|_c\(|\\\$RefreshReg\\\$\("
136
145
  ```
137
146
 
138
147
  For a production build, grep the built client bundle for the compiler's
@@ -150,19 +159,48 @@ also defines that symbol once with a single `=` assignment, so count comparisons
150
159
  not the bare string.) Run the same grep over `dist/rsc` / `dist/ssr` and you
151
160
  should find **none** — that is the client-only contract.
152
161
 
162
+ Output is not byte-identical to Babel's: the port tracks React's experimental
163
+ compiler channel, so a handful of components memoize a different number of
164
+ values, and comments inside a compiled function body are dropped. Neither changes
165
+ rendered output.
166
+
167
+ ## Babel fallback
168
+
169
+ On `@vitejs/plugin-react` < 6.1, or if you need a Babel-only compiler option, the
170
+ previous wiring still works: a top-level
171
+ [`@rolldown/plugin-babel`](https://www.npmjs.com/package/@rolldown/plugin-babel)
172
+ running `reactCompilerPreset()` from `@vitejs/plugin-react`, placed **after
173
+ `react()`** and **before the plugin that supplies `@vitejs/plugin-rsc`**. The
174
+ preset gates itself to `consumer === "client"`, so the client-only contract is
175
+ the same. Do not combine it with `compiler: true`.
176
+
177
+ ```bash
178
+ pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler @types/babel__core
179
+ ```
180
+
181
+ ```ts
182
+ import react, { reactCompilerPreset } from "@vitejs/plugin-react";
183
+ import babel from "@rolldown/plugin-babel";
184
+
185
+ // plugins: [react(), babel({ presets: [reactCompilerPreset()] }), rango()]
186
+ ```
187
+
153
188
  ## Troubleshooting
154
189
 
155
- | Symptom | Cause / fix |
156
- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
157
- | Nothing is compiled; no `compiler-runtime` import anywhere | You used `react({ babel: { plugins: [...] } })`. plugin-react v6 has no internal Babel add `@rolldown/plugin-babel` as its own plugin. |
158
- | Client compiled, but server/RSC components are not | Expected. `reactCompilerPreset()` is client-only (see the table). Not a bug. |
159
- | `Cannot find module 'babel-plugin-react-compiler'` (or `@babel/core`) | Install the peer deps from Step 1; they are not bundled by `reactCompilerPreset()`. |
160
- | Build pulls in `react-compiler-runtime` | You set `target: '17'`/`'18'` on React 19. Drop `target` React 19 ships `react/compiler-runtime` in-tree. |
161
- | Output looks compiled but a component misbehaves | The component likely breaks the Rules of React. Fix the component, or scope the compiler with `compilationMode: 'annotation'` while you do. |
190
+ | Symptom | Cause / fix |
191
+ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
192
+ | `React Compiler requires the optional oxc-transform-react package` on startup | Install it (Step 1). plugin-react checks in its `config` hook, so the error is immediate. |
193
+ | Nothing is compiled; no `compiler-runtime` import anywhere | You used `react({ babel: { plugins: [...] } })`. plugin-react v6 has no internal Babel — use `react({ compiler: true })`. |
194
+ | pnpm warns `unmet peer oxc-transform-react`, npm fails with `ERESOLVE` | The binding is newer than plugin-react's peer range. Pin the range plugin-react declares. |
195
+ | Client compiled, but server/RSC components are not | Expected. The option is client-only (see the table). Not a bug. |
196
+ | `logDiagnostics: true` prints nothing | You are on `oxc-transform-react` 0.148.0 or later, which dropped recoverable diagnostics. On plugin-react 6.1.x stay on 0.145.x; 0.146 and 0.147 also report, but need a later plugin-react peer range. |
197
+ | Build pulls in `react-compiler-runtime` | You set `target: '17'`/`'18'` on React 19. Drop `target` — React 19 ships `react/compiler-runtime` in-tree. |
198
+ | Output looks compiled but a component misbehaves | The component likely breaks the Rules of React. Fix the component, or scope the compiler with `compilationMode: 'annotation'` while you do. |
162
199
 
163
200
  ## Reference
164
201
 
165
202
  A worked, tested wiring (dev + production e2e markers, incl. the client-only
166
- contract) lives in the `@rangojs/router` repository — not shipped in this
167
- package: `docs/react-compiler.md` and the `react-compiler.test.ts` files under
168
- `e2e/e2e-basic`, `tests/cloudflare-basic`, and `tests/vite-rsc-demo`.
203
+ contract and the Fast Refresh check) lives in the `@rangojs/router` repository —
204
+ not shipped in this package: `docs/react-compiler.md` and the
205
+ `react-compiler.test.ts` files under `e2e/e2e-basic`, `tests/cloudflare-basic`,
206
+ and `tests/vite-rsc-demo`.
@@ -114,7 +114,7 @@ Scripts:
114
114
  - The forked rsc worker (`pool: "forks"`) must force the condition via `execArgv: ["--conditions=react-server"]`, or React throws "the react-server condition must be enabled".
115
115
  - The `@rangojs/router:version` and `@vitejs/plugin-rsc/rsc` (`/rsc/server`, `/rsc/client`) virtuals must be stubbed; the preset does it. A bare router import without stubbing throws.
116
116
  - The rango fragment goes under `test` (`test.alias` + `test.server.deps.inline`, both returned by `rangoTestConfig`), NOT under top-level `resolve`.
117
- - Wire `rangoUseClientTransform()` into the rsc project `plugins` so islands auto-discover from the server tree imports (see `./server-tree.md`); without it, register islands explicitly with `clientComponents`.
117
+ - Wire `rangoUseClientTransform()` into the rsc project `plugins` so islands auto-discover from the server tree imports (see `./server-tree.md`); without it, register islands explicitly with `clientComponents`. The transform's `registerClientReference` import is resolved from `@rangojs/router`'s `@vitejs/plugin-rsc` — the consumer does not need a direct plugin-rsc dependency.
118
118
 
119
119
  ## See also
120
120
 
@@ -76,7 +76,8 @@
76
76
  * a focused include, or use e2e.
77
77
  */
78
78
 
79
- import { fileURLToPath } from "node:url";
79
+ import { createRequire } from "node:module";
80
+ import { fileURLToPath, pathToFileURL } from "node:url";
80
81
 
81
82
  /** A single Vite/Vitest resolve alias entry. Structurally a Vite `Alias`. */
82
83
  export interface TestAlias {
@@ -108,6 +109,32 @@ function here(relativeFromRoot: string): string {
108
109
  return fileURLToPath(new URL(`../../${relativeFromRoot}`, import.meta.url));
109
110
  }
110
111
 
112
+ /**
113
+ * Spec `rangoUseClientTransform` injects into `"use client"` modules. A
114
+ * consumer app does not depend on `@vitejs/plugin-rsc`, so the bare specifier
115
+ * is unresolvable from their `"use client"` files (cloudflare-basic
116
+ * `server-tree.rsc-test.tsx` on plugin-rsc 0.5.34). Resolve it from THIS
117
+ * module — `@rangojs/router` does depend on plugin-rsc — and alias / inject
118
+ * the absolute path.
119
+ */
120
+ const RSD_SERVER_EDGE_SPEC: string =
121
+ "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
122
+
123
+ const requireFromHere: ReturnType<typeof createRequire> = createRequire(
124
+ import.meta.url,
125
+ );
126
+
127
+ function resolveFromRouter(spec: string): string | undefined {
128
+ try {
129
+ return requireFromHere.resolve(spec);
130
+ } catch {
131
+ return undefined;
132
+ }
133
+ }
134
+
135
+ const rsdServerEdgePath: string | undefined =
136
+ resolveFromRouter(RSD_SERVER_EDGE_SPEC);
137
+
111
138
  /**
112
139
  * Build the `resolve.alias` entries a consumer's node/DOM Vitest project needs to
113
140
  * import a real @rangojs/router app's router/loaders/middleware. Spread into a
@@ -131,6 +158,12 @@ export function rangoTestAliases(
131
158
  replacement: here("src/testing/vitest-stubs/plugin-rsc.ts"),
132
159
  },
133
160
  ];
161
+ if (rsdServerEdgePath) {
162
+ aliases.push({
163
+ find: RSD_SERVER_EDGE_SPEC,
164
+ replacement: rsdServerEdgePath,
165
+ });
166
+ }
134
167
 
135
168
  if (opts.preset === "cloudflare") {
136
169
  aliases.push(
@@ -292,10 +325,13 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
292
325
  });
293
326
  if (!result) return undefined;
294
327
  const { output } = result;
295
- // The vendored server serializer is the one renderToFlightString uses;
296
- // resolvable here under the react-server condition.
328
+ // Absolute file URL, not the bare specifier: the consumer's "use client"
329
+ // module cannot resolve @vitejs/plugin-rsc (it is a router dependency).
330
+ const rsdHref = rsdServerEdgePath
331
+ ? pathToFileURL(rsdServerEdgePath).href
332
+ : RSD_SERVER_EDGE_SPEC;
297
333
  output.prepend(
298
- `import * as $$RangoRSD from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";\n`,
334
+ `import * as $$RangoRSD from ${JSON.stringify(rsdHref)};\n`,
299
335
  );
300
336
  return {
301
337
  code: output.toString(),
@@ -28,9 +28,11 @@
28
28
  * type:module in scope the deployed (isolated) function loads them as
29
29
  * CommonJS and fails on the first `import`.
30
30
  *
31
- * The launcher is bundled with srvx (the Web->Node streaming bridge, a
32
- * @rangojs/router dependency) and @vercel/functions (resolved from the app)
33
- * inlined, keeping the RSC bundle a runtime-relative external.
31
+ * The launcher is bundled with rolldown (Vite 8's bundler a production
32
+ * dependency of `vite`, resolved through the app's vite install) so a
33
+ * standalone consumer does not need `esbuild`. srvx (the Web->Node streaming
34
+ * bridge, a @rangojs/router dependency) and @vercel/functions (resolved from
35
+ * the app) are inlined; the RSC bundle stays a runtime-relative external.
34
36
  *
35
37
  * Timing: this runs in the `buildApp` hook (order "post"), which fires once
36
38
  * after every environment has built, so dist/{client,rsc,ssr} all exist.
@@ -52,19 +54,30 @@ import type {
52
54
  VercelPresetOptions,
53
55
  } from "../plugin-types.js";
54
56
 
55
- // Minimal structural types for the esbuild API we use, resolved dynamically from
56
- // the app so @rangojs/router does not depend on esbuild's type package.
57
- interface EsbuildPluginBuild {
58
- onResolve(
59
- options: { filter: RegExp },
60
- callback: () => { path: string; external: boolean },
61
- ): void;
57
+ // Minimal structural types for the rolldown API we use. Resolved dynamically
58
+ // from the app's vite install so @rangojs/router does not depend on rolldown's
59
+ // type package (same stance the previous esbuild path took).
60
+ interface RolldownResolveResult {
61
+ id: string;
62
+ external?: boolean;
62
63
  }
63
- type EsbuildBuild = (options: Record<string, unknown>) => Promise<unknown>;
64
- interface EsbuildModule {
65
- build?: EsbuildBuild;
66
- default?: { build?: EsbuildBuild };
64
+ interface RolldownBundle {
65
+ write: (output: {
66
+ file: string;
67
+ format: string;
68
+ exports: string;
69
+ codeSplitting?: boolean;
70
+ }) => Promise<unknown>;
71
+ close: () => Promise<void>;
67
72
  }
73
+ interface RolldownModule {
74
+ rolldown?: (options: Record<string, unknown>) => Promise<RolldownBundle>;
75
+ default?: {
76
+ rolldown?: (options: Record<string, unknown>) => Promise<RolldownBundle>;
77
+ };
78
+ }
79
+
80
+ const VIRTUAL_LAUNCHER_ID = "\0rango-vercel-launcher";
68
81
 
69
82
  const LAUNCHER_SOURCE = `import { toNodeHandler } from "srvx/node";
70
83
  import { waitUntil } from "@vercel/functions";
@@ -86,6 +99,122 @@ const fetchHandler = (request) =>
86
99
  export default toNodeHandler(fetchHandler);
87
100
  `;
88
101
 
102
+ /**
103
+ * Resolve rolldown through the app's vite install (rolldown is a production
104
+ * dependency of Vite 8, unlike esbuild which is only an optional peer). Fall
105
+ * back to the app root, then the plugin's own vite, so a hoisted or
106
+ * workspace copy still works. Issue #785: the previous "esbuild ships with
107
+ * Vite" path broke standalone consumers on Vite 8.
108
+ */
109
+ export function resolveRolldownPath(root: string): string {
110
+ const appRequire = createRequire(join(root, "package.json"));
111
+ const rangoRequire = createRequire(import.meta.url);
112
+ const attempts: Array<() => string> = [
113
+ () => createRequire(appRequire.resolve("vite")).resolve("rolldown"),
114
+ () => appRequire.resolve("rolldown"),
115
+ () => createRequire(rangoRequire.resolve("vite")).resolve("rolldown"),
116
+ () => rangoRequire.resolve("rolldown"),
117
+ ];
118
+ for (const attempt of attempts) {
119
+ try {
120
+ return attempt();
121
+ } catch {
122
+ // Intentionally empty: try the next resolver.
123
+ }
124
+ }
125
+ throw new Error(
126
+ '[rango] preset "vercel" requires "rolldown" to bundle the function launcher. Vite 8 depends on it; reinstall dependencies.',
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Bundle the Node launcher into `funcDir/index.mjs`: srvx + @vercel/functions
132
+ * inlined, `./rsc/index.js` left as a runtime-relative external.
133
+ */
134
+ export async function bundleVercelLauncher(opts: {
135
+ root: string;
136
+ funcDir: string;
137
+ srvxNodePath: string;
138
+ }): Promise<void> {
139
+ const { root, funcDir, srvxNodePath } = opts;
140
+ const appRequire = createRequire(join(root, "package.json"));
141
+ let vercelFunctionsPath: string;
142
+ try {
143
+ vercelFunctionsPath = appRequire.resolve("@vercel/functions");
144
+ } catch {
145
+ throw new Error(
146
+ '[rango] preset "vercel": could not resolve "@vercel/functions". Add it to your app dependencies (it also backs VercelCacheStore).',
147
+ );
148
+ }
149
+ let rolldownModule: RolldownModule;
150
+ try {
151
+ rolldownModule = (await import(
152
+ pathToFileURL(resolveRolldownPath(root)).href
153
+ )) as RolldownModule;
154
+ } catch {
155
+ throw new Error(
156
+ '[rango] preset "vercel" requires "rolldown" to bundle the function launcher. Vite 8 depends on it; reinstall dependencies.',
157
+ );
158
+ }
159
+ const rolldownFn =
160
+ rolldownModule.rolldown ?? rolldownModule.default?.rolldown;
161
+ if (typeof rolldownFn !== "function") {
162
+ throw new Error('[rango] preset "vercel": could not load rolldown().');
163
+ }
164
+
165
+ let bundle: RolldownBundle | undefined;
166
+ try {
167
+ bundle = await rolldownFn({
168
+ input: VIRTUAL_LAUNCHER_ID,
169
+ cwd: root,
170
+ platform: "node",
171
+ logLevel: "silent",
172
+ resolve: {
173
+ alias: {
174
+ "srvx/node": srvxNodePath,
175
+ "@vercel/functions": vercelFunctionsPath,
176
+ },
177
+ },
178
+ plugins: [
179
+ {
180
+ name: "rango-vercel-launcher",
181
+ resolveId(id: string): string | RolldownResolveResult | null {
182
+ if (id === VIRTUAL_LAUNCHER_ID) return VIRTUAL_LAUNCHER_ID;
183
+ if (id === "./rsc/index.js") {
184
+ return { id: "./rsc/index.js", external: true };
185
+ }
186
+ return null;
187
+ },
188
+ load(id: string): string | null {
189
+ if (id === VIRTUAL_LAUNCHER_ID) return LAUNCHER_SOURCE;
190
+ return null;
191
+ },
192
+ },
193
+ ],
194
+ });
195
+ await bundle.write({
196
+ file: join(funcDir, "index.mjs"),
197
+ format: "esm",
198
+ exports: "default",
199
+ // @vercel/functions (and srvx) may contain dynamic import(); the
200
+ // launcher must stay a single index.mjs — Vercel's handler field
201
+ // points at that one file.
202
+ codeSplitting: false,
203
+ });
204
+ } catch (error) {
205
+ const message = error instanceof Error ? error.message : String(error);
206
+ if (/@vercel\/functions/.test(message)) {
207
+ throw new Error(
208
+ '[rango] preset "vercel": could not resolve "@vercel/functions". Add it to your app dependencies (it also backs VercelCacheStore).\n' +
209
+ message,
210
+ );
211
+ }
212
+ throw error;
213
+ } finally {
214
+ await bundle?.close();
215
+ }
216
+ }
217
+
89
218
  /**
90
219
  * Reject a non-Node runtime for the vercel preset. The preset only emits a Node
91
220
  * serverless function (launcherType "Nodejs", bundled Node APIs, response
@@ -237,7 +366,8 @@ async function assemble(
237
366
 
238
367
  // 3. Bundle the Node launcher. srvx (a @rangojs/router dependency) is aliased
239
368
  // to its resolved path; @vercel/functions resolves from the app; the RSC
240
- // server bundle stays a runtime-relative external.
369
+ // server bundle stays a runtime-relative external. Rolldown (Vite 8's
370
+ // bundler) is resolved through the app's vite install — #785.
241
371
  const rangoRequire = createRequire(import.meta.url);
242
372
  let srvxNodePath: string;
243
373
  try {
@@ -247,79 +377,7 @@ async function assemble(
247
377
  '[rango] preset "vercel" requires "srvx" (a dependency of @rangojs/router). Reinstall dependencies.',
248
378
  );
249
379
  }
250
-
251
- // esbuild ships with Vite, so we never add it as a @rangojs/router dependency.
252
- // It is a DIRECT dependency of Vite but only a TRANSITIVE one from the app's
253
- // view, so under strict pnpm it is NOT resolvable from the app root. Resolve it
254
- // through Vite's module location (Vite is a direct app dependency, and esbuild
255
- // is a direct dependency of Vite). Minimal structural types avoid coupling to
256
- // esbuild's type package at compile time.
257
- const appRequire = createRequire(join(root, "package.json"));
258
- const resolveEsbuildPath = (): string => {
259
- try {
260
- const viteRequire = createRequire(appRequire.resolve("vite"));
261
- return viteRequire.resolve("esbuild");
262
- } catch {
263
- // Intentionally empty: fall through to the app/rango fallbacks below.
264
- }
265
- try {
266
- return appRequire.resolve("esbuild");
267
- } catch {
268
- // Intentionally empty: last resort is @rangojs/router's own resolver.
269
- }
270
- return rangoRequire.resolve("esbuild");
271
- };
272
- let esbuildModule: EsbuildModule;
273
- try {
274
- esbuildModule = (await import(
275
- pathToFileURL(resolveEsbuildPath()).href
276
- )) as EsbuildModule;
277
- } catch {
278
- throw new Error(
279
- '[rango] preset "vercel" requires "esbuild" to bundle the function launcher. It ships with Vite; reinstall dependencies (or add esbuild to your app dependencies).',
280
- );
281
- }
282
- const esbuildBuild = esbuildModule.build ?? esbuildModule.default?.build;
283
- if (typeof esbuildBuild !== "function") {
284
- throw new Error('[rango] preset "vercel": could not load esbuild.build.');
285
- }
286
-
287
- try {
288
- await esbuildBuild({
289
- stdin: {
290
- contents: LAUNCHER_SOURCE,
291
- resolveDir: root,
292
- sourcefile: "func-entry.mjs",
293
- loader: "js",
294
- },
295
- outfile: join(funcDir, "index.mjs"),
296
- bundle: true,
297
- format: "esm",
298
- platform: "node",
299
- target: "node18",
300
- alias: { "srvx/node": srvxNodePath },
301
- plugins: [
302
- {
303
- name: "external-rsc-entry",
304
- setup(b: EsbuildPluginBuild) {
305
- b.onResolve({ filter: /^\.\/rsc\/index\.js$/ }, () => ({
306
- path: "./rsc/index.js",
307
- external: true,
308
- }));
309
- },
310
- },
311
- ],
312
- });
313
- } catch (error) {
314
- const message = error instanceof Error ? error.message : String(error);
315
- if (/@vercel\/functions/.test(message)) {
316
- throw new Error(
317
- '[rango] preset "vercel": could not resolve "@vercel/functions". Add it to your app dependencies (it also backs VercelCacheStore).\n' +
318
- message,
319
- );
320
- }
321
- throw error;
322
- }
380
+ await bundleVercelLauncher({ root, funcDir, srvxNodePath });
323
381
 
324
382
  // 3b. Mark the function as ESM. The rsc/ssr bundles are .js ESM files with no
325
383
  // package.json in scope on the deployed function (it is isolated at