@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  // src/vite/rango.ts
2
2
  import { readFileSync as readFileSync7 } from "node:fs";
3
- import { resolve as resolve8 } 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";
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
1745
1745
  // package.json
1746
1746
  var package_default = {
1747
1747
  name: "@rangojs/router",
1748
- version: "0.0.0-experimental.20",
1748
+ version: "0.0.0-experimental.21",
1749
1749
  description: "Django-inspired RSC router with composable URL patterns",
1750
1750
  keywords: [
1751
1751
  "react",
@@ -3024,7 +3024,7 @@ function createCjsToEsmPlugin() {
3024
3024
 
3025
3025
  // src/vite/router-discovery.ts
3026
3026
  import { createServer as createViteServer } from "vite";
3027
- import { resolve as resolve7 } from "node:path";
3027
+ import { resolve as resolve8 } from "node:path";
3028
3028
  import { readFileSync as readFileSync6 } from "node:fs";
3029
3029
 
3030
3030
  // src/vite/plugins/virtual-stub-plugin.ts
@@ -3207,8 +3207,8 @@ function createDiscoveryState(entryPath, opts) {
3207
3207
  perRouterTrieMap: /* @__PURE__ */ new Map(),
3208
3208
  perRouterPrecomputedMap: /* @__PURE__ */ new Map(),
3209
3209
  perRouterManifestDataMap: /* @__PURE__ */ new Map(),
3210
- prerenderCollectedData: null,
3211
- staticCollectedData: null,
3210
+ prerenderManifestEntries: null,
3211
+ staticManifestEntries: null,
3212
3212
  handlerChunkInfo: null,
3213
3213
  staticHandlerChunkInfo: null,
3214
3214
  rscEntryFileName: null,
@@ -3308,6 +3308,16 @@ function contextSet(variables, keyOrVar, value) {
3308
3308
  }
3309
3309
 
3310
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";
3311
3321
  function escapeRegExp2(str) {
3312
3322
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3313
3323
  }
@@ -3386,6 +3396,37 @@ function notifyOnError(registry, error, phase, routeKey, pathname, skipped) {
3386
3396
  break;
3387
3397
  }
3388
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
+ }
3389
3430
 
3390
3431
  // src/vite/discovery/prerender-collection.ts
3391
3432
  async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
@@ -3487,7 +3528,7 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3487
3528
  `[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`
3488
3529
  );
3489
3530
  const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
3490
- const collectedData = {};
3531
+ const manifestEntries = {};
3491
3532
  let doneCount = 0;
3492
3533
  let skipCount = 0;
3493
3534
  const startTotal = performance.now();
@@ -3517,18 +3558,30 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3517
3558
  break;
3518
3559
  }
3519
3560
  const paramHash = hashParams(result.params || {});
3520
- collectedData[`${result.routeName}/${paramHash}`] = {
3561
+ const mainKey = `${result.routeName}/${paramHash}`;
3562
+ const mainValue = JSON.stringify({
3521
3563
  segments: result.segments,
3522
3564
  handles: result.handles
3523
- };
3565
+ });
3566
+ manifestEntries[mainKey] = stageBuildAssetModule(
3567
+ state.projectRoot,
3568
+ "__pr",
3569
+ mainValue
3570
+ );
3524
3571
  if (result.interceptSegments?.length) {
3525
- collectedData[`${result.routeName}/${paramHash}/i`] = {
3572
+ const interceptKey = `${result.routeName}/${paramHash}/i`;
3573
+ const interceptValue = JSON.stringify({
3526
3574
  segments: [...result.segments, ...result.interceptSegments],
3527
3575
  handles: {
3528
3576
  ...result.handles,
3529
3577
  ...result.interceptHandles || {}
3530
3578
  }
3531
- };
3579
+ });
3580
+ manifestEntries[interceptKey] = stageBuildAssetModule(
3581
+ state.projectRoot,
3582
+ "__pr",
3583
+ interceptValue
3584
+ );
3532
3585
  }
3533
3586
  const elapsed = (performance.now() - startUrl).toFixed(0);
3534
3587
  console.log(
@@ -3572,7 +3625,7 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3572
3625
  }
3573
3626
  const totalElapsed = (performance.now() - startTotal).toFixed(0);
3574
3627
  if (doneCount > 0) {
3575
- state.prerenderCollectedData = collectedData;
3628
+ state.prerenderManifestEntries = manifestEntries;
3576
3629
  }
3577
3630
  const parts = [`${doneCount} done`];
3578
3631
  if (skipCount > 0) parts.push(`${skipCount} skipped`);
@@ -3583,7 +3636,7 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3583
3636
  async function renderStaticHandlers(state, rscEnv, registry) {
3584
3637
  if (!state.opts?.enableBuildPrerender || !state.isBuildMode || !state.resolvedStaticModules?.size)
3585
3638
  return;
3586
- const collected = {};
3639
+ const manifestEntries = {};
3587
3640
  let staticDone = 0;
3588
3641
  let staticSkip = 0;
3589
3642
  let totalStaticCount = 0;
@@ -3620,7 +3673,13 @@ async function renderStaticHandlers(state, rscEnv, registry) {
3620
3673
  def.$$routePrefix
3621
3674
  );
3622
3675
  if (result) {
3623
- 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
+ );
3624
3683
  const elapsed = (performance.now() - startHandler).toFixed(0);
3625
3684
  console.log(
3626
3685
  `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`
@@ -3657,7 +3716,7 @@ async function renderStaticHandlers(state, rscEnv, registry) {
3657
3716
  }
3658
3717
  const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
3659
3718
  if (staticDone > 0) {
3660
- state.staticCollectedData = collected;
3719
+ state.staticManifestEntries = manifestEntries;
3661
3720
  }
3662
3721
  const staticParts = [`${staticDone} done`];
3663
3722
  if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
@@ -3879,8 +3938,8 @@ async function discoverRouters(state, rscEnv) {
3879
3938
  }
3880
3939
 
3881
3940
  // src/vite/discovery/route-types-writer.ts
3882
- import { dirname as dirname3, basename, join as join3, resolve as resolve5 } from "node:path";
3883
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "node:fs";
3941
+ import { dirname as dirname3, basename, join as join3, resolve as resolve6 } from "node:path";
3942
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
3884
3943
  function filterUserNamedRoutes(manifest) {
3885
3944
  const filtered = {};
3886
3945
  for (const [name, pattern] of Object.entries(manifest)) {
@@ -3914,7 +3973,7 @@ function writeCombinedRouteTypesWithTracking(state, opts) {
3914
3973
  ""
3915
3974
  );
3916
3975
  const outPath = join3(routerDir, `${routerBasename}.named-routes.gen.ts`);
3917
- if (!existsSync4(outPath)) continue;
3976
+ if (!existsSync5(outPath)) continue;
3918
3977
  try {
3919
3978
  const content = readFileSync4(outPath, "utf-8");
3920
3979
  if (content !== preContent.get(outPath)) {
@@ -3928,10 +3987,10 @@ function writeRouteTypesFiles(state) {
3928
3987
  if (state.perRouterManifests.length === 0) return;
3929
3988
  try {
3930
3989
  const entryDir = dirname3(
3931
- resolve5(state.projectRoot, state.resolvedEntryPath)
3990
+ resolve6(state.projectRoot, state.resolvedEntryPath)
3932
3991
  );
3933
3992
  const oldCombinedPath = join3(entryDir, "named-routes.gen.ts");
3934
- if (existsSync4(oldCombinedPath)) {
3993
+ if (existsSync5(oldCombinedPath)) {
3935
3994
  unlinkSync2(oldCombinedPath);
3936
3995
  console.log(
3937
3996
  `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`
@@ -3975,10 +4034,10 @@ Set an explicit \`id\` on createRouter() or check the call site.`
3975
4034
  userRoutes,
3976
4035
  effectiveSearchSchemas && Object.keys(effectiveSearchSchemas).length > 0 ? effectiveSearchSchemas : void 0
3977
4036
  );
3978
- const existing = existsSync4(outPath) ? readFileSync4(outPath, "utf-8") : null;
4037
+ const existing = existsSync5(outPath) ? readFileSync4(outPath, "utf-8") : null;
3979
4038
  if (existing !== source) {
3980
4039
  markSelfGenWrite(state, outPath, source);
3981
- writeFileSync2(outPath, source);
4040
+ writeFileSync3(outPath, source);
3982
4041
  console.log(`[rsc-router] Generated route types -> ${outPath}`);
3983
4042
  }
3984
4043
  }
@@ -4025,10 +4084,10 @@ function supplementGenFilesWithRuntimeRoutes(state) {
4025
4084
  mergedRoutes,
4026
4085
  Object.keys(mergedSearchSchemas).length > 0 ? mergedSearchSchemas : void 0
4027
4086
  );
4028
- const existing = existsSync4(outPath) ? readFileSync4(outPath, "utf-8") : null;
4087
+ const existing = existsSync5(outPath) ? readFileSync4(outPath, "utf-8") : null;
4029
4088
  if (existing !== source) {
4030
4089
  markSelfGenWrite(state, outPath, source);
4031
- writeFileSync2(outPath, source);
4090
+ writeFileSync3(outPath, source);
4032
4091
  }
4033
4092
  }
4034
4093
  }
@@ -4173,14 +4232,13 @@ function generatePerRouterModule(state, routerId) {
4173
4232
  }
4174
4233
 
4175
4234
  // src/vite/discovery/bundle-postprocess.ts
4176
- import { resolve as resolve6 } from "node:path";
4177
- import { createHash as createHash4 } from "node:crypto";
4178
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync } from "node:fs";
4235
+ import { resolve as resolve7 } from "node:path";
4236
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
4179
4237
  function postprocessBundle(state) {
4180
- const hasPrerenderData = state.prerenderCollectedData && Object.keys(state.prerenderCollectedData).length > 0;
4181
- const hasStaticData = state.staticCollectedData && Object.keys(state.staticCollectedData).length > 0;
4238
+ const hasPrerenderData = state.prerenderManifestEntries && Object.keys(state.prerenderManifestEntries).length > 0;
4239
+ const hasStaticData = state.staticManifestEntries && Object.keys(state.staticManifestEntries).length > 0;
4182
4240
  if (!hasPrerenderData && !hasStaticData) return;
4183
- const rscEntryPath = resolve6(
4241
+ const rscEntryPath = resolve7(
4184
4242
  state.projectRoot,
4185
4243
  "dist/rsc",
4186
4244
  state.rscEntryFileName ?? "index.js"
@@ -4201,7 +4259,7 @@ function postprocessBundle(state) {
4201
4259
  ];
4202
4260
  for (const target of evictionTargets) {
4203
4261
  if (!target.info) continue;
4204
- const chunkPath = resolve6(
4262
+ const chunkPath = resolve7(
4205
4263
  state.projectRoot,
4206
4264
  "dist/rsc",
4207
4265
  target.info.fileName
@@ -4215,7 +4273,7 @@ function postprocessBundle(state) {
4215
4273
  target.brand
4216
4274
  );
4217
4275
  if (result) {
4218
- writeFileSync3(chunkPath, result.code);
4276
+ writeFileSync4(chunkPath, result.code);
4219
4277
  const savedKB = (result.savedBytes / 1024).toFixed(1);
4220
4278
  console.log(
4221
4279
  `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${target.info.fileName}`
@@ -4229,44 +4287,38 @@ function postprocessBundle(state) {
4229
4287
  }
4230
4288
  state.handlerChunkInfo = null;
4231
4289
  state.staticHandlerChunkInfo = null;
4232
- if (hasPrerenderData && existsSync5(rscEntryPath)) {
4290
+ if (hasPrerenderData && existsSync6(rscEntryPath)) {
4233
4291
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
4234
4292
  if (!rscCode.includes("__prerender-manifest.js")) {
4235
4293
  try {
4236
- const assetsDir = resolve6(state.projectRoot, "dist/rsc/assets");
4237
- mkdirSync(assetsDir, { recursive: true });
4238
- const manifestEntries = [];
4239
- let totalBytes = 0;
4240
- for (const [key, entry] of Object.entries(
4241
- state.prerenderCollectedData
4294
+ let totalBytes = copyStagedBuildAssets(
4295
+ state.projectRoot,
4296
+ Object.values(state.prerenderManifestEntries)
4297
+ );
4298
+ const manifestMap = {};
4299
+ for (const [key, assetFileName] of Object.entries(
4300
+ state.prerenderManifestEntries
4242
4301
  )) {
4243
- const entryJson = JSON.stringify(entry);
4244
- const contentHash = createHash4("sha256").update(entryJson).digest("hex").slice(0, 8);
4245
- const assetFileName = `__pr-${contentHash}.js`;
4246
- const assetPath = resolve6(assetsDir, assetFileName);
4247
- const assetCode = `export default ${entryJson};
4248
- `;
4249
- writeFileSync3(assetPath, assetCode);
4250
- totalBytes += Buffer.byteLength(assetCode);
4251
- manifestEntries.push(
4252
- `${JSON.stringify(key)}:()=>import("./assets/${assetFileName}")`
4253
- );
4302
+ manifestMap[key] = `./assets/${assetFileName}`;
4254
4303
  }
4255
- const manifestCode = `const m={${manifestEntries.join(",")}};export default m;
4256
- `;
4257
- const manifestPath = resolve6(
4304
+ const manifestCode = [
4305
+ `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
4306
+ `export function loadPrerenderAsset(s){return import(s)}`,
4307
+ `export default m;`,
4308
+ ""
4309
+ ].join("\n");
4310
+ const manifestPath = resolve7(
4258
4311
  state.projectRoot,
4259
4312
  "dist/rsc/__prerender-manifest.js"
4260
4313
  );
4261
- writeFileSync3(manifestPath, manifestCode);
4314
+ writeFileSync4(manifestPath, manifestCode);
4262
4315
  totalBytes += Buffer.byteLength(manifestCode);
4263
- const injection = `import __pm from "./__prerender-manifest.js";
4264
- globalThis.__PRERENDER_MANIFEST = __pm;
4316
+ const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");
4265
4317
  `;
4266
- writeFileSync3(rscEntryPath, injection + rscCode);
4318
+ writeFileSync4(rscEntryPath, injection + rscCode);
4267
4319
  const totalKB = (totalBytes / 1024).toFixed(1);
4268
4320
  console.log(
4269
- `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderCollectedData).length} entries)`
4321
+ `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries).length} entries)`
4270
4322
  );
4271
4323
  } catch (err) {
4272
4324
  throw new Error(
@@ -4275,44 +4327,36 @@ globalThis.__PRERENDER_MANIFEST = __pm;
4275
4327
  }
4276
4328
  }
4277
4329
  }
4278
- if (hasStaticData && existsSync5(rscEntryPath)) {
4330
+ if (hasStaticData && existsSync6(rscEntryPath)) {
4279
4331
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
4280
4332
  if (!rscCode.includes("__STATIC_MANIFEST")) {
4281
4333
  try {
4282
- const assetsDir = resolve6(state.projectRoot, "dist/rsc/assets");
4283
- mkdirSync(assetsDir, { recursive: true });
4284
4334
  const manifestEntries = [];
4285
- let totalBytes = 0;
4286
- for (const [handlerId, { encoded, handles }] of Object.entries(
4287
- state.staticCollectedData
4335
+ let totalBytes = copyStagedBuildAssets(
4336
+ state.projectRoot,
4337
+ Object.values(state.staticManifestEntries)
4338
+ );
4339
+ for (const [handlerId, assetFileName] of Object.entries(
4340
+ state.staticManifestEntries
4288
4341
  )) {
4289
- const hasHandles = Object.keys(handles).length > 0;
4290
- const exportValue = hasHandles ? JSON.stringify({ encoded, handles }) : JSON.stringify(encoded);
4291
- const contentHash = createHash4("sha256").update(exportValue).digest("hex").slice(0, 8);
4292
- const assetFileName = `__st-${contentHash}.js`;
4293
- const assetPath = resolve6(assetsDir, assetFileName);
4294
- const assetCode = `export default ${exportValue};
4295
- `;
4296
- writeFileSync3(assetPath, assetCode);
4297
- totalBytes += Buffer.byteLength(assetCode);
4298
4342
  manifestEntries.push(
4299
4343
  `${JSON.stringify(handlerId)}:()=>import("./assets/${assetFileName}")`
4300
4344
  );
4301
4345
  }
4302
4346
  const manifestCode = `const m={${manifestEntries.join(",")}};globalThis.__STATIC_MANIFEST=m;export default m;
4303
4347
  `;
4304
- const manifestPath = resolve6(
4348
+ const manifestPath = resolve7(
4305
4349
  state.projectRoot,
4306
4350
  "dist/rsc/__static-manifest.js"
4307
4351
  );
4308
- writeFileSync3(manifestPath, manifestCode);
4352
+ writeFileSync4(manifestPath, manifestCode);
4309
4353
  totalBytes += Buffer.byteLength(manifestCode);
4310
4354
  const injection = `import "./__static-manifest.js";
4311
4355
  `;
4312
- writeFileSync3(rscEntryPath, injection + rscCode);
4356
+ writeFileSync4(rscEntryPath, injection + rscCode);
4313
4357
  const totalKB = (totalBytes / 1024).toFixed(1);
4314
4358
  console.log(
4315
- `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticCollectedData).length} entries)`
4359
+ `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries).length} entries)`
4316
4360
  );
4317
4361
  } catch (err) {
4318
4362
  throw new Error(
@@ -4427,8 +4471,8 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4427
4471
  if (globalThis.__rscRouterDiscoveryActive) return;
4428
4472
  s.devServer = server;
4429
4473
  let resolveDiscovery;
4430
- const discoveryPromise = new Promise((resolve9) => {
4431
- resolveDiscovery = resolve9;
4474
+ const discoveryPromise = new Promise((resolve10) => {
4475
+ resolveDiscovery = resolve10;
4432
4476
  });
4433
4477
  const getDevServerOrigin = () => server.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || `http://localhost:${server.config.server.port || 5173}`;
4434
4478
  let prerenderTempServer = null;
@@ -4503,8 +4547,8 @@ ${err.stack}`
4503
4547
  resolveDiscovery();
4504
4548
  }
4505
4549
  };
4506
- s.discoveryDone = new Promise((resolve9) => {
4507
- setTimeout(() => discover().then(resolve9, resolve9), 0);
4550
+ s.discoveryDone = new Promise((resolve10) => {
4551
+ setTimeout(() => discover().then(resolve10, resolve10), 0);
4508
4552
  });
4509
4553
  let mainRegistry = null;
4510
4554
  const propagateDiscoveryState = async (rscEnv) => {
@@ -4673,7 +4717,7 @@ ${err.stack}`
4673
4717
  if (hasCreateRouter) {
4674
4718
  const nestedRouterConflict = findNestedRouterConflict([
4675
4719
  ...s.cachedRouterFiles ?? [],
4676
- resolve7(filePath)
4720
+ resolve8(filePath)
4677
4721
  ]);
4678
4722
  if (nestedRouterConflict) {
4679
4723
  server.config.logger.error(
@@ -4703,6 +4747,9 @@ ${err.stack}`
4703
4747
  async buildStart() {
4704
4748
  if (!s.isBuildMode) return;
4705
4749
  if (s.mergedRouteManifest !== null) return;
4750
+ resetStagedBuildAssets(s.projectRoot);
4751
+ s.prerenderManifestEntries = null;
4752
+ s.staticManifestEntries = null;
4706
4753
  let tempServer = null;
4707
4754
  globalThis.__rscRouterDiscoveryActive = true;
4708
4755
  try {
@@ -4944,7 +4991,7 @@ async function rango(options) {
4944
4991
  name: "@rangojs/router:auto-discover",
4945
4992
  config(userConfig) {
4946
4993
  if (routerRef.path) return;
4947
- const root = userConfig.root ? resolve8(process.cwd(), userConfig.root) : process.cwd();
4994
+ const root = userConfig.root ? resolve9(process.cwd(), userConfig.root) : process.cwd();
4948
4995
  const filter = createScanFilter(root, {
4949
4996
  include: resolvedOptions.include,
4950
4997
  exclude: resolvedOptions.exclude
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.20",
3
+ "version": "0.0.0-experimental.21",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -2,9 +2,10 @@
2
2
  * Prerender Store
3
3
  *
4
4
  * Reads pre-rendered segment data from the worker bundle at build time.
5
- * The data is stored as globalThis.__PRERENDER_MANIFEST, a map of
6
- * "<routeName>/<paramHash>" to dynamic import functions that resolve
7
- * individual prerender entry modules.
5
+ * The manifest module is lazily loaded via globalThis.__loadPrerenderManifestModule,
6
+ * a function injected into the RSC entry that returns the manifest module
7
+ * containing a key-to-specifier map and a `loadPrerenderAsset` function
8
+ * that anchors import() resolution relative to the manifest file.
8
9
  */
9
10
 
10
11
  import type {
@@ -34,11 +35,20 @@ export interface StaticStore {
34
35
  get(handlerId: string): Promise<StaticEntry | null>;
35
36
  }
36
37
 
38
+ interface PrerenderManifestModule {
39
+ default: Record<string, string>;
40
+ loadPrerenderAsset: (
41
+ specifier: string,
42
+ ) => Promise<{ default: PrerenderEntry }>;
43
+ }
44
+
37
45
  declare global {
38
- // Injected by closeBundle post-processing: map of key -> () => import("./assets/__pr-*.js")
46
+ // Injected by closeBundle post-processing: lazy loader for the prerender
47
+ // manifest module. The module exports a key→specifier map and a
48
+ // loadPrerenderAsset function that anchors import() relative to the manifest.
39
49
  // eslint-disable-next-line no-var
40
- var __PRERENDER_MANIFEST:
41
- | Record<string, () => Promise<{ default: PrerenderEntry }>>
50
+ var __loadPrerenderManifestModule:
51
+ | (() => Promise<PrerenderManifestModule>)
42
52
  | undefined;
43
53
  // Injected by closeBundle post-processing: map of handlerId -> () => import("./assets/__st-*.js")
44
54
  // Asset default export is either a string (no handles) or { encoded, handles } object.
@@ -78,17 +88,28 @@ export function createDevPrerenderStore(devUrl: string): PrerenderStore {
78
88
  /**
79
89
  * Create a prerender store.
80
90
  * Dev mode: on-demand fetch from Vite dev server (node:fs works there).
81
- * Production: backed by globalThis.__PRERENDER_MANIFEST injected at build time.
91
+ * Production: backed by globalThis.__loadPrerenderManifestModule which lazily
92
+ * loads the manifest module on first access.
82
93
  * Returns null if no prerender data is available.
83
94
  */
84
95
  export function createPrerenderStore(): PrerenderStore | null {
85
96
  if (globalThis.__PRERENDER_DEV_URL) {
86
97
  return createDevPrerenderStore(globalThis.__PRERENDER_DEV_URL);
87
98
  }
88
- const manifest = globalThis.__PRERENDER_MANIFEST;
89
- if (!manifest || Object.keys(manifest).length === 0) return null;
99
+ if (!globalThis.__loadPrerenderManifestModule) return null;
90
100
 
91
101
  const cache = new Map<string, Promise<PrerenderEntry | null>>();
102
+ let manifestModulePromise: Promise<PrerenderManifestModule | null> | null =
103
+ null;
104
+
105
+ function loadManifestModule(): Promise<PrerenderManifestModule | null> {
106
+ if (!manifestModulePromise) {
107
+ manifestModulePromise = globalThis.__loadPrerenderManifestModule!().catch(
108
+ () => null,
109
+ );
110
+ }
111
+ return manifestModulePromise;
112
+ }
92
113
 
93
114
  return {
94
115
  get(routeName: string, paramHash: string): Promise<PrerenderEntry | null> {
@@ -96,18 +117,38 @@ export function createPrerenderStore(): PrerenderStore | null {
96
117
  const cached = cache.get(key);
97
118
  if (cached) return cached;
98
119
 
99
- const loader = manifest[key];
100
- if (!loader) return Promise.resolve(null);
101
-
102
- const promise = loader()
103
- .then((mod) => mod.default)
104
- .catch(() => null);
120
+ const promise = loadManifestModule().then((mod) => {
121
+ if (!mod) return null;
122
+ const specifier = mod.default[key];
123
+ if (!specifier) return null;
124
+ return mod
125
+ .loadPrerenderAsset(specifier)
126
+ .then((asset) => asset.default)
127
+ .catch(() => null);
128
+ });
105
129
  cache.set(key, promise);
106
130
  return promise;
107
131
  },
108
132
  };
109
133
  }
110
134
 
135
+ /**
136
+ * Load the prerender manifest index for test introspection.
137
+ * Returns the key→specifier map or null if unavailable.
138
+ */
139
+ export async function loadPrerenderManifestIndex(): Promise<Record<
140
+ string,
141
+ string
142
+ > | null> {
143
+ if (!globalThis.__loadPrerenderManifestModule) return null;
144
+ try {
145
+ const mod = await globalThis.__loadPrerenderManifestModule();
146
+ return mod.default;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
111
152
  /**
112
153
  * Create a static segment store.
113
154
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
@@ -6,9 +6,9 @@
6
6
  */
7
7
 
8
8
  import { resolve } from "node:path";
9
- import { createHash } from "node:crypto";
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
9
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
11
10
  import { evictHandlerCode } from "../utils/bundle-analysis.js";
11
+ import { copyStagedBuildAssets } from "../utils/prerender-utils.js";
12
12
  import type { DiscoveryState } from "./state.js";
13
13
 
14
14
  /**
@@ -17,11 +17,11 @@ import type { DiscoveryState } from "./state.js";
17
17
  */
18
18
  export function postprocessBundle(state: DiscoveryState): void {
19
19
  const hasPrerenderData =
20
- state.prerenderCollectedData &&
21
- Object.keys(state.prerenderCollectedData).length > 0;
20
+ state.prerenderManifestEntries &&
21
+ Object.keys(state.prerenderManifestEntries).length > 0;
22
22
  const hasStaticData =
23
- state.staticCollectedData &&
24
- Object.keys(state.staticCollectedData).length > 0;
23
+ state.staticManifestEntries &&
24
+ Object.keys(state.staticManifestEntries).length > 0;
25
25
  if (!hasPrerenderData && !hasStaticData) return;
26
26
 
27
27
  // Find RSC entry (recorded in generateBundle, fallback to dist/rsc/index.js)
@@ -88,39 +88,30 @@ export function postprocessBundle(state: DiscoveryState): void {
88
88
  state.staticHandlerChunkInfo = null;
89
89
 
90
90
  // 2. Write prerender data as separate importable asset modules
91
- // and inject a manifest import into the RSC entry.
91
+ // and inject a lazy manifest loader into the RSC entry.
92
92
  if (hasPrerenderData && existsSync(rscEntryPath)) {
93
93
  const rscCode = readFileSync(rscEntryPath, "utf-8");
94
- // Check for the specific injection marker, not just the variable name.
95
- // The runtime code (prerender store) also references __PRERENDER_MANIFEST,
96
- // so a broad string check would false-positive and skip injection.
94
+ // Check for the specific injection marker to avoid double-injection.
97
95
  if (!rscCode.includes("__prerender-manifest.js")) {
98
96
  try {
99
- const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
100
- mkdirSync(assetsDir, { recursive: true });
101
-
102
- const manifestEntries: string[] = [];
103
- let totalBytes = 0;
97
+ let totalBytes = copyStagedBuildAssets(
98
+ state.projectRoot,
99
+ Object.values(state.prerenderManifestEntries!),
100
+ );
104
101
 
105
- for (const [key, entry] of Object.entries(
106
- state.prerenderCollectedData!,
102
+ const manifestMap: Record<string, string> = {};
103
+ for (const [key, assetFileName] of Object.entries(
104
+ state.prerenderManifestEntries!,
107
105
  )) {
108
- const entryJson = JSON.stringify(entry);
109
- const contentHash = createHash("sha256")
110
- .update(entryJson)
111
- .digest("hex")
112
- .slice(0, 8);
113
- const assetFileName = `__pr-${contentHash}.js`;
114
- const assetPath = resolve(assetsDir, assetFileName);
115
- const assetCode = `export default ${entryJson};\n`;
116
- writeFileSync(assetPath, assetCode);
117
- totalBytes += Buffer.byteLength(assetCode);
118
- manifestEntries.push(
119
- `${JSON.stringify(key)}:()=>import("./assets/${assetFileName}")`,
120
- );
106
+ manifestMap[key] = `./assets/${assetFileName}`;
121
107
  }
122
108
 
123
- const manifestCode = `const m={${manifestEntries.join(",")}};export default m;\n`;
109
+ const manifestCode = [
110
+ `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
111
+ `export function loadPrerenderAsset(s){return import(s)}`,
112
+ `export default m;`,
113
+ "",
114
+ ].join("\n");
124
115
  const manifestPath = resolve(
125
116
  state.projectRoot,
126
117
  "dist/rsc/__prerender-manifest.js",
@@ -128,12 +119,12 @@ export function postprocessBundle(state: DiscoveryState): void {
128
119
  writeFileSync(manifestPath, manifestCode);
129
120
  totalBytes += Buffer.byteLength(manifestCode);
130
121
 
131
- const injection = `import __pm from "./__prerender-manifest.js";\nglobalThis.__PRERENDER_MANIFEST = __pm;\n`;
122
+ const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");\n`;
132
123
  writeFileSync(rscEntryPath, injection + rscCode);
133
124
 
134
125
  const totalKB = (totalBytes / 1024).toFixed(1);
135
126
  console.log(
136
- `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderCollectedData!).length} entries)`,
127
+ `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries!).length} entries)`,
137
128
  );
138
129
  } catch (err: any) {
139
130
  throw new Error(
@@ -149,31 +140,15 @@ export function postprocessBundle(state: DiscoveryState): void {
149
140
  const rscCode = readFileSync(rscEntryPath, "utf-8");
150
141
  if (!rscCode.includes("__STATIC_MANIFEST")) {
151
142
  try {
152
- const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
153
- mkdirSync(assetsDir, { recursive: true });
154
-
155
143
  const manifestEntries: string[] = [];
156
- let totalBytes = 0;
144
+ let totalBytes = copyStagedBuildAssets(
145
+ state.projectRoot,
146
+ Object.values(state.staticManifestEntries!),
147
+ );
157
148
 
158
- for (const [handlerId, { encoded, handles }] of Object.entries(
159
- state.staticCollectedData!,
149
+ for (const [handlerId, assetFileName] of Object.entries(
150
+ state.staticManifestEntries!,
160
151
  )) {
161
- // Store both the Flight payload and handle data
162
- const hasHandles = Object.keys(handles).length > 0;
163
- const exportValue = hasHandles
164
- ? JSON.stringify({ encoded, handles })
165
- : JSON.stringify(encoded);
166
- // Hash the full payload that is written so distinct handle
167
- // snapshots produce distinct asset filenames.
168
- const contentHash = createHash("sha256")
169
- .update(exportValue)
170
- .digest("hex")
171
- .slice(0, 8);
172
- const assetFileName = `__st-${contentHash}.js`;
173
- const assetPath = resolve(assetsDir, assetFileName);
174
- const assetCode = `export default ${exportValue};\n`;
175
- writeFileSync(assetPath, assetCode);
176
- totalBytes += Buffer.byteLength(assetCode);
177
152
  manifestEntries.push(
178
153
  `${JSON.stringify(handlerId)}:()=>import("./assets/${assetFileName}")`,
179
154
  );
@@ -197,7 +172,7 @@ export function postprocessBundle(state: DiscoveryState): void {
197
172
 
198
173
  const totalKB = (totalBytes / 1024).toFixed(1);
199
174
  console.log(
200
- `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticCollectedData!).length} entries)`,
175
+ `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries!).length} entries)`,
201
176
  );
202
177
  } catch (err: any) {
203
178
  throw new Error(
@@ -13,12 +13,14 @@ import {
13
13
  runWithConcurrency,
14
14
  groupByConcurrency,
15
15
  notifyOnError,
16
+ stageBuildAssetModule,
16
17
  } from "../utils/prerender-utils.js";
17
18
  import type { DiscoveryState } from "./state.js";
18
19
 
19
20
  /**
20
21
  * Expand prerender routes into concrete URLs and render them via the
21
- * RSC runner. Stores collected data in state.prerenderCollectedData.
22
+ * RSC runner. Stages asset modules and stores key-to-file entries in
23
+ * state.prerenderManifestEntries.
22
24
  */
23
25
  export async function expandPrerenderRoutes(
24
26
  state: DiscoveryState,
@@ -150,7 +152,7 @@ export async function expandPrerenderRoutes(
150
152
 
151
153
  const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
152
154
 
153
- const collectedData: Record<string, any> = {};
155
+ const manifestEntries: Record<string, string> = {};
154
156
  let doneCount = 0;
155
157
  let skipCount = 0;
156
158
  const startTotal = performance.now();
@@ -187,18 +189,30 @@ export async function expandPrerenderRoutes(
187
189
  }
188
190
 
189
191
  const paramHash = hashParams(result.params || {});
190
- collectedData[`${result.routeName}/${paramHash}`] = {
192
+ const mainKey = `${result.routeName}/${paramHash}`;
193
+ const mainValue = JSON.stringify({
191
194
  segments: result.segments,
192
195
  handles: result.handles,
193
- };
196
+ });
197
+ manifestEntries[mainKey] = stageBuildAssetModule(
198
+ state.projectRoot,
199
+ "__pr",
200
+ mainValue,
201
+ );
194
202
  if (result.interceptSegments?.length) {
195
- collectedData[`${result.routeName}/${paramHash}/i`] = {
203
+ const interceptKey = `${result.routeName}/${paramHash}/i`;
204
+ const interceptValue = JSON.stringify({
196
205
  segments: [...result.segments, ...result.interceptSegments],
197
206
  handles: {
198
207
  ...result.handles,
199
208
  ...(result.interceptHandles || {}),
200
209
  },
201
- };
210
+ });
211
+ manifestEntries[interceptKey] = stageBuildAssetModule(
212
+ state.projectRoot,
213
+ "__pr",
214
+ interceptValue,
215
+ );
202
216
  }
203
217
  const elapsed = (performance.now() - startUrl).toFixed(0);
204
218
  console.log(
@@ -244,7 +258,7 @@ export async function expandPrerenderRoutes(
244
258
 
245
259
  const totalElapsed = (performance.now() - startTotal).toFixed(0);
246
260
  if (doneCount > 0) {
247
- state.prerenderCollectedData = collectedData;
261
+ state.prerenderManifestEntries = manifestEntries;
248
262
  }
249
263
  const parts = [`${doneCount} done`];
250
264
  if (skipCount > 0) parts.push(`${skipCount} skipped`);
@@ -256,7 +270,8 @@ export async function expandPrerenderRoutes(
256
270
  /**
257
271
  * Render Static handlers at build time. Each Static handler is called
258
272
  * with a synthetic BuildContext and its output is RSC-serialized.
259
- * Stores collected data in state.staticCollectedData.
273
+ * Stages asset modules and stores handlerId-to-file entries in
274
+ * state.staticManifestEntries.
260
275
  */
261
276
  export async function renderStaticHandlers(
262
277
  state: DiscoveryState,
@@ -270,10 +285,7 @@ export async function renderStaticHandlers(
270
285
  )
271
286
  return;
272
287
 
273
- const collected: Record<
274
- string,
275
- { encoded: string; handles: Record<string, unknown[]> }
276
- > = {};
288
+ const manifestEntries: Record<string, string> = {};
277
289
  let staticDone = 0;
278
290
  let staticSkip = 0;
279
291
  let totalStaticCount = 0;
@@ -316,7 +328,15 @@ export async function renderStaticHandlers(
316
328
  (def as any).$$routePrefix,
317
329
  );
318
330
  if (result) {
319
- collected[def.$$id] = result;
331
+ const hasHandles = Object.keys(result.handles).length > 0;
332
+ const exportValue = hasHandles
333
+ ? JSON.stringify(result)
334
+ : JSON.stringify(result.encoded);
335
+ manifestEntries[def.$$id] = stageBuildAssetModule(
336
+ state.projectRoot,
337
+ "__st",
338
+ exportValue,
339
+ );
320
340
  const elapsed = (performance.now() - startHandler).toFixed(0);
321
341
  console.log(
322
342
  `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
@@ -355,7 +375,7 @@ export async function renderStaticHandlers(
355
375
 
356
376
  const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
357
377
  if (staticDone > 0) {
358
- state.staticCollectedData = collected;
378
+ state.staticManifestEntries = manifestEntries;
359
379
  }
360
380
  const staticParts = [`${staticDone} done`];
361
381
  if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
@@ -56,11 +56,8 @@ export interface DiscoveryState {
56
56
  perRouterPrecomputedMap: Map<string, PrecomputedEntry[]>;
57
57
  perRouterManifestDataMap: Map<string, Record<string, string>>;
58
58
 
59
- prerenderCollectedData: Record<string, any> | null;
60
- staticCollectedData: Record<
61
- string,
62
- { encoded: string; handles: Record<string, unknown[]> }
63
- > | null;
59
+ prerenderManifestEntries: Record<string, string> | null;
60
+ staticManifestEntries: Record<string, string> | null;
64
61
  handlerChunkInfo: ChunkInfo | null;
65
62
  staticHandlerChunkInfo: ChunkInfo | null;
66
63
  rscEntryFileName: string | null;
@@ -96,8 +93,8 @@ export function createDiscoveryState(
96
93
  perRouterPrecomputedMap: new Map(),
97
94
  perRouterManifestDataMap: new Map(),
98
95
 
99
- prerenderCollectedData: null,
100
- staticCollectedData: null,
96
+ prerenderManifestEntries: null,
97
+ staticManifestEntries: null,
101
98
  handlerChunkInfo: null,
102
99
  staticHandlerChunkInfo: null,
103
100
  rscEntryFileName: null,
@@ -42,6 +42,7 @@ import {
42
42
  generatePerRouterModule,
43
43
  } from "./discovery/virtual-module-codegen.js";
44
44
  import { postprocessBundle } from "./discovery/bundle-postprocess.js";
45
+ import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
45
46
 
46
47
  export { VIRTUAL_ROUTES_MANIFEST_ID };
47
48
 
@@ -604,6 +605,9 @@ export function createRouterDiscoveryPlugin(
604
605
  if (!s.isBuildMode) return;
605
606
  // Only run once across environment builds
606
607
  if (s.mergedRouteManifest !== null) return;
608
+ resetStagedBuildAssets(s.projectRoot);
609
+ s.prerenderManifestEntries = null;
610
+ s.staticManifestEntries = null;
607
611
 
608
612
  let tempServer: any = null;
609
613
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
@@ -1,3 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ rmSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { resolve } from "node:path";
11
+
1
12
  /**
2
13
  * Escape special RegExp characters in a string for safe interpolation
3
14
  * into new RegExp() patterns.
@@ -127,3 +138,52 @@ export function notifyOnError(
127
138
  break; // Only notify the first router with onError
128
139
  }
129
140
  }
141
+
142
+ function getStagedAssetDir(projectRoot: string): string {
143
+ return resolve(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
144
+ }
145
+
146
+ export function resetStagedBuildAssets(projectRoot: string): void {
147
+ rmSync(getStagedAssetDir(projectRoot), { recursive: true, force: true });
148
+ }
149
+
150
+ export function stageBuildAssetModule(
151
+ projectRoot: string,
152
+ prefix: "__pr" | "__st",
153
+ exportValue: string,
154
+ ): string {
155
+ const stagedDir = getStagedAssetDir(projectRoot);
156
+ mkdirSync(stagedDir, { recursive: true });
157
+
158
+ const contentHash = createHash("sha256")
159
+ .update(exportValue)
160
+ .digest("hex")
161
+ .slice(0, 8);
162
+ const fileName = `${prefix}-${contentHash}.js`;
163
+ const filePath = resolve(stagedDir, fileName);
164
+
165
+ if (!existsSync(filePath)) {
166
+ writeFileSync(filePath, `export default ${exportValue};\n`);
167
+ }
168
+
169
+ return fileName;
170
+ }
171
+
172
+ export function copyStagedBuildAssets(
173
+ projectRoot: string,
174
+ fileNames: Iterable<string>,
175
+ ): number {
176
+ const stagedDir = getStagedAssetDir(projectRoot);
177
+ const distAssetsDir = resolve(projectRoot, "dist/rsc/assets");
178
+ mkdirSync(distAssetsDir, { recursive: true });
179
+
180
+ let totalBytes = 0;
181
+ for (const fileName of new Set(fileNames)) {
182
+ const stagedPath = resolve(stagedDir, fileName);
183
+ const distPath = resolve(distAssetsDir, fileName);
184
+ copyFileSync(stagedPath, distPath);
185
+ totalBytes += statSync(stagedPath).size;
186
+ }
187
+
188
+ return totalBytes;
189
+ }