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

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.
@@ -239,7 +239,7 @@ export interface RSCRouterOptions<TEnv = any> {
239
239
  *
240
240
  * @example Static config
241
241
  * ```typescript
242
- * import { MemorySegmentCacheStore } from "rsc-router/rsc";
242
+ * import { MemorySegmentCacheStore } from "@rangojs/router/cache";
243
243
  *
244
244
  * const router = createRouter({
245
245
  * cache: {
package/src/rsc/index.ts CHANGED
@@ -29,28 +29,8 @@ export type {
29
29
  NonceProvider,
30
30
  } from "./types.js";
31
31
 
32
- // Re-export HandleStore types for consumers who need custom handling
33
- export {
34
- createHandleStore,
35
- type HandleStore,
36
- type HandleData,
37
- } from "../server/handle-store.js";
38
-
39
32
  // Re-export request context utilities for server-side access to env/request/params
40
33
  export {
41
34
  getRequestContext,
42
35
  requireRequestContext,
43
- setRequestContextParams,
44
36
  } from "../server/request-context.js";
45
-
46
- // Re-export cache store types and implementations
47
- export type {
48
- SegmentCacheStore,
49
- CachedEntryData,
50
- CachedEntryResult,
51
- SegmentCacheProvider,
52
- SegmentHandleData,
53
- } from "../cache/types.js";
54
-
55
- export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
56
- export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
package/src/server.ts CHANGED
@@ -11,6 +11,12 @@
11
11
  // Router registry (used by Vite plugin for build-time discovery)
12
12
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router.js";
13
13
 
14
+ // Host router registry (used by Vite plugin for host-router lazy discovery)
15
+ export {
16
+ HostRouterRegistry,
17
+ type HostRouterRegistryEntry,
18
+ } from "./host/router.js";
19
+
14
20
  // Route map builder (Vite plugin injects these via virtual modules)
15
21
  export {
16
22
  registerRouteMap,
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Theme module exports for @rangojs/router/theme
3
3
  *
4
- * This module provides theme management for rsc-router:
4
+ * This module provides the public theme API:
5
5
  * - useTheme: Hook for accessing theme state in client components
6
6
  * - ThemeProvider: Component for manual theme provider setup (typically not needed)
7
+ * - ThemeScript: FOUC-prevention script component for document/head usage
7
8
  * - Types for theme configuration
8
9
  *
9
10
  * @example
@@ -43,15 +44,5 @@ export type {
43
44
  ThemeContextValue,
44
45
  } from "./types.js";
45
46
 
46
- // Constants (for advanced use cases)
47
- export {
48
- THEME_DEFAULTS,
49
- THEME_COOKIE,
50
- resolveThemeConfig,
51
- } from "./constants.js";
52
-
53
- // Script generation (for advanced SSR use cases)
54
- export { generateThemeScript, getNonceAttribute } from "./theme-script.js";
55
-
56
- // Context (for advanced use cases)
57
- export { ThemeContext, useThemeContext } from "./theme-context.js";
47
+ // Constants
48
+ export { THEME_DEFAULTS, THEME_COOKIE } from "./constants.js";
@@ -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(
@@ -48,9 +48,8 @@ export async function discoverRouters(
48
48
  // No RSC routers found directly. Check for host routers with lazy handlers
49
49
  // that need to be resolved to trigger sub-app createRouter() calls.
50
50
  try {
51
- const hostMod = await rscEnv.runner.import("@rangojs/router/host");
52
51
  const hostRegistry: Map<string, any> | undefined =
53
- hostMod.HostRouterRegistry;
52
+ serverMod.HostRouterRegistry;
54
53
 
55
54
  if (hostRegistry && hostRegistry.size > 0) {
56
55
  console.log(
@@ -89,7 +88,7 @@ export async function discoverRouters(
89
88
  }
90
89
  }
91
90
  } catch {
92
- // @rangojs/router/host not available or import failed, skip
91
+ // Host-router discovery is best-effort; skip if unavailable
93
92
  }
94
93
 
95
94
  // If still no routers after host router resolution, fail
@@ -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
+ }