@askrjs/askr 0.3.0 → 0.4.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/CHANGELOG.md +874 -0
- package/README.md +61 -29
- package/capabilities.json +19 -9
- package/dist/actions/index.js +56 -12
- package/dist/benchmark.js +10 -15
- package/dist/boot/index.d.ts +3 -2
- package/dist/boot/index.js +1 -1
- package/dist/boot-DaoTYoCw.js +3989 -0
- package/dist/browser-activity-CB8e-V4f.js +220 -0
- package/dist/components/index.d.ts +8 -1
- package/dist/components/index.js +1 -43
- package/dist/{aria-hKjThHcp.js → compose-ref-DWzdbxsD.js} +41 -4
- package/dist/control/index.d.ts +2 -0
- package/dist/control/index.js +1 -1
- package/dist/control-nfpT2fbK.js +209 -0
- package/dist/core.d.ts +5 -9
- package/dist/csp-nonce-DwGw1A6Q.js +35 -0
- package/dist/data/index.js +3 -3
- package/dist/{data-2edW0jhF.js → data-BCwClJiF.js} +283 -94
- package/dist/data-runtime-C1bFAlba.js +242 -0
- package/dist/{deferred-CbgNhf67.js → deferred-bEEHHvic.js} +42 -20
- package/dist/domains/actions.d.ts +8 -3
- package/dist/domains/component.d.ts +1 -107
- package/dist/domains/context.d.ts +0 -15
- package/dist/domains/control.d.ts +36 -249
- package/dist/domains/data.d.ts +31 -13
- package/dist/domains/diagnostics.d.ts +1 -2
- package/dist/domains/lifecycle.d.ts +2 -5
- package/dist/domains/refs.d.ts +0 -2
- package/dist/domains/routing.d.ts +43 -8
- package/dist/domains/scheduler.d.ts +1 -76
- package/dist/domains/server.d.ts +5 -2
- package/dist/domains/state.d.ts +2 -5
- package/dist/domains/telemetry.d.ts +0 -2
- package/dist/elements.d.ts +214 -51
- package/dist/error-boundary-Bpwofwps.js +293 -0
- package/dist/foundations/icon/index.d.ts +0 -58
- package/dist/foundations/icon/index.js +2 -2
- package/dist/foundations/index.js +2 -1
- package/dist/foundations/interactions/index.js +1 -1
- package/dist/foundations/state/index.d.ts +3 -3
- package/dist/foundations/state/index.js +5 -3
- package/dist/foundations/structures/index.js +2 -1
- package/dist/foundations/utilities/index.d.ts +2 -0
- package/dist/foundations/utilities/index.js +1 -2
- package/dist/fx/index.d.ts +31 -58
- package/dist/fx/index.js +313 -195
- package/dist/hooks-BimKStr4.js +202 -0
- package/dist/index.d.ts +11 -74
- package/dist/index.js +8 -447
- package/dist/instance-BAnfmXOR.js +988 -0
- package/dist/{interactions-CTeACdT_.js → interactions-D2BoREb-.js} +3 -2
- package/dist/jsx-dev-runtime.d.ts +41 -18
- package/dist/jsx-dev-runtime.js +1 -6
- package/dist/{jsx-runtime-CPJXAOBF.js → jsx-runtime-C4idqHY0.js} +18 -7
- package/dist/jsx-runtime.js +1 -1
- package/dist/jsx.d.ts +61 -37
- package/dist/{manifest-D1P3UNWV.js → manifest-CW9gyNBY.js} +3 -3
- package/dist/{navigate-prC5V8XV.js → navigate-CP_0Zy5U.js} +454 -87
- package/dist/navigation.d.ts +12 -3
- package/dist/peer-types.d.ts +8 -0
- package/dist/portal-DuOKfnLa.js +349 -0
- package/dist/promise-Dz8iIHAm.js +16 -0
- package/dist/query-registry-WMCN0BJL.js +320 -0
- package/dist/readable-DFWOsTac.js +21 -0
- package/dist/{render-context-B7ohlh6t.js → render-context-DGHnEveC.js} +27 -11
- package/dist/render-sync-IC5lMFpq.js +1240 -0
- package/dist/resolution-Cx82-xjW.js +1244 -0
- package/dist/resource-CLjB7RBQ.js +377 -0
- package/dist/resources/index.d.ts +21 -11
- package/dist/resources/index.js +45 -244
- package/dist/router/index.d.ts +14 -5
- package/dist/router/index.js +22 -53
- package/dist/scope-CwpVSHVf.js +55 -0
- package/dist/{shared-kU8yvryp.js → shared-O2ZnmYAf.js} +1 -7
- package/dist/{snapshot-source-An4iKajG.js → snapshot-Cjx4N988.js} +6 -1
- package/dist/ssg/index.d.ts +1 -2
- package/dist/ssg/index.js +5 -13
- package/dist/ssr/index.d.ts +26 -28
- package/dist/ssr/index.js +4 -4
- package/dist/{ssr-CiNm6Vzl.js → ssr-BJ8ug7zH.js} +133 -122
- package/dist/ssr-DoV-vJ-5.js +229 -0
- package/dist/state-DWO1KzkX.js +232 -0
- package/dist/structures-D4dDMqHX.js +231 -0
- package/dist/testing/index.d.ts +8 -4
- package/dist/testing/index.js +29 -34
- package/dist/url-CnbfpX0K.js +185 -0
- package/dist/utilities.d.ts +48 -2
- package/dist/verify-hydration-DpVaDnqz.js +152 -0
- package/package.json +19 -9
- package/dist/access-CEA2pOry.js +0 -1046
- package/dist/activity-oy2YFTFF.js +0 -201
- package/dist/boot-BMBMWUGI.js +0 -1084
- package/dist/capabilities-Bbf0mlp9.js +0 -42
- package/dist/compose-ref-v0qGlHoN.js +0 -26
- package/dist/control-BR_mrX__.js +0 -173
- package/dist/control-C-ApnFs1.js +0 -20
- package/dist/csp-nonce-BfFWcd0w.js +0 -173
- package/dist/data-runtime-CxoY98lX.js +0 -149
- package/dist/default-portal-runtime-DvMvWJVp.js +0 -20
- package/dist/dom-renderer.d.ts +0 -77
- package/dist/domains/renderer.d.ts +0 -127
- package/dist/eager-control.d.ts +0 -8
- package/dist/effect-C3Zsc1k6.js +0 -292
- package/dist/for-state-BDykeOJL.js +0 -1753
- package/dist/instance-VAFYVJOa.js +0 -1027
- package/dist/notify-DxxuDPeY.js +0 -266
- package/dist/portal-w3oQ6oQU.js +0 -29
- package/dist/query-registry-LrYpQsDJ.js +0 -83
- package/dist/render-resolved-BC0W7_5b.js +0 -1123
- package/dist/renderer-CT0HeY58.js +0 -8623
- package/dist/resolution-3DskUkjG.js +0 -577
- package/dist/resource-18-sFRl9.js +0 -344
- package/dist/route-matching-CUzZ31gT.js +0 -551
- package/dist/router-internal-7nuNEHfG.js +0 -36
- package/dist/scope-Cq_iYbTR.js +0 -403
- package/dist/selector-BSPVkjKP.js +0 -311
- package/dist/ssr-EPjwJH5R.js +0 -29
- package/dist/state-BXJfudq1.js +0 -91
- package/dist/structures-CjEzfj4h.js +0 -654
- package/dist/timer-C7uqnJVt.js +0 -85
- package/dist/verify-hydration-ikaxotf7.js +0 -27
- package/dist/vnode-D24p6IW2.js +0 -7
|
@@ -0,0 +1,1244 @@
|
|
|
1
|
+
import { n as Fragment, t as ELEMENT_TYPE } from "./jsx-DRS-qjiN.js";
|
|
2
|
+
import { r as currentAppRuntime } from "./hooks-BimKStr4.js";
|
|
3
|
+
import { r as readScope, t as defineScope } from "./scope-CwpVSHVf.js";
|
|
4
|
+
import { S as replacePageRoute, n as getActiveRenderContext, w as withPageFramework } from "./render-context-DGHnEveC.js";
|
|
5
|
+
import { n as isPromiseLike } from "./promise-Dz8iIHAm.js";
|
|
6
|
+
import { c as withTelemetry, l as jsonTransportPropertyPath, p as isDeferred, t as createQueryPrefetchContext, u as validateJsonTransportValue } from "./query-registry-WMCN0BJL.js";
|
|
7
|
+
import { c as resolveNavigationUrl, i as formatNavigationUrl } from "./url-CnbfpX0K.js";
|
|
8
|
+
//#region src/router/route-hydration.ts
|
|
9
|
+
function isDeferredHydrationValue(value) {
|
|
10
|
+
return isDeferred(value);
|
|
11
|
+
}
|
|
12
|
+
const omissionGuards = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
const propertyPath = jsonTransportPropertyPath;
|
|
14
|
+
/** Validate the exact value that will enter the route hydration envelope. */
|
|
15
|
+
function validateRouteHydrationData(value, route) {
|
|
16
|
+
validateJsonTransportValue(value, `Route hydration data for "${route}"`, "Return JSON-compatible data or use route(..., { dehydrate(data) { ... } }) to omit server-only values.");
|
|
17
|
+
}
|
|
18
|
+
function omissionMap(complete, selected, ancestors = /* @__PURE__ */ new Set()) {
|
|
19
|
+
if (!complete || typeof complete !== "object" || isDeferredHydrationValue(complete)) return void 0;
|
|
20
|
+
if (ancestors.has(complete)) return void 0;
|
|
21
|
+
ancestors.add(complete);
|
|
22
|
+
const selectedObject = selected && typeof selected === "object" ? selected : void 0;
|
|
23
|
+
const output = Object.create(null);
|
|
24
|
+
try {
|
|
25
|
+
for (const key of Object.keys(complete)) {
|
|
26
|
+
if (!selectedObject || !Object.prototype.hasOwnProperty.call(selectedObject, key)) {
|
|
27
|
+
output[key] = 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const completeDescriptor = Object.getOwnPropertyDescriptor(complete, key);
|
|
31
|
+
const selectedDescriptor = Object.getOwnPropertyDescriptor(selectedObject, key);
|
|
32
|
+
if (!completeDescriptor || !selectedDescriptor) continue;
|
|
33
|
+
if (!("value" in completeDescriptor) || !("value" in selectedDescriptor)) continue;
|
|
34
|
+
const nested = omissionMap(completeDescriptor.value, selectedDescriptor.value, ancestors);
|
|
35
|
+
if (nested) output[key] = nested;
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
ancestors.delete(complete);
|
|
39
|
+
}
|
|
40
|
+
return Object.keys(output).length > 0 ? Object.freeze(output) : void 0;
|
|
41
|
+
}
|
|
42
|
+
function prepareRouteHydrationData(complete, dehydrate, context, route = context.href || context.pathname) {
|
|
43
|
+
const selected = dehydrate ? dehydrate(complete, context) : complete;
|
|
44
|
+
if (isPromiseLike(selected)) throw new TypeError(`[Askr] Route dehydrate() for "${route}" returned a Promise. dehydrate(data, context) must be synchronous.`);
|
|
45
|
+
validateRouteHydrationData(selected, route);
|
|
46
|
+
return {
|
|
47
|
+
data: selected,
|
|
48
|
+
metadata: Object.freeze({
|
|
49
|
+
r: route,
|
|
50
|
+
...dehydrate ? { o: omissionMap(complete, selected) } : {}
|
|
51
|
+
})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function readMetadata(framework) {
|
|
55
|
+
const value = framework["rh"];
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
57
|
+
const metadata = value;
|
|
58
|
+
return typeof metadata.r === "string" ? metadata : void 0;
|
|
59
|
+
}
|
|
60
|
+
function omittedRead(route, path) {
|
|
61
|
+
throw new Error(`[Askr] routeData() read "${path}" on the initial hydration of "${route}", but that branch was intentionally omitted by dehydrate(). Keep initial hydrated rendering within the selected data, or wait for a client navigation that reruns the loader with the complete result.`);
|
|
62
|
+
}
|
|
63
|
+
function guardOmissions(value, omissions, route, path = "$") {
|
|
64
|
+
if (!omissions || !value || typeof value !== "object" || isDeferredHydrationValue(value)) return value;
|
|
65
|
+
let guards = omissionGuards.get(value);
|
|
66
|
+
if (!guards) {
|
|
67
|
+
guards = /* @__PURE__ */ new WeakMap();
|
|
68
|
+
omissionGuards.set(value, guards);
|
|
69
|
+
}
|
|
70
|
+
const cached = guards.get(omissions);
|
|
71
|
+
if (cached) return cached;
|
|
72
|
+
const guarded = new Proxy(value, { get(target, property, receiver) {
|
|
73
|
+
if (typeof property === "string") {
|
|
74
|
+
const omitted = omissions[property];
|
|
75
|
+
if (omitted && !Object.prototype.hasOwnProperty.call(target, property)) omittedRead(route, propertyPath(path, property));
|
|
76
|
+
const child = Reflect.get(target, property, receiver);
|
|
77
|
+
return omitted === 1 ? child : guardOmissions(child, omitted, route, propertyPath(path, property));
|
|
78
|
+
}
|
|
79
|
+
return Reflect.get(target, property, receiver);
|
|
80
|
+
} });
|
|
81
|
+
guards.set(omissions, guarded);
|
|
82
|
+
return guarded;
|
|
83
|
+
}
|
|
84
|
+
function guardHydratedRouteData(value, framework) {
|
|
85
|
+
const metadata = readMetadata(framework);
|
|
86
|
+
return metadata ? guardOmissions(value, metadata.o, metadata.r) : value;
|
|
87
|
+
}
|
|
88
|
+
function getRouteHydrationMetadata(framework) {
|
|
89
|
+
return readMetadata(framework);
|
|
90
|
+
}
|
|
91
|
+
/** @internal Remove initial-only omission guards after a client loader reruns. */
|
|
92
|
+
function withoutRouteHydrationMetadata(framework) {
|
|
93
|
+
if (!framework || !("rh" in framework)) return framework;
|
|
94
|
+
const next = { ...framework };
|
|
95
|
+
delete next["rh"];
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/router/match.ts
|
|
100
|
+
function splitPathSegments(path) {
|
|
101
|
+
const normalized = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
102
|
+
if (!normalized || normalized === "/") return [];
|
|
103
|
+
const withoutLeadingSlash = normalized.startsWith("/") ? normalized.slice(1) : normalized;
|
|
104
|
+
return withoutLeadingSlash.length === 0 ? [] : withoutLeadingSlash.split("/");
|
|
105
|
+
}
|
|
106
|
+
function normalizeRouteSegmentName(value) {
|
|
107
|
+
return value.trim();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Parse a route template path into typed segments.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* parseSegments('/users/{id}') // [{kind:'static',value:'users'},{kind:'param',value:'id'}]
|
|
114
|
+
* parseSegments('/*') // [{kind:'catchall',value:'*'}]
|
|
115
|
+
* parseSegments('/posts/*') // [{kind:'static',value:'posts'},{kind:'wildcard',value:'*'}]
|
|
116
|
+
* parseSegments('/files/{*path}') // [{kind:'static',value:'files'},{kind:'splat',value:'path'}]
|
|
117
|
+
*/
|
|
118
|
+
function parseSegments(path) {
|
|
119
|
+
const parts = (path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path).split("/").filter(Boolean);
|
|
120
|
+
if (parts.length === 1 && parts[0] === "*") return [{
|
|
121
|
+
kind: "catchall",
|
|
122
|
+
value: "*"
|
|
123
|
+
}];
|
|
124
|
+
return parts.map((segment) => {
|
|
125
|
+
if (segment.startsWith("{") && segment.endsWith("}")) {
|
|
126
|
+
const value = normalizeRouteSegmentName(segment.slice(1, -1));
|
|
127
|
+
if (value.startsWith("*")) return {
|
|
128
|
+
kind: "splat",
|
|
129
|
+
value: normalizeRouteSegmentName(value.slice(1))
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
kind: "param",
|
|
133
|
+
value
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (segment === "*") return {
|
|
137
|
+
kind: "wildcard",
|
|
138
|
+
value: "*"
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
kind: "static",
|
|
142
|
+
value: segment
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Per-segment specificity weights. Routes are compared segment by segment and
|
|
148
|
+
* the first differing segment decides: static > param > wildcard > (end of
|
|
149
|
+
* route) > splat. "End of route" only ever meets a splat at the same position
|
|
150
|
+
* for routes that match the same URL, so an exact route beats an empty splat.
|
|
151
|
+
*/
|
|
152
|
+
const SEGMENT_WEIGHT = {
|
|
153
|
+
static: 4,
|
|
154
|
+
param: 3,
|
|
155
|
+
wildcard: 2,
|
|
156
|
+
splat: 0,
|
|
157
|
+
catchall: 0
|
|
158
|
+
};
|
|
159
|
+
const END_OF_ROUTE_WEIGHT = 1;
|
|
160
|
+
const RANK_BASE = 8;
|
|
161
|
+
function segmentWeight(segment) {
|
|
162
|
+
if (segment === void 0) return END_OF_ROUTE_WEIGHT;
|
|
163
|
+
return SEGMENT_WEIGHT[segment.kind];
|
|
164
|
+
}
|
|
165
|
+
function isCatchAll(segments) {
|
|
166
|
+
return segments.length === 1 && segments[0].kind === "catchall";
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Order two parsed routes by specificity: negative when `a` is more specific
|
|
170
|
+
* than `b`, positive when less, `0` when they tie (declaration order decides).
|
|
171
|
+
*
|
|
172
|
+
* Segments are compared left to right and the first differing segment decides
|
|
173
|
+
* (static > param > wildcard > splat); the bare `/*` catch-all is always last.
|
|
174
|
+
*/
|
|
175
|
+
function compareRouteSpecificity(a, b) {
|
|
176
|
+
const aCatchAll = isCatchAll(a);
|
|
177
|
+
const bCatchAll = isCatchAll(b);
|
|
178
|
+
if (aCatchAll || bCatchAll) return Number(aCatchAll) - Number(bCatchAll);
|
|
179
|
+
const length = Math.max(a.length, b.length);
|
|
180
|
+
for (let i = 0; i < length; i++) {
|
|
181
|
+
const difference = segmentWeight(b[i]) - segmentWeight(a[i]);
|
|
182
|
+
if (difference !== 0) return difference;
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Key for the set of URLs a route matches, used to reject duplicate
|
|
188
|
+
* registrations. Parameter and splat names do not affect matching, a `*`
|
|
189
|
+
* wildcard matches exactly one segment like a param (and always loses to it),
|
|
190
|
+
* static segments compare decoded (see {@link staticSegmentMatches}), and a
|
|
191
|
+
* scoped fallback matches exactly what a splat at its prefix matches, so
|
|
192
|
+
* routes with equal keys match the same URLs and the later one could never be
|
|
193
|
+
* reached.
|
|
194
|
+
*/
|
|
195
|
+
function routeMatchKey(segments, fallbackPrefix) {
|
|
196
|
+
const parts = (fallbackPrefix === void 0 ? segments : parseSegments(fallbackPrefix)).map((segment) => {
|
|
197
|
+
switch (segment.kind) {
|
|
198
|
+
case "static": return encodePathSegment(decodePathSegment(segment.value));
|
|
199
|
+
case "param":
|
|
200
|
+
case "wildcard": return "{}";
|
|
201
|
+
case "splat":
|
|
202
|
+
case "catchall": return "{*}";
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
if (fallbackPrefix !== void 0) parts.push("{*}");
|
|
206
|
+
return "/" + parts.join("/");
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Compute a numeric specificity rank from a parsed segment list.
|
|
210
|
+
*
|
|
211
|
+
* The rank encodes the segment-by-segment order of
|
|
212
|
+
* {@link compareRouteSpecificity} as base-8 digits (higher = more specific);
|
|
213
|
+
* the bare `/*` catch-all is `-1`. It is informational: route ordering uses
|
|
214
|
+
* `compareRouteSpecificity` so arbitrarily deep routes compare exactly.
|
|
215
|
+
*/
|
|
216
|
+
function computeRank(segments) {
|
|
217
|
+
if (isCatchAll(segments)) return -1;
|
|
218
|
+
let rank = 0;
|
|
219
|
+
let scale = 1;
|
|
220
|
+
for (const segment of segments) {
|
|
221
|
+
scale /= RANK_BASE;
|
|
222
|
+
rank += segmentWeight(segment) * scale;
|
|
223
|
+
}
|
|
224
|
+
return rank + END_OF_ROUTE_WEIGHT * scale / 7;
|
|
225
|
+
}
|
|
226
|
+
/** Reused frozen empty params object — returned for purely-static (no-capture) routes. */
|
|
227
|
+
const emptyParams = Object.freeze(Object.create(null));
|
|
228
|
+
Object.freeze({
|
|
229
|
+
matched: false,
|
|
230
|
+
params: emptyParams
|
|
231
|
+
});
|
|
232
|
+
/**
|
|
233
|
+
* Encoded path separators (`/` and backslash) that stay percent-encoded when a URL
|
|
234
|
+
* segment is decoded, so a capture never gains a separator the URL did not
|
|
235
|
+
* have (`..%2F..%2Fetc` must not become `../../etc`).
|
|
236
|
+
*/
|
|
237
|
+
const ENCODED_SEPARATOR = /(%2F|%5C)/i;
|
|
238
|
+
function decodeSegmentPiece(piece) {
|
|
239
|
+
if (!piece.includes("%")) return piece;
|
|
240
|
+
try {
|
|
241
|
+
return decodeURIComponent(piece);
|
|
242
|
+
} catch {
|
|
243
|
+
return piece;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Decode one URL path segment, keeping encoded separators as upper-case
|
|
248
|
+
* `%2F`/`%5C` and malformed encodings as written.
|
|
249
|
+
*/
|
|
250
|
+
function decodePathSegment(part) {
|
|
251
|
+
if (!part.includes("%")) return part;
|
|
252
|
+
return part.split(ENCODED_SEPARATOR).map((piece, index) => index % 2 === 1 ? piece.toUpperCase() : decodeSegmentPiece(piece)).join("");
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Percent-encode one path segment value, the inverse of
|
|
256
|
+
* {@link decodePathSegment}: kept `%2F`/`%5C` separators pass through so a
|
|
257
|
+
* capture round-trips to the URL it came from.
|
|
258
|
+
*/
|
|
259
|
+
function encodePathSegment(value) {
|
|
260
|
+
return value.split(ENCODED_SEPARATOR).map((piece, index) => index % 2 === 1 ? piece.toUpperCase() : encodeURIComponent(piece)).join("");
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Compare a static route segment with a URL segment. URL parts arrive
|
|
264
|
+
* percent-encoded (`caf%C3%A9`), so decoded forms are compared and `/café`,
|
|
265
|
+
* `/a b` and reserved-character routes match.
|
|
266
|
+
*/
|
|
267
|
+
function staticSegmentMatches(value, part) {
|
|
268
|
+
return value === part || decodePathSegment(value) === decodePathSegment(part);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Format the `*` capture of a catch-all or fallback from the remaining URL
|
|
272
|
+
* parts, decoding each segment like param and splat captures.
|
|
273
|
+
*/
|
|
274
|
+
function formatCatchAllCapture(parts) {
|
|
275
|
+
if (parts.length === 0) return "/";
|
|
276
|
+
if (parts.length === 1) return decodePathSegment(parts[0]);
|
|
277
|
+
return "/" + parts.map(decodePathSegment).join("/");
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Match pre-split URL parts against pre-parsed route segments.
|
|
281
|
+
*
|
|
282
|
+
* This is the hot-path matcher used by `resolveRoute` and
|
|
283
|
+
* `resolveRouteFromRoutes`. Callers split the URL path **once** and reuse
|
|
284
|
+
* `urlParts` across all route comparisons in a single resolution call.
|
|
285
|
+
*
|
|
286
|
+
* - Returns a params object (possibly the shared `{}`) on match.
|
|
287
|
+
* - Returns `null` on no match.
|
|
288
|
+
* - Params are allocated lazily — purely-static routes return the frozen
|
|
289
|
+
* empty sentinel without any heap allocation.
|
|
290
|
+
*/
|
|
291
|
+
function matchSegments(urlParts, segments) {
|
|
292
|
+
if (segments.length === 1 && segments[0].kind === "catchall") return { "*": formatCatchAllCapture(urlParts) };
|
|
293
|
+
const splatIndex = segments.findIndex((segment) => segment.kind === "splat");
|
|
294
|
+
if (splatIndex !== -1) {
|
|
295
|
+
if (splatIndex !== segments.length - 1) return null;
|
|
296
|
+
if (urlParts.length < splatIndex) return null;
|
|
297
|
+
} else if (urlParts.length !== segments.length) return null;
|
|
298
|
+
let params = null;
|
|
299
|
+
const normalizeCapturedSplatParts = (parts) => {
|
|
300
|
+
let start = 0;
|
|
301
|
+
while (start < parts.length && parts[start] === "") start += 1;
|
|
302
|
+
return start === 0 ? parts : parts.slice(start);
|
|
303
|
+
};
|
|
304
|
+
for (let i = 0; i < segments.length; i++) {
|
|
305
|
+
const seg = segments[i];
|
|
306
|
+
const part = urlParts[i];
|
|
307
|
+
if (seg.kind === "static") {
|
|
308
|
+
if (!staticSegmentMatches(seg.value, part)) return null;
|
|
309
|
+
} else if (seg.kind === "splat") {
|
|
310
|
+
if (params === null) params = {};
|
|
311
|
+
params[seg.value] = normalizeCapturedSplatParts(urlParts.slice(i).map(decodePathSegment)).join("/");
|
|
312
|
+
return params;
|
|
313
|
+
} else {
|
|
314
|
+
if (params === null) params = {};
|
|
315
|
+
if (seg.kind === "param") params[seg.value] = decodePathSegment(part);
|
|
316
|
+
else params["*"] = decodePathSegment(part);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return params ?? emptyParams;
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/common/router-internal.ts
|
|
323
|
+
const ROUTE_ROOT_COMPONENT = Symbol.for("askr.route-root-component");
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/router/route-data-loader.ts
|
|
326
|
+
/** Rejection of a `lazyRouteData()` loader whose import or `select` failed; wraps the original `cause`. */
|
|
327
|
+
var RouteDataLoadError = class extends Error {
|
|
328
|
+
constructor(route, phase, cause) {
|
|
329
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
330
|
+
super("[Askr] Route data loader failed for \"" + route + "\" during " + phase + " loading: " + detail);
|
|
331
|
+
this.name = "RouteDataLoadError";
|
|
332
|
+
this.route = route;
|
|
333
|
+
this.phase = phase;
|
|
334
|
+
this.cause = cause;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
function isAbortError(error) {
|
|
338
|
+
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
339
|
+
}
|
|
340
|
+
function loadPhase(mode) {
|
|
341
|
+
if (mode === "spa") return "client";
|
|
342
|
+
if (mode === "ssg") return "ssg";
|
|
343
|
+
return "server";
|
|
344
|
+
}
|
|
345
|
+
/** @internal Add route context to failures from lazyRouteData(). */
|
|
346
|
+
function _wrapRouteDataLoadError(error, context) {
|
|
347
|
+
if (error instanceof RouteDataLoadError || isAbortError(error)) throw error;
|
|
348
|
+
throw new RouteDataLoadError(context.href || context.pathname, loadPhase(context.mode), error);
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/router/lazy.ts
|
|
352
|
+
const pendingLazy = /* @__PURE__ */ new Set();
|
|
353
|
+
const lazyStates = /* @__PURE__ */ new WeakMap();
|
|
354
|
+
const handlerLazyComponents = /* @__PURE__ */ new WeakMap();
|
|
355
|
+
function startLazyLoad(state) {
|
|
356
|
+
if (state.promise) return state.promise;
|
|
357
|
+
let promise;
|
|
358
|
+
promise = Promise.resolve().then(state.factory).then((mod) => {
|
|
359
|
+
state.resolved = typeof mod === "function" ? mod : mod.default;
|
|
360
|
+
state.loadError = null;
|
|
361
|
+
state.hasLoadError = false;
|
|
362
|
+
}, (err) => {
|
|
363
|
+
state.loadError = err;
|
|
364
|
+
state.hasLoadError = true;
|
|
365
|
+
}).finally(() => {
|
|
366
|
+
if (state.hasLoadError) state.promise = null;
|
|
367
|
+
pendingLazy.delete(promise);
|
|
368
|
+
});
|
|
369
|
+
state.promise = promise;
|
|
370
|
+
pendingLazy.add(promise);
|
|
371
|
+
return promise;
|
|
372
|
+
}
|
|
373
|
+
/** Wrap a dynamic-import factory as a {@link LazyRouteComponent}, loaded on first use. */
|
|
374
|
+
function lazy(factory) {
|
|
375
|
+
const state = {
|
|
376
|
+
factory,
|
|
377
|
+
promise: null,
|
|
378
|
+
resolved: null,
|
|
379
|
+
loadError: null,
|
|
380
|
+
hasLoadError: false
|
|
381
|
+
};
|
|
382
|
+
const component = ((params) => {
|
|
383
|
+
if (state.hasLoadError) throw state.loadError;
|
|
384
|
+
if (!state.resolved) throw new Error("lazy() component used before it was resolved. Matched routes load automatically; call component.preload() for explicit preloading.");
|
|
385
|
+
return state.resolved(params);
|
|
386
|
+
});
|
|
387
|
+
Object.defineProperty(component, "preload", { value: () => startLazyLoad(state) });
|
|
388
|
+
lazyStates.set(component, state);
|
|
389
|
+
return component;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Create a cached route loader backed by a dynamic import. The module is only
|
|
393
|
+
* requested when the owning route matches, unless preload() is called.
|
|
394
|
+
*/
|
|
395
|
+
function lazyRouteData(factory, select) {
|
|
396
|
+
let imported = null;
|
|
397
|
+
const load = () => {
|
|
398
|
+
if (imported) return imported;
|
|
399
|
+
imported = Promise.resolve().then(factory).catch((error) => {
|
|
400
|
+
imported = null;
|
|
401
|
+
throw error;
|
|
402
|
+
});
|
|
403
|
+
return imported;
|
|
404
|
+
};
|
|
405
|
+
const loader = ((context) => load().then((module) => select ? select(module, context) : module).catch((error) => _wrapRouteDataLoadError(error, context)));
|
|
406
|
+
Object.defineProperty(loader, "preload", { value: () => load().then(() => void 0) });
|
|
407
|
+
return loader;
|
|
408
|
+
}
|
|
409
|
+
function _preloadRouteRecord(record) {
|
|
410
|
+
const imports = [
|
|
411
|
+
record.component,
|
|
412
|
+
...record.pageChain.map((scope) => scope.component),
|
|
413
|
+
...record.layoutChain.map((scope) => scope.component)
|
|
414
|
+
].flatMap((component) => {
|
|
415
|
+
const state = lazyStates.get(component);
|
|
416
|
+
return state && !state.resolved ? [startLazyLoad(state)] : [];
|
|
417
|
+
});
|
|
418
|
+
return imports.length > 0 ? Promise.all(imports).then(() => void 0) : null;
|
|
419
|
+
}
|
|
420
|
+
function _associateLazyHandler(handler, components) {
|
|
421
|
+
handlerLazyComponents.set(handler, components);
|
|
422
|
+
}
|
|
423
|
+
function _preloadRouteHandler(handler) {
|
|
424
|
+
const imports = (handlerLazyComponents.get(handler) ?? []).flatMap((component) => {
|
|
425
|
+
const state = lazyStates.get(component);
|
|
426
|
+
return state ? [startLazyLoad(state)] : [];
|
|
427
|
+
});
|
|
428
|
+
return imports.length > 0 ? Promise.all(imports).then(() => void 0) : null;
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/router/rendering.ts
|
|
432
|
+
const outletScope = defineScope(null);
|
|
433
|
+
const clientLayouts = /* @__PURE__ */ new WeakMap();
|
|
434
|
+
function clientLayout(layout) {
|
|
435
|
+
let wrapped = clientLayouts.get(layout);
|
|
436
|
+
if (!wrapped) {
|
|
437
|
+
wrapped = (props) => {
|
|
438
|
+
const output = layout(props);
|
|
439
|
+
return typeof Node !== "undefined" && output instanceof Node ? null : output;
|
|
440
|
+
};
|
|
441
|
+
clientLayouts.set(layout, wrapped);
|
|
442
|
+
}
|
|
443
|
+
return wrapped;
|
|
444
|
+
}
|
|
445
|
+
/** Renders the nested route content for the enclosing layout or page scope. */
|
|
446
|
+
function Outlet() {
|
|
447
|
+
return {
|
|
448
|
+
$$typeof: ELEMENT_TYPE,
|
|
449
|
+
type: Fragment,
|
|
450
|
+
props: { children: readScope(outletScope) },
|
|
451
|
+
key: null
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function applyPageChain(pageChain, params, content, deferComponents = false) {
|
|
455
|
+
let nextContent = content;
|
|
456
|
+
for (let i = pageChain.length - 1; i >= 0; i--) nextContent = outletScope({
|
|
457
|
+
value: nextContent,
|
|
458
|
+
children: deferComponents ? createRouteComponentVNode(pageChain[i].component, params) : pageChain[i].component(params)
|
|
459
|
+
});
|
|
460
|
+
return nextContent;
|
|
461
|
+
}
|
|
462
|
+
function createRouteComponentVNode(component, params, routeRoot = false) {
|
|
463
|
+
return {
|
|
464
|
+
$$typeof: ELEMENT_TYPE,
|
|
465
|
+
type: component,
|
|
466
|
+
props: params,
|
|
467
|
+
key: routeRoot ? currentAppRuntime()?.lifetime ?? null : null,
|
|
468
|
+
...routeRoot ? { [ROUTE_ROOT_COMPONENT]: true } : {}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function createRouteHandler(component, pageChain, layoutChain, deferComponents = false) {
|
|
472
|
+
const handler = (params) => {
|
|
473
|
+
let content = deferComponents ? createRouteComponentVNode(component, params, true) : component(params);
|
|
474
|
+
content = applyPageChain(pageChain, params, content, deferComponents);
|
|
475
|
+
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
476
|
+
const layout = layoutChain[i].component;
|
|
477
|
+
const rendered = deferComponents ? {
|
|
478
|
+
$$typeof: ELEMENT_TYPE,
|
|
479
|
+
type: clientLayout(layout),
|
|
480
|
+
props: { children: content },
|
|
481
|
+
key: null
|
|
482
|
+
} : layout({ children: content });
|
|
483
|
+
content = typeof Node !== "undefined" && rendered instanceof Node ? null : rendered;
|
|
484
|
+
}
|
|
485
|
+
return content;
|
|
486
|
+
};
|
|
487
|
+
_associateLazyHandler(handler, [
|
|
488
|
+
component,
|
|
489
|
+
...pageChain.map((scope) => scope.component),
|
|
490
|
+
...layoutChain.map((scope) => scope.component)
|
|
491
|
+
]);
|
|
492
|
+
return handler;
|
|
493
|
+
}
|
|
494
|
+
function getRenderHandler(record) {
|
|
495
|
+
return record.renderHandler ?? createRouteHandler(record.component, record.pageChain, record.layoutChain, true);
|
|
496
|
+
}
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/router/store.ts
|
|
499
|
+
function createRouteTable() {
|
|
500
|
+
return {
|
|
501
|
+
routes: [],
|
|
502
|
+
records: [],
|
|
503
|
+
namespaces: /* @__PURE__ */ new Set(),
|
|
504
|
+
routesByDepth: /* @__PURE__ */ new Map(),
|
|
505
|
+
registrationScopeStack: [],
|
|
506
|
+
registrationLocked: false,
|
|
507
|
+
defaultRouteAuthOptions: void 0,
|
|
508
|
+
activeClientRouteAuthOptions: void 0,
|
|
509
|
+
defaultRouteBasePath: ""
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
let activeTable = createRouteTable();
|
|
513
|
+
/** Run `fn` with `table` as the registration target, then restore the previous one. */
|
|
514
|
+
function withRouteTable(table, fn) {
|
|
515
|
+
const previous = activeTable;
|
|
516
|
+
activeTable = table;
|
|
517
|
+
try {
|
|
518
|
+
return fn();
|
|
519
|
+
} finally {
|
|
520
|
+
activeTable = previous;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function getDepth(path) {
|
|
524
|
+
const normalized = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
525
|
+
return normalized === "/" ? 0 : normalized.split("/").filter(Boolean).length;
|
|
526
|
+
}
|
|
527
|
+
function getRouteRecords() {
|
|
528
|
+
return activeTable.records;
|
|
529
|
+
}
|
|
530
|
+
function isRouteStoreRoutes(routeList) {
|
|
531
|
+
return routeList === activeTable.routes;
|
|
532
|
+
}
|
|
533
|
+
function insertRecordSorted(record) {
|
|
534
|
+
let lo = 0;
|
|
535
|
+
let hi = activeTable.records.length;
|
|
536
|
+
while (lo < hi) {
|
|
537
|
+
const mid = lo + hi >>> 1;
|
|
538
|
+
if (compareRouteSpecificity(activeTable.records[mid].segments, record.segments) <= 0) lo = mid + 1;
|
|
539
|
+
else hi = mid;
|
|
540
|
+
}
|
|
541
|
+
activeTable.records.splice(lo, 0, record);
|
|
542
|
+
}
|
|
543
|
+
function addRouteToStores(routeObj) {
|
|
544
|
+
activeTable.routes.push(routeObj);
|
|
545
|
+
const depth = getDepth(routeObj.path);
|
|
546
|
+
let depthRoutes = activeTable.routesByDepth.get(depth);
|
|
547
|
+
if (!depthRoutes) {
|
|
548
|
+
depthRoutes = [];
|
|
549
|
+
activeTable.routesByDepth.set(depth, depthRoutes);
|
|
550
|
+
}
|
|
551
|
+
depthRoutes.push(routeObj);
|
|
552
|
+
if (routeObj.namespace) activeTable.namespaces.add(routeObj.namespace);
|
|
553
|
+
}
|
|
554
|
+
function getActiveRoutes() {
|
|
555
|
+
return getActiveRenderContext()?.routes ?? currentAppRuntime()?.routeRegistry?.routes ?? activeTable.routes;
|
|
556
|
+
}
|
|
557
|
+
function getActiveRouteAuthOptions(override) {
|
|
558
|
+
if (override !== void 0) return override;
|
|
559
|
+
const renderContext = getActiveRenderContext();
|
|
560
|
+
if (renderContext) return renderContext.routeAuth;
|
|
561
|
+
const appRuntime = currentAppRuntime();
|
|
562
|
+
if (appRuntime) return appRuntime.routeAuth;
|
|
563
|
+
return activeTable.activeClientRouteAuthOptions ?? activeTable.defaultRouteAuthOptions;
|
|
564
|
+
}
|
|
565
|
+
function _setActiveRouteAuthOptions(auth) {
|
|
566
|
+
activeTable.activeClientRouteAuthOptions = auth;
|
|
567
|
+
}
|
|
568
|
+
function getDefaultRouteAuthOptions() {
|
|
569
|
+
return activeTable.defaultRouteAuthOptions;
|
|
570
|
+
}
|
|
571
|
+
function setDefaultRouteAuthOptions(auth) {
|
|
572
|
+
activeTable.defaultRouteAuthOptions = auth;
|
|
573
|
+
}
|
|
574
|
+
function getDefaultRouteBasePath() {
|
|
575
|
+
return activeTable.defaultRouteBasePath;
|
|
576
|
+
}
|
|
577
|
+
function setDefaultRouteBasePath(basePath) {
|
|
578
|
+
activeTable.defaultRouteBasePath = basePath;
|
|
579
|
+
}
|
|
580
|
+
function getActiveRouteBasePath() {
|
|
581
|
+
const renderContext = getActiveRenderContext();
|
|
582
|
+
if (renderContext?.basePath !== void 0) return renderContext.basePath;
|
|
583
|
+
const appRegistry = currentAppRuntime()?.routeRegistry;
|
|
584
|
+
if (appRegistry) return appRegistry.manifest.basePath ?? "";
|
|
585
|
+
return activeTable.defaultRouteBasePath;
|
|
586
|
+
}
|
|
587
|
+
function getCurrentLayoutChain() {
|
|
588
|
+
const layoutChain = [];
|
|
589
|
+
for (const scope of activeTable.registrationScopeStack) if (scope.layout) layoutChain.push({ component: scope.layout });
|
|
590
|
+
return layoutChain;
|
|
591
|
+
}
|
|
592
|
+
function getCurrentPageChain() {
|
|
593
|
+
const pageChain = [];
|
|
594
|
+
for (const scope of activeTable.registrationScopeStack) if (scope.page) pageChain.push({ component: scope.page });
|
|
595
|
+
return pageChain;
|
|
596
|
+
}
|
|
597
|
+
function hasActivePageScope() {
|
|
598
|
+
return activeTable.registrationScopeStack.some((scope) => !!scope.page);
|
|
599
|
+
}
|
|
600
|
+
function getCurrentPageScope() {
|
|
601
|
+
for (let index = activeTable.registrationScopeStack.length - 1; index >= 0; index -= 1) {
|
|
602
|
+
const scope = activeTable.registrationScopeStack[index];
|
|
603
|
+
if (scope.kind === "page") return scope;
|
|
604
|
+
}
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
function getCurrentScopeKind() {
|
|
608
|
+
return activeTable.registrationScopeStack[activeTable.registrationScopeStack.length - 1]?.kind ?? null;
|
|
609
|
+
}
|
|
610
|
+
function getCurrentPathPrefix() {
|
|
611
|
+
return activeTable.registrationScopeStack[activeTable.registrationScopeStack.length - 1]?.pathPrefix ?? "";
|
|
612
|
+
}
|
|
613
|
+
function getCurrentInheritedPolicies() {
|
|
614
|
+
const policies = [];
|
|
615
|
+
for (const scope of activeTable.registrationScopeStack) if (scope.policies.length > 0) policies.push(...scope.policies);
|
|
616
|
+
return policies;
|
|
617
|
+
}
|
|
618
|
+
function getCurrentInheritedAuthRequirements() {
|
|
619
|
+
return activeTable.registrationScopeStack.flatMap((scope) => scope.auth ? [scope.auth] : []);
|
|
620
|
+
}
|
|
621
|
+
function getCurrentInheritedMeta() {
|
|
622
|
+
return activeTable.registrationScopeStack.flatMap((scope) => scope.meta ? [scope.meta] : []);
|
|
623
|
+
}
|
|
624
|
+
function pushRegistrationScope(scope, fn) {
|
|
625
|
+
activeTable.registrationScopeStack.push(scope);
|
|
626
|
+
try {
|
|
627
|
+
fn();
|
|
628
|
+
} finally {
|
|
629
|
+
activeTable.registrationScopeStack.pop();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function lockRouteRegistration() {
|
|
633
|
+
activeTable.registrationLocked = true;
|
|
634
|
+
}
|
|
635
|
+
function assertRouteRegistrationUnlocked() {
|
|
636
|
+
if (activeTable.registrationLocked) throw new Error("Route registration is locked after app startup. Register routes at module load time before calling createSPA or hydrateSPA.");
|
|
637
|
+
}
|
|
638
|
+
function getRouteList() {
|
|
639
|
+
return [...activeTable.routes];
|
|
640
|
+
}
|
|
641
|
+
function hasRegisteredRoutes() {
|
|
642
|
+
return activeTable.routes.length > 0 || activeTable.records.length > 0;
|
|
643
|
+
}
|
|
644
|
+
function clearRouteState() {
|
|
645
|
+
activeTable.routes.length = 0;
|
|
646
|
+
activeTable.records.length = 0;
|
|
647
|
+
activeTable.namespaces.clear();
|
|
648
|
+
activeTable.routesByDepth.clear();
|
|
649
|
+
activeTable.registrationScopeStack.length = 0;
|
|
650
|
+
activeTable.registrationLocked = false;
|
|
651
|
+
activeTable.defaultRouteAuthOptions = void 0;
|
|
652
|
+
activeTable.activeClientRouteAuthOptions = void 0;
|
|
653
|
+
activeTable.defaultRouteBasePath = "";
|
|
654
|
+
}
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/router/route-context.ts
|
|
657
|
+
function parseLocation(url) {
|
|
658
|
+
try {
|
|
659
|
+
const parsedUrl = new URL(url, "http://localhost");
|
|
660
|
+
return {
|
|
661
|
+
pathname: parsedUrl.pathname,
|
|
662
|
+
search: parsedUrl.search,
|
|
663
|
+
hash: parsedUrl.hash
|
|
664
|
+
};
|
|
665
|
+
} catch {
|
|
666
|
+
return {
|
|
667
|
+
pathname: "/",
|
|
668
|
+
search: "",
|
|
669
|
+
hash: ""
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function deepFreeze(obj) {
|
|
674
|
+
if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
|
|
675
|
+
Object.freeze(obj);
|
|
676
|
+
for (const key of Object.keys(obj)) {
|
|
677
|
+
const value = obj[key];
|
|
678
|
+
if (value && typeof value === "object") deepFreeze(value);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return obj;
|
|
682
|
+
}
|
|
683
|
+
function makeQuery(search) {
|
|
684
|
+
const searchParams = new URLSearchParams(search || "");
|
|
685
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
686
|
+
for (const [key, value] of searchParams.entries()) {
|
|
687
|
+
const existing = mapping.get(key);
|
|
688
|
+
if (existing) existing.push(value);
|
|
689
|
+
else mapping.set(key, [value]);
|
|
690
|
+
}
|
|
691
|
+
return deepFreeze({
|
|
692
|
+
get(key) {
|
|
693
|
+
const values = mapping.get(key);
|
|
694
|
+
return values ? values[0] : null;
|
|
695
|
+
},
|
|
696
|
+
getAll(key) {
|
|
697
|
+
const values = mapping.get(key);
|
|
698
|
+
return values ? [...values] : [];
|
|
699
|
+
},
|
|
700
|
+
has(key) {
|
|
701
|
+
return mapping.has(key);
|
|
702
|
+
},
|
|
703
|
+
toJSON() {
|
|
704
|
+
const output = {};
|
|
705
|
+
for (const [key, values] of mapping.entries()) output[key] = values.length > 1 ? [...values] : values[0];
|
|
706
|
+
return output;
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function buildRouteContextBase(target, params, options) {
|
|
711
|
+
const parsed = parseLocation(target);
|
|
712
|
+
const href = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
713
|
+
return {
|
|
714
|
+
mode: options.mode,
|
|
715
|
+
params,
|
|
716
|
+
pathname: parsed.pathname,
|
|
717
|
+
search: parsed.search,
|
|
718
|
+
hash: parsed.hash,
|
|
719
|
+
href,
|
|
720
|
+
signal: options.signal
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function buildRouteContext(target, params, options) {
|
|
724
|
+
return {
|
|
725
|
+
...buildRouteContextBase(target, params, options),
|
|
726
|
+
auth: options.authContext
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region src/router/base-path.ts
|
|
731
|
+
/** Normalize the public mount point used by one route registry. */
|
|
732
|
+
function normalizeRouteBasePath(value) {
|
|
733
|
+
if (value === void 0 || value === "" || value === "/") return "";
|
|
734
|
+
if (!value.startsWith("/") || /\/{2,}/.test(value) || value.includes("?") || value.includes("#") || value.includes("\\")) throw new TypeError(`Route basePath must be an absolute pathname without a query or hash. Received: ${JSON.stringify(value)}`);
|
|
735
|
+
const normalized = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
736
|
+
if (normalized.split("/").some((segment) => segment === "." || segment === "..")) throw new TypeError("Route basePath must not contain dot segments.");
|
|
737
|
+
return normalized;
|
|
738
|
+
}
|
|
739
|
+
function parsedTarget(target) {
|
|
740
|
+
return new URL(target, "http://askr.invalid");
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Strip `basePath` from a pathname, comparing segments in decoded form so an
|
|
744
|
+
* encoded pathname (`/caf%C3%A9/menu`) matches its base (`/café`). Returns the
|
|
745
|
+
* remaining pathname (at least `/`), or `undefined` outside the base.
|
|
746
|
+
*/
|
|
747
|
+
function stripRouteBasePath(pathname, basePath) {
|
|
748
|
+
const pathParts = pathname.split("/");
|
|
749
|
+
const baseParts = basePath.split("/");
|
|
750
|
+
if (pathParts.length < baseParts.length) return void 0;
|
|
751
|
+
for (let i = 1; i < baseParts.length; i++) if (!staticSegmentMatches(baseParts[i], pathParts[i])) return void 0;
|
|
752
|
+
return `/${pathParts.slice(baseParts.length).join("/")}`;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Add a registry mount point to one logical root-relative route target. The
|
|
756
|
+
* target is always logical, so `/app/settings` under `/app` becomes
|
|
757
|
+
* `/app/app/settings`; callers holding a public href must not pass it here.
|
|
758
|
+
*/
|
|
759
|
+
function addRouteBasePath(target, basePath) {
|
|
760
|
+
if (!basePath || !target.startsWith("/") || target.startsWith("//")) return target;
|
|
761
|
+
const parsed = parsedTarget(target);
|
|
762
|
+
if (parsed.origin !== "http://askr.invalid") return target;
|
|
763
|
+
return `${basePath}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Add the mount point to an app-supplied string target (`navigate()`,
|
|
767
|
+
* `<Link href>`, `redirect()`). In development, warn when the target already
|
|
768
|
+
* starts with the base: strings are logical, so it gains the base again.
|
|
769
|
+
*/
|
|
770
|
+
function addLogicalRouteBasePath(target, basePath) {
|
|
771
|
+
return addRouteBasePath(target, basePath);
|
|
772
|
+
}
|
|
773
|
+
/** Remove a registry mount point and retain query/hash for logical matching. */
|
|
774
|
+
function removeRouteBasePath(target, basePath) {
|
|
775
|
+
const parsed = parsedTarget(target);
|
|
776
|
+
if (!basePath) return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
777
|
+
const logicalPath = stripRouteBasePath(parsed.pathname, basePath);
|
|
778
|
+
if (logicalPath === void 0) return void 0;
|
|
779
|
+
return `${logicalPath}${parsed.search}${parsed.hash}`;
|
|
780
|
+
}
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/router/route-matching.ts
|
|
783
|
+
const routeSegsCache = /* @__PURE__ */ new WeakMap();
|
|
784
|
+
const sortedListCache = /* @__PURE__ */ new WeakMap();
|
|
785
|
+
function cachedSegs(route) {
|
|
786
|
+
let segments = routeSegsCache.get(route);
|
|
787
|
+
if (!segments) {
|
|
788
|
+
segments = parseSegments(route.path);
|
|
789
|
+
routeSegsCache.set(route, segments);
|
|
790
|
+
}
|
|
791
|
+
return segments;
|
|
792
|
+
}
|
|
793
|
+
function cachedSortedList(routeList) {
|
|
794
|
+
let sorted = sortedListCache.get(routeList);
|
|
795
|
+
if (!sorted) {
|
|
796
|
+
sorted = [...routeList].sort((a, b) => compareRouteSpecificity(cachedSegs(a), cachedSegs(b)));
|
|
797
|
+
sortedListCache.set(routeList, sorted);
|
|
798
|
+
}
|
|
799
|
+
return sorted;
|
|
800
|
+
}
|
|
801
|
+
const prefixSegsCache = /* @__PURE__ */ new Map();
|
|
802
|
+
function cachedPrefixSegs(prefix) {
|
|
803
|
+
let segments = prefixSegsCache.get(prefix);
|
|
804
|
+
if (!segments) {
|
|
805
|
+
segments = parseSegments(prefix);
|
|
806
|
+
prefixSegsCache.set(prefix, segments);
|
|
807
|
+
}
|
|
808
|
+
return segments;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Match a scoped fallback's page prefix against the start of `pathname`.
|
|
812
|
+
* Prefix segments match like route segments, so a fallback inside
|
|
813
|
+
* `page('/{lang}')` matches `/en/...` and captures `lang`; the rest of the
|
|
814
|
+
* path is the `*` capture.
|
|
815
|
+
*/
|
|
816
|
+
function matchFallbackPrefix(pathname, fallbackPrefix) {
|
|
817
|
+
const urlParts = splitPathSegments(pathname);
|
|
818
|
+
const prefixSegments = cachedPrefixSegs(fallbackPrefix);
|
|
819
|
+
if (urlParts.length < prefixSegments.length) return null;
|
|
820
|
+
const prefixParams = matchSegments(urlParts.slice(0, prefixSegments.length), prefixSegments);
|
|
821
|
+
if (prefixParams === null) return null;
|
|
822
|
+
return {
|
|
823
|
+
...prefixParams,
|
|
824
|
+
"*": formatCatchAllCapture(urlParts.slice(prefixSegments.length))
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function findBestResolvedRouteFromRoutes(pathname, routeList) {
|
|
828
|
+
const normalized = pathname.endsWith("/") && pathname !== "/" ? pathname.slice(0, -1) : pathname;
|
|
829
|
+
const urlParts = splitPathSegments(normalized);
|
|
830
|
+
for (const route of cachedSortedList(routeList)) {
|
|
831
|
+
if (route.fallbackPrefix) continue;
|
|
832
|
+
const params = matchSegments(urlParts, cachedSegs(route));
|
|
833
|
+
if (params !== null) return {
|
|
834
|
+
route,
|
|
835
|
+
params
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
const fallback = findDeepestFallback(normalized, routeList);
|
|
839
|
+
return fallback ? {
|
|
840
|
+
route: fallback.entry,
|
|
841
|
+
params: fallback.params
|
|
842
|
+
} : null;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Pick the scoped fallback whose prefix matches `pathname` with the most
|
|
846
|
+
* segments. Depth is counted in segments, not characters, so an encoded prefix
|
|
847
|
+
* such as `/caf%C3%A9` does not outrank a deeper `/café/x`; equal depths are
|
|
848
|
+
* ordered by segment specificity.
|
|
849
|
+
*/
|
|
850
|
+
function findDeepestFallback(pathname, entries) {
|
|
851
|
+
let best = null;
|
|
852
|
+
let bestSegments = [];
|
|
853
|
+
for (const entry of entries) {
|
|
854
|
+
if (!entry.fallbackPrefix) continue;
|
|
855
|
+
const params = matchFallbackPrefix(pathname, entry.fallbackPrefix);
|
|
856
|
+
if (params === null) continue;
|
|
857
|
+
const segments = cachedPrefixSegs(entry.fallbackPrefix);
|
|
858
|
+
if (best === null || segments.length > bestSegments.length || segments.length === bestSegments.length && compareRouteSpecificity(segments, bestSegments) < 0) {
|
|
859
|
+
best = {
|
|
860
|
+
entry,
|
|
861
|
+
params
|
|
862
|
+
};
|
|
863
|
+
bestSegments = segments;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return best;
|
|
867
|
+
}
|
|
868
|
+
function findBestScopedFallbackRecord(pathname, routeRecords) {
|
|
869
|
+
const fallback = findDeepestFallback(pathname, routeRecords);
|
|
870
|
+
return fallback ? {
|
|
871
|
+
record: fallback.entry,
|
|
872
|
+
params: fallback.params
|
|
873
|
+
} : null;
|
|
874
|
+
}
|
|
875
|
+
function getMatchingRouteRecord(target, routeRecords) {
|
|
876
|
+
const location = parseLocation(target);
|
|
877
|
+
const normalized = location.pathname.endsWith("/") && location.pathname !== "/" ? location.pathname.slice(0, -1) : location.pathname;
|
|
878
|
+
const urlParts = splitPathSegments(normalized);
|
|
879
|
+
for (const record of routeRecords) {
|
|
880
|
+
if (record.fallbackPrefix) continue;
|
|
881
|
+
const params = matchSegments(urlParts, record.segments);
|
|
882
|
+
if (params !== null) return {
|
|
883
|
+
record,
|
|
884
|
+
params
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
return findBestScopedFallbackRecord(normalized, routeRecords);
|
|
888
|
+
}
|
|
889
|
+
function computeMatchesFromRoutes(pathname, routesList) {
|
|
890
|
+
const bestMatch = isRouteStoreRoutes(routesList) ? getMatchingRouteRecord(pathname, getRouteRecords()) : findBestResolvedRouteFromRoutes(pathname, routesList);
|
|
891
|
+
if (!bestMatch) return [];
|
|
892
|
+
return [{
|
|
893
|
+
path: "route" in bestMatch ? bestMatch.route.path : bestMatch.record.path,
|
|
894
|
+
params: deepFreeze({ ...bestMatch.params }),
|
|
895
|
+
name: "route" in bestMatch ? bestMatch.route.name : void 0,
|
|
896
|
+
namespace: "route" in bestMatch ? bestMatch.route.namespace : bestMatch.record.options.namespace
|
|
897
|
+
}];
|
|
898
|
+
}
|
|
899
|
+
function computeMatchesFromRouteRecords(pathname, routeRecords) {
|
|
900
|
+
const bestMatch = getMatchingRouteRecord(pathname, routeRecords);
|
|
901
|
+
if (!bestMatch) return [];
|
|
902
|
+
return [{
|
|
903
|
+
path: bestMatch.record.path,
|
|
904
|
+
params: deepFreeze({ ...bestMatch.params }),
|
|
905
|
+
namespace: bestMatch.record.options.namespace
|
|
906
|
+
}];
|
|
907
|
+
}
|
|
908
|
+
function computeRouteActivityMatches(pathname, options) {
|
|
909
|
+
const logicalPath = removeRouteBasePath(pathname, normalizeRouteBasePath(options.registry.manifest.basePath));
|
|
910
|
+
if (logicalPath === void 0) return [];
|
|
911
|
+
return computeMatchesFromRouteRecords(logicalPath, options.registry.manifest.records);
|
|
912
|
+
}
|
|
913
|
+
/** Match a path against a route registry without resolving route policies or loading data. */
|
|
914
|
+
function matchRoute(pathname, options) {
|
|
915
|
+
if (!options?.registry) throw new TypeError("matchRoute requires options.registry.");
|
|
916
|
+
return computeRouteActivityMatches(pathname, options)[0] ?? null;
|
|
917
|
+
}
|
|
918
|
+
function resolveRoute(pathname) {
|
|
919
|
+
const normalized = pathname.endsWith("/") && pathname !== "/" ? pathname.slice(0, -1) : pathname;
|
|
920
|
+
const urlParts = splitPathSegments(normalized);
|
|
921
|
+
const records = getRouteRecords();
|
|
922
|
+
for (const record of records) {
|
|
923
|
+
if (record.fallbackPrefix) continue;
|
|
924
|
+
const params = matchSegments(urlParts, record.segments);
|
|
925
|
+
if (params !== null) return {
|
|
926
|
+
handler: record.handler,
|
|
927
|
+
params
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const fallbackMatch = findBestScopedFallbackRecord(normalized, records);
|
|
931
|
+
return fallbackMatch ? {
|
|
932
|
+
handler: fallbackMatch.record.handler,
|
|
933
|
+
params: fallbackMatch.params
|
|
934
|
+
} : null;
|
|
935
|
+
}
|
|
936
|
+
function resolveRouteFromRoutes(pathname, routeList) {
|
|
937
|
+
if (isRouteStoreRoutes(routeList)) return resolveRoute(pathname);
|
|
938
|
+
const match = findBestResolvedRouteFromRoutes(pathname, routeList);
|
|
939
|
+
return match ? {
|
|
940
|
+
handler: match.route.handler,
|
|
941
|
+
params: match.params
|
|
942
|
+
} : null;
|
|
943
|
+
}
|
|
944
|
+
const anonymous = Object.freeze({
|
|
945
|
+
authenticated: false,
|
|
946
|
+
principal: null,
|
|
947
|
+
session: null,
|
|
948
|
+
tenant: null
|
|
949
|
+
});
|
|
950
|
+
let clientAuth = anonymous;
|
|
951
|
+
let hydratedAuth;
|
|
952
|
+
/** Return the identity resolved for the route currently being rendered. */
|
|
953
|
+
function currentAuth() {
|
|
954
|
+
const render = getActiveRenderContext();
|
|
955
|
+
if (render) return render.authContext ?? anonymous;
|
|
956
|
+
return clientAuth;
|
|
957
|
+
}
|
|
958
|
+
/** @internal Updated atomically with route resolution. */
|
|
959
|
+
function setCurrentAuth(context, mode) {
|
|
960
|
+
const render = getActiveRenderContext();
|
|
961
|
+
if (render) {
|
|
962
|
+
render.authContext = context;
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (mode === "spa") clientAuth = context;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* @internal Read and validate the server's opted-in identity snapshot. It
|
|
969
|
+
* decides the initial route the server already authorized and, when the app
|
|
970
|
+
* configures no `resolve`, stays the client identity for navigations. It is
|
|
971
|
+
* never a credential; the server enforces access.
|
|
972
|
+
*/
|
|
973
|
+
function readHydratedAuth(envelope) {
|
|
974
|
+
hydratedAuth = void 0;
|
|
975
|
+
const value = envelope?.framework["au"];
|
|
976
|
+
if (value === void 0) return void 0;
|
|
977
|
+
const { authenticated, principal, tenant, scopes } = value ?? {};
|
|
978
|
+
if (typeof authenticated !== "boolean" || principal !== null && typeof principal?.id !== "string" || tenant !== null && typeof tenant !== "string" || scopes !== void 0 && !(Array.isArray(scopes) && scopes.every((s) => typeof s === "string"))) throw new TypeError("[Askr] Malformed hydration auth snapshot.");
|
|
979
|
+
return hydratedAuth = Object.freeze({
|
|
980
|
+
authenticated,
|
|
981
|
+
principal,
|
|
982
|
+
session: null,
|
|
983
|
+
tenant,
|
|
984
|
+
...scopes ? { scopes } : {}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
/** @internal The hydration envelope without the auth snapshot. */
|
|
988
|
+
function withoutHydratedAuth(envelope) {
|
|
989
|
+
if (!envelope || !("au" in envelope.framework)) return envelope;
|
|
990
|
+
const framework = { ...envelope.framework };
|
|
991
|
+
delete framework["au"];
|
|
992
|
+
return withPageFramework(envelope, framework);
|
|
993
|
+
}
|
|
994
|
+
/** @internal Identity for a request when no `resolve` is configured. */
|
|
995
|
+
function unresolvedAuth(mode) {
|
|
996
|
+
return mode === "spa" && !getActiveRenderContext() && hydratedAuth || anonymous;
|
|
997
|
+
}
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region src/router/policy.ts
|
|
1000
|
+
/**
|
|
1001
|
+
* Records the public href of a redirect built from `to()`. It is an own
|
|
1002
|
+
* enumerable symbol, so `{ ...redirect(to(route)), status: 303 }` keeps it;
|
|
1003
|
+
* it only applies while `to` still equals that href. Trade-off: the key is
|
|
1004
|
+
* visible to `toEqual()` and `util.inspect()`, while `structuredClone()` and
|
|
1005
|
+
* JSON drop it, so a cloned decision is treated as logical and re-prefixed.
|
|
1006
|
+
*/
|
|
1007
|
+
const PUBLIC_REDIRECT_HREF = Symbol("askr.publicRedirectHref");
|
|
1008
|
+
/**
|
|
1009
|
+
* Validate a non-allow access decision before it leaves route resolution: a
|
|
1010
|
+
* redirect to a path-like target that resolves to another origin throws.
|
|
1011
|
+
*/
|
|
1012
|
+
function checkAccessDecision(decision) {
|
|
1013
|
+
if (decision.kind === "redirect") resolveNavigationUrl(decision.to);
|
|
1014
|
+
return decision;
|
|
1015
|
+
}
|
|
1016
|
+
/** Build a redirect decision; a typed destination's href is kept public. */
|
|
1017
|
+
function redirectDecision(to, init = {}, mapHref = (href) => href) {
|
|
1018
|
+
const decision = {
|
|
1019
|
+
kind: "redirect",
|
|
1020
|
+
to: mapHref(typeof to === "string" ? to : to.href),
|
|
1021
|
+
...init.status ? { status: init.status } : {},
|
|
1022
|
+
...init.replace !== void 0 ? { replace: init.replace } : {}
|
|
1023
|
+
};
|
|
1024
|
+
if (typeof to !== "string") decision[PUBLIC_REDIRECT_HREF] = decision.to;
|
|
1025
|
+
return checkAccessDecision(decision);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Expose a redirect with a public target: a logical string gains the registry
|
|
1029
|
+
* `basePath`, while a typed destination is already public.
|
|
1030
|
+
*/
|
|
1031
|
+
function exposeRedirectDecision(decision, basePath) {
|
|
1032
|
+
const publicHref = decision[PUBLIC_REDIRECT_HREF];
|
|
1033
|
+
if (publicHref !== void 0 && publicHref === decision.to) return decision;
|
|
1034
|
+
return {
|
|
1035
|
+
...decision,
|
|
1036
|
+
to: addLogicalRouteBasePath(decision.to, basePath)
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
/** Policy decision: allow the route to render. */
|
|
1040
|
+
function allow() {
|
|
1041
|
+
return { kind: "allow" };
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Policy decision: redirect the visitor to `to`. A string is a logical path
|
|
1045
|
+
* that gains the registry `basePath`; a `to()` destination is used as-is.
|
|
1046
|
+
*/
|
|
1047
|
+
function redirect(to, init = {}) {
|
|
1048
|
+
return redirectDecision(to, init);
|
|
1049
|
+
}
|
|
1050
|
+
/** Policy decision: deny the request with the given HTTP status. */
|
|
1051
|
+
function deny(status) {
|
|
1052
|
+
return {
|
|
1053
|
+
kind: "deny",
|
|
1054
|
+
status
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
/** Policy decision: deny with 401 Unauthorized. */
|
|
1058
|
+
function unauthorized() {
|
|
1059
|
+
return deny(401);
|
|
1060
|
+
}
|
|
1061
|
+
/** Policy decision: deny with 403 Forbidden. */
|
|
1062
|
+
function forbidden() {
|
|
1063
|
+
return deny(403);
|
|
1064
|
+
}
|
|
1065
|
+
/** Policy decision: deny with 404 Not Found. */
|
|
1066
|
+
function notFound() {
|
|
1067
|
+
return deny(404);
|
|
1068
|
+
}
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/router/resolution.ts
|
|
1071
|
+
const routeRenderContexts = /* @__PURE__ */ new WeakMap();
|
|
1072
|
+
const routeRenderData = /* @__PURE__ */ new WeakMap();
|
|
1073
|
+
function getRouteRenderContext(result) {
|
|
1074
|
+
return routeRenderContexts.get(result);
|
|
1075
|
+
}
|
|
1076
|
+
function getRouteRenderData(result) {
|
|
1077
|
+
return routeRenderData.get(result);
|
|
1078
|
+
}
|
|
1079
|
+
function createRouteRenderResult(record, params, context, handler) {
|
|
1080
|
+
const result = {
|
|
1081
|
+
kind: "render",
|
|
1082
|
+
handler,
|
|
1083
|
+
params
|
|
1084
|
+
};
|
|
1085
|
+
Object.defineProperty(result, "record", {
|
|
1086
|
+
value: record,
|
|
1087
|
+
enumerable: false
|
|
1088
|
+
});
|
|
1089
|
+
routeRenderContexts.set(result, context);
|
|
1090
|
+
return result;
|
|
1091
|
+
}
|
|
1092
|
+
function getDefaultRouteMode() {
|
|
1093
|
+
return typeof window !== "undefined" ? "spa" : "ssr";
|
|
1094
|
+
}
|
|
1095
|
+
function withoutInitialRouteHydrationMetadata(envelope) {
|
|
1096
|
+
return withPageFramework(envelope, withoutRouteHydrationMetadata(envelope.framework) ?? {});
|
|
1097
|
+
}
|
|
1098
|
+
/** @internal Bind resolved loader output to one route subtree and its hydration envelope. */
|
|
1099
|
+
function bindResolvedRouteData(handler, data, hydration) {
|
|
1100
|
+
return (params, context) => {
|
|
1101
|
+
const renderContext = getActiveRenderContext();
|
|
1102
|
+
if (renderContext) {
|
|
1103
|
+
const envelope = replacePageRoute(renderContext.renderData, data);
|
|
1104
|
+
renderContext.renderData = hydration ? envelope : withoutInitialRouteHydrationMetadata(envelope);
|
|
1105
|
+
const hydrationEnvelope = replacePageRoute(renderContext.hydrationData, hydration?.data ?? data);
|
|
1106
|
+
renderContext.hydrationData = hydration ? withPageFramework(hydrationEnvelope, {
|
|
1107
|
+
...hydrationEnvelope.framework,
|
|
1108
|
+
["rh"]: hydration.metadata
|
|
1109
|
+
}) : withoutInitialRouteHydrationMetadata(hydrationEnvelope);
|
|
1110
|
+
}
|
|
1111
|
+
return handler(params, context);
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function hasRouteRenderData(result) {
|
|
1115
|
+
return routeRenderData.has(result);
|
|
1116
|
+
}
|
|
1117
|
+
function buildRenderResult(record, params, context, request, telemetry, load, dataRuntime) {
|
|
1118
|
+
const lazyImport = _preloadRouteRecord(record);
|
|
1119
|
+
if (lazyImport) return lazyImport.then(() => buildLoadedRenderResult(record, params, context, request, telemetry, load, dataRuntime));
|
|
1120
|
+
return buildLoadedRenderResult(record, params, context, request, telemetry, load, dataRuntime);
|
|
1121
|
+
}
|
|
1122
|
+
function buildLoadedRenderResult(record, params, context, request, telemetry, load, dataRuntime) {
|
|
1123
|
+
const renderHandler = getRenderHandler(record);
|
|
1124
|
+
const loader = context.mode !== "ssg" && load ? record.options?.loader : void 0;
|
|
1125
|
+
const preload = record.options?.preload;
|
|
1126
|
+
const active = getActiveRenderContext();
|
|
1127
|
+
const prefetch = active?.queryPrefetch ?? createQueryPrefetchContext({
|
|
1128
|
+
mode: context.mode === "ssg" ? "ssr" : context.mode,
|
|
1129
|
+
request,
|
|
1130
|
+
signal: context.signal,
|
|
1131
|
+
runtime: active?.dataRuntime ?? dataRuntime,
|
|
1132
|
+
telemetry
|
|
1133
|
+
});
|
|
1134
|
+
const runPreload = preload ? Promise.resolve(preload({
|
|
1135
|
+
...context,
|
|
1136
|
+
params,
|
|
1137
|
+
request,
|
|
1138
|
+
data: prefetch
|
|
1139
|
+
})).then(() => void 0) : void 0;
|
|
1140
|
+
if (loader) {
|
|
1141
|
+
const load = () => withTelemetry(telemetry?.loader, { route: record.path }, () => loader({
|
|
1142
|
+
...context,
|
|
1143
|
+
params,
|
|
1144
|
+
request
|
|
1145
|
+
}));
|
|
1146
|
+
const loaded = runPreload ? runPreload.then(load) : load();
|
|
1147
|
+
const finalize = (data) => {
|
|
1148
|
+
const hydration = context.mode === "ssr" ? prepareRouteHydrationData(data, record.options?.dehydrate, {
|
|
1149
|
+
...context,
|
|
1150
|
+
params,
|
|
1151
|
+
request
|
|
1152
|
+
}, context.href || context.pathname || record.path) : void 0;
|
|
1153
|
+
const result = createRouteRenderResult(record, params, context, bindResolvedRouteData(renderHandler, data, hydration));
|
|
1154
|
+
routeRenderData.set(result, data);
|
|
1155
|
+
return result;
|
|
1156
|
+
};
|
|
1157
|
+
return isPromiseLike(loaded) ? Promise.resolve(loaded).then(finalize) : finalize(loaded);
|
|
1158
|
+
}
|
|
1159
|
+
if (runPreload) return runPreload.then(() => createRouteRenderResult(record, params, context, renderHandler));
|
|
1160
|
+
return createRouteRenderResult(record, params, context, renderHandler);
|
|
1161
|
+
}
|
|
1162
|
+
function runPolicies(policies, context, record, params, request, telemetry, load, dataRuntime, start = 0) {
|
|
1163
|
+
for (let index = start; index < policies.length; index += 1) {
|
|
1164
|
+
const result = policies[index](context);
|
|
1165
|
+
if (isPromiseLike(result)) return Promise.resolve(result).then((decision) => decision.kind === "allow" ? runPolicies(policies, context, record, params, request, telemetry, load, dataRuntime, index + 1) : checkAccessDecision(decision));
|
|
1166
|
+
if (result.kind !== "allow") return checkAccessDecision(result);
|
|
1167
|
+
}
|
|
1168
|
+
return buildRenderResult(record, params, context, request, telemetry, load, dataRuntime);
|
|
1169
|
+
}
|
|
1170
|
+
function resolvePath(value, fallback, context) {
|
|
1171
|
+
return typeof value === "function" ? value(context) : value ?? fallback;
|
|
1172
|
+
}
|
|
1173
|
+
function appendNext(path, href) {
|
|
1174
|
+
const target = resolveNavigationUrl(path);
|
|
1175
|
+
target.searchParams.set("next", href);
|
|
1176
|
+
return formatNavigationUrl(target);
|
|
1177
|
+
}
|
|
1178
|
+
function mapAuthDecision(decision, context, options) {
|
|
1179
|
+
if (decision.allowed) return { kind: "allow" };
|
|
1180
|
+
if (decision.reason === "forbidden") return {
|
|
1181
|
+
kind: "deny",
|
|
1182
|
+
status: 403
|
|
1183
|
+
};
|
|
1184
|
+
const target = resolvePath(decision.reason === "unauthenticated" ? options?.loginPath : options?.authenticatedRedirectTo, decision.reason === "unauthenticated" ? "/login" : "/", context);
|
|
1185
|
+
const finalize = (path) => redirectDecision(path, { replace: decision.reason === "unauthenticated" ? context.mode === "spa" : true }, (href) => decision.reason === "unauthenticated" ? appendNext(href, context.href) : href);
|
|
1186
|
+
return isPromiseLike(target) ? Promise.resolve(target).then(finalize) : finalize(target);
|
|
1187
|
+
}
|
|
1188
|
+
function resolveMatchedRoute(record, params, context, authOptions, request, telemetry, load, dataRuntime) {
|
|
1189
|
+
const continueResolution = (decision) => {
|
|
1190
|
+
if (decision) {
|
|
1191
|
+
const access = mapAuthDecision(decision, context, authOptions);
|
|
1192
|
+
if (isPromiseLike(access)) return Promise.resolve(access).then((next) => next.kind === "allow" ? runPolicies(record.options.policies ?? [], context, record, params, request, telemetry, load, dataRuntime) : next);
|
|
1193
|
+
if (access.kind !== "allow") return access;
|
|
1194
|
+
}
|
|
1195
|
+
return runPolicies(record.options.policies ?? [], context, record, params, request, telemetry, load, dataRuntime);
|
|
1196
|
+
};
|
|
1197
|
+
if (!record.options.auth) return continueResolution();
|
|
1198
|
+
const decision = record.options.auth(context.auth);
|
|
1199
|
+
return isPromiseLike(decision) ? Promise.resolve(decision).then(continueResolution) : continueResolution(decision);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Callers may omit `signal`, and `RouteRequestOptions.signal` is optional in the
|
|
1203
|
+
* published contract. Route context still needs an `AbortSignal`, so this stands
|
|
1204
|
+
* in for "this caller supplied no cancellation". It never aborts: a route loader
|
|
1205
|
+
* receiving it cannot be cancelled through `context.signal`. Prefer passing a
|
|
1206
|
+
* real signal.
|
|
1207
|
+
*/
|
|
1208
|
+
const NEVER_ABORTED = new AbortController().signal;
|
|
1209
|
+
/** Resolve `target` against a route registry, applying auth/policies to produce a render/redirect/deny result. */
|
|
1210
|
+
function resolveRouteRequest(target, options) {
|
|
1211
|
+
if (!options?.registry) throw new TypeError("resolveRouteRequest requires options.registry.");
|
|
1212
|
+
const basePath = normalizeRouteBasePath(options.registry.manifest.basePath);
|
|
1213
|
+
const logicalTarget = removeRouteBasePath(target, basePath);
|
|
1214
|
+
if (logicalTarget === void 0) return null;
|
|
1215
|
+
return withTelemetry(options.telemetry?.routeMatch, {}, () => {
|
|
1216
|
+
const records = options.registry.manifest.records;
|
|
1217
|
+
const match = getMatchingRouteRecord(logicalTarget, records);
|
|
1218
|
+
if (!match) return null;
|
|
1219
|
+
const mode = options.mode ?? getDefaultRouteMode();
|
|
1220
|
+
const signal = options.signal ?? getActiveRenderContext()?.signal ?? NEVER_ABORTED;
|
|
1221
|
+
const authOptions = getActiveRouteAuthOptions(options.auth ?? options.registry.manifest.auth);
|
|
1222
|
+
const base = buildRouteContextBase(logicalTarget, match.params, {
|
|
1223
|
+
mode,
|
|
1224
|
+
signal
|
|
1225
|
+
});
|
|
1226
|
+
const exposeRedirect = (result) => result?.kind === "redirect" ? exposeRedirectDecision(result, basePath) : result;
|
|
1227
|
+
const finalize = (authContext) => {
|
|
1228
|
+
if (!signal.aborted) setCurrentAuth(authContext, mode);
|
|
1229
|
+
const result = resolveMatchedRoute(match.record, match.params, buildRouteContext(logicalTarget, match.params, {
|
|
1230
|
+
mode,
|
|
1231
|
+
signal,
|
|
1232
|
+
authContext
|
|
1233
|
+
}), authOptions, options.request, options.telemetry, options.load !== false, options.dataRuntime);
|
|
1234
|
+
if (!basePath) return result;
|
|
1235
|
+
return isPromiseLike(result) ? Promise.resolve(result).then(exposeRedirect) : exposeRedirect(result);
|
|
1236
|
+
};
|
|
1237
|
+
if (options.authContext) return finalize(options.authContext);
|
|
1238
|
+
if (!authOptions?.resolve) return finalize(unresolvedAuth(mode));
|
|
1239
|
+
const resolved = authOptions.resolve(base);
|
|
1240
|
+
return isPromiseLike(resolved) ? Promise.resolve(resolved).then(finalize) : finalize(resolved);
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
//#endregion
|
|
1244
|
+
export { withRouteTable as $, clearRouteState as A, getCurrentPathPrefix as B, removeRouteBasePath as C, _setActiveRouteAuthOptions as D, parseLocation as E, getCurrentInheritedMeta as F, getRouteRecords as G, getDefaultRouteAuthOptions as H, getCurrentInheritedPolicies as I, insertRecordSorted as J, hasActivePageScope as K, getCurrentLayoutChain as L, getActiveRouteBasePath as M, getActiveRoutes as N, addRouteToStores as O, getCurrentInheritedAuthRequirements as P, setDefaultRouteBasePath as Q, getCurrentPageChain as R, normalizeRouteBasePath as S, makeQuery as T, getDefaultRouteBasePath as U, getCurrentScopeKind as V, getRouteList as W, pushRegistrationScope as X, lockRouteRegistration as Y, setDefaultRouteAuthOptions as Z, getMatchingRouteRecord as _, resolveRouteRequest as a, lazyRouteData as at, addLogicalRouteBasePath as b, forbidden as c, encodePathSegment as ct, unauthorized as d, routeMatchKey as dt, Outlet as et, currentAuth as f, getRouteHydrationMetadata as ft, computeRouteActivityMatches as g, withoutRouteHydrationMetadata as gt, computeMatchesFromRoutes as h, validateRouteHydrationData as ht, hasRouteRenderData as i, lazy as it, createRouteTable as j, assertRouteRegistrationUnlocked as k, notFound as l, normalizeRouteSegmentName as lt, withoutHydratedAuth as m, prepareRouteHydrationData as mt, getRouteRenderContext as n, getRenderHandler as nt, allow as o, RouteDataLoadError as ot, readHydratedAuth as p, guardHydratedRouteData as pt, hasRegisteredRoutes as q, getRouteRenderData as r, _preloadRouteHandler as rt, deny as s, computeRank as st, bindResolvedRouteData as t, createRouteHandler as tt, redirect as u, parseSegments as ut, matchRoute as v, deepFreeze as w, addRouteBasePath as x, resolveRouteFromRoutes as y, getCurrentPageScope as z };
|