@rangojs/router 0.1.0 → 0.1.1
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 +11 -1
- package/dist/types/browser/dev-discovery.d.ts +11 -0
- package/dist/types/browser/navigation-store.d.ts +3 -13
- package/dist/types/browser/prefetch/loader.d.ts +10 -0
- package/dist/types/browser/prefetch/runtime.d.ts +3 -0
- package/dist/types/browser/types.d.ts +4 -16
- package/dist/types/build/generate-manifest.d.ts +6 -0
- package/dist/types/cache/cache-scope.d.ts +7 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +10 -9
- package/dist/types/cache/memory-segment-store.d.ts +6 -6
- package/dist/types/cache/shell-snapshot.d.ts +21 -8
- package/dist/types/cache/types.d.ts +24 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +9 -8
- package/dist/types/dev-discovery-protocol.d.ts +5 -0
- package/dist/types/prerender/store.d.ts +1 -0
- package/dist/types/route-map-builder.d.ts +4 -29
- package/dist/types/router/prerender-match.d.ts +4 -1
- package/dist/types/router/router-interfaces.d.ts +3 -1
- package/dist/types/rsc/handler-context.d.ts +1 -0
- package/dist/types/rsc/render-pipeline.d.ts +3 -2
- package/dist/types/rsc/rsc-rendering.d.ts +5 -10
- package/dist/types/rsc/shell-capture.d.ts +4 -8
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/server/context.d.ts +16 -0
- package/dist/types/server/request-context.d.ts +18 -8
- package/dist/types/server.d.ts +1 -1
- package/dist/types/types/route-entry.d.ts +3 -0
- package/dist/types/vite/discovery/state.d.ts +3 -2
- package/dist/types/vite/utils/manifest-utils.d.ts +1 -1
- package/dist/vite/index.js +194 -107
- package/package.json +7 -2
- package/skills/caching/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +48 -4
- package/src/browser/dev-discovery.ts +66 -0
- package/src/browser/navigation-client.ts +6 -0
- package/src/browser/navigation-store.ts +4 -271
- package/src/browser/partial-update.ts +7 -14
- package/src/browser/prefetch/cache.ts +1 -1
- package/src/browser/prefetch/loader.ts +110 -0
- package/src/browser/prefetch/runtime.ts +7 -0
- package/src/browser/react/Link.tsx +7 -10
- package/src/browser/react/NavigationProvider.tsx +1 -1
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/rsc-router.tsx +4 -2
- package/src/browser/types.ts +4 -27
- package/src/build/generate-manifest.ts +11 -0
- package/src/cache/cache-scope.ts +17 -6
- package/src/cache/cf/cf-cache-store.ts +63 -39
- package/src/cache/memory-segment-store.ts +17 -6
- package/src/cache/shell-snapshot.ts +45 -15
- package/src/cache/types.ts +23 -6
- package/src/cache/vercel/vercel-cache-store.ts +52 -22
- package/src/dev-discovery-protocol.ts +11 -0
- package/src/prerender/build-shell-capture.ts +7 -1
- package/src/prerender/store.ts +5 -0
- package/src/route-definition/dsl-helpers.ts +7 -2
- package/src/route-map-builder.ts +56 -49
- package/src/router/handler-context.ts +13 -5
- package/src/router/lazy-includes.ts +1 -0
- package/src/router/match-api.ts +8 -4
- package/src/router/prerender-match.ts +19 -2
- package/src/router/router-interfaces.ts +4 -0
- package/src/router/segment-resolution/static-store.ts +36 -10
- package/src/router.ts +34 -3
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +3 -1
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +4 -11
- package/src/rsc/render-pipeline.ts +10 -2
- package/src/rsc/rsc-rendering.ts +207 -146
- package/src/rsc/shell-capture.ts +34 -11
- package/src/rsc/types.ts +2 -0
- package/src/server/context.ts +28 -0
- package/src/server/request-context.ts +32 -10
- package/src/server.ts +0 -2
- package/src/testing/dispatch.ts +1 -0
- package/src/types/route-entry.ts +3 -0
- package/src/urls/include-helper.ts +1 -0
- package/src/urls/path-helper.ts +8 -4
- package/src/vite/discovery/bundle-postprocess.ts +10 -7
- package/src/vite/discovery/discover-routers.ts +7 -66
- package/src/vite/discovery/prerender-collection.ts +13 -2
- package/src/vite/discovery/state.ts +4 -4
- package/src/vite/discovery/virtual-module-codegen.ts +75 -42
- package/src/vite/plugins/version-injector.ts +0 -1
- package/src/vite/plugins/virtual-entries.ts +11 -1
- package/src/vite/router-discovery.ts +132 -22
- package/src/vite/utils/manifest-utils.ts +1 -4
- package/src/vite/utils/shared-utils.ts +7 -0
|
@@ -24,6 +24,19 @@ import type { DiscoveryState } from "./state.js";
|
|
|
24
24
|
*/
|
|
25
25
|
const MANIFEST_EXTERNALIZE_THRESHOLD = 512 * 1024;
|
|
26
26
|
|
|
27
|
+
function devDiscoveryBootstrap(state: DiscoveryState): string[] {
|
|
28
|
+
if (
|
|
29
|
+
state.isBuildMode ||
|
|
30
|
+
state.opts?.preset !== "cloudflare" ||
|
|
31
|
+
state.devDiscoveryEpoch === undefined
|
|
32
|
+
) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const epoch = state.devDiscoveryEpoch;
|
|
37
|
+
return [`globalThis.__RANGO_DEV_DISCOVERY_EPOCH = ${epoch};`];
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
/**
|
|
28
41
|
* Generate the code for the main virtual:rsc-router/routes-manifest module.
|
|
29
42
|
*/
|
|
@@ -39,11 +52,9 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
39
52
|
// module and re-evaluates it on the next request, calling
|
|
40
53
|
// setCachedManifest() with fresh data. No manual sync needed.
|
|
41
54
|
const genFileImports: string[] = [];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
manifest: Record<string, string>;
|
|
46
|
-
}> = [];
|
|
55
|
+
// Per-entry NamedRoutes import var, aligned with perRouterManifests
|
|
56
|
+
// (null for routers without a gen file).
|
|
57
|
+
const genFileVarByEntry: Array<string | null> = [];
|
|
47
58
|
let varIdx = 0;
|
|
48
59
|
|
|
49
60
|
for (const entry of state.perRouterManifests) {
|
|
@@ -61,17 +72,20 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
61
72
|
genFileImports.push(
|
|
62
73
|
`import { NamedRoutes as ${varName} } from ${JSON.stringify(genPath)};`,
|
|
63
74
|
);
|
|
64
|
-
|
|
75
|
+
genFileVarByEntry.push(varName);
|
|
65
76
|
} else {
|
|
66
|
-
|
|
67
|
-
id: entry.id,
|
|
68
|
-
manifest: entry.routeManifest,
|
|
69
|
-
});
|
|
77
|
+
genFileVarByEntry.push(null);
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
const serverImports = [
|
|
82
|
+
"setCachedManifest",
|
|
83
|
+
"setRouterManifest",
|
|
84
|
+
...(state.isBuildMode ? ["registerRouterManifestLoader"] : []),
|
|
85
|
+
"clearAllRouterData",
|
|
86
|
+
];
|
|
73
87
|
const lines = [
|
|
74
|
-
`import {
|
|
88
|
+
`import { ${serverImports.join(", ")} } from "@rangojs/router/server";`,
|
|
75
89
|
...genFileImports,
|
|
76
90
|
// Clear stale per-router cached data (manifest, trie, precomputed entries)
|
|
77
91
|
// before re-populating. In Cloudflare dev mode, program reloads re-evaluate
|
|
@@ -81,62 +95,80 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
81
95
|
`clearAllRouterData();`,
|
|
82
96
|
];
|
|
83
97
|
|
|
84
|
-
if (
|
|
98
|
+
if (varIdx > 0) {
|
|
85
99
|
lines.push(
|
|
86
100
|
`function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`,
|
|
87
101
|
);
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
// Each router's flattened map is materialized ONCE and the same object is
|
|
105
|
+
// shared between the global cachedManifest and the per-router map (a
|
|
106
|
+
// single-router app previously held two byte-identical maps, and __flat
|
|
107
|
+
// ran twice per router on cold start). Sharing is safe: every consumer
|
|
108
|
+
// reads these maps; lazy-include deltas mutate globalRouteMap, a different
|
|
109
|
+
// object (see registerRouteMap in route-map-builder.ts).
|
|
110
|
+
const routerMapVars: string[] = [];
|
|
111
|
+
for (const [i, entry] of state.perRouterManifests.entries()) {
|
|
112
|
+
const mapVar = `__m${i}`;
|
|
113
|
+
const genVar = genFileVarByEntry[i];
|
|
114
|
+
const init = genVar
|
|
115
|
+
? `__flat(${genVar})`
|
|
116
|
+
: jsonParseExpression(entry.routeManifest);
|
|
117
|
+
lines.push(`const ${mapVar} = ${init};`);
|
|
118
|
+
routerMapVars.push(mapVar);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (routerMapVars.length === 1) {
|
|
122
|
+
lines.push(`setCachedManifest(${routerMapVars[0]});`);
|
|
92
123
|
} else {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
parts.push(`...${jsonParseExpression(manifest)}`);
|
|
97
|
-
lines.push(`setCachedManifest({ ${parts.join(", ")} });`);
|
|
124
|
+
lines.push(
|
|
125
|
+
`setCachedManifest({ ${routerMapVars.map((v) => `...${v}`).join(", ")} });`,
|
|
126
|
+
);
|
|
98
127
|
}
|
|
99
128
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
lines.push(
|
|
105
|
-
`setRouterManifest(${JSON.stringify(entry.id)}, __flat(${varName}));`,
|
|
106
|
-
);
|
|
107
|
-
} else {
|
|
108
|
-
lines.push(
|
|
109
|
-
`setRouterManifest(${JSON.stringify(entry.id)}, ${jsonParseExpression(entry.routeManifest)});`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
129
|
+
for (const [i, entry] of state.perRouterManifests.entries()) {
|
|
130
|
+
lines.push(
|
|
131
|
+
`setRouterManifest(${JSON.stringify(entry.id)}, ${routerMapVars[i]});`,
|
|
132
|
+
);
|
|
112
133
|
}
|
|
113
134
|
|
|
114
135
|
// Per-router trie and precomputedEntries are NOT inlined eagerly.
|
|
115
136
|
// They live in the per-router lazy chunks (generatePerRouterModule) and
|
|
116
137
|
// are loaded via ensureRouterManifest(routerId), which is awaited before
|
|
117
|
-
// every request in router.fetch() and before findMatch is reached
|
|
118
|
-
//
|
|
119
|
-
// (the merged trie/precomputedEntries equal the per-router data for
|
|
120
|
-
// single-router apps; for multi-router, the merged trie is dead code
|
|
121
|
-
// because find-match.ts only consumes per-router tries).
|
|
138
|
+
// every request in router.fetch() and before findMatch is reached
|
|
139
|
+
// (Bundle Hygiene rule #1: route data in exactly one lazy chunk).
|
|
122
140
|
//
|
|
123
141
|
// In dev mode, the handler also falls back to Phase 2 regex matching
|
|
124
142
|
// against live router.urlpatterns, which is always correct after a
|
|
125
143
|
// program reload.
|
|
126
144
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
// Loaders are registered in BUILD only. ensureRouterManifest() marks a
|
|
146
|
+
// loader-supplied trie authoritative (misses are hard 404s), which is only
|
|
147
|
+
// valid for a build-time trie from complete discovery. In dev the loader
|
|
148
|
+
// would preempt the handler's buildRouterTrieFromUrlpatterns fallback
|
|
149
|
+
// (rsc/handler.ts) and can install a STALE trie after a Cloudflare program
|
|
150
|
+
// reload: the per-router virtual module's only invalidation edge is the
|
|
151
|
+
// gen-file import, which lags the urls.tsx edit by one reload cycle, so a
|
|
152
|
+
// browser full-reload landing in that window renders a removed route from
|
|
153
|
+
// the stale-but-authoritative trie and never converges. Dev must always
|
|
154
|
+
// rebuild from live router.urlpatterns instead.
|
|
155
|
+
if (state.isBuildMode) {
|
|
156
|
+
for (const routerId of state.perRouterManifestDataMap.keys()) {
|
|
157
|
+
lines.push(
|
|
158
|
+
`registerRouterManifestLoader(${JSON.stringify(routerId)}, () => import(${JSON.stringify(VIRTUAL_ROUTES_MANIFEST_ID + "/" + routerId)}));`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
131
161
|
}
|
|
132
162
|
if (!state.isBuildMode && state.devServerOrigin) {
|
|
133
163
|
lines.push(
|
|
134
164
|
`globalThis.__PRERENDER_DEV_URL = ${JSON.stringify(state.devServerOrigin)};`,
|
|
135
165
|
);
|
|
136
166
|
}
|
|
167
|
+
lines.push(...devDiscoveryBootstrap(state));
|
|
137
168
|
return lines.join("\n");
|
|
138
169
|
}
|
|
139
170
|
|
|
171
|
+
const lines: string[] = [];
|
|
140
172
|
if (!state.isBuildMode) {
|
|
141
173
|
const origin =
|
|
142
174
|
state.devServerOrigin ||
|
|
@@ -145,10 +177,11 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
145
177
|
`http://localhost:${state.devServer.config.server.port || 5173}`);
|
|
146
178
|
if (origin) {
|
|
147
179
|
state.devServerOrigin = origin;
|
|
148
|
-
|
|
180
|
+
lines.push(`globalThis.__PRERENDER_DEV_URL = ${JSON.stringify(origin)};`);
|
|
149
181
|
}
|
|
150
182
|
}
|
|
151
|
-
|
|
183
|
+
lines.push(...devDiscoveryBootstrap(state));
|
|
184
|
+
return lines.join("\n") || `// Route manifest will be populated at runtime`;
|
|
152
185
|
}
|
|
153
186
|
|
|
154
187
|
/**
|
|
@@ -26,7 +26,17 @@ async function initializeApp() {
|
|
|
26
26
|
// context, including strictMode (default true) from createRouter. StrictMode
|
|
27
27
|
// is the default; createRouter({ strictMode: false }) ships the opt-out in the
|
|
28
28
|
// payload metadata. StrictMode emits no DOM, so toggling never changes markup.
|
|
29
|
-
const { strictMode } = await initBrowserApp({ rscStream, deps });
|
|
29
|
+
const { strictMode, initialPayload } = await initBrowserApp({ rscStream, deps });
|
|
30
|
+
|
|
31
|
+
if (import.meta.hot) {
|
|
32
|
+
const { startDevDiscoveryHandshake } = await import(
|
|
33
|
+
"@rangojs/router/internal/browser/dev-discovery"
|
|
34
|
+
);
|
|
35
|
+
startDevDiscoveryHandshake(
|
|
36
|
+
initialPayload.metadata?.devDiscoveryEpoch,
|
|
37
|
+
import.meta.hot
|
|
38
|
+
);
|
|
39
|
+
}
|
|
30
40
|
|
|
31
41
|
const app = createElement(Rango);
|
|
32
42
|
hydrateRoot(
|
|
@@ -19,6 +19,12 @@ 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_DISCOVERY_EPOCH_HEADER,
|
|
24
|
+
DEV_DISCOVERY_PROBE_HEADER,
|
|
25
|
+
DEV_DISCOVERY_QUERY_EVENT,
|
|
26
|
+
DEV_DISCOVERY_READY_EVENT,
|
|
27
|
+
} from "../dev-discovery-protocol.js";
|
|
22
28
|
import {
|
|
23
29
|
DEV_SHELL_PROBE_TIMEOUT_MS,
|
|
24
30
|
normalizeCaptureTimeout,
|
|
@@ -472,6 +478,45 @@ export function createRouterDiscoveryPlugin(
|
|
|
472
478
|
if ((globalThis as any).__rscRouterDiscoveryActive) return;
|
|
473
479
|
s.devServer = server;
|
|
474
480
|
|
|
481
|
+
let workerReadyEpoch: number | undefined;
|
|
482
|
+
const publishDevDiscoveryReady = (epoch: number) => {
|
|
483
|
+
if (epoch <= (workerReadyEpoch ?? -1)) return;
|
|
484
|
+
workerReadyEpoch = epoch;
|
|
485
|
+
(server.environments as any)?.client?.hot?.send({
|
|
486
|
+
type: "custom",
|
|
487
|
+
event: DEV_DISCOVERY_READY_EVENT,
|
|
488
|
+
data: { epoch },
|
|
489
|
+
});
|
|
490
|
+
debugDiscovery?.("hmr: workerd ready at epoch %d", epoch);
|
|
491
|
+
};
|
|
492
|
+
if (opts?.preset === "cloudflare") {
|
|
493
|
+
const registeredClientHotChannels = new Set<any>();
|
|
494
|
+
const registerDevDiscoveryHotChannels = () => {
|
|
495
|
+
const clientHot = (server.environments as any)?.client?.hot;
|
|
496
|
+
if (clientHot && !registeredClientHotChannels.has(clientHot)) {
|
|
497
|
+
registeredClientHotChannels.add(clientHot);
|
|
498
|
+
clientHot.on(
|
|
499
|
+
DEV_DISCOVERY_QUERY_EVENT,
|
|
500
|
+
(_payload: unknown, client: any) => {
|
|
501
|
+
if (workerReadyEpoch === undefined) return;
|
|
502
|
+
client.send(DEV_DISCOVERY_READY_EVENT, {
|
|
503
|
+
epoch: workerReadyEpoch,
|
|
504
|
+
});
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
debugDiscovery?.(
|
|
509
|
+
"hmr: dev discovery browser channel registered (client=%s)",
|
|
510
|
+
!!clientHot,
|
|
511
|
+
);
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
registerDevDiscoveryHotChannels();
|
|
515
|
+
if (server.httpServer && !server.httpServer.listening) {
|
|
516
|
+
server.httpServer.once("listening", registerDevDiscoveryHotChannels);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
475
520
|
// Serve the internal-debug module no-cache: consumers resolve it into
|
|
476
521
|
// node_modules, where dev's immutable `?v=` caching pinned browsers to a
|
|
477
522
|
// stale baked INTERNAL_RANGO_DEBUG. See internalDebugNoCacheMiddleware.
|
|
@@ -500,6 +545,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
500
545
|
const getDevServerOrigin = () =>
|
|
501
546
|
server.resolvedUrls?.local?.[0]?.replace(/\/$/, "") ||
|
|
502
547
|
`http://localhost:${server.config.server.port || 5173}`;
|
|
548
|
+
let devServerClosed = false;
|
|
503
549
|
|
|
504
550
|
// Shared temp server for Cloudflare dev (no module runner in workerd).
|
|
505
551
|
// Used by both discover() (route type generation) and the prerender
|
|
@@ -510,6 +556,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
510
556
|
|
|
511
557
|
// Clean up the temporary server and build env when the dev server shuts down
|
|
512
558
|
server.httpServer?.on("close", () => {
|
|
559
|
+
devServerClosed = true;
|
|
513
560
|
if (prerenderTempServer) {
|
|
514
561
|
prerenderTempServer.close().catch(() => {});
|
|
515
562
|
prerenderTempServer = null;
|
|
@@ -953,16 +1000,6 @@ export function createRouterDiscoveryPlugin(
|
|
|
953
1000
|
if (s.mergedRouteManifest && serverMod.setCachedManifest) {
|
|
954
1001
|
serverMod.setCachedManifest(s.mergedRouteManifest);
|
|
955
1002
|
}
|
|
956
|
-
if (
|
|
957
|
-
s.mergedPrecomputedEntries &&
|
|
958
|
-
s.mergedPrecomputedEntries.length > 0 &&
|
|
959
|
-
serverMod.setPrecomputedEntries
|
|
960
|
-
) {
|
|
961
|
-
serverMod.setPrecomputedEntries(s.mergedPrecomputedEntries);
|
|
962
|
-
}
|
|
963
|
-
if (s.mergedRouteTrie && serverMod.setRouteTrie) {
|
|
964
|
-
serverMod.setRouteTrie(s.mergedRouteTrie);
|
|
965
|
-
}
|
|
966
1003
|
const perRouterSetters: Array<[Map<string, any>, string]> = [
|
|
967
1004
|
[s.perRouterManifestDataMap, "setRouterManifest"],
|
|
968
1005
|
[s.perRouterTrieMap, "setRouterTrie"],
|
|
@@ -1436,6 +1473,22 @@ export function createRouterDiscoveryPlugin(
|
|
|
1436
1473
|
writeCombinedRouteTypesWithTracking(s);
|
|
1437
1474
|
}
|
|
1438
1475
|
};
|
|
1476
|
+
const routeShapeSignature = () =>
|
|
1477
|
+
JSON.stringify(
|
|
1478
|
+
s.perRouterManifests.map(
|
|
1479
|
+
({
|
|
1480
|
+
id,
|
|
1481
|
+
routeManifest,
|
|
1482
|
+
routeTrailingSlash,
|
|
1483
|
+
routeSearchSchemas,
|
|
1484
|
+
}) => ({
|
|
1485
|
+
id,
|
|
1486
|
+
routeManifest,
|
|
1487
|
+
routeTrailingSlash,
|
|
1488
|
+
routeSearchSchemas,
|
|
1489
|
+
}),
|
|
1490
|
+
),
|
|
1491
|
+
);
|
|
1439
1492
|
|
|
1440
1493
|
const maybeHandleGeneratedRouteFileMutation = (
|
|
1441
1494
|
filePath: string,
|
|
@@ -1474,6 +1527,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
1474
1527
|
// populated manifest there's nothing useful to do, so bail
|
|
1475
1528
|
// before involving the gate machine at all.
|
|
1476
1529
|
if (!hasMainRunner && s.perRouterManifests.length === 0) return;
|
|
1530
|
+
let previousRouteShape = routeShapeSignature();
|
|
1477
1531
|
await gate.runRefreshCycle(async () => {
|
|
1478
1532
|
const hmrStart = performance.now();
|
|
1479
1533
|
try {
|
|
@@ -1528,7 +1582,19 @@ export function createRouterDiscoveryPlugin(
|
|
|
1528
1582
|
// original call already resolved on the failed cycle. A failed
|
|
1529
1583
|
// cycle throws above and never reaches here, so a broken edit
|
|
1530
1584
|
// never reloads the worker onto bad source.
|
|
1531
|
-
if (rscEnv && !rscEnv.runner)
|
|
1585
|
+
if (rscEnv && !rscEnv.runner) {
|
|
1586
|
+
const nextRouteShape = routeShapeSignature();
|
|
1587
|
+
let expectedEpoch: number | undefined;
|
|
1588
|
+
if (nextRouteShape !== previousRouteShape) {
|
|
1589
|
+
expectedEpoch = Math.max(
|
|
1590
|
+
Date.now(),
|
|
1591
|
+
(s.devDiscoveryEpoch ?? 0) + 1,
|
|
1592
|
+
);
|
|
1593
|
+
s.devDiscoveryEpoch = expectedEpoch;
|
|
1594
|
+
}
|
|
1595
|
+
previousRouteShape = nextRouteShape;
|
|
1596
|
+
forceCloudflareWorkerReload(rscEnv, expectedEpoch);
|
|
1597
|
+
}
|
|
1532
1598
|
} catch (err: any) {
|
|
1533
1599
|
s.lastDiscoveryError = {
|
|
1534
1600
|
message: err?.message ?? String(err),
|
|
@@ -1562,7 +1628,8 @@ export function createRouterDiscoveryPlugin(
|
|
|
1562
1628
|
// HMR update for them and the entry chain is never evicted.
|
|
1563
1629
|
//
|
|
1564
1630
|
// Fix: after discovery completes, (1) invalidate the worker env's
|
|
1565
|
-
// Node-side module graph,
|
|
1631
|
+
// Node-side module graph, (2) send a full-reload to the worker, then
|
|
1632
|
+
// (3) probe until the active router reports the new discovery epoch.
|
|
1566
1633
|
// Step (2) alone is insufficient: the full-reload handler clears the
|
|
1567
1634
|
// runner's evaluatedModules and re-imports entrypoints, but each
|
|
1568
1635
|
// re-import fetches the module back through this Node-side graph, which
|
|
@@ -1570,13 +1637,19 @@ export function createRouterDiscoveryPlugin(
|
|
|
1570
1637
|
// rebuilds the stale route table and the new route 404s/hits the
|
|
1571
1638
|
// catch-all. Invalidating the graph forces a fresh transform on
|
|
1572
1639
|
// re-fetch (the same mechanism refreshTempRscEnv uses for discovery),
|
|
1573
|
-
// so the re-import re-runs createRouter() with the new routes.
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
|
|
1640
|
+
// so the re-import re-runs createRouter() with the new routes. The probe
|
|
1641
|
+
// is detached because the refresh callback still holds the discovery
|
|
1642
|
+
// gate; awaiting it here would deadlock the request that proves the new
|
|
1643
|
+
// router is active. Once it succeeds, the client hot channel broadcasts
|
|
1644
|
+
// readiness to open and newly-booted stale documents.
|
|
1645
|
+
const forceCloudflareWorkerReload = (
|
|
1646
|
+
rscEnv: any,
|
|
1647
|
+
expectedEpoch: number | undefined,
|
|
1648
|
+
) => {
|
|
1577
1649
|
if (!rscEnv?.hot) return;
|
|
1578
|
-
|
|
1579
|
-
|
|
1650
|
+
|
|
1651
|
+
const graph = rscEnv.moduleGraph;
|
|
1652
|
+
const reloadWorkerd = () => {
|
|
1580
1653
|
if (graph?.invalidateAll) {
|
|
1581
1654
|
graph.invalidateAll();
|
|
1582
1655
|
debugDiscovery?.("hmr: invalidated workerd rsc module graph");
|
|
@@ -1585,12 +1658,49 @@ export function createRouterDiscoveryPlugin(
|
|
|
1585
1658
|
debugDiscovery?.(
|
|
1586
1659
|
"hmr: forced workerd rsc env reload (full-reload)",
|
|
1587
1660
|
);
|
|
1588
|
-
}
|
|
1661
|
+
};
|
|
1662
|
+
reloadWorkerd();
|
|
1663
|
+
|
|
1664
|
+
if (expectedEpoch === undefined) return;
|
|
1665
|
+
void (async () => {
|
|
1666
|
+
const deadline = Date.now() + 15_000;
|
|
1667
|
+
do {
|
|
1668
|
+
if (devServerClosed || expectedEpoch !== s.devDiscoveryEpoch) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 25));
|
|
1672
|
+
if (devServerClosed || expectedEpoch !== s.devDiscoveryEpoch) {
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
try {
|
|
1676
|
+
const response = await fetch(getDevServerOrigin() + "/", {
|
|
1677
|
+
cache: "no-store",
|
|
1678
|
+
headers: {
|
|
1679
|
+
[DEV_DISCOVERY_PROBE_HEADER]: String(expectedEpoch),
|
|
1680
|
+
},
|
|
1681
|
+
signal: AbortSignal.timeout(1_000),
|
|
1682
|
+
});
|
|
1683
|
+
if (
|
|
1684
|
+
!devServerClosed &&
|
|
1685
|
+
expectedEpoch === s.devDiscoveryEpoch &&
|
|
1686
|
+
response.headers.get(DEV_DISCOVERY_EPOCH_HEADER) ===
|
|
1687
|
+
String(expectedEpoch)
|
|
1688
|
+
) {
|
|
1689
|
+
publishDevDiscoveryReady(expectedEpoch);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
// A response without the expected epoch proves an older worker
|
|
1693
|
+
// evaluation won the race after the initial invalidation. Clear
|
|
1694
|
+
// that completed evaluation and retry the reload.
|
|
1695
|
+
reloadWorkerd();
|
|
1696
|
+
} catch {}
|
|
1697
|
+
} while (Date.now() < deadline);
|
|
1698
|
+
|
|
1589
1699
|
debugDiscovery?.(
|
|
1590
|
-
"hmr: workerd
|
|
1591
|
-
|
|
1700
|
+
"hmr: workerd readiness probe timed out at epoch %d",
|
|
1701
|
+
expectedEpoch,
|
|
1592
1702
|
);
|
|
1593
|
-
}
|
|
1703
|
+
})();
|
|
1594
1704
|
};
|
|
1595
1705
|
|
|
1596
1706
|
const scheduleRouteRegeneration = () => {
|
|
@@ -252,6 +252,13 @@ export function onwarn(
|
|
|
252
252
|
export function getManualChunks(id: string): string | undefined {
|
|
253
253
|
const normalized = Vite.normalizePath(id);
|
|
254
254
|
|
|
255
|
+
if (
|
|
256
|
+
/\/browser\/prefetch\/(?:runtime|fetch|queue|observer|policy|resource-ready)\.[cm]?[jt]sx?(?:[?#].*)?$/.test(
|
|
257
|
+
normalized,
|
|
258
|
+
)
|
|
259
|
+
) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
255
262
|
if (
|
|
256
263
|
normalized.includes("node_modules/react/") ||
|
|
257
264
|
normalized.includes("node_modules/react-dom/") ||
|