@rangojs/router 0.0.0-experimental.c65e9248 → 0.0.0-experimental.c73f3e16

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.
Files changed (132) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +133 -47
  3. package/dist/vite/index.js +708 -234
  4. package/package.json +2 -2
  5. package/skills/cache-guide/SKILL.md +32 -0
  6. package/skills/caching/SKILL.md +8 -0
  7. package/skills/links/SKILL.md +3 -1
  8. package/skills/loader/SKILL.md +53 -43
  9. package/skills/middleware/SKILL.md +2 -0
  10. package/skills/parallel/SKILL.md +126 -0
  11. package/skills/prerender/SKILL.md +110 -68
  12. package/skills/route/SKILL.md +31 -0
  13. package/skills/router-setup/SKILL.md +87 -2
  14. package/skills/typesafety/SKILL.md +10 -0
  15. package/src/__internal.ts +1 -1
  16. package/src/browser/app-version.ts +14 -0
  17. package/src/browser/event-controller.ts +5 -0
  18. package/src/browser/navigation-bridge.ts +16 -3
  19. package/src/browser/navigation-client.ts +77 -66
  20. package/src/browser/navigation-store.ts +43 -8
  21. package/src/browser/partial-update.ts +37 -4
  22. package/src/browser/prefetch/cache.ts +6 -6
  23. package/src/browser/prefetch/fetch.ts +21 -16
  24. package/src/browser/prefetch/queue.ts +61 -29
  25. package/src/browser/prefetch/resource-ready.ts +77 -0
  26. package/src/browser/react/Link.tsx +44 -8
  27. package/src/browser/react/NavigationProvider.tsx +13 -4
  28. package/src/browser/react/context.ts +7 -2
  29. package/src/browser/react/use-handle.ts +9 -58
  30. package/src/browser/react/use-router.ts +21 -8
  31. package/src/browser/rsc-router.tsx +48 -4
  32. package/src/browser/scroll-restoration.ts +10 -8
  33. package/src/browser/segment-reconciler.ts +6 -1
  34. package/src/browser/server-action-bridge.ts +8 -6
  35. package/src/browser/types.ts +27 -5
  36. package/src/build/generate-manifest.ts +6 -6
  37. package/src/build/generate-route-types.ts +3 -0
  38. package/src/build/route-types/include-resolution.ts +8 -1
  39. package/src/build/route-types/router-processing.ts +211 -72
  40. package/src/build/route-types/scan-filter.ts +8 -1
  41. package/src/cache/cache-runtime.ts +15 -11
  42. package/src/cache/cache-scope.ts +46 -5
  43. package/src/cache/document-cache.ts +17 -7
  44. package/src/cache/taint.ts +55 -0
  45. package/src/client.tsx +2 -56
  46. package/src/context-var.ts +72 -2
  47. package/src/debug.ts +2 -2
  48. package/src/handle.ts +40 -0
  49. package/src/index.rsc.ts +3 -1
  50. package/src/index.ts +12 -0
  51. package/src/prerender/store.ts +5 -4
  52. package/src/prerender.ts +138 -77
  53. package/src/reverse.ts +22 -1
  54. package/src/route-definition/dsl-helpers.ts +73 -25
  55. package/src/route-definition/helpers-types.ts +10 -6
  56. package/src/route-definition/index.ts +3 -0
  57. package/src/route-definition/redirect.ts +9 -1
  58. package/src/route-definition/resolve-handler-use.ts +149 -0
  59. package/src/route-types.ts +11 -0
  60. package/src/router/content-negotiation.ts +100 -1
  61. package/src/router/handler-context.ts +79 -23
  62. package/src/router/intercept-resolution.ts +9 -4
  63. package/src/router/lazy-includes.ts +2 -1
  64. package/src/router/loader-resolution.ts +156 -21
  65. package/src/router/logging.ts +1 -1
  66. package/src/router/manifest.ts +6 -2
  67. package/src/router/match-api.ts +124 -189
  68. package/src/router/match-middleware/background-revalidation.ts +30 -2
  69. package/src/router/match-middleware/cache-lookup.ts +92 -16
  70. package/src/router/match-middleware/cache-store.ts +53 -10
  71. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  72. package/src/router/match-middleware/segment-resolution.ts +60 -5
  73. package/src/router/match-result.ts +22 -6
  74. package/src/router/metrics.ts +6 -1
  75. package/src/router/middleware-types.ts +6 -8
  76. package/src/router/middleware.ts +2 -5
  77. package/src/router/navigation-snapshot.ts +182 -0
  78. package/src/router/prerender-match.ts +110 -10
  79. package/src/router/preview-match.ts +30 -102
  80. package/src/router/request-classification.ts +310 -0
  81. package/src/router/route-snapshot.ts +245 -0
  82. package/src/router/router-context.ts +1 -0
  83. package/src/router/router-interfaces.ts +36 -4
  84. package/src/router/router-options.ts +37 -11
  85. package/src/router/segment-resolution/fresh.ts +193 -20
  86. package/src/router/segment-resolution/helpers.ts +29 -24
  87. package/src/router/segment-resolution/loader-cache.ts +1 -0
  88. package/src/router/segment-resolution/revalidation.ts +431 -296
  89. package/src/router/types.ts +1 -0
  90. package/src/router.ts +55 -6
  91. package/src/rsc/handler.ts +472 -372
  92. package/src/rsc/loader-fetch.ts +23 -3
  93. package/src/rsc/manifest-init.ts +5 -1
  94. package/src/rsc/progressive-enhancement.ts +14 -2
  95. package/src/rsc/rsc-rendering.ts +10 -1
  96. package/src/rsc/server-action.ts +8 -0
  97. package/src/rsc/ssr-setup.ts +2 -2
  98. package/src/rsc/types.ts +9 -1
  99. package/src/segment-system.tsx +140 -4
  100. package/src/server/context.ts +140 -14
  101. package/src/server/handle-store.ts +19 -0
  102. package/src/server/loader-registry.ts +9 -8
  103. package/src/server/request-context.ts +175 -15
  104. package/src/ssr/index.tsx +4 -0
  105. package/src/static-handler.ts +18 -6
  106. package/src/types/cache-types.ts +4 -4
  107. package/src/types/handler-context.ts +137 -33
  108. package/src/types/loader-types.ts +36 -9
  109. package/src/types/route-entry.ts +1 -1
  110. package/src/types/segments.ts +2 -0
  111. package/src/urls/path-helper-types.ts +9 -2
  112. package/src/urls/path-helper.ts +48 -13
  113. package/src/urls/pattern-types.ts +12 -0
  114. package/src/urls/response-types.ts +16 -6
  115. package/src/use-loader.tsx +77 -5
  116. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  117. package/src/vite/discovery/discover-routers.ts +5 -1
  118. package/src/vite/discovery/prerender-collection.ts +128 -74
  119. package/src/vite/discovery/state.ts +13 -4
  120. package/src/vite/index.ts +4 -0
  121. package/src/vite/plugin-types.ts +60 -5
  122. package/src/vite/plugins/expose-id-utils.ts +12 -0
  123. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  124. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  125. package/src/vite/plugins/performance-tracks.ts +88 -0
  126. package/src/vite/plugins/refresh-cmd.ts +88 -26
  127. package/src/vite/plugins/version-plugin.ts +9 -0
  128. package/src/vite/rango.ts +19 -2
  129. package/src/vite/router-discovery.ts +178 -37
  130. package/src/vite/utils/banner.ts +3 -3
  131. package/src/vite/utils/prerender-utils.ts +18 -0
  132. package/src/vite/utils/shared-utils.ts +3 -2
@@ -18,6 +18,9 @@ function hashId(filePath, exportName) {
18
18
  const hash = crypto.createHash("sha256").update(input).digest("hex");
19
19
  return `${hash.slice(0, 8)}#${exportName}`;
20
20
  }
21
+ function makeStubId(filePath, exportName, isBuild) {
22
+ return isBuild ? hashId(filePath, exportName) : `${filePath}#${exportName}`;
23
+ }
21
24
  function hashInlineId(filePath, lineNumber, index) {
22
25
  const input = index !== void 0 && index > 0 ? `${filePath}:${lineNumber}:${index}` : `${filePath}:${lineNumber}`;
23
26
  return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
@@ -910,9 +913,7 @@ function generateWholeFileStubs(cfg, bindings, code, filePath, isBuild) {
910
913
  });
911
914
  return { code: stubs.join("\n") + "\n", map: null };
912
915
  }
