@rangojs/router 0.22.1 → 0.24.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 +10 -4
- 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/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- 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 +24 -10
- 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 +22 -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/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- 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/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- 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 +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- 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 +4 -4
- 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/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- 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 +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -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/request-scope.d.ts +7 -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 +14 -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 +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- 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 +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- 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 +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- 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 +45 -11
- 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-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- 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/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- 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/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -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/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- 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/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -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/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- 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/request-scope.ts +8 -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 +28 -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/expose-internal-ids.ts +2 -3
- 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 +116 -53
- 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/testing/vitest.ts
CHANGED
|
@@ -139,6 +139,37 @@ function resolveFromRouter(spec: string): string | undefined {
|
|
|
139
139
|
const rsdServerEdgePath: string | undefined =
|
|
140
140
|
resolveFromRouter(RSD_SERVER_EDGE_SPEC);
|
|
141
141
|
|
|
142
|
+
/** Marks the import of a clientUrls() module's untransformed source. */
|
|
143
|
+
const CLIENT_URLS_SOURCE_QUERY = "rango-client-urls-source";
|
|
144
|
+
|
|
145
|
+
/** `@rangojs/router/client` under the react-server condition. */
|
|
146
|
+
const CLIENT_ENTRY_RSC_PATH: string = here("src/client.rsc.tsx").replaceAll(
|
|
147
|
+
"\\",
|
|
148
|
+
"/",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Imported by absolute path so it shares the registry instance the router
|
|
153
|
+
* source under test materializes include() from.
|
|
154
|
+
*/
|
|
155
|
+
const CLIENT_URLS_INSTALLER_PATH: string = here(
|
|
156
|
+
"src/testing/internal/client-urls-projection.ts",
|
|
157
|
+
).replaceAll("\\", "/");
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Appended to a transformed `"use client"` clientUrls() module: install the
|
|
161
|
+
* projection of its own source under its default export's reference id
|
|
162
|
+
* (`<id>#default`, as registerClientReference composes it).
|
|
163
|
+
*/
|
|
164
|
+
function clientUrlsProjectionInstall(id: string): string {
|
|
165
|
+
const source = `${id}${id.includes("?") ? "&" : "?"}${CLIENT_URLS_SOURCE_QUERY}`;
|
|
166
|
+
return (
|
|
167
|
+
`\nimport * as $$RangoClientUrlsSource from ${JSON.stringify(source)};\n` +
|
|
168
|
+
`import { installClientUrlsProjection as $$rangoInstallClientUrlsProjection } from ${JSON.stringify(CLIENT_URLS_INSTALLER_PATH)};\n` +
|
|
169
|
+
`$$rangoInstallClientUrlsProjection(${JSON.stringify(`${id}#default`)}, $$RangoClientUrlsSource);\n`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
142
173
|
/**
|
|
143
174
|
* Build the `resolve.alias` entries a consumer's node/DOM Vitest project needs to
|
|
144
175
|
* import a real @rangojs/router app's router/loaders/middleware. Spread into a
|
|
@@ -241,6 +272,10 @@ export function rangoTestConfig(
|
|
|
241
272
|
/** A minimal Vite plugin shape (avoids a hard dependency on Vite's types). */
|
|
242
273
|
interface FlightTransformPlugin {
|
|
243
274
|
name: string;
|
|
275
|
+
resolveId: {
|
|
276
|
+
order: "pre";
|
|
277
|
+
handler(source: string, importer: string | undefined): string | undefined;
|
|
278
|
+
};
|
|
244
279
|
transform(
|
|
245
280
|
code: string,
|
|
246
281
|
id: string,
|
|
@@ -298,12 +333,39 @@ interface FlightTransformPlugin {
|
|
|
298
333
|
* by the module's absolute path (the boundary id), the export name becoming the
|
|
299
334
|
* boundary name. Modules without the directive (server components) are untouched,
|
|
300
335
|
* so `renderToFlightString` of pure leaf trees is unaffected.
|
|
336
|
+
*
|
|
337
|
+
* A `"use client"` module whose default export is a `clientUrls()` definition
|
|
338
|
+
* also installs its server projection, as the rango plugin's discovery does for
|
|
339
|
+
* an app: the module imports its own source without this transform and projects
|
|
340
|
+
* it under its client reference. A router that mounts the group
|
|
341
|
+
* (`include("/shop", shopUrls)` or `.routes(shopUrls)`) then serves its routes
|
|
342
|
+
* through `serveShellRequest` and `dispatch` with no setup.
|
|
301
343
|
*/
|
|
302
344
|
export function rangoUseClientTransform(): FlightTransformPlugin {
|
|
303
345
|
return {
|
|
304
346
|
name: "rango:testing-use-client",
|
|
347
|
+
resolveId: {
|
|
348
|
+
// Before Vite's resolver, which resolves the bare specifier first.
|
|
349
|
+
order: "pre",
|
|
350
|
+
handler(source, importer) {
|
|
351
|
+
// A clientUrls() module's source resolves the router's client entry
|
|
352
|
+
// as the RSC environment does (the react-server condition): this
|
|
353
|
+
// project resolves it to client.tsx, a "use client" module whose
|
|
354
|
+
// clientUrls is a client reference here. Both entries export the
|
|
355
|
+
// same clientUrls.
|
|
356
|
+
if (
|
|
357
|
+
source === "@rangojs/router/client" &&
|
|
358
|
+
importer?.includes(CLIENT_URLS_SOURCE_QUERY)
|
|
359
|
+
) {
|
|
360
|
+
return CLIENT_ENTRY_RSC_PATH;
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
},
|
|
364
|
+
},
|
|
305
365
|
async transform(code, id) {
|
|
306
366
|
if (id.includes("/node_modules/")) return undefined;
|
|
367
|
+
// A clientUrls() module's own source, imported by its transformed copy.
|
|
368
|
+
if (id.includes(CLIENT_URLS_SOURCE_QUERY)) return undefined;
|
|
307
369
|
// Fast path: only parse modules that mention the directive.
|
|
308
370
|
if (!code.includes("use client")) return undefined;
|
|
309
371
|
const { parseAstAsync } = await import("vite");
|
|
@@ -329,7 +391,7 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
|
|
|
329
391
|
`${JSON.stringify(id)}, ${JSON.stringify(name)})`,
|
|
330
392
|
});
|
|
331
393
|
if (!result) return undefined;
|
|
332
|
-
const { output } = result;
|
|
394
|
+
const { output, exportNames } = result;
|
|
333
395
|
// Absolute file URL, not the bare specifier: the consumer's "use client"
|
|
334
396
|
// module cannot resolve @vitejs/plugin-rsc (it is a router dependency).
|
|
335
397
|
const rsdHref = rsdServerEdgePath
|
|
@@ -338,6 +400,10 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
|
|
|
338
400
|
output.prepend(
|
|
339
401
|
`import * as $$RangoRSD from ${JSON.stringify(rsdHref)};\n`,
|
|
340
402
|
);
|
|
403
|
+
// The modules discovery records (recordClientUrlsModule).
|
|
404
|
+
if (code.includes("clientUrls(") && exportNames.includes("default")) {
|
|
405
|
+
output.append(clientUrlsProjectionInstall(id));
|
|
406
|
+
}
|
|
341
407
|
return {
|
|
342
408
|
code: output.toString(),
|
|
343
409
|
map: output.generateMap({ hires: true }),
|
package/src/thenable-status.ts
CHANGED
|
@@ -24,3 +24,14 @@ export const SETTLED_THENABLE: Promise<undefined> = Object.assign(
|
|
|
24
24
|
Promise.resolve(undefined),
|
|
25
25
|
{ status: "fulfilled", value: undefined, _debugInfo: [] },
|
|
26
26
|
);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The value React reads from `value` without suspending: `value` itself when
|
|
30
|
+
* it is not a promise, a promise's `value` once it carries React's or Flight's
|
|
31
|
+
* `status: "fulfilled"`. Undefined while it is pending or rejected.
|
|
32
|
+
*/
|
|
33
|
+
export function settledValue<T>(value: T | Promise<T>): T | undefined {
|
|
34
|
+
if (!(value instanceof Promise)) return value;
|
|
35
|
+
const thenable = value as Promise<T> & { status?: string; value?: T };
|
|
36
|
+
return thenable.status === "fulfilled" ? thenable.value : undefined;
|
|
37
|
+
}
|
package/src/types/boundaries.ts
CHANGED
|
@@ -104,6 +104,18 @@ export function isLoaderDataResult(value: unknown): value is LoaderDataResult {
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* A loading skeleton is renderable only when it is a real ReactNode value;
|
|
109
|
+
* `false` counts as "not renderable". This is the three-term gate. The
|
|
110
|
+
* distinct two-term gate at the LoaderBoundary site (segment-system.tsx)
|
|
111
|
+
* deliberately treats `false` as "create a boundary without a
|
|
112
|
+
* RouteContentWrapper" (tree-structure.md), so it must NOT use this helper.
|
|
113
|
+
* Leaf module: shared by client (segment-system) and server (DSL, resolution).
|
|
114
|
+
*/
|
|
115
|
+
export function isRenderableLoading(loading: unknown): boolean {
|
|
116
|
+
return loading !== undefined && loading !== null && loading !== false;
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
export interface NotFoundInfo {
|
|
108
120
|
message: string;
|
|
109
121
|
segmentId: string;
|
|
@@ -492,6 +492,20 @@ export type InternalHandlerContext<
|
|
|
492
492
|
* setupLoaderAccess.
|
|
493
493
|
*/
|
|
494
494
|
_bindLoaderCacheTags?: (loaderId: string, tags: Set<string>) => void;
|
|
495
|
+
/**
|
|
496
|
+
* @internal This match's `prefetch: false` plan: what a prefetch defers,
|
|
497
|
+
* whether the request is a fill, and the loaders that cannot call
|
|
498
|
+
* ctx.rendered(). Set by match-api.ts when the tree declares a flag or the
|
|
499
|
+
* request is a fill. See router/segment-resolution/prefetch-deferral.ts.
|
|
500
|
+
*/
|
|
501
|
+
_prefetchDeferral?: import("../router/segment-resolution/prefetch-deferral.js").PrefetchDeferral;
|
|
502
|
+
/**
|
|
503
|
+
* @internal Whether a loader was started in this request: a run of it (a
|
|
504
|
+
* handler's or another loader's ctx.use()) or its cache() binding. A
|
|
505
|
+
* prefetch delivers such a loader instead of deferring it. See
|
|
506
|
+
* setupLoaderAccess.
|
|
507
|
+
*/
|
|
508
|
+
_loaderStarted?: (loaderId: string) => boolean;
|
|
495
509
|
};
|
|
496
510
|
|
|
497
511
|
/**
|
|
@@ -200,6 +200,36 @@ export type LoaderFn<
|
|
|
200
200
|
*/
|
|
201
201
|
export type LoaderOptions = {
|
|
202
202
|
ssr?: boolean;
|
|
203
|
+
/**
|
|
204
|
+
* `prefetch: false` keeps this loader out of prefetches. A `<Link>`
|
|
205
|
+
* prefetch (a partial request with `X-Rango-Prefetch`) does not execute it;
|
|
206
|
+
* the payload marks it deferred. A document request and a navigation with
|
|
207
|
+
* no prefetch to adopt run it as usual.
|
|
208
|
+
*
|
|
209
|
+
* A deferred loader never blocks the navigation that adopts the prefetch:
|
|
210
|
+
* it behaves like a loader that is still streaming. Its own `<Suspense>`
|
|
211
|
+
* shows at once; if the nearest boundary is the route's `loading()`, that
|
|
212
|
+
* shows; with no boundary React holds the page until the fill returns. The
|
|
213
|
+
* fill is the one follow-up request that runs the loader.
|
|
214
|
+
*
|
|
215
|
+
* The fill reads through the loader's own `cache()` and any `"use cache"`
|
|
216
|
+
* function it calls. A prefetch reads neither, by intent: looking would
|
|
217
|
+
* mean running the loader on a miss.
|
|
218
|
+
*
|
|
219
|
+
* The flag applies only to a segment the client does not have yet. A
|
|
220
|
+
* loader the client already holds (the same route with new params, say)
|
|
221
|
+
* runs in the prefetch when it revalidates, exactly as without the flag:
|
|
222
|
+
* a click that adopts a prefetch is never worse than the same click with
|
|
223
|
+
* no prefetch at all.
|
|
224
|
+
*
|
|
225
|
+
* Limits: a handler that awaits the loader with `ctx.use()` makes it run in
|
|
226
|
+
* the prefetch anyway; the loader cannot call `ctx.rendered()` (it throws
|
|
227
|
+
* on every request); on a `ppr` route the flag is ignored for an
|
|
228
|
+
* `ssr: false` loader (the bake lane), with a dev warning. In the fill
|
|
229
|
+
* request only deferred work and middleware run, so values a held handler
|
|
230
|
+
* would have set with `ctx.set()` are not visible.
|
|
231
|
+
*/
|
|
232
|
+
prefetch?: boolean;
|
|
203
233
|
};
|
|
204
234
|
|
|
205
235
|
/**
|
|
@@ -63,6 +63,14 @@ export interface RequestScope<TEnv = DefaultEnv> {
|
|
|
63
63
|
*/
|
|
64
64
|
originalUrl: URL;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The visitor's URL (the address bar, search included) when middleware
|
|
68
|
+
* rewrote this request with `throw rewrite()`; `undefined` otherwise. `url`,
|
|
69
|
+
* `pathname` and `searchParams` are the rewrite target's. After several
|
|
70
|
+
* rewrites this is still the first URL. Each context gets its own copy.
|
|
71
|
+
*/
|
|
72
|
+
rewrittenFrom?: URL;
|
|
73
|
+
|
|
66
74
|
/** URL pathname (same as `url.pathname`). */
|
|
67
75
|
pathname: string;
|
|
68
76
|
|
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
|
}
|