@rangojs/router 0.12.0 → 0.12.1

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.1",
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.1",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -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