@rangojs/router 0.0.0-experimental.138 → 0.0.0-experimental.139

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,6 +1,6 @@
1
1
  // src/vite/rango.ts
2
- import { readFileSync as readFileSync7 } from "node:fs";
3
- import { resolve as resolve9 } from "node:path";
2
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "node:fs";
3
+ import { resolve as resolve11 } from "node:path";
4
4
 
5
5
  // src/vite/plugins/expose-action-id.ts
6
6
  import MagicString from "magic-string";
@@ -2279,6 +2279,77 @@ export default function handler(request, env) {
2279
2279
  }
2280
2280
  `.trim();
2281
2281
  }
2282
+ function getVirtualEntryRSCHost(hostEntryPath) {
2283
+ return `
2284
+ import * as __hostEntry from "${hostEntryPath}";
2285
+ import { isNoRouteMatchError } from "@rangojs/router/host";
2286
+
2287
+ // Register every sub-app's fetchable loaders + route manifests at startup, same
2288
+ // as the single-router entry. Discovery's host fallback populates these for all
2289
+ // mounted sub-apps, so the aggregate manifests cover the whole host tree.
2290
+ import "virtual:rsc-router/loader-manifest";
2291
+ import "virtual:rsc-router/routes-manifest";
2292
+
2293
+ // The host entry module must export the HostRouter instance (createHostRouter()),
2294
+ // as a default export or a named \`hostRouter\`/\`router\` export. A Cloudflare-style
2295
+ // \`export default { fetch }\` object is not a HostRouter and is rejected (on first
2296
+ // request; see the lazy resolution below).
2297
+ // We require BOTH .match() and .host(): a regular createRouter() also exposes
2298
+ // .match(), so matching on .match() alone would accept an ordinary router and then
2299
+ // return its MatchResult (not a Response) at runtime. .host() is unique to a
2300
+ // HostRouter, so it disambiguates a mistaken \`hostRouter\` path.
2301
+ // Exports are read dynamically (m[name]) so Rollup does not emit IMPORT_IS_UNDEFINED
2302
+ // warnings for the named exports a default-only host module legitimately omits.
2303
+ const __resolveHostRouter = (m) => {
2304
+ for (const name of ["default", "hostRouter", "router"]) {
2305
+ const candidate = m[name];
2306
+ if (
2307
+ candidate &&
2308
+ typeof candidate.match === "function" &&
2309
+ typeof candidate.host === "function"
2310
+ )
2311
+ return candidate;
2312
+ }
2313
+ return undefined;
2314
+ };
2315
+
2316
+ // Resolve + validate the HostRouter lazily on first request, mirroring the
2317
+ // single-router entry's \`_handler\`. During HMR the host module can re-evaluate
2318
+ // before its createHostRouter() export has resolved; validating at module-
2319
+ // evaluation time would then throw a spurious "must export a HostRouter" error
2320
+ // overlay for a perfectly valid app. Resolving on first request lets the live
2321
+ // binding settle first.
2322
+ let _hostRouter;
2323
+
2324
+ // input = { env, ctx } from the launcher / node server. The host router threads
2325
+ // it unchanged to each matched sub-app's handler and cache factory.
2326
+ // On node/vercel rango owns this entry, so there is no user worker to translate
2327
+ // an unmatched host into a response: catch NoRouteMatchError and return 404
2328
+ // (parity with the documented Cloudflare catch). Other errors propagate.
2329
+ export default async function handler(request, input) {
2330
+ if (!_hostRouter) {
2331
+ _hostRouter = __resolveHostRouter(__hostEntry);
2332
+ if (!_hostRouter) {
2333
+ throw new Error(
2334
+ "[rango] The host entry (${hostEntryPath}) must export a HostRouter instance (createHostRouter()) for the node/vercel preset: a default export, or a named 'hostRouter'/'router' export. An ordinary createRouter() is not a host router (it has no .host()), and a Cloudflare-style 'export default { fetch }' object is not supported on this preset."
2335
+ );
2336
+ }
2337
+ }
2338
+ try {
2339
+ return await _hostRouter.match(request, input);
2340
+ } catch (err) {
2341
+ // isNoRouteMatchError also matches by name: a workspace with a duplicated
2342
+ // @rangojs/router copy can throw a NoRouteMatchError whose prototype differs
2343
+ // from this module's import, so a bare instanceof would turn an
2344
+ // unmatched-host 404 into a 500.
2345
+ if (isNoRouteMatchError(err)) {
2346
+ return new Response("Not Found", { status: 404 });
2347
+ }
2348
+ throw err;
2349
+ }
2350
+ }
2351
+ `.trim();
2352
+ }
2282
2353
  var VIRTUAL_IDS = {
2283
2354
  browser: "virtual:rsc-router/entry.browser.js",
2284
2355
  ssr: "virtual:rsc-router/entry.ssr.js",
@@ -2297,7 +2368,7 @@ import { resolve } from "node:path";
2297
2368
  // package.json
2298
2369
  var package_default = {
2299
2370
  name: "@rangojs/router",
2300
- version: "0.0.0-experimental.138",
2371
+ version: "0.0.0-experimental.139",
2301
2372
  description: "Django-inspired RSC router with composable URL patterns",
2302
2373
  keywords: [
2303
2374
  "react",
@@ -2412,6 +2483,11 @@ var package_default = {
2412
2483
  "react-server": "./src/cloudflare/index.ts",
2413
2484
  default: "./src/cloudflare/index.ts"
2414
2485
  },
2486
+ "./vercel": {
2487
+ types: "./src/vercel/index.ts",
2488
+ "react-server": "./src/vercel/index.ts",
2489
+ default: "./src/vercel/index.ts"
2490
+ },
2415
2491
  "./theme": {
2416
2492
  types: "./src/theme/index.ts",
2417
2493
  default: "./src/theme/index.ts"
@@ -2477,9 +2553,11 @@ var package_default = {
2477
2553
  "magic-string": "^0.30.17",
2478
2554
  picomatch: "^4.0.4",
2479
2555
  "rsc-html-stream": "^0.0.7",
2556
+ srvx: "^0.11.15",
2480
2557
  tinyexec: "^0.3.2"
2481
2558
  },
2482
2559
  devDependencies: {
2560
+ "@opentelemetry/api": "^1.9.0",
2483
2561
  "@playwright/test": "^1.49.1",
2484
2562
  "@shared/e2e": "workspace:*",
2485
2563
  "@testing-library/dom": "^10.4.1",
@@ -2497,8 +2575,10 @@ var package_default = {
2497
2575
  },
2498
2576
  peerDependencies: {
2499
2577
  "@cloudflare/vite-plugin": "^1.42.1",
2578
+ "@opentelemetry/api": "^1.9.0",
2500
2579
  "@playwright/test": "^1.49.1",
2501
2580
  "@testing-library/react": ">=16",
2581
+ "@vercel/functions": "^3.0.0",
2502
2582
  "@vitejs/plugin-rsc": "^0.5.27",
2503
2583
  react: ">=19.2.6 <20",
2504
2584
  "react-dom": ">=19.2.6 <20",
@@ -2509,12 +2589,18 @@ var package_default = {
2509
2589
  "@cloudflare/vite-plugin": {
2510
2590
  optional: true
2511
2591
  },
2592
+ "@opentelemetry/api": {
2593
+ optional: true
2594
+ },
2512
2595
  "@playwright/test": {
2513
2596
  optional: true
2514
2597
  },
2515
2598
  "@testing-library/react": {
2516
2599
  optional: true
2517
2600
  },
2601
+ "@vercel/functions": {
2602
+ optional: true
2603
+ },
2518
2604
  vite: {
2519
2605
  optional: true
2520
2606
  },
@@ -3270,7 +3356,10 @@ var ROUTER_CALL_PATTERN_G = /\bcreateRouter\s*[<(]/g;
3270
3356
  function isRoutableSourceFile(name) {
3271
3357
  return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
3272
3358
  }
3273
- function findRouterFilesRecursive(dir, filter, results) {
3359
+ function isExcludedScanDir(name) {
3360
+ return name === "node_modules" || name === "dist" || name === "coverage" || name === "__tests__" || name === "__mocks__" || name.startsWith(".");
3361
+ }
3362
+ function findCallSiteFilesRecursive(dir, pattern, patternG, stopAtMatchDir, filter, results) {
3274
3363
  let entries;
3275
3364
  try {
3276
3365
  entries = readdirSync(dir, { withFileTypes: true });
@@ -3281,32 +3370,36 @@ function findRouterFilesRecursive(dir, filter, results) {
3281
3370
  return;
3282
3371
  }
3283
3372
  const childDirs = [];
3284
- const routerFilesInDir = [];
3373
+ const matchesInDir = [];
3285
3374
  for (const entry of entries) {
3286
3375
  const fullPath = join2(dir, entry.name);
3287
3376
  if (entry.isDirectory()) {
3288
- if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage" || entry.name === "__tests__" || entry.name === "__mocks__" || entry.name.startsWith("."))
3289
- continue;
3290
- childDirs.push(fullPath);
3377
+ if (!isExcludedScanDir(entry.name)) childDirs.push(fullPath);
3291
3378
  continue;
3292
3379
  }
3293
3380
  if (!isRoutableSourceFile(entry.name)) continue;
3294
3381
  if (filter && !filter(fullPath)) continue;
3295
3382
  try {
3296
3383
  const source = readFileSync2(fullPath, "utf-8");
3297
- if (ROUTER_CALL_PATTERN.test(source) && firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0) {
3298
- routerFilesInDir.push(fullPath);
3384
+ if (pattern.test(source) && firstCodeMatchIndex(source, patternG) >= 0) {
3385
+ matchesInDir.push(fullPath);
3299
3386
  }
3300
3387
  } catch {
3301
3388
  continue;
3302
3389
  }
3303
3390
  }
3304
- if (routerFilesInDir.length > 0) {
3305
- results.push(...routerFilesInDir);
3306
- return;
3307
- }
3308
- for (const childDir of childDirs) {
3309
- findRouterFilesRecursive(childDir, filter, results);
3391
+ results.push(...matchesInDir);
3392
+ if (!stopAtMatchDir || matchesInDir.length === 0) {
3393
+ for (const childDir of childDirs) {
3394
+ findCallSiteFilesRecursive(
3395
+ childDir,
3396
+ pattern,
3397
+ patternG,
3398
+ stopAtMatchDir,
3399
+ filter,
3400
+ results
3401
+ );
3402
+ }
3310
3403
  }
3311
3404
  }
3312
3405
  function findNestedRouterConflict(routerFiles) {
@@ -3504,7 +3597,28 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
3504
3597
  }
3505
3598
  function findRouterFiles(root, filter) {
3506
3599
  const result = [];
3507
- findRouterFilesRecursive(root, filter, result);
3600
+ findCallSiteFilesRecursive(
3601
+ root,
3602
+ ROUTER_CALL_PATTERN,
3603
+ ROUTER_CALL_PATTERN_G,
3604
+ true,
3605
+ filter,
3606
+ result
3607
+ );
3608
+ return result;
3609
+ }
3610
+ var HOST_ROUTER_CALL_PATTERN = /\bcreateHostRouter\s*[<(]/;
3611
+ var HOST_ROUTER_CALL_PATTERN_G = /\bcreateHostRouter\s*[<(]/g;
3612
+ function findHostRouterFiles(root, filter) {
3613
+ const result = [];
3614
+ findCallSiteFilesRecursive(
3615
+ root,
3616
+ HOST_ROUTER_CALL_PATTERN,
3617
+ HOST_ROUTER_CALL_PATTERN_G,
3618
+ false,
3619
+ filter,
3620
+ result
3621
+ );
3508
3622
  return result;
3509
3623
  }
3510
3624
  function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
@@ -3778,6 +3892,7 @@ function isViteDepCachePath(filePath, cacheDir) {
3778
3892
 
3779
3893
  // src/vite/utils/shared-utils.ts
3780
3894
  import * as Vite from "vite";
3895
+ import { isAbsolute, resolve as resolve4 } from "node:path";
3781
3896
 
3782
3897
  // src/vite/plugins/performance-tracks.ts
3783
3898
  import { readFile } from "node:fs/promises";
@@ -3860,6 +3975,21 @@ var versionRolldownPlugin = {
3860
3975
  var sharedRolldownOptions = {
3861
3976
  plugins: [versionRolldownPlugin, performanceTracksOptimizeDepsPlugin()]
3862
3977
  };
3978
+ function normalizeHostRouterEntry(rawInput, root, exists) {
3979
+ const raw = rawInput.replaceAll("\\", "/");
3980
+ const isRelative = raw.startsWith("./") || raw.startsWith("../");
3981
+ const isRooted = raw.startsWith("/") || isAbsolute(rawInput);
3982
+ if (isRooted) return raw;
3983
+ if (isRelative) {
3984
+ if (!exists(resolve4(root, raw))) {
3985
+ throw new Error(
3986
+ `[rango] hostRouter entry not found: "${rawInput}" (resolved under ${root}). Point it at your createHostRouter() module, e.g. rango({ hostRouter: "./src/worker.rsc.tsx" }).`
3987
+ );
3988
+ }
3989
+ return raw;
3990
+ }
3991
+ return exists(resolve4(root, raw)) ? "./" + raw : raw;
3992
+ }
3863
3993
  function createVirtualEntriesPlugin(entries, routerPathRef) {
3864
3994
  const virtualModules = {};
3865
3995
  if (entries.client === VIRTUAL_IDS.browser) {
@@ -3893,7 +4023,7 @@ function createVirtualEntriesPlugin(entries, routerPathRef) {
3893
4023
  if (virtualId === VIRTUAL_IDS.rsc && routerPathRef?.path) {
3894
4024
  const raw = routerPathRef.path.startsWith(".") ? "/" + routerPathRef.path.slice(2) : routerPathRef.path;
3895
4025
  const absoluteRouterPath = raw.replaceAll("\\", "/");
3896
- return getVirtualEntryRSC(absoluteRouterPath);
4026
+ return routerPathRef.kind === "host" ? getVirtualEntryRSCHost(absoluteRouterPath) : getVirtualEntryRSC(absoluteRouterPath);
3897
4027
  }
3898
4028
  }
3899
4029
  return null;
@@ -4072,6 +4202,221 @@ function resolveClientChunks(option, ctx) {
4072
4202
  return option;
4073
4203
  }
4074
4204
 
4205
+ // src/vite/plugins/vercel-output.ts
4206
+ import { rm, mkdir, cp, writeFile } from "node:fs/promises";
4207
+ import { existsSync as existsSync4 } from "node:fs";
4208
+ import { resolve as resolve5, join as join3 } from "node:path";
4209
+ import { createRequire as createRequire2 } from "node:module";
4210
+ import { pathToFileURL } from "node:url";
4211
+ var LAUNCHER_SOURCE = `import { toNodeHandler } from "srvx/node";
4212
+ import { waitUntil } from "@vercel/functions";
4213
+ import rscHandler from "./rsc/index.js";
4214
+
4215
+ // The Vercel Node launcher invokes a Node (req, res) handler, not a Web fetch
4216
+ // handler. srvx's toNodeHandler bridges the Rango Web fetch handler and pipes
4217
+ // the streamed Response to the Node response (set supportsResponseStreaming).
4218
+ const onVercel = Boolean(process.env.VERCEL);
4219
+
4220
+ const fetchHandler = (request) =>
4221
+ rscHandler(request, {
4222
+ env: process.env,
4223
+ // Forward Vercel's waitUntil so cache writes / revalidation run off the
4224
+ // response path. Omitted off-platform so those writes settle inline.
4225
+ ctx: onVercel ? { waitUntil } : undefined,
4226
+ });
4227
+
4228
+ export default toNodeHandler(fetchHandler);
4229
+ `;
4230
+ function assertVercelNodeRuntime(runtime) {
4231
+ if (runtime != null && !runtime.startsWith("nodejs")) {
4232
+ throw new Error(
4233
+ `[rango] preset "vercel": runtime "${runtime}" is not supported. This preset emits a Node serverless function; use a "nodejs*" runtime (default "nodejs22.x"). The Edge runtime is not supported.`
4234
+ );
4235
+ }
4236
+ }
4237
+ function assertValidVercelFunctionName(functionName) {
4238
+ if (!/^[A-Za-z0-9._-]+$/.test(functionName)) {
4239
+ throw new Error(
4240
+ `[rango] preset "vercel": invalid functionName ${JSON.stringify(
4241
+ functionName
4242
+ )}. Use letters, digits, ".", "_" or "-" only (it becomes the .func directory and the routing dest).`
4243
+ );
4244
+ }
4245
+ }
4246
+ function buildVercelVcConfig(vercel) {
4247
+ const vcConfig = {
4248
+ runtime: vercel.runtime ?? "nodejs22.x",
4249
+ handler: "index.mjs",
4250
+ launcherType: "Nodejs",
4251
+ shouldAddHelpers: false,
4252
+ supportsResponseStreaming: true,
4253
+ maxDuration: vercel.maxDuration ?? 30
4254
+ };
4255
+ if (vercel.memory != null) vcConfig.memory = vercel.memory;
4256
+ if (vercel.regions != null) vcConfig.regions = vercel.regions;
4257
+ return vcConfig;
4258
+ }
4259
+ function buildVercelOutputConfig(functionName, assetsDir) {
4260
+ const assetsPrefix = assetsDir.replace(/^\/+|\/+$/g, "");
4261
+ const assetHeaderRoute = assetsPrefix ? [
4262
+ {
4263
+ src: `/${escapeRegExp(assetsPrefix)}/(.*)`,
4264
+ headers: { "cache-control": "public, max-age=31536000, immutable" },
4265
+ continue: true
4266
+ }
4267
+ ] : [];
4268
+ return {
4269
+ version: 3,
4270
+ routes: [
4271
+ ...assetHeaderRoute,
4272
+ { handle: "filesystem" },
4273
+ { src: "/(.*)", dest: `/${functionName}` }
4274
+ ]
4275
+ };
4276
+ }
4277
+ async function assemble(root, options, assetsDir, publicDir) {
4278
+ const vercel = options.vercel ?? {};
4279
+ assertVercelNodeRuntime(vercel.runtime);
4280
+ const assetsPrefix = assetsDir.replace(/^\/+|\/+$/g, "");
4281
+ if (publicDir && assetsPrefix && existsSync4(join3(publicDir, assetsPrefix))) {
4282
+ console.warn(
4283
+ `[rango] preset "vercel": ${join3(publicDir, assetsPrefix)} exists. Files under public/${assetsPrefix}/ share the /${assetsPrefix}/ URL prefix with hashed build assets and will be served with a one-year immutable cache-control header. Move un-hashed public files out of "${assetsPrefix}/".`
4284
+ );
4285
+ }
4286
+ const dist = join3(root, "dist");
4287
+ for (const dir of ["client", "rsc", "ssr"]) {
4288
+ if (!existsSync4(join3(dist, dir))) {
4289
+ throw new Error(
4290
+ `[rango] preset "vercel": missing dist/${dir}. Run the production build first.`
4291
+ );
4292
+ }
4293
+ }
4294
+ const functionName = vercel.functionName ?? "index";
4295
+ assertValidVercelFunctionName(functionName);
4296
+ const out = join3(root, ".vercel", "output");
4297
+ const funcDir = join3(out, "functions", `${functionName}.func`);
4298
+ await rm(out, { recursive: true, force: true });
4299
+ await mkdir(funcDir, { recursive: true });
4300
+ await cp(join3(dist, "client"), join3(out, "static"), { recursive: true });
4301
+ await cp(join3(dist, "rsc"), join3(funcDir, "rsc"), { recursive: true });
4302
+ await cp(join3(dist, "ssr"), join3(funcDir, "ssr"), { recursive: true });
4303
+ if (existsSync4(join3(dist, "static"))) {
4304
+ await cp(join3(dist, "static"), join3(funcDir, "static"), {
4305
+ recursive: true
4306
+ });
4307
+ }
4308
+ const rangoRequire = createRequire2(import.meta.url);
4309
+ let srvxNodePath;
4310
+ try {
4311
+ srvxNodePath = rangoRequire.resolve("srvx/node");
4312
+ } catch {
4313
+ throw new Error(
4314
+ '[rango] preset "vercel" requires "srvx" (a dependency of @rangojs/router). Reinstall dependencies.'
4315
+ );
4316
+ }
4317
+ const appRequire = createRequire2(join3(root, "package.json"));
4318
+ const resolveEsbuildPath = () => {
4319
+ try {
4320
+ const viteRequire = createRequire2(appRequire.resolve("vite"));
4321
+ return viteRequire.resolve("esbuild");
4322
+ } catch {
4323
+ }
4324
+ try {
4325
+ return appRequire.resolve("esbuild");
4326
+ } catch {
4327
+ }
4328
+ return rangoRequire.resolve("esbuild");
4329
+ };
4330
+ let esbuildModule;
4331
+ try {
4332
+ esbuildModule = await import(pathToFileURL(resolveEsbuildPath()).href);
4333
+ } catch {
4334
+ throw new Error(
4335
+ '[rango] preset "vercel" requires "esbuild" to bundle the function launcher. It ships with Vite; reinstall dependencies (or add esbuild to your app dependencies).'
4336
+ );
4337
+ }
4338
+ const esbuildBuild = esbuildModule.build ?? esbuildModule.default?.build;
4339
+ if (typeof esbuildBuild !== "function") {
4340
+ throw new Error('[rango] preset "vercel": could not load esbuild.build.');
4341
+ }
4342
+ try {
4343
+ await esbuildBuild({
4344
+ stdin: {
4345
+ contents: LAUNCHER_SOURCE,
4346
+ resolveDir: root,
4347
+ sourcefile: "func-entry.mjs",
4348
+ loader: "js"
4349
+ },
4350
+ outfile: join3(funcDir, "index.mjs"),
4351
+ bundle: true,
4352
+ format: "esm",
4353
+ platform: "node",
4354
+ target: "node18",
4355
+ alias: { "srvx/node": srvxNodePath },
4356
+ plugins: [
4357
+ {
4358
+ name: "external-rsc-entry",
4359
+ setup(b) {
4360
+ b.onResolve({ filter: /^\.\/rsc\/index\.js$/ }, () => ({
4361
+ path: "./rsc/index.js",
4362
+ external: true
4363
+ }));
4364
+ }
4365
+ }
4366
+ ]
4367
+ });
4368
+ } catch (error) {
4369
+ const message = error instanceof Error ? error.message : String(error);
4370
+ if (/@vercel\/functions/.test(message)) {
4371
+ throw new Error(
4372
+ '[rango] preset "vercel": could not resolve "@vercel/functions". Add it to your app dependencies (it also backs VercelCacheStore).\n' + message
4373
+ );
4374
+ }
4375
+ throw error;
4376
+ }
4377
+ await writeFile(
4378
+ join3(funcDir, "package.json"),
4379
+ JSON.stringify({ type: "module" }, null, 2) + "\n"
4380
+ );
4381
+ await writeFile(
4382
+ join3(funcDir, ".vc-config.json"),
4383
+ JSON.stringify(buildVercelVcConfig(vercel), null, 2) + "\n"
4384
+ );
4385
+ await writeFile(
4386
+ join3(out, "config.json"),
4387
+ JSON.stringify(buildVercelOutputConfig(functionName, assetsDir), null, 2) + "\n"
4388
+ );
4389
+ console.log(
4390
+ `[rango] assembled .vercel/output (function: ${functionName}.func)`
4391
+ );
4392
+ }
4393
+ function createVercelOutputPlugin(options) {
4394
+ let root = process.cwd();
4395
+ let isBuild = false;
4396
+ let assetsDir = "assets";
4397
+ let publicDir = "";
4398
+ return {
4399
+ name: "@rangojs/router:vercel-output",
4400
+ configResolved(config) {
4401
+ root = resolve5(config.root);
4402
+ isBuild = config.command === "build";
4403
+ assetsDir = config.environments?.client?.build?.assetsDir ?? config.build?.assetsDir ?? "assets";
4404
+ publicDir = config.publicDir || "";
4405
+ },
4406
+ // buildApp runs once after the whole multi-environment build (rsc, client,
4407
+ // ssr), so dist/ is complete here. closeBundle is unusable for this: it
4408
+ // fires per environment, and twice for ssr (the server-reference scan and
4409
+ // the real build), so it would run before dist/client exists.
4410
+ buildApp: {
4411
+ order: "post",
4412
+ async handler() {
4413
+ if (!isBuild) return;
4414
+ await assemble(root, options, assetsDir, publicDir);
4415
+ }
4416
+ }
4417
+ };
4418
+ }
4419
+
4075
4420
  // src/vite/utils/banner.ts
4076
4421
  var rangoVersion = package_default.version;
4077
4422
  var _bannerPrinted = false;
@@ -4099,7 +4444,7 @@ ${dim} ${reset}${bold}\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550$
4099
4444
  }
4100
4445
 
4101
4446
  // src/vite/plugins/version-injector.ts
4102
- import { resolve as resolve4 } from "node:path";
4447
+ import { resolve as resolve6 } from "node:path";
4103
4448
  import * as Vite2 from "vite";
4104
4449
  function createVersionInjectorPlugin(rscEntryPath) {
4105
4450
  let resolvedEntryPath = "";
@@ -4110,7 +4455,7 @@ function createVersionInjectorPlugin(rscEntryPath) {
4110
4455
  let entryPath = rscEntryPath;
4111
4456
  if (!entryPath) entryPath = resolveRscEntryFromConfig(config);
4112
4457
  if (entryPath) {
4113
- resolvedEntryPath = resolve4(config.root, entryPath);
4458
+ resolvedEntryPath = resolve6(config.root, entryPath);
4114
4459
  }
4115
4460
  },
4116
4461
  transform(code, id) {
@@ -4219,10 +4564,10 @@ function createCjsToEsmPlugin() {
4219
4564
 
4220
4565
  // src/vite/router-discovery.ts
4221
4566
  import { createServer as createViteServer } from "vite";
4222
- import { resolve as resolve8 } from "node:path";
4567
+ import { resolve as resolve10 } from "node:path";
4223
4568
  import { readFileSync as readFileSync6 } from "node:fs";
4224
- import { createRequire as createRequire2, register } from "node:module";
4225
- import { pathToFileURL } from "node:url";
4569
+ import { createRequire as createRequire3, register } from "node:module";
4570
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
4226
4571
 
4227
4572
  // src/vite/inject-client-debug.ts
4228
4573
  function injectClientDebugFlag(id) {
@@ -4860,13 +5205,13 @@ function contextSet(variables, keyOrVar, value, options) {
4860
5205
  import { createHash as createHash4 } from "node:crypto";
4861
5206
  import {
4862
5207
  copyFileSync,
4863
- existsSync as existsSync4,
5208
+ existsSync as existsSync5,
4864
5209
  mkdirSync,
4865
5210
  rmSync,
4866
5211
  statSync,
4867
5212
  writeFileSync as writeFileSync2
4868
5213
  } from "node:fs";
4869
- import { resolve as resolve5 } from "node:path";
5214
+ import { resolve as resolve7 } from "node:path";
4870
5215
  function encodePathParam(value) {
4871
5216
  return String(value).split("/").map((segment) => encodeURIComponent(segment)).join("/");
4872
5217
  }
@@ -4989,7 +5334,7 @@ function resolvePrerenderError(registry, error, onError, label, elapsed, phase,
4989
5334
  throw error;
4990
5335
  }
4991
5336
  function getStagedAssetDir(projectRoot) {
4992
- return resolve5(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
5337
+ return resolve7(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
4993
5338
  }
4994
5339
  function resetStagedBuildAssets(projectRoot) {
4995
5340
  rmSync(getStagedAssetDir(projectRoot), { recursive: true, force: true });
@@ -4999,8 +5344,8 @@ function stageBuildAssetModule(projectRoot, prefix, exportValue) {
4999
5344
  mkdirSync(stagedDir, { recursive: true });
5000
5345
  const contentHash = createHash4("sha256").update(exportValue).digest("hex").slice(0, 8);
5001
5346
  const fileName = `${prefix}-${contentHash}.js`;
5002
- const filePath = resolve5(stagedDir, fileName);
5003
- if (!existsSync4(filePath)) {
5347
+ const filePath = resolve7(stagedDir, fileName);
5348
+ if (!existsSync5(filePath)) {
5004
5349
  writeFileSync2(filePath, `export default ${exportValue};