913
- function generateExprStubs(cfg, bindings, code, filePath, sourceId, isBuild) {
914
- if (bindings.length === 0) return null;
915
- const s = new MagicString2(code);
916
+ function stubHandlerExprs(cfg, bindings, s, filePath, isBuild) {
916
917
  let hasChanges = false;
917
918
  for (const binding of bindings) {
918
919
  const exportName = binding.exportNames[0];
@@ -924,15 +925,7 @@ function generateExprStubs(cfg, bindings, code, filePath, sourceId, isBuild) {
924
925
  );
925
926
  hasChanges = true;
926
927
  }
927
- if (!hasChanges) return null;
928
- return {
929
- code: s.toString(),
930
- map: s.generateMap({
931
- source: sourceId,
932
- includeContent: true,
933
- hires: "boundary"
934
- })
935
- };
928
+ return hasChanges;
936
929
  }
937
930
  function transformHandlerIds(cfg, bindings, s, filePath, isBuild) {
938
931
  let hasChanges = false;
@@ -1269,15 +1262,6 @@ ${lazyImports.join(",\n")}
1269
1262
  isBuild
1270
1263
  );
1271
1264
  if (wholeFile) return wholeFile;
1272
- const exprStubs = generateExprStubs(
1273
- PRERENDER_CONFIG,
1274
- bindings,
1275
- code,
1276
- filePath,
1277
- id,
1278
- isBuild
1279
- );
1280
- if (exprStubs) return exprStubs;
1281
1265
  }
1282
1266
  if (hasPrerenderHandlerCode && isRscEnv && isBuild) {
1283
1267
  const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
@@ -1329,15 +1313,134 @@ ${lazyImports.join(",\n")}
1329
1313
  isBuild
1330
1314
  );
1331
1315
  if (wholeFile) return wholeFile;
1332
- const exprStubs = generateExprStubs(
1333
- STATIC_CONFIG,
1334
- bindings,
1335
- code,
1336
- filePath,
1337
- id,
1338
- isBuild
1339
- );
1340
- if (exprStubs) return exprStubs;
1316
+ }
1317
+ if (!isRscEnv && (hasPrerenderHandlerCode || hasStaticHandlerCode)) {
1318
+ const prerenderFnNames = hasPrerenderHandlerCode ? getFnNames(PRERENDER_CONFIG.fnName) : [];
1319
+ const staticFnNames = hasStaticHandlerCode ? getFnNames(STATIC_CONFIG.fnName) : [];
1320
+ const loaderFnNames = hasLoaderCode ? getFnNames("createLoader") : [];
1321
+ const handleFnNames = hasHandleCode ? getFnNames("createHandle") : [];
1322
+ const lsFnNames = hasLocationStateCode ? getFnNames("createLocationState") : [];
1323
+ const allBindings = [];
1324
+ for (const fnNames of [
1325
+ prerenderFnNames,
1326
+ staticFnNames,
1327
+ loaderFnNames,
1328
+ handleFnNames,
1329
+ lsFnNames
1330
+ ]) {
1331
+ if (fnNames.length > 0) {
1332
+ allBindings.push(...getBindings(code, fnNames));
1333
+ }
1334
+ }
1335
+ let canStubWholeFile = allBindings.length > 0 && isExportOnlyFile(code, allBindings);
1336
+ if (canStubWholeFile && (handleFnNames.length > 0 || lsFnNames.length > 0)) {
1337
+ const exportedLocals = new Set(allBindings.map((b) => b.localName));
1338
+ const strippedBindings = [];
1339
+ const localDeclPattern = /(?:^|;|\n)\s*(?:const|let|var|function)\s+(\w+)/g;
1340
+ let declMatch;
1341
+ while ((declMatch = localDeclPattern.exec(code)) !== null) {
1342
+ const name = declMatch[1];
1343
+ if (!exportedLocals.has(name) && !/^_c\d*$/.test(name)) {
1344
+ strippedBindings.push(name);
1345
+ }
1346
+ }
1347
+ const importPattern = /import\s*\{([^}]*)\}\s*from\s*["'](?!@rangojs\/router)[^"']*["']/g;
1348
+ let importMatch;
1349
+ while ((importMatch = importPattern.exec(code)) !== null) {
1350
+ for (const spec of importMatch[1].split(",")) {
1351
+ const m = spec.trim().match(/^[A-Za-z_$][\w$]*(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
1352
+ if (m) strippedBindings.push(m[1] || m[0].trim().split(/\s/)[0]);
1353
+ }
1354
+ }
1355
+ const defaultImportPattern = /import\s+([A-Za-z_$][\w$]*)\s+from\s*["'](?!@rangojs\/router)[^"']*["']/g;
1356
+ while ((importMatch = defaultImportPattern.exec(code)) !== null) {
1357
+ strippedBindings.push(importMatch[1]);
1358
+ }
1359
+ const nsImportPattern = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s*["'](?!@rangojs\/router)[^"']*["']/g;
1360
+ while ((importMatch = nsImportPattern.exec(code)) !== null) {
1361
+ strippedBindings.push(importMatch[1]);
1362
+ }
1363
+ if (strippedBindings.length > 0) {
1364
+ const preservedBindings = allBindings.filter((b) => {
1365
+ const fc = code.slice(b.callExprStart, b.callOpenParenPos + 1);
1366
+ return handleFnNames.some((n) => fc.includes(n)) || lsFnNames.some((n) => fc.includes(n));
1367
+ });
1368
+ const strippedRe = new RegExp(
1369
+ `\\b(?:${strippedBindings.join("|")})\\b`
1370
+ );
1371
+ canStubWholeFile = !preservedBindings.some((b) => {
1372
+ const expr = code.slice(b.callExprStart, b.callCloseParenPos + 1);
1373
+ return strippedRe.test(expr);
1374
+ });
1375
+ }
1376
+ }
1377
+ if (canStubWholeFile) {
1378
+ const lines = [];
1379
+ const neededImports = [];
1380
+ if (handleFnNames.length > 0) neededImports.push("createHandle");
1381
+ if (lsFnNames.length > 0) neededImports.push("createLocationState");
1382
+ if (neededImports.length > 0) {
1383
+ lines.push(
1384
+ `import { ${neededImports.join(", ")} } from "@rangojs/router";`
1385
+ );
1386
+ }
1387
+ for (const binding of allBindings) {
1388
+ const fnCall = code.slice(
1389
+ binding.callExprStart,
1390
+ binding.callOpenParenPos + 1
1391
+ );
1392
+ const isHandle = handleFnNames.some((n) => fnCall.includes(n));
1393
+ const isLocationState = lsFnNames.some((n) => fnCall.includes(n));
1394
+ const primaryName = binding.exportNames[0];
1395
+ const stubId = makeStubId(filePath, primaryName, isBuild);
1396
+ if (isHandle || isLocationState) {
1397
+ const rawArgs = code.slice(binding.callOpenParenPos + 1, binding.callCloseParenPos).replace(/\b_c\d*\s*=\s*/g, "");
1398
+ const canonicalName = isHandle ? "createHandle" : "createLocationState";
1399
+ const activeFnNames = isHandle ? handleFnNames : lsFnNames;
1400
+ let rawCallee = code.slice(
1401
+ binding.callExprStart,
1402
+ binding.callOpenParenPos
1403
+ );
1404
+ for (const alias of activeFnNames) {
1405
+ if (alias !== canonicalName && rawCallee.startsWith(alias)) {
1406
+ rawCallee = canonicalName + rawCallee.slice(alias.length);
1407
+ break;
1408
+ }
1409
+ }
1410
+ if (isHandle) {
1411
+ const idParam = binding.argCount === 0 ? `undefined, "${stubId}"` : `, "${stubId}"`;
1412
+ lines.push(
1413
+ `export const ${primaryName} = ${rawCallee}(${rawArgs}${idParam});`
1414
+ );
1415
+ lines.push(`${primaryName}.$$id = "${stubId}";`);
1416
+ } else {
1417
+ lines.push(
1418
+ `export const ${primaryName} = ${rawCallee}(${rawArgs});`
1419
+ );
1420
+ lines.push(
1421
+ `${primaryName}.__rsc_ls_key = "__rsc_ls_${stubId}";`
1422
+ );
1423
+ }
1424
+ for (const name of binding.exportNames.slice(1)) {
1425
+ lines.push(`export const ${name} = ${primaryName};`);
1426
+ }
1427
+ } else {
1428
+ let brand = "loader";
1429
+ if (prerenderFnNames.some((n) => fnCall.includes(n))) {
1430
+ brand = PRERENDER_CONFIG.brand;
1431
+ } else if (staticFnNames.some((n) => fnCall.includes(n))) {
1432
+ brand = STATIC_CONFIG.brand;
1433
+ }
1434
+ lines.push(
1435
+ `export const ${primaryName} = { __brand: "${brand}", $$id: "${stubId}" };`
1436
+ );
1437
+ for (const name of binding.exportNames.slice(1)) {
1438
+ lines.push(`export const ${name} = ${primaryName};`);
1439
+ }
1440
+ }
1441
+ }
1442
+ return { code: lines.join("\n") + "\n", map: null };
1443
+ }
1341
1444
  }
1342
1445
  if (hasStaticHandlerCode && isRscEnv && isBuild) {
1343
1446
  const fnNames = getFnNames(STATIC_CONFIG.fnName);
@@ -1372,25 +1475,41 @@ ${lazyImports.join(",\n")}
1372
1475
  isBuild
1373
1476
  ) || changed;
1374
1477
  }
