@rangojs/router 0.24.3 → 0.25.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/README.md +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-root.d.ts +4 -1
- package/dist/types/client-urls/client-urls.d.ts +28 -2
- package/dist/types/client-urls/navigation.d.ts +6 -8
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +20 -5
- package/dist/types/client-urls/types.d.ts +35 -17
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +19 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/urls/path-helper.d.ts +5 -0
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +195 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +119 -98
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-reverse.ts +4 -17
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-root.tsx +10 -5
- package/src/client-urls/client-urls.ts +246 -166
- package/src/client-urls/navigation.ts +45 -38
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +286 -75
- package/src/client-urls/types.ts +40 -16
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +37 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/urls/path-helper.ts +16 -3
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
import { validateUserRouteName } from "../route-name.js";
|
|
3
|
-
import { buildRouteTrie } from "../router/route-trie-builder.js";
|
|
3
|
+
import { buildRouteTrie, type TrieNode } from "../router/route-trie-builder.js";
|
|
4
|
+
import { joinMount } from "../router/substitute-pattern-params.js";
|
|
4
5
|
import { tryTrieMatch } from "../router/trie-matching.js";
|
|
5
6
|
import type { LoaderDefinition } from "../types.js";
|
|
6
7
|
import { isHoistedRevalidateRef } from "./hoisted-revalidate.js";
|
|
@@ -8,6 +9,7 @@ import type {
|
|
|
8
9
|
ClientPathOptions,
|
|
9
10
|
ClientTransitionConfig,
|
|
10
11
|
ClientUrlBuilder,
|
|
12
|
+
ClientUrlComponent,
|
|
11
13
|
ClientUrlHelpers,
|
|
12
14
|
ClientUrlInterceptRecord,
|
|
13
15
|
ClientUrlItem,
|
|
@@ -223,10 +225,60 @@ function rejectItems(
|
|
|
223
225
|
}
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
const STRUCTURAL_ITEMS = new Set<InternalItem["type"]>([
|
|
229
|
+
"path",
|
|
230
|
+
"layout",
|
|
231
|
+
"intercept",
|
|
232
|
+
]);
|
|
233
|
+
const LEAF_ITEMS = new Set<InternalItem["type"]>(["loader", "loading"]);
|
|
234
|
+
const PATH_USE_ITEMS = new Set<InternalItem["type"]>([
|
|
235
|
+
"loader",
|
|
236
|
+
"loading",
|
|
237
|
+
"transition",
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A mounted component's own items (`Dashboard.use = () => [...]`, the server
|
|
242
|
+
* handler.use pattern), validated for the mount site, followed by the site's
|
|
243
|
+
* `explicit` items, as mergeHandlerUse orders them on the server
|
|
244
|
+
* (route-definition/resolve-handler-use.ts): an explicit loading() or
|
|
245
|
+
* transition() wins, loaders accumulate.
|
|
246
|
+
*/
|
|
247
|
+
function componentUse(
|
|
248
|
+
component: ClientUrlComponent,
|
|
249
|
+
allowed: ReadonlySet<InternalItem["type"]>,
|
|
250
|
+
explicit: InternalItem[],
|
|
251
|
+
): InternalItem[] {
|
|
252
|
+
const use = component.use;
|
|
253
|
+
if (use === undefined) return explicit;
|
|
254
|
+
const label = `${component.name}.use`;
|
|
255
|
+
const items = runUse(use, label);
|
|
256
|
+
if (process.env.NODE_ENV !== "production") {
|
|
257
|
+
const structural = items.find((item) => STRUCTURAL_ITEMS.has(item.type));
|
|
258
|
+
if (structural) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`clientUrls() does not support ${structural.type}() inside ${label}: ` +
|
|
261
|
+
"routes come from the clientUrls() builder's own items, which " +
|
|
262
|
+
"route types and .gen.ts are generated from. Declare it there.",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
rejectItems(items, allowed, label);
|
|
267
|
+
if (process.env.NODE_ENV !== "production") assertTransitionCount(items);
|
|
268
|
+
return [...items, ...explicit];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Dev-only: in production the last transition() wins (addPathRecord). */
|
|
272
|
+
function assertTransitionCount(items: readonly InternalItem[]): void {
|
|
273
|
+
if (items.filter((item) => item.type === "transition").length > 1) {
|
|
274
|
+
throw new Error("clientUrls() path() received more than one transition()");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
226
278
|
function assertNamedComponent(
|
|
227
279
|
component: unknown,
|
|
228
280
|
helper: "path" | "layout" | "intercept",
|
|
229
|
-
): asserts component is
|
|
281
|
+
): asserts component is ClientUrlComponent {
|
|
230
282
|
// Build-time handler wrappers get a TARGETED rejection ahead of the generic
|
|
231
283
|
// shape check: their handler code cannot live in a "use client" bundle and
|
|
232
284
|
// their render runs at build, so accepting one could only fail later and
|
|
@@ -275,10 +327,139 @@ function assertLoaderDefinition(
|
|
|
275
327
|
}
|
|
276
328
|
}
|
|
277
329
|
|
|
330
|
+
// Stateless item constructors: the builder callback hands these out, and
|
|
331
|
+
// `@rangojs/router/client` exports them for a component's `.use`.
|
|
332
|
+
|
|
333
|
+
export const loader: ClientUrlHelpers["loader"] = (
|
|
334
|
+
definition,
|
|
335
|
+
optionsOrUse,
|
|
336
|
+
maybeUse,
|
|
337
|
+
) => {
|
|
338
|
+
assertLoaderDefinition(definition);
|
|
339
|
+
// Same options-or-use disambiguation as the server DSL loader(): the
|
|
340
|
+
// 2-arg loader(Def, use) form is untouched.
|
|
341
|
+
if (typeof optionsOrUse === "function" && maybeUse !== undefined) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
"clientUrls() loader() received two use() callbacks. Pass loader(Def, options, use) or loader(Def, use).",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const options = typeof optionsOrUse === "function" ? undefined : optionsOrUse;
|
|
347
|
+
const use = typeof optionsOrUse === "function" ? optionsOrUse : maybeUse;
|
|
348
|
+
if ((options as { stream?: unknown } | undefined)?.stream !== undefined) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
"clientUrls() loader() stream was replaced: use loader(Def, { ssr: false }) — the same knob as loading(fallback, { ssr: false }) — to await the loader before first flush on document requests.",
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
// Inline, not errors.ts assertBooleanOption: this module is in the
|
|
354
|
+
// client router chunk, where the helper costs 20 B gzip.
|
|
355
|
+
if (options?.ssr !== undefined && typeof options.ssr !== "boolean") {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`clientUrls() loader() ssr must be a boolean (got ${JSON.stringify(options.ssr)}). Omit it (or pass true) to stream on every render.`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (
|
|
361
|
+
process.env.NODE_ENV !== "production" &&
|
|
362
|
+
options?.prefetch !== undefined &&
|
|
363
|
+
typeof options.prefetch !== "boolean"
|
|
364
|
+
) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`clientUrls() loader() prefetch must be a boolean (got ${JSON.stringify(options.prefetch)}). Omit it (or pass true) to run the loader in prefetches.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const items = use ? runUse(use, "loader use") : [];
|
|
370
|
+
rejectItems(items, new Set(["revalidate"]), "loader");
|
|
371
|
+
return toPublicItem({
|
|
372
|
+
[ITEM_KIND]: true,
|
|
373
|
+
type: "loader",
|
|
374
|
+
definition,
|
|
375
|
+
revalidate: items
|
|
376
|
+
.filter(
|
|
377
|
+
(item): item is Extract<InternalItem, { type: "revalidate" }> =>
|
|
378
|
+
item.type === "revalidate",
|
|
379
|
+
)
|
|
380
|
+
.map((item) => item.fn),
|
|
381
|
+
...(options?.ssr === false ? { ssr: false as const } : {}),
|
|
382
|
+
...(options?.prefetch === false ? { prefetch: false as const } : {}),
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export const revalidate: ClientUrlHelpers["revalidate"] = (fn) => {
|
|
387
|
+
// Dev-only: client-urls-revalidate-hoist.ts rejects the rest at build.
|
|
388
|
+
if (
|
|
389
|
+
process.env.NODE_ENV !== "production" &&
|
|
390
|
+
typeof fn !== "function" &&
|
|
391
|
+
typeof fn !== "boolean" &&
|
|
392
|
+
!isHoistedRevalidateRef(fn)
|
|
393
|
+
) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"clientUrls() revalidate() expects a predicate function or a boolean",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
return toPublicItem({
|
|
399
|
+
[ITEM_KIND]: true,
|
|
400
|
+
type: "revalidate",
|
|
401
|
+
fn,
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
export const loading: ClientUrlHelpers["loading"] = (component) =>
|
|
406
|
+
toPublicItem({ [ITEM_KIND]: true, type: "loading", component });
|
|
407
|
+
|
|
408
|
+
export const transition: ClientUrlHelpers["transition"] = (config) => {
|
|
409
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
410
|
+
throw new Error("clientUrls() transition() expects a configuration object");
|
|
411
|
+
}
|
|
412
|
+
for (const key of Reflect.ownKeys(config)) {
|
|
413
|
+
if (!TRANSITION_CONFIG_KEYS.has(key)) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`clientUrls() transition() option ${JSON.stringify(String(key))} is not supported`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (config.name !== undefined && typeof config.name !== "string") {
|
|
420
|
+
throw new Error("clientUrls() transition() name must be a string");
|
|
421
|
+
}
|
|
422
|
+
if (config.when !== undefined && typeof config.when !== "function") {
|
|
423
|
+
throw new TypeError(
|
|
424
|
+
process.env.NODE_ENV !== "production"
|
|
425
|
+
? `clientUrls() transition() when must be a function (it runs in the browser), got ${config.when === null ? "null" : typeof config.when}`
|
|
426
|
+
: "clientUrls() transition() when must be a function",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (
|
|
430
|
+
config.viewTransition !== undefined &&
|
|
431
|
+
config.viewTransition !== "auto" &&
|
|
432
|
+
config.viewTransition !== false
|
|
433
|
+
) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
'clientUrls() transition() viewTransition must be "auto" or false',
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
for (const key of ["enter", "exit", "update", "share", "default"] as const) {
|
|
439
|
+
const value = config[key];
|
|
440
|
+
if (value === undefined || typeof value === "string") continue;
|
|
441
|
+
const isStringMap =
|
|
442
|
+
typeof value === "object" &&
|
|
443
|
+
value !== null &&
|
|
444
|
+
!Array.isArray(value) &&
|
|
445
|
+
Object.values(value).every((entry) => typeof entry === "string");
|
|
446
|
+
if (!isStringMap) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`clientUrls() transition() ${key} must be a string or a string map of transition types`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return toPublicItem({
|
|
453
|
+
[ITEM_KIND]: true,
|
|
454
|
+
type: "transition",
|
|
455
|
+
config: { ...config },
|
|
456
|
+
});
|
|
457
|
+
};
|
|
458
|
+
|
|
278
459
|
function createHelpers(): ClientUrlHelpers {
|
|
279
460
|
const path = ((
|
|
280
461
|
pattern: string,
|
|
281
|
-
component:
|
|
462
|
+
component: ClientUrlComponent,
|
|
282
463
|
optionsOrUse?: ClientPathOptions | ClientUrlUse,
|
|
283
464
|
maybeUse?: ClientUrlUse,
|
|
284
465
|
): ClientUrlItem => {
|
|
@@ -333,24 +514,20 @@ function createHelpers(): ClientUrlHelpers {
|
|
|
333
514
|
}
|
|
334
515
|
|
|
335
516
|
const items = use ? runUse(use, "path use") : [];
|
|
336
|
-
rejectItems(items,
|
|
337
|
-
if (
|
|
338
|
-
throw new Error(
|
|
339
|
-
"clientUrls() path() received more than one transition()",
|
|
340
|
-
);
|
|
341
|
-
}
|
|
517
|
+
rejectItems(items, PATH_USE_ITEMS, "path");
|
|
518
|
+
if (process.env.NODE_ENV !== "production") assertTransitionCount(items);
|
|
342
519
|
return toPublicItem({
|
|
343
520
|
[ITEM_KIND]: true,
|
|
344
521
|
type: "path",
|
|
345
522
|
pattern,
|
|
346
523
|
component,
|
|
347
524
|
options,
|
|
348
|
-
use: items,
|
|
525
|
+
use: componentUse(component, PATH_USE_ITEMS, items),
|
|
349
526
|
});
|
|
350
527
|
}) as ClientUrlHelpers["path"];
|
|
351
528
|
|
|
352
529
|
const layout = ((
|
|
353
|
-
component:
|
|
530
|
+
component: ClientUrlComponent,
|
|
354
531
|
children: ClientUrlUse,
|
|
355
532
|
): ClientUrlItem => {
|
|
356
533
|
assertNamedComponent(component, "layout");
|
|
@@ -360,147 +537,16 @@ function createHelpers(): ClientUrlHelpers {
|
|
|
360
537
|
new Set(["path", "layout", "loader", "loading", "intercept"]),
|
|
361
538
|
"layout",
|
|
362
539
|
);
|
|
540
|
+
// Merged here, before flattenLayout's applyConfig creates the layout's
|
|
541
|
+
// loader records once for every route below it (layoutLoaders identity).
|
|
363
542
|
return toPublicItem({
|
|
364
543
|
[ITEM_KIND]: true,
|
|
365
544
|
type: "layout",
|
|
366
545
|
component,
|
|
367
|
-
children: items,
|
|
546
|
+
children: componentUse(component, LEAF_ITEMS, items),
|
|
368
547
|
});
|
|
369
548
|
}) as ClientUrlHelpers["layout"];
|
|
370
549
|
|
|
371
|
-
const loader: ClientUrlHelpers["loader"] = (
|
|
372
|
-
definition,
|
|
373
|
-
optionsOrUse,
|
|
374
|
-
maybeUse,
|
|
375
|
-
) => {
|
|
376
|
-
assertLoaderDefinition(definition);
|
|
377
|
-
// Same options-or-use disambiguation as the server DSL loader(): the
|
|
378
|
-
// 2-arg loader(Def, use) form is untouched.
|
|
379
|
-
if (typeof optionsOrUse === "function" && maybeUse !== undefined) {
|
|
380
|
-
throw new Error(
|
|
381
|
-
"clientUrls() loader() received two use() callbacks. Pass loader(Def, options, use) or loader(Def, use).",
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
const options =
|
|
385
|
-
typeof optionsOrUse === "function" ? undefined : optionsOrUse;
|
|
386
|
-
const use = typeof optionsOrUse === "function" ? optionsOrUse : maybeUse;
|
|
387
|
-
if ((options as { stream?: unknown } | undefined)?.stream !== undefined) {
|
|
388
|
-
throw new Error(
|
|
389
|
-
"clientUrls() loader() stream was replaced: use loader(Def, { ssr: false }) — the same knob as loading(fallback, { ssr: false }) — to await the loader before first flush on document requests.",
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
// Inline, not errors.ts assertBooleanOption: this module is in the
|
|
393
|
-
// client router chunk, where the helper costs 20 B gzip.
|
|
394
|
-
if (options?.ssr !== undefined && typeof options.ssr !== "boolean") {
|
|
395
|
-
throw new Error(
|
|
396
|
-
`clientUrls() loader() ssr must be a boolean (got ${JSON.stringify(options.ssr)}). Omit it (or pass true) to stream on every render.`,
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
if (
|
|
400
|
-
process.env.NODE_ENV !== "production" &&
|
|
401
|
-
options?.prefetch !== undefined &&
|
|
402
|
-
typeof options.prefetch !== "boolean"
|
|
403
|
-
) {
|
|
404
|
-
throw new Error(
|
|
405
|
-
`clientUrls() loader() prefetch must be a boolean (got ${JSON.stringify(options.prefetch)}). Omit it (or pass true) to run the loader in prefetches.`,
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
const items = use ? runUse(use, "loader use") : [];
|
|
409
|
-
rejectItems(items, new Set(["revalidate"]), "loader");
|
|
410
|
-
return toPublicItem({
|
|
411
|
-
[ITEM_KIND]: true,
|
|
412
|
-
type: "loader",
|
|
413
|
-
definition,
|
|
414
|
-
revalidate: items
|
|
415
|
-
.filter(
|
|
416
|
-
(item): item is Extract<InternalItem, { type: "revalidate" }> =>
|
|
417
|
-
item.type === "revalidate",
|
|
418
|
-
)
|
|
419
|
-
.map((item) => item.fn),
|
|
420
|
-
...(options?.ssr === false ? { ssr: false as const } : {}),
|
|
421
|
-
...(options?.prefetch === false ? { prefetch: false as const } : {}),
|
|
422
|
-
});
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const revalidate: ClientUrlHelpers["revalidate"] = (fn) => {
|
|
426
|
-
// The build leaves a marker in place of the predicate it moved to the
|
|
427
|
-
// server (vite/plugins/client-urls-revalidate-hoist.ts).
|
|
428
|
-
if (typeof fn !== "function" && !isHoistedRevalidateRef(fn)) {
|
|
429
|
-
throw new Error("clientUrls() revalidate() expects a predicate function");
|
|
430
|
-
}
|
|
431
|
-
return toPublicItem({
|
|
432
|
-
[ITEM_KIND]: true,
|
|
433
|
-
type: "revalidate",
|
|
434
|
-
fn,
|
|
435
|
-
});
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const loading: ClientUrlHelpers["loading"] = (component) =>
|
|
439
|
-
toPublicItem({ [ITEM_KIND]: true, type: "loading", component });
|
|
440
|
-
|
|
441
|
-
const transition: ClientUrlHelpers["transition"] = (config) => {
|
|
442
|
-
if (
|
|
443
|
-
typeof config !== "object" ||
|
|
444
|
-
config === null ||
|
|
445
|
-
Array.isArray(config)
|
|
446
|
-
) {
|
|
447
|
-
throw new Error(
|
|
448
|
-
"clientUrls() transition() expects a configuration object",
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
for (const key of Reflect.ownKeys(config)) {
|
|
452
|
-
if (!TRANSITION_CONFIG_KEYS.has(key)) {
|
|
453
|
-
throw new Error(
|
|
454
|
-
`clientUrls() transition() option ${JSON.stringify(String(key))} is not supported`,
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
if (config.name !== undefined && typeof config.name !== "string") {
|
|
459
|
-
throw new Error("clientUrls() transition() name must be a string");
|
|
460
|
-
}
|
|
461
|
-
if (config.when !== undefined && typeof config.when !== "function") {
|
|
462
|
-
throw new TypeError(
|
|
463
|
-
process.env.NODE_ENV !== "production"
|
|
464
|
-
? `clientUrls() transition() when must be a function (it runs in the browser), got ${config.when === null ? "null" : typeof config.when}`
|
|
465
|
-
: "clientUrls() transition() when must be a function",
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
if (
|
|
469
|
-
config.viewTransition !== undefined &&
|
|
470
|
-
config.viewTransition !== "auto" &&
|
|
471
|
-
config.viewTransition !== false
|
|
472
|
-
) {
|
|
473
|
-
throw new Error(
|
|
474
|
-
'clientUrls() transition() viewTransition must be "auto" or false',
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
for (const key of [
|
|
478
|
-
"enter",
|
|
479
|
-
"exit",
|
|
480
|
-
"update",
|
|
481
|
-
"share",
|
|
482
|
-
"default",
|
|
483
|
-
] as const) {
|
|
484
|
-
const value = config[key];
|
|
485
|
-
if (value === undefined || typeof value === "string") continue;
|
|
486
|
-
const isStringMap =
|
|
487
|
-
typeof value === "object" &&
|
|
488
|
-
value !== null &&
|
|
489
|
-
!Array.isArray(value) &&
|
|
490
|
-
Object.values(value).every((entry) => typeof entry === "string");
|
|
491
|
-
if (!isStringMap) {
|
|
492
|
-
throw new Error(
|
|
493
|
-
`clientUrls() transition() ${key} must be a string or a string map of transition types`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return toPublicItem({
|
|
498
|
-
[ITEM_KIND]: true,
|
|
499
|
-
type: "transition",
|
|
500
|
-
config: { ...config },
|
|
501
|
-
});
|
|
502
|
-
};
|
|
503
|
-
|
|
504
550
|
const intercept: ClientUrlHelpers["intercept"] = (
|
|
505
551
|
slotName,
|
|
506
552
|
targetName,
|
|
@@ -522,8 +568,9 @@ function createHelpers(): ClientUrlHelpers {
|
|
|
522
568
|
);
|
|
523
569
|
}
|
|
524
570
|
assertNamedComponent(component, "intercept");
|
|
525
|
-
const
|
|
526
|
-
rejectItems(
|
|
571
|
+
const own = use ? runUse(use, "intercept use") : [];
|
|
572
|
+
rejectItems(own, LEAF_ITEMS, "intercept");
|
|
573
|
+
const items = componentUse(component, LEAF_ITEMS, own);
|
|
527
574
|
for (const item of items) {
|
|
528
575
|
// Intercept loaders run on soft navigations only — a document-render
|
|
529
576
|
// await can never apply, so accepting the flag would be silently inert.
|
|
@@ -607,11 +654,12 @@ function addPathRecord(
|
|
|
607
654
|
const routeContext = applyConfig(context, item.use);
|
|
608
655
|
const options = item.options ? Object.freeze({ ...item.options }) : undefined;
|
|
609
656
|
// Route-scoped only: transition() is rejected outside path() use callbacks,
|
|
610
|
-
// so there is no layout-level inheritance to fold in.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
657
|
+
// so there is no layout-level inheritance to fold in. The last one wins:
|
|
658
|
+
// the explicit use callback's over the component's `.use`.
|
|
659
|
+
let transitionItem: Extract<InternalItem, { type: "transition" }> | undefined;
|
|
660
|
+
for (const useItem of item.use) {
|
|
661
|
+
if (useItem.type === "transition") transitionItem = useItem;
|
|
662
|
+
}
|
|
615
663
|
routes.push(
|
|
616
664
|
Object.freeze({
|
|
617
665
|
id: `client-route-${routes.length}`,
|
|
@@ -702,12 +750,13 @@ function collectIntercepts(
|
|
|
702
750
|
let loading: ReactNode | undefined;
|
|
703
751
|
for (const useItem of item.use) {
|
|
704
752
|
if (useItem.type === "loader") {
|
|
705
|
-
// `prefetch` is not carried: an intercept prefetch defers nothing,
|
|
706
|
-
// so the flag is accepted and ignored, as on a urls() intercept().
|
|
707
753
|
loaders.push(
|
|
708
754
|
Object.freeze({
|
|
709
755
|
loader: useItem.definition,
|
|
710
756
|
revalidate: Object.freeze([...useItem.revalidate]),
|
|
757
|
+
...(useItem.prefetch === false
|
|
758
|
+
? { prefetch: false as const }
|
|
759
|
+
: {}),
|
|
711
760
|
}),
|
|
712
761
|
);
|
|
713
762
|
} else if (useItem.type === "loading") {
|
|
@@ -768,31 +817,62 @@ export function clientUrls<const TItems extends ClientUrlItems>(
|
|
|
768
817
|
collectIntercepts(items, intercepts);
|
|
769
818
|
validateIntercepts(intercepts, routes);
|
|
770
819
|
|
|
771
|
-
const
|
|
772
|
-
const staticPrefixes: Record<string, string> = {};
|
|
773
|
-
const trailingSlash: Record<string, string> = {};
|
|
774
|
-
for (const route of routes) {
|
|
775
|
-
routeManifest[route.id] = route.pattern;
|
|
776
|
-
staticPrefixes[route.id] = "";
|
|
777
|
-
if (route.options?.trailingSlash) {
|
|
778
|
-
trailingSlash[route.id] = route.options.trailingSlash;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
const trie = buildRouteTrie(routeManifest, staticPrefixes, trailingSlash);
|
|
820
|
+
const tries = new Map<string, TrieNode>();
|
|
782
821
|
|
|
783
822
|
return Object.freeze({
|
|
784
823
|
__brand: "client-urls" as const,
|
|
785
824
|
routes: Object.freeze(routes),
|
|
786
825
|
intercepts: Object.freeze(intercepts),
|
|
787
|
-
match(pathname: string) {
|
|
826
|
+
match(pathname: string, mount = "/", foreign: readonly string[] = []) {
|
|
827
|
+
const key = `${mount} ${foreign}`;
|
|
828
|
+
let trie = tries.get(key);
|
|
829
|
+
if (!trie) {
|
|
830
|
+
tries.set(key, (trie = buildClientUrlsTrie(routes, mount, foreign)));
|
|
831
|
+
}
|
|
788
832
|
return tryTrieMatch(trie, pathname);
|
|
789
833
|
},
|
|
790
834
|
}) as ClientUrlPatterns<ExtractRoutes<TItems>>;
|
|
791
835
|
}
|
|
792
836
|
|
|
837
|
+
/**
|
|
838
|
+
* The router's trie over a group's routes, each pattern joined to the
|
|
839
|
+
* include() mount PATTERN (`/:locale?`, `/:accountId/:projectId`): an
|
|
840
|
+
* absolute pathname matches with the mount's params bound next to the
|
|
841
|
+
* route's, by the server's own precedence (an optional mount param expands
|
|
842
|
+
* like any other). Comparing the pathname with the mount as a literal prefix
|
|
843
|
+
* never matched a parameterized mount.
|
|
844
|
+
*
|
|
845
|
+
* `foreign` are the server's routes outside the group that compete with its
|
|
846
|
+
* routes for a URL (competingPatterns in server-projection.ts): a `/:slug`
|
|
847
|
+
* route under `/:locale?` matches a root `/login` too. Each is a leaf keyed by
|
|
848
|
+
* its own pattern, inserted last so an exact tie goes to it; a match on one is
|
|
849
|
+
* not the group's.
|
|
850
|
+
*/
|
|
851
|
+
export function buildClientUrlsTrie(
|
|
852
|
+
routes: readonly {
|
|
853
|
+
readonly id: string;
|
|
854
|
+
readonly pattern: string;
|
|
855
|
+
readonly options?: { readonly trailingSlash?: string };
|
|
856
|
+
}[],
|
|
857
|
+
mount: string,
|
|
858
|
+
foreign: readonly string[] = [],
|
|
859
|
+
): TrieNode {
|
|
860
|
+
const routeManifest: Record<string, string> = {};
|
|
861
|
+
const trailingSlash: Record<string, string> = {};
|
|
862
|
+
for (const route of routes) {
|
|
863
|
+
routeManifest[route.id] = joinMount(mount, route.pattern);
|
|
864
|
+
if (route.options?.trailingSlash) {
|
|
865
|
+
trailingSlash[route.id] = route.options.trailingSlash;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
for (const pattern of foreign) routeManifest[pattern] = pattern;
|
|
869
|
+
return buildRouteTrie(routeManifest, {}, trailingSlash);
|
|
870
|
+
}
|
|
871
|
+
|
|
793
872
|
export type {
|
|
794
873
|
ClientTransitionConfig,
|
|
795
874
|
ClientUrlBuilder,
|
|
875
|
+
ClientUrlComponent,
|
|
796
876
|
ClientUrlHelpers,
|
|
797
877
|
ClientUrlInterceptRecord,
|
|
798
878
|
ClientUrlItem,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { startTransition } from "react";
|
|
4
|
+
import { sameMountInstance } from "../router/substitute-pattern-params.js";
|
|
4
5
|
import type { ClientUrlPatterns } from "./types.js";
|
|
5
6
|
import type { TransitionWhenFn } from "../types/segments.js";
|
|
6
7
|
|
|
@@ -13,7 +14,10 @@ export interface ClientUrlNavigationIntent {
|
|
|
13
14
|
* screen.
|
|
14
15
|
*/
|
|
15
16
|
readonly fromFrame: boolean;
|
|
16
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Destination params: the route's and its include() mount's, the params
|
|
19
|
+
* useParams() reads after the commit.
|
|
20
|
+
*/
|
|
17
21
|
readonly params: Readonly<Record<string, string>>;
|
|
18
22
|
/** Absolute destination pathname (mount included) and search ("?..." or ""). */
|
|
19
23
|
readonly pathname: string;
|
|
@@ -29,27 +33,27 @@ export interface ClientUrlNavigationIntent {
|
|
|
29
33
|
|
|
30
34
|
/** What the bridge needs to decide transition({ when }) at the swap. */
|
|
31
35
|
export interface ClientUrlDestination {
|
|
32
|
-
/**
|
|
36
|
+
/** Destination params, the include() mount's included. */
|
|
33
37
|
readonly params: Readonly<Record<string, string>>;
|
|
34
38
|
/** Canonical route name (include prefix applied); undefined when unnamed. */
|
|
35
39
|
readonly routeName: string | undefined;
|
|
36
40
|
/** The destination route's own `when`, from its "use client" definition. */
|
|
37
41
|
readonly when: TransitionWhenFn | undefined;
|
|
38
|
-
/**
|
|
39
|
-
* Param names the ORIGIN route matched inside the group: the committed
|
|
40
|
-
* params minus these are the mount's (include prefix) params.
|
|
41
|
-
*/
|
|
42
|
-
readonly originLocalParamNames: readonly string[];
|
|
43
42
|
/** `history.state` the navigation will push (ClientUrlNavigationIntent.state). */
|
|
44
43
|
readonly state: unknown;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
interface ActiveClientUrlGroup {
|
|
48
47
|
readonly definition: ClientUrlPatterns;
|
|
49
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* include() mount the group is rendered under ("/" at root): the PATTERN
|
|
50
|
+
* (`/:locale?`), as useMount() returns it.
|
|
51
|
+
*/
|
|
50
52
|
readonly mount: string;
|
|
51
53
|
/** include() route-name prefix ("" at root) for canonical name composition. */
|
|
52
54
|
readonly namePrefix: string;
|
|
55
|
+
/** The server's routes that compete with the group's (ClientUrlsRoot). */
|
|
56
|
+
readonly foreign: readonly string[] | undefined;
|
|
53
57
|
/**
|
|
54
58
|
* The group route on screen (ClientUrlsRoot's committed routeId), undefined
|
|
55
59
|
* while an optimistic frame is. Registered at commit.
|
|
@@ -90,34 +94,19 @@ export interface ClientUrlNavigationPresentation {
|
|
|
90
94
|
|
|
91
95
|
let activeGroup: ActiveClientUrlGroup | null = null;
|
|
92
96
|
|
|
93
|
-
/**
|
|
94
|
-
* Strip the include mount prefix from an absolute pathname, yielding the
|
|
95
|
-
* definition-local pathname the module trie was built from. Returns null when
|
|
96
|
-
* the pathname lies outside the mount — the navigation then targets a server
|
|
97
|
-
* route and gets no optimistic presentation. Mirrors joinMount() in
|
|
98
|
-
* use-reverse.ts: the bare mount maps to the module index "/".
|
|
99
|
-
*/
|
|
100
|
-
function stripMountPrefix(pathname: string, mount: string): string | null {
|
|
101
|
-
if (mount === "" || mount === "/") return pathname;
|
|
102
|
-
const normalized = mount.endsWith("/") ? mount.slice(0, -1) : mount;
|
|
103
|
-
if (pathname === normalized) return "/";
|
|
104
|
-
if (pathname.startsWith(`${normalized}/`)) {
|
|
105
|
-
return pathname.slice(normalized.length);
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
97
|
export function registerClientUrlGroup(
|
|
111
98
|
definition: ClientUrlPatterns,
|
|
112
99
|
mount: string,
|
|
113
100
|
namePrefix: string,
|
|
114
101
|
setIntent: (intent: ClientUrlNavigationIntent | null) => void,
|
|
115
102
|
routeId?: string,
|
|
103
|
+
foreign?: readonly string[],
|
|
116
104
|
): () => void {
|
|
117
105
|
const group: ActiveClientUrlGroup = {
|
|
118
106
|
definition,
|
|
119
107
|
mount,
|
|
120
108
|
namePrefix,
|
|
109
|
+
foreign,
|
|
121
110
|
routeId,
|
|
122
111
|
setIntent,
|
|
123
112
|
};
|
|
@@ -138,20 +127,42 @@ export function beginClientUrlNavigation(
|
|
|
138
127
|
const group = activeGroup;
|
|
139
128
|
if (!group) return null;
|
|
140
129
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
130
|
+
// Not one of the group's routes under its mount pattern, or a server route
|
|
131
|
+
// that wins the URL (a foreign leaf): no optimistic presentation.
|
|
132
|
+
const match = group.definition.match(
|
|
133
|
+
targetUrl.pathname,
|
|
134
|
+
group.mount,
|
|
135
|
+
group.foreign,
|
|
136
|
+
);
|
|
145
137
|
if (!match || match.redirectTo) return null;
|
|
138
|
+
const record = group.definition.routes.find(
|
|
139
|
+
(candidate) => candidate.id === match.routeKey,
|
|
140
|
+
);
|
|
141
|
+
if (!record) return null;
|
|
142
|
+
// Other mount params (`/en/...` to `/de/...`) are another instance of the
|
|
143
|
+
// group: a frame would render the destination inside the old instance's
|
|
144
|
+
// chrome (the server's layouts above the group, still the old locale's).
|
|
145
|
+
// Such a click leaves the group: a plain navigation. History still holds the
|
|
146
|
+
// committed entry while a frame is on screen, so it is the origin.
|
|
147
|
+
if (group.mount.includes(":")) {
|
|
148
|
+
const origin = group.definition.match(
|
|
149
|
+
window.location.pathname,
|
|
150
|
+
group.mount,
|
|
151
|
+
group.foreign,
|
|
152
|
+
);
|
|
153
|
+
if (
|
|
154
|
+
!origin ||
|
|
155
|
+
!sameMountInstance(group.mount, origin.params, match.params)
|
|
156
|
+
) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
146
160
|
|
|
147
161
|
// Decline when an intercept would claim this target: compose the matched
|
|
148
162
|
// record's canonical route name (include namePrefix + local name) and check
|
|
149
163
|
// it against the current location's intercept target set. Unnamed records
|
|
150
164
|
// cannot be intercept targets (intercepts target route NAMES).
|
|
151
|
-
const
|
|
152
|
-
(candidate) => candidate.id === match.routeKey,
|
|
153
|
-
);
|
|
154
|
-
const canonicalName = record?.name
|
|
165
|
+
const canonicalName = record.name
|
|
155
166
|
? group.namePrefix
|
|
156
167
|
? `${group.namePrefix}.${record.name}`
|
|
157
168
|
: record.name
|
|
@@ -166,15 +177,11 @@ export function beginClientUrlNavigation(
|
|
|
166
177
|
// screen, group.routeId is undefined and every intent swaps.
|
|
167
178
|
let transitionGatedOff: boolean | undefined;
|
|
168
179
|
if (decideTransition && match.routeKey !== group.routeId) {
|
|
169
|
-
const
|
|
170
|
-
const originMatch =
|
|
171
|
-
originLocal === null ? null : group.definition.match(originLocal);
|
|
172
|
-
const when = record?.transition?.when;
|
|
180
|
+
const when = record.transition?.when;
|
|
173
181
|
transitionGatedOff = decideTransition({
|
|
174
182
|
params: match.params,
|
|
175
183
|
routeName: canonicalName,
|
|
176
184
|
when: typeof when === "function" ? when : undefined,
|
|
177
|
-
originLocalParamNames: Object.keys(originMatch?.params ?? {}),
|
|
178
185
|
state,
|
|
179
186
|
});
|
|
180
187
|
}
|