@rangojs/router 0.22.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +8 -3
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +34 -5
- package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +19 -9
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +16 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +23 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +14 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -3
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +38 -0
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +11 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +842 -308
- package/package.json +1 -1
- package/skills/caching/SKILL.md +42 -5
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +113 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +11 -3
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +192 -1
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +13 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +12 -6
- package/skills/route/SKILL.md +139 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/serve-shell-request.md +88 -18
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +43 -32
- package/src/browser/navigation-bridge.ts +64 -14
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +528 -135
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/NavigationProvider.tsx +47 -28
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +19 -63
- package/src/browser/types.ts +19 -9
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +37 -10
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache.ts +22 -1
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/errors.ts +15 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +122 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +83 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +11 -1
- package/src/router/match-middleware/cache-lookup.ts +29 -6
- package/src/router/match-middleware/cache-store.ts +17 -1
- package/src/router/match-result.ts +14 -2
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
- package/src/router/segment-resolution/revalidation.ts +169 -60
- package/src/rsc/rsc-rendering.ts +50 -7
- package/src/rsc/shell-capture.ts +2 -1
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/handle-store.ts +104 -8
- package/src/server/request-context.ts +21 -1
- package/src/server.ts +4 -0
- package/src/testing/action-form.ts +2 -11
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +6 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/run-loader.ts +2 -1
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +116 -6
- package/src/testing/vitest.ts +67 -1
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +20 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +93 -32
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
package/src/types/segments.ts
CHANGED
|
@@ -172,6 +172,15 @@ export interface ResolvedSegment {
|
|
|
172
172
|
*/
|
|
173
173
|
awaitBeforeFlush?: true;
|
|
174
174
|
parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
|
|
175
|
+
/**
|
|
176
|
+
* A prefetch skipped this segment's work (`prefetch: false`): a loader
|
|
177
|
+
* carries no `loaderData`, a layout/route/parallel unit carries
|
|
178
|
+
* `component: null` and its fallback. The browser shows the fallback and
|
|
179
|
+
* sends a fill request for it (browser/partial-update.ts); until that
|
|
180
|
+
* lands the segment is a placeholder, never a held copy. Only ever on a
|
|
181
|
+
* prefetch response: see docs/design/prefetch-false.md.
|
|
182
|
+
*/
|
|
183
|
+
deferred?: true;
|
|
175
184
|
// Intercept loader fields (for streaming loader data in parallel segments)
|
|
176
185
|
loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
|
|
177
186
|
loaderIds?: string[]; // IDs ($$id) of loaders for this segment
|
|
@@ -253,10 +262,12 @@ export interface MatchResult {
|
|
|
253
262
|
/**
|
|
254
263
|
* Every segment id whose handler actually ran on the server this request,
|
|
255
264
|
* including ones with `component === null` that get filtered out of
|
|
256
|
-
* `segments`/`diff` to avoid wasted bytes
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
265
|
+
* `segments`/`diff` to avoid wasted bytes, and every loader segment that
|
|
266
|
+
* ran. Drives the client's handle merge (handles/pushed-by.ts) — a slot
|
|
267
|
+
* that re-resolves and pushes nothing must clear its previous handle
|
|
268
|
+
* bucket, but `diff` doesn't carry it because the segment payload doesn't
|
|
269
|
+
* either; a loader that ran replaces its pushes, one that did not keeps
|
|
270
|
+
* them. A superset of `diff`.
|
|
260
271
|
*/
|
|
261
272
|
resolvedIds: string[];
|
|
262
273
|
/**
|
|
@@ -304,3 +315,14 @@ export interface MatchResult {
|
|
|
304
315
|
params: Record<string, string>;
|
|
305
316
|
}>;
|
|
306
317
|
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* A `prefetch: false` placeholder (ResolvedSegment.deferred): a segment whose
|
|
321
|
+
* work a prefetch skipped, waiting for its fill. Never a held copy of the
|
|
322
|
+
* segment it stands for.
|
|
323
|
+
*/
|
|
324
|
+
export function isPlaceholder(
|
|
325
|
+
segment: Pick<ResolvedSegment, "deferred">,
|
|
326
|
+
): boolean {
|
|
327
|
+
return segment.deferred === true;
|
|
328
|
+
}
|
|
@@ -30,7 +30,6 @@ import type {
|
|
|
30
30
|
InterceptUseItem,
|
|
31
31
|
LoaderUseItem,
|
|
32
32
|
TypedCacheItem,
|
|
33
|
-
TransitionItem,
|
|
34
33
|
TypedTransitionItem,
|
|
35
34
|
TypedRouteItem,
|
|
36
35
|
TypedIncludeItem,
|
|
@@ -362,6 +361,12 @@ export type PathHelpers<TEnv> = {
|
|
|
362
361
|
* flag BAKES: the loader executes at shell capture and its settled return
|
|
363
362
|
* freezes into the shell (nested promises stay live holes) — the pre-flush
|
|
364
363
|
* promise applied to the prelude.
|
|
364
|
+
*
|
|
365
|
+
* Pass `{ prefetch: false }` to keep the loader out of prefetches: a
|
|
366
|
+
* prefetch skips it and the click that adopts the prefetch fetches it in
|
|
367
|
+
* one follow-up request (see {@link LoaderOptions}). Only on a segment the
|
|
368
|
+
* client does not have yet: a loader the client already holds runs in the
|
|
369
|
+
* prefetch when it revalidates.
|
|
365
370
|
*/
|
|
366
371
|
loader: <TData>(
|
|
367
372
|
loaderDef: LoaderDefinition<TData>,
|
|
@@ -370,11 +375,25 @@ export type PathHelpers<TEnv> = {
|
|
|
370
375
|
) => LoaderItem;
|
|
371
376
|
|
|
372
377
|
/**
|
|
373
|
-
* Attach a loading component to the current route/layout
|
|
378
|
+
* Attach a loading component to the current route/layout.
|
|
379
|
+
*
|
|
380
|
+
* `{ ssr: false }` skips the fallback on document requests. `{ prefetch:
|
|
381
|
+
* false }` keeps what the fallback covers out of prefetches, when this
|
|
382
|
+
* entry's segment is new to the client: the entry's handler, its loaders,
|
|
383
|
+
* its orphan layouts and parallel slots and, for a layout, every deeper
|
|
384
|
+
* entry. The click that adopts the prefetch shows the fallback and fetches
|
|
385
|
+
* them in one follow-up request, which reads cache() and "use cache" as
|
|
386
|
+
* any request does (a prefetch reads neither for skipped work). A segment
|
|
387
|
+
* the client already holds is never deferred: a prefetch renders it as
|
|
388
|
+
* usual when it revalidates, and what is below it follows its own flags.
|
|
389
|
+
* To keep a child out of prefetches while moving inside a section, flag
|
|
390
|
+
* the child's own loading(). An entry whose handler output is stored
|
|
391
|
+
* (cache(), ppr, Prerender/Static) is served as usual and only its loaders
|
|
392
|
+
* are skipped.
|
|
374
393
|
*/
|
|
375
394
|
loading: (
|
|
376
395
|
component: ReactNode | (() => ReactNode),
|
|
377
|
-
options?: { ssr?: boolean },
|
|
396
|
+
options?: { ssr?: boolean; prefetch?: boolean },
|
|
378
397
|
) => LoadingItem;
|
|
379
398
|
|
|
380
399
|
/**
|
|
@@ -422,8 +441,11 @@ export type PathHelpers<TEnv> = {
|
|
|
422
441
|
* the browser (never on the server); see TransitionWhenFn.
|
|
423
442
|
*/
|
|
424
443
|
transition: {
|
|
425
|
-
|
|
426
|
-
|
|
444
|
+
// Typed {} phantoms, not bare TransitionItem: a phantom-less item matches
|
|
445
|
+
// `TypedTransitionItem<infer R>` with R inferred as Record<string, any>,
|
|
446
|
+
// which disables route-name checking for the sibling map.
|
|
447
|
+
(): TypedTransitionItem<{}, {}>;
|
|
448
|
+
(config: TransitionConfig): TypedTransitionItem<{}, {}>;
|
|
427
449
|
<const TChildren extends readonly (AllUseItems | readonly AllUseItems[])[]>(
|
|
428
450
|
children: () => TChildren,
|
|
429
451
|
): TypedTransitionItem<
|
|
@@ -12,6 +12,11 @@ import type { LocalOnlyInclude, UnnamedRoute } from "./pattern-types.js";
|
|
|
12
12
|
// Route Type Extraction Utilities
|
|
13
13
|
// ============================================================================
|
|
14
14
|
|
|
15
|
+
/** Drops string index signatures, keeping literal and template keys. */
|
|
16
|
+
type StripStringIndex<T> = {
|
|
17
|
+
[K in keyof T as string extends K ? never : K]: T[K];
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* Prefix route names with a given prefix (e.g., "blog" + "post" = "blog.post")
|
|
17
22
|
*
|
|
@@ -117,15 +122,21 @@ type ExtractRoutesFromItem<T> =
|
|
|
117
122
|
: TUrlPrefix extends string
|
|
118
123
|
? PrefixPatterns<TRoutes, TUrlPrefix>
|
|
119
124
|
: TRoutes
|
|
120
|
-
: //
|
|
125
|
+
: // Phantom-less items (global transition/cache/layout, factories typed
|
|
126
|
+
// as a use item) match the Typed* shapes with the child map inferred
|
|
127
|
+
// as the Record<string, any> constraint; the string index signature
|
|
128
|
+
// turns route-name checking off for the whole map. Strip only the
|
|
129
|
+
// index and keep literal keys, so a loosely typed include() beside
|
|
130
|
+
// named routes still contributes those names.
|
|
131
|
+
// TypedLayoutItem: extract child routes from phantom type
|
|
121
132
|
T extends TypedLayoutItem<infer TChildRoutes>
|
|
122
|
-
? TChildRoutes
|
|
133
|
+
? StripStringIndex<TChildRoutes>
|
|
123
134
|
: // TypedCacheItem: extract child routes from phantom type
|
|
124
135
|
T extends TypedCacheItem<infer TChildRoutes>
|
|
125
|
-
? TChildRoutes
|
|
136
|
+
? StripStringIndex<TChildRoutes>
|
|
126
137
|
: // TypedTransitionItem: extract child routes from phantom type
|
|
127
138
|
T extends TypedTransitionItem<infer TChildRoutes>
|
|
128
|
-
? TChildRoutes
|
|
139
|
+
? StripStringIndex<TChildRoutes>
|
|
129
140
|
: // Fallback (won't extract routes)
|
|
130
141
|
{};
|
|
131
142
|
|
|
@@ -206,16 +217,25 @@ type ExtractResponsesFromItem<T> =
|
|
|
206
217
|
? TResponses
|
|
207
218
|
: {}
|
|
208
219
|
: T extends TypedLayoutItem<any, infer TChildResponses>
|
|
209
|
-
? TChildResponses extends Record<
|
|
210
|
-
|
|
220
|
+
? StripStringIndex<TChildResponses> extends infer R extends Record<
|
|
221
|
+
string,
|
|
222
|
+
unknown
|
|
223
|
+
>
|
|
224
|
+
? R
|
|
211
225
|
: {}
|
|
212
226
|
: T extends TypedCacheItem<any, infer TChildResponses>
|
|
213
|
-
? TChildResponses extends Record<
|
|
214
|
-
|
|
227
|
+
? StripStringIndex<TChildResponses> extends infer R extends Record<
|
|
228
|
+
string,
|
|
229
|
+
unknown
|
|
230
|
+
>
|
|
231
|
+
? R
|
|
215
232
|
: {}
|
|
216
233
|
: T extends TypedTransitionItem<any, infer TChildResponses>
|
|
217
|
-
? TChildResponses extends Record<
|
|
218
|
-
|
|
234
|
+
? StripStringIndex<TChildResponses> extends infer R extends Record<
|
|
235
|
+
string,
|
|
236
|
+
unknown
|
|
237
|
+
>
|
|
238
|
+
? R
|
|
219
239
|
: {}
|
|
220
240
|
: {};
|
|
221
241
|
|
package/src/use-loader.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type LoaderStore,
|
|
21
21
|
} from "./loader-store.js";
|
|
22
22
|
import { NavigationStoreContext } from "./browser/react/context.js";
|
|
23
|
+
import { Defer, readDefer } from "./browser/defer.js";
|
|
23
24
|
import { decodeLoaderEntry } from "./decode-loader-results.js";
|
|
24
25
|
import { warnAwaitedSsrSuspension } from "./ssr-suspension-warning.js";
|
|
25
26
|
import { SETTLED_THENABLE } from "./thenable-status.js";
|
|
@@ -51,12 +52,14 @@ const NOT_FOUND = Symbol("not-found");
|
|
|
51
52
|
* available value from a still-streaming per-loader promise. A pending stream
|
|
52
53
|
* is `use()`d at the read site (implicit suspension); the promise identity is
|
|
53
54
|
* stable per commit (it comes from the provider's memoized context value /
|
|
54
|
-
* element props), which use() requires across replays.
|
|
55
|
+
* element props), which use() requires across replays. A `prefetch: false`
|
|
56
|
+
* hole is a Defer (browser/defer.ts), read by its rule: every reader of the
|
|
57
|
+
* loader reads the one defer.
|
|
55
58
|
*/
|
|
56
59
|
type LoaderLookup =
|
|
57
60
|
| typeof NOT_FOUND
|
|
58
61
|
| { value: unknown }
|
|
59
|
-
| { stream: Promise<unknown> };
|
|
62
|
+
| { stream: Promise<unknown> | Defer<unknown> };
|
|
60
63
|
|
|
61
64
|
function lookupLoaderStreams(
|
|
62
65
|
loaderStreams: Record<string, unknown> | undefined,
|
|
@@ -64,9 +67,9 @@ function lookupLoaderStreams(
|
|
|
64
67
|
): LoaderLookup {
|
|
65
68
|
if (!loaderStreams || !(loaderId in loaderStreams)) return NOT_FOUND;
|
|
66
69
|
const entry = loaderStreams[loaderId];
|
|
67
|
-
// Within the loaderStreams channel a Promise always means "still
|
|
68
|
-
// settled entries are undecoded result values, never promises.
|
|
69
|
-
return entry instanceof Promise
|
|
70
|
+
// Within the loaderStreams channel a Promise or a Defer always means "still
|
|
71
|
+
// streaming"; settled entries are undecoded result values, never promises.
|
|
72
|
+
return entry instanceof Promise || entry instanceof Defer
|
|
70
73
|
? { stream: entry }
|
|
71
74
|
: { value: decodeLoaderEntry(entry) };
|
|
72
75
|
}
|
|
@@ -163,7 +166,7 @@ function useLoaderInternal<T>(
|
|
|
163
166
|
const walk = useMemo((): {
|
|
164
167
|
contextData: T | undefined;
|
|
165
168
|
hasContextData: boolean;
|
|
166
|
-
pendingStream?: Promise<unknown>;
|
|
169
|
+
pendingStream?: Promise<unknown> | Defer<unknown>;
|
|
167
170
|
/** { ssr: false } loader ids seen on the chain up to (and including) the
|
|
168
171
|
* level that owned the read — input for the dev SSR suspension warning. */
|
|
169
172
|
awaitedLoaderIds?: string[];
|
|
@@ -217,18 +220,20 @@ function useLoaderInternal<T>(
|
|
|
217
220
|
// every COMPLETED render.
|
|
218
221
|
const { hasContextData } = walk;
|
|
219
222
|
let contextData = walk.contextData;
|
|
223
|
+
const stream = walk.pendingStream;
|
|
220
224
|
// Dev only (suspense-audit.ts): the stream this reader is handed. The test
|
|
221
225
|
// is a build-time constant, so the hook is called on every render or never.
|
|
222
226
|
if (process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT) {
|
|
223
|
-
useReadAudit(
|
|
224
|
-
}
|
|
225
|
-
if (walk.pendingStream) {
|
|
226
|
-
warnAwaitedSsrSuspension(
|
|
227
|
+
useReadAudit(
|
|
227
228
|
loader.$$id,
|
|
228
|
-
|
|
229
|
-
walk.pendingStream,
|
|
229
|
+
stream instanceof Defer ? stream.promise : stream,
|
|
230
230
|
);
|
|
231
|
-
|
|
231
|
+
}
|
|
232
|
+
if (stream instanceof Defer) {
|
|
233
|
+
contextData = decodeLoaderEntry(readDefer(stream)) as T;
|
|
234
|
+
} else if (stream) {
|
|
235
|
+
warnAwaitedSsrSuspension(loader.$$id, walk.awaitedLoaderIds, stream);
|
|
236
|
+
contextData = decodeLoaderEntry(use(stream)) as T;
|
|
232
237
|
} else if (hasContextData) {
|
|
233
238
|
// React logs a render that finishes without use() at a place whose reader
|
|
234
239
|
// suspended in use() on mount, so every read with route context calls it.
|
|
@@ -2,10 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { normalizePath, parseAst } from "vite";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
projectClientUrlsModule,
|
|
6
6
|
type ClientUrlProjection,
|
|
7
7
|
} from "../../client-urls/server-projection.js";
|
|
8
|
-
import type { ClientUrlPatterns } from "../../client-urls/types.js";
|
|
9
8
|
import {
|
|
10
9
|
computeProductionHash,
|
|
11
10
|
hashRefKey,
|
|
@@ -191,22 +190,19 @@ export async function refreshRecordedClientUrlProjections(
|
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
for (const [source, referenceIds] of referenceIdsBySource) {
|
|
194
|
-
let
|
|
193
|
+
let module: Record<string, unknown>;
|
|
195
194
|
try {
|
|
196
|
-
|
|
195
|
+
module = await ssrEnv.runner.import(source);
|
|
197
196
|
} catch {
|
|
198
197
|
continue;
|
|
199
198
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
) {
|
|
199
|
+
let projection: ClientUrlProjection | undefined;
|
|
200
|
+
try {
|
|
201
|
+
projection = projectClientUrlsModule(module, { hoisted: true });
|
|
202
|
+
} catch {
|
|
205
203
|
continue;
|
|
206
204
|
}
|
|
207
|
-
|
|
208
|
-
definition as ClientUrlPatterns,
|
|
209
|
-
);
|
|
205
|
+
if (!projection) continue;
|
|
210
206
|
debug?.(
|
|
211
207
|
"pre-entry refresh %s -> [%s]",
|
|
212
208
|
source,
|
|
@@ -288,21 +284,24 @@ export async function discoverClientUrlProjections(
|
|
|
288
284
|
cause,
|
|
289
285
|
);
|
|
290
286
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
(
|
|
296
|
-
|
|
287
|
+
let projection: ClientUrlProjection | undefined;
|
|
288
|
+
try {
|
|
289
|
+
projection = projectClientUrlsModule(module, { hoisted: true });
|
|
290
|
+
} catch (cause) {
|
|
291
|
+
throw projectionError(
|
|
292
|
+
referenceIds[0],
|
|
293
|
+
source,
|
|
294
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
295
|
+
cause,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (!projection) {
|
|
297
299
|
throw projectionError(
|
|
298
300
|
referenceIds[0],
|
|
299
301
|
source,
|
|
300
302
|
'the module default export must be created by clientUrls() and have __brand === "client-urls"',
|
|
301
303
|
);
|
|
302
304
|
}
|
|
303
|
-
const projection = serializeClientUrlPatterns(
|
|
304
|
-
definition as ClientUrlPatterns,
|
|
305
|
-
);
|
|
306
305
|
for (const referenceId of referenceIds) {
|
|
307
306
|
nextProjections.set(referenceId, projection);
|
|
308
307
|
}
|
|
@@ -42,6 +42,30 @@ import {
|
|
|
42
42
|
|
|
43
43
|
const debug = createRangoDebugger(NS.discovery);
|
|
44
44
|
|
|
45
|
+
const SERVER_MODULE_ID = "@rangojs/router/server";
|
|
46
|
+
|
|
47
|
+
/** Passes discoverRouters runs before giving up on a runner that keeps reloading. */
|
|
48
|
+
const MAX_DISCOVERY_PASSES = 3;
|
|
49
|
+
|
|
50
|
+
/** A discovery pass result: the runner reloaded mid-pass, redo it. */
|
|
51
|
+
const RUNNER_RELOADED = Symbol("rango.discovery.runnerReloaded");
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether the runner's `@rangojs/router/server` is no longer `serverMod`. A
|
|
55
|
+
* Vite module runner full reload clears its evaluated-module cache, so the
|
|
56
|
+
* next import evaluates a fresh copy with empty module-level registries.
|
|
57
|
+
*/
|
|
58
|
+
async function runnerReloadedSince(
|
|
59
|
+
rscEnv: any,
|
|
60
|
+
serverMod: unknown,
|
|
61
|
+
): Promise<boolean> {
|
|
62
|
+
try {
|
|
63
|
+
return (await rscEnv.runner.import(SERVER_MODULE_ID)) !== serverMod;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
/**
|
|
46
70
|
* Import the user's entry via RSC runner, generate manifests for each
|
|
47
71
|
* discovered router, build route tries, and optionally run prerender
|
|
@@ -49,25 +73,70 @@ const debug = createRangoDebugger(NS.discovery);
|
|
|
49
73
|
*
|
|
50
74
|
* Returns the imported `@rangojs/router/server` module so the caller
|
|
51
75
|
* can access the RouterRegistry and manifest setters.
|
|
76
|
+
*
|
|
77
|
+
* A pass reads the router registry and installs clientUrls projections on ONE
|
|
78
|
+
* `@rangojs/router/server` instance, and checks that the runner still serves
|
|
79
|
+
* it. A runner full reload during the pass (a watched-file change at boot)
|
|
80
|
+
* splits the realm: a router module bound to the old instance registers there
|
|
81
|
+
* while urls modules re-evaluate against a fresh one, so include() looks its
|
|
82
|
+
* projection up in the fresh, empty Map ("include() could not resolve the
|
|
83
|
+
* server projection"). Such a pass is redone, at most
|
|
84
|
+
* MAX_DISCOVERY_PASSES times; the redo evaluates the entry in one realm.
|
|
52
85
|
*/
|
|
53
86
|
export async function discoverRouters(
|
|
54
87
|
state: DiscoveryState,
|
|
55
88
|
rscEnv: any,
|
|
56
89
|
ssrEnv?: any,
|
|
57
90
|
): Promise<any> {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
const entryPath = state.resolvedEntryPath;
|
|
92
|
+
if (!entryPath) return;
|
|
93
|
+
|
|
94
|
+
for (let pass = 1; ; pass++) {
|
|
95
|
+
// Imported BEFORE the entry so recorded clientUrls projections can be
|
|
96
|
+
// refreshed first: routers re-created by the entry import materialize
|
|
97
|
+
// whatever projection is installed at that moment, which on HMR
|
|
98
|
+
// re-discovery must be the CURRENT module contents, not the previous
|
|
99
|
+
// pass's snapshot (see refreshRecordedClientUrlProjections). It is also
|
|
100
|
+
// the realm baseline of the reload checks; #637 read the registry off a
|
|
101
|
+
// pre-entry copy without one.
|
|
102
|
+
const serverMod = await timed(
|
|
103
|
+
debug,
|
|
104
|
+
"inner: import @rangojs/router/server",
|
|
105
|
+
() => rscEnv.runner.import(SERVER_MODULE_ID),
|
|
106
|
+
);
|
|
107
|
+
let result: unknown;
|
|
108
|
+
try {
|
|
109
|
+
result = await discoverRoutersPass(
|
|
110
|
+
state,
|
|
111
|
+
rscEnv,
|
|
112
|
+
ssrEnv,
|
|
113
|
+
entryPath,
|
|
114
|
+
serverMod,
|
|
115
|
+
);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (!(await runnerReloadedSince(rscEnv, serverMod))) throw error;
|
|
118
|
+
result = RUNNER_RELOADED;
|
|
119
|
+
}
|
|
120
|
+
if (result !== RUNNER_RELOADED) return result;
|
|
121
|
+
if (pass === MAX_DISCOVERY_PASSES) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`[rango] Route discovery did not settle: the RSC module runner reloaded during ${MAX_DISCOVERY_PASSES} consecutive discovery passes.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
debug?.(
|
|
127
|
+
"inner: module runner reloaded mid-pass, redoing discovery (pass %d)",
|
|
128
|
+
pass + 1,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function discoverRoutersPass(
|
|
134
|
+
state: DiscoveryState,
|
|
135
|
+
rscEnv: any,
|
|
136
|
+
ssrEnv: any,
|
|
137
|
+
entryPath: string,
|
|
138
|
+
serverMod: any,
|
|
139
|
+
): Promise<any> {
|
|
71
140
|
await timed(debug, "inner: refresh recorded client URL projections", () =>
|
|
72
141
|
refreshRecordedClientUrlProjections(state, ssrEnv, serverMod),
|
|
73
142
|
);
|
|
@@ -76,10 +145,11 @@ export async function discoverRouters(
|
|
|
76
145
|
// For node preset: this is the router file (createRouter() registers in RouterRegistry).
|
|
77
146
|
// For cloudflare preset: this is the worker entry (which imports the router).
|
|
78
147
|
await timed(debug, "inner: import entry", () =>
|
|
79
|
-
rscEnv.runner.import(
|
|
148
|
+
rscEnv.runner.import(entryPath),
|
|
80
149
|
);
|
|
150
|
+
if (await runnerReloadedSince(rscEnv, serverMod)) return RUNNER_RELOADED;
|
|
81
151
|
|
|
82
|
-
|
|
152
|
+
const registry: Map<string, any> = serverMod.RouterRegistry;
|
|
83
153
|
|
|
84
154
|
if (!registry || registry.size === 0) {
|
|
85
155
|
// No RSC routers found directly. Check for host routers with lazy handlers
|
|
@@ -105,16 +175,10 @@ export async function discoverRouters(
|
|
|
105
175
|
debug?.("caught error while resolving %s: %O", context, error);
|
|
106
176
|
}
|
|
107
177
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const freshRegistry: Map<string, any> = freshServerMod.RouterRegistry;
|
|
113
|
-
|
|
114
|
-
if (freshRegistry && freshRegistry.size > 0) {
|
|
115
|
-
// Update references so the manifest generation below uses the fresh data
|
|
116
|
-
Object.assign(serverMod, freshServerMod);
|
|
117
|
-
registry = freshRegistry;
|
|
178
|
+
// Sub-app createRouter() calls populate the same live registry, unless
|
|
179
|
+
// a reload moved them to a fresh realm.
|
|
180
|
+
if (await runnerReloadedSince(rscEnv, serverMod)) {
|
|
181
|
+
return RUNNER_RELOADED;
|
|
118
182
|
}
|
|
119
183
|
}
|
|
120
184
|
} catch (error) {
|
|
@@ -125,7 +189,7 @@ export async function discoverRouters(
|
|
|
125
189
|
|
|
126
190
|
// If still no routers after host router resolution, fail
|
|
127
191
|
if (!registry || registry.size === 0) {
|
|
128
|
-
throw new DiscoveryError(
|
|
192
|
+
throw new DiscoveryError(entryPath, discoveryErrors);
|
|
129
193
|
}
|
|
130
194
|
}
|
|
131
195
|
|
|
@@ -351,6 +415,10 @@ export async function discoverRouters(
|
|
|
351
415
|
}
|
|
352
416
|
}
|
|
353
417
|
|
|
418
|
+
// A reload after the entry import leaves this pass's projections and
|
|
419
|
+
// manifests on a realm the runner no longer serves.
|
|
420
|
+
if (await runnerReloadedSince(rscEnv, serverMod)) return RUNNER_RELOADED;
|
|
421
|
+
|
|
354
422
|
// Commit all local state to the shared discovery state atomically.
|
|
355
423
|
// This ensures a failed re-discovery (e.g. from a transient module
|
|
356
424
|
// evaluation error) preserves the last known-good state.
|
|
@@ -196,14 +196,30 @@ export interface DiscoveryState {
|
|
|
196
196
|
buildEnvDispose?: (() => Promise<void> | void) | null;
|
|
197
197
|
|
|
198
198
|
/**
|
|
199
|
-
* Set when the most recent HMR re-discovery
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* rediscovery trigger
|
|
199
|
+
* Set when the most recent discovery threw: an HMR re-discovery, or the
|
|
200
|
+
* cold dev discovery on a main RSC runner. Cleared on the next successful
|
|
201
|
+
* discovery. While set, the route-file watcher treats any in-scan source
|
|
202
|
+
* change as a rediscovery trigger (recovery mode).
|
|
203
203
|
*/
|
|
204
204
|
lastDiscoveryError?: { message: string; at: number } | null;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Whether the route-file watcher can re-run runtime discovery. A committed
|
|
209
|
+
* manifest is the usual precondition. A failed cold discovery commits none,
|
|
210
|
+
* so on a main RSC runner its recorded failure qualifies too; without that
|
|
211
|
+
* the dev server stayed broken until a restart.
|
|
212
|
+
*/
|
|
213
|
+
export function canRefreshRuntimeDiscovery(
|
|
214
|
+
state: Pick<DiscoveryState, "perRouterManifests" | "lastDiscoveryError">,
|
|
215
|
+
hasMainRunner: boolean,
|
|
216
|
+
): boolean {
|
|
217
|
+
return (
|
|
218
|
+
state.perRouterManifests.length > 0 ||
|
|
219
|
+
(hasMainRunner && !!state.lastDiscoveryError)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
207
223
|
export function createDiscoveryState(
|
|
208
224
|
entryPath: string | undefined,
|
|
209
225
|
opts: PluginOptions | undefined,
|
|
@@ -46,6 +46,22 @@ function devDiscoveryBootstrap(state: DiscoveryState): string[] {
|
|
|
46
46
|
return [`globalThis.__RANGO_DEV_DISCOVERY_EPOCH = ${epoch};`];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Clear, then install every projection in state.clientUrlProjectionMap. */
|
|
50
|
+
function clientUrlProjectionReplay(state: DiscoveryState): string[] {
|
|
51
|
+
// Sorted: the map fills in transform order, which varies between runs,
|
|
52
|
+
// and this module's bytes feed cache versions (build-versions.ts).
|
|
53
|
+
const projections = [...(state.clientUrlProjectionMap ?? [])].sort(
|
|
54
|
+
([a], [b]) => compareStrings(a, b),
|
|
55
|
+
);
|
|
56
|
+
return [
|
|
57
|
+
`clearClientUrlProjections();`,
|
|
58
|
+
...projections.map(
|
|
59
|
+
([referenceId, projection]) =>
|
|
60
|
+
`setClientUrlProjection(${JSON.stringify(referenceId)}, ${jsonParseExpression(projection)});`,
|
|
61
|
+
),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* Generate the code for the main virtual:rsc-router/routes-manifest module.
|
|
51
67
|
*/
|
|
@@ -110,19 +126,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
110
126
|
TRANSITION_WHEN_VALIDATION,
|
|
111
127
|
];
|
|
112
128
|
|
|
113
|
-
if (hasClientUrlModules)
|
|
114
|
-
lines.push(`clearClientUrlProjections();`);
|
|
115
|
-
// Sorted: the map fills in transform order, which varies between runs,
|
|
116
|
-
// and this module's bytes feed cache versions (build-versions.ts).
|
|
117
|
-
const projections = [...(state.clientUrlProjectionMap ?? [])].sort(
|
|
118
|
-
([a], [b]) => compareStrings(a, b),
|
|
119
|
-
);
|
|
120
|
-
for (const [referenceId, projection] of projections) {
|
|
121
|
-
lines.push(
|
|
122
|
-
`setClientUrlProjection(${JSON.stringify(referenceId)}, ${jsonParseExpression(projection)});`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
129
|
+
if (hasClientUrlModules) lines.push(...clientUrlProjectionReplay(state));
|
|
126
130
|
|
|
127
131
|
if (varIdx > 0) {
|
|
128
132
|
lines.push(
|
|
@@ -197,13 +201,14 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
197
201
|
return lines.join("\n");
|
|
198
202
|
}
|
|
199
203
|
|
|
204
|
+
// No manifest: discovery has not committed one (a failed cold pass commits
|
|
205
|
+
// nothing). The projections the failed pass computed still replay, so the
|
|
206
|
+
// realm serving requests can resolve include()s of clientUrls() modules.
|
|
200
207
|
const lines: string[] = [
|
|
201
|
-
`import { ${hasClientUrlModules ? "clearClientUrlProjections, " : ""}enableTransitionWhenValidation } from "@rangojs/router/server";`,
|
|
208
|
+
`import { ${hasClientUrlModules ? "clearClientUrlProjections, setClientUrlProjection, " : ""}enableTransitionWhenValidation } from "@rangojs/router/server";`,
|
|
202
209
|
TRANSITION_WHEN_VALIDATION,
|
|
203
210
|
];
|
|
204
|
-
if (hasClientUrlModules)
|
|
205
|
-
lines.push(`clearClientUrlProjections();`);
|
|
206
|
-
}
|
|
211
|
+
if (hasClientUrlModules) lines.push(...clientUrlProjectionReplay(state));
|
|
207
212
|
if (!state.isBuildMode) {
|
|
208
213
|
const origin =
|
|
209
214
|
state.devServerOrigin ||
|