@rangojs/router 0.0.0-experimental.69 → 0.0.0-experimental.6c70a2ab
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 +1456 -467
- 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 +364 -0
- package/skills/hooks/SKILL.md +54 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +45 -0
- package/skills/layout/SKILL.md +24 -0
- package/skills/links/SKILL.md +234 -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 +68 -0
- package/skills/rango/SKILL.md +26 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +48 -0
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +9 -1
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +80 -5
- package/src/browser/navigation-client.ts +64 -13
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +58 -12
- 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/index.ts +3 -0
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +17 -4
- package/src/browser/react/use-reverse.ts +99 -0
- 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/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/types.ts +19 -0
- package/src/build/route-trie.ts +52 -25
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +87 -175
- package/src/href-client.ts +4 -1
- 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 +62 -36
- 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 +21 -38
- 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-middleware/cache-lookup.ts +2 -1
- package/src/router/match-result.ts +101 -4
- package/src/router/middleware-types.ts +14 -25
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/fresh.ts +13 -0
- package/src/router/segment-resolution/revalidation.ts +135 -101
- package/src/router/substitute-pattern-params.ts +56 -0
- 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 +16 -8
- 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 +71 -70
- 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 +18 -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/use-loader.tsx +4 -1
- 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 +172 -84
- 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-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +540 -376
- 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 +21 -6
package/src/types/segments.ts
CHANGED
|
@@ -50,18 +50,26 @@ export interface ResolvedSegment {
|
|
|
50
50
|
parallelName?: string; // For parallels: the parallel group name (used to match with revalidations)
|
|
51
51
|
// Loader-specific fields
|
|
52
52
|
loaderId?: string; // For loaders: the loader $$id identifier
|
|
53
|
+
_inherited?: boolean; // For inherited loaders: dedup marker for buildMatchResult
|
|
53
54
|
loaderData?: any; // For loaders: the resolved data from loader execution
|
|
54
55
|
parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
|
|
55
56
|
// Intercept loader fields (for streaming loader data in parallel segments)
|
|
56
57
|
loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
|
|
57
58
|
loaderIds?: string[]; // IDs ($$id) of loaders for this segment
|
|
58
|
-
parallelLoaderSources?: any[]; // Internal: preserves stable aggregate promise across renders
|
|
59
59
|
// Error-specific fields
|
|
60
60
|
error?: ErrorInfo; // For error segments: the error information
|
|
61
61
|
// NotFound-specific fields
|
|
62
62
|
notFoundInfo?: NotFoundInfo; // For notFound segments: the not found information
|
|
63
63
|
// Mount path from include() scope, used for MountContext.Provider wrapping
|
|
64
64
|
mountPath?: string;
|
|
65
|
+
/**
|
|
66
|
+
* @internal Server-side marker: true when the segment's handler actually ran
|
|
67
|
+
* this request (not skipped via the revalidate cache path). Used by
|
|
68
|
+
* match-result.ts to populate `MatchResult.resolvedIds` for client-side
|
|
69
|
+
* handle-bucket cleanup. Stripped from the wire payload before serialization
|
|
70
|
+
* — never reaches the client.
|
|
71
|
+
*/
|
|
72
|
+
_handlerRan?: boolean;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/**
|
|
@@ -116,6 +124,15 @@ export interface MatchResult {
|
|
|
116
124
|
segments: ResolvedSegment[];
|
|
117
125
|
matched: string[];
|
|
118
126
|
diff: string[];
|
|
127
|
+
/**
|
|
128
|
+
* Every segment id whose handler actually ran on the server this request,
|
|
129
|
+
* including ones with `component === null` that get filtered out of
|
|
130
|
+
* `segments`/`diff` to avoid wasted bytes. Drives the client's handle-
|
|
131
|
+
* cleanup pass — a slot that re-resolves and pushes nothing must clear
|
|
132
|
+
* its previous handle bucket, but `diff` doesn't carry it because the
|
|
133
|
+
* segment payload doesn't either. A superset of `diff`.
|
|
134
|
+
*/
|
|
135
|
+
resolvedIds: string[];
|
|
119
136
|
/**
|
|
120
137
|
* Merged route params from all matched segments
|
|
121
138
|
* Available for use by the handler after route matching
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
runWithPrefixes,
|
|
5
5
|
getUrlPrefix,
|
|
6
6
|
getNamePrefix,
|
|
7
|
-
getRootScoped,
|
|
8
7
|
} from "../server/context";
|
|
9
8
|
import {
|
|
10
9
|
INTERNAL_INCLUDE_SCOPE_PREFIX,
|
|
@@ -149,22 +148,32 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
|
149
148
|
});
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// and
|
|
161
|
-
|
|
151
|
+
// Allocate an include-scope token for this include() call. The token is
|
|
152
|
+
// appended to the parent's shortCode prefix whenever the include's
|
|
153
|
+
// direct-descendant shortCodes are generated (see getShortCode in
|
|
154
|
+
// context.ts), partitioning the parent's counter namespace so routes
|
|
155
|
+
// inside an include cannot collide with siblings declared outside it.
|
|
156
|
+
//
|
|
157
|
+
// Scopes compose: a nested include inside an outer include with scope
|
|
158
|
+
// "I0" allocates against the `${parent.shortCode}I0_include` counter
|
|
159
|
+
// and produces scope "I0I0", "I0I1", etc.
|
|
160
|
+
const parentScope = ctx.includeScope ?? "";
|
|
161
|
+
let includeScope = parentScope;
|
|
162
162
|
if (capturedParent?.shortCode) {
|
|
163
|
-
const
|
|
164
|
-
ctx.counters[
|
|
165
|
-
ctx.counters[
|
|
163
|
+
const includeCounterKey = `${capturedParent.shortCode}${parentScope}_include`;
|
|
164
|
+
ctx.counters[includeCounterKey] ??= 0;
|
|
165
|
+
const includeIdx = ctx.counters[includeCounterKey];
|
|
166
|
+
ctx.counters[includeCounterKey] = includeIdx + 1;
|
|
167
|
+
includeScope = `${parentScope}I${includeIdx}`;
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
// Snapshot parent's counters AFTER allocating the include scope so lazy
|
|
171
|
+
// manifest generation starts with the same counter state this include
|
|
172
|
+
// observed — its descendants still get fresh per-scope counters because
|
|
173
|
+
// they key off `${parent.shortCode}${includeScope}_*` (not shared with
|
|
174
|
+
// siblings outside the include).
|
|
175
|
+
const capturedCounters = { ...ctx.counters };
|
|
176
|
+
|
|
168
177
|
// Compute rootScoped at capture time, mirroring the logic in runWithPrefixes.
|
|
169
178
|
// This ensures lazy evaluation restores the correct scope state.
|
|
170
179
|
const parentRootScoped = ctx.rootScoped;
|
|
@@ -191,6 +200,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
|
191
200
|
counters: capturedCounters,
|
|
192
201
|
cacheProfiles: ctx.cacheProfiles,
|
|
193
202
|
rootScoped: capturedRootScoped,
|
|
203
|
+
includeScope,
|
|
194
204
|
},
|
|
195
205
|
} as IncludeItem;
|
|
196
206
|
};
|
|
@@ -233,12 +233,27 @@ export type PathHelpers<TEnv> = {
|
|
|
233
233
|
include: IncludeFn<TEnv>;
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
|
-
* Define parallel routes that render simultaneously in named slots
|
|
236
|
+
* Define parallel routes that render simultaneously in named slots.
|
|
237
|
+
*
|
|
238
|
+
* A slot value can be a Handler / ReactNode / StaticHandlerDefinition
|
|
239
|
+
* (legacy form, broadcast use applies to every slot) or a slot descriptor
|
|
240
|
+
* `{ handler, use? }` whose `use` is scoped to that slot only. Per-slot
|
|
241
|
+
* merge order is `handler.use` → shared `use` → slot-local `use`, with
|
|
242
|
+
* narrowest scope winning for last-write-wins items like `loading()`.
|
|
237
243
|
*/
|
|
238
244
|
parallel: <
|
|
239
245
|
TSlots extends Record<
|
|
240
246
|
`@${string}`,
|
|
241
|
-
Handler<any, any, TEnv>
|
|
247
|
+
| Handler<any, any, TEnv>
|
|
248
|
+
| ReactNode
|
|
249
|
+
| StaticHandlerDefinition
|
|
250
|
+
| {
|
|
251
|
+
handler:
|
|
252
|
+
| Handler<any, any, TEnv>
|
|
253
|
+
| ReactNode
|
|
254
|
+
| StaticHandlerDefinition;
|
|
255
|
+
use?: () => ParallelUseItem[];
|
|
256
|
+
}
|
|
242
257
|
>,
|
|
243
258
|
>(
|
|
244
259
|
slots: TSlots,
|
|
@@ -264,9 +279,20 @@ export type PathHelpers<TEnv> = {
|
|
|
264
279
|
) => InterceptItem;
|
|
265
280
|
|
|
266
281
|
/**
|
|
267
|
-
* Attach middleware to the current route/layout
|
|
282
|
+
* Attach middleware to the current route/layout, or wrap child segments
|
|
268
283
|
*/
|
|
269
|
-
middleware:
|
|
284
|
+
middleware: {
|
|
285
|
+
(fn: MiddlewareFn<TEnv>): MiddlewareItem;
|
|
286
|
+
(
|
|
287
|
+
fn: MiddlewareFn<TEnv>,
|
|
288
|
+
children: () => UseItems<LayoutUseItem>,
|
|
289
|
+
): MiddlewareItem;
|
|
290
|
+
(fns: MiddlewareFn<TEnv>[]): MiddlewareItem;
|
|
291
|
+
(
|
|
292
|
+
fns: MiddlewareFn<TEnv>[],
|
|
293
|
+
children: () => UseItems<LayoutUseItem>,
|
|
294
|
+
): MiddlewareItem;
|
|
295
|
+
};
|
|
270
296
|
|
|
271
297
|
/**
|
|
272
298
|
* Control when a segment should revalidate during navigation
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
DefaultVars,
|
|
6
6
|
} from "../types/global-namespace.js";
|
|
7
7
|
import type { UseItems, ResponseRouteUseItem } from "../route-types.js";
|
|
8
|
+
import type { RequestScope } from "../types/request-scope.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Reverse function for response handler contexts.
|
|
@@ -93,19 +94,10 @@ export type TextResponseHandler<
|
|
|
93
94
|
export interface ResponseHandlerContext<
|
|
94
95
|
TParams = Record<string, string>,
|
|
95
96
|
TEnv = any,
|
|
96
|
-
> {
|
|
97
|
-
request: Request;
|
|
97
|
+
> extends RequestScope<TEnv> {
|
|
98
98
|
params: TParams;
|
|
99
99
|
/** @internal Phantom property for params type invariance. Prevents mounting handlers on wrong routes. */
|
|
100
100
|
readonly _paramCheck?: (params: TParams) => TParams;
|
|
101
|
-
/** Platform bindings (DB, KV, secrets, etc.). */
|
|
102
|
-
env: TEnv;
|
|
103
|
-
/** Query parameters from the URL (system params like `_rsc*` are filtered). */
|
|
104
|
-
searchParams: URLSearchParams;
|
|
105
|
-
/** The full URL object (with system params filtered). */
|
|
106
|
-
url: URL;
|
|
107
|
-
/** The pathname portion of the request URL. */
|
|
108
|
-
pathname: string;
|
|
109
101
|
reverse: ResponseReverseFunction;
|
|
110
102
|
/** Read a variable set by middleware via ctx.set(key, value) or ctx.set(ContextVar, value). */
|
|
111
103
|
get: {
|
package/src/use-loader.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
isValidElement,
|
|
5
|
+
startTransition,
|
|
5
6
|
useCallback,
|
|
6
7
|
useContext,
|
|
7
8
|
useEffect,
|
|
@@ -284,7 +285,9 @@ function useLoaderInternal<T>(
|
|
|
284
285
|
|
|
285
286
|
const result = payload.loaderResult;
|
|
286
287
|
if (requestId === requestIdRef.current) {
|
|
287
|
-
|
|
288
|
+
startTransition(() => {
|
|
289
|
+
setFetchedData(result);
|
|
290
|
+
});
|
|
288
291
|
}
|
|
289
292
|
return result;
|
|
290
293
|
} catch (e) {
|
|
@@ -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
|
+
}
|