@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379

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 (84) hide show
  1. package/README.md +46 -12
  2. package/dist/bin/rango.js +109 -15
  3. package/dist/vite/index.js +323 -121
  4. package/package.json +15 -16
  5. package/skills/breadcrumbs/SKILL.md +250 -0
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +33 -31
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/loader/SKILL.md +55 -15
  11. package/skills/prerender/SKILL.md +2 -2
  12. package/skills/rango/SKILL.md +0 -1
  13. package/skills/route/SKILL.md +3 -4
  14. package/skills/router-setup/SKILL.md +8 -3
  15. package/skills/typesafety/SKILL.md +25 -23
  16. package/src/__internal.ts +92 -0
  17. package/src/bin/rango.ts +18 -0
  18. package/src/browser/link-interceptor.ts +4 -0
  19. package/src/browser/navigation-bridge.ts +95 -5
  20. package/src/browser/navigation-client.ts +97 -72
  21. package/src/browser/prefetch/cache.ts +112 -25
  22. package/src/browser/prefetch/fetch.ts +28 -30
  23. package/src/browser/prefetch/policy.ts +6 -0
  24. package/src/browser/react/Link.tsx +19 -7
  25. package/src/browser/rsc-router.tsx +11 -2
  26. package/src/browser/server-action-bridge.ts +448 -432
  27. package/src/browser/types.ts +24 -0
  28. package/src/build/generate-route-types.ts +2 -0
  29. package/src/build/route-trie.ts +19 -3
  30. package/src/build/route-types/router-processing.ts +125 -15
  31. package/src/client.rsc.tsx +2 -1
  32. package/src/client.tsx +1 -46
  33. package/src/handles/breadcrumbs.ts +66 -0
  34. package/src/handles/index.ts +1 -0
  35. package/src/host/index.ts +0 -3
  36. package/src/index.rsc.ts +5 -36
  37. package/src/index.ts +32 -66
  38. package/src/prerender/store.ts +56 -15
  39. package/src/route-definition/index.ts +0 -3
  40. package/src/router/handler-context.ts +30 -3
  41. package/src/router/loader-resolution.ts +1 -1
  42. package/src/router/match-api.ts +1 -1
  43. package/src/router/match-result.ts +0 -9
  44. package/src/router/metrics.ts +233 -13
  45. package/src/router/middleware-types.ts +53 -10
  46. package/src/router/middleware.ts +170 -81
  47. package/src/router/pattern-matching.ts +20 -5
  48. package/src/router/prerender-match.ts +4 -0
  49. package/src/router/revalidation.ts +27 -7
  50. package/src/router/router-interfaces.ts +14 -1
  51. package/src/router/router-options.ts +13 -8
  52. package/src/router/segment-resolution/fresh.ts +18 -0
  53. package/src/router/segment-resolution/helpers.ts +1 -1
  54. package/src/router/segment-resolution/revalidation.ts +22 -9
  55. package/src/router/trie-matching.ts +20 -2
  56. package/src/router.ts +29 -9
  57. package/src/rsc/handler.ts +106 -11
  58. package/src/rsc/index.ts +0 -20
  59. package/src/rsc/progressive-enhancement.ts +21 -8
  60. package/src/rsc/rsc-rendering.ts +30 -43
  61. package/src/rsc/server-action.ts +14 -10
  62. package/src/rsc/ssr-setup.ts +128 -0
  63. package/src/rsc/types.ts +2 -0
  64. package/src/search-params.ts +16 -13
  65. package/src/server/context.ts +8 -2
  66. package/src/server/request-context.ts +38 -16
  67. package/src/server.ts +6 -0
  68. package/src/theme/index.ts +4 -13
  69. package/src/types/handler-context.ts +12 -16
  70. package/src/types/route-config.ts +17 -8
  71. package/src/types/segments.ts +0 -5
  72. package/src/vite/discovery/bundle-postprocess.ts +31 -56
  73. package/src/vite/discovery/discover-routers.ts +18 -4
  74. package/src/vite/discovery/prerender-collection.ts +34 -14
  75. package/src/vite/discovery/state.ts +4 -7
  76. package/src/vite/index.ts +4 -3
  77. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  78. package/src/vite/plugins/refresh-cmd.ts +65 -0
  79. package/src/vite/rango.ts +11 -0
  80. package/src/vite/router-discovery.ts +16 -0
  81. package/src/vite/utils/prerender-utils.ts +60 -0
  82. package/skills/testing/SKILL.md +0 -226
  83. package/src/route-definition/route-function.ts +0 -119
  84. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -1,6 +1,6 @@
1
1
  // src/vite/rango.ts
2
2
  import { readFileSync as readFileSync7 } from "node:fs";
3
- import { resolve as resolve7 } from "node:path";
3
+ import { resolve as resolve9 } from "node:path";
4
4
 
5
5
  // src/vite/plugins/expose-action-id.ts
6
6
  import MagicString from "magic-string";
@@ -1585,6 +1585,53 @@ function warnOnNearMissDirectives(ast, fileId, warn) {
1585
1585
  }
1586
1586
  }
1587
1587
 