5005
5350
  `);
5006
5351
  }
@@ -5008,12 +5353,12 @@ function stageBuildAssetModule(projectRoot, prefix, exportValue) {
5008
5353
  }
5009
5354
  function copyStagedBuildAssets(projectRoot, fileNames) {
5010
5355
  const stagedDir = getStagedAssetDir(projectRoot);
5011
- const distAssetsDir = resolve5(projectRoot, "dist/rsc/assets");
5356
+ const distAssetsDir = resolve7(projectRoot, "dist/rsc/assets");
5012
5357
  mkdirSync(distAssetsDir, { recursive: true });
5013
5358
  let totalBytes = 0;
5014
5359
  for (const fileName of new Set(fileNames)) {
5015
- const stagedPath = resolve5(stagedDir, fileName);
5016
- const distPath = resolve5(distAssetsDir, fileName);
5360
+ const stagedPath = resolve7(stagedDir, fileName);
5361
+ const distPath = resolve7(distAssetsDir, fileName);
5017
5362
  copyFileSync(stagedPath, distPath);
5018
5363
  totalBytes += statSync(stagedPath).size;
5019
5364
  }
@@ -5682,8 +6027,8 @@ async function discoverRouters(state, rscEnv) {
5682
6027
  }
5683
6028
 
5684
6029
  // src/vite/discovery/route-types-writer.ts
5685
- import { dirname as dirname3, join as join3, resolve as resolve6 } from "node:path";
5686
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
6030
+ import { dirname as dirname3, join as join4, resolve as resolve8 } from "node:path";
6031
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
5687
6032
  function filterUserNamedRoutes(manifest) {
5688
6033
  const filtered = {};
5689
6034
  for (const [name, pattern] of Object.entries(manifest)) {
@@ -5694,7 +6039,7 @@ function filterUserNamedRoutes(manifest) {
5694
6039
  return filtered;
5695
6040
  }
5696
6041
  function writeGenFileIfChanged(state, outPath, source, opts) {
5697
- const existing = existsSync5(outPath) ? readFileSync4(outPath, "utf-8") : null;
6042
+ const existing = existsSync6(outPath) ? readFileSync4(outPath, "utf-8") : null;
5698
6043
  if (existing === source) return;
5699
6044
  markSelfGenWrite(state, outPath, source);
5700
6045
  writeFileSync3(outPath, source);
@@ -5712,10 +6057,10 @@ function writeRouteTypesFiles(state) {
5712
6057
  if (state.perRouterManifests.length === 0) return;
5713
6058
  try {
5714
6059
  const entryDir = dirname3(
5715
- resolve6(state.projectRoot, state.resolvedEntryPath)
6060
+ resolve8(state.projectRoot, state.resolvedEntryPath)
5716
6061
  );
5717
- const oldCombinedPath = join3(entryDir, "named-routes.gen.ts");
5718
- if (existsSync5(oldCombinedPath)) {
6062
+ const oldCombinedPath = join4(entryDir, "named-routes.gen.ts");
6063
+ if (existsSync6(oldCombinedPath)) {
5719
6064
  unlinkSync2(oldCombinedPath);
5720
6065
  console.log(
5721
6066
  `[rango] Removed stale combined route types: ${oldCombinedPath}`
@@ -5796,7 +6141,7 @@ function supplementGenFilesWithRuntimeRoutes(state) {
5796
6141
  }
5797
6142
 
5798
6143
  // src/vite/discovery/virtual-module-codegen.ts
5799
- import { dirname as dirname4, basename, join as join4 } from "node:path";
6144
+ import { dirname as dirname4, basename, join as join5 } from "node:path";
5800
6145
  function generateRoutesManifestModule(state) {
5801
6146
  const hasManifest = state.mergedRouteManifest && Object.keys(state.mergedRouteManifest).length > 0;
5802
6147
  if (hasManifest) {
@@ -5811,7 +6156,7 @@ function generateRoutesManifestModule(state) {
5811
6156
  /\.(tsx?|jsx?)$/,
5812
6157
  ""
5813
6158
  );
5814
- const genPath = join4(
6159
+ const genPath = join5(
5815
6160
  routerDir,
5816
6161
  `${routerBasename}.named-routes.gen.js`
5817
6162
  ).replaceAll("\\", "/");
@@ -5896,7 +6241,7 @@ function generatePerRouterModule(state, routerId) {
5896
6241
  /\.(tsx?|jsx?)$/,
5897
6242
  ""
5898
6243
  );
5899
- const genPath = join4(
6244
+ const genPath = join5(
5900
6245
  routerDir,
5901
6246
  `${routerBasename}.named-routes.gen.js`
5902
6247
  ).replaceAll("\\", "/");
@@ -5923,13 +6268,13 @@ function generatePerRouterModule(state, routerId) {
5923
6268
  }
5924
6269
 
5925
6270
  // src/vite/discovery/bundle-postprocess.ts
5926
- import { resolve as resolve7 } from "node:path";
5927
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
6271
+ import { resolve as resolve9 } from "node:path";
6272
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "node:fs";
5928
6273
  function postprocessBundle(state) {
5929
6274
  const hasPrerenderData = state.prerenderManifestEntries && Object.keys(state.prerenderManifestEntries).length > 0;
5930
6275
  const hasStaticData = state.staticManifestEntries && Object.keys(state.staticManifestEntries).length > 0;
5931
6276
  if (!hasPrerenderData && !hasStaticData) return;
5932
- const rscEntryPath = resolve7(
6277
+ const rscEntryPath = resolve9(
5933
6278
  state.projectRoot,
5934
6279
  "dist/rsc",
5935
6280
  state.rscEntryFileName ?? "index.js"
@@ -5950,7 +6295,7 @@ function postprocessBundle(state) {
5950
6295
  ];
5951
6296
  for (const target of evictionTargets) {
5952
6297
  for (const info of target.infos) {
5953
- const chunkPath = resolve7(state.projectRoot, "dist/rsc", info.fileName);
6298
+ const chunkPath = resolve9(state.projectRoot, "dist/rsc", info.fileName);
5954
6299
  try {
5955
6300
  const code = readFileSync5(chunkPath, "utf-8");
5956
6301
  const result = evictHandlerCode(
@@ -5975,7 +6320,7 @@ function postprocessBundle(state) {
5975
6320
  }
5976
6321
  state.handlerChunkInfoMap.clear();
5977
6322
  state.staticHandlerChunkInfoMap.clear();
5978
- if (hasPrerenderData && existsSync6(rscEntryPath)) {
6323
+ if (hasPrerenderData && existsSync7(rscEntryPath)) {
5979
6324
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
5980
6325
  if (!rscCode.includes("__prerender-manifest.js")) {
5981
6326
  try {
@@ -5995,7 +6340,7 @@ function postprocessBundle(state) {
5995
6340
  `export default m;`,
5996
6341
  ""
5997
6342
  ].join("\n");
5998
- const manifestPath = resolve7(
6343
+ const manifestPath = resolve9(
5999
6344
  state.projectRoot,
6000
6345
  "dist/rsc/__prerender-manifest.js"
6001
6346
  );
@@ -6015,7 +6360,7 @@ function postprocessBundle(state) {
6015
6360
  }
6016
6361
  }
6017
6362
  }
6018
- if (hasStaticData && existsSync6(rscEntryPath)) {
6363
+ if (hasStaticData && existsSync7(rscEntryPath)) {
6019
6364
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
6020
6365
  if (!rscCode.includes("__static-manifest.js")) {
6021
6366
  try {
@@ -6033,7 +6378,7 @@ function postprocessBundle(state) {
6033
6378
  }
6034
6379
  const manifestCode = `const m={${manifestEntries.join(",")}};globalThis.__STATIC_MANIFEST=m;export default m;
