@rangojs/router 0.0.0-experimental.83 → 0.0.0-experimental.8332dbe4
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 +1197 -454
- package/package.json +4 -2
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +30 -2
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +25 -0
- package/skills/layout/SKILL.md +2 -0
- package/skills/links/SKILL.md +234 -16
- package/skills/loader/SKILL.md +70 -3
- package/skills/middleware/SKILL.md +2 -0
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/migrate-react-router/SKILL.md +4 -0
- package/skills/parallel/SKILL.md +9 -0
- package/skills/rango/SKILL.md +2 -0
- 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 +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 +113 -6
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +44 -10
- package/src/browser/prefetch/cache.ts +16 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/NavigationProvider.tsx +64 -16
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/use-params.ts +8 -5
- 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/types.ts +19 -0
- package/src/build/route-trie.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +5 -1
- package/src/href-client.ts +4 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +62 -39
- package/src/route-definition/dsl-helpers.ts +16 -3
- package/src/route-definition/helpers-types.ts +6 -1
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/router/handler-context.ts +21 -41
- package/src/router/lazy-includes.ts +1 -1
- package/src/router/loader-resolution.ts +3 -0
- 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 +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 +8 -0
- package/src/router/segment-resolution/revalidation.ts +128 -100
- 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 +8 -4
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/response-route-handler.ts +11 -10
- package/src/rsc/rsc-rendering.ts +3 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/types.ts +6 -0
- package/src/segment-system.tsx +60 -9
- 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/segments.ts +17 -0
- 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/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 +498 -52
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestScope: the fields every user-facing context shares.
|
|
3
|
+
*
|
|
4
|
+
* A handler, middleware, loader, response handler, and the ALS-bound
|
|
5
|
+
* RequestContext are all different phases of the same request, and they
|
|
6
|
+
* all carry the same set of request-scoped capabilities: the raw Request,
|
|
7
|
+
* the parsed URL pair (`url` is cleaned of internal `_rsc*` params,
|
|
8
|
+
* `originalUrl` retains them), pathname/searchParams, platform bindings
|
|
9
|
+
* (`env`), and two escape hatches for work that outlives the response
|
|
10
|
+
* (`waitUntil`) or needs the raw Cloudflare runtime object
|
|
11
|
+
* (`executionContext`).
|
|
12
|
+
*
|
|
13
|
+
* Each public context type intersects `RequestScope<TEnv>` with its own
|
|
14
|
+
* phase-specific fields (e.g. `params`/`reverse` on HandlerContext,
|
|
15
|
+
* `headers`/`header()` on MiddlewareContext). That keeps platform surface
|
|
16
|
+
* in one place and lets the next runtime escape hatch we need land in
|
|
17
|
+
* one file instead of four.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { DefaultEnv } from "./global-namespace.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Minimal subset of Cloudflare Workers' ExecutionContext that the router
|
|
24
|
+
* uses. Defined locally so the package does not depend on
|
|
25
|
+
* `@cloudflare/workers-types`. Consumers that want the full type can cast.
|
|
26
|
+
*
|
|
27
|
+
* On non-Cloudflare runtimes (Node, dev server, tests), this is undefined
|
|
28
|
+
* — portable apps should prefer `ctx.waitUntil(...)`, which degrades
|
|
29
|
+
* gracefully. `ctx.executionContext` is the escape hatch for libraries
|
|
30
|
+
* (MCP, Durable Object routing, etc.) that type their arguments as the
|
|
31
|
+
* raw ExecutionContext.
|
|
32
|
+
*/
|
|
33
|
+
export interface ExecutionContext {
|
|
34
|
+
waitUntil(promise: Promise<any>): void;
|
|
35
|
+
passThroughOnException(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fallback `waitUntil` body used when no Cloudflare `ExecutionContext`
|
|
40
|
+
* is available (Node, dev, tests). Runs the work fire-and-forget and
|
|
41
|
+
* logs errors so they don't silently swallow.
|
|
42
|
+
*
|
|
43
|
+
* Exported so every `waitUntil` call site degrades identically instead
|
|
44
|
+
* of inventing its own fallback policy.
|
|
45
|
+
*/
|
|
46
|
+
export function fireAndForgetWaitUntil(fn: () => Promise<void>): void {
|
|
47
|
+
fn().catch((err) =>
|
|
48
|
+
console.error("[waitUntil] Background task failed:", err),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fields present on every user-facing request context.
|
|
54
|
+
*
|
|
55
|
+
* @template TEnv - Platform bindings type (Cloudflare env, etc.).
|
|
56
|
+
*/
|
|
57
|
+
export interface RequestScope<TEnv = DefaultEnv> {
|
|
58
|
+
/**
|
|
59
|
+
* The original incoming Request object (transport URL intact).
|
|
60
|
+
* Use `url` / `searchParams` for application logic — those have
|
|
61
|
+
* internal `_rsc*` params stripped. `request` preserves the raw URL
|
|
62
|
+
* when you need original headers, method, or body.
|
|
63
|
+
*/
|
|
64
|
+
request: Request;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The request URL with internal `_rsc*` transport params stripped.
|
|
68
|
+
* Use this for routing, link generation, and display.
|
|
69
|
+
*/
|
|
70
|
+
url: URL;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The original request URL with all parameters intact, including
|
|
74
|
+
* internal `_rsc*` transport params. Use `url` for application logic
|
|
75
|
+
* — this is only needed for advanced cases like debugging or custom
|
|
76
|
+
* cache keying.
|
|
77
|
+
*/
|
|
78
|
+
originalUrl: URL;
|
|
79
|
+
|
|
80
|
+
/** URL pathname (same as `url.pathname`). */
|
|
81
|
+
pathname: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Query parameters from the URL (system params like `_rsc*` are
|
|
85
|
+
* filtered). Always a standard `URLSearchParams` instance.
|
|
86
|
+
*/
|
|
87
|
+
searchParams: URLSearchParams;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Platform bindings (DB, KV, secrets, etc.). On Cloudflare Workers
|
|
91
|
+
* these are the `env` object passed to the Worker's `fetch()` handler.
|
|
92
|
+
*/
|
|
93
|
+
env: TEnv;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Schedule work to run after the response is sent.
|
|
97
|
+
* On Cloudflare Workers, delegates to `executionContext.waitUntil()`.
|
|
98
|
+
* On Node / dev / tests, runs as fire-and-forget with error logging.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* ctx.waitUntil(async () => {
|
|
103
|
+
* await cacheStore.set(key, data, ttl);
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
waitUntil(fn: () => Promise<void>): void;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Raw Cloudflare Workers `ExecutionContext`, when running on a
|
|
111
|
+
* Cloudflare-compatible runtime. Undefined elsewhere.
|
|
112
|
+
*
|
|
113
|
+
* Escape hatch for libraries that type their arguments as
|
|
114
|
+
* `ExecutionContext` (MCP `fetch`, `routeAgentRequest`, etc.).
|
|
115
|
+
* For the common "do work after the response" case, prefer
|
|
116
|
+
* `ctx.waitUntil(...)` — it is platform-neutral.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* path.any("/mcp", (ctx) =>
|
|
121
|
+
* emailMcp.fetch(ctx.request, ctx.env, ctx.executionContext!),
|
|
122
|
+
* );
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
executionContext?: ExecutionContext;
|
|
126
|
+
}
|
package/src/types/segments.ts
CHANGED
|
@@ -62,6 +62,14 @@ export interface ResolvedSegment {
|
|
|
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
|
|
@@ -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: {
|
|
@@ -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
|
+
}
|