@rangojs/router 0.5.2 → 0.7.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 +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +71 -0
- package/dist/types/client-urls/types.d.ts +147 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +13 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
- package/dist/vite/index.js +6159 -2959
- package/package.json +6 -5
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +776 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +670 -0
- package/src/client-urls/types.ts +201 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +55 -2
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +34 -23
- package/src/router/segment-resolution/loader-mask.ts +9 -9
- package/src/router/segment-resolution/revalidation.ts +23 -2
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +17 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/client-ref-dedup.ts +281 -19
- package/src/vite/plugins/expose-action-id.ts +45 -23
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import { validateUserRouteName } from "../route-name.js";
|
|
3
|
+
import { buildRouteTrie } from "../router/route-trie-builder.js";
|
|
4
|
+
import { tryTrieMatch } from "../router/trie-matching.js";
|
|
5
|
+
import type { LoaderDefinition } from "../types.js";
|
|
6
|
+
import type {
|
|
7
|
+
ClientPathOptions,
|
|
8
|
+
ClientRevalidateFn,
|
|
9
|
+
ClientTransitionConfig,
|
|
10
|
+
ClientUrlBuilder,
|
|
11
|
+
ClientUrlHelpers,
|
|
12
|
+
ClientUrlInterceptRecord,
|
|
13
|
+
ClientUrlItem,
|
|
14
|
+
ClientUrlLoaderRecord,
|
|
15
|
+
ClientUrlPatterns,
|
|
16
|
+
ClientUrlRouteRecord,
|
|
17
|
+
ClientUrlUse,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
import type { ExtractRoutes } from "../urls/type-extraction.js";
|
|
20
|
+
import type { ClientUrlItems } from "./types.js";
|
|
21
|
+
|
|
22
|
+
const ITEM_KIND = Symbol("client-url-item");
|
|
23
|
+
|
|
24
|
+
const TRANSITION_CONFIG_KEYS = new Set<PropertyKey>([
|
|
25
|
+
"enter",
|
|
26
|
+
"exit",
|
|
27
|
+
"update",
|
|
28
|
+
"share",
|
|
29
|
+
"default",
|
|
30
|
+
"name",
|
|
31
|
+
"viewTransition",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const PPR_NUMBER_KEYS = new Set<PropertyKey>([
|
|
35
|
+
"ttl",
|
|
36
|
+
"swr",
|
|
37
|
+
"captureTimeout",
|
|
38
|
+
"maxSnapshotBytes",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* DSL-time shape check for the projected `ppr` path option. Mirrors
|
|
43
|
+
* PartialPrerenderProps but validates by hand: the value must survive the
|
|
44
|
+
* JSON projection to the server, so only `true` or a plain object of finite
|
|
45
|
+
* numbers plus a string[] `tags` is legal. The projection serializer
|
|
46
|
+
* (server-projection.ts) re-validates independently — this copy exists so a
|
|
47
|
+
* bad value fails in the authoring module with a DSL-shaped message.
|
|
48
|
+
*/
|
|
49
|
+
function validateClientPpr(value: unknown): void {
|
|
50
|
+
if (value === true) return;
|
|
51
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"clientUrls() path() ppr must be true or a PartialPrerenderProps object",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
57
|
+
if (PPR_NUMBER_KEYS.has(key)) {
|
|
58
|
+
const n = (value as Record<PropertyKey, unknown>)[key];
|
|
59
|
+
if (typeof n !== "number" || !Number.isFinite(n)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`clientUrls() path() ppr.${String(key)} must be a finite number`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (key === "tags") {
|
|
67
|
+
const tags = (value as { tags: unknown }).tags;
|
|
68
|
+
if (
|
|
69
|
+
!Array.isArray(tags) ||
|
|
70
|
+
tags.some((tag) => typeof tag !== "string" || tag === "")
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"clientUrls() path() ppr.tags must be an array of non-empty strings",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(
|
|
79
|
+
`clientUrls() path() ppr received unsupported key ${JSON.stringify(String(key))}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const UNSUPPORTED_HELPERS = new Set([
|
|
85
|
+
"include",
|
|
86
|
+
"parallel",
|
|
87
|
+
"cache",
|
|
88
|
+
"error",
|
|
89
|
+
"notFound",
|
|
90
|
+
"errorBoundary",
|
|
91
|
+
"notFoundBoundary",
|
|
92
|
+
"middleware",
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
type InternalItem =
|
|
96
|
+
| PathItem
|
|
97
|
+
| LayoutItem
|
|
98
|
+
| LoaderItem
|
|
99
|
+
| LoadingItem
|
|
100
|
+
| InterceptItem
|
|
101
|
+
| TransitionItem
|
|
102
|
+
| RevalidateItem;
|
|
103
|
+
|
|
104
|
+
interface ItemBase {
|
|
105
|
+
readonly [ITEM_KIND]: true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface PathItem extends ItemBase {
|
|
109
|
+
readonly type: "path";
|
|
110
|
+
readonly pattern: string;
|
|
111
|
+
readonly component: ComponentType;
|
|
112
|
+
readonly options: ClientPathOptions | undefined;
|
|
113
|
+
readonly use: readonly InternalItem[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface LayoutItem extends ItemBase {
|
|
117
|
+
readonly type: "layout";
|
|
118
|
+
readonly component: ComponentType;
|
|
119
|
+
readonly children: readonly InternalItem[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface InterceptItem extends ItemBase {
|
|
123
|
+
readonly type: "intercept";
|
|
124
|
+
readonly slotName: `@${string}`;
|
|
125
|
+
/** Bare target name — the leading dot of the dot-local argument is stripped. */
|
|
126
|
+
readonly targetName: string;
|
|
127
|
+
readonly component: ComponentType;
|
|
128
|
+
readonly use: readonly InternalItem[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface TransitionItem extends ItemBase {
|
|
132
|
+
readonly type: "transition";
|
|
133
|
+
readonly config: ClientTransitionConfig;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface LoaderItem extends ItemBase {
|
|
137
|
+
readonly type: "loader";
|
|
138
|
+
readonly definition: LoaderDefinition<any, any>;
|
|
139
|
+
readonly revalidate: readonly ClientRevalidateFn[];
|
|
140
|
+
readonly stream?: "navigation";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface RevalidateItem extends ItemBase {
|
|
144
|
+
readonly type: "revalidate";
|
|
145
|
+
readonly fn: ClientRevalidateFn;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface LoadingItem extends ItemBase {
|
|
149
|
+
readonly type: "loading";
|
|
150
|
+
readonly component: ReactNode;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface FlattenContext {
|
|
154
|
+
readonly layouts: readonly ComponentType[];
|
|
155
|
+
readonly loaders: readonly ClientUrlLoaderRecord[];
|
|
156
|
+
readonly loading: ReactNode | undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const EMPTY_CONTEXT: FlattenContext = {
|
|
160
|
+
layouts: [],
|
|
161
|
+
loaders: [],
|
|
162
|
+
loading: undefined,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
function toPublicItem(item: InternalItem): ClientUrlItem {
|
|
166
|
+
return item as unknown as ClientUrlItem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isInternalItem(value: unknown): value is InternalItem {
|
|
170
|
+
return (
|
|
171
|
+
typeof value === "object" &&
|
|
172
|
+
value !== null &&
|
|
173
|
+
ITEM_KIND in value &&
|
|
174
|
+
(value as ItemBase)[ITEM_KIND] === true
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function flattenItems(value: unknown, label: string): InternalItem[] {
|
|
179
|
+
if (!Array.isArray(value)) {
|
|
180
|
+
throw new Error(`clientUrls() ${label} callback must return an array`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const flattened: InternalItem[] = [];
|
|
184
|
+
const visit = (input: unknown): void => {
|
|
185
|
+
if (Array.isArray(input)) {
|
|
186
|
+
for (const child of input) visit(child);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!isInternalItem(input)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`clientUrls() ${label} callback must return only client URL helper items`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
flattened.push(input);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
visit(value);
|
|
198
|
+
return flattened;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function runUse(use: ClientUrlUse, label: string): InternalItem[] {
|
|
202
|
+
if (typeof use !== "function") {
|
|
203
|
+
throw new Error(`clientUrls() ${label} expects a callback`);
|
|
204
|
+
}
|
|
205
|
+
return flattenItems(use(), label);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function rejectItems(
|
|
209
|
+
items: readonly InternalItem[],
|
|
210
|
+
allowed: ReadonlySet<InternalItem["type"]>,
|
|
211
|
+
label: string,
|
|
212
|
+
): void {
|
|
213
|
+
for (const item of items) {
|
|
214
|
+
if (!allowed.has(item.type)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`clientUrls() does not support ${item.type}() inside ${label}()`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function assertNamedComponent(
|
|
223
|
+
component: unknown,
|
|
224
|
+
helper: "path" | "layout" | "intercept",
|
|
225
|
+
): asserts component is ComponentType {
|
|
226
|
+
// Build-time handler wrappers get a TARGETED rejection ahead of the generic
|
|
227
|
+
// shape check: their handler code cannot live in a "use client" bundle and
|
|
228
|
+
// their render runs at build, so accepting one could only fail later and
|
|
229
|
+
// further from the mistake. Brand strings checked directly — importing
|
|
230
|
+
// static-handler.ts/prerender.ts here would pull server modules into the
|
|
231
|
+
// client graph for an error message.
|
|
232
|
+
const brand =
|
|
233
|
+
component !== null &&
|
|
234
|
+
(typeof component === "object" || typeof component === "function")
|
|
235
|
+
? (component as { __brand?: unknown }).__brand
|
|
236
|
+
: undefined;
|
|
237
|
+
if (
|
|
238
|
+
brand === "staticHandler" ||
|
|
239
|
+
brand === "prerenderHandler" ||
|
|
240
|
+
brand === "prerenderPassthrough"
|
|
241
|
+
) {
|
|
242
|
+
const wrapper = brand === "staticHandler" ? "Static()" : "Prerender()";
|
|
243
|
+
throw new Error(
|
|
244
|
+
`clientUrls() ${helper}() received a ${wrapper} handler. Build-time ` +
|
|
245
|
+
"handlers are server-DSL surface and cannot mount inside a client " +
|
|
246
|
+
"group — routes in a group render client components. For shell " +
|
|
247
|
+
"caching of a group route use the `ppr` path option; build-time " +
|
|
248
|
+
"prerendering belongs to the server tree around the include.",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (typeof component !== "function" || component.name.trim() === "") {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`clientUrls() ${helper}() expects a named client component value`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function assertLoaderDefinition(
|
|
259
|
+
definition: unknown,
|
|
260
|
+
): asserts definition is LoaderDefinition<any, any> {
|
|
261
|
+
const candidate = definition as Partial<LoaderDefinition<any, any>> | null;
|
|
262
|
+
if (
|
|
263
|
+
typeof candidate !== "object" ||
|
|
264
|
+
candidate === null ||
|
|
265
|
+
candidate.__brand !== "loader" ||
|
|
266
|
+
typeof candidate.$$id !== "string"
|
|
267
|
+
) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
"clientUrls() loader() expects a LoaderDefinition created by createLoader()",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createHelpers(): ClientUrlHelpers {
|
|
275
|
+
const path = ((
|
|
276
|
+
pattern: string,
|
|
277
|
+
component: ComponentType,
|
|
278
|
+
optionsOrUse?: ClientPathOptions | ClientUrlUse,
|
|
279
|
+
maybeUse?: ClientUrlUse,
|
|
280
|
+
): ClientUrlItem => {
|
|
281
|
+
if (typeof pattern !== "string" || !pattern.startsWith("/")) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
'clientUrls() path() pattern must be a string beginning with "/"',
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
assertNamedComponent(component, "path");
|
|
287
|
+
|
|
288
|
+
let options: ClientPathOptions | undefined;
|
|
289
|
+
let use: ClientUrlUse | undefined;
|
|
290
|
+
if (typeof optionsOrUse === "function") {
|
|
291
|
+
if (maybeUse !== undefined) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"clientUrls() path() received more than one use callback",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
use = optionsOrUse;
|
|
297
|
+
} else if (optionsOrUse !== undefined) {
|
|
298
|
+
if (typeof optionsOrUse !== "object" || optionsOrUse === null) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"clientUrls() path() third argument must be PathOptions or a use callback",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
options = { ...optionsOrUse };
|
|
304
|
+
use = maybeUse;
|
|
305
|
+
} else if (maybeUse !== undefined) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
"clientUrls() path() cannot receive a fourth argument without PathOptions",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (options?.name !== undefined) {
|
|
312
|
+
if (typeof options.name !== "string" || options.name === "") {
|
|
313
|
+
throw new Error("clientUrls() path() name must be a non-empty string");
|
|
314
|
+
}
|
|
315
|
+
validateUserRouteName(options.name);
|
|
316
|
+
}
|
|
317
|
+
if (
|
|
318
|
+
options?.trailingSlash !== undefined &&
|
|
319
|
+
options.trailingSlash !== "always" &&
|
|
320
|
+
options.trailingSlash !== "never" &&
|
|
321
|
+
options.trailingSlash !== "ignore"
|
|
322
|
+
) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
'clientUrls() path() trailingSlash must be "always", "never", or "ignore"',
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (options?.ppr !== undefined) {
|
|
328
|
+
validateClientPpr(options.ppr);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const items = use ? runUse(use, "path use") : [];
|
|
332
|
+
rejectItems(items, new Set(["loader", "loading", "transition"]), "path");
|
|
333
|
+
if (items.filter((item) => item.type === "transition").length > 1) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
"clientUrls() path() received more than one transition()",
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return toPublicItem({
|
|
339
|
+
[ITEM_KIND]: true,
|
|
340
|
+
type: "path",
|
|
341
|
+
pattern,
|
|
342
|
+
component,
|
|
343
|
+
options,
|
|
344
|
+
use: items,
|
|
345
|
+
});
|
|
346
|
+
}) as ClientUrlHelpers["path"];
|
|
347
|
+
|
|
348
|
+
const layout = ((
|
|
349
|
+
component: ComponentType,
|
|
350
|
+
children: ClientUrlUse,
|
|
351
|
+
): ClientUrlItem => {
|
|
352
|
+
assertNamedComponent(component, "layout");
|
|
353
|
+
const items = runUse(children, "layout children");
|
|
354
|
+
rejectItems(
|
|
355
|
+
items,
|
|
356
|
+
new Set(["path", "layout", "loader", "loading", "intercept"]),
|
|
357
|
+
"layout",
|
|
358
|
+
);
|
|
359
|
+
return toPublicItem({
|
|
360
|
+
[ITEM_KIND]: true,
|
|
361
|
+
type: "layout",
|
|
362
|
+
component,
|
|
363
|
+
children: items,
|
|
364
|
+
});
|
|
365
|
+
}) as ClientUrlHelpers["layout"];
|
|
366
|
+
|
|
367
|
+
const loader: ClientUrlHelpers["loader"] = (
|
|
368
|
+
definition,
|
|
369
|
+
optionsOrUse,
|
|
370
|
+
maybeUse,
|
|
371
|
+
) => {
|
|
372
|
+
assertLoaderDefinition(definition);
|
|
373
|
+
// Same options-or-use disambiguation as the server DSL loader(): the
|
|
374
|
+
// 2-arg loader(Def, use) form is untouched.
|
|
375
|
+
if (typeof optionsOrUse === "function" && maybeUse !== undefined) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"clientUrls() loader() received two use() callbacks. Pass loader(Def, options, use) or loader(Def, use).",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const options =
|
|
381
|
+
typeof optionsOrUse === "function" ? undefined : optionsOrUse;
|
|
382
|
+
const use = typeof optionsOrUse === "function" ? optionsOrUse : maybeUse;
|
|
383
|
+
if (options?.stream !== undefined && options.stream !== "navigation") {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`clientUrls() loader() stream must be "navigation" (got ${JSON.stringify(options.stream)}). Omit it to stream on every render.`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const items = use ? runUse(use, "loader use") : [];
|
|
389
|
+
rejectItems(items, new Set(["revalidate"]), "loader");
|
|
390
|
+
return toPublicItem({
|
|
391
|
+
[ITEM_KIND]: true,
|
|
392
|
+
type: "loader",
|
|
393
|
+
definition,
|
|
394
|
+
revalidate: items
|
|
395
|
+
.filter(
|
|
396
|
+
(item): item is Extract<InternalItem, { type: "revalidate" }> =>
|
|
397
|
+
item.type === "revalidate",
|
|
398
|
+
)
|
|
399
|
+
.map((item) => item.fn),
|
|
400
|
+
...(options?.stream === "navigation"
|
|
401
|
+
? { stream: "navigation" as const }
|
|
402
|
+
: {}),
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const revalidate: ClientUrlHelpers["revalidate"] = (fn) => {
|
|
407
|
+
if (typeof fn !== "function") {
|
|
408
|
+
throw new Error("clientUrls() revalidate() expects a predicate function");
|
|
409
|
+
}
|
|
410
|
+
return toPublicItem({
|
|
411
|
+
[ITEM_KIND]: true,
|
|
412
|
+
type: "revalidate",
|
|
413
|
+
fn,
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const loading: ClientUrlHelpers["loading"] = (component) =>
|
|
418
|
+
toPublicItem({ [ITEM_KIND]: true, type: "loading", component });
|
|
419
|
+
|
|
420
|
+
const transition: ClientUrlHelpers["transition"] = (config) => {
|
|
421
|
+
if (
|
|
422
|
+
typeof config !== "object" ||
|
|
423
|
+
config === null ||
|
|
424
|
+
Array.isArray(config)
|
|
425
|
+
) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
"clientUrls() transition() expects a configuration object",
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
for (const key of Reflect.ownKeys(config)) {
|
|
431
|
+
if (key === "when") {
|
|
432
|
+
throw new Error(
|
|
433
|
+
"clientUrls() transition() does not support `when` — the gate is a " +
|
|
434
|
+
"server-executed predicate; declare it with a server-tree transition()",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (!TRANSITION_CONFIG_KEYS.has(key)) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`clientUrls() transition() option ${JSON.stringify(String(key))} is not supported`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (config.name !== undefined && typeof config.name !== "string") {
|
|
444
|
+
throw new Error("clientUrls() transition() name must be a string");
|
|
445
|
+
}
|
|
446
|
+
if (
|
|
447
|
+
config.viewTransition !== undefined &&
|
|
448
|
+
config.viewTransition !== "auto" &&
|
|
449
|
+
config.viewTransition !== false
|
|
450
|
+
) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
'clientUrls() transition() viewTransition must be "auto" or false',
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
for (const key of [
|
|
456
|
+
"enter",
|
|
457
|
+
"exit",
|
|
458
|
+
"update",
|
|
459
|
+
"share",
|
|
460
|
+
"default",
|
|
461
|
+
] as const) {
|
|
462
|
+
const value = config[key];
|
|
463
|
+
if (value === undefined || typeof value === "string") continue;
|
|
464
|
+
const isStringMap =
|
|
465
|
+
typeof value === "object" &&
|
|
466
|
+
value !== null &&
|
|
467
|
+
!Array.isArray(value) &&
|
|
468
|
+
Object.values(value).every((entry) => typeof entry === "string");
|
|
469
|
+
if (!isStringMap) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`clientUrls() transition() ${key} must be a string or a string map of transition types`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return toPublicItem({
|
|
476
|
+
[ITEM_KIND]: true,
|
|
477
|
+
type: "transition",
|
|
478
|
+
config: { ...config },
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const intercept: ClientUrlHelpers["intercept"] = (
|
|
483
|
+
slotName,
|
|
484
|
+
targetName,
|
|
485
|
+
component,
|
|
486
|
+
use,
|
|
487
|
+
) => {
|
|
488
|
+
if (typeof slotName !== "string" || !/^@.+/.test(slotName)) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
'clientUrls() intercept() slot name must be a string beginning with "@"',
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
if (
|
|
494
|
+
typeof targetName !== "string" ||
|
|
495
|
+
!targetName.startsWith(".") ||
|
|
496
|
+
targetName.length < 2
|
|
497
|
+
) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
'clientUrls() intercept() target must be a dot-local route name like ".detail"',
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
assertNamedComponent(component, "intercept");
|
|
503
|
+
const items = use ? runUse(use, "intercept use") : [];
|
|
504
|
+
rejectItems(items, new Set(["loader", "loading"]), "intercept");
|
|
505
|
+
for (const item of items) {
|
|
506
|
+
// Intercept loaders run on soft navigations only — a document-render
|
|
507
|
+
// await can never apply, so accepting the flag would be silently inert.
|
|
508
|
+
if (item.type === "loader" && item.stream !== undefined) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
'clientUrls() intercept() loaders cannot use stream: "navigation" — intercepts render on client navigations only',
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return toPublicItem({
|
|
515
|
+
[ITEM_KIND]: true,
|
|
516
|
+
type: "intercept",
|
|
517
|
+
slotName,
|
|
518
|
+
targetName: targetName.slice(1),
|
|
519
|
+
component,
|
|
520
|
+
use: items,
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const supported: ClientUrlHelpers = {
|
|
525
|
+
path,
|
|
526
|
+
layout,
|
|
527
|
+
loader,
|
|
528
|
+
loading,
|
|
529
|
+
intercept,
|
|
530
|
+
transition,
|
|
531
|
+
revalidate,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
return new Proxy(supported, {
|
|
535
|
+
get(target, property, receiver): unknown {
|
|
536
|
+
if (typeof property === "string" && UNSUPPORTED_HELPERS.has(property)) {
|
|
537
|
+
throw new Error(`clientUrls() does not support ${property}()`);
|
|
538
|
+
}
|
|
539
|
+
if (typeof property === "string" && !(property in target)) {
|
|
540
|
+
throw new Error(`clientUrls() has no ${property}() helper`);
|
|
541
|
+
}
|
|
542
|
+
return Reflect.get(target, property, receiver);
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function applyConfig(
|
|
548
|
+
context: FlattenContext,
|
|
549
|
+
items: readonly InternalItem[],
|
|
550
|
+
): FlattenContext {
|
|
551
|
+
const loaders = [...context.loaders];
|
|
552
|
+
let loading = context.loading;
|
|
553
|
+
|
|
554
|
+
for (const item of items) {
|
|
555
|
+
if (item.type === "loader") {
|
|
556
|
+
loaders.push(
|
|
557
|
+
Object.freeze({
|
|
558
|
+
loader: item.definition,
|
|
559
|
+
revalidate: Object.freeze([...item.revalidate]),
|
|
560
|
+
...(item.stream ? { stream: item.stream } : {}),
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
} else if (item.type === "loading") {
|
|
564
|
+
loading = item.component;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
layouts: context.layouts,
|
|
570
|
+
loaders,
|
|
571
|
+
loading,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function addPathRecord(
|
|
576
|
+
item: PathItem,
|
|
577
|
+
context: FlattenContext,
|
|
578
|
+
routes: ClientUrlRouteRecord[],
|
|
579
|
+
): void {
|
|
580
|
+
const routeContext = applyConfig(context, item.use);
|
|
581
|
+
const options = item.options ? Object.freeze({ ...item.options }) : undefined;
|
|
582
|
+
// Route-scoped only: transition() is rejected outside path() use callbacks,
|
|
583
|
+
// so there is no layout-level inheritance to fold in.
|
|
584
|
+
const transitionItem = item.use.find(
|
|
585
|
+
(useItem): useItem is Extract<InternalItem, { type: "transition" }> =>
|
|
586
|
+
useItem.type === "transition",
|
|
587
|
+
);
|
|
588
|
+
routes.push(
|
|
589
|
+
Object.freeze({
|
|
590
|
+
id: `client-route-${routes.length}`,
|
|
591
|
+
pattern: item.pattern,
|
|
592
|
+
name: options?.name,
|
|
593
|
+
options,
|
|
594
|
+
component: item.component,
|
|
595
|
+
layouts: Object.freeze([...routeContext.layouts]),
|
|
596
|
+
loaders: Object.freeze([...routeContext.loaders]),
|
|
597
|
+
loading: routeContext.loading,
|
|
598
|
+
transition: transitionItem
|
|
599
|
+
? Object.freeze({ ...transitionItem.config })
|
|
600
|
+
: undefined,
|
|
601
|
+
}),
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function flattenLayout(
|
|
606
|
+
item: LayoutItem,
|
|
607
|
+
context: FlattenContext,
|
|
608
|
+
routes: ClientUrlRouteRecord[],
|
|
609
|
+
): void {
|
|
610
|
+
const routeCount = routes.length;
|
|
611
|
+
const layoutContext = applyConfig(
|
|
612
|
+
{
|
|
613
|
+
...context,
|
|
614
|
+
layouts: [...context.layouts, item.component],
|
|
615
|
+
},
|
|
616
|
+
item.children,
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
flattenRoutes(item.children, layoutContext, routes);
|
|
620
|
+
if (routes.length === routeCount) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
"clientUrls() layout() children must contain at least one path()",
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function flattenRoutes(
|
|
628
|
+
items: readonly InternalItem[],
|
|
629
|
+
context: FlattenContext,
|
|
630
|
+
routes: ClientUrlRouteRecord[],
|
|
631
|
+
): void {
|
|
632
|
+
for (const item of items) {
|
|
633
|
+
if (item.type === "path") {
|
|
634
|
+
addPathRecord(item, context, routes);
|
|
635
|
+
} else if (item.type === "layout") {
|
|
636
|
+
flattenLayout(item, context, routes);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function validateRoutes(routes: readonly ClientUrlRouteRecord[]): void {
|
|
642
|
+
const patterns = new Set<string>();
|
|
643
|
+
const names = new Set<string>();
|
|
644
|
+
for (const route of routes) {
|
|
645
|
+
if (patterns.has(route.pattern)) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`clientUrls() cannot define duplicate path pattern "${route.pattern}"`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
patterns.add(route.pattern);
|
|
651
|
+
|
|
652
|
+
if (route.name !== undefined) {
|
|
653
|
+
if (names.has(route.name)) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`clientUrls() cannot define duplicate route name "${route.name}"`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
names.add(route.name);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function collectIntercepts(
|
|
664
|
+
items: readonly InternalItem[],
|
|
665
|
+
intercepts: ClientUrlInterceptRecord[],
|
|
666
|
+
): void {
|
|
667
|
+
for (const item of items) {
|
|
668
|
+
if (item.type === "intercept") {
|
|
669
|
+
const loaders: ClientUrlLoaderRecord[] = [];
|
|
670
|
+
let loading: ReactNode | undefined;
|
|
671
|
+
for (const useItem of item.use) {
|
|
672
|
+
if (useItem.type === "loader") {
|
|
673
|
+
loaders.push(
|
|
674
|
+
Object.freeze({
|
|
675
|
+
loader: useItem.definition,
|
|
676
|
+
revalidate: Object.freeze([...useItem.revalidate]),
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
679
|
+
} else if (useItem.type === "loading") {
|
|
680
|
+
loading = useItem.component;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
intercepts.push(
|
|
684
|
+
Object.freeze({
|
|
685
|
+
slotName: item.slotName,
|
|
686
|
+
targetName: item.targetName,
|
|
687
|
+
component: item.component,
|
|
688
|
+
loaders: Object.freeze(loaders),
|
|
689
|
+
loading,
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
} else if (item.type === "layout") {
|
|
693
|
+
collectIntercepts(item.children, intercepts);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function validateIntercepts(
|
|
699
|
+
intercepts: readonly ClientUrlInterceptRecord[],
|
|
700
|
+
routes: readonly ClientUrlRouteRecord[],
|
|
701
|
+
): void {
|
|
702
|
+
const names = new Set<string>();
|
|
703
|
+
for (const route of routes) {
|
|
704
|
+
if (route.name !== undefined) names.add(route.name);
|
|
705
|
+
}
|
|
706
|
+
for (const intercept of intercepts) {
|
|
707
|
+
if (!names.has(intercept.targetName)) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`clientUrls() intercept() target ".${intercept.targetName}" does not match a named path() in this definition`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function clientUrls<const TItems extends ClientUrlItems>(
|
|
716
|
+
builder: ClientUrlBuilder<TItems>,
|
|
717
|
+
): ClientUrlPatterns<ExtractRoutes<TItems>> {
|
|
718
|
+
if (typeof builder !== "function") {
|
|
719
|
+
throw new Error("clientUrls() expects a builder function");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const items = flattenItems(builder(createHelpers()), "builder");
|
|
723
|
+
rejectItems(items, new Set(["path", "layout", "intercept"]), "clientUrls");
|
|
724
|
+
|
|
725
|
+
const routes: ClientUrlRouteRecord[] = [];
|
|
726
|
+
const rootContext = applyConfig(EMPTY_CONTEXT, items);
|
|
727
|
+
flattenRoutes(items, rootContext, routes);
|
|
728
|
+
if (routes.length === 0) {
|
|
729
|
+
throw new Error("clientUrls() builder must define at least one path()");
|
|
730
|
+
}
|
|
731
|
+
validateRoutes(routes);
|
|
732
|
+
|
|
733
|
+
const intercepts: ClientUrlInterceptRecord[] = [];
|
|
734
|
+
collectIntercepts(items, intercepts);
|
|
735
|
+
validateIntercepts(intercepts, routes);
|
|
736
|
+
|
|
737
|
+
const routeManifest: Record<string, string> = {};
|
|
738
|
+
const staticPrefixes: Record<string, string> = {};
|
|
739
|
+
const trailingSlash: Record<string, string> = {};
|
|
740
|
+
for (const route of routes) {
|
|
741
|
+
routeManifest[route.id] = route.pattern;
|
|
742
|
+
staticPrefixes[route.id] = "";
|
|
743
|
+
if (route.options?.trailingSlash) {
|
|
744
|
+
trailingSlash[route.id] = route.options.trailingSlash;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const trie = buildRouteTrie(routeManifest, staticPrefixes, trailingSlash);
|
|
748
|
+
|
|
749
|
+
return Object.freeze({
|
|
750
|
+
__brand: "client-urls" as const,
|
|
751
|
+
routes: Object.freeze(routes),
|
|
752
|
+
intercepts: Object.freeze(intercepts),
|
|
753
|
+
match(pathname: string) {
|
|
754
|
+
return tryTrieMatch(trie, pathname);
|
|
755
|
+
},
|
|
756
|
+
}) as ClientUrlPatterns<ExtractRoutes<TItems>>;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export type {
|
|
760
|
+
ClientRevalidateArgs,
|
|
761
|
+
ClientRevalidateFn,
|
|
762
|
+
ClientTransitionConfig,
|
|
763
|
+
ClientUrlBuilder,
|
|
764
|
+
ClientUrlHelpers,
|
|
765
|
+
ClientUrlInterceptRecord,
|
|
766
|
+
ClientUrlItem,
|
|
767
|
+
ClientUrlItemInput,
|
|
768
|
+
ClientUrlItems,
|
|
769
|
+
ClientLayoutFn,
|
|
770
|
+
ClientPathOptions,
|
|
771
|
+
ClientUrlLoaderRecord,
|
|
772
|
+
ClientPathFn,
|
|
773
|
+
ClientUrlPatterns,
|
|
774
|
+
ClientUrlRouteRecord,
|
|
775
|
+
ClientUrlUse,
|
|
776
|
+
} from "./types.js";
|