1588
+ // src/vite/plugins/client-ref-dedup.ts
1589
+ var CLIENT_IN_SERVER_PROXY_PREFIX = "virtual:vite-rsc/client-in-server-package-proxy/";
1590
+ function extractPackageName(absolutePath) {
1591
+ const marker = "/node_modules/";
1592
+ const idx = absolutePath.lastIndexOf(marker);
1593
+ if (idx === -1) return null;
1594
+ const afterModules = absolutePath.slice(idx + marker.length);
1595
+ if (afterModules.startsWith("@")) {
1596
+ const parts = afterModules.split("/");
1597
+ if (parts.length < 2 || !parts[1]) return null;
1598
+ return `${parts[0]}/${parts[1]}`;
1599
+ }
1600
+ const name = afterModules.split("/")[0];
1601
+ return name || null;
1602
+ }
1603
+ function clientRefDedup() {
1604
+ let clientExclude = [];
1605
+ return {
1606
+ name: "@rangojs/router:client-ref-dedup",
1607
+ enforce: "pre",
1608
+ apply: "serve",
1609
+ configResolved(config) {
1610
+ const clientEnv = config.environments?.["client"];
1611
+ clientExclude = clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
1612
+ },
1613
+ resolveId(source, importer, options) {
1614
+ if (this.environment?.name !== "client") return;
1615
+ if (!importer?.includes(CLIENT_IN_SERVER_PROXY_PREFIX)) return;
1616
+ if (!source.includes("/node_modules/")) return;
1617
+ if (!importer) return;
1618
+ const packageName = extractPackageName(source);
1619
+ if (!packageName) return;
1620
+ if (clientExclude.includes(packageName)) return;
1621
+ return `\0rango:dedup/${packageName}`;
1622
+ },
1623
+ load(id) {
1624
+ if (!id.startsWith("\0rango:dedup/")) return;
1625
+ const packageName = id.slice("\0rango:dedup/".length);
1626
+ return [
1627
+ `export * from ${JSON.stringify(packageName)};`,
1628
+ `import * as __all__ from ${JSON.stringify(packageName)};`,
1629
+ `export default __all__.default;`
1630
+ ].join("\n");
1631
+ }
1632
+ };
1633
+ }
1634
+
1588
1635
  // src/vite/plugins/virtual-entries.ts