6035
6380
  `;
6036
- const manifestPath = resolve7(
6381
+ const manifestPath = resolve9(
6037
6382
  state.projectRoot,
6038
6383
  "dist/rsc/__static-manifest.js"
6039
6384
  );
@@ -6065,8 +6410,8 @@ function createDiscoveryGate(s, debug11) {
6065
6410
  let pendingEvents = false;
6066
6411
  const beginGate = () => {
6067
6412
  if (gatePending) return;
6068
- s.discoveryDone = new Promise((resolve10) => {
6069
- gateResolver = resolve10;
6413
+ s.discoveryDone = new Promise((resolve12) => {
6414
+ gateResolver = resolve12;
6070
6415
  });
6071
6416
  gatePending = true;
6072
6417
  };
@@ -6158,15 +6503,15 @@ function selectForwardableResolvePlugins(plugins) {
6158
6503
  }
6159
6504
  function pickForwardedRunnerConfig(config) {
6160
6505
  const r = config.resolve ?? {};
6161
- const resolve10 = {};
6162
- if (r.alias !== void 0) resolve10.alias = r.alias;
6163
- if (r.dedupe !== void 0) resolve10.dedupe = r.dedupe;
6164
- if (r.conditions !== void 0) resolve10.conditions = r.conditions;
6165
- if (r.mainFields !== void 0) resolve10.mainFields = r.mainFields;
6166
- if (r.extensions !== void 0) resolve10.extensions = r.extensions;
6506
+ const resolve12 = {};
6507
+ if (r.alias !== void 0) resolve12.alias = r.alias;
6508
+ if (r.dedupe !== void 0) resolve12.dedupe = r.dedupe;
6509
+ if (r.conditions !== void 0) resolve12.conditions = r.conditions;
6510
+ if (r.mainFields !== void 0) resolve12.mainFields = r.mainFields;
6511
+ if (r.extensions !== void 0) resolve12.extensions = r.extensions;
6167
6512
  if (r.preserveSymlinks !== void 0)
6168
- resolve10.preserveSymlinks = r.preserveSymlinks;
6169
- if (r.tsconfigPaths !== void 0) resolve10.tsconfigPaths = r.tsconfigPaths;
6513
+ resolve12.preserveSymlinks = r.preserveSymlinks;
6514
+ if (r.tsconfigPaths !== void 0) resolve12.tsconfigPaths = r.tsconfigPaths;
6170
6515
  const userOxc = config.oxc;
6171
6516
  const userJsx = userOxc && typeof userOxc === "object" && typeof userOxc.jsx === "object" && userOxc.jsx !== null ? userOxc.jsx : {};
6172
6517
  const oxc = userOxc && typeof userOxc === "object" ? {
@@ -6174,7 +6519,7 @@ function pickForwardedRunnerConfig(config) {
6174
6519
  jsx: { ...userJsx, runtime: "automatic", importSource: "react" }
6175
6520
  } : { jsx: { runtime: "automatic", importSource: "react" } };
6176
6521
  return {
6177
- resolve: resolve10,
6522
+ resolve: resolve12,
6178
6523
  define: config.define,
6179
6524
  oxc
6180
6525
  };
@@ -6252,11 +6597,11 @@ async function resolveBuildEnv(option, factoryCtx) {
6252
6597
  );
6253
6598
  }
6254
6599
  try {
6255
- const userRequire = createRequire2(
6256
- resolve8(factoryCtx.root, "package.json")
6600
+ const userRequire = createRequire3(
6601
+ resolve10(factoryCtx.root, "package.json")
6257
6602
  );
6258
6603
  const wranglerPath = userRequire.resolve("wrangler");
6259
- const { getPlatformProxy } = await import(pathToFileURL(wranglerPath).href);
6604
+ const { getPlatformProxy } = await import(pathToFileURL2(wranglerPath).href);
6260
6605
  const proxy = await getPlatformProxy();
6261
6606
  return {
6262
6607
  env: proxy.env,
@@ -6353,8 +6698,8 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
6353
6698
  if (globalThis.__rscRouterDiscoveryActive) return;
6354
6699
  s.devServer = server;
6355
6700
  let resolveDiscovery;
6356
- const discoveryPromise = new Promise((resolve10) => {
6357
- resolveDiscovery = resolve10;
6701
+ const discoveryPromise = new Promise((resolve12) => {
6702
+ resolveDiscovery = resolve12;
6358
6703
  });
6359
6704
  const gate = createDiscoveryGate(s, debugDiscovery);
6360
6705
  const beginDiscoveryGate = gate.beginGate;
@@ -6936,7 +7281,7 @@ ${err.stack}`
6936
7281
  if (hasCreateRouter) {
6937
7282
  const nestedRouterConflict = findNestedRouterConflict([
6938
7283
  ...s.cachedRouterFiles ?? [],
6939
- resolve8(filePath)
7284
+ resolve10(filePath)
6940
7285
  ]);
6941
7286
  if (nestedRouterConflict) {
6942
7287
  server.config.logger.error(
@@ -7248,7 +7593,11 @@ async function rango(options) {
7248
7593
  ];
7249
7594
  const pkg = getPublishedPackageName();
7250
7595
  const nested = (spec) => `${pkg} > ${spec}`;
7251
- const routerRef = { path: void 0 };
7596
+ const routerRef = {
7597
+ path: void 0,
7598
+ kind: "router"
7599
+ };
7600
+ const explicitHostRouter = preset !== "cloudflare" ? resolvedOptions.hostRouter : void 0;
7252
7601
  const prerenderEnabled = true;
7253
7602
  if (preset === "cloudflare") {
7254
7603
  const { default: rsc } = await import("@vitejs/plugin-rsc");
@@ -7356,17 +7705,49 @@ async function rango(options) {
7356
7705
  name: "@rangojs/router:auto-discover",
7357
7706
  config(userConfig) {
7358
7707
  if (routerRef.path) return;
7359
- const root = userConfig.root ? resolve9(process.cwd(), userConfig.root) : process.cwd();
7708
+ const root = userConfig.root ? resolve11(process.cwd(), userConfig.root) : process.cwd();
7709
+ const toRootRelative = (abs) => (abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs).replaceAll("\\", "/");
7710
+ const bulletList = (files) => files.map((f) => " - " + toRootRelative(f)).join("\n");
7711
+ if (explicitHostRouter) {
7712
+ routerRef.path = normalizeHostRouterEntry(
7713
+ explicitHostRouter,
7714
+ root,
7715
+ existsSync8
7716
+ );
7717
+ routerRef.kind = "host";
7718
+ return;
7719
+ }
7720
+ const hostCandidates = findHostRouterFiles(root);
7721
+ if (hostCandidates.length === 1) {
7722
+ const hostPath = toRootRelative(hostCandidates[0]);
7723
+ console.info(
7724
+ `[rango] Serving host router entry ${hostPath} (auto-detected). Set the \`hostRouter\` option to override.`
7725
+ );
7726
+ routerRef.path = hostPath;
7727
+ routerRef.kind = "host";
7728
+ return;
7729
+ }
7730
+ if (hostCandidates.length > 1) {
7731
+ throw new Error(
7732
+ `[rango] Multiple host routers found:
7733
+ ${bulletList(hostCandidates)}
7734
+
7735
+ Set the \`hostRouter\` option to the entry to serve, e.g. rango({ preset: "${preset}", hostRouter: "./src/worker.rsc.tsx" }).`
7736
+ );
7737
+ }
7360
7738
  const candidates = findRouterFiles(root);
7361
7739
  if (candidates.length === 1) {
7362
- const abs = candidates[0];
7363
- routerRef.path = (abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs).replaceAll("\\", "/");
7364
- } else if (candidates.length > 1) {
7365
- const list = candidates.map(
7366
- (f) => " - " + (f.startsWith(root) ? f.slice(root.length + 1) : f)
7367
- ).join("\n");
7368
- throw new Error(`[rango] Multiple routers found:
7369
- ${list}`);
7740
+ routerRef.path = toRootRelative(candidates[0]);
7741
+ routerRef.kind = "router";
7742
+ return;
7743
+ }
7744
+ if (candidates.length > 1) {
7745
+ throw new Error(
7746
+ `[rango] Multiple routers found:
7747
+ ${bulletList(candidates)}
7748
+
7749
+ If this is a multi-app host router, export a createHostRouter() instance and set the \`hostRouter\` option (e.g. rango({ preset: "${preset}", hostRouter: "./src/worker.rsc.tsx" })), or use preset: "cloudflare" where you own the worker entry.`
7750
+ );
7370
7751
  }
7371
7752
  }
7372
7753
  });
@@ -7380,8 +7761,11 @@ ${list}`);
7380
7761
  plugins.push({
7381
7762
  name: "@rangojs/router:rsc-integration",
7382
7763
  enforce: "pre",
7383
- config() {
7764
+ config(_userConfig, configEnv) {
7765
+ const vercelDefine = preset === "vercel" && configEnv.command === "build" ? { "process.env.NODE_ENV": JSON.stringify("production") } : void 0;
7766
+ const vercelServerEnv = preset === "vercel" && configEnv.command === "build" ? { resolve: { noExternal: true } } : void 0;
7384
7767
  return {
7768
+ ...vercelDefine ? { define: vercelDefine } : {},
7385
7769
  optimizeDeps: {
7386
7770
  exclude: excludeDeps,
7387
7771
  rolldownOptions: sharedRolldownOptions
@@ -7424,6 +7808,7 @@ ${list}`);
7424
7808
  }
7425
7809
  },
