@devp0nt/route0 1.0.0-next.7 → 1.0.0-next.71
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/cjs/index.cjs +795 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +440 -92
- package/dist/esm/index.d.ts +440 -92
- package/dist/esm/index.js +734 -98
- package/dist/esm/index.js.map +1 -1
- package/package.json +43 -24
- package/dist/cjs/index.js +0 -158
- package/dist/cjs/index.js.map +0 -1
- package/src/index.test.ts +0 -206
- package/src/index.ts +0 -365
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var index_exports = {};
|
|
20
|
+
__export(index_exports, {
|
|
21
|
+
Route0: () => Route0,
|
|
22
|
+
Routes: () => Routes
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(index_exports);
|
|
25
|
+
var import_flat0 = require("@devp0nt/flat0");
|
|
26
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
const getPathSegments = (definition) => {
|
|
28
|
+
if (definition === "" || definition === "/") return [];
|
|
29
|
+
return definition.split("/").filter(Boolean);
|
|
30
|
+
};
|
|
31
|
+
const getPathTokens = (definition) => {
|
|
32
|
+
const segments = getPathSegments(definition);
|
|
33
|
+
return segments.map((segment) => {
|
|
34
|
+
const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
|
|
35
|
+
if (param) {
|
|
36
|
+
return { kind: "param", name: param[1], optional: param[2] === "?" };
|
|
37
|
+
}
|
|
38
|
+
if (segment === "*" || segment === "*?") {
|
|
39
|
+
return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
|
|
40
|
+
}
|
|
41
|
+
const wildcard = segment.match(/^(.*)\*(\?)?$/);
|
|
42
|
+
if (wildcard && !segment.includes("\\*")) {
|
|
43
|
+
return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
|
|
44
|
+
}
|
|
45
|
+
return { kind: "static", value: segment };
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const getPathRegexBaseStrictString = (definition) => {
|
|
49
|
+
const tokens = getPathTokens(definition);
|
|
50
|
+
if (tokens.length === 0) return "";
|
|
51
|
+
let pattern = "";
|
|
52
|
+
for (const token of tokens) {
|
|
53
|
+
if (token.kind === "static") {
|
|
54
|
+
pattern += `/${escapeRegex(token.value)}`;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (token.kind === "param") {
|
|
58
|
+
pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (token.prefix.length > 0) {
|
|
62
|
+
pattern += `/${escapeRegex(token.prefix)}(.*)`;
|
|
63
|
+
} else {
|
|
64
|
+
pattern += "(?:/(.*))?";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return pattern;
|
|
68
|
+
};
|
|
69
|
+
const getPathCaptureKeys = (definition) => {
|
|
70
|
+
const keys = [];
|
|
71
|
+
for (const token of getPathTokens(definition)) {
|
|
72
|
+
if (token.kind === "param") keys.push(token.name);
|
|
73
|
+
if (token.kind === "wildcard") keys.push("*");
|
|
74
|
+
}
|
|
75
|
+
return keys;
|
|
76
|
+
};
|
|
77
|
+
const getPathParamsDefinition = (definition) => {
|
|
78
|
+
const entries = getPathTokens(definition).filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
|
|
79
|
+
return Object.fromEntries(entries);
|
|
80
|
+
};
|
|
81
|
+
const validatePathDefinition = (definition) => {
|
|
82
|
+
const segments = getPathSegments(definition);
|
|
83
|
+
const wildcardSegments = segments.filter((segment) => segment.includes("*"));
|
|
84
|
+
if (wildcardSegments.length === 0) return;
|
|
85
|
+
if (wildcardSegments.length > 1) {
|
|
86
|
+
throw new Error(`Invalid route definition "${definition}": only one wildcard segment is allowed`);
|
|
87
|
+
}
|
|
88
|
+
const wildcardSegmentIndex = segments.findIndex((segment) => segment.includes("*"));
|
|
89
|
+
const wildcardSegment = segments[wildcardSegmentIndex];
|
|
90
|
+
if (!wildcardSegment.match(/^(?:\*|\*\?|[^*]+\*|\S+\*\?)$/)) {
|
|
91
|
+
throw new Error(`Invalid route definition "${definition}": wildcard must be trailing in its segment`);
|
|
92
|
+
}
|
|
93
|
+
if (wildcardSegmentIndex !== segments.length - 1) {
|
|
94
|
+
throw new Error(`Invalid route definition "${definition}": wildcard segment is allowed only at the end`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const stripTrailingWildcard = (definition) => definition.replace(/\*\??$/, "");
|
|
98
|
+
class Route0 {
|
|
99
|
+
definition;
|
|
100
|
+
params;
|
|
101
|
+
_origin;
|
|
102
|
+
_callable;
|
|
103
|
+
Infer = null;
|
|
104
|
+
/** Base URL used when generating absolute URLs (`abs: true`). */
|
|
105
|
+
get origin() {
|
|
106
|
+
if (!this._origin) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"origin for route " + this.definition + ' is not set, please provide it like Route0.create(route, {origin: "https://example.com"}) in config or set via clones like routes._.clone({origin: "https://example.com"})'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return this._origin;
|
|
112
|
+
}
|
|
113
|
+
set origin(origin) {
|
|
114
|
+
this._origin = origin;
|
|
115
|
+
}
|
|
116
|
+
constructor(definition, config = {}) {
|
|
117
|
+
validatePathDefinition(definition);
|
|
118
|
+
this.definition = definition;
|
|
119
|
+
this.params = Route0._getParamsDefinitionByDefinition(definition);
|
|
120
|
+
const { origin } = config;
|
|
121
|
+
if (origin && typeof origin === "string" && origin.length) {
|
|
122
|
+
this._origin = origin;
|
|
123
|
+
} else {
|
|
124
|
+
const g = globalThis;
|
|
125
|
+
if (typeof g?.location?.origin === "string" && g.location.origin.length > 0) {
|
|
126
|
+
this._origin = g.location.origin;
|
|
127
|
+
} else {
|
|
128
|
+
this._origin = void 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const callable = this.get.bind(this);
|
|
132
|
+
Object.setPrototypeOf(callable, this);
|
|
133
|
+
Object.defineProperty(callable, Symbol.toStringTag, {
|
|
134
|
+
value: this.definition
|
|
135
|
+
});
|
|
136
|
+
this._callable = callable;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Creates a callable route instance.
|
|
140
|
+
*
|
|
141
|
+
* If an existing route/callable route is provided, it is cloned.
|
|
142
|
+
*/
|
|
143
|
+
static create(definition, config) {
|
|
144
|
+
if (typeof definition === "function" || typeof definition === "object") {
|
|
145
|
+
return definition.clone(config);
|
|
146
|
+
}
|
|
147
|
+
const original = new Route0(definition, config);
|
|
148
|
+
return original._callable;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Normalizes a definition/route into a callable route.
|
|
152
|
+
*
|
|
153
|
+
* Unlike `create`, passing a callable route returns the same instance.
|
|
154
|
+
*/
|
|
155
|
+
static from(definition) {
|
|
156
|
+
if (typeof definition === "function") {
|
|
157
|
+
return definition;
|
|
158
|
+
}
|
|
159
|
+
const original = typeof definition === "object" ? definition : new Route0(definition);
|
|
160
|
+
return original._callable;
|
|
161
|
+
}
|
|
162
|
+
static _getAbsPath(origin, url) {
|
|
163
|
+
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
164
|
+
}
|
|
165
|
+
static _getParamsDefinitionByDefinition(definition) {
|
|
166
|
+
return getPathParamsDefinition(definition);
|
|
167
|
+
}
|
|
168
|
+
search() {
|
|
169
|
+
return this._callable;
|
|
170
|
+
}
|
|
171
|
+
/** Extends the current route definition by appending a suffix route. */
|
|
172
|
+
extend(suffixDefinition) {
|
|
173
|
+
const sourceDefinitionWithoutWildcard = stripTrailingWildcard(this.definition);
|
|
174
|
+
const definition = `${sourceDefinitionWithoutWildcard}/${suffixDefinition}`.replace(/\/{2,}/g, "/");
|
|
175
|
+
return Route0.create(
|
|
176
|
+
definition,
|
|
177
|
+
{ origin: this._origin }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
// implementation
|
|
181
|
+
get(...args) {
|
|
182
|
+
const { searchInput, paramsInput, absInput, absOriginInput, hashInput } = (() => {
|
|
183
|
+
if (args.length === 0) {
|
|
184
|
+
return {
|
|
185
|
+
searchInput: {},
|
|
186
|
+
paramsInput: {},
|
|
187
|
+
absInput: false,
|
|
188
|
+
absOriginInput: void 0,
|
|
189
|
+
hashInput: void 0
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const [input, abs] = (() => {
|
|
193
|
+
if (typeof args[0] === "object" && args[0] !== null) {
|
|
194
|
+
return [args[0], args[1]];
|
|
195
|
+
}
|
|
196
|
+
if (typeof args[1] === "object" && args[1] !== null) {
|
|
197
|
+
return [args[1], args[0]];
|
|
198
|
+
}
|
|
199
|
+
if (typeof args[0] === "boolean" || typeof args[0] === "string") {
|
|
200
|
+
return [{}, args[0]];
|
|
201
|
+
}
|
|
202
|
+
if (typeof args[1] === "boolean" || typeof args[1] === "string") {
|
|
203
|
+
return [{}, args[1]];
|
|
204
|
+
}
|
|
205
|
+
return [{}, void 0];
|
|
206
|
+
})();
|
|
207
|
+
let searchInput2 = {};
|
|
208
|
+
let hashInput2 = void 0;
|
|
209
|
+
const paramsInput2 = {};
|
|
210
|
+
for (const [key, value] of Object.entries(input)) {
|
|
211
|
+
if (key === "?" && typeof value === "object" && value !== null) {
|
|
212
|
+
searchInput2 = value;
|
|
213
|
+
} else if (key === "#" && (typeof value === "string" || typeof value === "number")) {
|
|
214
|
+
hashInput2 = String(value);
|
|
215
|
+
} else if (key in this.params && (typeof value === "string" || typeof value === "number")) {
|
|
216
|
+
Object.assign(paramsInput2, { [key]: String(value) });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const absOriginInput2 = typeof abs === "string" && abs.length > 0 ? abs : void 0;
|
|
220
|
+
return {
|
|
221
|
+
searchInput: searchInput2,
|
|
222
|
+
paramsInput: paramsInput2,
|
|
223
|
+
absInput: absOriginInput2 !== void 0 || abs === true,
|
|
224
|
+
absOriginInput: absOriginInput2,
|
|
225
|
+
hashInput: hashInput2
|
|
226
|
+
};
|
|
227
|
+
})();
|
|
228
|
+
let url = this.definition;
|
|
229
|
+
url = url.replace(/\/:([A-Za-z0-9_]+)\?/g, (_m, k) => {
|
|
230
|
+
const value = paramsInput[k];
|
|
231
|
+
if (value === void 0) return "";
|
|
232
|
+
return `/${encodeURIComponent(String(value))}`;
|
|
233
|
+
});
|
|
234
|
+
url = url.replace(/:([A-Za-z0-9_]+)(?!\?)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "undefined")));
|
|
235
|
+
url = url.replace(/\/\*\?/g, () => {
|
|
236
|
+
const value = paramsInput["*"];
|
|
237
|
+
if (value === void 0) return "";
|
|
238
|
+
const stringValue = String(value);
|
|
239
|
+
return stringValue.startsWith("/") ? stringValue : `/${stringValue}`;
|
|
240
|
+
});
|
|
241
|
+
url = url.replace(/\/\*/g, () => {
|
|
242
|
+
const value = String(paramsInput["*"] ?? "");
|
|
243
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
244
|
+
});
|
|
245
|
+
url = url.replace(/\*\?/g, () => String(paramsInput["*"] ?? ""));
|
|
246
|
+
url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
|
|
247
|
+
const searchString = (0, import_flat0.stringify)(searchInput);
|
|
248
|
+
url = [url, searchString].filter(Boolean).join("?");
|
|
249
|
+
url = url.replace(/\/{2,}/g, "/");
|
|
250
|
+
url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
|
|
251
|
+
if (hashInput !== void 0) {
|
|
252
|
+
url = `${url}#${hashInput}`;
|
|
253
|
+
}
|
|
254
|
+
return url;
|
|
255
|
+
}
|
|
256
|
+
/** Returns path param keys extracted from route definition. */
|
|
257
|
+
getParamsKeys() {
|
|
258
|
+
return Object.keys(this.params);
|
|
259
|
+
}
|
|
260
|
+
getPathTokens() {
|
|
261
|
+
return getPathTokens(this.definition);
|
|
262
|
+
}
|
|
263
|
+
/** Clones route with optional config override. */
|
|
264
|
+
clone(config) {
|
|
265
|
+
return Route0.create(this.definition, config);
|
|
266
|
+
}
|
|
267
|
+
getRegexBaseStrictString() {
|
|
268
|
+
return getPathRegexBaseStrictString(this.definition);
|
|
269
|
+
}
|
|
270
|
+
getRegexBaseString() {
|
|
271
|
+
return this.getRegexBaseStrictString().replace(/\/+$/, "") + "/?";
|
|
272
|
+
}
|
|
273
|
+
getRegexStrictString() {
|
|
274
|
+
return `^${this.getRegexBaseStrictString()}$`;
|
|
275
|
+
}
|
|
276
|
+
getRegexString() {
|
|
277
|
+
return `^${this.getRegexBaseString()}$`;
|
|
278
|
+
}
|
|
279
|
+
getRegexStrict() {
|
|
280
|
+
return new RegExp(this.getRegexStrictString());
|
|
281
|
+
}
|
|
282
|
+
getRegex() {
|
|
283
|
+
return new RegExp(this.getRegexString());
|
|
284
|
+
}
|
|
285
|
+
/** Creates a grouped strict regex pattern string from many routes. */
|
|
286
|
+
static getRegexStrictStringGroup(routes) {
|
|
287
|
+
const patterns = routes.map((route) => route.getRegexStrictString()).join("|");
|
|
288
|
+
return `(${patterns})`;
|
|
289
|
+
}
|
|
290
|
+
/** Creates a strict grouped regex from many routes. */
|
|
291
|
+
static getRegexStrictGroup(routes) {
|
|
292
|
+
const patterns = Route0.getRegexStrictStringGroup(routes);
|
|
293
|
+
return new RegExp(`^(${patterns})$`);
|
|
294
|
+
}
|
|
295
|
+
/** Creates a grouped regex pattern string from many routes. */
|
|
296
|
+
static getRegexStringGroup(routes) {
|
|
297
|
+
const patterns = routes.map((route) => route.getRegexString()).join("|");
|
|
298
|
+
return `(${patterns})`;
|
|
299
|
+
}
|
|
300
|
+
/** Creates a grouped regex from many routes. */
|
|
301
|
+
static getRegexGroup(routes) {
|
|
302
|
+
const patterns = Route0.getRegexStringGroup(routes);
|
|
303
|
+
return new RegExp(`^(${patterns})$`);
|
|
304
|
+
}
|
|
305
|
+
/** Converts any location shape to relative form (removes host/origin fields). */
|
|
306
|
+
static toRelLocation(location) {
|
|
307
|
+
return {
|
|
308
|
+
...location,
|
|
309
|
+
abs: false,
|
|
310
|
+
origin: void 0,
|
|
311
|
+
href: void 0,
|
|
312
|
+
port: void 0,
|
|
313
|
+
host: void 0,
|
|
314
|
+
hostname: void 0
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** Converts a location to absolute form using provided origin URL. */
|
|
318
|
+
static toAbsLocation(location, origin) {
|
|
319
|
+
const relLoc = Route0.toRelLocation(location);
|
|
320
|
+
const url = new URL(relLoc.hrefRel, origin);
|
|
321
|
+
return {
|
|
322
|
+
...location,
|
|
323
|
+
abs: true,
|
|
324
|
+
origin: url.origin,
|
|
325
|
+
href: url.href,
|
|
326
|
+
port: url.port,
|
|
327
|
+
host: url.host,
|
|
328
|
+
hostname: url.hostname
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
static getLocation(hrefOrHrefRelOrLocation) {
|
|
332
|
+
if (hrefOrHrefRelOrLocation instanceof URL) {
|
|
333
|
+
return Route0.getLocation(hrefOrHrefRelOrLocation.href);
|
|
334
|
+
}
|
|
335
|
+
if (typeof hrefOrHrefRelOrLocation !== "string") {
|
|
336
|
+
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
|
|
337
|
+
}
|
|
338
|
+
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
339
|
+
const base = abs ? void 0 : "http://example.com";
|
|
340
|
+
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
341
|
+
const search = (0, import_flat0.parse)(url.search);
|
|
342
|
+
let pathname = url.pathname;
|
|
343
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
344
|
+
pathname = pathname.slice(0, -1);
|
|
345
|
+
}
|
|
346
|
+
const hrefRel = pathname + url.search + url.hash;
|
|
347
|
+
const location = {
|
|
348
|
+
pathname,
|
|
349
|
+
search,
|
|
350
|
+
searchString: url.search,
|
|
351
|
+
hash: url.hash,
|
|
352
|
+
origin: abs ? url.origin : void 0,
|
|
353
|
+
href: abs ? url.href : void 0,
|
|
354
|
+
hrefRel,
|
|
355
|
+
abs,
|
|
356
|
+
// extra host-related fields (available even for relative with dummy base)
|
|
357
|
+
host: abs ? url.host : void 0,
|
|
358
|
+
hostname: abs ? url.hostname : void 0,
|
|
359
|
+
port: abs ? url.port || void 0 : void 0,
|
|
360
|
+
// specific to UnknownLocation
|
|
361
|
+
params: void 0,
|
|
362
|
+
route: void 0,
|
|
363
|
+
known: false,
|
|
364
|
+
exact: false,
|
|
365
|
+
ancestor: false,
|
|
366
|
+
descendant: false,
|
|
367
|
+
unmatched: false
|
|
368
|
+
};
|
|
369
|
+
return location;
|
|
370
|
+
}
|
|
371
|
+
getLocation(hrefOrHrefRelOrLocation) {
|
|
372
|
+
if (hrefOrHrefRelOrLocation instanceof URL) {
|
|
373
|
+
return this.getLocation(hrefOrHrefRelOrLocation.href);
|
|
374
|
+
}
|
|
375
|
+
if (typeof hrefOrHrefRelOrLocation !== "string") {
|
|
376
|
+
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
|
|
377
|
+
}
|
|
378
|
+
const location = Route0.getLocation(hrefOrHrefRelOrLocation);
|
|
379
|
+
location.route = this.definition;
|
|
380
|
+
location.params = {};
|
|
381
|
+
const pathname = location.pathname.length > 1 && location.pathname.endsWith("/") ? location.pathname.slice(0, -1) : location.pathname;
|
|
382
|
+
const paramNames = getPathCaptureKeys(this.definition);
|
|
383
|
+
const def = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
|
|
384
|
+
const exactRe = new RegExp(`^${this.getRegexBaseString()}$`);
|
|
385
|
+
const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`);
|
|
386
|
+
const exactMatch = pathname.match(exactRe);
|
|
387
|
+
const ancestorMatch = pathname.match(ancestorRe);
|
|
388
|
+
const exact = !!exactMatch;
|
|
389
|
+
const ancestor = !exact && !!ancestorMatch;
|
|
390
|
+
const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
|
|
391
|
+
if (paramsMatch) {
|
|
392
|
+
const values = paramsMatch.slice(1, 1 + paramNames.length);
|
|
393
|
+
const params = Object.fromEntries(
|
|
394
|
+
paramNames.map((n, i) => {
|
|
395
|
+
const value = values[i];
|
|
396
|
+
return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
location.params = params;
|
|
400
|
+
} else {
|
|
401
|
+
location.params = {};
|
|
402
|
+
}
|
|
403
|
+
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
404
|
+
const defParts = getParts(def);
|
|
405
|
+
const pathParts = getParts(pathname);
|
|
406
|
+
let isPrefix = true;
|
|
407
|
+
if (pathParts.length > defParts.length) {
|
|
408
|
+
isPrefix = false;
|
|
409
|
+
} else {
|
|
410
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
411
|
+
const defPart = defParts[i];
|
|
412
|
+
const pathPart = pathParts[i];
|
|
413
|
+
if (!defPart) {
|
|
414
|
+
isPrefix = false;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
if (defPart.startsWith(":")) continue;
|
|
418
|
+
if (defPart.includes("*")) {
|
|
419
|
+
const prefix = defPart.replace(/\*\??$/, "");
|
|
420
|
+
if (pathPart.startsWith(prefix)) continue;
|
|
421
|
+
isPrefix = false;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
if (defPart !== pathPart) {
|
|
425
|
+
isPrefix = false;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const descendant = !exact && isPrefix;
|
|
431
|
+
const unmatched = !exact && !ancestor && !descendant;
|
|
432
|
+
if (descendant) {
|
|
433
|
+
const descendantParams = {};
|
|
434
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
435
|
+
const defPart = defParts[i];
|
|
436
|
+
const pathPart = pathParts[i];
|
|
437
|
+
if (!defPart || !pathPart) continue;
|
|
438
|
+
if (defPart.startsWith(":")) {
|
|
439
|
+
descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
|
|
440
|
+
} else if (defPart.includes("*")) {
|
|
441
|
+
descendantParams["*"] = decodeURIComponent(pathPart);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
location.params = descendantParams;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
...location,
|
|
448
|
+
known: true,
|
|
449
|
+
exact,
|
|
450
|
+
ancestor,
|
|
451
|
+
descendant,
|
|
452
|
+
unmatched
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
_validateParamsInput(input) {
|
|
456
|
+
const paramsEntries = Object.entries(this.params);
|
|
457
|
+
const paramsMap = this.params;
|
|
458
|
+
const requiredParamsKeys = paramsEntries.filter(([, required]) => required).map(([k]) => k);
|
|
459
|
+
const paramsKeys = paramsEntries.map(([k]) => k);
|
|
460
|
+
if (input === void 0) {
|
|
461
|
+
if (requiredParamsKeys.length) {
|
|
462
|
+
return {
|
|
463
|
+
issues: [
|
|
464
|
+
{
|
|
465
|
+
message: `Missing params: ${requiredParamsKeys.map((k) => `"${k}"`).join(", ")}`
|
|
466
|
+
}
|
|
467
|
+
]
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
input = {};
|
|
471
|
+
}
|
|
472
|
+
if (typeof input !== "object" || input === null) {
|
|
473
|
+
return {
|
|
474
|
+
issues: [{ message: "Invalid route params: expected object" }]
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const inputObj = input;
|
|
478
|
+
const inputKeys = Object.keys(inputObj);
|
|
479
|
+
const notDefinedKeys = requiredParamsKeys.filter((k) => !inputKeys.includes(k));
|
|
480
|
+
if (notDefinedKeys.length) {
|
|
481
|
+
return {
|
|
482
|
+
issues: [
|
|
483
|
+
{
|
|
484
|
+
message: `Missing params: ${notDefinedKeys.map((k) => `"${k}"`).join(", ")}`
|
|
485
|
+
}
|
|
486
|
+
]
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const data = {};
|
|
490
|
+
for (const k of paramsKeys) {
|
|
491
|
+
const v = inputObj[k];
|
|
492
|
+
const required = paramsMap[k];
|
|
493
|
+
if (v === void 0 && !required) {
|
|
494
|
+
data[k] = void 0;
|
|
495
|
+
} else if (typeof v === "string") {
|
|
496
|
+
data[k] = v;
|
|
497
|
+
} else if (typeof v === "number") {
|
|
498
|
+
data[k] = String(v);
|
|
499
|
+
} else {
|
|
500
|
+
return {
|
|
501
|
+
issues: [{ message: `Invalid route params: expected string, number, got ${typeof v} for "${k}"` }]
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
value: data
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
_safeParseSchemaResult(result) {
|
|
510
|
+
if ("issues" in result) {
|
|
511
|
+
return {
|
|
512
|
+
success: false,
|
|
513
|
+
data: void 0,
|
|
514
|
+
error: new Error(result.issues?.[0]?.message ?? "Invalid input")
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
success: true,
|
|
519
|
+
data: result.value,
|
|
520
|
+
error: void 0
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
_parseSchemaResult(result) {
|
|
524
|
+
const safeResult = this._safeParseSchemaResult(result);
|
|
525
|
+
if (safeResult.error) {
|
|
526
|
+
throw safeResult.error;
|
|
527
|
+
}
|
|
528
|
+
return safeResult.data;
|
|
529
|
+
}
|
|
530
|
+
/** Standard Schema for route params input. */
|
|
531
|
+
schema = {
|
|
532
|
+
"~standard": {
|
|
533
|
+
version: 1,
|
|
534
|
+
vendor: "route0",
|
|
535
|
+
validate: (value) => this._validateParamsInput(value),
|
|
536
|
+
types: void 0
|
|
537
|
+
},
|
|
538
|
+
parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
|
|
539
|
+
safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
|
|
540
|
+
};
|
|
541
|
+
/** True when path structure is equal (param names are ignored). */
|
|
542
|
+
isSame(other) {
|
|
543
|
+
return getPathTokens(this.definition).map((t) => {
|
|
544
|
+
if (t.kind === "static") return `s:${t.value}`;
|
|
545
|
+
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
546
|
+
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
547
|
+
}).join("/") === getPathTokens(other.definition).map((t) => {
|
|
548
|
+
if (t.kind === "static") return `s:${t.value}`;
|
|
549
|
+
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
550
|
+
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
551
|
+
}).join("/");
|
|
552
|
+
}
|
|
553
|
+
/** Static convenience wrapper for `isSame`. */
|
|
554
|
+
static isSame(a, b) {
|
|
555
|
+
if (!a) {
|
|
556
|
+
if (!b) return true;
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
if (!b) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
return Route0.create(a).isSame(Route0.create(b));
|
|
563
|
+
}
|
|
564
|
+
/** True when current route is more specific/deeper than `other`. */
|
|
565
|
+
isDescendant(other) {
|
|
566
|
+
if (!other) return false;
|
|
567
|
+
other = Route0.create(other);
|
|
568
|
+
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
569
|
+
if (other.definition === "/" && this.definition !== "/") {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
const thisParts = getParts(this.definition);
|
|
573
|
+
const otherParts = getParts(other.definition);
|
|
574
|
+
if (thisParts.length <= otherParts.length) return false;
|
|
575
|
+
for (let i = 0; i < otherParts.length; i++) {
|
|
576
|
+
const otherPart = otherParts[i];
|
|
577
|
+
const thisPart = thisParts[i];
|
|
578
|
+
if (otherPart.startsWith(":")) continue;
|
|
579
|
+
if (otherPart !== thisPart) return false;
|
|
580
|
+
}
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
/** True when current route is broader/shallower than `other`. */
|
|
584
|
+
isAncestor(other) {
|
|
585
|
+
if (!other) return false;
|
|
586
|
+
other = Route0.create(other);
|
|
587
|
+
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
588
|
+
if (this.definition === "/" && other.definition !== "/") {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
const thisParts = getParts(this.definition);
|
|
592
|
+
const otherParts = getParts(other.definition);
|
|
593
|
+
if (thisParts.length >= otherParts.length) return false;
|
|
594
|
+
for (let i = 0; i < thisParts.length; i++) {
|
|
595
|
+
const thisPart = thisParts[i];
|
|
596
|
+
const otherPart = otherParts[i];
|
|
597
|
+
if (thisPart.startsWith(":")) continue;
|
|
598
|
+
if (thisPart !== otherPart) return false;
|
|
599
|
+
}
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
/** True when two route patterns can match the same concrete URL. */
|
|
603
|
+
isConflict(other) {
|
|
604
|
+
if (!other) return false;
|
|
605
|
+
other = Route0.create(other);
|
|
606
|
+
const thisRegex = this.getRegex();
|
|
607
|
+
const otherRegex = other.getRegex();
|
|
608
|
+
const makeCandidates = (definition) => {
|
|
609
|
+
const tokens = getPathTokens(definition);
|
|
610
|
+
const values = (token) => {
|
|
611
|
+
if (token.kind === "static") return [token.value];
|
|
612
|
+
if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
|
|
613
|
+
if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
|
|
614
|
+
return ["", "x", "x/y"];
|
|
615
|
+
};
|
|
616
|
+
let acc = [""];
|
|
617
|
+
for (const token of tokens) {
|
|
618
|
+
const next = [];
|
|
619
|
+
for (const base of acc) {
|
|
620
|
+
for (const value of values(token)) {
|
|
621
|
+
if (value === "") {
|
|
622
|
+
next.push(base);
|
|
623
|
+
} else if (value.startsWith("/")) {
|
|
624
|
+
next.push(`${base}${value}`);
|
|
625
|
+
} else {
|
|
626
|
+
next.push(`${base}/${value}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
acc = next;
|
|
631
|
+
}
|
|
632
|
+
if (acc.length === 0) return ["/"];
|
|
633
|
+
return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
|
|
634
|
+
};
|
|
635
|
+
const thisCandidates = makeCandidates(this.definition);
|
|
636
|
+
const otherCandidates = makeCandidates(other.definition);
|
|
637
|
+
if (thisCandidates.some((path) => otherRegex.test(path))) return true;
|
|
638
|
+
if (otherCandidates.some((path) => thisRegex.test(path))) return true;
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
/** True when paths are same or can overlap when optional parts are omitted. */
|
|
642
|
+
isMayBeSame(other) {
|
|
643
|
+
if (!other) return false;
|
|
644
|
+
other = Route0.create(other);
|
|
645
|
+
return this.isSame(other) || this.isConflict(other);
|
|
646
|
+
}
|
|
647
|
+
/** Specificity comparator used for deterministic route ordering. */
|
|
648
|
+
isMoreSpecificThan(other) {
|
|
649
|
+
if (!other) return false;
|
|
650
|
+
other = Route0.create(other);
|
|
651
|
+
const getParts = (path) => {
|
|
652
|
+
if (path === "/") return ["/"];
|
|
653
|
+
return path.split("/").filter(Boolean);
|
|
654
|
+
};
|
|
655
|
+
const rank = (part) => {
|
|
656
|
+
if (part.includes("*")) return -1;
|
|
657
|
+
if (part.startsWith(":") && part.endsWith("?")) return 0;
|
|
658
|
+
if (part.startsWith(":")) return 1;
|
|
659
|
+
return 2;
|
|
660
|
+
};
|
|
661
|
+
const thisParts = getParts(this.definition);
|
|
662
|
+
const otherParts = getParts(other.definition);
|
|
663
|
+
for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
|
|
664
|
+
const thisRank = rank(thisParts[i]);
|
|
665
|
+
const otherRank = rank(otherParts[i]);
|
|
666
|
+
if (thisRank > otherRank) return true;
|
|
667
|
+
if (thisRank < otherRank) return false;
|
|
668
|
+
}
|
|
669
|
+
return this.definition < other.definition;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
class Routes {
|
|
673
|
+
_routes;
|
|
674
|
+
_pathsOrdering;
|
|
675
|
+
_keysOrdering;
|
|
676
|
+
_ordered;
|
|
677
|
+
_;
|
|
678
|
+
constructor({
|
|
679
|
+
routes,
|
|
680
|
+
isHydrated = false,
|
|
681
|
+
pathsOrdering,
|
|
682
|
+
keysOrdering,
|
|
683
|
+
ordered
|
|
684
|
+
}) {
|
|
685
|
+
this._routes = isHydrated ? routes : Routes.hydrate(routes);
|
|
686
|
+
if (!pathsOrdering || !keysOrdering || !ordered) {
|
|
687
|
+
const ordering = Routes.makeOrdering(this._routes);
|
|
688
|
+
this._pathsOrdering = ordering.pathsOrdering;
|
|
689
|
+
this._keysOrdering = ordering.keysOrdering;
|
|
690
|
+
this._ordered = this._keysOrdering.map((key) => this._routes[key]);
|
|
691
|
+
} else {
|
|
692
|
+
this._pathsOrdering = pathsOrdering;
|
|
693
|
+
this._keysOrdering = keysOrdering;
|
|
694
|
+
this._ordered = ordered;
|
|
695
|
+
}
|
|
696
|
+
this._ = {
|
|
697
|
+
routes: this._routes,
|
|
698
|
+
getLocation: this._getLocation.bind(this),
|
|
699
|
+
clone: this._clone.bind(this),
|
|
700
|
+
pathsOrdering: this._pathsOrdering,
|
|
701
|
+
keysOrdering: this._keysOrdering,
|
|
702
|
+
ordered: this._ordered
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/** Creates and hydrates a typed routes collection. */
|
|
706
|
+
static create(routes, override) {
|
|
707
|
+
const result = Routes.prettify(new Routes({ routes }));
|
|
708
|
+
if (!override) {
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
return result._.clone(override);
|
|
712
|
+
}
|
|
713
|
+
static prettify(instance) {
|
|
714
|
+
Object.setPrototypeOf(instance, Routes.prototype);
|
|
715
|
+
Object.defineProperty(instance, Symbol.toStringTag, {
|
|
716
|
+
value: "Routes"
|
|
717
|
+
});
|
|
718
|
+
Object.assign(instance, {
|
|
719
|
+
clone: instance._clone.bind(instance)
|
|
720
|
+
});
|
|
721
|
+
Object.assign(instance, instance._routes);
|
|
722
|
+
return instance;
|
|
723
|
+
}
|
|
724
|
+
static hydrate(routes) {
|
|
725
|
+
const result = {};
|
|
726
|
+
for (const key in routes) {
|
|
727
|
+
if (Object.hasOwn(routes, key)) {
|
|
728
|
+
const value = routes[key];
|
|
729
|
+
result[key] = typeof value === "string" ? Route0.create(value) : value;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
_getLocation(hrefOrHrefRelOrLocation) {
|
|
735
|
+
const input = hrefOrHrefRelOrLocation;
|
|
736
|
+
for (const route of this._ordered) {
|
|
737
|
+
const loc = route.getLocation(hrefOrHrefRelOrLocation);
|
|
738
|
+
if (loc.exact) {
|
|
739
|
+
return loc;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return typeof input === "string" ? Route0.getLocation(input) : Route0.getLocation(input);
|
|
743
|
+
}
|
|
744
|
+
static makeOrdering(routes) {
|
|
745
|
+
const hydrated = Routes.hydrate(routes);
|
|
746
|
+
const entries = Object.entries(hydrated);
|
|
747
|
+
const getParts = (path) => {
|
|
748
|
+
if (path === "/") return ["/"];
|
|
749
|
+
return path.split("/").filter(Boolean);
|
|
750
|
+
};
|
|
751
|
+
entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
|
|
752
|
+
const partsA = getParts(routeA.definition);
|
|
753
|
+
const partsB = getParts(routeB.definition);
|
|
754
|
+
if (routeA.isMayBeSame(routeB)) {
|
|
755
|
+
if (routeA.isMoreSpecificThan(routeB)) return -1;
|
|
756
|
+
if (routeB.isMoreSpecificThan(routeA)) return 1;
|
|
757
|
+
}
|
|
758
|
+
if (partsA.length !== partsB.length) {
|
|
759
|
+
return partsA.length - partsB.length;
|
|
760
|
+
}
|
|
761
|
+
return routeA.definition.localeCompare(routeB.definition);
|
|
762
|
+
});
|
|
763
|
+
const pathsOrdering = entries.map(([_key, route]) => route.definition);
|
|
764
|
+
const keysOrdering = entries.map(([_key]) => _key);
|
|
765
|
+
return { pathsOrdering, keysOrdering };
|
|
766
|
+
}
|
|
767
|
+
/** Returns a cloned routes collection with config applied to each route. */
|
|
768
|
+
_clone(config) {
|
|
769
|
+
const newRoutes = {};
|
|
770
|
+
for (const key in this._routes) {
|
|
771
|
+
if (Object.hasOwn(this._routes, key)) {
|
|
772
|
+
newRoutes[key] = this._routes[key].clone(config);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const instance = new Routes({
|
|
776
|
+
routes: newRoutes,
|
|
777
|
+
isHydrated: true,
|
|
778
|
+
pathsOrdering: this._pathsOrdering,
|
|
779
|
+
keysOrdering: this._keysOrdering,
|
|
780
|
+
ordered: this._keysOrdering.map((key) => newRoutes[key])
|
|
781
|
+
});
|
|
782
|
+
return Routes.prettify(instance);
|
|
783
|
+
}
|
|
784
|
+
static _ = {
|
|
785
|
+
prettify: Routes.prettify.bind(Routes),
|
|
786
|
+
hydrate: Routes.hydrate.bind(Routes),
|
|
787
|
+
makeOrdering: Routes.makeOrdering.bind(Routes)
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
791
|
+
0 && (module.exports = {
|
|
792
|
+
Route0,
|
|
793
|
+
Routes
|
|
794
|
+
});
|
|
795
|
+
//# sourceMappingURL=index.cjs.map
|