@rangojs/router 0.0.0-experimental.150 → 0.0.0-experimental.151

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.
@@ -2520,7 +2520,7 @@ import { resolve } from "node:path";
2520
2520
  // package.json
2521
2521
  var package_default = {
2522
2522
  name: "@rangojs/router",
2523
- version: "0.0.0-experimental.150",
2523
+ version: "0.0.0-experimental.151",
2524
2524
  description: "Django-inspired RSC router with composable URL patterns",
2525
2525
  keywords: [
2526
2526
  "react",
@@ -2768,7 +2768,7 @@ var package_default = {
2768
2768
  }
2769
2769
  },
2770
2770
  engines: {
2771
- node: "^20.19.0 || >=22.12.0"
2771
+ node: ">=24.0.0"
2772
2772
  }
2773
2773
  };
2774
2774
 
@@ -5140,10 +5140,9 @@ function buildRouteToStaticPrefix(prefixTree, result) {
5140
5140
  }
5141
5141
 
5142
5142
  // src/build/route-trie.ts
5143
- function buildRouteTrie(routeManifest, routeAncestry, routeToStaticPrefix, routeTrailingSlash, prerenderRouteNames, passthroughRouteNames, responseTypeRoutes) {
5143
+ function buildRouteTrie(routeManifest, routeToStaticPrefix, routeTrailingSlash, prerenderRouteNames, passthroughRouteNames, responseTypeRoutes) {
5144
5144
  const root = {};
5145
5145
  for (const [routeName, pattern] of Object.entries(routeManifest)) {
5146
- const ancestry = routeAncestry[routeName] || [];
5147
5146
  const staticPrefix = routeToStaticPrefix[routeName] || "";
5148
5147
  const trailingSlash = routeTrailingSlash?.[routeName];
5149
5148
  const responseType = responseTypeRoutes?.[routeName];
@@ -5153,7 +5152,6 @@ function buildRouteTrie(routeManifest, routeAncestry, routeToStaticPrefix, route
5153
5152
  insertRoute(root, segments, 0, {
5154
5153
  n: routeName,
5155
5154
  sp: staticPrefix,
5156
- a: ancestry,
5157
5155
  ...trailingSlash ? { ts: trailingSlash } : {},
5158
5156
  ...prerenderRouteNames?.has(routeName) ? { pr: true } : {},
5159
5157
  ...passthroughRouteNames?.has(routeName) ? { pt: true } : {},
@@ -5186,8 +5184,7 @@ function sortSuffixParams(node) {
5186
5184
  }
5187
5185
  }
5188
5186
  function buildPerRouterTrie(manifest) {
5189
- const ancestry = manifest._routeAncestry;
5190
- if (!ancestry || Object.keys(ancestry).length === 0) {
5187
+ if (Object.keys(manifest.routeManifest).length === 0) {
5191
5188
  return null;
5192
5189
  }
5193
5190
  const routeToStaticPrefix = {};
@@ -5199,7 +5196,6 @@ function buildPerRouterTrie(manifest) {
5199
5196
  }
5200
5197
  return buildRouteTrie(
5201
5198
  manifest.routeManifest,
5202
- ancestry,
5203
5199
  routeToStaticPrefix,
5204
5200
  manifest.routeTrailingSlash,
5205
5201
  manifest.prerenderRoutes ? new Set(manifest.prerenderRoutes) : void 0,
@@ -6114,7 +6110,6 @@ async function discoverRouters(state, rscEnv) {
6114
6110
  const newPerRouterManifestDataMap = /* @__PURE__ */ new Map();
6115
6111
  const newPerRouterPrecomputedMap = /* @__PURE__ */ new Map();
6116
6112
  const newPerRouterTrieMap = /* @__PURE__ */ new Map();
6117
- let mergedRouteAncestry = {};
6118
6113
  let mergedRouteTrailingSlash = {};
6119
6114
  let routerMountIndex = 0;
6120
6115
  const allManifests = [];
@@ -6177,9 +6172,6 @@ async function discoverRouters(state, rscEnv) {
6177
6172
  sourceFile: router.__sourceFile,
6178
6173
  factoryOnlyPrefixes
6179
6174
  });
6180
- if (manifest._routeAncestry) {
6181
- Object.assign(mergedRouteAncestry, manifest._routeAncestry);
6182
- }
6183
6175
  if (manifest.routeTrailingSlash) {
6184
6176
  Object.assign(mergedRouteTrailingSlash, manifest.routeTrailingSlash);
6185
6177
  }
@@ -6214,49 +6206,46 @@ async function discoverRouters(state, rscEnv) {
6214
6206
  let newMergedRouteTrie = null;
6215
6207
  const trieStart = debug10 ? performance.now() : 0;
6216
6208
  if (Object.keys(newMergedRouteManifest).length > 0) {
6217
- if (mergedRouteAncestry) {
6218
- const routeToStaticPrefix = {};
6219
- for (const { manifest } of allManifests) {
6220
- for (const name of Object.keys(manifest.routeManifest)) {
6221
- if (!(name in routeToStaticPrefix)) {
6222
- routeToStaticPrefix[name] = "";
6223
- }
6224
- }
6225
- buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
6226
- }
6227
- const prerenderRouteNames = /* @__PURE__ */ new Set();
6228
- const passthroughRouteNames = /* @__PURE__ */ new Set();
6229
- const mergedResponseTypeRoutes = {};
6230
- for (const { manifest } of allManifests) {
6231
- if (manifest.prerenderRoutes) {
6232
- for (const name of manifest.prerenderRoutes) {
6233
- prerenderRouteNames.add(name);
6234
- }
6235
- }
6236
- if (manifest.passthroughRoutes) {
6237
- for (const name of manifest.passthroughRoutes) {
6238
- passthroughRouteNames.add(name);
6239
- }
6209
+ const routeToStaticPrefix = {};
6210
+ for (const { manifest } of allManifests) {
6211
+ for (const name of Object.keys(manifest.routeManifest)) {
6212
+ if (!(name in routeToStaticPrefix)) {
6213
+ routeToStaticPrefix[name] = "";
6240
6214
  }
6241
- if (manifest.responseTypeRoutes) {
6242
- Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
6215
+ }
6216
+ buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
6217
+ }
6218
+ const prerenderRouteNames = /* @__PURE__ */ new Set();
6219
+ const passthroughRouteNames = /* @__PURE__ */ new Set();
6220
+ const mergedResponseTypeRoutes = {};
6221
+ for (const { manifest } of allManifests) {
6222
+ if (manifest.prerenderRoutes) {
6223
+ for (const name of manifest.prerenderRoutes) {
6224
+ prerenderRouteNames.add(name);
6243
6225
  }
6244
6226
  }
6245
- newMergedRouteTrie = buildRouteTrie(
6246
- newMergedRouteManifest,
6247
- mergedRouteAncestry,
6248
- routeToStaticPrefix,
6249
- mergedRouteTrailingSlash,
6250
- prerenderRouteNames,
6251
- passthroughRouteNames,
6252
- mergedResponseTypeRoutes
6253
- );
6254
- for (const { id, manifest } of allManifests) {
6255
- const perRouterTrie = buildPerRouterTrie(manifest);
6256
- if (perRouterTrie) {
6257
- newPerRouterTrieMap.set(id, perRouterTrie);
6227
+ if (manifest.passthroughRoutes) {
6228
+ for (const name of manifest.passthroughRoutes) {
6229
+ passthroughRouteNames.add(name);
6258
6230
  }
6259
6231
  }
6232
+ if (manifest.responseTypeRoutes) {
6233
+ Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
6234
+ }
6235
+ }
6236
+ newMergedRouteTrie = buildRouteTrie(
6237
+ newMergedRouteManifest,
6238
+ routeToStaticPrefix,
6239
+ mergedRouteTrailingSlash,
6240
+ prerenderRouteNames,
6241
+ passthroughRouteNames,
6242
+ mergedResponseTypeRoutes
6243
+ );
6244
+ for (const { id, manifest } of allManifests) {
6245
+ const perRouterTrie = buildPerRouterTrie(manifest);
6246
+ if (perRouterTrie) {
6247
+ newPerRouterTrieMap.set(id, perRouterTrie);
6248
+ }
6260
6249
  }
6261
6250
  }
6262
6251
  debug10?.(
@@ -6702,7 +6691,9 @@ function supplementGenFilesWithRuntimeRoutes(state) {
6702
6691
  }
6703
6692
 
6704
6693
  // src/vite/discovery/virtual-module-codegen.ts
6694
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "node:fs";
6705
6695
  import { dirname as dirname4, basename, join as join6 } from "node:path";
6696
+ var MANIFEST_EXTERNALIZE_THRESHOLD = 512 * 1024;
6706
6697
  function generateRoutesManifestModule(state) {
6707
6698
  const hasManifest = state.mergedRouteManifest && Object.keys(state.mergedRouteManifest).length > 0;
6708
6699
  if (hasManifest) {
@@ -6791,6 +6782,14 @@ function generateRoutesManifestModule(state) {
6791
6782
  }
6792
6783
  return `// Route manifest will be populated at runtime`;
6793
6784
  }
6785
+ function shortHash(input) {
6786
+ let h = 2166136261;
6787
+ for (let i = 0; i < input.length; i++) {
6788
+ h ^= input.charCodeAt(i);
6789
+ h = Math.imul(h, 16777619);
6790
+ }
6791
+ return (h >>> 0).toString(36);
6792
+ }
6794
6793
  function generatePerRouterModule(state, routerId) {
6795
6794
  const routerEntry = state.perRouterManifests.find((e) => e.id === routerId);
6796
6795
  const trie = state.perRouterTrieMap.get(routerId);
@@ -6817,20 +6816,49 @@ function generatePerRouterModule(state, routerId) {
6817
6816
  lines.push(`export const manifest = ${jsonParseExpression(manifest)};`);
6818
6817
  }
6819
6818
  }
6820
- if (trie) {
6819
+ const hasTrie = !!trie;
6820
+ const hasEntries = !!entries && entries.length > 0;
6821
+ const preset = state.opts?.preset ?? "node";
6822
+ const override = process.env.RANGO_MANIFEST_TEXT;
6823
+ const payload = manifestPayload(trie, hasTrie, entries, hasEntries);
6824
+ const payloadJson = hasTrie || hasEntries ? JSON.stringify(payload) : "";
6825
+ const useTextModule = state.isBuildMode && preset === "cloudflare" && override !== "0" && (hasTrie || hasEntries) && (override === "1" || payloadJson.length >= MANIFEST_EXTERNALIZE_THRESHOLD);
6826
+ if (useTextModule) {
6827
+ const dir = join6(state.projectRoot, "node_modules", ".rango");
6828
+ mkdirSync2(dir, { recursive: true });
6829
+ const safeId = `${routerId.replace(/[^a-zA-Z0-9_-]/g, "_")}-${shortHash(routerId)}`;
6830
+ const filePath = join6(dir, `manifest-${safeId}.txt`).replaceAll("\\", "/");
6831
+ writeFileSync5(filePath, payloadJson);
6832
+ lines.push(`import __manifestJson from ${JSON.stringify(filePath)};`);
6833
+ lines.push(`const __manifestData = JSON.parse(__manifestJson);`);
6834
+ emitManifestExports(lines, hasTrie, hasEntries);
6835
+ return lines.join("\n");
6836
+ }
6837
+ if (hasTrie) {
6821
6838
  lines.push(`export const trie = ${jsonParseExpression(trie)};`);
6822
6839
  }
6823
- if (entries && entries.length > 0) {
6840
+ if (hasEntries) {
6824
6841
  lines.push(
6825
6842
  `export const precomputedEntries = ${jsonParseExpression(entries)};`
6826
6843
  );
6827
6844
  }
6828
6845
  return lines.join("\n") || "";
6829
6846
  }
6847
+ function manifestPayload(trie, hasTrie, entries, hasEntries) {
6848
+ const payload = {};
6849
+ if (hasTrie) payload.t = trie;
6850
+ if (hasEntries) payload.p = entries;
6851
+ return payload;
6852
+ }
6853
+ function emitManifestExports(lines, hasTrie, hasEntries) {
6854
+ if (hasTrie) lines.push(`export const trie = __manifestData.t;`);
6855
+ if (hasEntries)
6856
+ lines.push(`export const precomputedEntries = __manifestData.p;`);
6857
+ }
6830
6858
 
6831
6859
  // src/vite/discovery/bundle-postprocess.ts
6832
6860
  import { resolve as resolve10 } from "node:path";
6833
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
6861
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "node:fs";
6834
6862
  function postprocessBundle(state) {
6835
6863
  const hasPrerenderData = state.prerenderManifestEntries && Object.keys(state.prerenderManifestEntries).length > 0;
6836
6864
  const hasStaticData = state.staticManifestEntries && Object.keys(state.staticManifestEntries).length > 0;
@@ -6866,7 +6894,7 @@ function postprocessBundle(state) {
6866
6894
  target.brand
6867
6895
  );
6868
6896
  if (result) {
6869
- writeFileSync5(chunkPath, result.code);
6897
+ writeFileSync6(chunkPath, result.code);
6870
6898
  const savedKB = (result.savedBytes / 1024).toFixed(1);
6871
6899
  console.log(
6872
6900
  `[rango] Evicted ${target.label} (${savedKB} KB saved): ${info.fileName}`
@@ -6905,11 +6933,11 @@ function postprocessBundle(state) {
6905
6933
  state.projectRoot,
6906
6934
  "dist/rsc/__prerender-manifest.js"
6907
6935
  );
6908
- writeFileSync5(manifestPath, manifestCode);
6936
+ writeFileSync6(manifestPath, manifestCode);
6909
6937
  totalBytes += Buffer.byteLength(manifestCode);
6910
6938
  const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");
6911
6939
  `;
6912
- writeFileSync5(rscEntryPath, injection + rscCode);
6940
+ writeFileSync6(rscEntryPath, injection + rscCode);
6913
6941
  const totalKB = (totalBytes / 1024).toFixed(1);
6914
6942
  console.log(
6915
6943
  `[rango] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries).length} entries)`
@@ -6943,11 +6971,11 @@ function postprocessBundle(state) {
6943
6971
  state.projectRoot,
6944
6972
  "dist/rsc/__static-manifest.js"
6945
6973
  );
6946
- writeFileSync5(manifestPath, manifestCode);
6974
+ writeFileSync6(manifestPath, manifestCode);
6947
6975
  totalBytes += Buffer.byteLength(manifestCode);
6948
6976
  const injection = `import "./__static-manifest.js";
6949
6977
  `;
6950
- writeFileSync5(rscEntryPath, injection + rscCode);
6978
+ writeFileSync6(rscEntryPath, injection + rscCode);
6951
6979
  const totalKB = (totalBytes / 1024).toFixed(1);
6952
6980
  console.log(
6953
6981
  `[rango] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries).length} entries)`
@@ -7106,6 +7134,7 @@ function ensureCloudflareProtocolLoaderRegistered() {
7106
7134
  );
7107
7135
  }
7108
7136
  }
7137
+ var injectedShellNotReadyPaths = /* @__PURE__ */ new Set();
7109
7138
  async function createTempRscServer(state, options = {}) {
7110
7139
  ensureCloudflareProtocolLoaderRegistered();
7111
7140
  const { default: rsc } = await import("@vitejs/plugin-rsc");
@@ -7338,14 +7367,14 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7338
7367
  debugDiscovery?.(
7339
7368
  "getOrCreateTempServer: cached temp runner reused"
7340
7369
  );
7341
- return existingEnv;
7370
+ return { env: existingEnv, error: null };
7342
7371
  }
7343
7372
  debugDiscovery?.(
7344
7373
  "getOrCreateTempServer: server alive but registry missing \u2014 re-importing"
7345
7374
  );
7346
7375
  try {
7347
7376
  await importEntryAndRegistry(existingEnv);
7348
- return existingEnv;
7377
+ return { env: existingEnv, error: null };
7349
7378
  } catch (err) {
7350
7379
  debugDiscovery?.(
7351
7380
  "getOrCreateTempServer: reuse import failed (%s) \u2014 closing orphan and creating fresh",
@@ -7370,6 +7399,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7370
7399
  "getOrCreateTempServer: creating new temp server, entry=%s",
7371
7400
  s.resolvedEntryPath ?? "(unset)"
7372
7401
  );
7402
+ let createError = null;
7373
7403
  try {
7374
7404
  prerenderTempServer = await createTempRscServer(s, {
7375
7405
  cacheDir: "node_modules/.vite_prerender",
@@ -7381,12 +7411,13 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7381
7411
  const tempRscEnv = prerenderTempServer.environments?.rsc;
7382
7412
  if (tempRscEnv?.runner) {
7383
7413
  await importEntryAndRegistry(tempRscEnv);
7384
- return tempRscEnv;
7414
+ return { env: tempRscEnv, error: null };
7385
7415
  }
7386
7416
  debugDiscovery?.(
7387
7417
  "getOrCreateTempServer: tempRscEnv.runner unavailable"
7388
7418
  );
7389
7419
  } catch (err) {
7420
+ createError = err;
7390
7421
  debugDiscovery?.(
7391
7422
  "getOrCreateTempServer: FAILED message=%s",
7392
7423
  err.message
@@ -7397,7 +7428,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7397
7428
  });
7398
7429
  prerenderTempServer = null;
7399
7430
  prerenderNodeRegistry = null;
7400
- return null;
7431
+ return { env: null, error: createError };
7401
7432
  }
7402
7433
  async function clearTempRegistries(tempRscEnv) {
7403
7434
  try {
@@ -7421,7 +7452,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7421
7452
  }
7422
7453
  }
7423
7454
  async function refreshTempRscEnv() {
7424
- let tempRscEnv = await getOrCreateTempServer();
7455
+ const tempRscEnv = (await getOrCreateTempServer()).env;
7425
7456
  if (!tempRscEnv) return null;
7426
7457
  const envGraph = tempRscEnv.moduleGraph;
7427
7458
  const serverGraph = prerenderTempServer?.moduleGraph;
@@ -7436,7 +7467,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7436
7467
  prerenderTempServer = null;
7437
7468
  prerenderNodeRegistry = null;
7438
7469
  }
7439
- return await getOrCreateTempServer();
7470
+ return (await getOrCreateTempServer()).env;
7440
7471
  }
7441
7472
  debugDiscovery?.(
7442
7473
  "refreshTempRscEnv: invalidating module graph (%s)",
@@ -7474,11 +7505,11 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7474
7505
  "acquireBuildEnv",
7475
7506
  () => acquireBuildEnv(s, viteCommand, viteMode)
7476
7507
  );
7477
- tempRscEnv = await timed(
7508
+ tempRscEnv = (await timed(
7478
7509
  debugDiscovery,
7479
7510
  "getOrCreateTempServer",
7480
7511
  () => getOrCreateTempServer()
7481
- );
7512
+ )).env;
7482
7513
  if (tempRscEnv) {
7483
7514
  optimizerHashBefore2 = tempRscEnv.depsOptimizer?.metadata?.browserHash;
7484
7515
  await timed(
@@ -7623,7 +7654,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7623
7654
  registry = mainRegistry;
7624
7655
  }
7625
7656
  if (!registry) {
7626
- const tempRscEnv = await getOrCreateTempServer();
7657
+ const tempRscEnv = (await getOrCreateTempServer()).env;
7627
7658
  if (tempRscEnv) {
7628
7659
  try {
7629
7660
  await importEntryAndRegistry(tempRscEnv);
@@ -7720,6 +7751,32 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7720
7751
  res.end("Missing pathname/routeName/version/ttl");
7721
7752
  return;
7722
7753
  }
7754
+ const sendNotReady = (detail) => {
7755
+ res.statusCode = 503;
7756
+ res.setHeader("x-rango-shell-dev", "NOT-READY");
7757
+ res.end(detail);
7758
+ };
7759
+ const isReoptimizing = (err) => err?.code === "ERR_OUTDATED_OPTIMIZED_DEP" || /Outdated Optimize Dep|optimized dependency|new dependencies optimized/i.test(
7760
+ String(err?.message ?? "")
7761
+ );
7762
+ const handledAsReoptimizing = (err) => {
7763
+ if (!isReoptimizing(err)) return false;
7764
+ sendNotReady(`Shell capture re-optimizing: ${err.message}`);
7765
+ return true;
7766
+ };
7767
+ const handleShellImportError = (err) => {
7768
+ if (handledAsReoptimizing(err)) return;
7769
+ res.statusCode = 500;
7770
+ res.end(`Shell capture module refresh failed: ${err.message}`);
7771
+ };
7772
+ if (process.env.RANGO_E2E_INJECT_SHELL_NOTREADY === "1" && !injectedShellNotReadyPaths.has(pathname)) {
7773
+ injectedShellNotReadyPaths.add(pathname);
7774
+ const injected = Object.assign(
7775
+ new Error("Outdated Optimize Dep (injected boot-race)"),
7776
+ { code: "ERR_OUTDATED_OPTIMIZED_DEP" }
7777
+ );
7778
+ if (handledAsReoptimizing(injected)) return;
7779
+ }
7723
7780
  const ttl = Number(ttlRaw);
7724
7781
  const swrRaw = url.searchParams.get("swr");
7725
7782
  const swr = swrRaw === null ? void 0 : Number(swrRaw);
@@ -7734,47 +7791,52 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7734
7791
  let rscRealm = null;
7735
7792
  let ssrRealm = null;
7736
7793
  let ssrEntryId;
7794
+ let tempServerError = null;
7737
7795
  if (rscEnvMain?.runner && s.resolvedEntryPath) {
7738
7796
  try {
7739
7797
  await rscEnvMain.runner.import(s.resolvedEntryPath);
7740
7798
  } catch (err) {
7741
- res.statusCode = 500;
7742
- res.end(`Shell capture module refresh failed: ${err.message}`);
7799
+ handleShellImportError(err);
7743
7800
  return;
7744
7801
  }
7745
7802
  rscRealm = rscEnvMain;
7746
7803
  ssrRealm = server.environments?.ssr;
7747
7804
  ssrEntryId = server.environments?.ssr?.config?.build?.rollupOptions?.input?.index ?? VIRTUAL_IDS.ssr;
7748
7805
  } else {
7749
- const tempRscEnv = await getOrCreateTempServer();
7750
- if (tempRscEnv) {
7806
+ const tempResult = await getOrCreateTempServer();
7807
+ if (tempResult.env) {
7751
7808
  try {
7752
- await importEntryAndRegistry(tempRscEnv);
7809
+ await importEntryAndRegistry(tempResult.env);
7753
7810
  } catch (err) {
7754
- res.statusCode = 500;
7755
- res.end(`Shell capture module refresh failed: ${err.message}`);
7811
+ handleShellImportError(err);
7756
7812
  return;
7757
7813
  }
7814
+ } else {
7815
+ tempServerError = tempResult.error;
7758
7816
  }
7759
- rscRealm = tempRscEnv;
7817
+ rscRealm = tempResult.env;
7760
7818
  ssrRealm = prerenderTempServer?.environments?.ssr;
7761
7819
  ssrEntryId = "virtual:entry-ssr";
7762
7820
  }
7763
7821
  if (!rscRealm?.runner || !ssrRealm?.runner) {
7822
+ if (tempServerError && handledAsReoptimizing(tempServerError)) return;
7764
7823
  res.statusCode = 503;
7765
7824
  res.end("Shell capture runners not available");
7766
7825
  return;
7767
7826
  }
7768
7827
  let registry = null;
7828
+ let registryError = null;
7769
7829
  try {
7770
7830
  const serverMod = await rscRealm.runner.import(
7771
7831
  "@rangojs/router/server"
7772
7832
  );
7773
7833
  registry = serverMod.RouterRegistry ?? null;
7774
- } catch {
7834
+ } catch (err) {
7835
+ registryError = err;
7775
7836
  registry = null;
7776
7837
  }
7777
7838
  if (!registry || registry.size === 0) {
7839
+ if (registryError && handledAsReoptimizing(registryError)) return;
7778
7840
  res.statusCode = 503;
7779
7841
  res.end("Shell capture registry not available");
7780
7842
  return;
@@ -7853,6 +7915,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
7853
7915
  res.end(body);
7854
7916
  return;
7855
7917
  } catch (err) {
7918
+ if (handledAsReoptimizing(err)) return;
7856
7919
  console.warn(
7857
7920
  `[rango] Dev shell capture error for ${pathname} (route keeps runtime capture): ${err.message}`
7858
7921
  );
@@ -8388,6 +8451,7 @@ ${details}`,
8388
8451
  // src/vite/rango.ts
8389
8452
  init_debug();
8390
8453
  var debugConfig = createRangoDebugger(NS.config);
8454
+ var SERVER_BUILD_TARGET = "esnext";
8391
8455
  async function rango(options) {
8392
8456
  const rangoStart = performance.now();
8393
8457
  const resolvedOptions = options ?? { preset: "node" };
@@ -8635,6 +8699,9 @@ If this is a multi-app host router, export a createHostRouter() instance and set
8635
8699
  },
8636
8700
  ssr: {
8637
8701
  ...vercelServerEnv ?? {},
8702
+ build: {
8703
+ target: SERVER_BUILD_TARGET
8704
+ },
8638
8705
  optimizeDeps: {
8639
8706
  entries: [VIRTUAL_IDS.ssr],
8640
8707
  include: [
@@ -8654,6 +8721,9 @@ If this is a multi-app host router, export a createHostRouter() instance and set
8654
8721
  },
8655
8722
  rsc: {
8656
8723
  ...vercelServerEnv ?? {},
8724
+ build: {
8725
+ target: SERVER_BUILD_TARGET
8726
+ },
8657
8727
  optimizeDeps: {
8658
8728
  entries: [VIRTUAL_IDS.rsc],
8659
8729
  include: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.150",
3
+ "version": "0.0.0-experimental.151",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -248,6 +248,6 @@
248
248
  }
249
249
  },
250
250
  "engines": {
251
- "node": "^20.19.0 || >=22.12.0"
251
+ "node": ">=24.0.0"
252
252
  }
253
253
  }
@@ -8,18 +8,10 @@ argument-hint:
8
8
 
9
9
  Inspect the route manifest to verify parent relationships, shortCodes, and route structure.
10
10
 
11
- ## Quick Access
12
-
13
- In development, visit:
14
-
15
- ```
16
- http://localhost:PORT/?__debug_manifest
17
- ```
18
-
19
- Returns formatted JSON. The HTTP endpoint shape is `{ routerId, routeManifest, routeAncestry, routeTrie, precomputedEntries }` (see below for the programmatic API shape).
20
-
21
11
  ## Programmatic Access
22
12
 
13
+ Call `router.debugManifest()` — an `async` method on the router instance:
14
+
23
15
  ```typescript
24
16
  import { router } from "./router.js";
25
17
 
@@ -197,6 +197,7 @@ export const myMiddleware: Middleware = async (ctx, next) => {
197
197
  ctx.request; // Request object
198
198
  ctx.url; // Parsed URL
199
199
  ctx.params; // Route parameters
200
+ ctx.build; // in middleware: true only during Prerender + ppr build-shell capture (plain Prerender does not run middleware)
200
201
 
201
202
  // Access platform bindings (plain bindings from createRouter<TEnv>())
202
203
  ctx.env.DB; // D1Database
@@ -205,6 +206,9 @@ export const myMiddleware: Middleware = async (ctx, next) => {
205
206
  // Set variables for downstream handlers (typed via Rango.Vars)
206
207
  ctx.set("user", { id: "123", name: "John" });
207
208
 
209
+ // Opt the current request out of PPR shell lookup/capture.
210
+ ctx.dynamic();
211
+
208
212
  // Continue to next middleware/handler
209
213
  await next();
210
214
 
@@ -246,6 +250,35 @@ This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
246
250
  via Rango.Vars augmentation). Use `createVar` for route-local or feature-scoped
247
251
  data; use Rango.Vars for app-wide middleware state.
248
252
 
253
+ ## Build-Time PPR Middleware
254
+
255
+ Normal `Prerender` Flight payload collection does not run middleware: there is
256
+ no request to wrap. The exception is `Prerender` + `ppr` build-shell capture.
257
+ After the Flight payload exists, the shell producer replays global and route
258
+ middleware for each generated URL before it captures HTML.
259
+
260
+ In that build-shell pass:
261
+
262
+ - `ctx.build === true`;
263
+ - `ctx.waitUntil()` is inert;
264
+ - `ctx.dynamic()` skips the baked shell for that URL;
265
+ - context variables set by middleware are visible to the shell render.
266
+
267
+ Use this to keep side effects predictable:
268
+
269
+ ```typescript
270
+ export const commerceMiddleware: Middleware = async (ctx, next) => {
271
+ if (ctx.build) {
272
+ ctx.dynamic(); // leave this shell to runtime PPR
273
+ return next();
274
+ }
275
+
276
+ const session = await commerce.auth(ctx.request);
277
+ ctx.set("session", session);
278
+ return next();
279
+ };
280
+ ```
281
+
249
282
  ## Redirect with State in Middleware
250
283
 
251
284
  ```typescript
@@ -21,11 +21,12 @@ shell is shared per host+URL, the holes are per request.
21
21
 
22
22
  - You want the WHOLE response frozen, loader output included — see
23
23
  `/document-cache`.
24
- - You want routes rendered at build time with `Static()`/`Prerender()` — the
25
- `ppr` path option captures at runtime; see `/prerender`.
24
+ - You want build-time Flight segment payloads from `Static()`/`Prerender()` —
25
+ see `/prerender`. A `Prerender` page may also declare `ppr`; then producer B
26
+ can bake the HTML shell at build time while loaders stay live.
26
27
  - You are unsure which cache layer you need — start at `/cache-guide`.
27
28
 
28
- ## Setup: one path option, no middleware
29
+ ## Setup: one path option, no PPR middleware to mount
29
30
 
30
31
  PPR is a DOCUMENT-level property declared on the page route via the `ppr` path
31
32
  option. Serving is **integral to the router** — there is nothing to mount. The
@@ -169,6 +170,20 @@ On a document GET to a ppr route the router runs:
169
170
  point is after the chain, an unauthorized request NEVER sees shell bytes — put
170
171
  auth middleware anywhere (global or route DSL) and it guards PPR for free.
171
172
 
173
+ ### Opting out per request with `ctx.dynamic()`
174
+
175
+ Middleware and handlers can call `ctx.dynamic()` to force this request back to
176
+ axis 1. In middleware it runs before the PPR commit point, so the router skips
177
+ shell lookup, HIT serving, and MISS capture for that request. In handlers it is
178
+ too late to prevent a MISS render from already happening, but it still prevents
179
+ the follow-up shell capture.
180
+
181
+ During `Prerender` + `ppr` build-shell capture, middleware is replayed with
182
+ `ctx.build === true`, `ctx.waitUntil()` inert, and the same `ctx.dynamic()`
183
+ opt-out. Use that for routes where the shell depends on runtime-only auth,
184
+ cookies, or side-effectful SDK calls. A skipped build shell can still be owned
185
+ later by runtime capture when runtime middleware does not call `ctx.dynamic()`.
186
+
172
187
  ## Verifying it works
173
188
 
174
189
  The header exists on DOCUMENT responses only. A bare `curl` gets the HTML