@rangojs/router 0.1.0 → 0.2.0

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 (128) hide show
  1. package/README.md +2 -1
  2. package/dist/bin/rango.js +11 -1
  3. package/dist/types/browser/dev-discovery.d.ts +11 -0
  4. package/dist/types/browser/navigation-store.d.ts +3 -13
  5. package/dist/types/browser/prefetch/loader.d.ts +10 -0
  6. package/dist/types/browser/prefetch/runtime.d.ts +3 -0
  7. package/dist/types/browser/types.d.ts +4 -16
  8. package/dist/types/build/generate-manifest.d.ts +6 -0
  9. package/dist/types/cache/cache-scope.d.ts +7 -5
  10. package/dist/types/cache/cf/cf-cache-store.d.ts +10 -9
  11. package/dist/types/cache/document-cache.d.ts +3 -1
  12. package/dist/types/cache/memory-segment-store.d.ts +6 -6
  13. package/dist/types/cache/shell-snapshot.d.ts +21 -8
  14. package/dist/types/cache/types.d.ts +25 -7
  15. package/dist/types/cache/vercel/vercel-cache-store.d.ts +9 -8
  16. package/dist/types/dev-discovery-protocol.d.ts +5 -0
  17. package/dist/types/prerender/store.d.ts +1 -0
  18. package/dist/types/route-definition/helpers-types.d.ts +6 -6
  19. package/dist/types/route-map-builder.d.ts +4 -29
  20. package/dist/types/router/match-handlers.d.ts +2 -3
  21. package/dist/types/router/prerender-match.d.ts +4 -1
  22. package/dist/types/router/router-interfaces.d.ts +3 -1
  23. package/dist/types/router/segment-resolution/view-transition-default.d.ts +3 -4
  24. package/dist/types/router/transition-when.d.ts +13 -0
  25. package/dist/types/rsc/handler-context.d.ts +1 -0
  26. package/dist/types/rsc/render-pipeline.d.ts +3 -2
  27. package/dist/types/rsc/rsc-rendering.d.ts +5 -10
  28. package/dist/types/rsc/shell-capture.d.ts +4 -8
  29. package/dist/types/rsc/shell-serve.d.ts +2 -0
  30. package/dist/types/rsc/transition-gate.d.ts +10 -14
  31. package/dist/types/rsc/types.d.ts +2 -0
  32. package/dist/types/server/context.d.ts +16 -0
  33. package/dist/types/server/request-context.d.ts +22 -8
  34. package/dist/types/server.d.ts +1 -1
  35. package/dist/types/testing/e2e/index.d.ts +1 -1
  36. package/dist/types/testing/index.d.ts +2 -2
  37. package/dist/types/testing/run-transition-when.d.ts +6 -5
  38. package/dist/types/testing/shell-status.d.ts +23 -3
  39. package/dist/types/types/route-entry.d.ts +3 -0
  40. package/dist/types/types/segments.d.ts +27 -22
  41. package/dist/types/urls/path-helper-types.d.ts +4 -3
  42. package/dist/types/vite/discovery/state.d.ts +3 -2
  43. package/dist/types/vite/utils/manifest-utils.d.ts +1 -1
  44. package/dist/vite/index.js +194 -107
  45. package/package.json +24 -20
  46. package/skills/cache-guide/SKILL.md +6 -3
  47. package/skills/caching/SKILL.md +1 -1
  48. package/skills/catalog.json +7 -1
  49. package/skills/deployment-caching/SKILL.md +176 -0
  50. package/skills/document-cache/SKILL.md +30 -3
  51. package/skills/ppr/SKILL.md +90 -14
  52. package/skills/prerender/SKILL.md +15 -8
  53. package/skills/rango/SKILL.md +20 -17
  54. package/skills/testing/SKILL.md +1 -1
  55. package/skills/testing/cache-prerender.md +5 -1
  56. package/skills/vercel/SKILL.md +22 -1
  57. package/skills/view-transitions/SKILL.md +12 -8
  58. package/src/browser/dev-discovery.ts +66 -0
  59. package/src/browser/navigation-client.ts +6 -0
  60. package/src/browser/navigation-store.ts +4 -271
  61. package/src/browser/partial-update.ts +7 -14
  62. package/src/browser/prefetch/cache.ts +1 -1
  63. package/src/browser/prefetch/loader.ts +110 -0
  64. package/src/browser/prefetch/runtime.ts +7 -0
  65. package/src/browser/react/Link.tsx +7 -10
  66. package/src/browser/react/NavigationProvider.tsx +1 -1
  67. package/src/browser/react/use-router.ts +1 -1
  68. package/src/browser/rsc-router.tsx +4 -2
  69. package/src/browser/types.ts +4 -27
  70. package/src/build/generate-manifest.ts +11 -0
  71. package/src/cache/cache-scope.ts +23 -7
  72. package/src/cache/cf/cf-cache-store.ts +63 -39
  73. package/src/cache/document-cache.ts +4 -2
  74. package/src/cache/memory-segment-store.ts +17 -6
  75. package/src/cache/shell-snapshot.ts +45 -15
  76. package/src/cache/types.ts +24 -6
  77. package/src/cache/vercel/vercel-cache-store.ts +52 -22
  78. package/src/dev-discovery-protocol.ts +11 -0
  79. package/src/prerender/build-shell-capture.ts +7 -1
  80. package/src/prerender/store.ts +5 -0
  81. package/src/route-definition/dsl-helpers.ts +7 -2
  82. package/src/route-definition/helpers-types.ts +6 -6
  83. package/src/route-map-builder.ts +56 -49
  84. package/src/router/handler-context.ts +13 -5
  85. package/src/router/lazy-includes.ts +1 -0
  86. package/src/router/match-api.ts +8 -4
  87. package/src/router/match-handlers.ts +44 -6
  88. package/src/router/match-middleware/cache-lookup.ts +10 -3
  89. package/src/router/prerender-match.ts +19 -2
  90. package/src/router/router-interfaces.ts +4 -0
  91. package/src/router/segment-resolution/static-store.ts +36 -10
  92. package/src/router/segment-resolution/view-transition-default.ts +9 -5
  93. package/src/router/transition-when.ts +76 -0
  94. package/src/router.ts +34 -3
  95. package/src/rsc/handler-context.ts +1 -0
  96. package/src/rsc/handler.ts +3 -1
  97. package/src/rsc/loader-fetch.ts +2 -2
  98. package/src/rsc/manifest-init.ts +4 -11
  99. package/src/rsc/progressive-enhancement.ts +9 -14
  100. package/src/rsc/render-pipeline.ts +10 -2
  101. package/src/rsc/rsc-rendering.ts +319 -147
  102. package/src/rsc/shell-capture.ts +34 -11
  103. package/src/rsc/shell-serve.ts +3 -0
  104. package/src/rsc/transition-gate.ts +37 -40
  105. package/src/rsc/types.ts +2 -0
  106. package/src/server/context.ts +28 -0
  107. package/src/server/request-context.ts +38 -10
  108. package/src/server.ts +0 -2
  109. package/src/testing/dispatch.ts +1 -0
  110. package/src/testing/e2e/index.ts +5 -0
  111. package/src/testing/index.ts +9 -1
  112. package/src/testing/run-transition-when.ts +42 -9
  113. package/src/testing/shell-status.ts +92 -3
  114. package/src/types/route-entry.ts +3 -0
  115. package/src/types/segments.ts +27 -22
  116. package/src/urls/include-helper.ts +1 -0
  117. package/src/urls/path-helper-types.ts +4 -3
  118. package/src/urls/path-helper.ts +8 -4
  119. package/src/vite/discovery/bundle-postprocess.ts +10 -7
  120. package/src/vite/discovery/discover-routers.ts +7 -66
  121. package/src/vite/discovery/prerender-collection.ts +13 -2
  122. package/src/vite/discovery/state.ts +4 -4
  123. package/src/vite/discovery/virtual-module-codegen.ts +75 -42
  124. package/src/vite/plugins/version-injector.ts +0 -1
  125. package/src/vite/plugins/virtual-entries.ts +11 -1
  126. package/src/vite/router-discovery.ts +132 -22
  127. package/src/vite/utils/manifest-utils.ts +1 -4
  128. package/src/vite/utils/shared-utils.ts +7 -0