1375
- if (hasPrerenderHandlerCode && isRscEnv) {
1478
+ if (hasPrerenderHandlerCode) {
1376
1479
  const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
1377
- changed = transformHandlerIds(
1378
- PRERENDER_CONFIG,
1379
- getBindings(code, fnNames),
1380
- s,
1381
- filePath,
1382
- isBuild
1383
- ) || changed;
1480
+ const bindings = getBindings(code, fnNames);
1481
+ if (isRscEnv) {
1482
+ changed = transformHandlerIds(
1483
+ PRERENDER_CONFIG,
1484
+ bindings,
1485
+ s,
1486
+ filePath,
1487
+ isBuild
1488
+ ) || changed;
1489
+ } else {
1490
+ changed = stubHandlerExprs(
1491
+ PRERENDER_CONFIG,
1492
+ bindings,
1493
+ s,
1494
+ filePath,
1495
+ isBuild
1496
+ ) || changed;
1497
+ }
1384
1498
  }
1385
- if (hasStaticHandlerCode && isRscEnv) {
1499
+ if (hasStaticHandlerCode) {
1386
1500
  const fnNames = getFnNames(STATIC_CONFIG.fnName);
1387
- changed = transformHandlerIds(
1388
- STATIC_CONFIG,
1389
- getBindings(code, fnNames),
1390
- s,
1391
- filePath,
1392
- isBuild
1393
- ) || changed;
1501
+ const bindings = getBindings(code, fnNames);
1502
+ if (isRscEnv) {
1503
+ changed = transformHandlerIds(
1504
+ STATIC_CONFIG,
1505
+ bindings,
1506
+ s,
1507
+ filePath,
1508
+ isBuild
1509
+ ) || changed;
1510
+ } else {
1511
+ changed = stubHandlerExprs(STATIC_CONFIG, bindings, s, filePath, isBuild) || changed;
1512
+ }
1394
1513
  }
1395
1514
  if (!changed) return;
1396
1515
  return {
@@ -1745,7 +1864,7 @@ import { resolve } from "node:path";
1745
1864
  // package.json
1746
1865
  var package_default = {
1747
1866
  name: "@rangojs/router",
1748
- version: "0.0.0-experimental.c65e9248",
1867
+ version: "0.0.0-experimental.c73f3e16",
1749
1868
  description: "Django-inspired RSC router with composable URL patterns",
1750
1869
  keywords: [
1751
1870
  "react",
@@ -1887,7 +2006,7 @@ var package_default = {
1887
2006
  "test:unit:watch": "vitest"
1888
2007
  },
1889
2008
  dependencies: {
1890
- "@vitejs/plugin-rsc": "^0.5.14",
2009
+ "@vitejs/plugin-rsc": "^0.5.19",
1891
2010
  "magic-string": "^0.30.17",
1892
2011
  picomatch: "^4.0.3",
1893
2012
  "rsc-html-stream": "^0.0.7"
@@ -2317,7 +2436,7 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
2317
2436
  }
2318
2437
  return routeMap;
2319
2438
  }
2320
- function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut) {
2439
+ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock) {
2321
2440
  visited = visited ?? /* @__PURE__ */ new Set();
2322
2441
  const realPath = resolve2(filePath);
2323
2442
  const key = variableName ? `${realPath}:${variableName}` : realPath;
@@ -2333,7 +2452,9 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
2333
2452
  return { routes: {}, searchSchemas: {} };
2334
2453
  }
2335
2454
  let block;
2336
- if (variableName) {
2455
+ if (inlineBlock) {
2456
+ block = inlineBlock;
2457
+ } else if (variableName) {
2337
2458
  const extracted = extractUrlsBlockForVariable(source, variableName);
2338
2459
  if (!extracted) return { routes: {}, searchSchemas: {} };
2339
2460
  block = extracted;
@@ -2452,7 +2573,7 @@ Router root: ${conflict.ancestor}
2452
2573
  Nested router: ${conflict.nested}
2453
2574
  Move the nested router into a sibling directory or configure it as a separate app root.`;
2454
2575
  }
2455
- function extractUrlsVariableFromRouter(code) {
2576
+ function extractUrlsFromRouter(code) {
2456
2577
  const sourceFile = ts5.createSourceFile(
2457
2578
  "router.tsx",
2458
2579
  code,
@@ -2466,24 +2587,70 @@ function extractUrlsVariableFromRouter(code) {
2466
2587
  const callee = node.expression;
2467
2588
  return ts5.isIdentifier(callee) && callee.text === "createRouter";
2468
2589
  }
2590
+ function isInlineBuilder(node) {
2591
+ return ts5.isArrowFunction(node) || ts5.isFunctionExpression(node);
2592
+ }
2593
+ function isRoutesOnCreateRouter(node) {
2594
+ if (!ts5.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "routes")
2595
+ return false;
2596
+ let inner = node.expression.expression;
2597
+ while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
2598
+ inner = inner.expression.expression;
2599
+ }
2600
+ return isCreateRouterCall(inner);
2601
+ }
2469
2602
  function visit(node) {
2470
2603
  if (result) return;
2471
- if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && node.expression.name.text === "routes" && node.arguments.length >= 1 && ts5.isIdentifier(node.arguments[0])) {
2472
- let inner = node.expression.expression;
2473
- while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
2474
- inner = inner.expression.expression;
2475
- }
2476
- if (isCreateRouterCall(inner)) {
2477
- result = node.arguments[0].text;
2478
- return;
2604
+ if (ts5.isCallExpression(node) && node.arguments.length >= 1 && isRoutesOnCreateRouter(node)) {
2605
+ const arg = node.arguments[0];
2606
+ if (ts5.isIdentifier(arg)) {
2607
+ result = { kind: "variable", name: arg.text };
2608
+ } else if (isInlineBuilder(arg)) {
2609
+ result = { kind: "inline", block: arg.getText(sourceFile) };
2479
2610
  }
2611
+ return;
2480
2612
  }
2481
2613
  if (isCreateRouterCall(node)) {
2482
2614
  const callExpr = node;
2483
- for (const arg of callExpr.arguments) {
2615
+ for (const callArg of callExpr.arguments) {
2616
+ if (ts5.isObjectLiteralExpression(callArg)) {
2617
+ for (const prop of callArg.properties) {
2618
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls") {
2619
+ if (ts5.isIdentifier(prop.initializer)) {
2620
+ result = { kind: "variable", name: prop.initializer.text };
2621
+ } else if (isInlineBuilder(prop.initializer)) {
2622
+ result = {
2623
+ kind: "inline",
2624
+ block: prop.initializer.getText(sourceFile)
2625
+ };
2626
+ }
2627
+ return;
2628
+ }
2629
+ }
2630
+ }
2631
+ }
2632
+ }
2633
+ ts5.forEachChild(node, visit);
2634
+ }
2635
+ visit(sourceFile);
2636
+ return result;
2637
+ }
2638
+ function extractBasenameFromRouter(code) {
2639
+ const sourceFile = ts5.createSourceFile(
2640
+ "router.tsx",
2641
+ code,
2642
+ ts5.ScriptTarget.Latest,
2643
+ true,
2644
+ ts5.ScriptKind.TSX
2645
+ );
2646
+ let result;
2647
+ function visit(node) {
2648
+ if (result !== void 0) return;
2649
+ if (ts5.isCallExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "createRouter") {
2650
+ for (const arg of node.arguments) {
2484
2651
  if (ts5.isObjectLiteralExpression(arg)) {
2485
2652
  for (const prop of arg.properties) {
2486
- if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls" && ts5.isIdentifier(prop.initializer)) {
2653
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "basename" && ts5.isStringLiteral(prop.initializer)) {
2487
2654
  result = prop.initializer.text;
2488
2655
  return;
2489
2656
  }
@@ -2496,6 +2663,19 @@ function extractUrlsVariableFromRouter(code) {
2496
2663
  visit(sourceFile);
2497
2664
  return result;
2498
2665
  }
2666
+ function applyBasenameToRoutes(result, basename3) {
2667
+ const prefixed = {};
2668
+ for (const [name, pattern] of Object.entries(result.routes)) {
2669
+ if (pattern === "/") {
2670
+ prefixed[name] = basename3;
2671
+ } else if (basename3.endsWith("/") && pattern.startsWith("/")) {
2672
+ prefixed[name] = basename3 + pattern.slice(1);
2673
+ } else {
2674
+ prefixed[name] = basename3 + pattern;
2675
+ }
2676
+ }
2677
+ return { routes: prefixed, searchSchemas: result.searchSchemas };
2678
+ }
2499
2679
  function buildCombinedRouteMapForRouterFile(routerFilePath) {
2500
2680
  let routerSource;
2501
2681
  try {
@@ -2503,19 +2683,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
2503
2683
  } catch {
2504
2684
  return { routes: {}, searchSchemas: {} };
2505
2685
  }
2506
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
2507
- if (!urlsVarName) {
2686
+ const extraction = extractUrlsFromRouter(routerSource);
2687
+ if (!extraction) {
2508
2688
  return { routes: {}, searchSchemas: {} };
2509
2689
  }
2510
- const imported = resolveImportedVariable(routerSource, urlsVarName);
2511
- if (imported) {
2512
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2513
- if (!targetFile) {
2514
- return { routes: {}, searchSchemas: {} };
2690
+ const rawBasename = extractBasenameFromRouter(routerSource);
2691
+ const basename3 = rawBasename ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "") : void 0;
2692
+ let result;
2693
+ if (extraction.kind === "inline") {
2694
+ result = buildCombinedRouteMapWithSearch(
2695
+ routerFilePath,
2696
+ void 0,
2697
+ void 0,
2698
+ void 0,
2699
+ extraction.block
2700
+ );
2701
+ } else {
2702
+ const imported = resolveImportedVariable(routerSource, extraction.name);
2703
+ if (imported) {
2704
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2705
+ if (!targetFile) {
2706
+ return { routes: {}, searchSchemas: {} };
2707
+ }
2708
+ result = buildCombinedRouteMapWithSearch(
2709
+ targetFile,
2710
+ imported.exportedName
2711
+ );
2712
+ } else {
2713
+ result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
2515
2714
  }
2516
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
2517
2715
  }
2518
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
2716
+ if (basename3) {
2717
+ result = applyBasenameToRoutes(result, basename3);
2718
+ }
2719
+ return result;
2519
2720
  }
2520
2721
  function findRouterFiles(root, filter) {
2521
2722
  const result = [];
@@ -2540,25 +2741,15 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
2540
2741
  throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
2541
2742
  }
2542
2743
  for (const routerFilePath of routerFilePaths) {
2543
- let routerSource;
2544
- try {
2545
- routerSource = readFileSync2(routerFilePath, "utf-8");
2546
- } catch {
2547
- continue;
2548
- }
2549
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
2550
- if (!urlsVarName) continue;
2551
- let result;
2552
- const imported = resolveImportedVariable(routerSource, urlsVarName);
2553
- if (imported) {
2554
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2555
- if (!targetFile) continue;
2556
- result = buildCombinedRouteMapWithSearch(
2557
- targetFile,
2558
- imported.exportedName
2559
- );
2560
- } else {
2561
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
2744
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
2745
+ if (Object.keys(result.routes).length === 0 && Object.keys(result.searchSchemas).length === 0) {
2746
+ let routerSource;
2747
+ try {
2748
+ routerSource = readFileSync2(routerFilePath, "utf-8");
2749
+ } catch {
2750
+ continue;
2751
+ }
2752
+ if (!extractUrlsFromRouter(routerSource)) continue;
2562
2753
  }
2563
2754
  const routerBasename = pathBasename(routerFilePath).replace(
2564
2755
  /\.(tsx?|jsx?)$/,
@@ -2752,6 +2943,9 @@ function createVersionPlugin() {
2752
2943
  if (!isDev) return;
2753
2944
  const isRscModule = this.environment?.name === "rsc";
2754
2945
  if (!isRscModule) return;
2946
+ if (ctx.modules.length === 1 && ctx.modules[0].id === "\0" + VIRTUAL_IDS.version) {
2947
+ return;
2948
+ }
2755
2949
  if (isCodeModule(ctx.file)) {
2756
2950
  const filePath = normalizeModuleId(ctx.file);
2757
2951
  const previousSignature = clientModuleSignatures.get(filePath);
@@ -2781,6 +2975,68 @@ function createVersionPlugin() {
2781
2975
 
2782
2976
  // src/vite/utils/shared-utils.ts
2783
2977
  import * as Vite from "vite";
2978
+
2979
+ // src/vite/plugins/performance-tracks.ts
2980
+ import { readFile } from "node:fs/promises";
2981
+ var RSDW_PATCH_RE = /((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
2982
+ function buildPatchReplacement(match, debugInfoVar) {
2983
+ return `${match}
2984
+ if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.status) {
2985
+ var _resolved = "function" === typeof resolveLazy ? resolveLazy(root.value) : root.value;
2986
+ if ("object" === typeof _resolved && null !== _resolved && isArrayImpl(_resolved._debugInfo)) {
2987
+ ${debugInfoVar} = _resolved._debugInfo;
2988
+ }
2989
+ }`;
2990
+ }
2991
+ function patchRsdwClientDebugInfoRecovery(code) {
2992
+ const match = code.match(RSDW_PATCH_RE);
2993
+ if (!match) {
2994
+ return { code, debugInfoVar: null };
2995
+ }
2996
+ return {
2997
+ code: code.replace(match[1], buildPatchReplacement(match[1], match[2])),
2998
+ debugInfoVar: match[2]
2999
+ };
3000
+ }
3001
+ function performanceTracksOptimizeDepsPlugin() {
3002
+ return {
3003
+ name: "@rangojs/router:performance-tracks-optimize-deps",
3004
+ setup(build) {
3005
+ build.onLoad(
3006
+ {
3007
+ filter: /react-server-dom-webpack-client\.browser\.(development|production)\.js$/
3008
+ },
3009
+ async (args) => {
3010
+ const code = await readFile(args.path, "utf8");
3011
+ const patched = patchRsdwClientDebugInfoRecovery(code);
3012
+ return {
3013
+ contents: patched.code,
3014
+ loader: "js"
3015
+ };
3016
+ }
3017
+ );
3018
+ }
3019
+ };
3020
+ }
3021
+ function performanceTracksPlugin() {
3022
+ return {
3023
+ name: "@rangojs/router:performance-tracks",
3024
+ transform(code, id) {
3025
+ if (!id.includes("react-server-dom") || !id.includes("client")) return;
3026
+ const patched = patchRsdwClientDebugInfoRecovery(code);
3027
+ if (!patched.debugInfoVar) return;
3028
+ if (process.env.INTERNAL_RANGO_DEBUG)
3029
+ console.log(
3030
+ "[perf-tracks] patched RSDW client (var:",
3031
+ patched.debugInfoVar,
3032
+ ")"
3033
+ );
3034
+ return patched.code;
3035
+ }
3036
+ };
3037
+ }
3038
+
3039
+ // src/vite/utils/shared-utils.ts
2784
3040
  var versionEsbuildPlugin = {
2785
3041
  name: "@rangojs/router-version",
2786
3042
  setup(build) {
@@ -2798,7 +3054,7 @@ var versionEsbuildPlugin = {
2798
3054
  }
2799
3055
  };
2800
3056
  var sharedEsbuildOptions = {
2801
- plugins: [versionEsbuildPlugin]
3057
+ plugins: [versionEsbuildPlugin, performanceTracksOptimizeDepsPlugin()]
2802
3058
  };
2803
3059
  function createVirtualEntriesPlugin(entries, routerPathRef) {
2804
3060
  const virtualModules = {};
@@ -2881,11 +3137,11 @@ ${dim} \u2571${reset} ${bold}\u2554\u2550\u2557${reset}${dim} * \u2
2881
3137
  ${dim} ${reset}${bold}\u2551 \u2551${reset} ${bold}\u2554\u2550\u2557${reset}${dim} * \u2727. \u2571${reset}
2882
3138
  ${dim} ${reset}${bold}\u2554\u2557 \u2551 \u2551 \u2551 \u2551${reset}${dim} * \u2571${reset}
2883
3139
  ${dim} ${reset}${bold}\u2551\u2551 \u2551 \u2551 \u2551 \u2551 \u2566\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557\u2554\u2550\u2557${reset}${dim} \u2727 \u2726${reset}
2884
- ${dim} ${reset}${bold}\u2550\u2563\u2551 \u2551 \u2560\u2550\u255D \u2551 \u2560\u2566\u255D\u2560\u2550\u2563\u2551\u2551\u2551\u2551 \u2566\u2551 \u2551${reset}${dim} * \u2727${reset}
3140
+ ${dim} ${reset}${bold}\u2551\u2551 \u2551 \u2560\u2550\u255D \u2551 \u2560\u2566\u255D\u2560\u2550\u2563\u2551\u2551\u2551\u2551 \u2566\u2551 \u2551${reset}${dim} * \u2727${reset}
2885
3141
  ${dim} ${reset}${bold}\u2551\u255A\u2550\u255D \u2554\u2550\u2550\u2550\u255D \u2569\u255A\u2550\u2569 \u2569\u255D\u255A\u255D\u255A\u2550\u255D\u255A\u2550\u255D${reset}${dim} \u2726 . *${reset}
2886
3142
  ${dim} ${reset}${bold}\u255A\u2550\u2550\u2557 \u2551${reset}${dim} * RSC Wrangler \u2727 \u2726${reset}
2887
- ${dim} * ${reset}${bold}\u2551 \u2560\u2550${reset}${dim} * \u2727. \u2571${reset}
2888
- ${bold}\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2569\u2550\u2550\u2550${reset}${dim} \u2726 *${reset}
3143
+ ${dim} * ${reset}${bold}\u2551 \u2551${reset}${dim} * \u2727. \u2571${reset}
3144
+ ${dim} ${reset}${bold}\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550${reset}${dim} \u2726 *${reset}
2889
3145
 
2890
3146
  v${version} \xB7 ${preset} \xB7 ${mode}
2891
3147
  `;
@@ -3004,6 +3260,8 @@ function createCjsToEsmPlugin() {
3004
3260
  import { createServer as createViteServer } from "vite";
3005
3261
  import { resolve as resolve8 } from "node:path";
3006
3262
  import { readFileSync as readFileSync6 } from "node:fs";
3263
+ import { createRequire } from "node:module";
3264
+ import { pathToFileURL } from "node:url";
3007
3265
 
3008
3266
  // src/vite/plugins/virtual-stub-plugin.ts
3009
3267
  function createVirtualStubPlugin() {
@@ -3187,8 +3445,8 @@ function createDiscoveryState(entryPath, opts) {
3187
3445
  perRouterManifestDataMap: /* @__PURE__ */ new Map(),
3188
3446
  prerenderManifestEntries: null,
3189
3447
  staticManifestEntries: null,
3190
- handlerChunkInfo: null,
3191
- staticHandlerChunkInfo: null,
3448
+ handlerChunkInfoMap: /* @__PURE__ */ new Map(),
3449
+ staticHandlerChunkInfoMap: /* @__PURE__ */ new Map(),
3192
3450
  rscEntryFileName: null,
3193
3451
  resolvedPrerenderModules: void 0,
3194
3452
  resolvedStaticModules: void 0,
@@ -3271,8 +3529,17 @@ function jsonParseExpression(value) {
3271
3529
  }
3272
3530
 
3273
3531
  // src/context-var.ts
3532
+ var NON_CACHEABLE_KEYS = /* @__PURE__ */ Symbol.for(
3533
+ "rango:non-cacheable-keys"
3534
+ );
3535
+ function getNonCacheableKeys(variables) {
3536
+ if (!variables[NON_CACHEABLE_KEYS]) {
3537
+ variables[NON_CACHEABLE_KEYS] = /* @__PURE__ */ new Set();
3538
+ }
3539
+ return variables[NON_CACHEABLE_KEYS];
3540
+ }
3274
3541
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3275
- function contextSet(variables, keyOrVar, value) {
3542
+ function contextSet(variables, keyOrVar, value, options) {
3276
3543
  if (typeof keyOrVar === "string") {
3277
3544
  if (FORBIDDEN_KEYS.has(keyOrVar)) {
3278
3545
  throw new Error(
@@ -3280,8 +3547,14 @@ function contextSet(variables, keyOrVar, value) {
3280
3547
  );
3281
3548
  }
3282
3549
  variables[keyOrVar] = value;
3550
+ if (options?.cache === false) {
3551
+ getNonCacheableKeys(variables).add(keyOrVar);
3552
+ }
3283
3553
  } else {
3284
3554
  variables[keyOrVar.key] = value;
3555
+ if (options?.cache === false) {
3556
+ getNonCacheableKeys(variables).add(keyOrVar.key);
3557
+ }
3285
3558
  }
3286
3559
  }
3287
3560
 
@@ -3304,6 +3577,7 @@ function encodePathParam(value) {
3304
3577
  }
3305
3578
  function substituteRouteParams(pattern, params, encode = encodeURIComponent) {
3306
3579
  let result = pattern;
3580
+ let hadOmittedOptional = false;
3307
3581
  for (const [key, value] of Object.entries(params)) {
3308
3582
  const escaped = escapeRegExp2(key);
3309
3583
  result = result.replace(
@@ -3312,6 +3586,15 @@ function substituteRouteParams(pattern, params, encode = encodeURIComponent) {
3312
3586
  );
3313
3587
  result = result.replace(`*${key}`, encode(value));
3314
3588
  }
3589
+ result = result.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\?/g, () => {
3590
+ hadOmittedOptional = true;
3591
+ return "";
3592
+ });
3593
+ if (hadOmittedOptional) {
3594
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
3595
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
3596
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
3597
+ }
3315
3598
  return result;
3316
3599
  }
3317
3600
  async function runWithConcurrency(items, concurrency, fn) {
@@ -3420,84 +3703,126 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3420
3703
  if (!params) return pattern;
3421
3704
  return substituteRouteParams(pattern, params);
3422
3705
  };
3706
+ let resolvedRoutes = 0;
3707
+ let totalDynamic = 0;
3423
3708
  for (const { manifest } of allManifests) {
3424
3709
  if (!manifest.prerenderRoutes) continue;
3425
- const defs = manifest._prerenderDefs || {};
3426
3710
  for (const routeName of manifest.prerenderRoutes) {
3427
3711
  const pattern = manifest.routeManifest[routeName];
3428
- if (!pattern) continue;
3429
- const def = defs[routeName];
3430
- const isPassthroughRoute = !!def?.options?.passthrough;
3431
- const hasDynamic = pattern.includes(":") || pattern.includes("*");
3432
- if (!hasDynamic) {
3433
- entries.push({
3434
- urlPath: pattern.replace(/\/$/, "") || "/",
3435
- routeName,
3436
- concurrency: 1,
3437
- isPassthroughRoute
3438
- });
3439
- } else {
3440
- if (def?.getParams) {
3441
- try {
3442
- const buildVars = {};
3443
- const getParamsCtx = {
3444
- build: true,
3445
- set: ((keyOrVar, value) => {
3446
- contextSet(buildVars, keyOrVar, value);
3447
- }),
3448
- reverse: getParamsReverse
3449
- };
3450
- const paramsList = await def.getParams(getParamsCtx);
3451
- const concurrency = def.options?.concurrency ?? 1;
3452
- const hasBuildVars = Object.keys(buildVars).length > 0 || Object.getOwnPropertySymbols(buildVars).length > 0;
3453
- for (const params of paramsList) {
3454
- let url = substituteRouteParams(
3455
- pattern,
3456
- params,
3457
- encodePathParam
3458
- );
3459
- if (url.includes("*")) {
3460
- const wildcardValue = params["*"] ?? params.splat;
3461
- if (wildcardValue !== void 0) {
3462
- url = url.replace(/\*[^/]*$/, encodePathParam(wildcardValue));
3712
+ if (pattern && (pattern.includes(":") || pattern.includes("*"))) {
3713
+ totalDynamic++;
3714
+ }
3715
+ }
3716
+ }
3717
+ const paramsStart = performance.now();
3718
+ const progressInterval = totalDynamic > 0 ? setInterval(() => {
3719
+ const elapsed = ((performance.now() - paramsStart) / 1e3).toFixed(1);
3720
+ console.log(
3721
+ `[rsc-router] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`
3722
+ );
3723
+ }, 5e3) : void 0;
3724
+ try {
3725
+ for (const { manifest } of allManifests) {
3726
+ if (!manifest.prerenderRoutes) continue;
3727
+ const defs = manifest._prerenderDefs || {};
3728
+ const passthroughSet = new Set(manifest.passthroughRoutes || []);
3729
+ for (const routeName of manifest.prerenderRoutes) {
3730
+ const pattern = manifest.routeManifest[routeName];
3731
+ if (!pattern) continue;
3732
+ const def = defs[routeName];
3733
+ const isPassthroughRoute = passthroughSet.has(routeName);
3734
+ const hasDynamic = pattern.includes(":") || pattern.includes("*");
3735
+ if (!hasDynamic) {
3736
+ entries.push({
3737
+ urlPath: pattern.replace(/\/$/, "") || "/",
3738
+ routeName,
3739
+ concurrency: 1,
3740
+ isPassthroughRoute
3741
+ });
3742
+ } else {
3743
+ if (def?.getParams) {
3744
+ try {
3745
+ const buildVars = {};
3746
+ const buildEnv = state.resolvedBuildEnv;
3747
+ const getParamsCtx = {
3748
+ build: true,
3749
+ dev: !state.isBuildMode,
3750
+ set: ((keyOrVar, value) => {
3751
+ contextSet(buildVars, keyOrVar, value);
3752
+ }),
3753
+ reverse: getParamsReverse,
3754
+ get env() {
3755
+ if (buildEnv !== void 0) return buildEnv;
3756
+ throw new Error(
3757
+ "[rsc-router] ctx.env is not available during build-time getParams(). Configure buildEnv in your rango() plugin options to enable build-time env access."
3758
+ );
3463
3759
  }
3760
+ };
3761
+ const paramsList = await def.getParams(getParamsCtx);
3762
+ const concurrency = def.options?.concurrency ?? 1;
3763
+ const hasBuildVars = Object.keys(buildVars).length > 0 || Object.getOwnPropertySymbols(buildVars).length > 0;
3764
+ for (const params of paramsList) {
3765
+ let url = substituteRouteParams(
3766
+ pattern,
3767
+ params,
3768
+ encodePathParam
3769
+ );
3770
+ if (url.includes("*")) {
3771
+ const wildcardValue = params["*"] ?? params.splat;
3772
+ if (wildcardValue !== void 0) {
3773
+ url = url.replace(
3774
+ /\*[^/]*$/,
3775
+ encodePathParam(wildcardValue)
3776
+ );
3777
+ }
3778
+ }
3779
+ entries.push({
3780
+ urlPath: url.replace(/\/$/, "") || "/",
3781
+ routeName,
3782
+ concurrency,
3783
+ ...hasBuildVars ? { buildVars } : {},
3784
+ isPassthroughRoute
3785
+ });
3464
3786
  }
3465
- entries.push({
3466
- urlPath: url.replace(/\/$/, "") || "/",
3467
- routeName,
3468
- concurrency,
3469
- ...hasBuildVars ? { buildVars } : {},
3470
- isPassthroughRoute
3471
- });
3472
- }
3473
- } catch (err) {
3474
- if (err.name === "Skip") {
3475
- console.log(
3476
- `[rsc-router] SKIP route "${routeName}" - ${err.message}`
3477
- );
3478
- notifyOnError(
3479
- registry,
3480
- err,
3481
- "prerender",
3482
- routeName,
3483
- void 0,
3484
- true
3787
+ resolvedRoutes++;
3788
+ } catch (err) {
3789
+ resolvedRoutes++;
3790
+ if (err.name === "Skip") {
3791
+ console.log(
3792
+ `[rsc-router] SKIP route "${routeName}" - ${err.message}`
3793
+ );
3794
+ notifyOnError(
3795
+ registry,
3796
+ err,
3797
+ "prerender",
3798
+ routeName,
3799
+ void 0,
3800
+ true
3801
+ );
3802
+ continue;
3803
+ }
3804
+ console.error(
3805
+ `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`
3485
3806
  );
3486
- continue;
3807
+ notifyOnError(registry, err, "prerender", routeName);
3808
+ throw err;
3487
3809
  }
3488
- console.error(
3489
- `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`
3810
+ } else {
3811
+ console.warn(
3812
+ `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`
3490
3813
  );
3491
- notifyOnError(registry, err, "prerender", routeName);
3492
- throw err;
3493
3814
  }
3494
- } else {
3495
- console.warn(
3496
- `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`
3497
- );
3498
3815
  }
3499
3816
  }
3500
3817
  }
3818
+ } finally {
3819
+ if (progressInterval) {
3820
+ clearInterval(progressInterval);
3821
+ const elapsed = ((performance.now() - paramsStart) / 1e3).toFixed(1);
3822
+ console.log(
3823
+ `[rsc-router] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`
3824
+ );
3825
+ }
3501
3826
  }
3502
3827
  if (entries.length === 0) return;
3503
3828
  const maxConcurrency = Math.max(...entries.map((e) => e.concurrency));
@@ -3524,7 +3849,8 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3524
3849
  entry.urlPath,
3525
3850
  {},
3526
3851
  entry.buildVars,
3527
- entry.isPassthroughRoute
3852
+ entry.isPassthroughRoute,
3853
+ state.resolvedBuildEnv
3528
3854
  );
3529
3855
  if (!result) continue;
3530
3856
  if (result.passthrough) {
@@ -3648,7 +3974,9 @@ async function renderStaticHandlers(state, rscEnv, registry) {
3648
3974
  const result = await routerInstance.renderStaticSegment(
3649
3975
  def.handler,
3650
3976
  def.$$id,
3651
- def.$$routePrefix
3977
+ def.$$routePrefix,
3978
+ state.resolvedBuildEnv,
3979
+ !state.isBuildMode
3652
3980
  );
3653
3981
  if (result) {
3654
3982
  const hasHandles = Object.keys(result.handles).length > 0;
@@ -3773,7 +4101,11 @@ async function discoverRouters(state, rscEnv) {
3773
4101
  if (!router.urlpatterns || !generateManifestFull) {
3774
4102
  continue;
3775
4103
  }
3776
- const manifest = generateManifestFull(router.urlpatterns, routerMountIndex);
4104
+ const manifest = generateManifestFull(
4105
+ router.urlpatterns,
4106
+ routerMountIndex,
4107
+ router.__basename ? { urlPrefix: router.__basename } : void 0
4108
+ );
3777
4109
  routerMountIndex++;
3778
4110
  allManifests.push({ id, manifest });
3779
4111
  const routeCount = Object.keys(manifest.routeManifest).length;
@@ -4222,48 +4554,45 @@ function postprocessBundle(state) {
4222
4554
  );
4223
4555
  const evictionTargets = [
4224
4556
  {
4225
- info: state.handlerChunkInfo,
4557
+ infos: state.handlerChunkInfoMap.values(),
4226
4558
  fnName: "Prerender",
4227
4559
  brand: "prerenderHandler",
4228
4560
  label: "handler code from RSC bundle"
4229
4561
  },
4230
4562
  {
4231
- info: state.staticHandlerChunkInfo,
4563
+ infos: state.staticHandlerChunkInfoMap.values(),
4232
4564
  fnName: "Static",
4233
4565
  brand: "staticHandler",
4234
4566
  label: "static handler code"
4235
4567
  }
4236
4568
  ];
4237
4569
  for (const target of evictionTargets) {
4238
- if (!target.info) continue;
4239
- const chunkPath = resolve7(
4240
- state.projectRoot,
4241
- "dist/rsc",
4242
- target.info.fileName
4243
- );
4244
- try {
4245
- const code = readFileSync5(chunkPath, "utf-8");
4246
- const result = evictHandlerCode(
4247
- code,
4248
- target.info.exports,
4249
- target.fnName,
4250
- target.brand
4251
- );
4252
- if (result) {
4253
- writeFileSync4(chunkPath, result.code);
4254
- const savedKB = (result.savedBytes / 1024).toFixed(1);
4255
- console.log(
4256
- `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${target.info.fileName}`
4570
+ for (const info of target.infos) {
4571
+ const chunkPath = resolve7(state.projectRoot, "dist/rsc", info.fileName);
4572
+ try {
4573
+ const code = readFileSync5(chunkPath, "utf-8");
4574
+ const result = evictHandlerCode(
4575
+ code,
4576
+ info.exports,
4577
+ target.fnName,
4578
+ target.brand
4579
+ );
4580
+ if (result) {
4581
+ writeFileSync4(chunkPath, result.code);
4582
+ const savedKB = (result.savedBytes / 1024).toFixed(1);
4583
+ console.log(
4584
+ `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${info.fileName}`
4585
+ );
4586
+ }
4587
+ } catch (replaceErr) {
4588
+ console.warn(
4589
+ `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`
4257
4590
  );
4258
4591
  }
4259
- } catch (replaceErr) {
4260
- console.warn(
4261
- `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`
4262
- );
4263
4592
  }
4264
4593
  }
4265
- state.handlerChunkInfo = null;
4266
- state.staticHandlerChunkInfo = null;
4594
+ state.handlerChunkInfoMap.clear();
4595
+ state.staticHandlerChunkInfoMap.clear();
4267
4596
  if (hasPrerenderData && existsSync6(rscEntryPath)) {
4268
4597
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
4269
4598
  if (!rscCode.includes("__prerender-manifest.js")) {
@@ -4306,7 +4635,7 @@ function postprocessBundle(state) {
4306
4635
  }
4307
4636
  if (hasStaticData && existsSync6(rscEntryPath)) {
4308
4637
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
4309
- if (!rscCode.includes("__STATIC_MANIFEST")) {
4638
+ if (!rscCode.includes("__static-manifest.js")) {
4310
4639
  try {
4311
4640
  const manifestEntries = [];
4312
4641
  let totalBytes = copyStagedBuildAssets(
@@ -4375,8 +4704,67 @@ async function createTempRscServer(state, options = {}) {
4375
4704
  ]
4376
4705
  });
4377
4706
  }
4707
+ async function resolveBuildEnv(option, factoryCtx) {
4708
+ if (!option) return null;
4709
+ if (option === "auto") {
4710
+ if (factoryCtx.preset !== "cloudflare") {
4711
+ throw new Error(
4712
+ '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". Use a factory function or plain object for other presets.'
4713
+ );
4714
+ }
4715
+ try {
4716
+ const userRequire = createRequire(
4717
+ resolve8(factoryCtx.root, "package.json")
4718
+ );
4719
+ const wranglerPath = userRequire.resolve("wrangler");
4720
+ const { getPlatformProxy } = await import(pathToFileURL(wranglerPath).href);
4721
+ const proxy = await getPlatformProxy();
4722
+ return {
4723
+ env: proxy.env,
4724
+ dispose: proxy.dispose
4725
+ };
4726
+ } catch (err) {
4727
+ throw new Error(
4728
+ `[rsc-router] buildEnv: "auto" requires wrangler to be installed.
4729
+ Install it with: pnpm add -D wrangler
4730
+ ${err.message}`
4731
+ );
4732
+ }
4733
+ }
4734
+ if (typeof option === "function") {
4735
+ return await option(factoryCtx);
4736
+ }
4737
+ return { env: option };
4738
+ }
4739
+ async function acquireBuildEnv(s, command, mode) {
4740
+ const option = s.opts?.buildEnv;
4741
+ if (!option) return false;
4742
+ const result = await resolveBuildEnv(option, {
4743
+ root: s.projectRoot,
4744
+ mode,
4745
+ command,
4746
+ preset: s.opts?.preset ?? "node"
4747
+ });
4748
+ if (!result) return false;
4749
+ s.resolvedBuildEnv = result.env;
4750
+ s.buildEnvDispose = result.dispose ?? null;
4751
+ return true;
4752
+ }
4753
+ async function releaseBuildEnv(s) {
4754
+ if (s.buildEnvDispose) {
4755
+ try {
4756
+ await s.buildEnvDispose();
4757
+ } catch (err) {
4758
+ console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
4759
+ }
4760
+ s.buildEnvDispose = null;
4761
+ }
4762
+ s.resolvedBuildEnv = void 0;
4763
+ }
4378
4764
  function createRouterDiscoveryPlugin(entryPath, opts) {
4379
4765
  const s = createDiscoveryState(entryPath, opts);
4766
+ let viteCommand = "build";
4767
+ let viteMode = "production";
4380
4768
  return {
4381
4769
  name: "@rangojs/router:discovery",
4382
4770
  config() {
@@ -4385,31 +4773,13 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4385
4773
  __RANGO_DEBUG__: JSON.stringify(!!process.env.INTERNAL_RANGO_DEBUG)
4386
4774
  }
4387
4775
  };
4388
- if (opts?.enableBuildPrerender) {
4389
- config.environments = {
4390
- rsc: {
4391
- build: {
4392
- rollupOptions: {
4393
- output: {
4394
- manualChunks(id) {
4395
- if (s.resolvedPrerenderModules?.has(id)) {
4396
- return "__prerender-handlers";
4397
- }
4398
- if (s.resolvedStaticModules?.has(id)) {
4399
- return "__static-handlers";
4400
- }
4401
- }
4402
- }
4403
- }
4404
- }
4405
- }
4406
- };
4407
- }
4408
4776
  return config;
4409
4777
  },
4410
4778
  configResolved(config) {
4411
4779
  s.projectRoot = config.root;
4412
4780
  s.isBuildMode = config.command === "build";
4781
+ viteCommand = config.command;
4782
+ viteMode = config.mode;
4413
4783
  s.userResolveAlias = config.resolve.alias;
4414
4784
  if (!s.resolvedEntryPath && opts?.routerPathRef?.path) {
4415
4785
  s.resolvedEntryPath = opts.routerPathRef.path;
@@ -4454,6 +4824,8 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4454
4824
  });
4455
4825
  prerenderTempServer = null;
4456
4826
  }
4827
+ releaseBuildEnv(s).catch(() => {
4828
+ });
4457
4829
  });
4458
4830
  async function getOrCreateTempServer() {
4459
4831
  if (prerenderNodeRegistry) {
@@ -4484,6 +4856,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4484
4856
  if (!rscEnv?.runner) {
4485
4857
  s.devServerOrigin = getDevServerOrigin();
4486
4858
  try {
4859
+ await acquireBuildEnv(s, viteCommand, viteMode);
4487
4860
  const tempRscEnv = await getOrCreateTempServer();
4488
4861
  if (tempRscEnv) {
4489
4862
  await discoverRouters(s, tempRscEnv);
@@ -4499,6 +4872,7 @@ ${err.stack}`
4499
4872
  return;
4500
4873
  }
4501
4874
  try {
4875
+ await acquireBuildEnv(s, viteCommand, viteMode);
4502
4876
  const serverMod = await rscEnv.runner.import(
4503
4877
  "@rangojs/router/server"
4504
4878
  );
@@ -4563,7 +4937,26 @@ ${err.stack}`
4563
4937
  res.end("Missing pathname");
4564
4938
  return;
4565
4939
  }
4566
- let registry = mainRegistry;
4940
+ const rscEnv = server.environments?.rsc;
4941
+ let registry = null;
4942
+ if (rscEnv?.runner && s.resolvedEntryPath) {
4943
+ try {
4944
+ await rscEnv.runner.import(s.resolvedEntryPath);
4945
+ const serverMod = await rscEnv.runner.import(
4946
+ "@rangojs/router/server"
4947
+ );
4948
+ registry = serverMod.RouterRegistry ?? null;
4949
+ } catch (err) {
4950
+ console.warn(
4951
+ `[rsc-router] Dev prerender module refresh failed: ${err.message}`
4952
+ );
4953
+ res.statusCode = 500;
4954
+ res.end(`Prerender handler error: ${err.message}`);
4955
+ return;
4956
+ }
4957
+ } else {
4958
+ registry = mainRegistry;
4959
+ }
4567
4960
  if (!registry) {
4568
4961
  if (!prerenderNodeRegistry) {
4569
4962
  await getOrCreateTempServer();
@@ -4585,7 +4978,10 @@ ${err.stack}`
4585
4978
  pathname,
4586
4979
  {},
4587
4980
  void 0,
4588
- wantPassthrough
4981
+ wantPassthrough,
4982
+ s.resolvedBuildEnv,
4983
+ true
4984
+ // devMode: check getParams for passthrough routes
4589
4985
  );
4590
4986
  if (!result) continue;
4591
4987
  if (result.passthrough) continue;
@@ -4721,6 +5117,7 @@ ${err.stack}`
4721
5117
  resetStagedBuildAssets(s.projectRoot);
4722
5118
  s.prerenderManifestEntries = null;
4723
5119
  s.staticManifestEntries = null;
5120
+ await acquireBuildEnv(s, viteCommand, viteMode);
4724
5121
  let tempServer = null;
4725
5122
  globalThis.__rscRouterDiscoveryActive = true;
4726
5123
  try {
@@ -4760,6 +5157,7 @@ ${details}`
4760
5157
  if (tempServer) {
4761
5158
  await tempServer.close();
4762
5159
  }
5160
+ await releaseBuildEnv(s);
4763
5161
  }
4764
5162
  },
4765
5163
  // Virtual module: provides the pre-generated route manifest as a JS module
@@ -4802,20 +5200,30 @@ ${details}`
4802
5200
  }
4803
5201
  if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size)
4804
5202
  return;
5203
+ s.handlerChunkInfoMap.clear();
5204
+ s.staticHandlerChunkInfoMap.clear();
4805
5205
  for (const [fileName, chunk] of Object.entries(bundle)) {
4806
5206
  if (chunk.type !== "chunk") continue;
4807
- if (fileName.includes("__prerender-handlers") && s.resolvedPrerenderModules?.size) {
5207
+ if (s.resolvedPrerenderModules?.size) {
4808
5208
  const handlers = extractHandlerExportsFromChunk(
4809
5209
  chunk.code,
4810
5210
  s.resolvedPrerenderModules,
4811
5211
  "Prerender",
4812
- true
5212
+ false
4813
5213
  );
4814
5214
  if (handlers.length > 0) {
4815
- s.handlerChunkInfo = { fileName, exports: handlers };
5215
+ const existing = s.handlerChunkInfoMap.get(fileName);
5216
+ if (existing) {
5217
+ existing.exports.push(...handlers);
5218
+ } else {
5219
+ s.handlerChunkInfoMap.set(fileName, {
5220
+ fileName,
5221
+ exports: handlers
5222
+ });
5223
+ }
4816
5224
  }
4817
5225
  }
4818
- if (fileName.includes("__static-handlers") && s.resolvedStaticModules?.size) {
5226
+ if (s.resolvedStaticModules?.size) {
4819
5227
  const handlers = extractHandlerExportsFromChunk(
4820
5228
  chunk.code,
4821
5229
  s.resolvedStaticModules,
@@ -4823,7 +5231,15 @@ ${details}`
4823
5231
  false
4824
5232
  );
4825
5233
  if (handlers.length > 0) {
4826
- s.staticHandlerChunkInfo = { fileName, exports: handlers };
5234
+ const existing = s.staticHandlerChunkInfoMap.get(fileName);
5235
+ if (existing) {
5236
+ existing.exports.push(...handlers);
5237
+ } else {
5238
+ s.staticHandlerChunkInfoMap.set(fileName, {
5239
+ fileName,
5240
+ exports: handlers
5241
+ });
5242
+ }
4827
5243
  }
4828
5244
  }
4829
5245
  }
@@ -4850,7 +5266,16 @@ async function rango(options) {
4850
5266
  const showBanner = resolvedOptions.banner ?? true;
4851
5267
  const plugins = [];
4852
5268
  const rangoAliases = getPackageAliases();
4853
- const excludeDeps = getExcludeDeps();
5269
+ const excludeDeps = [
5270
+ ...getExcludeDeps(),
5271
+ // The public browser entry re-exports the RSDW browser client.
5272
+ // Excluding both keeps Vite from freezing the unpatched bundle into
5273
+ // .vite/deps before our source transforms run.
5274
+ "@vitejs/plugin-rsc/browser",
5275
+ // Keep the browser RSDW client out of Vite's dep optimizer so our
5276
+ // cjs-to-esm transform can patch the real file.
5277
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.browser"
5278
+ ];
4854
5279
  const routerRef = { path: void 0 };
4855
5280
  const prerenderEnabled = true;
4856
5281
  if (preset === "cloudflare") {
@@ -4946,6 +5371,7 @@ async function rango(options) {
4946
5371
  }
4947
5372
  });
4948
5373
  plugins.push(createVirtualEntriesPlugin(finalEntries));
5374
+ plugins.push(performanceTracksPlugin());
4949
5375
  plugins.push(
4950
5376
  rsc({
4951
5377
  entries: finalEntries,
@@ -5064,6 +5490,7 @@ ${list}`);
5064
5490
  }
5065
5491
  });
5066
5492
  plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
5493
+ plugins.push(performanceTracksPlugin());
5067
5494
  plugins.push(
5068
5495
  rsc({
5069
5496
  entries: finalEntries
@@ -5104,7 +5531,8 @@ ${list}`);
5104
5531
  createRouterDiscoveryPlugin(discoveryEntryPath, {
5105
5532
  routerPathRef: discoveryRouterRef,
5106
5533
  enableBuildPrerender: prerenderEnabled,
5107
- staticRouteTypesGeneration: resolvedOptions.staticRouteTypesGeneration
5534
+ buildEnv: options?.buildEnv,
5535
+ preset
5108
5536
  })
5109
5537
  );
5110
5538
  return plugins;
@@ -5117,29 +5545,75 @@ function poke() {
5117
5545
  apply: "serve",
5118
5546
  configureServer(server) {
5119
5547
  const stdin = process.stdin;
5120
- const previousRawMode = stdin.isTTY ? stdin.isRaw : null;
5548
+ const debug = process.env.RANGO_POKE_DEBUG === "1";
5549
+ const triggerReload = (source) => {
5550
+ server.hot.send({ type: "full-reload", path: "*" });
5551
+ server.config.logger.info(` browser reload (${source})`, {
5552
+ timestamp: true
5553
+ });
5554
+ };
5555
+ const toBuffer = (chunk) => {
5556
+ return typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
5557
+ };
5558
+ const formatChunk = (chunk) => {
5559
+ const data = toBuffer(chunk);
5560
+ const hex = Array.from(data).map((byte) => `0x${byte.toString(16).padStart(2, "0")}`).join(" ");
5561
+ const ascii = Array.from(data).map((byte) => {
5562
+ if (byte >= 32 && byte <= 126) return String.fromCharCode(byte);
5563
+ if (byte === 10) return "\\n";
5564
+ if (byte === 13) return "\\r";
5565
+ if (byte === 9) return "\\t";
5566
+ return ".";
5567
+ }).join("");
5568
+ return `len=${data.length} hex=[${hex}] ascii="${ascii}"`;
5569
+ };
5570
+ const readCtrlR = (chunk) => {
5571
+ const data = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
5572
+ return data.length === 1 && data[0] === 18;
5573
+ };
5574
+ const readSubmittedCommands = (chunk) => {
5575
+ const text = toBuffer(chunk).toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5576
+ if (!text.includes("\n")) return [];
5577
+ const lines = text.split("\n");
5578
+ lines.pop();
5579
+ return lines;
5580
+ };
5581
+ if (debug) {
5582
+ server.config.logger.info(
5583
+ ` poke debug enabled (isTTY=${stdin.isTTY ? "yes" : "no"}, isRaw=${stdin.isTTY ? stdin.isRaw ? "yes" : "no" : "n/a"})`,
5584
+ { timestamp: true }
5585
+ );
5586
+ }
5121
5587
  if (stdin.isTTY) {
5122
- stdin.setRawMode(true);
5588
+ server.config.logger.info(
5589
+ " poke ready: press e + enter to reload browser (ctrl+r also works when available)",
5590
+ { timestamp: true }
5591
+ );
5123
5592
  }
5124
5593
  const onData = (data) => {
5125
- if (data.length !== 1) return;
5126
- if (data[0] === 3) {
5127
- process.emit("SIGINT", "SIGINT");
5128
- return;
5129
- }
5130
- if (data[0] === 18) {
5131
- server.hot.send({ type: "full-reload", path: "*" });
5132
- server.config.logger.info(" browser reload (ctrl+r)", {
5594
+ if (debug) {
5595
+ server.config.logger.info(` poke stdin ${formatChunk(data)}`, {
5133
5596
  timestamp: true
5134
5597
  });
5135
5598
  }
5599
+ if (readCtrlR(data)) {
5600
+ triggerReload("ctrl+r");
5601
+ return;
5602
+ }
5603
+ for (const command of readSubmittedCommands(data)) {
5604
+ if (command === "e") {
5605
+ triggerReload("e+enter");
5606
+ return;
5607
+ }
5608
+ if (command === "\x1Br") {
5609
+ triggerReload("option+r+enter");
5610
+ return;
5611
+ }
5612
+ }
5136
5613
  };
5137
5614
  stdin.on("data", onData);
5138
5615
  server.httpServer?.on("close", () => {
5139
5616
  stdin.off("data", onData);
5140
- if (stdin.isTTY && previousRawMode !== null) {
5141
- stdin.setRawMode(previousRawMode);
5142
- }
5143
5617
  });
5144
5618
  }
5145
5619
  };