@rangojs/router 0.0.0-experimental.b02a2fec → 0.0.0-experimental.b30bbf02
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/README.md +112 -17
- package/dist/vite/index.js +1338 -462
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +33 -20
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +90 -16
- package/skills/loader/SKILL.md +70 -3
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +66 -0
- package/skills/rango/SKILL.md +25 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +24 -0
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +3 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +71 -5
- package/src/browser/navigation-client.ts +64 -13
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +34 -3
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +70 -18
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/types.ts +19 -0
- package/src/build/route-trie.ts +50 -24
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.tsx +82 -174
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +40 -9
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +7 -3
- package/src/route-definition/dsl-helpers.ts +175 -23
- package/src/route-definition/helpers-types.ts +63 -14
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-types.ts +7 -0
- package/src/router/handler-context.ts +24 -4
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-result.ts +21 -2
- package/src/router/middleware-types.ts +2 -22
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +87 -17
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/fresh.ts +8 -0
- package/src/router/segment-resolution/revalidation.ts +128 -100
- package/src/router/trie-matching.ts +18 -13
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +8 -4
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +10 -0
- package/src/rsc/server-action.ts +4 -0
- package/src/rsc/types.ts +6 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +11 -61
- package/src/server/context.ts +26 -3
- package/src/server/request-context.ts +10 -42
- package/src/ssr/index.tsx +5 -1
- package/src/types/handler-context.ts +12 -39
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +17 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +30 -4
- package/src/urls/response-types.ts +2 -10
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +31 -3
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +48 -1
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +52 -28
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +516 -486
- package/src/vite/plugins/performance-tracks.ts +17 -9
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +558 -53
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +20 -6
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug logging for the Rango Vite plugin.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over the `debug` package (the same one Vite uses for its
|
|
5
|
+
* own `vite:*` namespaces). Enable with either:
|
|
6
|
+
*
|
|
7
|
+
* DEBUG='rango:*' vite dev
|
|
8
|
+
* vite --debug rango:* # vite prepends `vite:`, we bridge it
|
|
9
|
+
*
|
|
10
|
+
* Returns `undefined` when no matching namespace is enabled, so call sites
|
|
11
|
+
* can guard expensive diagnostics with a simple truthiness check:
|
|
12
|
+
*
|
|
13
|
+
* const debug = createRangoDebugger(NS.routes);
|
|
14
|
+
* if (debug) debug("built manifest (%d routes) in %dms", n, ms);
|
|
15
|
+
*
|
|
16
|
+
* Back-compat: INTERNAL_RANGO_DEBUG=1 still enables all rango namespaces.
|
|
17
|
+
*
|
|
18
|
+
* Vite CLI note: `vite --debug <feat>` rewrites to `DEBUG=vite:<feat>` — it
|
|
19
|
+
* always prefixes with `vite:` and cannot enable bare `rango:*` namespaces.
|
|
20
|
+
* We work around this by registering a shadow `vite:rango:*` instance for
|
|
21
|
+
* each debugger, so either invocation works.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import debugFactory from "debug";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Canonical debug namespaces. Import as `NS.xxx` instead of string literals
|
|
28
|
+
* so typos become type errors and the full set lives in one place.
|
|
29
|
+
*/
|
|
30
|
+
export const NS = {
|
|
31
|
+
config: "rango:config",
|
|
32
|
+
discovery: "rango:discovery",
|
|
33
|
+
routes: "rango:routes",
|
|
34
|
+
prerender: "rango:prerender",
|
|
35
|
+
build: "rango:build",
|
|
36
|
+
dev: "rango:dev",
|
|
37
|
+
transform: "rango:transform",
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
// Back-compat: the legacy INTERNAL_RANGO_DEBUG env var enabled per-site
|
|
41
|
+
// console.logs in this plugin. Map it to `rango:*` so those call sites can
|
|
42
|
+
// be migrated to the `debug` pipeline without breaking existing setups.
|
|
43
|
+
// Uses debug.enable() rather than mutating process.env because the `debug`
|
|
44
|
+
// package already snapshotted DEBUG when it was imported above.
|
|
45
|
+
if (process.env.INTERNAL_RANGO_DEBUG) {
|
|
46
|
+
const existing = debugFactory.disable();
|
|
47
|
+
debugFactory.enable(existing ? `${existing},rango:*` : "rango:*");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type Debugger = (formatter: string, ...args: unknown[]) => void;
|
|
51
|
+
|
|
52
|
+
export function createRangoDebugger(namespace: string): Debugger | undefined {
|
|
53
|
+
const primary = debugFactory(namespace);
|
|
54
|
+
// Shadow namespace so `vite --debug rango:*` (which expands to
|
|
55
|
+
// DEBUG=vite:rango:*) and `vite --debug` (DEBUG=vite:*) both pick us up.
|
|
56
|
+
const shadow = debugFactory(`vite:${namespace}`);
|
|
57
|
+
if (primary.enabled) return primary as Debugger;
|
|
58
|
+
if (shadow.enabled) return shadow as Debugger;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Measure an async block and log its duration via `debug`. No-ops (still
|
|
64
|
+
* runs `fn`) when the namespace is disabled, so production cost is a single
|
|
65
|
+
* `.enabled` check per call.
|
|
66
|
+
*
|
|
67
|
+
* await timed(debug, "discover routers", () => discoverRouters(state));
|
|
68
|
+
*/
|
|
69
|
+
export async function timed<T>(
|
|
70
|
+
debug: Debugger | undefined,
|
|
71
|
+
label: string,
|
|
72
|
+
fn: () => T | Promise<T>,
|
|
73
|
+
): Promise<T> {
|
|
74
|
+
if (!debug) return await fn();
|
|
75
|
+
const start = performance.now();
|
|
76
|
+
try {
|
|
77
|
+
return await fn();
|
|
78
|
+
} finally {
|
|
79
|
+
debug("%s (%sms)", label, (performance.now() - start).toFixed(1));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Synchronous variant of `timed`. Use for sync call sites — wrapping them
|
|
85
|
+
* with the async `timed` would create a floating promise that discards any
|
|
86
|
+
* throw, bypassing the surrounding try/catch.
|
|
87
|
+
*/
|
|
88
|
+
export function timedSync<T>(
|
|
89
|
+
debug: Debugger | undefined,
|
|
90
|
+
label: string,
|
|
91
|
+
fn: () => T,
|
|
92
|
+
): T {
|
|
93
|
+
if (!debug) return fn();
|
|
94
|
+
const start = performance.now();
|
|
95
|
+
try {
|
|
96
|
+
return fn();
|
|
97
|
+
} finally {
|
|
98
|
+
debug("%s (%sms)", label, (performance.now() - start).toFixed(1));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Aggregate counter for high-frequency call sites (typically Vite
|
|
104
|
+
* `transform` hooks that run on many files). Per-call logging would
|
|
105
|
+
* drown real signal; this collects totals and reports once on flush.
|
|
106
|
+
*
|
|
107
|
+
* const counter = createCounter(debug, "use-cache-transform");
|
|
108
|
+
* // inside transform():
|
|
109
|
+
* return counter?.time(id, () => doWork()) ?? doWork();
|
|
110
|
+
* // or manually:
|
|
111
|
+
* counter?.record(id, ms);
|
|
112
|
+
* // flush on buildEnd (counter resets, so multi-env builds each get
|
|
113
|
+
* // their own summary line):
|
|
114
|
+
* counter?.flush();
|
|
115
|
+
*
|
|
116
|
+
* Returns `undefined` when the namespace is disabled so call sites pay
|
|
117
|
+
* nothing when off.
|
|
118
|
+
*/
|
|
119
|
+
export interface Counter {
|
|
120
|
+
record(file: string, ms: number): void;
|
|
121
|
+
/**
|
|
122
|
+
* Convenience: time a sync or async block and record it. Propagates
|
|
123
|
+
* throws; records regardless of outcome. Returns the function's result.
|
|
124
|
+
*/
|
|
125
|
+
time<T>(file: string, fn: () => T): T;
|
|
126
|
+
time<T>(file: string, fn: () => Promise<T>): Promise<T>;
|
|
127
|
+
flush(): void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createCounter(
|
|
131
|
+
debug: Debugger | undefined,
|
|
132
|
+
label: string,
|
|
133
|
+
): Counter | undefined {
|
|
134
|
+
if (!debug) return undefined;
|
|
135
|
+
let n = 0;
|
|
136
|
+
let totalMs = 0;
|
|
137
|
+
let slowestMs = 0;
|
|
138
|
+
let slowestFile = "";
|
|
139
|
+
const record = (file: string, ms: number): void => {
|
|
140
|
+
n++;
|
|
141
|
+
totalMs += ms;
|
|
142
|
+
if (ms > slowestMs) {
|
|
143
|
+
slowestMs = ms;
|
|
144
|
+
slowestFile = file;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
record,
|
|
149
|
+
time<T>(file: string, fn: () => T | Promise<T>): T | Promise<T> {
|
|
150
|
+
const start = performance.now();
|
|
151
|
+
let out: T | Promise<T>;
|
|
152
|
+
try {
|
|
153
|
+
out = fn();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
record(file, performance.now() - start);
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
if (out && typeof (out as any).then === "function") {
|
|
159
|
+
return (out as Promise<T>).finally(() =>
|
|
160
|
+
record(file, performance.now() - start),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
record(file, performance.now() - start);
|
|
164
|
+
return out;
|
|
165
|
+
},
|
|
166
|
+
flush(): void {
|
|
167
|
+
if (n === 0) return;
|
|
168
|
+
debug(
|
|
169
|
+
"%s: %d files, %sms total, slowest %sms %s",
|
|
170
|
+
label,
|
|
171
|
+
n,
|
|
172
|
+
totalMs.toFixed(1),
|
|
173
|
+
slowestMs.toFixed(1),
|
|
174
|
+
slowestFile,
|
|
175
|
+
);
|
|
176
|
+
// Reset so buildEnd firing once per environment (Vite 6+ multi-env)
|
|
177
|
+
// gives one log line per env rather than silently dropping later data.
|
|
178
|
+
n = 0;
|
|
179
|
+
totalMs = 0;
|
|
180
|
+
slowestMs = 0;
|
|
181
|
+
slowestFile = "";
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
expandPrerenderRoutes,
|
|
21
21
|
renderStaticHandlers,
|
|
22
22
|
} from "./prerender-collection.js";
|
|
23
|
+
import { createRangoDebugger, timed, NS } from "../debug.js";
|
|
24
|
+
|
|
25
|
+
const debug = createRangoDebugger(NS.discovery);
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* Import the user's entry via RSC runner, generate manifests for each
|
|
@@ -38,10 +41,16 @@ export async function discoverRouters(
|
|
|
38
41
|
// Import the entry file via RSC environment.
|
|
39
42
|
// For node preset: this is the router file (createRouter() registers in RouterRegistry).
|
|
40
43
|
// For cloudflare preset: this is the worker entry (which imports the router).
|
|
41
|
-
await
|
|
44
|
+
await timed(debug, "inner: import entry", () =>
|
|
45
|
+
rscEnv.runner.import(state.resolvedEntryPath),
|
|
46
|
+
);
|
|
42
47
|
|
|
43
48
|
// Import the router package to access the registry
|
|
44
|
-
const serverMod = await
|
|
49
|
+
const serverMod = await timed(
|
|
50
|
+
debug,
|
|
51
|
+
"inner: import @rangojs/router/server",
|
|
52
|
+
() => rscEnv.runner.import("@rangojs/router/server"),
|
|
53
|
+
);
|
|
45
54
|
let registry: Map<string, any> = serverMod.RouterRegistry;
|
|
46
55
|
|
|
47
56
|
if (!registry || registry.size === 0) {
|
|
@@ -100,9 +109,15 @@ export async function discoverRouters(
|
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
// Import build utilities for manifest generation
|
|
103
|
-
const buildMod = await
|
|
112
|
+
const buildMod = await timed(
|
|
113
|
+
debug,
|
|
114
|
+
"inner: import @rangojs/router/build",
|
|
115
|
+
() => rscEnv.runner.import("@rangojs/router/build"),
|
|
116
|
+
);
|
|
104
117
|
const generateManifestFull = buildMod.generateManifestFull;
|
|
105
118
|
|
|
119
|
+
debug?.("inner: found %d router(s) in registry", registry.size);
|
|
120
|
+
|
|
106
121
|
const nestedRouterConflict = findNestedRouterConflict(
|
|
107
122
|
[...registry.values()]
|
|
108
123
|
.map((router) => router.__sourceFile)
|
|
@@ -130,6 +145,7 @@ export async function discoverRouters(
|
|
|
130
145
|
// Collect all manifests for trie building (avoid re-running generateManifest)
|
|
131
146
|
const allManifests: Array<{ id: string; manifest: any }> = [];
|
|
132
147
|
|
|
148
|
+
const manifestGenStart = debug ? performance.now() : 0;
|
|
133
149
|
for (const [id, router] of registry) {
|
|
134
150
|
if (!router.urlpatterns || !generateManifestFull) {
|
|
135
151
|
continue;
|
|
@@ -234,8 +250,15 @@ export async function discoverRouters(
|
|
|
234
250
|
}
|
|
235
251
|
}
|
|
236
252
|
|
|
253
|
+
debug?.(
|
|
254
|
+
"inner: generated manifests for %d router(s) (%sms)",
|
|
255
|
+
allManifests.length,
|
|
256
|
+
(performance.now() - manifestGenStart).toFixed(1),
|
|
257
|
+
);
|
|
258
|
+
|
|
237
259
|
// Build route trie from merged manifest + ancestry
|
|
238
260
|
let newMergedRouteTrie: any = null;
|
|
261
|
+
const trieStart = debug ? performance.now() : 0;
|
|
239
262
|
if (Object.keys(newMergedRouteManifest).length > 0) {
|
|
240
263
|
const buildRouteTrie = buildMod.buildRouteTrie;
|
|
241
264
|
if (buildRouteTrie && mergedRouteAncestry) {
|
|
@@ -329,6 +352,11 @@ export async function discoverRouters(
|
|
|
329
352
|
}
|
|
330
353
|
}
|
|
331
354
|
|
|
355
|
+
debug?.(
|
|
356
|
+
"inner: trie build done (%sms)",
|
|
357
|
+
(performance.now() - trieStart).toFixed(1),
|
|
358
|
+
);
|
|
359
|
+
|
|
332
360
|
// Commit all local state to the shared discovery state atomically.
|
|
333
361
|
// This ensures a failed re-discovery (e.g. from a transient module
|
|
334
362
|
// evaluation error) preserves the last known-good state.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Debugger } from "../debug.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manifest-readiness gate + rediscovery scheduler.
|
|
5
|
+
*
|
|
6
|
+
* Owns the four pieces of state that cooperate to keep
|
|
7
|
+
* `s.discoveryDone` (the promise the manifest virtual module's `load()`
|
|
8
|
+
* hook awaits) consistent across HMR fan-out:
|
|
9
|
+
*
|
|
10
|
+
* - **gatePending**: a Promise has been issued and not yet resolved.
|
|
11
|
+
* Workerd's manifest virtual module load() is blocked on it.
|
|
12
|
+
* - **inProgress**: a refresh's work callback is currently executing.
|
|
13
|
+
* - **queued**: a refresh was attempted while one was already in
|
|
14
|
+
* flight; the active run consumes this in its `finally` and
|
|
15
|
+
* recurses.
|
|
16
|
+
* - **pendingEvents**: a route-file event has been received (gate
|
|
17
|
+
* already reset) but the corresponding refresh's work hasn't started
|
|
18
|
+
* yet — i.e. the debounce hasn't fired. Set in `noteRouteEvent`,
|
|
19
|
+
* cleared at the start of each refresh cycle. Refresh's finally MUST
|
|
20
|
+
* hold the gate if this is true even when `queued` is false,
|
|
21
|
+
* otherwise an event whose debounce fires AFTER the active refresh
|
|
22
|
+
* completes (the "tail-race" window) would observe a resolved gate.
|
|
23
|
+
*
|
|
24
|
+
* The HMR-event flow (cloudflare-stress repro):
|
|
25
|
+
*
|
|
26
|
+
* t=0 Touch 1 → noteRouteEvent → pendingEvents=true, beginGate
|
|
27
|
+
* (gate1 pending)
|
|
28
|
+
* → debounce 100ms
|
|
29
|
+
* t=100 runRefreshCycle(work) → clear pendingEvents, work starts
|
|
30
|
+
* t=750 Touch 2 → noteRouteEvent → pendingEvents=true (no-op gate)
|
|
31
|
+
* → debounce fires at t=850
|
|
32
|
+
* t=800 refresh A's finally → queued=false, pendingEvents=true
|
|
33
|
+
* → HOLD gate (don't resolve)
|
|
34
|
+
* t=850 runRefreshCycle (debounce) → clear pendingEvents, work starts
|
|
35
|
+
* t=1500 refresh B's finally → queued=false, pendingEvents=false
|
|
36
|
+
* → resolveGate (gate1 resolves)
|
|
37
|
+
*
|
|
38
|
+
* @internal Exported only for unit tests.
|
|
39
|
+
*/
|
|
40
|
+
export interface DiscoveryGate {
|
|
41
|
+
/**
|
|
42
|
+
* Reset the gate to a fresh pending Promise via `s.discoveryDone`.
|
|
43
|
+
* No-op when a gate is already pending — file watchers can fire
|
|
44
|
+
* multiple events for one save, and replacing the resolver would
|
|
45
|
+
* orphan the original promise (workerd's manifest load() would hang).
|
|
46
|
+
*/
|
|
47
|
+
beginGate(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the current pending gate. No-op when no gate is pending.
|
|
50
|
+
* Called at the tail of the last refresh cycle in a burst.
|
|
51
|
+
*/
|
|
52
|
+
resolveGate(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Record that a route-file event has arrived. Sets `pendingEvents`
|
|
55
|
+
* and begins the gate. Idempotent for both flags.
|
|
56
|
+
*/
|
|
57
|
+
noteRouteEvent(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Run one refresh cycle, managing queue + pending state around it.
|
|
60
|
+
* If a cycle is already in flight, sets `queued=true` and returns.
|
|
61
|
+
* Otherwise clears `pendingEvents`, runs `work`, and in `finally`:
|
|
62
|
+
*
|
|
63
|
+
* - queued → recurse, gate stays pending
|
|
64
|
+
* - pendingEvents → hold gate (next debounced cycle resolves)
|
|
65
|
+
* - neither → resolveGate
|
|
66
|
+
*/
|
|
67
|
+
runRefreshCycle(work: () => Promise<void>): Promise<void>;
|
|
68
|
+
/** Snapshot of internal state. Test-only. */
|
|
69
|
+
readonly state: () => Readonly<{
|
|
70
|
+
gatePending: boolean;
|
|
71
|
+
inProgress: boolean;
|
|
72
|
+
queued: boolean;
|
|
73
|
+
pendingEvents: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** State container the gate writes `discoveryDone` into. */
|
|
78
|
+
export interface GateOwner {
|
|
79
|
+
discoveryDone: Promise<void> | null | undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createDiscoveryGate(
|
|
83
|
+
s: GateOwner,
|
|
84
|
+
debug?: Debugger,
|
|
85
|
+
): DiscoveryGate {
|
|
86
|
+
let gatePending = false;
|
|
87
|
+
let gateResolver: () => void = () => {};
|
|
88
|
+
let inProgress = false;
|
|
89
|
+
let queued = false;
|
|
90
|
+
let pendingEvents = false;
|
|
91
|
+
|
|
92
|
+
const beginGate = (): void => {
|
|
93
|
+
if (gatePending) return;
|
|
94
|
+
s.discoveryDone = new Promise<void>((resolve) => {
|
|
95
|
+
gateResolver = resolve;
|
|
96
|
+
});
|
|
97
|
+
gatePending = true;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const resolveGate = (): void => {
|
|
101
|
+
if (!gatePending) return;
|
|
102
|
+
// Defer resolution while a refresh cycle is in flight or queued, or
|
|
103
|
+
// while an unprocessed route-file event is pending its debounce.
|
|
104
|
+
// Without this guard, cold-start's `discover().then(resolveGate)`
|
|
105
|
+
// could fire while an HMR-triggered runRefreshCycle is mid-flight,
|
|
106
|
+
// prematurely unblocking workerd's manifest load() against the
|
|
107
|
+
// stale cold-start gen. The active cycle's `finally` calls
|
|
108
|
+
// resolveGate again at the tail and finishes the resolution then.
|
|
109
|
+
if (inProgress || queued || pendingEvents) {
|
|
110
|
+
debug?.(
|
|
111
|
+
"hmr: resolveGate deferred — work in flight (inProgress=%s queued=%s pendingEvents=%s)",
|
|
112
|
+
inProgress,
|
|
113
|
+
queued,
|
|
114
|
+
pendingEvents,
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
gatePending = false;
|
|
119
|
+
debug?.("hmr: discoveryDone resolved");
|
|
120
|
+
gateResolver();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const noteRouteEvent = (): void => {
|
|
124
|
+
pendingEvents = true;
|
|
125
|
+
beginGate();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const runRefreshCycle = async (work: () => Promise<void>): Promise<void> => {
|
|
129
|
+
if (inProgress) {
|
|
130
|
+
queued = true;
|
|
131
|
+
debug?.("hmr: rediscovery in flight — queued for a follow-up cycle");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Snapshot the current pendingEvents into "we're about to process";
|
|
135
|
+
// events arriving from now on re-set it.
|
|
136
|
+
pendingEvents = false;
|
|
137
|
+
inProgress = true;
|
|
138
|
+
try {
|
|
139
|
+
await work();
|
|
140
|
+
} finally {
|
|
141
|
+
inProgress = false;
|
|
142
|
+
if (queued) {
|
|
143
|
+
queued = false;
|
|
144
|
+
debug?.("hmr: consuming queued rediscovery");
|
|
145
|
+
runRefreshCycle(work).catch((err: unknown) => {
|
|
146
|
+
debug?.(
|
|
147
|
+
"hmr: queued cycle rejected — releasing gate (%s)",
|
|
148
|
+
err instanceof Error ? err.message : String(err),
|
|
149
|
+
);
|
|
150
|
+
// Belt-and-suspenders: even if the queued cycle's own try/catch
|
|
151
|
+
// missed something, ensure workerd doesn't hang.
|
|
152
|
+
resolveGate();
|
|
153
|
+
});
|
|
154
|
+
} else if (pendingEvents) {
|
|
155
|
+
debug?.(
|
|
156
|
+
"hmr: holding gate for pending events (debounce not yet fired)",
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
resolveGate();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
beginGate,
|
|
166
|
+
resolveGate,
|
|
167
|
+
noteRouteEvent,
|
|
168
|
+
runRefreshCycle,
|
|
169
|
+
state: () => ({ gatePending, inProgress, queued, pendingEvents }),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
stageBuildAssetModule,
|
|
17
17
|
} from "../utils/prerender-utils.js";
|
|
18
18
|
import type { DiscoveryState } from "./state.js";
|
|
19
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
20
|
+
|
|
21
|
+
const debug = createRangoDebugger(NS.prerender);
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Expand prerender routes into concrete URLs and render them via the
|
|
@@ -30,6 +33,12 @@ export async function expandPrerenderRoutes(
|
|
|
30
33
|
): Promise<void> {
|
|
31
34
|
if (!state.opts?.enableBuildPrerender || !state.isBuildMode) return;
|
|
32
35
|
|
|
36
|
+
const overallStart = debug ? performance.now() : 0;
|
|
37
|
+
debug?.(
|
|
38
|
+
"expandPrerenderRoutes: start (%d router manifest(s))",
|
|
39
|
+
allManifests.length,
|
|
40
|
+
);
|
|
41
|
+
|
|
33
42
|
type PrerenderEntry = {
|
|
34
43
|
urlPath: string;
|
|
35
44
|
routeName: string;
|
|
@@ -99,6 +108,7 @@ export async function expandPrerenderRoutes(
|
|
|
99
108
|
} else {
|
|
100
109
|
// Dynamic route: call getParams() to enumerate param combinations
|
|
101
110
|
if (def?.getParams) {
|
|
111
|
+
const getParamsStart = debug ? performance.now() : 0;
|
|
102
112
|
try {
|
|
103
113
|
const buildVars: Record<string, any> = {};
|
|
104
114
|
const buildEnv = state.resolvedBuildEnv;
|
|
@@ -118,6 +128,12 @@ export async function expandPrerenderRoutes(
|
|
|
118
128
|
},
|
|
119
129
|
};
|
|
120
130
|
const paramsList = await def.getParams(getParamsCtx);
|
|
131
|
+
debug?.(
|
|
132
|
+
"getParams %s -> %d params (%sms)",
|
|
133
|
+
routeName,
|
|
134
|
+
paramsList.length,
|
|
135
|
+
(performance.now() - getParamsStart).toFixed(1),
|
|
136
|
+
);
|
|
121
137
|
const concurrency = def.options?.concurrency ?? 1;
|
|
122
138
|
const hasBuildVars =
|
|
123
139
|
Object.keys(buildVars).length > 0 ||
|
|
@@ -191,7 +207,13 @@ export async function expandPrerenderRoutes(
|
|
|
191
207
|
}
|
|
192
208
|
}
|
|
193
209
|
|
|
194
|
-
if (entries.length === 0)
|
|
210
|
+
if (entries.length === 0) {
|
|
211
|
+
debug?.(
|
|
212
|
+
"no prerender entries (done in %sms)",
|
|
213
|
+
(performance.now() - overallStart).toFixed(1),
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
195
217
|
|
|
196
218
|
// Determine the max concurrency for the log header
|
|
197
219
|
const maxConcurrency = Math.max(...entries.map((e) => e.concurrency));
|
|
@@ -200,6 +222,11 @@ export async function expandPrerenderRoutes(
|
|
|
200
222
|
console.log(
|
|
201
223
|
`[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
|
|
202
224
|
);
|
|
225
|
+
debug?.(
|
|
226
|
+
"prerender loop: %d entries, max concurrency %d",
|
|
227
|
+
entries.length,
|
|
228
|
+
maxConcurrency,
|
|
229
|
+
);
|
|
203
230
|
|
|
204
231
|
const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
|
|
205
232
|
|
|
@@ -317,6 +344,13 @@ export async function expandPrerenderRoutes(
|
|
|
317
344
|
console.log(
|
|
318
345
|
`[rsc-router] Pre-render complete: ${parts.join(", ")} (${totalElapsed}ms total)`,
|
|
319
346
|
);
|
|
347
|
+
debug?.(
|
|
348
|
+
"expandPrerenderRoutes done: %d done, %d skipped, %sms (overall %sms)",
|
|
349
|
+
doneCount,
|
|
350
|
+
skipCount,
|
|
351
|
+
totalElapsed,
|
|
352
|
+
(performance.now() - overallStart).toFixed(1),
|
|
353
|
+
);
|
|
320
354
|
}
|
|
321
355
|
|
|
322
356
|
/**
|
|
@@ -337,6 +371,12 @@ export async function renderStaticHandlers(
|
|
|
337
371
|
)
|
|
338
372
|
return;
|
|
339
373
|
|
|
374
|
+
const overallStart = debug ? performance.now() : 0;
|
|
375
|
+
debug?.(
|
|
376
|
+
"renderStaticHandlers: start (%d static module(s))",
|
|
377
|
+
state.resolvedStaticModules.size,
|
|
378
|
+
);
|
|
379
|
+
|
|
340
380
|
const manifestEntries: Record<string, string> = {};
|
|
341
381
|
let staticDone = 0;
|
|
342
382
|
let staticSkip = 0;
|
|
@@ -436,4 +476,11 @@ export async function renderStaticHandlers(
|
|
|
436
476
|
console.log(
|
|
437
477
|
`[rsc-router] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
|
|
438
478
|
);
|
|
479
|
+
debug?.(
|
|
480
|
+
"renderStaticHandlers done: %d done, %d skipped, %sms (overall %sms)",
|
|
481
|
+
staticDone,
|
|
482
|
+
staticSkip,
|
|
483
|
+
totalStaticElapsed,
|
|
484
|
+
(performance.now() - overallStart).toFixed(1),
|
|
485
|
+
);
|
|
439
486
|
}
|
|
@@ -22,6 +22,32 @@ export function markSelfGenWrite(
|
|
|
22
22
|
export function consumeSelfGenWrite(
|
|
23
23
|
state: DiscoveryState,
|
|
24
24
|
filePath: string,
|
|
25
|
+
): boolean {
|
|
26
|
+
return checkSelfGenWrite(state, filePath, true);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Non-consuming variant. Used by the `handleHotUpdate` plugin hook to
|
|
31
|
+
* suppress vite's HMR cascade for our own gen-file writes WITHOUT
|
|
32
|
+
* consuming the entry — `consumeSelfGenWrite` (called later from the
|
|
33
|
+
* chokidar `change` handler in `handleRouteFileChange`) still needs to
|
|
34
|
+
* see and consume the same entry to short-circuit our regen path.
|
|
35
|
+
*
|
|
36
|
+
* Both hooks fire for the same file change event:
|
|
37
|
+
* - `handleHotUpdate` runs first (vite's HMR pipeline).
|
|
38
|
+
* - chokidar `change` callback runs after (filesystem watcher).
|
|
39
|
+
*/
|
|
40
|
+
export function peekSelfGenWrite(
|
|
41
|
+
state: DiscoveryState,
|
|
42
|
+
filePath: string,
|
|
43
|
+
): boolean {
|
|
44
|
+
return checkSelfGenWrite(state, filePath, false);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkSelfGenWrite(
|
|
48
|
+
state: DiscoveryState,
|
|
49
|
+
filePath: string,
|
|
50
|
+
consume: boolean,
|
|
25
51
|
): boolean {
|
|
26
52
|
const info = state.selfWrittenGenFiles.get(filePath);
|
|
27
53
|
if (!info) return false;
|
|
@@ -33,7 +59,7 @@ export function consumeSelfGenWrite(
|
|
|
33
59
|
const current = readFileSync(filePath, "utf-8");
|
|
34
60
|
const currentHash = createHash("sha256").update(current).digest("hex");
|
|
35
61
|
if (currentHash === info.hash) {
|
|
36
|
-
state.selfWrittenGenFiles.delete(filePath);
|
|
62
|
+
if (consume) state.selfWrittenGenFiles.delete(filePath);
|
|
37
63
|
return true;
|
|
38
64
|
}
|
|
39
65
|
// Hash mismatch: file was changed externally. Keep the entry so a
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
3
|
+
|
|
4
|
+
const debug = createRangoDebugger(NS.transform);
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
|
|
@@ -21,6 +24,7 @@ export function createCjsToEsmPlugin(): Plugin {
|
|
|
21
24
|
? "./cjs/react-server-dom-webpack-client.browser.production.js"
|
|
22
25
|
: "./cjs/react-server-dom-webpack-client.browser.development.js";
|
|
23
26
|
|
|
27
|
+
debug?.("cjs-to-esm entry redirect %s", id);
|
|
24
28
|
return {
|
|
25
29
|
code: `export * from "${cjsFile}";`,
|
|
26
30
|
map: null,
|
|
@@ -81,6 +85,7 @@ export function createCjsToEsmPlugin(): Plugin {
|
|
|
81
85
|
// Reconstruct with license at the top
|
|
82
86
|
transformed = license + "\n" + transformed;
|
|
83
87
|
|
|
88
|
+
debug?.("cjs-to-esm body rewrite %s", id);
|
|
84
89
|
return {
|
|
85
90
|
code: transformed,
|
|
86
91
|
map: null,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
|
+
import { createRangoDebugger, NS } from "../debug.js";
|
|
3
|
+
|
|
4
|
+
const debug = createRangoDebugger(NS.transform);
|
|
2
5
|
|
|
3
6
|
const CLIENT_IN_SERVER_PROXY_PREFIX =
|
|
4
7
|
"virtual:vite-rsc/client-in-server-package-proxy/";
|
|
@@ -62,6 +65,7 @@ export function extractPackageName(absolutePath: string): string | null {
|
|
|
62
65
|
*/
|
|
63
66
|
export function clientRefDedup(): Plugin {
|
|
64
67
|
let clientExclude: string[] = [];
|
|
68
|
+
const dedupedPackages = new Set<string>();
|
|
65
69
|
|
|
66
70
|
return {
|
|
67
71
|
name: "@rangojs/router:client-ref-dedup",
|
|
@@ -76,6 +80,16 @@ export function clientRefDedup(): Plugin {
|
|
|
76
80
|
clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
|
|
77
81
|
},
|
|
78
82
|
|
|
83
|
+
buildEnd() {
|
|
84
|
+
if (debug && dedupedPackages.size > 0) {
|
|
85
|
+
debug(
|
|
86
|
+
"client-ref-dedup: redirected %d package(s) (%s)",
|
|
87
|
+
dedupedPackages.size,
|
|
88
|
+
[...dedupedPackages].join(","),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
79
93
|
resolveId(source, importer, options) {
|
|
80
94
|
// Only intercept in the client environment
|
|
81
95
|
if (this.environment?.name !== "client") return;
|
|
@@ -95,6 +109,8 @@ export function clientRefDedup(): Plugin {
|
|
|
95
109
|
// Don't redirect packages that are excluded from optimization
|
|
96
110
|
if (clientExclude.includes(packageName)) return;
|
|
97
111
|
|
|
112
|
+
if (debug) dedupedPackages.add(packageName);
|
|
113
|
+
|
|
98
114
|
// Return a virtual module that re-exports via bare specifier
|
|
99
115
|
return `\0rango:dedup/${packageName}`;
|
|
100
116
|
},
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
import { relative } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { createRangoDebugger, createCounter, NS } from "../debug.js";
|
|
5
|
+
|
|
6
|
+
const debug = createRangoDebugger(NS.transform);
|
|
4
7
|
|
|
5
8
|
// Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
|
|
6
9
|
const CLIENT_PKG_PROXY_PREFIX =
|
|
@@ -89,6 +92,7 @@ export function transformClientRefs(
|
|
|
89
92
|
* regex replacement of Flight payloads.
|
|
90
93
|
*/
|
|
91
94
|
export function hashClientRefs(projectRoot: string): Plugin {
|
|
95
|
+
const counter = createCounter(debug, "hash-client-refs");
|
|
92
96
|
return {
|
|
93
97
|
name: "@rangojs/router:hash-client-refs",
|
|
94
98
|
// Run after the RSC plugin's transform (default enforce is normal)
|
|
@@ -96,10 +100,18 @@ export function hashClientRefs(projectRoot: string): Plugin {
|
|
|
96
100
|
applyToEnvironment(env) {
|
|
97
101
|
return env.name === "rsc";
|
|
98
102
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
buildEnd() {
|
|
104
|
+
counter?.flush();
|
|
105
|
+
},
|
|
106
|
+
transform(code, id) {
|
|
107
|
+
const start = counter ? performance.now() : 0;
|
|
108
|
+
try {
|
|
109
|
+
const result = transformClientRefs(code, projectRoot);
|
|
110
|
+
if (result === null) return;
|
|
111
|
+
return { code: result, map: null };
|
|
112
|
+
} finally {
|
|
113
|
+
counter?.record(id, performance.now() - start);
|
|
114
|
+
}
|
|
103
115
|
},
|
|
104
116
|
};
|
|
105
117
|
}
|