@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc
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 +196 -43
- package/dist/bin/rango.js +277 -99
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2779 -1064
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +155 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +273 -53
- package/skills/middleware/SKILL.md +49 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +197 -6
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +88 -4
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +716 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/__internal.ts +1 -1
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +102 -16
- package/src/browser/navigation-client.ts +164 -59
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +139 -38
- package/src/browser/prefetch/cache.ts +175 -15
- package/src/browser/prefetch/fetch.ts +180 -33
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +81 -9
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +23 -64
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +191 -74
- package/src/browser/scroll-restoration.ts +41 -14
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +57 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +94 -238
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +12 -5
- package/src/index.ts +61 -11
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +77 -38
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +174 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +125 -10
- package/src/router/metrics.ts +7 -2
- package/src/router/middleware-types.ts +21 -34
- package/src/router/middleware.ts +103 -90
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +58 -2
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +77 -28
- package/src/router/router-options.ts +76 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +223 -24
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +466 -285
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +91 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +440 -381
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +41 -48
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +25 -37
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +17 -3
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +277 -61
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -60
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +255 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +179 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +194 -72
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -1
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +34 -37
- package/src/vite/discovery/discover-routers.ts +105 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +188 -93
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +46 -6
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +6 -0
- package/src/vite/plugin-types.ts +111 -72
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- 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 +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +920 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router discovery error aggregation.
|
|
3
|
+
*
|
|
4
|
+
* During host-router discovery the lazy mounts registered by a host router are
|
|
5
|
+
* invoked to trigger each sub-app's createRouter() registration. Some mount
|
|
6
|
+
* failures are expected in the temporary discovery server context (a sub-app may
|
|
7
|
+
* reference runtime-only bindings), so each is invoked defensively and its error
|
|
8
|
+
* is collected rather than thrown.
|
|
9
|
+
*
|
|
10
|
+
* Previously these errors were discarded with an empty `catch {}`. When a real
|
|
11
|
+
* failure - typically a sub-app whose router module fails to import - left the
|
|
12
|
+
* registry empty, discovery reported the misleading "No routers found" message
|
|
13
|
+
* with no trace of the underlying cause. The collected errors are now surfaced
|
|
14
|
+
* via the `DiscoveryError` thrown at the end of discovery (issue #499).
|
|
15
|
+
*
|
|
16
|
+
* Which entries to invoke is taken from the consumer's declared intent, not
|
|
17
|
+
* inferred from the function's shape. A host route is registered either with
|
|
18
|
+
* `.map((request) => Response)` (an inline request handler, `kind: "handler"`)
|
|
19
|
+
* or `.lazy(() => import("./sub-app"))` (a lazy mount, `kind: "lazy"`). Only
|
|
20
|
+
* `kind === "lazy"` entries are invoked here; inline handlers are never invoked
|
|
21
|
+
* during discovery (they need a Request and register no routers). Because a lazy
|
|
22
|
+
* entry is known to be a module loader, ANY failure it produces - a synchronous
|
|
23
|
+
* throw or a rejected promise - is a genuine discovery failure and is collected.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** An error caught (and previously swallowed) while resolving host routers. */
|
|
27
|
+
export interface CaughtDiscoveryError {
|
|
28
|
+
/** Human-readable description of where the error was caught. */
|
|
29
|
+
context: string;
|
|
30
|
+
/** The caught value (an Error or otherwise). */
|
|
31
|
+
error: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Minimal shape of a host registry entry needed for mount resolution.
|
|
36
|
+
* Mirrors the runtime HostRouterRegistry value without coupling to its type.
|
|
37
|
+
*/
|
|
38
|
+
interface HostRegistryRoute {
|
|
39
|
+
handler?: unknown;
|
|
40
|
+
kind?: string;
|
|
41
|
+
}
|
|
42
|
+
interface HostRegistryEntry {
|
|
43
|
+
routes: HostRegistryRoute[];
|
|
44
|
+
fallback?: HostRegistryRoute | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Indent every non-empty line of `text` by `pad`. */
|
|
48
|
+
function indent(text: string, pad: string): string {
|
|
49
|
+
return text
|
|
50
|
+
.split("\n")
|
|
51
|
+
.map((line) => (line.length > 0 ? pad + line : line))
|
|
52
|
+
.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Invoke a single lazy mount to trigger its sub-app import (and createRouter()
|
|
57
|
+
* registration), collecting any failure under `context`. The entry is known to
|
|
58
|
+
* be a loader (`kind === "lazy"`), so both a synchronous throw and a rejected
|
|
59
|
+
* promise are genuine failures - no shape heuristics are needed.
|
|
60
|
+
*/
|
|
61
|
+
async function invokeLazyMount(
|
|
62
|
+
loader: () => unknown,
|
|
63
|
+
context: string,
|
|
64
|
+
errors: CaughtDiscoveryError[],
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
await loader();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
errors.push({ context, error });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether a registry route is a `.lazy()` mount with an invokable loader. */
|
|
74
|
+
function isLazyMount(
|
|
75
|
+
route: HostRegistryRoute | null | undefined,
|
|
76
|
+
): route is { handler: () => unknown; kind: "lazy" } {
|
|
77
|
+
return (
|
|
78
|
+
!!route && route.kind === "lazy" && typeof route.handler === "function"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Invoke every lazy mount in the host registry to trigger sub-app
|
|
84
|
+
* createRouter() registration, collecting (not throwing) any failures.
|
|
85
|
+
*
|
|
86
|
+
* Only `.lazy()` entries are invoked; `.map()` inline request handlers are
|
|
87
|
+
* skipped (they need a Request and register no routers). Failures are returned
|
|
88
|
+
* rather than thrown because some mounts legitimately fail in the temporary
|
|
89
|
+
* discovery server context; the caller decides whether the failures matter,
|
|
90
|
+
* which is only when discovery finds no routers at all.
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveHostRouterHandlers(
|
|
93
|
+
hostRegistry: Map<string, HostRegistryEntry>,
|
|
94
|
+
): Promise<CaughtDiscoveryError[]> {
|
|
95
|
+
const errors: CaughtDiscoveryError[] = [];
|
|
96
|
+
|
|
97
|
+
for (const [hostId, entry] of hostRegistry) {
|
|
98
|
+
for (const route of entry.routes) {
|
|
99
|
+
if (isLazyMount(route)) {
|
|
100
|
+
await invokeLazyMount(
|
|
101
|
+
route.handler,
|
|
102
|
+
`host "${hostId}" route handler`,
|
|
103
|
+
errors,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (isLazyMount(entry.fallback)) {
|
|
108
|
+
await invokeLazyMount(
|
|
109
|
+
entry.fallback.handler,
|
|
110
|
+
`host "${hostId}" fallback handler`,
|
|
111
|
+
errors,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build the terminal "No routers found" message, appending any errors caught
|
|
121
|
+
* during host-router discovery so the real cause is visible.
|
|
122
|
+
*
|
|
123
|
+
* The aggregated errors are inlined into the message (in addition to being
|
|
124
|
+
* attached via `cause` on `DiscoveryError`) so they survive every caller: the
|
|
125
|
+
* dev/HMR paths log `err.message`, and the build path re-throws using
|
|
126
|
+
* `err.stack`, which begins with the message. None of those callers traverse
|
|
127
|
+
* `cause`, so the message must carry the detail. Each error includes its stack
|
|
128
|
+
* when available.
|
|
129
|
+
*/
|
|
130
|
+
export function formatNoRoutersError(
|
|
131
|
+
entryPath: string | undefined,
|
|
132
|
+
errors: CaughtDiscoveryError[],
|
|
133
|
+
): string {
|
|
134
|
+
const base = `[rango] No routers found in registry after importing ${entryPath}`;
|
|
135
|
+
if (errors.length === 0) {
|
|
136
|
+
return base;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const formatted = errors
|
|
140
|
+
.map(({ context, error }) => {
|
|
141
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
142
|
+
const detail = err.stack ?? err.message;
|
|
143
|
+
return ` - while resolving ${context}:\n${indent(detail, " ")}`;
|
|
144
|
+
})
|
|
145
|
+
.join("\n");
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
`${base}\n\n` +
|
|
149
|
+
`${errors.length} error(s) were caught during host-router discovery and ` +
|
|
150
|
+
`likely explain why no routers were registered:\n${formatted}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Reduce the caught errors to an `ErrorOptions.cause`: a single failure becomes
|
|
156
|
+
* the direct cause; multiple failures are wrapped in an `AggregateError` so
|
|
157
|
+
* each underlying error remains reachable. No errors -> no cause.
|
|
158
|
+
*/
|
|
159
|
+
function toCause(errors: CaughtDiscoveryError[]): unknown {
|
|
160
|
+
if (errors.length === 0) return undefined;
|
|
161
|
+
if (errors.length === 1) return errors[0].error;
|
|
162
|
+
return new AggregateError(
|
|
163
|
+
errors.map((e) => e.error),
|
|
164
|
+
"Multiple host-router handlers failed during discovery",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Thrown when router discovery completes without finding any routers.
|
|
170
|
+
*
|
|
171
|
+
* Carries the entry path and the individual failures caught while resolving
|
|
172
|
+
* host-router lazy handlers. The formatted detail is embedded in `message` (for
|
|
173
|
+
* callers that log `err.message`/`err.stack`) and the underlying error(s) are
|
|
174
|
+
* also attached via `cause` (a single failure directly, multiple wrapped in an
|
|
175
|
+
* `AggregateError`) for cause-aware tooling such as the Vite error overlay.
|
|
176
|
+
*/
|
|
177
|
+
export class DiscoveryError extends Error {
|
|
178
|
+
/** The entry file that was imported before discovery gave up. */
|
|
179
|
+
readonly entryPath: string | undefined;
|
|
180
|
+
/** Individual failures caught while resolving host-router handlers. */
|
|
181
|
+
readonly caught: CaughtDiscoveryError[];
|
|
182
|
+
|
|
183
|
+
constructor(entryPath: string | undefined, caught: CaughtDiscoveryError[]) {
|
|
184
|
+
super(formatNoRoutersError(entryPath, caught));
|
|
185
|
+
const cause = toCause(caught);
|
|
186
|
+
if (cause !== undefined) {
|
|
187
|
+
this.cause = cause;
|
|
188
|
+
}
|
|
189
|
+
this.name = "DiscoveryError";
|
|
190
|
+
this.entryPath = entryPath;
|
|
191
|
+
this.caught = caught;
|
|
192
|
+
Object.setPrototypeOf(this, DiscoveryError.prototype);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -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
|
+
}
|