7426
7810
  ssr: {
7811
+ ...vercelServerEnv ?? {},
7427
7812
  optimizeDeps: {
7428
7813
  entries: [VIRTUAL_IDS.ssr],
7429
7814
  include: [
@@ -7442,6 +7827,7 @@ ${list}`);
7442
7827
  }
7443
7828
  },
7444
7829
  rsc: {
7830
+ ...vercelServerEnv ?? {},
7445
7831
  optimizeDeps: {
7446
7832
  entries: [VIRTUAL_IDS.rsc],
7447
7833
  include: [
@@ -7467,7 +7853,11 @@ ${list}`);
7467
7853
  configResolved(config) {
7468
7854
  if (showBanner) {
7469
7855
  const mode = config.command === "serve" ? process.argv.includes("preview") ? "preview" : "dev" : "build";
7470
- printBanner(mode, "node", rangoVersion);
7856
+ printBanner(
7857
+ mode,
7858
+ preset === "vercel" ? "vercel" : "node",
7859
+ rangoVersion
7860
+ );
7471
7861
  }
7472
7862
  const rscMinimalCount = config.plugins.filter(
7473
7863
  (p) => p.name === "rsc:minimal"
@@ -7529,6 +7919,11 @@ ${list}`);
7529
7919
  clientChunkCtx
7530
7920
  })
7531
7921
  );
7922
+ if (preset === "vercel") {
7923
+ plugins.push(
7924
+ createVercelOutputPlugin(resolvedOptions)
7925
+ );
7926
+ }
7532
7927
  debugConfig?.(
7533
7928
  "rango(%s) setup done: %d plugin(s) (%sms)",
7534
7929
  preset,