@@ -2309,7 +2309,17 @@ async function initializeApp() {
2309
2309
  // context, including strictMode (default true) from createRouter. StrictMode
2310
2310
  // is the default; createRouter({ strictMode: false }) ships the opt-out in the
2311
2311
  // payload metadata. StrictMode emits no DOM, so toggling never changes markup.
2312
- const { strictMode } = await initBrowserApp({ rscStream, deps });
2312
+ const { strictMode, initialPayload } = await initBrowserApp({ rscStream, deps });
2313
+
2314
+ if (import.meta.hot) {
2315
+ const { startDevDiscoveryHandshake } = await import(
2316
+ "@rangojs/router/internal/browser/dev-discovery"
2317
+ );
2318
+ startDevDiscoveryHandshake(
2319
+ initialPayload.metadata?.devDiscoveryEpoch,
2320
+ import.meta.hot
2321
+ );
2322
+ }
2313
2323
 
2314
2324
  const app = createElement(Rango);
2315
2325
  hydrateRoot(
@@ -2520,7 +2530,7 @@ import { resolve } from "node:path";
2520
2530
  // package.json
2521
2531
  var package_default = {
2522
2532
  name: "@rangojs/router",
2523
- version: "0.1.0",
2533
+ version: "0.2.0",
2524
2534
  description: "Django-inspired RSC router with composable URL patterns",
2525
2535
  keywords: [
2526
2536
  "react",
@@ -2620,6 +2630,10 @@ var package_default = {
2620
2630
  "react-server": "./src/rsc/handler.ts",
2621
2631
  default: "./src/rsc/handler.ts"
2622
2632
  },
2633
+ "./internal/browser/dev-discovery": {
2634
+ types: "./dist/types/browser/dev-discovery.d.ts",
2635
+ default: "./src/browser/dev-discovery.ts"
2636
+ },
2623
2637
  "./cache": {
2624
2638
  types: "./dist/types/cache/index.d.ts",
2625
2639
  "react-server": "./src/cache/index.ts",
@@ -2699,7 +2713,8 @@ var package_default = {
2699
2713
  typecheck: "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
2700
2714
  test: "playwright test",
2701
2715
  "test:ui": "playwright test --ui",
2702
- "test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
2716
+ "test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
2717
+ "test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
2703
2718
  "test:unit": "pnpm run build:types && vitest run",
2704
2719
  "test:unit:watch": "vitest",
2705
2720
  "test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
@@ -4236,6 +4251,11 @@ function onwarn(warning, defaultHandler) {
4236
4251
  }
4237
4252
  function getManualChunks(id) {
4238
4253
  const normalized = Vite.normalizePath(id);
4254
+ if (/\/browser\/prefetch\/(?:runtime|fetch|queue|observer|policy|resource-ready)\.[cm]?[jt]sx?(?:[?#].*)?$/.test(
4255
+ normalized
4256
+ )) {
4257
+ return void 0;
4258
+ }
4239
4259
  if (normalized.includes("node_modules/react/") || normalized.includes("node_modules/react-dom/") || normalized.includes("node_modules/react-server-dom-webpack/") || normalized.includes("node_modules/@vitejs/plugin-rsc/")) {
4240
4260
  return "react";
4241
4261
  }
@@ -4673,6 +4693,12 @@ import { readFileSync as readFileSync7 } from "node:fs";
4673
4693
  import { createRequire as createRequire3, register } from "node:module";
4674
4694
  import { pathToFileURL as pathToFileURL2 } from "node:url";
4675
4695
 
4696
+ // src/dev-discovery-protocol.ts
4697
+ var DEV_DISCOVERY_READY_EVENT = "rango:dev-discovery-ready";
4698
+ var DEV_DISCOVERY_QUERY_EVENT = "rango:dev-discovery-query";
4699
+ var DEV_DISCOVERY_PROBE_HEADER = "x-rango-dev-discovery-probe";
4700
+ var DEV_DISCOVERY_EPOCH_HEADER = "x-rango-dev-discovery-epoch";
4701
+
4676
4702
  // src/rsc/shell-serve.ts
4677
4703
  import React from "react";
4678
4704
 
@@ -4955,8 +4981,6 @@ function createDiscoveryState(entryPath, opts) {
4955
4981
  opts,
4956
4982
  mergedRouteManifest: null,
4957
4983
  perRouterManifests: [],
4958
- mergedPrecomputedEntries: null,
4959
- mergedRouteTrie: null,
4960
4984
  perRouterTrieMap: /* @__PURE__ */ new Map(),
4961
4985
  perRouterPrecomputedMap: /* @__PURE__ */ new Map(),
4962
4986
  perRouterManifestDataMap: /* @__PURE__ */ new Map(),
@@ -4971,6 +4995,7 @@ function createDiscoveryState(entryPath, opts) {
4971
4995
  resolvedPrerenderModules: void 0,
4972
4996
  resolvedStaticModules: void 0,
4973
4997
  discoveryDone: null,
4998
+ devDiscoveryEpoch: opts?.preset === "cloudflare" ? Date.now() : void 0,
4974
4999
  devServerOrigin: null,
4975
5000
  devServer: null,
4976
5001
  selfWrittenGenFiles: /* @__PURE__ */ new Map(),
@@ -5887,15 +5912,21 @@ async function renderStaticHandlers(state, rscEnv, registry) {
5887
5912
  if (def.options?.passthrough) continue;
5888
5913
  const startHandler = performance.now();
5889
5914
  let handled = false;
5890
- for (const [, routerInstance] of registry) {
5915
+ const owner = def.$$routerId;
5916
+ const owned = owner !== void 0 ? registry.get(owner) : void 0;
5917
+ const candidates = owned ? [owned] : [...registry.values()];
5918
+ for (const routerInstance of candidates) {
5891
5919
  if (!routerInstance.renderStaticSegment) continue;
5892
5920
  try {
5893
5921
  const result = await routerInstance.renderStaticSegment(
5894
5922
  def.handler,
5895
5923
  def.$$id,
5896
- def.$$routePrefix,
5924
+ // Full route name when stamped; the historical $$routePrefix is a
5925
+ // name PREFIX (never in the root-scope registry) kept as fallback.
5926
+ def.$$routeName ?? def.$$routePrefix,
5897
5927
  state.resolvedBuildEnv,
5898
- !state.isBuildMode
5928
+ !state.isBuildMode,
5929
+ def.$$rootScoped
5899
5930
  );
5900
5931
  if (result) {
5901
5932
  const hasHandles = result.handles !== "";
@@ -6107,12 +6138,10 @@ async function discoverRouters(state, rscEnv) {
6107
6138
  throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
6108
6139
  }
6109
6140
  const newMergedRouteManifest = {};
6110
- const newMergedPrecomputedEntries = [];
6111
6141
  const newPerRouterManifests = [];
6112
6142
  const newPerRouterManifestDataMap = /* @__PURE__ */ new Map();
6113
6143
  const newPerRouterPrecomputedMap = /* @__PURE__ */ new Map();
6114
6144
  const newPerRouterTrieMap = /* @__PURE__ */ new Map();
6115
- let mergedRouteTrailingSlash = {};
6116
6145
  let routerMountIndex = 0;
6117
6146
  const allManifests = [];
6118
6147
  const clientChunkCtx = state.opts?.clientChunkCtx;
@@ -6133,6 +6162,7 @@ async function discoverRouters(state, rscEnv) {
6133
6162
  router.urlpatterns,
6134
6163
  routerMountIndex,
6135
6164
  {
6165
+ routerId: id,
6136
6166
  ...router.__basename ? { urlPrefix: router.__basename } : {},
6137
6167
  ...collectClientFallbackRef ? { collectClientFallbackRef } : {}
6138
6168
  }
@@ -6170,20 +6200,17 @@ async function discoverRouters(state, rscEnv) {
6170
6200
  newPerRouterManifests.push({
6171
6201
  id,
6172
6202
  routeManifest: manifest.routeManifest,
6203
+ routeTrailingSlash: manifest.routeTrailingSlash,
6173
6204
  routeSearchSchemas: manifest.routeSearchSchemas,
6174
6205
  sourceFile: router.__sourceFile,
6175
6206
  factoryOnlyPrefixes
6176
6207
  });
6177
- if (manifest.routeTrailingSlash) {
6178
- Object.assign(mergedRouteTrailingSlash, manifest.routeTrailingSlash);
6179
- }
6180
6208
  const routerPrecomputed = [];
6181
6209
  flattenLeafEntries(
6182
6210
  manifest.prefixTree,
6183
6211
  manifest.routeManifest,
6184
6212
  routerPrecomputed
6185
6213
  );
6186
- newMergedPrecomputedEntries.push(...routerPrecomputed);
6187
6214
  newPerRouterManifestDataMap.set(id, manifest.routeManifest);
6188
6215
  newPerRouterPrecomputedMap.set(id, routerPrecomputed);
6189
6216
  console.log(
@@ -6205,44 +6232,8 @@ async function discoverRouters(state, rscEnv) {
6205
6232
  allManifests.length,
6206
6233
  (performance.now() - manifestGenStart).toFixed(1)
6207
6234
  );
6208
- let newMergedRouteTrie = null;
6209
6235
  const trieStart = debug10 ? performance.now() : 0;
6210
6236
  if (Object.keys(newMergedRouteManifest).length > 0) {
6211
- const routeToStaticPrefix = {};
6212
- for (const { manifest } of allManifests) {
6213
- for (const name of Object.keys(manifest.routeManifest)) {
6214
- if (!(name in routeToStaticPrefix)) {
6215
- routeToStaticPrefix[name] = "";
6216
- }
6217
- }
6218
- buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
6219
- }
6220
- const prerenderRouteNames = /* @__PURE__ */ new Set();
6221
- const passthroughRouteNames = /* @__PURE__ */ new Set();
6222
- const mergedResponseTypeRoutes = {};
6223
- for (const { manifest } of allManifests) {
6224
- if (manifest.prerenderRoutes) {
6225
- for (const name of manifest.prerenderRoutes) {
6226
- prerenderRouteNames.add(name);
6227
- }
6228
- }
6229
- if (manifest.passthroughRoutes) {
6230
- for (const name of manifest.passthroughRoutes) {
6231
- passthroughRouteNames.add(name);
6232
- }
6233
- }
6234
- if (manifest.responseTypeRoutes) {
6235
- Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
6236
- }
6237
- }
6238
- newMergedRouteTrie = buildRouteTrie(
6239
- newMergedRouteManifest,
6240
- routeToStaticPrefix,
6241
- mergedRouteTrailingSlash,
6242
- prerenderRouteNames,
6243
- passthroughRouteNames,
6244
- mergedResponseTypeRoutes
6245
- );
6246
6237
  for (const { id, manifest } of allManifests) {
6247
6238
  const perRouterTrie = buildPerRouterTrie(manifest);
6248
6239
  if (perRouterTrie) {
@@ -6255,15 +6246,10 @@ async function discoverRouters(state, rscEnv) {
6255
6246
  (performance.now() - trieStart).toFixed(1)
6256
6247
  );
6257
6248
  state.mergedRouteManifest = newMergedRouteManifest;
6258
- state.mergedPrecomputedEntries = newMergedPrecomputedEntries;
6259
6249
  state.perRouterManifests = newPerRouterManifests;
6260
6250
  state.perRouterManifestDataMap = newPerRouterManifestDataMap;
6261
6251
  state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
6262
6252
  state.perRouterTrieMap = newPerRouterTrieMap;
6263
- state.mergedRouteTrie = newMergedRouteTrie;
6264
- if (serverMod.setRouteTrie && newMergedRouteTrie) {
6265
- serverMod.setRouteTrie(newMergedRouteTrie);
6266
- }
6267
6253
  const perRouterSetters = [
6268
6254
  [newPerRouterManifestDataMap, "setRouterManifest"],
6269
6255
  [newPerRouterTrieMap, "setRouterTrie"],
@@ -6696,12 +6682,18 @@ function supplementGenFilesWithRuntimeRoutes(state) {
6696
6682
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "node:fs";
6697
6683
  import { dirname as dirname4, basename, join as join6 } from "node:path";
6698
6684
  var MANIFEST_EXTERNALIZE_THRESHOLD = 512 * 1024;
6685
+ function devDiscoveryBootstrap(state) {
6686
+ if (state.isBuildMode || state.opts?.preset !== "cloudflare" || state.devDiscoveryEpoch === void 0) {
6687
+ return [];
6688
+ }
6689
+ const epoch = state.devDiscoveryEpoch;
6690
+ return [`globalThis.__RANGO_DEV_DISCOVERY_EPOCH = ${epoch};`];
6691
+ }
6699
6692
  function generateRoutesManifestModule(state) {
6700
6693
  const hasManifest = state.mergedRouteManifest && Object.keys(state.mergedRouteManifest).length > 0;
6701
6694
  if (hasManifest) {
6702
6695
  const genFileImports = [];
6703
- const genFileVars = [];
6704
- const routersWithoutGenFile = [];
6696
+ const genFileVarByEntry = [];
6705
6697
  let varIdx = 0;
6706
6698
  for (const entry of state.perRouterManifests) {
6707
6699
  if (entry.sourceFile) {
@@ -6718,16 +6710,19 @@ function generateRoutesManifestModule(state) {
6718
6710
  genFileImports.push(
6719
6711
  `import { NamedRoutes as ${varName} } from ${JSON.stringify(genPath)};`
6720
6712
  );
6721
- genFileVars.push(varName);
6713
+ genFileVarByEntry.push(varName);
6722
6714
  } else {
6723
- routersWithoutGenFile.push({
6724
- id: entry.id,
6725
- manifest: entry.routeManifest
6726
- });
6715
+ genFileVarByEntry.push(null);
6727
6716
  }
6728
6717
  }
6729
- const lines = [
6730
- `import { setCachedManifest, setRouterManifest, registerRouterManifestLoader, clearAllRouterData } from "@rangojs/router/server";`,
6718
+ const serverImports = [
6719
+ "setCachedManifest",
6720
+ "setRouterManifest",
6721
+ ...state.isBuildMode ? ["registerRouterManifestLoader"] : [],
6722
+ "clearAllRouterData"
6723
+ ];
6724
+ const lines2 = [
6725
+ `import { ${serverImports.join(", ")} } from "@rangojs/router/server";`,
6731
6726
  ...genFileImports,
6732
6727
  // Clear stale per-router cached data (manifest, trie, precomputed entries)
6733
6728
  // before re-populating. In Cloudflare dev mode, program reloads re-evaluate
@@ -6736,53 +6731,56 @@ function generateRoutesManifestModule(state) {
6736
6731
  // handler finds stale trie data and never rebuilds from updated urlpatterns.
6737
6732
  `clearAllRouterData();`
6738
6733
  ];
6739
- if (genFileVars.length > 0) {
6740
- lines.push(
6734
+ if (varIdx > 0) {
6735
+ lines2.push(
6741
6736
  `function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`
6742
6737
  );
6743
6738
  }
6744
- if (genFileVars.length === 1 && routersWithoutGenFile.length === 0) {
6745
- lines.push(`setCachedManifest(__flat(${genFileVars[0]}));`);
6739
+ const routerMapVars = [];
6740
+ for (const [i, entry] of state.perRouterManifests.entries()) {
6741
+ const mapVar = `__m${i}`;
6742
+ const genVar = genFileVarByEntry[i];
6743
+ const init = genVar ? `__flat(${genVar})` : jsonParseExpression(entry.routeManifest);
6744
+ lines2.push(`const ${mapVar} = ${init};`);
6745
+ routerMapVars.push(mapVar);
6746
+ }
6747
+ if (routerMapVars.length === 1) {
6748
+ lines2.push(`setCachedManifest(${routerMapVars[0]});`);
6746
6749
  } else {
6747
- const parts = [];
6748
- for (const v of genFileVars) parts.push(`...__flat(${v})`);
6749
- for (const { manifest } of routersWithoutGenFile)
6750
- parts.push(`...${jsonParseExpression(manifest)}`);
6751
- lines.push(`setCachedManifest({ ${parts.join(", ")} });`);
6750
+ lines2.push(
6751
+ `setCachedManifest({ ${routerMapVars.map((v) => `...${v}`).join(", ")} });`
6752
+ );
6752
6753
  }
6753
- let genVarIdx = 0;
6754
- for (const entry of state.perRouterManifests) {
6755
- if (entry.sourceFile) {
6756
- const varName = genFileVars[genVarIdx++];
6757
- lines.push(
6758
- `setRouterManifest(${JSON.stringify(entry.id)}, __flat(${varName}));`
6759
- );
6760
- } else {
6761
- lines.push(
6762
- `setRouterManifest(${JSON.stringify(entry.id)}, ${jsonParseExpression(entry.routeManifest)});`
6754
+ for (const [i, entry] of state.perRouterManifests.entries()) {
6755
+ lines2.push(
6756
+ `setRouterManifest(${JSON.stringify(entry.id)}, ${routerMapVars[i]});`
6757
+ );
6758
+ }
6759
+ if (state.isBuildMode) {
6760
+ for (const routerId of state.perRouterManifestDataMap.keys()) {
6761
+ lines2.push(
6762
+ `registerRouterManifestLoader(${JSON.stringify(routerId)}, () => import(${JSON.stringify(VIRTUAL_ROUTES_MANIFEST_ID + "/" + routerId)}));`
6763
6763
  );
6764
6764
  }
6765
6765
  }
6766
- for (const routerId of state.perRouterManifestDataMap.keys()) {
6767
- lines.push(
6768
- `registerRouterManifestLoader(${JSON.stringify(routerId)}, () => import(${JSON.stringify(VIRTUAL_ROUTES_MANIFEST_ID + "/" + routerId)}));`
6769
- );
6770
- }
6771
6766
  if (!state.isBuildMode && state.devServerOrigin) {
6772
- lines.push(
6767
+ lines2.push(
6773
6768
  `globalThis.__PRERENDER_DEV_URL = ${JSON.stringify(state.devServerOrigin)};`
6774
6769
  );
6775
6770
  }
6776
- return lines.join("\n");
6771
+ lines2.push(...devDiscoveryBootstrap(state));
6772
+ return lines2.join("\n");
6777
6773
  }
6774
+ const lines = [];
6778
6775
  if (!state.isBuildMode) {
6779
6776
  const origin = state.devServerOrigin || state.devServer?.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || state.devServer && `http://localhost:${state.devServer.config.server.port || 5173}`;
6780
6777
  if (origin) {
6781
6778
  state.devServerOrigin = origin;
6782
- return `globalThis.__PRERENDER_DEV_URL = ${JSON.stringify(origin)};`;
6779
+ lines.push(`globalThis.__PRERENDER_DEV_URL = ${JSON.stringify(origin)};`);
6783
6780
  }
6784
6781
  }
6785
- return `// Route manifest will be populated at runtime`;
6782
+ lines.push(...devDiscoveryBootstrap(state));
6783
+ return lines.join("\n") || `// Route manifest will be populated at runtime`;
6786
6784
  }
6787
6785
  function shortHash(input) {
6788
6786
  let h = 2166136261;
@@ -6965,7 +6963,7 @@ function postprocessBundle(state) {
6965
6963
  );
6966
6964
  writeFileSync6(manifestPath, manifestCode);
6967
6965
  totalBytes += Buffer.byteLength(manifestCode);
6968
- const injection = `import "./__static-manifest.js";
6966
+ const injection = `globalThis.__loadStaticManifestModule = () => import("./__static-manifest.js");
6969
6967
  `;
6970
6968
  writeFileSync6(rscEntryPath, injection + rscCode);
6971
6969
  const totalKB = (totalBytes / 1024).toFixed(1);
@@ -7300,6 +7298,43 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7300
7298
  if (s.isBuildMode) return;
7301
7299
  if (globalThis.__rscRouterDiscoveryActive) return;
7302
7300
  s.devServer = server;
7301
+ let workerReadyEpoch;
7302
+ const publishDevDiscoveryReady = (epoch) => {
7303
+ if (epoch <= (workerReadyEpoch ?? -1)) return;
7304
+ workerReadyEpoch = epoch;
7305
+ server.environments?.client?.hot?.send({
7306
+ type: "custom",
7307
+ event: DEV_DISCOVERY_READY_EVENT,
7308
+ data: { epoch }
7309
+ });
7310
+ debugDiscovery?.("hmr: workerd ready at epoch %d", epoch);
7311
+ };
7312
+ if (opts?.preset === "cloudflare") {
7313
+ const registeredClientHotChannels = /* @__PURE__ */ new Set();
7314
+ const registerDevDiscoveryHotChannels = () => {
7315
+ const clientHot = server.environments?.client?.hot;
7316
+ if (clientHot && !registeredClientHotChannels.has(clientHot)) {
7317
+ registeredClientHotChannels.add(clientHot);
7318
+ clientHot.on(
7319
+ DEV_DISCOVERY_QUERY_EVENT,
7320
+ (_payload, client) => {
7321
+ if (workerReadyEpoch === void 0) return;
7322
+ client.send(DEV_DISCOVERY_READY_EVENT, {
7323
+ epoch: workerReadyEpoch
7324
+ });
7325
+ }
7326
+ );
7327
+ }
7328
+ debugDiscovery?.(
7329
+ "hmr: dev discovery browser channel registered (client=%s)",
7330
+ !!clientHot
7331
+ );
7332
+ };
7333
+ registerDevDiscoveryHotChannels();
7334
+ if (server.httpServer && !server.httpServer.listening) {
7335
+ server.httpServer.once("listening", registerDevDiscoveryHotChannels);
7336
+ }
7337
+ }
7303
7338
  server.middlewares.use(internalDebugNoCacheMiddleware());
7304
7339
  let resolveDiscovery;
7305
7340
  const discoveryPromise = new Promise((resolve13) => {
@@ -7309,9 +7344,11 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7309
7344
  const beginDiscoveryGate = gate.beginGate;
7310
7345
  const resolveDiscoveryGate = gate.resolveGate;
7311
7346
  const getDevServerOrigin = () => server.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || `http://localhost:${server.config.server.port || 5173}`;
7347
+ let devServerClosed = false;
7312
7348
  let prerenderTempServer = null;
7313
7349
  let prerenderNodeRegistry = null;
7314
7350
  server.httpServer?.on("close", () => {
7351
+ devServerClosed = true;
7315
7352
  if (prerenderTempServer) {
7316
7353
  prerenderTempServer.close().catch(() => {
7317
7354
  });
@@ -7587,12 +7624,6 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7587
7624
  if (s.mergedRouteManifest && serverMod.setCachedManifest) {
7588
7625
  serverMod.setCachedManifest(s.mergedRouteManifest);
7589
7626
  }
7590
- if (s.mergedPrecomputedEntries && s.mergedPrecomputedEntries.length > 0 && serverMod.setPrecomputedEntries) {
7591
- serverMod.setPrecomputedEntries(s.mergedPrecomputedEntries);
7592
- }
7593
- if (s.mergedRouteTrie && serverMod.setRouteTrie) {
7594
- serverMod.setRouteTrie(s.mergedRouteTrie);
7595
- }
7596
7627
  const perRouterSetters = [
7597
7628
  [s.perRouterManifestDataMap, "setRouterManifest"],
7598
7629
  [s.perRouterTrieMap, "setRouterTrie"],
@@ -7928,6 +7959,21 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7928
7959
  writeCombinedRouteTypesWithTracking(s);
7929
7960
  }
7930
7961
  };
7962
+ const routeShapeSignature = () => JSON.stringify(
7963
+ s.perRouterManifests.map(
7964
+ ({
7965
+ id,
7966
+ routeManifest,
7967
+ routeTrailingSlash,
7968
+ routeSearchSchemas
7969
+ }) => ({
7970
+ id,
7971
+ routeManifest,
7972
+ routeTrailingSlash,
7973
+ routeSearchSchemas
7974
+ })
7975
+ )
7976
+ );
7931
7977
  const maybeHandleGeneratedRouteFileMutation = (filePath) => {
7932
7978
  if (!isGeneratedRouteFile(filePath)) return false;
7933
7979
  if (consumeSelfGenWrite(s, filePath)) return true;
@@ -7941,6 +7987,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7941
7987
  const rscEnv = server.environments?.rsc;
7942
7988
  const hasMainRunner = !!rscEnv?.runner;
7943
7989
  if (!hasMainRunner && s.perRouterManifests.length === 0) return;
7990
+ let previousRouteShape = routeShapeSignature();
7944
7991
  await gate.runRefreshCycle(async () => {
7945
7992
  const hmrStart = performance.now();
7946
7993
  try {
@@ -7989,7 +8036,19 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7989
8036
  );
7990
8037
  s.lastDiscoveryError = null;
7991
8038
  }
7992
- if (rscEnv && !rscEnv.runner) forceCloudflareWorkerReload(rscEnv);
8039
+ if (rscEnv && !rscEnv.runner) {
8040
+ const nextRouteShape = routeShapeSignature();
8041
+ let expectedEpoch;
8042
+ if (nextRouteShape !== previousRouteShape) {
8043
+ expectedEpoch = Math.max(
8044
+ Date.now(),
8045
+ (s.devDiscoveryEpoch ?? 0) + 1
8046
+ );
8047
+ s.devDiscoveryEpoch = expectedEpoch;
8048
+ }
8049
+ previousRouteShape = nextRouteShape;
8050
+ forceCloudflareWorkerReload(rscEnv, expectedEpoch);
8051
+ }
7993
8052
  } catch (err) {
7994
8053
  s.lastDiscoveryError = {
7995
8054
  message: err?.message ?? String(err),
@@ -8010,10 +8069,10 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
8010
8069
  }
8011
8070
  });
8012
8071
  };
8013
- const forceCloudflareWorkerReload = (rscEnv) => {
8072
+ const forceCloudflareWorkerReload = (rscEnv, expectedEpoch) => {
8014
8073
  if (!rscEnv?.hot) return;
8015
- try {
8016
- const graph = rscEnv.moduleGraph;
8074
+ const graph = rscEnv.moduleGraph;
8075
+ const reloadWorkerd = () => {
8017
8076
  if (graph?.invalidateAll) {
8018
8077
  graph.invalidateAll();
8019
8078
  debugDiscovery?.("hmr: invalidated workerd rsc module graph");
@@ -8022,12 +8081,40 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
8022
8081
  debugDiscovery?.(
8023
8082
  "hmr: forced workerd rsc env reload (full-reload)"
8024
8083
  );
8025
- } catch (err) {
8084
+ };
8085
+ reloadWorkerd();
8086
+ if (expectedEpoch === void 0) return;
8087
+ void (async () => {
8088
+ const deadline = Date.now() + 15e3;
8089
+ do {
8090
+ if (devServerClosed || expectedEpoch !== s.devDiscoveryEpoch) {
8091
+ return;
8092
+ }
8093
+ await new Promise((resolve13) => setTimeout(resolve13, 25));
8094
+ if (devServerClosed || expectedEpoch !== s.devDiscoveryEpoch) {
8095
+ return;
8096
+ }
8097
+ try {
8098
+ const response = await fetch(getDevServerOrigin() + "/", {
8099
+ cache: "no-store",
8100
+ headers: {
8101
+ [DEV_DISCOVERY_PROBE_HEADER]: String(expectedEpoch)
8102
+ },
8103
+ signal: AbortSignal.timeout(1e3)
8104
+ });
8105
+ if (!devServerClosed && expectedEpoch === s.devDiscoveryEpoch && response.headers.get(DEV_DISCOVERY_EPOCH_HEADER) === String(expectedEpoch)) {
8106
+ publishDevDiscoveryReady(expectedEpoch);
8107
+ return;
8108
+ }
8109
+ reloadWorkerd();
8110
+ } catch {
8111
+ }
8112
+ } while (Date.now() < deadline);
8026
8113
  debugDiscovery?.(
8027
- "hmr: workerd reload failed: %s",
8028
- err?.message ?? err
8114
+ "hmr: workerd readiness probe timed out at epoch %d",
8115
+ expectedEpoch
8029
8116
  );
8030
- }
8117
+ })();
8031
8118
  };
8032
8119
  const scheduleRouteRegeneration = () => {
8033
8120
  clearTimeout(routeChangeTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -100,6 +100,10 @@
100
100
  "react-server": "./src/rsc/handler.ts",
101
101
  "default": "./src/rsc/handler.ts"
102
102
  },
103
+ "./internal/browser/dev-discovery": {
104
+ "types": "./dist/types/browser/dev-discovery.d.ts",
105
+ "default": "./src/browser/dev-discovery.ts"
106
+ },
103
107
  "./cache": {
104
108
  "types": "./dist/types/cache/index.d.ts",
105
109
  "react-server": "./src/cache/index.ts",
@@ -172,18 +176,6 @@
172
176
  "access": "public",
173
177
  "tag": "latest"
174
178
  },
175
- "scripts": {
176
- "build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
177
- "build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
178
- "prepublishOnly": "pnpm build",
179
- "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
180
- "test": "playwright test",
181
- "test:ui": "playwright test --ui",
182
- "test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
183
- "test:unit": "pnpm run build:types && vitest run",
184
- "test:unit:watch": "vitest",
185
- "test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
186
- },
187
179
  "dependencies": {
188
180
  "@types/debug": "^4.1.12",
189
181
  "@vitejs/plugin-rsc": "^0.5.27",
@@ -199,19 +191,19 @@
199
191
  "@opentelemetry/context-async-hooks": "^2.9.0",
200
192
  "@opentelemetry/sdk-trace-base": "^2.9.0",
201
193
  "@playwright/test": "^1.49.1",
202
- "@shared/e2e": "workspace:*",
203
194
  "@testing-library/dom": "^10.4.1",
204
195
  "@testing-library/react": "^16.3.2",
205
196
  "@types/node": "^24.10.1",
206
- "@types/react": "catalog:",
207
- "@types/react-dom": "catalog:",
197
+ "@types/react": "^19.2.7",
198
+ "@types/react-dom": "^19.2.3",
208
199
  "esbuild": "^0.28.1",
209
200
  "happy-dom": "^20.10.1",
210
201
  "jiti": "^2.7.0",
211
- "react": "catalog:",
212
- "react-dom": "catalog:",
202
+ "react": "^19.2.6",
203
+ "react-dom": "^19.2.6",
213
204
  "typescript": "^5.3.0",
214
- "vitest": "^4.1.9"
205
+ "vitest": "^4.1.9",
206
+ "@shared/e2e": "0.0.1"
215
207
  },
216
208
  "peerDependencies": {
217
209
  "@cloudflare/vite-plugin": "^1.42.1",
@@ -250,5 +242,17 @@
250
242
  },
251
243
  "engines": {
252
244
  "node": ">=24.0.0"
245
+ },
246
+ "scripts": {
247
+ "build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
248
+ "build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
249
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
250
+ "test": "playwright test",
251
+ "test:ui": "playwright test --ui",
252
+ "test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
253
+ "test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
254
+ "test:unit": "pnpm run build:types && vitest run",
255
+ "test:unit:watch": "vitest",
256
+ "test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
253
257
  }
254
- }
258
+ }
@@ -131,8 +131,10 @@ recompute for a merely-aging entry.
131
131
  layer.
132
132
  - **Client forward/back** is SWR after a mutation — see "Correctness &
133
133
  invalidation" → Client cache.
134
- - **Edge / document layer** uses the HTTP `stale-while-revalidate` directive; see
135
- `/document-cache`.
134
+ - **Store-backed document layer** uses the HTTP `stale-while-revalidate`
135
+ directive as policy for `createDocumentCacheMiddleware`; see
136
+ `/document-cache`. A platform CDN may independently consume the same header
137
+ and bypass the function on hits; see `/deployment-caching`.
136
138
 
137
139
  SWR softens normal TTL expiry, **not** a cross-deploy cold cache — a new build
138
140
  has no stale entry to serve (see version-segmented store keys above).
@@ -485,5 +487,6 @@ overrides — see `/loader` for the full reference.
485
487
 
486
488
  - `/caching` — cache() DSL setup, stores, nested boundaries
487
489
  - `/use-cache` — "use cache" directive details, profiles, transforms, guards
488
- - `/document-cache` — Edge caching with Cache-Control headers (different layer)
490
+ - `/document-cache` — store-backed complete-response middleware
491
+ - `/deployment-caching` — in-function versus external CDN cache boundaries
489
492
  - `/ppr` — PPR shell caching: cached HTML shell + live loader holes (different layer)
@@ -171,7 +171,7 @@ export async function POST() {
171
171
  | `updateTag(...tags)` | awaitable (`Promise<void>`) | server actions | immediate; next read is fresh |
172
172
  | `revalidateTag(...tags)` | background (`void`) | route handlers / webhooks | background (non-blocking); next read re-renders fresh |
173
173
 
174
- Both built-in stores support tags. For `CFCacheStore`, distributed (cross-colo)
174
+ All three built-in stores support tags. For `CFCacheStore`, distributed (cross-colo)
175
175
  invalidation requires a `kv` namespace — the tag-invalidation markers live in
176
176
  that same namespace; there is **no** separate tag-invalidation store to wire.
177
177
  If no tag-capable store is configured, `updateTag`/`revalidateTag` warn and no-op.
@@ -69,9 +69,15 @@
69
69
  "argumentHint": "",
70
70
  "path": "skills/defer-hydration/SKILL.md"
71
71
  },
72
+ {
73
+ "name": "deployment-caching",
74
+ "description": "Choose the deployment cache boundary for a Rango app: in-function segment/prerender/PPR caches, store-backed whole-response caching, or an external CDN cache. Use when comparing Cloudflare, Vercel, and Node deployments; deciding whether Cache-Control can reduce origin work; or reasoning about middleware, live loaders, PPR, and CDN behavior.",
75
+ "argumentHint": "[cloudflare|vercel|node]",
76
+ "path": "skills/deployment-caching/SKILL.md"
77
+ },
72
78
  {
73
79
  "name": "document-cache",
74
- "description": "Cache the whole HTTP response at the edge with Cache-Control headers. Use when caching an entire page or response at a CDN edge, setting Cache-Control headers, or cutting origin hits for public pages not for caching a single segment or function.",
80
+ "description": "Cache complete HTTP responses in Rango's configured app store with createDocumentCacheMiddleware, using Cache-Control s-maxage as policy. Use when reusing a whole HTML/RSC response, comparing the store-backed middleware with a platform CDN cache, or deciding whether full-response caching is safe.",
75
81
  "argumentHint": "[setup]",
76
82
  "path": "skills/document-cache/SKILL.md"
77
83
  },