@rangojs/router 0.0.0-experimental.146 → 0.0.0-experimental.148
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/bin/rango.js +7 -0
- package/dist/vite/index.js +823 -235
- package/package.json +6 -1
- package/skills/mime-routes/SKILL.md +25 -17
- package/skills/ppr/SKILL.md +20 -10
- package/src/browser/event-controller.ts +16 -2
- package/src/browser/rsc-router.tsx +11 -0
- package/src/cache/cache-scope.ts +11 -2
- package/src/cache/cf/cf-cache-store.ts +60 -2
- package/src/cache/memory-segment-store.ts +32 -0
- package/src/cache/segment-codec.ts +47 -0
- package/src/cache/types.ts +14 -0
- package/src/cache/vercel/vercel-cache-store.ts +71 -2
- package/src/index.rsc.ts +6 -0
- package/src/prerender/build-shell-capture.ts +253 -0
- package/src/prerender/shell-manifest-key.ts +20 -0
- package/src/prerender/store.ts +10 -1
- package/src/router/content-negotiation.ts +47 -5
- package/src/router/match-middleware/cache-lookup.ts +12 -1
- package/src/router/metrics.ts +17 -2
- package/src/router/prerender-match.ts +21 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +13 -0
- package/src/router.ts +5 -0
- package/src/rsc/capture-queue.ts +67 -0
- package/src/rsc/handler.ts +4 -2
- package/src/rsc/rsc-rendering.ts +131 -23
- package/src/rsc/shell-build-manifest.ts +274 -0
- package/src/rsc/shell-capture.ts +486 -63
- package/src/rsc/shell-serve.ts +44 -0
- package/src/rsc/ssr-setup.ts +54 -22
- package/src/segment-fragments.ts +124 -0
- package/src/server/context.ts +1 -0
- package/src/server/request-context.ts +65 -11
- package/src/ssr/index.tsx +47 -9
- package/src/ssr/ssr-root.tsx +35 -2
- package/src/urls/pattern-types.ts +27 -0
- package/src/vite/discovery/discover-routers.ts +27 -0
- package/src/vite/discovery/prerender-collection.ts +16 -0
- package/src/vite/discovery/shell-prerender-phase.ts +397 -0
- package/src/vite/discovery/state.ts +44 -0
- package/src/vite/plugins/version-plugin.ts +8 -0
- package/src/vite/rango.ts +1 -0
- package/src/vite/router-discovery.ts +310 -8
- package/src/vite/utils/prerender-utils.ts +25 -6
|
@@ -19,11 +19,16 @@ import {
|
|
|
19
19
|
createScanFilter,
|
|
20
20
|
} from "../build/generate-route-types.js";
|
|
21
21
|
import { firstCodeMatchIndex } from "../build/route-types/source-scan.js";
|
|
22
|
+
import {
|
|
23
|
+
DEV_SHELL_PROBE_TIMEOUT_MS,
|
|
24
|
+
normalizeCaptureTimeout,
|
|
25
|
+
} from "../rsc/shell-serve.js";
|
|
22
26
|
import {
|
|
23
27
|
injectClientDebugFlag,
|
|
24
28
|
internalDebugNoCacheMiddleware,
|
|
25
29
|
} from "./inject-client-debug.js";
|
|
26
30
|
import { createVersionPlugin } from "./plugins/version-plugin.js";
|
|
31
|
+
import { getVirtualEntrySSR, VIRTUAL_IDS } from "./plugins/virtual-entries.js";
|
|
27
32
|
import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
|
|
28
33
|
import {
|
|
29
34
|
BUILD_ENV_GLOBAL_KEY,
|
|
@@ -46,6 +51,7 @@ import {
|
|
|
46
51
|
peekSelfGenWrite,
|
|
47
52
|
} from "./discovery/self-gen-tracking.js";
|
|
48
53
|
import { discoverRouters } from "./discovery/discover-routers.js";
|
|
54
|
+
import { runShellPrerenderPhase } from "./discovery/shell-prerender-phase.js";
|
|
49
55
|
import { describeDiscoveryFailure } from "./discovery/discovery-errors.js";
|
|
50
56
|
import {
|
|
51
57
|
createDevPrerenderCache,
|
|
@@ -138,7 +144,21 @@ function ensureCloudflareProtocolLoaderRegistered(): void {
|
|
|
138
144
|
*/
|
|
139
145
|
async function createTempRscServer(
|
|
140
146
|
state: DiscoveryState,
|
|
141
|
-
options: {
|
|
147
|
+
options: {
|
|
148
|
+
forceBuild?: boolean;
|
|
149
|
+
cacheDir?: string;
|
|
150
|
+
/**
|
|
151
|
+
* Serve the REAL rango SSR entry (getVirtualEntrySSR) for
|
|
152
|
+
* "virtual:entry-ssr" instead of the discovery stub, so the dev
|
|
153
|
+
* /__rsc_shell endpoint can drive captureShellHTML in this server's SSR
|
|
154
|
+
* realm. Dev-correct by construction: the entry's bootstrap resolves to
|
|
155
|
+
* plugin-rsc's stable virtual browser-entry URL, which the MAIN dev
|
|
156
|
+
* server serves to the browser. Loaded lazily — discovery never imports
|
|
157
|
+
* the SSR entry, so the temp server stays as light as before until a
|
|
158
|
+
* shell capture actually runs.
|
|
159
|
+
*/
|
|
160
|
+
realSsrEntry?: boolean;
|
|
161
|
+
} = {},
|
|
142
162
|
) {
|
|
143
163
|
// Install the Node ESM loader hook before any module evaluation so
|
|
144
164
|
// `cloudflare:*` specifiers in externalized/loader-delegated modules
|
|
@@ -178,6 +198,26 @@ async function createTempRscServer(
|
|
|
178
198
|
// hashClientRefs only in build mode — production bundles need hashed refs
|
|
179
199
|
...(options.forceBuild ? [hashClientRefs(state.projectRoot)] : []),
|
|
180
200
|
createVersionPlugin(),
|
|
201
|
+
// Before the stub plugin, so "virtual:entry-ssr" resolves to the real
|
|
202
|
+
// SSR entry when the shell endpoint needs it (see the option doc).
|
|
203
|
+
...(options.realSsrEntry
|
|
204
|
+
? [
|
|
205
|
+
{
|
|
206
|
+
name: "@rangojs/router:temp-real-ssr-entry",
|
|
207
|
+
enforce: "pre" as const,
|
|
208
|
+
resolveId(id: string) {
|
|
209
|
+
return id === "virtual:entry-ssr"
|
|
210
|
+
? "\0rango-temp-real-ssr-entry"
|
|
211
|
+
: null;
|
|
212
|
+
},
|
|
213
|
+
load(id: string) {
|
|
214
|
+
return id === "\0rango-temp-real-ssr-entry"
|
|
215
|
+
? getVirtualEntrySSR(state.opts?.headScripts)
|
|
216
|
+
: null;
|
|
217
|
+
},
|
|
218
|
+
} satisfies import("vite").Plugin,
|
|
219
|
+
]
|
|
220
|
+
: []),
|
|
181
221
|
createVirtualStubPlugin(),
|
|
182
222
|
createCloudflareProtocolStubPlugin(),
|
|
183
223
|
// Dev prerender must use dev-mode IDs (path-based) to match the workerd
|
|
@@ -281,6 +321,19 @@ async function acquireBuildEnv(
|
|
|
281
321
|
return true;
|
|
282
322
|
}
|
|
283
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Reset the per-build prerender collection state. A helper (not inline
|
|
326
|
+
* assignments in buildStart) so TS's property narrowing does not pin
|
|
327
|
+
* `s.shellCandidates` to `null` across the discovery call that repopulates
|
|
328
|
+
* it — the finally block re-reads it to decide the temp-server keep-alive.
|
|
329
|
+
*/
|
|
330
|
+
function resetPrerenderCollection(s: DiscoveryState): void {
|
|
331
|
+
s.prerenderManifestEntries = null;
|
|
332
|
+
s.staticManifestEntries = null;
|
|
333
|
+
s.shellCandidates = null;
|
|
334
|
+
s.prerenderPayloadValues = null;
|
|
335
|
+
}
|
|
336
|
+
|
|
284
337
|
/**
|
|
285
338
|
* Release build-time env resources and clear state.
|
|
286
339
|
*/
|
|
@@ -541,6 +594,10 @@ export function createRouterDiscoveryPlugin(
|
|
|
541
594
|
try {
|
|
542
595
|
prerenderTempServer = await createTempRscServer(s, {
|
|
543
596
|
cacheDir: "node_modules/.vite_prerender",
|
|
597
|
+
// The dev /__rsc_shell endpoint drives captureShellHTML in this
|
|
598
|
+
// server's SSR realm; the entry is only imported when a shell
|
|
599
|
+
// capture runs, so discovery cost is unchanged.
|
|
600
|
+
realSsrEntry: true,
|
|
544
601
|
});
|
|
545
602
|
|
|
546
603
|
const tempRscEnv = (prerenderTempServer.environments as any)?.rsc;
|
|
@@ -1057,6 +1114,204 @@ export function createRouterDiscoveryPlugin(
|
|
|
1057
1114
|
logResult(404, "no match");
|
|
1058
1115
|
});
|
|
1059
1116
|
|
|
1117
|
+
// Dev on-demand PPR shell production (producer B, #699). There is no
|
|
1118
|
+
// build manifest in dev, so the serve path's read-through
|
|
1119
|
+
// (rsc/shell-build-manifest.ts) fetches the shell entry from here on a
|
|
1120
|
+
// Prerender+ppr route's first request — dev serves x-rango-shell: HIT
|
|
1121
|
+
// from request one, mirroring production. Memoized per router HMR
|
|
1122
|
+
// generation AND per caller version (a client-module edit bumps the
|
|
1123
|
+
// version without rotating the router instance; the stale entry would
|
|
1124
|
+
// fail the serve gate forever). The endpoint is policy-free: the caller
|
|
1125
|
+
// (the serve gate, which resolved the route's ppr option) sends
|
|
1126
|
+
// ttl/swr/tags/version. Only prerender-backed routes produce entries —
|
|
1127
|
+
// the /__rsc_prerender pre-flight below both warms the payload memo the
|
|
1128
|
+
// capture's dev store fetch will hit AND refuses non-prerenderable
|
|
1129
|
+
// routes (a live-handler render must never be served as a baked shell).
|
|
1130
|
+
server.middlewares.use("/__rsc_shell", async (req: any, res: any) => {
|
|
1131
|
+
await s.discoveryDone;
|
|
1132
|
+
const url = new URL(req.url ?? "", "http://localhost");
|
|
1133
|
+
const pathname = url.searchParams.get("pathname");
|
|
1134
|
+
const routeName = url.searchParams.get("routeName");
|
|
1135
|
+
const version = url.searchParams.get("version");
|
|
1136
|
+
// ttl is required like the identifiers: the endpoint is policy-free
|
|
1137
|
+
// (the serve gate resolved the route's ppr option and always sends
|
|
1138
|
+
// it), so there is deliberately no default to drift from
|
|
1139
|
+
// resolvePprConfig's.
|
|
1140
|
+
const ttlRaw = url.searchParams.get("ttl");
|
|
1141
|
+
if (!pathname || !routeName || !version || !ttlRaw) {
|
|
1142
|
+
res.statusCode = 400;
|
|
1143
|
+
res.end("Missing pathname/routeName/version/ttl");
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const ttl = Number(ttlRaw);
|
|
1147
|
+
const swrRaw = url.searchParams.get("swr");
|
|
1148
|
+
const swr = swrRaw === null ? undefined : Number(swrRaw);
|
|
1149
|
+
const tagsRaw = url.searchParams.get("tags");
|
|
1150
|
+
const tags = tagsRaw ? tagsRaw.split(",") : undefined;
|
|
1151
|
+
const maxSnapshotBytesRaw = url.searchParams.get("maxSnapshotBytes");
|
|
1152
|
+
const maxSnapshotBytes =
|
|
1153
|
+
maxSnapshotBytesRaw === null
|
|
1154
|
+
? undefined
|
|
1155
|
+
: Number(maxSnapshotBytesRaw);
|
|
1156
|
+
// Boundary revalidation via the SHARED normalizer (shell-serve.ts):
|
|
1157
|
+
// the param crossed an HTTP query string, and a garbage value must
|
|
1158
|
+
// fall back to the capture default, never reach setTimeout as NaN
|
|
1159
|
+
// (which Node clamps to ~1ms — an instant abort).
|
|
1160
|
+
const captureTimeout = normalizeCaptureTimeout(
|
|
1161
|
+
Number(url.searchParams.get("captureTimeout")),
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
// Resolve the capture realms: main-server envs (Node preset) or the
|
|
1165
|
+
// shared temp Node server (Cloudflare preset — no main RSC runner).
|
|
1166
|
+
// Entry re-import per request picks up HMR edits, exactly like the
|
|
1167
|
+
// prerender endpoint above.
|
|
1168
|
+
const rscEnvMain = (server.environments as any)?.rsc;
|
|
1169
|
+
let rscRealm: any = null;
|
|
1170
|
+
let ssrRealm: any = null;
|
|
1171
|
+
let ssrEntryId: string;
|
|
1172
|
+
if (rscEnvMain?.runner && s.resolvedEntryPath) {
|
|
1173
|
+
try {
|
|
1174
|
+
await rscEnvMain.runner.import(s.resolvedEntryPath);
|
|
1175
|
+
} catch (err: any) {
|
|
1176
|
+
res.statusCode = 500;
|
|
1177
|
+
res.end(`Shell capture module refresh failed: ${err.message}`);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
rscRealm = rscEnvMain;
|
|
1181
|
+
ssrRealm = (server.environments as any)?.ssr;
|
|
1182
|
+
ssrEntryId =
|
|
1183
|
+
(server.environments as any)?.ssr?.config?.build?.rollupOptions
|
|
1184
|
+
?.input?.index ?? VIRTUAL_IDS.ssr;
|
|
1185
|
+
} else {
|
|
1186
|
+
const tempRscEnv = await getOrCreateTempServer();
|
|
1187
|
+
if (tempRscEnv) {
|
|
1188
|
+
try {
|
|
1189
|
+
await importEntryAndRegistry(tempRscEnv);
|
|
1190
|
+
} catch (err: any) {
|
|
1191
|
+
res.statusCode = 500;
|
|
1192
|
+
res.end(`Shell capture module refresh failed: ${err.message}`);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
rscRealm = tempRscEnv;
|
|
1197
|
+
ssrRealm = (prerenderTempServer?.environments as any)?.ssr;
|
|
1198
|
+
ssrEntryId = "virtual:entry-ssr";
|
|
1199
|
+
}
|
|
1200
|
+
if (!rscRealm?.runner || !ssrRealm?.runner) {
|
|
1201
|
+
res.statusCode = 503;
|
|
1202
|
+
res.end("Shell capture runners not available");
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
let registry: Map<string, any> | null = null;
|
|
1206
|
+
try {
|
|
1207
|
+
const serverMod = await rscRealm.runner.import(
|
|
1208
|
+
"@rangojs/router/server",
|
|
1209
|
+
);
|
|
1210
|
+
registry = serverMod.RouterRegistry ?? null;
|
|
1211
|
+
} catch {
|
|
1212
|
+
registry = null;
|
|
1213
|
+
}
|
|
1214
|
+
if (!registry || registry.size === 0) {
|
|
1215
|
+
res.statusCode = 503;
|
|
1216
|
+
res.end("Shell capture registry not available");
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Memo sweep FIRST: after request one the common case is a memo HIT
|
|
1221
|
+
// (this fetch blocks a foreground document request), and the memoized
|
|
1222
|
+
// body needs neither the pre-flight round-trip nor a capture. Keyed
|
|
1223
|
+
// per router instance (= HMR generation) like the prerender memo.
|
|
1224
|
+
const cacheKey = `shell|${pathname}|r=${routeName}|t=${ttl}|s=${swr ?? ""}|g=${(tags ?? []).join("+")}|c=${captureTimeout ?? ""}|v=${version}`;
|
|
1225
|
+
for (const [, routerInstance] of registry) {
|
|
1226
|
+
if (typeof routerInstance.match !== "function") continue;
|
|
1227
|
+
const cached = devPrerenderCache.get(routerInstance, cacheKey);
|
|
1228
|
+
if (cached !== undefined) {
|
|
1229
|
+
res.setHeader("content-type", "application/json");
|
|
1230
|
+
res.setHeader("x-rango-shell-dev", "HIT");
|
|
1231
|
+
res.end(cached);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Pre-flight: the route must be prerender-backed. Warms the payload
|
|
1237
|
+
// memo the capture's dev prerender store will fetch, and closes the
|
|
1238
|
+
// live-handler-bake hole (a non-pr route 404s here).
|
|
1239
|
+
if (s.devServerOrigin) {
|
|
1240
|
+
try {
|
|
1241
|
+
const probe = await fetch(
|
|
1242
|
+
`${s.devServerOrigin}/__rsc_prerender?pathname=${encodeURIComponent(pathname)}&routeName=${encodeURIComponent(routeName)}`,
|
|
1243
|
+
{ signal: AbortSignal.timeout(DEV_SHELL_PROBE_TIMEOUT_MS) },
|
|
1244
|
+
);
|
|
1245
|
+
if (!probe.ok) {
|
|
1246
|
+
res.statusCode = 404;
|
|
1247
|
+
res.end("Route is not prerenderable");
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
} catch {
|
|
1251
|
+
res.statusCode = 404;
|
|
1252
|
+
res.end("Prerender pre-flight failed");
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
for (const [, routerInstance] of registry) {
|
|
1258
|
+
if (typeof routerInstance.match !== "function") continue;
|
|
1259
|
+
try {
|
|
1260
|
+
const ssrModule = await ssrRealm.runner.import(ssrEntryId);
|
|
1261
|
+
if (typeof ssrModule?.captureShellHTML !== "function") {
|
|
1262
|
+
res.statusCode = 404;
|
|
1263
|
+
res.end("SSR entry has no captureShellHTML");
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const captureMod = await rscRealm.runner.import(
|
|
1267
|
+
"@rangojs/router/build/shell-capture",
|
|
1268
|
+
);
|
|
1269
|
+
const result = await captureMod.captureShellForBuild({
|
|
1270
|
+
router: routerInstance,
|
|
1271
|
+
urlPath: pathname,
|
|
1272
|
+
routeName,
|
|
1273
|
+
key: `${pathname}:shell`,
|
|
1274
|
+
ttl,
|
|
1275
|
+
swr,
|
|
1276
|
+
tags,
|
|
1277
|
+
maxSnapshotBytes,
|
|
1278
|
+
captureTimeout,
|
|
1279
|
+
buildEnv: s.resolvedBuildEnv,
|
|
1280
|
+
buildVersion: version,
|
|
1281
|
+
captureShellHTML: ssrModule.captureShellHTML,
|
|
1282
|
+
debug: !!debugDiscovery,
|
|
1283
|
+
});
|
|
1284
|
+
if (result.outcome === "route-mismatch") continue;
|
|
1285
|
+
if (result.outcome !== "stored" || !result.entry) {
|
|
1286
|
+
res.statusCode = 404;
|
|
1287
|
+
res.end(`Shell capture ${result.outcome}`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const body = JSON.stringify({
|
|
1291
|
+
entry: result.entry,
|
|
1292
|
+
ttl,
|
|
1293
|
+
swr,
|
|
1294
|
+
tags: result.tags,
|
|
1295
|
+
routeName,
|
|
1296
|
+
});
|
|
1297
|
+
devPrerenderCache.set(routerInstance, cacheKey, body);
|
|
1298
|
+
res.setHeader("content-type", "application/json");
|
|
1299
|
+
res.setHeader("x-rango-shell-dev", "MISS");
|
|
1300
|
+
res.end(body);
|
|
1301
|
+
return;
|
|
1302
|
+
} catch (err: any) {
|
|
1303
|
+
console.warn(
|
|
1304
|
+
`[rango] Dev shell capture error for ${pathname} (route keeps runtime capture): ${err.message}`,
|
|
1305
|
+
);
|
|
1306
|
+
res.statusCode = 404;
|
|
1307
|
+
res.end(`Shell capture error: ${err.message}`);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
res.statusCode = 404;
|
|
1312
|
+
res.end("No router matched");
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1060
1315
|
// Watch url module and router files for changes and regenerate named-routes.gen.ts.
|
|
1061
1316
|
// Process files containing urls( or createRouter( to update the combined route map.
|
|
1062
1317
|
if (opts?.staticRouteTypesGeneration !== false) {
|
|
@@ -1430,8 +1685,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
1430
1685
|
const buildStartTime = performance.now();
|
|
1431
1686
|
debugDiscovery?.("build: start (env=%s)", this.environment?.name ?? "?");
|
|
1432
1687
|
resetStagedBuildAssets(s.projectRoot);
|
|
1433
|
-
s
|
|
1434
|
-
s.staticManifestEntries = null;
|
|
1688
|
+
resetPrerenderCollection(s);
|
|
1435
1689
|
|
|
1436
1690
|
// Acquire build-time env bindings if configured
|
|
1437
1691
|
await timed(debugDiscovery, "build acquireBuildEnv", () =>
|
|
@@ -1502,12 +1756,23 @@ export function createRouterDiscoveryPlugin(
|
|
|
1502
1756
|
);
|
|
1503
1757
|
} finally {
|
|
1504
1758
|
delete (globalThis as any).__rscRouterDiscoveryActive;
|
|
1505
|
-
if (tempServer) {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
)
|
|
1759
|
+
if (tempServer && s.shellCandidates?.length) {
|
|
1760
|
+
// Prerender+ppr candidates exist: keep the temp server (and its
|
|
1761
|
+
// realm — tries installed, registry populated) alive for the
|
|
1762
|
+
// post-build shell capture phase (buildApp post, producer B #699).
|
|
1763
|
+
// The prelude embeds built client asset URLs, so the capture can
|
|
1764
|
+
// only run after the client build; that phase closes the server.
|
|
1765
|
+
// buildEnv release is deferred with it — a bake-lane loader
|
|
1766
|
+
// executing during the capture may read ctx.env.
|
|
1767
|
+
s.shellPhaseTempServer = tempServer;
|
|
1768
|
+
} else {
|
|
1769
|
+
if (tempServer) {
|
|
1770
|
+
await timed(debugDiscovery, "build tempServer.close", () =>
|
|
1771
|
+
tempServer.close(),
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
await releaseBuildEnv(s);
|
|
1509
1775
|
}
|
|
1510
|
-
await releaseBuildEnv(s);
|
|
1511
1776
|
debugDiscovery?.(
|
|
1512
1777
|
"build discovery done (%sms)",
|
|
1513
1778
|
(performance.now() - buildStartTime).toFixed(1),
|
|
@@ -1515,6 +1780,43 @@ export function createRouterDiscoveryPlugin(
|
|
|
1515
1780
|
}
|
|
1516
1781
|
},
|
|
1517
1782
|
|
|
1783
|
+
// Post-build PPR shell capture (producer B, #699): runs after EVERY
|
|
1784
|
+
// environment bundle is written — the shell prelude embeds built client
|
|
1785
|
+
// asset URLs (bootstrap entry), which do not exist at buildStart. The
|
|
1786
|
+
// kept temp server and the buildEnv were deferred AS A PAIR in
|
|
1787
|
+
// buildStart's finally; this finally is the pair's success-path owner
|
|
1788
|
+
// (buildEnd below owns the aborted-build path) — the phase itself is a
|
|
1789
|
+
// pure producer and tears down only the globals it installs.
|
|
1790
|
+
buildApp: {
|
|
1791
|
+
order: "post",
|
|
1792
|
+
async handler(builder) {
|
|
1793
|
+
try {
|
|
1794
|
+
await runShellPrerenderPhase(s, builder as any);
|
|
1795
|
+
} finally {
|
|
1796
|
+
if (s.isBuildMode) {
|
|
1797
|
+
const tempServer = s.shellPhaseTempServer;
|
|
1798
|
+
s.shellPhaseTempServer = null;
|
|
1799
|
+
if (tempServer) await tempServer.close();
|
|
1800
|
+
await releaseBuildEnv(s);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
},
|
|
1804
|
+
},
|
|
1805
|
+
|
|
1806
|
+
// An environment build failure aborts the builder before the buildApp
|
|
1807
|
+
// post hook — never leak the kept temp server (open handles hang the CLI)
|
|
1808
|
+
// or the deferred buildEnv (a live miniflare proxy).
|
|
1809
|
+
async buildEnd(error) {
|
|
1810
|
+
if (!error || !s.shellPhaseTempServer) return;
|
|
1811
|
+
const tempServer = s.shellPhaseTempServer;
|
|
1812
|
+
s.shellPhaseTempServer = null;
|
|
1813
|
+
try {
|
|
1814
|
+
await tempServer.close();
|
|
1815
|
+
} finally {
|
|
1816
|
+
await releaseBuildEnv(s);
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
|
|
1518
1820
|
// Suppress vite's HMR cascade for our own gen-file writes.
|
|
1519
1821
|
//
|
|
1520
1822
|
// After every cf HMR cycle, refreshTempRscEnv → writeRouteTypesFiles
|
|
@@ -207,20 +207,27 @@ export function resetStagedBuildAssets(projectRoot: string): void {
|
|
|
207
207
|
rmSync(getStagedAssetDir(projectRoot), { recursive: true, force: true });
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Write one content-hashed `export default <value>;` asset module into `dir`
|
|
212
|
+
* (created if needed) and return its file name. Identical payloads dedupe to
|
|
213
|
+
* one file (the hash is the content). Shared by the staged prerender/static
|
|
214
|
+
* flow (stageBuildAssetModule below) and the shell prerender phase, which
|
|
215
|
+
* writes directly into the final RSC assets dir — it runs after buildApp,
|
|
216
|
+
* past the staging/copy window.
|
|
217
|
+
*/
|
|
218
|
+
export function writeBuildAssetModule(
|
|
219
|
+
dir: string,
|
|
220
|
+
prefix: "__pr" | "__st" | "__ps",
|
|
213
221
|
exportValue: string,
|
|
214
222
|
): string {
|
|
215
|
-
|
|
216
|
-
mkdirSync(stagedDir, { recursive: true });
|
|
223
|
+
mkdirSync(dir, { recursive: true });
|
|
217
224
|
|
|
218
225
|
const contentHash = createHash("sha256")
|
|
219
226
|
.update(exportValue)
|
|
220
227
|
.digest("hex")
|
|
221
228
|
.slice(0, 8);
|
|
222
229
|
const fileName = `${prefix}-${contentHash}.js`;
|
|
223
|
-
const filePath = resolve(
|
|
230
|
+
const filePath = resolve(dir, fileName);
|
|
224
231
|
|
|
225
232
|
if (!existsSync(filePath)) {
|
|
226
233
|
writeFileSync(filePath, `export default ${exportValue};\n`);
|
|
@@ -229,6 +236,18 @@ export function stageBuildAssetModule(
|
|
|
229
236
|
return fileName;
|
|
230
237
|
}
|
|
231
238
|
|
|
239
|
+
export function stageBuildAssetModule(
|
|
240
|
+
projectRoot: string,
|
|
241
|
+
prefix: "__pr" | "__st",
|
|
242
|
+
exportValue: string,
|
|
243
|
+
): string {
|
|
244
|
+
return writeBuildAssetModule(
|
|
245
|
+
getStagedAssetDir(projectRoot),
|
|
246
|
+
prefix,
|
|
247
|
+
exportValue,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
232
251
|
export function copyStagedBuildAssets(
|
|
233
252
|
projectRoot: string,
|
|
234
253
|
fileNames: Iterable<string>,
|