1589
1636
  var VIRTUAL_ENTRY_BROWSER = `
1590
1637
  import {
@@ -1698,7 +1745,7 @@ import { resolve } from "node:path";
1698
1745
  // package.json
1699
1746
  var package_default = {
1700
1747
  name: "@rangojs/router",
1701
- version: "0.0.0-experimental.19",
1748
+ version: "0.0.0-experimental.1b930379",
1702
1749
  description: "Django-inspired RSC router with composable URL patterns",
1703
1750
  keywords: [
1704
1751
  "react",
@@ -1729,7 +1776,7 @@ var package_default = {
1729
1776
  "!src/**/*.test.tsx",
1730
1777
  "dist",
1731
1778
  "skills",
1732
- "CLAUDE.md",
1779
+ "AGENTS.md",
1733
1780
  "README.md"
1734
1781
  ],
1735
1782
  type: "module",
@@ -2049,7 +2096,6 @@ declare global {
2049
2096
 
2050
2097
  // src/build/route-types/scan-filter.ts
2051
2098
  import { join, relative } from "node:path";
2052
- import { readdirSync } from "node:fs";
2053
2099
  import picomatch from "picomatch";
2054
2100
  var DEFAULT_EXCLUDE_PATTERNS = [
2055
2101
  "**/__tests__/**",
@@ -2074,29 +2120,6 @@ function createScanFilter(root, opts) {
2074
2120
  return true;
2075
2121
  };
2076
2122
  }
2077
- function findTsFiles(dir, filter) {
2078
- const results = [];
2079
- let entries;
2080
- try {
2081
- entries = readdirSync(dir, { withFileTypes: true });
2082
- } catch (err) {
2083
- console.warn(
2084
- `[rsc-router] Failed to scan directory ${dir}: ${err.message}`
2085
- );
2086
- return results;
2087
- }
2088
- for (const entry of entries) {
2089
- const fullPath = join(dir, entry.name);
2090
- if (entry.isDirectory()) {
2091
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
2092
- results.push(...findTsFiles(fullPath, filter));
2093
- } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
2094
- if (filter && !filter(fullPath)) continue;
2095
- results.push(fullPath);
2096
- }
2097
- }
2098
- return results;
2099
- }
2100
2123
 
2101
2124
  // src/build/route-types/per-module-writer.ts
2102
2125
  import ts4 from "typescript";
@@ -2355,8 +2378,20 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
2355
2378
  }
2356
2379
 
2357
2380
  // src/build/route-types/router-processing.ts
2358
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, unlinkSync } from "node:fs";
2359
- import { join as join2, dirname as dirname2, resolve as resolve3, basename as pathBasename } from "node:path";
2381
+ import {
2382
+ readFileSync as readFileSync2,
2383
+ writeFileSync,
2384
+ existsSync as existsSync3,
2385
+ unlinkSync,
2386
+ readdirSync
2387
+ } from "node:fs";
2388
+ import {
2389
+ join as join2,
2390
+ dirname as dirname2,
2391
+ resolve as resolve3,
2392
+ sep,
2393
+ basename as pathBasename
2394
+ } from "node:path";
2360
2395
  import ts5 from "typescript";
2361
2396
  function countPublicRouteEntries(source) {
2362
2397
  const matches = source.matchAll(/^\s+(?:"([^"]+)"|([a-zA-Z_$][^:]*)):\s*["{]/gm) ?? [];
@@ -2369,6 +2404,77 @@ function countPublicRouteEntries(source) {
2369
2404
  }
2370
2405
  return count;
2371
2406
  }
2407
+ var ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
2408
+ function isRoutableSourceFile(name) {
2409
+ return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.");
2410
+ }
2411
+ function findRouterFilesRecursive(dir, filter, results) {
2412
+ let entries;
2413
+ try {
2414
+ entries = readdirSync(dir, { withFileTypes: true });
2415
+ } catch (err) {
2416
+ console.warn(
2417
+ `[rsc-router] Failed to scan directory ${dir}: ${err.message}`
2418
+ );
2419
+ return;
2420
+ }
2421
+ const childDirs = [];
2422
+ const routerFilesInDir = [];
2423
+ for (const entry of entries) {
2424
+ const fullPath = join2(dir, entry.name);
2425
+ if (entry.isDirectory()) {
2426
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
2427
+ childDirs.push(fullPath);
2428
+ continue;
2429
+ }
2430
+ if (!isRoutableSourceFile(entry.name)) continue;
2431
+ if (filter && !filter(fullPath)) continue;
2432
+ try {
2433
+ const source = readFileSync2(fullPath, "utf-8");
2434
+ if (ROUTER_CALL_PATTERN.test(source)) {
2435
+ routerFilesInDir.push(fullPath);
2436
+ }
2437
+ } catch {
2438
+ continue;
2439
+ }
2440
+ }
2441
+ if (routerFilesInDir.length > 0) {
2442
+ results.push(...routerFilesInDir);
2443
+ return;
2444
+ }
2445
+ for (const childDir of childDirs) {
2446
+ findRouterFilesRecursive(childDir, filter, results);
2447
+ }
2448
+ }
2449
+ function findNestedRouterConflict(routerFiles) {
2450
+ const routerDirs = [
2451
+ ...new Set(routerFiles.map((filePath) => dirname2(resolve3(filePath))))
2452
+ ].sort((a, b) => a.length - b.length);
2453
+ for (let i = 0; i < routerDirs.length; i++) {
2454
+ const ancestorDir = routerDirs[i];
2455
+ const prefix = ancestorDir.endsWith(sep) ? ancestorDir : `${ancestorDir}${sep}`;
2456
+ for (let j = i + 1; j < routerDirs.length; j++) {
2457
+ const nestedDir = routerDirs[j];
2458
+ if (!nestedDir.startsWith(prefix)) continue;
2459
+ const ancestorFile = routerFiles.find(
2460
+ (filePath) => dirname2(resolve3(filePath)) === ancestorDir
2461
+ );
2462
+ const nestedFile = routerFiles.find(
2463
+ (filePath) => dirname2(resolve3(filePath)) === nestedDir
2464
+ );
2465
+ if (ancestorFile && nestedFile) {
2466
+ return { ancestor: ancestorFile, nested: nestedFile };
2467
+ }
2468
+ }
2469
+ }
2470
+ return null;
2471
+ }
2472
+ function formatNestedRouterConflictError(conflict, prefix = "[rsc-router]") {
2473
+ return `${prefix} Nested router roots are not supported.
2474
+ Router root: ${conflict.ancestor}
2475
+ Nested router: ${conflict.nested}
2476
+ Move the nested router into a sibling directory or configure it as a separate app root.`;
2477
+ }
2372
2478
  function extractUrlsVariableFromRouter(code) {
2373
2479
  const sourceFile = ts5.createSourceFile(
2374
2480
  "router.tsx",
@@ -2435,19 +2541,8 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
2435
2541
  return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
2436
2542
  }
2437
2543
  function findRouterFiles(root, filter) {
2438
- const files = findTsFiles(root, filter);
2439
2544
  const result = [];
2440
- for (const filePath of files) {
2441
- if (filePath.includes(".gen.")) continue;
2442
- try {
2443
- const source = readFileSync2(filePath, "utf-8");
2444
- if (/\bcreateRouter\s*[<(]/.test(source)) {
2445
- result.push(filePath);
2446
- }
2447
- } catch {
2448
- continue;
2449
- }
2450
- }
2545
+ findRouterFilesRecursive(root, filter, result);
2451
2546
  return result;
2452
2547
  }
2453
2548
  function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
@@ -2463,6 +2558,10 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
2463
2558
  }
2464
2559
  const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
2465
2560
  if (routerFilePaths.length === 0) return;
2561
+ const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
2562
+ if (nestedRouterConflict) {
2563
+ throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
2564
+ }
2466
2565
  for (const routerFilePath of routerFilePaths) {
2467
2566
  let routerSource;
2468
2567
  try {
@@ -2925,6 +3024,7 @@ function createCjsToEsmPlugin() {
2925
3024
 
2926
3025
  // src/vite/router-discovery.ts
2927
3026
  import { createServer as createViteServer } from "vite";
3027
+ import { resolve as resolve8 } from "node:path";
2928
3028
  import { readFileSync as readFileSync6 } from "node:fs";
2929
3029
 
2930
3030
  // src/vite/plugins/virtual-stub-plugin.ts
@@ -3107,8 +3207,8 @@ function createDiscoveryState(entryPath, opts) {
3107
3207
  perRouterTrieMap: /* @__PURE__ */ new Map(),
3108
3208
  perRouterPrecomputedMap: /* @__PURE__ */ new Map(),
3109
3209
  perRouterManifestDataMap: /* @__PURE__ */ new Map(),
3110
- prerenderCollectedData: null,
3111
- staticCollectedData: null,
3210
+ prerenderManifestEntries: null,
3211
+ staticManifestEntries: null,
3112
3212
  handlerChunkInfo: null,
3113
3213
  staticHandlerChunkInfo: null,
3114
3214
  rscEntryFileName: null,
@@ -3208,6 +3308,16 @@ function contextSet(variables, keyOrVar, value) {
3208
3308
  }
3209
3309
 
3210
3310
  // src/vite/utils/prerender-utils.ts
3311
+ import { createHash as createHash4 } from "node:crypto";
3312
+ import {
3313
+ copyFileSync,
3314
+ existsSync as existsSync4,
3315
+ mkdirSync,
3316
+ rmSync,
3317
+ statSync,
3318
+ writeFileSync as writeFileSync2
3319
+ } from "node:fs";
3320
+ import { resolve as resolve5 } from "node:path";
3211
3321
  function escapeRegExp2(str) {
3212
3322
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3213
3323
  }
@@ -3286,6 +3396,37 @@ function notifyOnError(registry, error, phase, routeKey, pathname, skipped) {
3286
3396
  break;
3287
3397
  }
3288
3398
  }
3399
+ function getStagedAssetDir(projectRoot) {
3400
+ return resolve5(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
3401
+ }
3402
+ function resetStagedBuildAssets(projectRoot) {
3403
+ rmSync(getStagedAssetDir(projectRoot), { recursive: true, force: true });
3404
+ }
3405
+ function stageBuildAssetModule(projectRoot, prefix, exportValue) {
3406
+ const stagedDir = getStagedAssetDir(projectRoot);
3407
+ mkdirSync(stagedDir, { recursive: true });
3408
+ const contentHash = createHash4("sha256").update(exportValue).digest("hex").slice(0, 8);
3409
+ const fileName = `${prefix}-${contentHash}.js`;
3410
+ const filePath = resolve5(stagedDir, fileName);
3411
+ if (!existsSync4(filePath)) {
3412
+ writeFileSync2(filePath, `export default ${exportValue};
3413
+ `);
3414
+ }
3415
+ return fileName;
3416
+ }
3417
+ function copyStagedBuildAssets(projectRoot, fileNames) {
3418
+ const stagedDir = getStagedAssetDir(projectRoot);
3419
+ const distAssetsDir = resolve5(projectRoot, "dist/rsc/assets");
3420
+ mkdirSync(distAssetsDir, { recursive: true });
3421
+ let totalBytes = 0;
3422
+ for (const fileName of new Set(fileNames)) {
3423
+ const stagedPath = resolve5(stagedDir, fileName);
3424
+ const distPath = resolve5(distAssetsDir, fileName);
3425
+ copyFileSync(stagedPath, distPath);
3426
+ totalBytes += statSync(stagedPath).size;
3427
+ }
3428
+ return totalBytes;
3429
+ }
3289
3430
 
3290
3431
  // src/vite/discovery/prerender-collection.ts
3291
3432
  async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
@@ -3387,7 +3528,7 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3387
3528
  `[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`
3388
3529
  );
3389
3530
  const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
3390
- const collectedData = {};
3531
+ const manifestEntries = {};
3391
3532
  let doneCount = 0;
3392
3533
  let skipCount = 0;
3393
3534
  const startTotal = performance.now();
@@ -3417,18 +3558,30 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3417
3558
  break;
3418
3559
  }
3419
3560
  const paramHash = hashParams(result.params || {});
3420
- collectedData[`${result.routeName}/${paramHash}`] = {
3561
+ const mainKey = `${result.routeName}/${paramHash}`;
3562
+ const mainValue = JSON.stringify({
3421
3563
  segments: result.segments,
3422
3564
  handles: result.handles
3423
- };
3565
+ });
3566
+ manifestEntries[mainKey] = stageBuildAssetModule(
3567
+ state.projectRoot,
3568
+ "__pr",
3569
+ mainValue
3570
+ );
3424
3571
  if (result.interceptSegments?.length) {
3425
- collectedData[`${result.routeName}/${paramHash}/i`] = {
3572
+ const interceptKey = `${result.routeName}/${paramHash}/i`;
3573
+ const interceptValue = JSON.stringify({
3426
3574
  segments: [...result.segments, ...result.interceptSegments],
3427
3575
  handles: {
3428
3576
  ...result.handles,
3429
3577
  ...result.interceptHandles || {}
3430
3578
  }
3431
- };
3579
+ });
3580
+ manifestEntries[interceptKey] = stageBuildAssetModule(
3581
+ state.projectRoot,
3582
+ "__pr",
3583
+ interceptValue
3584
+ );
3432
3585
  }
3433
3586
  const elapsed = (performance.now() - startUrl).toFixed(0);
3434
3587
  console.log(
@@ -3472,7 +3625,7 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3472
3625
  }
3473
3626
  const totalElapsed = (performance.now() - startTotal).toFixed(0);
3474
3627
  if (doneCount > 0) {
3475
- state.prerenderCollectedData = collectedData;
3628
+ state.prerenderManifestEntries = manifestEntries;
3476
3629
  }
3477
3630
  const parts = [`${doneCount} done`];
3478
3631
  if (skipCount > 0) parts.push(`${skipCount} skipped`);
@@ -3483,7 +3636,7 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3483
3636
  async function renderStaticHandlers(state, rscEnv, registry) {
3484
3637
  if (!state.opts?.enableBuildPrerender || !state.isBuildMode || !state.resolvedStaticModules?.size)
3485
3638
  return;
3486
- const collected = {};
3639
+ const manifestEntries = {};
3487
3640
  let staticDone = 0;
3488
3641
  let staticSkip = 0;
3489
3642
  let totalStaticCount = 0;
@@ -3520,7 +3673,13 @@ async function renderStaticHandlers(state, rscEnv, registry) {
3520
3673
  def.$$routePrefix
3521
3674
  );
3522
3675
  if (result) {
3523
- collected[def.$$id] = result;
3676
+ const hasHandles = Object.keys(result.handles).length > 0;
3677
+ const exportValue = hasHandles ? JSON.stringify(result) : JSON.stringify(result.encoded);
3678
+ manifestEntries[def.$$id] = stageBuildAssetModule(
3679
+ state.projectRoot,
3680
+ "__st",
3681
+ exportValue
3682
+ );
3524
3683
  const elapsed = (performance.now() - startHandler).toFixed(0);
3525
3684
  console.log(
3526
3685
  `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`
@@ -3557,7 +3716,7 @@ async function renderStaticHandlers(state, rscEnv, registry) {
3557
3716
  }
3558
3717
  const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
3559
3718
  if (staticDone > 0) {
3560
- state.staticCollectedData = collected;
3719
+ state.staticManifestEntries = manifestEntries;
3561
3720
  }
3562
3721
  const staticParts = [`${staticDone} done`];
3563
3722
  if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
@@ -3574,8 +3733,7 @@ async function discoverRouters(state, rscEnv) {
3574
3733
  let registry = serverMod.RouterRegistry;
3575
3734
  if (!registry || registry.size === 0) {
3576
3735
  try {
3577
- const hostMod = await rscEnv.runner.import("@rangojs/router/host");
3578
- const hostRegistry = hostMod.HostRouterRegistry;
3736
+ const hostRegistry = serverMod.HostRouterRegistry;
3579
3737
  if (hostRegistry && hostRegistry.size > 0) {
3580
3738
  console.log(
3581
3739
  `[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`
@@ -3615,6 +3773,14 @@ async function discoverRouters(state, rscEnv) {
3615
3773
  }
3616
3774
  const buildMod = await rscEnv.runner.import("@rangojs/router/build");
3617
3775
  const generateManifestFull = buildMod.generateManifestFull;
3776
+ const nestedRouterConflict = findNestedRouterConflict(
3777
+ [...registry.values()].map((router) => router.__sourceFile).filter(
3778
+ (sourceFile) => typeof sourceFile === "string"
3779
+ )
3780
+ );
3781
+ if (nestedRouterConflict) {
3782
+ throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
3783
+ }
3618
3784
  const newMergedRouteManifest = {};
3619
3785
  const newMergedPrecomputedEntries = [];
3620
3786
  const newPerRouterManifests = [];
@@ -3771,8 +3937,8 @@ async function discoverRouters(state, rscEnv) {
3771
3937
  }
3772
3938
 
3773
3939
  // src/vite/discovery/route-types-writer.ts
3774
- import { dirname as dirname3, basename, join as join3, resolve as resolve5 } from "node:path";
3775
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "node:fs";
3940
+ import { dirname as dirname3, basename, join as join3, resolve as resolve6 } from "node:path";
3941
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
3776
3942
  function filterUserNamedRoutes(manifest) {
3777
3943
  const filtered = {};
3778
3944
  for (const [name, pattern] of Object.entries(manifest)) {
@@ -3806,7 +3972,7 @@ function writeCombinedRouteTypesWithTracking(state, opts) {
3806
3972
  ""
3807
3973
  );
3808
3974
  const outPath = join3(routerDir, `${routerBasename}.named-routes.gen.ts`);
3809
- if (!existsSync4(outPath)) continue;
3975
+ if (!existsSync5(outPath)) continue;
3810
3976
  try {
3811
3977
  const content = readFileSync4(outPath, "utf-8");
3812
3978
  if (content !== preContent.get(outPath)) {
@@ -3820,10 +3986,10 @@ function writeRouteTypesFiles(state) {
3820
3986
  if (state.perRouterManifests.length === 0) return;
3821
3987
  try {
3822
3988
  const entryDir = dirname3(
3823
- resolve5(state.projectRoot, state.resolvedEntryPath)
3989
+ resolve6(state.projectRoot, state.resolvedEntryPath)
3824
3990
  );
3825
3991
  const oldCombinedPath = join3(entryDir, "named-routes.gen.ts");
3826
- if (existsSync4(oldCombinedPath)) {
3992
+ if (existsSync5(oldCombinedPath)) {
3827
3993
  unlinkSync2(oldCombinedPath);
3828
3994
  console.log(
3829
3995
  `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`
@@ -3867,10 +4033,10 @@ Set an explicit \`id\` on createRouter() or check the call site.`
3867
4033
  userRoutes,
3868
4034
  effectiveSearchSchemas && Object.keys(effectiveSearchSchemas).length > 0 ? effectiveSearchSchemas : void 0
3869
4035
  );
3870
- const existing = existsSync4(outPath) ? readFileSync4(outPath, "utf-8") : null;
4036
+ const existing = existsSync5(outPath) ? readFileSync4(outPath, "utf-8") : null;
3871
4037
  if (existing !== source) {
3872
4038
  markSelfGenWrite(state, outPath, source);
3873
- writeFileSync2(outPath, source);
4039
+ writeFileSync3(outPath, source);
3874
4040
  console.log(`[rsc-router] Generated route types -> ${outPath}`);
3875
4041
  }
3876
4042
  }
