@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.
- package/dist/vite/index.js +146 -76
- package/package.json +2 -2
- package/skills/debug-manifest/SKILL.md +2 -10
- package/skills/middleware/SKILL.md +33 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +44 -1
- package/src/build/generate-manifest.ts +5 -36
- package/src/build/route-trie.ts +5 -50
- package/src/cache/cache-key-utils.ts +0 -1
- package/src/prerender/build-shell-capture.ts +215 -21
- package/src/router/handler-context.ts +10 -1
- package/src/router/middleware-types.ts +17 -0
- package/src/router/middleware.ts +4 -0
- package/src/router/prerender-match.ts +7 -0
- package/src/router/router-interfaces.ts +0 -6
- package/src/router/router-options.ts +0 -8
- package/src/router.ts +0 -4
- package/src/rsc/handler.ts +55 -51
- package/src/rsc/helpers.ts +461 -0
- package/src/rsc/progressive-enhancement.ts +66 -27
- package/src/rsc/rsc-rendering.ts +91 -57
- package/src/rsc/server-action.ts +49 -46
- package/src/rsc/shell-build-manifest.ts +50 -8
- package/src/rsc/shell-capture.ts +17 -6
- package/src/server/context.ts +34 -10
- package/src/server/request-context.ts +31 -0
- package/src/testing/internal/context.ts +9 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/run-middleware.ts +14 -0
- package/src/types/handler-context.ts +12 -1
- package/src/vite/discovery/discover-routers.ts +44 -52
- package/src/vite/discovery/virtual-module-codegen.ts +112 -2
- package/src/vite/rango.ts +31 -0
- package/src/vite/router-discovery.ts +131 -22
|
@@ -131,6 +131,30 @@ function ensureCloudflareProtocolLoaderRegistered(): void {
|
|
|
131
131
|
// Temp Server Factory
|
|
132
132
|
// ============================================================================
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Outcome of getOrCreateTempServer. `env` is the temp RSC environment on
|
|
136
|
+
* success, else null with `error` carrying WHY the create/import failed — or
|
|
137
|
+
* null when createServer resolved cleanly but attached no runner (terminal, but
|
|
138
|
+
* not an exception). The dev /__rsc_shell endpoint runs `error` through the
|
|
139
|
+
* SAME reoptimization classifier the import sites use: only a transient Vite
|
|
140
|
+
* re-optimization is signalled NOT-READY (client re-polls); every terminal
|
|
141
|
+
* failure fails fast so the read-through MISSes immediately instead of
|
|
142
|
+
* re-polling a permanently-broken realm for the full readiness deadline
|
|
143
|
+
* (issue #719 P2).
|
|
144
|
+
*/
|
|
145
|
+
type TempServerResult = { env: any; error: unknown };
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Test-only one-shot boot-race injection (issue #719 P3). When
|
|
149
|
+
* RANGO_E2E_INJECT_SHELL_NOTREADY=1, the dev /__rsc_shell endpoint emits a
|
|
150
|
+
* single reoptimization-class NOT-READY per pathname before serving, so an e2e
|
|
151
|
+
* can drive the REAL endpoint + real client re-poll deterministically — a
|
|
152
|
+
* natural cold race settles too fast on quick machines to guard the regression.
|
|
153
|
+
* Keyed per pathname so the read-through's re-poll finds it already fired and
|
|
154
|
+
* gets the HIT. Never engaged without the env flag.
|
|
155
|
+
*/
|
|
156
|
+
const injectedShellNotReadyPaths = new Set<string>();
|
|
157
|
+
|
|
134
158
|
/**
|
|
135
159
|
* Create a minimal Vite server for router discovery.
|
|
136
160
|
*
|
|
@@ -533,7 +557,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
533
557
|
}
|
|
534
558
|
}
|
|
535
559
|
|
|
536
|
-
async function getOrCreateTempServer(): Promise<
|
|
560
|
+
async function getOrCreateTempServer(): Promise<TempServerResult> {
|
|
537
561
|
// Reuse path: if a temp server is already alive, prefer reusing
|
|
538
562
|
// it over orphaning the existing instance and spinning up a new
|
|
539
563
|
// one. This handles two cases:
|
|
@@ -554,7 +578,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
554
578
|
debugDiscovery?.(
|
|
555
579
|
"getOrCreateTempServer: cached temp runner reused",
|
|
556
580
|
);
|
|
557
|
-
return existingEnv;
|
|
581
|
+
return { env: existingEnv, error: null };
|
|
558
582
|
}
|
|
559
583
|
// Server alive but registry missing — likely after a prior
|
|
560
584
|
// refresh's invalidate + import threw. Try to re-import.
|
|
@@ -563,7 +587,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
563
587
|
);
|
|
564
588
|
try {
|
|
565
589
|
await importEntryAndRegistry(existingEnv);
|
|
566
|
-
return existingEnv;
|
|
590
|
+
return { env: existingEnv, error: null };
|
|
567
591
|
} catch (err: any) {
|
|
568
592
|
debugDiscovery?.(
|
|
569
593
|
"getOrCreateTempServer: reuse import failed (%s) — closing orphan and creating fresh",
|
|
@@ -591,6 +615,11 @@ export function createRouterDiscoveryPlugin(
|
|
|
591
615
|
"getOrCreateTempServer: creating new temp server, entry=%s",
|
|
592
616
|
s.resolvedEntryPath ?? "(unset)",
|
|
593
617
|
);
|
|
618
|
+
// Surface the create/import cause to the caller (issue #719 P2): a
|
|
619
|
+
// transient re-optimization is re-pollable, a terminal fault is not. A
|
|
620
|
+
// clean createServer that yields no runner leaves this null — terminal,
|
|
621
|
+
// but not an exception.
|
|
622
|
+
let createError: unknown = null;
|
|
594
623
|
try {
|
|
595
624
|
prerenderTempServer = await createTempRscServer(s, {
|
|
596
625
|
cacheDir: "node_modules/.vite_prerender",
|
|
@@ -603,12 +632,13 @@ export function createRouterDiscoveryPlugin(
|
|
|
603
632
|
const tempRscEnv = (prerenderTempServer.environments as any)?.rsc;
|
|
604
633
|
if (tempRscEnv?.runner) {
|
|
605
634
|
await importEntryAndRegistry(tempRscEnv);
|
|
606
|
-
return tempRscEnv;
|
|
635
|
+
return { env: tempRscEnv, error: null };
|
|
607
636
|
}
|
|
608
637
|
debugDiscovery?.(
|
|
609
638
|
"getOrCreateTempServer: tempRscEnv.runner unavailable",
|
|
610
639
|
);
|
|
611
640
|
} catch (err: any) {
|
|
641
|
+
createError = err;
|
|
612
642
|
debugDiscovery?.(
|
|
613
643
|
"getOrCreateTempServer: FAILED message=%s",
|
|
614
644
|
err.message,
|
|
@@ -623,7 +653,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
623
653
|
await prerenderTempServer?.close().catch(() => {});
|
|
624
654
|
prerenderTempServer = null;
|
|
625
655
|
prerenderNodeRegistry = null;
|
|
626
|
-
return null;
|
|
656
|
+
return { env: null, error: createError };
|
|
627
657
|
}
|
|
628
658
|
|
|
629
659
|
// Clear the package-level singleton registries that survive a Vite
|
|
@@ -676,7 +706,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
676
706
|
// versions / preset configurations may differ in which graph carries
|
|
677
707
|
// the module-runner cache).
|
|
678
708
|
async function refreshTempRscEnv(): Promise<any | null> {
|
|
679
|
-
|
|
709
|
+
const tempRscEnv = (await getOrCreateTempServer()).env;
|
|
680
710
|
if (!tempRscEnv) return null;
|
|
681
711
|
|
|
682
712
|
// Module-runner cache is on the per-environment graph in Vite 6+;
|
|
@@ -702,7 +732,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
702
732
|
prerenderTempServer = null;
|
|
703
733
|
prerenderNodeRegistry = null;
|
|
704
734
|
}
|
|
705
|
-
return await getOrCreateTempServer();
|
|
735
|
+
return (await getOrCreateTempServer()).env;
|
|
706
736
|
}
|
|
707
737
|
|
|
708
738
|
debugDiscovery?.(
|
|
@@ -775,11 +805,11 @@ export function createRouterDiscoveryPlugin(
|
|
|
775
805
|
acquireBuildEnv(s, viteCommand, viteMode),
|
|
776
806
|
);
|
|
777
807
|
|
|
778
|
-
tempRscEnv =
|
|
779
|
-
debugDiscovery,
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
);
|
|
808
|
+
tempRscEnv = (
|
|
809
|
+
await timed(debugDiscovery, "getOrCreateTempServer", () =>
|
|
810
|
+
getOrCreateTempServer(),
|
|
811
|
+
)
|
|
812
|
+
).env;
|
|
783
813
|
if (tempRscEnv) {
|
|
784
814
|
optimizerHashBefore =
|
|
785
815
|
tempRscEnv.depsOptimizer?.metadata?.browserHash;
|
|
@@ -1006,7 +1036,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
1006
1036
|
// instances. Before #654 the cached registry was only refreshed on
|
|
1007
1037
|
// route-file edits, so handler-only edits served stale prerender
|
|
1008
1038
|
// content on this path. Warm-cache re-imports are module-cache hits.
|
|
1009
|
-
const tempRscEnv = await getOrCreateTempServer();
|
|
1039
|
+
const tempRscEnv = (await getOrCreateTempServer()).env;
|
|
1010
1040
|
if (tempRscEnv) {
|
|
1011
1041
|
try {
|
|
1012
1042
|
await importEntryAndRegistry(tempRscEnv);
|
|
@@ -1143,6 +1173,63 @@ export function createRouterDiscoveryPlugin(
|
|
|
1143
1173
|
res.end("Missing pathname/routeName/version/ttl");
|
|
1144
1174
|
return;
|
|
1145
1175
|
}
|
|
1176
|
+
|
|
1177
|
+
// Boot-race readiness signal (issue #719): the capture realm is stood
|
|
1178
|
+
// up lazily on the first hit (temp server, registry import, and a Vite
|
|
1179
|
+
// dep re-optimization the first shell-capture import can trigger). All
|
|
1180
|
+
// are TRANSIENT — a retry seconds later succeeds. Tagging them 503 +
|
|
1181
|
+
// x-rango-shell-dev: NOT-READY lets the read-through re-poll ONLY these
|
|
1182
|
+
// (a bounded await of readiness) instead of mapping the boot window to
|
|
1183
|
+
// a hard first-request MISS. Genuine negatives (404) stay untagged so a
|
|
1184
|
+
// non-baked route never stalls the foreground.
|
|
1185
|
+
const sendNotReady = (detail: string): void => {
|
|
1186
|
+
res.statusCode = 503;
|
|
1187
|
+
res.setHeader("x-rango-shell-dev", "NOT-READY");
|
|
1188
|
+
res.end(detail);
|
|
1189
|
+
};
|
|
1190
|
+
// A Vite dependency re-optimization surfaces as ERR_OUTDATED_OPTIMIZED_DEP
|
|
1191
|
+
// (import throws once, then re-imports clean) — retryable, unlike a real
|
|
1192
|
+
// module fault (syntax error, missing export), which stays a hard 500.
|
|
1193
|
+
// The message-regex fallback is NOT gratuitous: err.code is stripped when
|
|
1194
|
+
// the error serializes across the module-runner/workerd RPC boundary (the
|
|
1195
|
+
// message survives), so under the Cloudflare preset .code alone misses it.
|
|
1196
|
+
const isReoptimizing = (err: any): boolean =>
|
|
1197
|
+
err?.code === "ERR_OUTDATED_OPTIMIZED_DEP" ||
|
|
1198
|
+
/Outdated Optimize Dep|optimized dependency|new dependencies optimized/i.test(
|
|
1199
|
+
String(err?.message ?? ""),
|
|
1200
|
+
);
|
|
1201
|
+
// Fold the reoptimize-guard pasted at all three import/capture catch
|
|
1202
|
+
// sites: emit NOT-READY + report handled for a transient re-optimization,
|
|
1203
|
+
// else leave the caller to send its own terminal (500 for an entry
|
|
1204
|
+
// import, 404 for a mid-capture failure that keeps runtime capture).
|
|
1205
|
+
const handledAsReoptimizing = (err: any): boolean => {
|
|
1206
|
+
if (!isReoptimizing(err)) return false;
|
|
1207
|
+
sendNotReady(`Shell capture re-optimizing: ${err.message}`);
|
|
1208
|
+
return true;
|
|
1209
|
+
};
|
|
1210
|
+
// Both entry-import sites (main-server rsc env, temp Node server) fail
|
|
1211
|
+
// identically: reoptimize → NOT-READY, else → hard 500.
|
|
1212
|
+
const handleShellImportError = (err: any): void => {
|
|
1213
|
+
if (handledAsReoptimizing(err)) return;
|
|
1214
|
+
res.statusCode = 500;
|
|
1215
|
+
res.end(`Shell capture module refresh failed: ${err.message}`);
|
|
1216
|
+
};
|
|
1217
|
+
// Deterministic boot-race injection for e2e (issue #719 P3): fire ONE
|
|
1218
|
+
// reopt-class NOT-READY per pathname through the REAL classifier so the
|
|
1219
|
+
// read-through's re-poll path is exercised end-to-end (not just the
|
|
1220
|
+
// unit-mocked fetch), then serve normally on the re-poll. Env-gated —
|
|
1221
|
+
// inert in every non-test run.
|
|
1222
|
+
if (
|
|
1223
|
+
process.env.RANGO_E2E_INJECT_SHELL_NOTREADY === "1" &&
|
|
1224
|
+
!injectedShellNotReadyPaths.has(pathname)
|
|
1225
|
+
) {
|
|
1226
|
+
injectedShellNotReadyPaths.add(pathname);
|
|
1227
|
+
const injected = Object.assign(
|
|
1228
|
+
new Error("Outdated Optimize Dep (injected boot-race)"),
|
|
1229
|
+
{ code: "ERR_OUTDATED_OPTIMIZED_DEP" },
|
|
1230
|
+
);
|
|
1231
|
+
if (handledAsReoptimizing(injected)) return;
|
|
1232
|
+
}
|
|
1146
1233
|
const ttl = Number(ttlRaw);
|
|
1147
1234
|
const swrRaw = url.searchParams.get("swr");
|
|
1148
1235
|
const swr = swrRaw === null ? undefined : Number(swrRaw);
|
|
@@ -1169,12 +1256,16 @@ export function createRouterDiscoveryPlugin(
|
|
|
1169
1256
|
let rscRealm: any = null;
|
|
1170
1257
|
let ssrRealm: any = null;
|
|
1171
1258
|
let ssrEntryId: string;
|
|
1259
|
+
// Why the temp-server path returned no runner, if it did: a transient
|
|
1260
|
+
// re-optimization is re-pollable (NOT-READY), a terminal create/import
|
|
1261
|
+
// fault is not. Carried from getOrCreateTempServer to the readiness
|
|
1262
|
+
// check below so only reopt re-polls (issue #719 P2).
|
|
1263
|
+
let tempServerError: unknown = null;
|
|
1172
1264
|
if (rscEnvMain?.runner && s.resolvedEntryPath) {
|
|
1173
1265
|
try {
|
|
1174
1266
|
await rscEnvMain.runner.import(s.resolvedEntryPath);
|
|
1175
1267
|
} catch (err: any) {
|
|
1176
|
-
|
|
1177
|
-
res.end(`Shell capture module refresh failed: ${err.message}`);
|
|
1268
|
+
handleShellImportError(err);
|
|
1178
1269
|
return;
|
|
1179
1270
|
}
|
|
1180
1271
|
rscRealm = rscEnvMain;
|
|
@@ -1183,35 +1274,49 @@ export function createRouterDiscoveryPlugin(
|
|
|
1183
1274
|
(server.environments as any)?.ssr?.config?.build?.rollupOptions
|
|
1184
1275
|
?.input?.index ?? VIRTUAL_IDS.ssr;
|
|
1185
1276
|
} else {
|
|
1186
|
-
const
|
|
1187
|
-
if (
|
|
1277
|
+
const tempResult = await getOrCreateTempServer();
|
|
1278
|
+
if (tempResult.env) {
|
|
1188
1279
|
try {
|
|
1189
|
-
await importEntryAndRegistry(
|
|
1280
|
+
await importEntryAndRegistry(tempResult.env);
|
|
1190
1281
|
} catch (err: any) {
|
|
1191
|
-
|
|
1192
|
-
res.end(`Shell capture module refresh failed: ${err.message}`);
|
|
1282
|
+
handleShellImportError(err);
|
|
1193
1283
|
return;
|
|
1194
1284
|
}
|
|
1285
|
+
} else {
|
|
1286
|
+
tempServerError = tempResult.error;
|
|
1195
1287
|
}
|
|
1196
|
-
rscRealm =
|
|
1288
|
+
rscRealm = tempResult.env;
|
|
1197
1289
|
ssrRealm = (prerenderTempServer?.environments as any)?.ssr;
|
|
1198
1290
|
ssrEntryId = "virtual:entry-ssr";
|
|
1199
1291
|
}
|
|
1200
1292
|
if (!rscRealm?.runner || !ssrRealm?.runner) {
|
|
1293
|
+
// Reoptimization is the ONLY transient class: re-poll it (NOT-READY).
|
|
1294
|
+
// A terminal temp-server create/import fault, or an SSR runner absent
|
|
1295
|
+
// after a clean createServer, fails fast with a plain 503 (no
|
|
1296
|
+
// NOT-READY header) so the read-through MISSes on its first attempt
|
|
1297
|
+
// instead of re-polling a permanently-broken realm for the full
|
|
1298
|
+
// readiness deadline (issue #719 P2).
|
|
1299
|
+
if (tempServerError && handledAsReoptimizing(tempServerError)) return;
|
|
1201
1300
|
res.statusCode = 503;
|
|
1202
1301
|
res.end("Shell capture runners not available");
|
|
1203
1302
|
return;
|
|
1204
1303
|
}
|
|
1205
1304
|
let registry: Map<string, any> | null = null;
|
|
1305
|
+
let registryError: unknown = null;
|
|
1206
1306
|
try {
|
|
1207
1307
|
const serverMod = await rscRealm.runner.import(
|
|
1208
1308
|
"@rangojs/router/server",
|
|
1209
1309
|
);
|
|
1210
1310
|
registry = serverMod.RouterRegistry ?? null;
|
|
1211
|
-
} catch {
|
|
1311
|
+
} catch (err: any) {
|
|
1312
|
+
registryError = err;
|
|
1212
1313
|
registry = null;
|
|
1213
1314
|
}
|
|
1214
1315
|
if (!registry || registry.size === 0) {
|
|
1316
|
+
// Same rule as the runner check: a re-optimization mid-import is
|
|
1317
|
+
// re-pollable (NOT-READY); a terminal import fault or a genuinely
|
|
1318
|
+
// empty registry (no routers registered) fails fast (issue #719 P2).
|
|
1319
|
+
if (registryError && handledAsReoptimizing(registryError)) return;
|
|
1215
1320
|
res.statusCode = 503;
|
|
1216
1321
|
res.end("Shell capture registry not available");
|
|
1217
1322
|
return;
|
|
@@ -1300,6 +1405,10 @@ export function createRouterDiscoveryPlugin(
|
|
|
1300
1405
|
res.end(body);
|
|
1301
1406
|
return;
|
|
1302
1407
|
} catch (err: any) {
|
|
1408
|
+
// A dep re-optimization mid-capture is a boot-race, not a capture
|
|
1409
|
+
// failure: signal NOT-READY so the read-through re-polls instead of
|
|
1410
|
+
// conceding a first-request MISS (issue #719).
|
|
1411
|
+
if (handledAsReoptimizing(err)) return;
|
|
1303
1412
|
console.warn(
|
|
1304
1413
|
`[rango] Dev shell capture error for ${pathname} (route keeps runtime capture): ${err.message}`,
|
|
1305
1414
|
);
|