@rangojs/router 0.24.3 → 0.24.4
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/types/client-urls/client-root.d.ts +4 -1
- package/dist/types/client-urls/client-urls.d.ts +22 -0
- package/dist/types/client-urls/navigation.d.ts +6 -8
- package/dist/types/client-urls/server-projection.d.ts +11 -0
- package/dist/types/client-urls/types.d.ts +6 -1
- package/dist/types/router/substitute-pattern-params.d.ts +16 -0
- package/dist/types/urls/path-helper.d.ts +5 -0
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/client-urls/SKILL.md +10 -0
- package/src/browser/navigation-bridge.ts +7 -14
- package/src/browser/react/use-reverse.ts +4 -17
- package/src/client-urls/client-root.tsx +10 -5
- package/src/client-urls/client-urls.ts +44 -13
- package/src/client-urls/navigation.ts +45 -38
- package/src/client-urls/server-projection.ts +149 -9
- package/src/client-urls/types.ts +10 -1
- package/src/router/substitute-pattern-params.ts +34 -0
- package/src/urls/path-helper.ts +16 -3
|
@@ -25,12 +25,15 @@ export declare function ClientUrlsInterceptLoading({ definition, interceptIndex,
|
|
|
25
25
|
definition: ClientUrlPatterns;
|
|
26
26
|
interceptIndex: number;
|
|
27
27
|
}): ReactNode;
|
|
28
|
-
export declare function ClientUrlsRoot({ definition, routeId, namePrefix, }: {
|
|
28
|
+
export declare function ClientUrlsRoot({ definition, routeId, namePrefix, foreign, }: {
|
|
29
29
|
definition: ClientUrlPatterns;
|
|
30
30
|
routeId: string;
|
|
31
31
|
/** include() route-name prefix, injected at materialization for canonical
|
|
32
32
|
* name composition (intercept-target coordination). */
|
|
33
33
|
namePrefix?: string;
|
|
34
|
+
/** The server's routes that compete with the group's for a URL
|
|
35
|
+
* (competingPatterns in server-projection.ts). */
|
|
36
|
+
foreign?: readonly string[];
|
|
34
37
|
}): ReactNode;
|
|
35
38
|
export declare function ClientUrlsLoading({ definition, routeId, }: {
|
|
36
39
|
definition: ClientUrlPatterns;
|
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
import { type TrieNode } from "../router/route-trie-builder.js";
|
|
1
2
|
import type { ClientUrlBuilder, ClientUrlPatterns } from "./types.js";
|
|
2
3
|
import type { ExtractRoutes } from "../urls/type-extraction.js";
|
|
3
4
|
import type { ClientUrlItems } from "./types.js";
|
|
4
5
|
export declare function clientUrls<const TItems extends ClientUrlItems>(builder: ClientUrlBuilder<TItems>): ClientUrlPatterns<ExtractRoutes<TItems>>;
|
|
6
|
+
/**
|
|
7
|
+
* The router's trie over a group's routes, each pattern joined to the
|
|
8
|
+
* include() mount PATTERN (`/:locale?`, `/:accountId/:projectId`): an
|
|
9
|
+
* absolute pathname matches with the mount's params bound next to the
|
|
10
|
+
* route's, by the server's own precedence (an optional mount param expands
|
|
11
|
+
* like any other). Comparing the pathname with the mount as a literal prefix
|
|
12
|
+
* never matched a parameterized mount.
|
|
13
|
+
*
|
|
14
|
+
* `foreign` are the server's routes outside the group that compete with its
|
|
15
|
+
* routes for a URL (competingPatterns in server-projection.ts): a `/:slug`
|
|
16
|
+
* route under `/:locale?` matches a root `/login` too. Each is a leaf keyed by
|
|
17
|
+
* its own pattern, inserted last so an exact tie goes to it; a match on one is
|
|
18
|
+
* not the group's.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildClientUrlsTrie(routes: readonly {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly pattern: string;
|
|
23
|
+
readonly options?: {
|
|
24
|
+
readonly trailingSlash?: string;
|
|
25
|
+
};
|
|
26
|
+
}[], mount: string, foreign?: readonly string[]): TrieNode;
|
|
5
27
|
export type { ClientTransitionConfig, ClientUrlBuilder, ClientUrlHelpers, ClientUrlInterceptRecord, ClientUrlItem, ClientUrlItemInput, ClientUrlItems, ClientLayoutFn, ClientPathOptions, ClientUrlLoaderRecord, ClientPathFn, ClientUrlPatterns, ClientUrlRouteRecord, ClientUrlUse, } from "./types.js";
|
|
@@ -9,7 +9,10 @@ export interface ClientUrlNavigationIntent {
|
|
|
9
9
|
* screen.
|
|
10
10
|
*/
|
|
11
11
|
readonly fromFrame: boolean;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Destination params: the route's and its include() mount's, the params
|
|
14
|
+
* useParams() reads after the commit.
|
|
15
|
+
*/
|
|
13
16
|
readonly params: Readonly<Record<string, string>>;
|
|
14
17
|
/** Absolute destination pathname (mount included) and search ("?..." or ""). */
|
|
15
18
|
readonly pathname: string;
|
|
@@ -24,17 +27,12 @@ export interface ClientUrlNavigationIntent {
|
|
|
24
27
|
}
|
|
25
28
|
/** What the bridge needs to decide transition({ when }) at the swap. */
|
|
26
29
|
export interface ClientUrlDestination {
|
|
27
|
-
/**
|
|
30
|
+
/** Destination params, the include() mount's included. */
|
|
28
31
|
readonly params: Readonly<Record<string, string>>;
|
|
29
32
|
/** Canonical route name (include prefix applied); undefined when unnamed. */
|
|
30
33
|
readonly routeName: string | undefined;
|
|
31
34
|
/** The destination route's own `when`, from its "use client" definition. */
|
|
32
35
|
readonly when: TransitionWhenFn | undefined;
|
|
33
|
-
/**
|
|
34
|
-
* Param names the ORIGIN route matched inside the group: the committed
|
|
35
|
-
* params minus these are the mount's (include prefix) params.
|
|
36
|
-
*/
|
|
37
|
-
readonly originLocalParamNames: readonly string[];
|
|
38
36
|
/** `history.state` the navigation will push (ClientUrlNavigationIntent.state). */
|
|
39
37
|
readonly state: unknown;
|
|
40
38
|
}
|
|
@@ -49,7 +47,7 @@ export interface ClientUrlNavigationPresentation {
|
|
|
49
47
|
readonly transitionGatedOff?: boolean;
|
|
50
48
|
clear(): void;
|
|
51
49
|
}
|
|
52
|
-
export declare function registerClientUrlGroup(definition: ClientUrlPatterns, mount: string, namePrefix: string, setIntent: (intent: ClientUrlNavigationIntent | null) => void, routeId?: string): () => void;
|
|
50
|
+
export declare function registerClientUrlGroup(definition: ClientUrlPatterns, mount: string, namePrefix: string, setIntent: (intent: ClientUrlNavigationIntent | null) => void, routeId?: string, foreign?: readonly string[]): () => void;
|
|
53
51
|
export declare function beginClientUrlNavigation(targetUrl: URL, signal: AbortSignal,
|
|
54
52
|
/** Builds the entry state; called only when the group presents the target. */
|
|
55
53
|
buildState?: () => unknown, decideTransition?: (destination: ClientUrlDestination) => boolean): ClientUrlNavigationPresentation | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SearchSchemaValue } from "../search-params.js";
|
|
2
2
|
import type { TrailingSlashMode } from "../types.js";
|
|
3
3
|
import type { PartialPrerenderProps, UrlPatterns } from "../urls/pattern-types.js";
|
|
4
|
+
import type { TrieNode } from "../router/route-trie-builder.js";
|
|
4
5
|
import type { ClientTransitionConfig, ClientUrlPatterns } from "./types.js";
|
|
5
6
|
export interface ClientUrlReference {
|
|
6
7
|
readonly $$typeof: symbol;
|
|
@@ -101,6 +102,16 @@ export declare function isClientUrlSource(value: unknown): value is ClientUrlDef
|
|
|
101
102
|
export declare function setClientUrlProjection(reference: string | ClientUrlReference, projection: ClientUrlProjection): void;
|
|
102
103
|
export declare function getClientUrlProjection(reference: string | ClientUrlReference): ClientUrlProjection | undefined;
|
|
103
104
|
export declare function clearClientUrlProjections(): void;
|
|
105
|
+
/**
|
|
106
|
+
* The patterns of the router's routes, outside the group, that a URL of the
|
|
107
|
+
* group matches too: the trie walked in step with the group's (its patterns
|
|
108
|
+
* joined to the mount), static against static, a param against either, a
|
|
109
|
+
* catch-all against everything below. Constraints are not compared, so the
|
|
110
|
+
* list may hold a route no URL of the group reaches: the client trie, which
|
|
111
|
+
* gets the patterns as they are, never lets one win a URL it does not match.
|
|
112
|
+
* `own` are the group's route keys in the router's trie.
|
|
113
|
+
*/
|
|
114
|
+
export declare function competingPatterns(router: TrieNode, group: TrieNode, own: ReadonlySet<string>): string[];
|
|
104
115
|
export declare function materializeClientUrlPatterns(reference: ClientUrlDefinitionSource, projection: ClientUrlProjection): UrlPatterns;
|
|
105
116
|
/**
|
|
106
117
|
* Adapt a clientUrls() source for `include()` mounting inside the canonical
|
|
@@ -149,6 +149,11 @@ export interface ClientUrlPatterns<TRoutes extends Record<string, any> = Record<
|
|
|
149
149
|
readonly intercepts: readonly ClientUrlInterceptRecord[];
|
|
150
150
|
readonly [CLIENT_URL_PATTERNS_BRAND]: void;
|
|
151
151
|
readonly _routes?: TRoutes;
|
|
152
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Match an absolute pathname against the routes joined to `mount`, the
|
|
154
|
+
* include() mount pattern (default "/"); params include the mount's. A
|
|
155
|
+
* `foreign` pattern that wins is a match whose routeKey is that pattern.
|
|
156
|
+
*/
|
|
157
|
+
match(pathname: string, mount?: string, foreign?: readonly string[]): TrieMatchResult | null;
|
|
153
158
|
}
|
|
154
159
|
export {};
|
|
@@ -21,3 +21,19 @@
|
|
|
21
21
|
* identical across all three call sites.
|
|
22
22
|
*/
|
|
23
23
|
export declare function substitutePatternParams(pattern: string, params: Record<string, string | undefined>, routeName: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Join an include() mount with a mount-relative pattern. The mount is the
|
|
26
|
+
* include PATTERN (`/:locale?`), so the result is a pattern too.
|
|
27
|
+
*
|
|
28
|
+
* `pattern === "/"` is the index of the local module: under a non-root mount
|
|
29
|
+
* it collapses, so `/` under `/blog` becomes `/blog`, not `/blog/`. This
|
|
30
|
+
* matches `ctx.reverse(".index")` and, for a mount without a trailing slash,
|
|
31
|
+
* the route pattern the server registers (applyUrlPrefix in
|
|
32
|
+
* urls/path-helper.ts).
|
|
33
|
+
*/
|
|
34
|
+
export declare function joinMount(mount: string, pattern: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Whether two matches of an include() mount pattern are in the same instance
|
|
37
|
+
* of the include: `/en/...` and `/de/...` under `/:locale` are not.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sameMountInstance(mount: string, a: Readonly<Record<string, string>>, b: Readonly<Record<string, string>>): boolean;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Handler } from "../types.js";
|
|
2
2
|
import type { PathFn, ResponsePathFn, JsonResponsePathFn, TextResponsePathFn } from "./path-helper-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The key path() registers a route under: its name, or a `$path_` name built
|
|
5
|
+
* from its pattern, behind the include's name prefix.
|
|
6
|
+
*/
|
|
7
|
+
export declare function routeKeyOf(namePrefix: string | undefined, pattern: string, name: string | undefined): string;
|
|
3
8
|
/**
|
|
4
9
|
* A retained on-demand producer must never render on a live production
|
|
5
10
|
* request: bundle eviction is skipped for onDemand routes (the producer body
|
package/dist/vite/index.js
CHANGED
|
@@ -5929,7 +5929,7 @@ import { resolve } from "node:path";
|
|
|
5929
5929
|
// package.json
|
|
5930
5930
|
var package_default = {
|
|
5931
5931
|
name: "@rangojs/router",
|
|
5932
|
-
version: "0.24.
|
|
5932
|
+
version: "0.24.4",
|
|
5933
5933
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
5934
5934
|
keywords: [
|
|
5935
5935
|
"react",
|
package/package.json
CHANGED
|
@@ -557,6 +557,16 @@ a hard load of the target URL renders the full route.
|
|
|
557
557
|
hold; it adds the view-transition animation. Inline `<Suspense>` above each
|
|
558
558
|
`useLoader` read is still the finer-grained tool when different reads on
|
|
559
559
|
one route should wait independently.
|
|
560
|
+
- **Parameterized mounts.** A group under `include("/:locale", ...)`,
|
|
561
|
+
`include("/:locale?", ...)` or `include("/:accountId/:projectId", ...)`
|
|
562
|
+
presents at the click like one under a literal mount, and `useParams()` in
|
|
563
|
+
the destination carries the mount's params. A click that changes a mount
|
|
564
|
+
param (`/en/...` to `/de/...`) is another instance of the include: no
|
|
565
|
+
optimistic frame, a plain navigation. The group's intercepts open only from
|
|
566
|
+
one of its routes in the same mount instance. A URL the server routes
|
|
567
|
+
elsewhere (a root route that a group `/:slug` under `/:locale?` also
|
|
568
|
+
matches) never renders as a group route: the server ships the competing
|
|
569
|
+
routes with the group.
|
|
560
570
|
- **Two parallel loaders with equal latency look "SSR'd" together.** Loaders
|
|
561
571
|
kick off in parallel, so awaiting one (`ssr: false`) gives
|
|
562
572
|
same-or-faster siblings time to settle coincidentally. Do not read "it was
|
|
@@ -457,20 +457,13 @@ export function createNavigationBridge(
|
|
|
457
457
|
).filter((segment) => segment.clientGroup === undefined),
|
|
458
458
|
options?.replace ? "replace" : "push",
|
|
459
459
|
{
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
return {
|
|
468
|
-
url: targetUrl,
|
|
469
|
-
params: { ...params, ...destination.params },
|
|
470
|
-
routeName: destination.routeName,
|
|
471
|
-
state: destination.state,
|
|
472
|
-
};
|
|
473
|
-
},
|
|
460
|
+
// The destination's params include the mount's.
|
|
461
|
+
to: () => ({
|
|
462
|
+
url: targetUrl,
|
|
463
|
+
params: destination.params,
|
|
464
|
+
routeName: destination.routeName,
|
|
465
|
+
state: destination.state,
|
|
466
|
+
}),
|
|
474
467
|
extra: destination.when,
|
|
475
468
|
},
|
|
476
469
|
),
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback } from "react";
|
|
4
4
|
import type { LocalReverseFunction } from "../../reverse.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
joinMount,
|
|
7
|
+
substitutePatternParams,
|
|
8
|
+
} from "../../router/substitute-pattern-params.js";
|
|
6
9
|
import { serializeSearchParams } from "../../search-params.js";
|
|
7
10
|
import { useMount } from "./use-mount.js";
|
|
8
11
|
import { useParams } from "./use-params.js";
|
|
@@ -15,22 +18,6 @@ function getPattern(entry: RouteEntry | undefined): string | undefined {
|
|
|
15
18
|
return typeof entry === "string" ? entry : entry.path;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
/**
|
|
19
|
-
* Join an include mount prefix with a mount-relative pattern.
|
|
20
|
-
*
|
|
21
|
-
* `pattern === "/"` is the index of the local module — under a non-root
|
|
22
|
-
* mount it must collapse so `/` under `/blog` becomes `/blog`, not
|
|
23
|
-
* `/blog/`. This matches `ctx.reverse(".index")` on the server.
|
|
24
|
-
*/
|
|
25
|
-
function joinMount(mount: string, pattern: string): string {
|
|
26
|
-
if (pattern === "/") {
|
|
27
|
-
if (mount === "" || mount === "/") return "/";
|
|
28
|
-
return mount.endsWith("/") ? mount.slice(0, -1) : mount;
|
|
29
|
-
}
|
|
30
|
-
const normalizedMount = mount === "/" ? "" : mount.replace(/\/+$/, "");
|
|
31
|
-
return normalizedMount + pattern;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
21
|
/**
|
|
35
22
|
* Mount-aware reverse function for a locally-imported `routes` map.
|
|
36
23
|
*
|
|
@@ -108,17 +108,21 @@ export function ClientUrlsRoot({
|
|
|
108
108
|
definition,
|
|
109
109
|
routeId,
|
|
110
110
|
namePrefix = "",
|
|
111
|
+
foreign,
|
|
111
112
|
}: {
|
|
112
113
|
definition: ClientUrlPatterns;
|
|
113
114
|
routeId: string;
|
|
114
115
|
/** include() route-name prefix, injected at materialization for canonical
|
|
115
116
|
* name composition (intercept-target coordination). */
|
|
116
117
|
namePrefix?: string;
|
|
118
|
+
/** The server's routes that compete with the group's for a URL
|
|
119
|
+
* (competingPatterns in server-projection.ts). */
|
|
120
|
+
foreign?: readonly string[];
|
|
117
121
|
}): ReactNode {
|
|
118
|
-
// The include() mount
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
+
// The include() mount pattern this group renders under ("/" at root).
|
|
123
|
+
// Navigation matches an absolute pathname against the definition's
|
|
124
|
+
// patterns joined to it (buildClientUrlsTrie in client-urls.ts), as
|
|
125
|
+
// useReverse joins it.
|
|
122
126
|
const mount = useMount();
|
|
123
127
|
const [intent, setIntent] = useState<ClientUrlNavigationIntent | null>(null);
|
|
124
128
|
|
|
@@ -155,8 +159,9 @@ export function ClientUrlsRoot({
|
|
|
155
159
|
namePrefix,
|
|
156
160
|
setIntent,
|
|
157
161
|
optimisticRoute ? undefined : routeId,
|
|
162
|
+
foreign,
|
|
158
163
|
),
|
|
159
|
-
[definition, mount, namePrefix, routeId, optimisticRoute],
|
|
164
|
+
[definition, mount, namePrefix, routeId, optimisticRoute, foreign],
|
|
160
165
|
);
|
|
161
166
|
|
|
162
167
|
// Pending entries for the destination's loaders: useLoader use()s a Promise
|
|
@@ -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";
|
|
@@ -768,28 +769,58 @@ export function clientUrls<const TItems extends ClientUrlItems>(
|
|
|
768
769
|
collectIntercepts(items, intercepts);
|
|
769
770
|
validateIntercepts(intercepts, routes);
|
|
770
771
|
|
|
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);
|
|
772
|
+
const tries = new Map<string, TrieNode>();
|
|
782
773
|
|
|
783
774
|
return Object.freeze({
|
|
784
775
|
__brand: "client-urls" as const,
|
|
785
776
|
routes: Object.freeze(routes),
|
|
786
777
|
intercepts: Object.freeze(intercepts),
|
|
787
|
-
match(pathname: string) {
|
|
778
|
+
match(pathname: string, mount = "/", foreign: readonly string[] = []) {
|
|
779
|
+
const key = `${mount} ${foreign}`;
|
|
780
|
+
let trie = tries.get(key);
|
|
781
|
+
if (!trie) {
|
|
782
|
+
tries.set(key, (trie = buildClientUrlsTrie(routes, mount, foreign)));
|
|
783
|
+
}
|
|
788
784
|
return tryTrieMatch(trie, pathname);
|
|
789
785
|
},
|
|
790
786
|
}) as ClientUrlPatterns<ExtractRoutes<TItems>>;
|
|
791
787
|
}
|
|
792
788
|
|
|
789
|
+
/**
|
|
790
|
+
* The router's trie over a group's routes, each pattern joined to the
|
|
791
|
+
* include() mount PATTERN (`/:locale?`, `/:accountId/:projectId`): an
|
|
792
|
+
* absolute pathname matches with the mount's params bound next to the
|
|
793
|
+
* route's, by the server's own precedence (an optional mount param expands
|
|
794
|
+
* like any other). Comparing the pathname with the mount as a literal prefix
|
|
795
|
+
* never matched a parameterized mount.
|
|
796
|
+
*
|
|
797
|
+
* `foreign` are the server's routes outside the group that compete with its
|
|
798
|
+
* routes for a URL (competingPatterns in server-projection.ts): a `/:slug`
|
|
799
|
+
* route under `/:locale?` matches a root `/login` too. Each is a leaf keyed by
|
|
800
|
+
* its own pattern, inserted last so an exact tie goes to it; a match on one is
|
|
801
|
+
* not the group's.
|
|
802
|
+
*/
|
|
803
|
+
export function buildClientUrlsTrie(
|
|
804
|
+
routes: readonly {
|
|
805
|
+
readonly id: string;
|
|
806
|
+
readonly pattern: string;
|
|
807
|
+
readonly options?: { readonly trailingSlash?: string };
|
|
808
|
+
}[],
|
|
809
|
+
mount: string,
|
|
810
|
+
foreign: readonly string[] = [],
|
|
811
|
+
): TrieNode {
|
|
812
|
+
const routeManifest: Record<string, string> = {};
|
|
813
|
+
const trailingSlash: Record<string, string> = {};
|
|
814
|
+
for (const route of routes) {
|
|
815
|
+
routeManifest[route.id] = joinMount(mount, route.pattern);
|
|
816
|
+
if (route.options?.trailingSlash) {
|
|
817
|
+
trailingSlash[route.id] = route.options.trailingSlash;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
for (const pattern of foreign) routeManifest[pattern] = pattern;
|
|
821
|
+
return buildRouteTrie(routeManifest, {}, trailingSlash);
|
|
822
|
+
}
|
|
823
|
+
|
|
793
824
|
export type {
|
|
794
825
|
ClientTransitionConfig,
|
|
795
826
|
ClientUrlBuilder,
|
|
@@ -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
|
}
|
|
@@ -32,6 +32,13 @@ import {
|
|
|
32
32
|
ClientUrlsRoot,
|
|
33
33
|
} from "./client-root.js";
|
|
34
34
|
import { createClientUrlsWhenRef } from "../transition-when-ref.js";
|
|
35
|
+
import type { TrieLeaf, TrieNode } from "../router/route-trie-builder.js";
|
|
36
|
+
import { tryTrieMatch } from "../router/trie-matching.js";
|
|
37
|
+
import { sameMountInstance } from "../router/substitute-pattern-params.js";
|
|
38
|
+
import { getRouterTrie } from "../route-map-builder.js";
|
|
39
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
40
|
+
import { routeKeyOf } from "../urls/path-helper.js";
|
|
41
|
+
import { buildClientUrlsTrie } from "./client-urls.js";
|
|
35
42
|
import type {
|
|
36
43
|
ClientTransitionConfig,
|
|
37
44
|
ClientUrlInterceptRecord,
|
|
@@ -607,6 +614,100 @@ function materializedPathOptions(
|
|
|
607
614
|
};
|
|
608
615
|
}
|
|
609
616
|
|
|
617
|
+
/** A param segment on a walked path; the leaf's `pa` names it. */
|
|
618
|
+
const PARAM = "\0";
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* The patterns of the router's routes, outside the group, that a URL of the
|
|
622
|
+
* group matches too: the trie walked in step with the group's (its patterns
|
|
623
|
+
* joined to the mount), static against static, a param against either, a
|
|
624
|
+
* catch-all against everything below. Constraints are not compared, so the
|
|
625
|
+
* list may hold a route no URL of the group reaches: the client trie, which
|
|
626
|
+
* gets the patterns as they are, never lets one win a URL it does not match.
|
|
627
|
+
* `own` are the group's route keys in the router's trie.
|
|
628
|
+
*/
|
|
629
|
+
export function competingPatterns(
|
|
630
|
+
router: TrieNode,
|
|
631
|
+
group: TrieNode,
|
|
632
|
+
own: ReadonlySet<string>,
|
|
633
|
+
): string[] {
|
|
634
|
+
const found = new Set<string>();
|
|
635
|
+
const add = (leaf: TrieLeaf, path: readonly string[], tail = ""): void => {
|
|
636
|
+
if (own.has(leaf.n)) return;
|
|
637
|
+
let index = 0;
|
|
638
|
+
const pattern = path.map((piece) => {
|
|
639
|
+
if (piece[0] !== PARAM) return piece;
|
|
640
|
+
const name = leaf.pa?.[index++] ?? `p${index}`;
|
|
641
|
+
const allowed = leaf.cv?.[name];
|
|
642
|
+
return `/:${name}${allowed ? `(${allowed.join("|")})` : ""}${piece.slice(1)}`;
|
|
643
|
+
});
|
|
644
|
+
found.add(pattern.join("") + tail || "/");
|
|
645
|
+
};
|
|
646
|
+
const catchAll = (node: TrieNode, path: readonly string[]): void => {
|
|
647
|
+
const leaf = node.w!;
|
|
648
|
+
add(
|
|
649
|
+
leaf,
|
|
650
|
+
path,
|
|
651
|
+
leaf.pn === "*" ? "/*" : `/:${leaf.pn}${leaf.w1 ? "+" : "*"}`,
|
|
652
|
+
);
|
|
653
|
+
};
|
|
654
|
+
const everything = (node: TrieNode, path: readonly string[]): void => {
|
|
655
|
+
if (node.r) add(node.r, path);
|
|
656
|
+
if (node.w) catchAll(node, path);
|
|
657
|
+
for (const [key, child] of Object.entries(node.s ?? {})) {
|
|
658
|
+
everything(child, [...path, `/${key}`]);
|
|
659
|
+
}
|
|
660
|
+
if (node.p) everything(node.p.c, [...path, PARAM]);
|
|
661
|
+
for (const [suffix, child] of Object.entries(node.xp ?? {})) {
|
|
662
|
+
everything(child.c, [...path, PARAM + suffix]);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
// Every child of the group's node that a segment `matches` lets through.
|
|
666
|
+
const groupSteps = (
|
|
667
|
+
node: TrieNode,
|
|
668
|
+
matches: (key: string) => boolean,
|
|
669
|
+
): TrieNode[] => [
|
|
670
|
+
...Object.entries(node.s ?? {}).flatMap(([key, child]) =>
|
|
671
|
+
matches(key) ? [child] : [],
|
|
672
|
+
),
|
|
673
|
+
...(node.p ? [node.p.c] : []),
|
|
674
|
+
...Object.values(node.xp ?? {}).map((child) => child.c),
|
|
675
|
+
];
|
|
676
|
+
const walk = (g: TrieNode, r: TrieNode, path: readonly string[]): void => {
|
|
677
|
+
if (g.w) return everything(r, path);
|
|
678
|
+
if (r.r && g.r) add(r.r, path);
|
|
679
|
+
if (r.w && (g.r || g.s || g.p || g.xp)) catchAll(r, path);
|
|
680
|
+
for (const [key, child] of Object.entries(r.s ?? {})) {
|
|
681
|
+
const next = [...path, `/${key}`];
|
|
682
|
+
if (g.s?.[key]) walk(g.s[key], child, next);
|
|
683
|
+
if (g.p) walk(g.p.c, child, next);
|
|
684
|
+
for (const [suffix, gx] of Object.entries(g.xp ?? {})) {
|
|
685
|
+
if (key.endsWith(suffix)) walk(gx.c, child, next);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (r.p) {
|
|
689
|
+
for (const step of groupSteps(g, () => true)) {
|
|
690
|
+
walk(step, r.p.c, [...path, PARAM]);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
for (const [suffix, child] of Object.entries(r.xp ?? {})) {
|
|
694
|
+
for (const step of groupSteps(g, (key) => key.endsWith(suffix))) {
|
|
695
|
+
walk(step, child.c, [...path, PARAM + suffix]);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
walk(group, router, []);
|
|
700
|
+
return [...found];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** competingPatterns per router trie, per group (name prefix and mount). */
|
|
704
|
+
const COMPETING = new WeakMap<TrieNode, Map<string, readonly string[]>>();
|
|
705
|
+
|
|
706
|
+
function currentRouterTrie(): TrieNode | undefined {
|
|
707
|
+
const routerId = _getRequestContext()?._routerId;
|
|
708
|
+
return routerId ? getRouterTrie(routerId) : undefined;
|
|
709
|
+
}
|
|
710
|
+
|
|
610
711
|
function materializeRouteItems(
|
|
611
712
|
reference: ClientUrlDefinitionSource,
|
|
612
713
|
projection: ClientUrlProjection,
|
|
@@ -627,6 +728,32 @@ function materializeRouteItems(
|
|
|
627
728
|
// One key per group MOUNT (the include's URL prefix): renderSegments keys
|
|
628
729
|
// every route segment of the group by it (ResolvedSegment.clientGroup).
|
|
629
730
|
const clientGroup = getUrlPrefix() || "/";
|
|
731
|
+
// The group's routes as the router's trie names them.
|
|
732
|
+
const own = new Set(
|
|
733
|
+
projection.routes.map((route) =>
|
|
734
|
+
routeKeyOf(namePrefix, route.pattern, route.name ?? undefined),
|
|
735
|
+
),
|
|
736
|
+
);
|
|
737
|
+
// ClientUrlsRoot's `foreign`: the router's routes that compete with the
|
|
738
|
+
// group's for a URL, so the browser presents only what the server routes
|
|
739
|
+
// here (a `/:slug` route under `/:locale?` would claim a root `/login`).
|
|
740
|
+
const foreignProp = (): { foreign?: readonly string[] } => {
|
|
741
|
+
const router = currentRouterTrie();
|
|
742
|
+
if (!router) return {};
|
|
743
|
+
let byGroup = COMPETING.get(router);
|
|
744
|
+
if (!byGroup) COMPETING.set(router, (byGroup = new Map()));
|
|
745
|
+
const key = `${namePrefix} ${clientGroup}`;
|
|
746
|
+
let foreign = byGroup.get(key);
|
|
747
|
+
if (!foreign) {
|
|
748
|
+
foreign = competingPatterns(
|
|
749
|
+
router,
|
|
750
|
+
buildClientUrlsTrie(projection.routes, clientGroup),
|
|
751
|
+
own,
|
|
752
|
+
);
|
|
753
|
+
byGroup.set(key, foreign);
|
|
754
|
+
}
|
|
755
|
+
return foreign.length ? { foreign } : {};
|
|
756
|
+
};
|
|
630
757
|
|
|
631
758
|
// Helper calls attach to the CURRENT ctx.parent as they execute, so these
|
|
632
759
|
// builders must run where the items belong: at the module top level for the
|
|
@@ -672,6 +799,7 @@ function materializeRouteItems(
|
|
|
672
799
|
definition: reference as ClientUrlPatterns,
|
|
673
800
|
routeId: route.id,
|
|
674
801
|
namePrefix,
|
|
802
|
+
...foreignProp(),
|
|
675
803
|
}),
|
|
676
804
|
materializedPathOptions(route, clientGroup),
|
|
677
805
|
recordingCopies(routeIndex, () => [
|
|
@@ -744,15 +872,27 @@ function materializeRouteItems(
|
|
|
744
872
|
// out-of-group origin the slot's host layout is not in the rendered
|
|
745
873
|
// tree, so the modal would silently vanish. The declaration contract is
|
|
746
874
|
// module-local, so materialization synthesizes the origin check the DSL
|
|
747
|
-
// deliberately does not expose: activate only when the origin
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
875
|
+
// deliberately does not expose: activate only when the origin is one of
|
|
876
|
+
// this group's routes, in the target's mount instance. The router
|
|
877
|
+
// matched the origin already: its route name, or for an unnamed one its
|
|
878
|
+
// key in the router's trie, says whether it is the group's (a URL
|
|
879
|
+
// prefix cannot: a `/:locale?` mount prefixes every path), and its
|
|
880
|
+
// params whether another locale is another instance, a full page
|
|
881
|
+
// (beginClientUrlNavigation in navigation.ts leaves the group for it
|
|
882
|
+
// too). This is server-generated code — no function crosses the
|
|
883
|
+
// "use client" projection boundary.
|
|
884
|
+
const isInGroupOrigin = ({ from, to }: InterceptSelectorContext): boolean => {
|
|
885
|
+
if (clientGroup === "/") return true;
|
|
886
|
+
const router = currentRouterTrie();
|
|
887
|
+
const key =
|
|
888
|
+
from.routeName ??
|
|
889
|
+
(router && tryTrieMatch(router, from.url.pathname)?.routeKey);
|
|
890
|
+
return (
|
|
891
|
+
!!key &&
|
|
892
|
+
own.has(key) &&
|
|
893
|
+
sameMountInstance(clientGroup, from.params, to.params)
|
|
894
|
+
);
|
|
895
|
+
};
|
|
756
896
|
const slotNames = [...new Set(intercepts.map((record) => record.slotName))];
|
|
757
897
|
const buildInterceptItems = (): AllUseItems[] =>
|
|
758
898
|
intercepts.map(
|
package/src/client-urls/types.ts
CHANGED
|
@@ -215,5 +215,14 @@ export interface ClientUrlPatterns<
|
|
|
215
215
|
readonly intercepts: readonly ClientUrlInterceptRecord[];
|
|
216
216
|
readonly [CLIENT_URL_PATTERNS_BRAND]: void;
|
|
217
217
|
readonly _routes?: TRoutes;
|
|
218
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Match an absolute pathname against the routes joined to `mount`, the
|
|
220
|
+
* include() mount pattern (default "/"); params include the mount's. A
|
|
221
|
+
* `foreign` pattern that wins is a match whose routeKey is that pattern.
|
|
222
|
+
*/
|
|
223
|
+
match(
|
|
224
|
+
pathname: string,
|
|
225
|
+
mount?: string,
|
|
226
|
+
foreign?: readonly string[],
|
|
227
|
+
): TrieMatchResult | null;
|
|
219
228
|
}
|
|
@@ -73,3 +73,37 @@ export function substitutePatternParams(
|
|
|
73
73
|
if (hasTrailingSlash && !result.endsWith("/")) result += "/";
|
|
74
74
|
return result;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Join an include() mount with a mount-relative pattern. The mount is the
|
|
79
|
+
* include PATTERN (`/:locale?`), so the result is a pattern too.
|
|
80
|
+
*
|
|
81
|
+
* `pattern === "/"` is the index of the local module: under a non-root mount
|
|
82
|
+
* it collapses, so `/` under `/blog` becomes `/blog`, not `/blog/`. This
|
|
83
|
+
* matches `ctx.reverse(".index")` and, for a mount without a trailing slash,
|
|
84
|
+
* the route pattern the server registers (applyUrlPrefix in
|
|
85
|
+
* urls/path-helper.ts).
|
|
86
|
+
*/
|
|
87
|
+
export function joinMount(mount: string, pattern: string): string {
|
|
88
|
+
if (pattern === "/") {
|
|
89
|
+
if (mount === "" || mount === "/") return "/";
|
|
90
|
+
return mount.endsWith("/") ? mount.slice(0, -1) : mount;
|
|
91
|
+
}
|
|
92
|
+
const normalizedMount = mount === "/" ? "" : mount.replace(/\/+$/, "");
|
|
93
|
+
return normalizedMount + pattern;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether two matches of an include() mount pattern are in the same instance
|
|
98
|
+
* of the include: `/en/...` and `/de/...` under `/:locale` are not.
|
|
99
|
+
*/
|
|
100
|
+
export function sameMountInstance(
|
|
101
|
+
mount: string,
|
|
102
|
+
a: Readonly<Record<string, string>>,
|
|
103
|
+
b: Readonly<Record<string, string>>,
|
|
104
|
+
): boolean {
|
|
105
|
+
return (
|
|
106
|
+
substitutePatternParams(mount, a, mount) ===
|
|
107
|
+
substitutePatternParams(mount, b, mount)
|
|
108
|
+
);
|
|
109
|
+
}
|
package/src/urls/path-helper.ts
CHANGED
|
@@ -54,6 +54,21 @@ function applyNamePrefix(prefix: string | undefined, name: string): string {
|
|
|
54
54
|
return `${prefix}.${name}`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The key path() registers a route under: its name, or a `$path_` name built
|
|
59
|
+
* from its pattern, behind the include's name prefix.
|
|
60
|
+
*/
|
|
61
|
+
export function routeKeyOf(
|
|
62
|
+
namePrefix: string | undefined,
|
|
63
|
+
pattern: string,
|
|
64
|
+
name: string | undefined,
|
|
65
|
+
): string {
|
|
66
|
+
return applyNamePrefix(
|
|
67
|
+
namePrefix,
|
|
68
|
+
name || `$path_${pattern.replace(/[/:*?]/g, "_")}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
function resolveResponseType(
|
|
58
73
|
options: PathOptions | undefined,
|
|
59
74
|
): string | undefined {
|
|
@@ -143,12 +158,10 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
143
158
|
|
|
144
159
|
const prefixedPattern = applyUrlPrefix(urlPrefix, pattern);
|
|
145
160
|
|
|
146
|
-
const localName =
|
|
147
|
-
options?.name || `$path_${pattern.replace(/[/:*?]/g, "_")}`;
|
|
148
161
|
if (options?.name) {
|
|
149
162
|
validateUserRouteName(options.name);
|
|
150
163
|
}
|
|
151
|
-
const routeName =
|
|
164
|
+
const routeName = routeKeyOf(namePrefix, pattern, options?.name);
|
|
152
165
|
|
|
153
166
|
const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${routeName}`;
|
|
154
167
|
|