@@ -3917,10 +4083,10 @@ function supplementGenFilesWithRuntimeRoutes(state) {
3917
4083
  mergedRoutes,
3918
4084
  Object.keys(mergedSearchSchemas).length > 0 ? mergedSearchSchemas : void 0
3919
4085
  );
3920
- const existing = existsSync4(outPath) ? readFileSync4(outPath, "utf-8") : null;
4086
+ const existing = existsSync5(outPath) ? readFileSync4(outPath, "utf-8") : null;
3921
4087
  if (existing !== source) {
3922
4088
  markSelfGenWrite(state, outPath, source);
3923
- writeFileSync2(outPath, source);
4089
+ writeFileSync3(outPath, source);
3924
4090
  }
3925
4091
  }
3926
4092
  }
@@ -4065,14 +4231,13 @@ function generatePerRouterModule(state, routerId) {
4065
4231
  }
4066
4232
 
4067
4233
  // src/vite/discovery/bundle-postprocess.ts
4068
- import { resolve as resolve6 } from "node:path";
4069
- import { createHash as createHash4 } from "node:crypto";
4070
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync } from "node:fs";
4234
+ import { resolve as resolve7 } from "node:path";
4235
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
4071
4236
  function postprocessBundle(state) {
4072
- const hasPrerenderData = state.prerenderCollectedData && Object.keys(state.prerenderCollectedData).length > 0;
4073
- const hasStaticData = state.staticCollectedData && Object.keys(state.staticCollectedData).length > 0;
4237
+ const hasPrerenderData = state.prerenderManifestEntries && Object.keys(state.prerenderManifestEntries).length > 0;
4238
+ const hasStaticData = state.staticManifestEntries && Object.keys(state.staticManifestEntries).length > 0;
4074
4239
  if (!hasPrerenderData && !hasStaticData) return;
4075
- const rscEntryPath = resolve6(
4240
+ const rscEntryPath = resolve7(
4076
4241
  state.projectRoot,
4077
4242
  "dist/rsc",
4078
4243
  state.rscEntryFileName ?? "index.js"
@@ -4093,7 +4258,7 @@ function postprocessBundle(state) {
4093
4258
  ];
4094
4259
  for (const target of evictionTargets) {
4095
4260
  if (!target.info) continue;
4096
- const chunkPath = resolve6(
4261
+ const chunkPath = resolve7(
4097
4262
  state.projectRoot,
4098
4263
  "dist/rsc",
4099
4264
  target.info.fileName
@@ -4107,7 +4272,7 @@ function postprocessBundle(state) {
4107
4272
  target.brand
4108
4273
  );
4109
4274
  if (result) {
4110
- writeFileSync3(chunkPath, result.code);
4275
+ writeFileSync4(chunkPath, result.code);
4111
4276
  const savedKB = (result.savedBytes / 1024).toFixed(1);
4112
4277
  console.log(
4113
4278
  `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${target.info.fileName}`
@@ -4121,44 +4286,38 @@ function postprocessBundle(state) {
4121
4286
  }
4122
4287
  state.handlerChunkInfo = null;
4123
4288
  state.staticHandlerChunkInfo = null;
4124
- if (hasPrerenderData && existsSync5(rscEntryPath)) {
4289
+ if (hasPrerenderData && existsSync6(rscEntryPath)) {
4125
4290
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
4126
4291
  if (!rscCode.includes("__prerender-manifest.js")) {
4127
4292
  try {
4128
- const assetsDir = resolve6(state.projectRoot, "dist/rsc/assets");
4129
- mkdirSync(assetsDir, { recursive: true });
4130
- const manifestEntries = [];
4131
- let totalBytes = 0;
4132
- for (const [key, entry] of Object.entries(
4133
- state.prerenderCollectedData
4293
+ let totalBytes = copyStagedBuildAssets(
4294
+ state.projectRoot,
4295
+ Object.values(state.prerenderManifestEntries)
4296
+ );
4297
+ const manifestMap = {};
4298
+ for (const [key, assetFileName] of Object.entries(
4299
+ state.prerenderManifestEntries
4134
4300
  )) {
4135
- const entryJson = JSON.stringify(entry);
4136
- const contentHash = createHash4("sha256").update(entryJson).digest("hex").slice(0, 8);
4137
- const assetFileName = `__pr-${contentHash}.js`;
4138
- const assetPath = resolve6(assetsDir, assetFileName);
4139
- const assetCode = `export default ${entryJson};
4140
- `;
4141
- writeFileSync3(assetPath, assetCode);
4142
- totalBytes += Buffer.byteLength(assetCode);
4143
- manifestEntries.push(
4144
- `${JSON.stringify(key)}:()=>import("./assets/${assetFileName}")`
4145
- );
4301
+ manifestMap[key] = `./assets/${assetFileName}`;
4146
4302
  }
4147
- const manifestCode = `const m={${manifestEntries.join(",")}};export default m;
4148
- `;
4149
- const manifestPath = resolve6(
4303
+ const manifestCode = [
4304
+ `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
4305
+ `export function loadPrerenderAsset(s){return import(s)}`,
4306
+ `export default m;`,
4307
+ ""
4308
+ ].join("\n");
4309
+ const manifestPath = resolve7(
4150
4310
  state.projectRoot,
4151
4311
  "dist/rsc/__prerender-manifest.js"
4152
4312
  );
4153
- writeFileSync3(manifestPath, manifestCode);
4313
+ writeFileSync4(manifestPath, manifestCode);
4154
4314
  totalBytes += Buffer.byteLength(manifestCode);
4155
- const injection = `import __pm from "./__prerender-manifest.js";
4156
- globalThis.__PRERENDER_MANIFEST = __pm;
4315
+ const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");
4157
4316
  `;
4158
- writeFileSync3(rscEntryPath, injection + rscCode);
4317
+ writeFileSync4(rscEntryPath, injection + rscCode);
4159
4318
  const totalKB = (totalBytes / 1024).toFixed(1);
4160
4319
  console.log(
4161
- `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderCollectedData).length} entries)`
4320
+ `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries).length} entries)`
4162
4321
  );
4163
4322
  } catch (err) {
4164
4323
  throw new Error(
@@ -4167,44 +4326,36 @@ globalThis.__PRERENDER_MANIFEST = __pm;
4167
4326
  }
4168
4327
  }
4169
4328
  }
4170
- if (hasStaticData && existsSync5(rscEntryPath)) {
4329
+ if (hasStaticData && existsSync6(rscEntryPath)) {
4171
4330
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
4172
4331
  if (!rscCode.includes("__STATIC_MANIFEST")) {
4173
4332
  try {
4174
- const assetsDir = resolve6(state.projectRoot, "dist/rsc/assets");
4175
- mkdirSync(assetsDir, { recursive: true });
4176
4333
  const manifestEntries = [];
4177
- let totalBytes = 0;
4178
- for (const [handlerId, { encoded, handles }] of Object.entries(
4179
- state.staticCollectedData
4334
+ let totalBytes = copyStagedBuildAssets(
4335
+ state.projectRoot,
4336
+ Object.values(state.staticManifestEntries)
4337
+ );
4338
+ for (const [handlerId, assetFileName] of Object.entries(
4339
+ state.staticManifestEntries
4180
4340
  )) {
4181
- const hasHandles = Object.keys(handles).length > 0;
4182
- const exportValue = hasHandles ? JSON.stringify({ encoded, handles }) : JSON.stringify(encoded);
4183
- const contentHash = createHash4("sha256").update(exportValue).digest("hex").slice(0, 8);
4184
- const assetFileName = `__st-${contentHash}.js`;
4185
- const assetPath = resolve6(assetsDir, assetFileName);
4186
- const assetCode = `export default ${exportValue};
4187
- `;
4188
- writeFileSync3(assetPath, assetCode);
4189
- totalBytes += Buffer.byteLength(assetCode);
4190
4341
  manifestEntries.push(
4191
4342
  `${JSON.stringify(handlerId)}:()=>import("./assets/${assetFileName}")`
4192
4343
  );
4193
4344
  }
4194
4345
  const manifestCode = `const m={${manifestEntries.join(",")}};globalThis.__STATIC_MANIFEST=m;export default m;
4195
4346
  `;
4196
- const manifestPath = resolve6(
4347
+ const manifestPath = resolve7(
4197
4348
  state.projectRoot,
4198
4349
  "dist/rsc/__static-manifest.js"
4199
4350
  );
4200
- writeFileSync3(manifestPath, manifestCode);
4351
+ writeFileSync4(manifestPath, manifestCode);
4201
4352
  totalBytes += Buffer.byteLength(manifestCode);
4202
4353
  const injection = `import "./__static-manifest.js";
4203
4354
  `;
4204
- writeFileSync3(rscEntryPath, injection + rscCode);
4355
+ writeFileSync4(rscEntryPath, injection + rscCode);
4205
4356
  const totalKB = (totalBytes / 1024).toFixed(1);
4206
4357
  console.log(
4207
- `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticCollectedData).length} entries)`
4358
+ `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries).length} entries)`
4208
4359
  );
4209
4360
  } catch (err) {
4210
4361
  throw new Error(
@@ -4319,8 +4470,8 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4319
4470
  if (globalThis.__rscRouterDiscoveryActive) return;
4320
4471
  s.devServer = server;
4321
4472
  let resolveDiscovery;
4322
- const discoveryPromise = new Promise((resolve8) => {
4323
- resolveDiscovery = resolve8;
4473
+ const discoveryPromise = new Promise((resolve10) => {
4474
+ resolveDiscovery = resolve10;
4324
4475
  });
4325
4476
  const getDevServerOrigin = () => server.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || `http://localhost:${server.config.server.port || 5173}`;
4326
4477
  let prerenderTempServer = null;
@@ -4395,8 +4546,8 @@ ${err.stack}`
4395
4546
  resolveDiscovery();
4396
4547
  }
4397
4548
  };
4398
- s.discoveryDone = new Promise((resolve8) => {
4399
- setTimeout(() => discover().then(resolve8, resolve8), 0);
4549
+ s.discoveryDone = new Promise((resolve10) => {
4550
+ setTimeout(() => discover().then(resolve10, resolve10), 0);
4400
4551
  });
4401
4552
  let mainRegistry = null;
4402
4553
  const propagateDiscoveryState = async (rscEnv) => {
@@ -4563,6 +4714,16 @@ ${err.stack}`
4563
4714
  const hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
4564
4715
  if (!hasUrls && !hasCreateRouter) return;
4565
4716
  if (hasCreateRouter) {
4717
+ const nestedRouterConflict = findNestedRouterConflict([
4718
+ ...s.cachedRouterFiles ?? [],
4719
+ resolve8(filePath)
4720
+ ]);
4721
+ if (nestedRouterConflict) {
4722
+ server.config.logger.error(
4723
+ formatNestedRouterConflictError(nestedRouterConflict)
4724
+ );
4725
+ return;
4726
+ }
4566
4727
  s.cachedRouterFiles = void 0;
4567
4728
  }
4568
4729
  scheduleRouteRegeneration();
@@ -4585,6 +4746,9 @@ ${err.stack}`
4585
4746
  async buildStart() {
4586
4747
  if (!s.isBuildMode) return;
4587
4748
  if (s.mergedRouteManifest !== null) return;
4749
+ resetStagedBuildAssets(s.projectRoot);
4750
+ s.prerenderManifestEntries = null;
4751
+ s.staticManifestEntries = null;
4588
4752
  let tempServer = null;
4589
4753
  globalThis.__rscRouterDiscoveryActive = true;
4590
4754
  try {
@@ -4817,6 +4981,7 @@ async function rango(options) {
4817
4981
  serverHandler: false
4818
4982
  })
4819
4983
  );
4984
+ plugins.push(clientRefDedup());
4820
4985
  } else {
4821
4986
  const nodeOptions = resolvedOptions;
4822
4987
  routerRef.path = nodeOptions.router;
@@ -4825,7 +4990,7 @@ async function rango(options) {
4825
4990
  name: "@rangojs/router:auto-discover",
4826
4991
  config(userConfig) {
4827
4992
  if (routerRef.path) return;
4828
- const root = userConfig.root ? resolve7(process.cwd(), userConfig.root) : process.cwd();
4993
+ const root = userConfig.root ? resolve9(process.cwd(), userConfig.root) : process.cwd();
4829
4994
  const filter = createScanFilter(root, {
4830
4995
  include: resolvedOptions.include,
4831
4996
  exclude: resolvedOptions.exclude
@@ -4965,6 +5130,7 @@ ${list}`
4965
5130
  })
4966
5131
  );
4967
5132
  }
5133
+ plugins.push(clientRefDedup());
4968
5134
  }
4969
5135
  plugins.push({
4970
5136
  name: "@rangojs/router:client-component-hmr",
@@ -5007,6 +5173,42 @@ ${list}`
5007
5173
  );
5008
5174
  return plugins;
5009
5175
  }
5176
+
5177
+ // src/vite/plugins/refresh-cmd.ts
5178
+ function poke() {
5179
+ return {
5180
+ name: "vite-plugin-poke",
5181
+ apply: "serve",
5182
+ configureServer(server) {
5183
+ const stdin = process.stdin;
5184
+ const previousRawMode = stdin.isTTY ? stdin.isRaw : null;
5185
+ if (stdin.isTTY) {
5186
+ stdin.setRawMode(true);
5187
+ }
5188
+ const onData = (data) => {
5189
+ if (data.length !== 1) return;
5190
+ if (data[0] === 3) {
5191
+ process.emit("SIGINT", "SIGINT");
5192
+ return;
5193
+ }
5194
+ if (data[0] === 18) {
5195
+ server.hot.send({ type: "full-reload", path: "*" });
5196
+ server.config.logger.info(" browser reload (ctrl+r)", {
5197
+ timestamp: true
5198
+ });
5199
+ }
5200
+ };
5201
+ stdin.on("data", onData);
5202
+ server.httpServer?.on("close", () => {
5203
+ stdin.off("data", onData);
5204
+ if (stdin.isTTY && previousRawMode !== null) {
5205
+ stdin.setRawMode(previousRawMode);
5206
+ }
5207
+ });
5208
+ }
5209
+ };
5210
+ }
5010
5211
  export {
5212
+ poke,
5011
5213
  rango
5012
5214
  };