@devp0nt/route0 1.0.0-next.67 → 1.0.0-next.69
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 +261 -315
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +105 -189
- package/dist/esm/index.d.ts +105 -189
- package/dist/esm/index.js +261 -315
- package/dist/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.test.ts +0 -2320
- package/src/index.ts +0 -1900
package/dist/esm/index.js
CHANGED
|
@@ -1,10 +1,82 @@
|
|
|
1
|
+
import { parse as parseSearchQuery, stringify as stringifySearchQuery } from "@devp0nt/flat0";
|
|
2
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
+
const getPathSegments = (definition) => {
|
|
4
|
+
if (definition === "" || definition === "/") return [];
|
|
5
|
+
return definition.split("/").filter(Boolean);
|
|
6
|
+
};
|
|
7
|
+
const getPathTokens = (definition) => {
|
|
8
|
+
const segments = getPathSegments(definition);
|
|
9
|
+
return segments.map((segment) => {
|
|
10
|
+
const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
|
|
11
|
+
if (param) {
|
|
12
|
+
return { kind: "param", name: param[1], optional: param[2] === "?" };
|
|
13
|
+
}
|
|
14
|
+
if (segment === "*" || segment === "*?") {
|
|
15
|
+
return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
|
|
16
|
+
}
|
|
17
|
+
const wildcard = segment.match(/^(.*)\*(\?)?$/);
|
|
18
|
+
if (wildcard && !segment.includes("\\*")) {
|
|
19
|
+
return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
|
|
20
|
+
}
|
|
21
|
+
return { kind: "static", value: segment };
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const getPathRegexBaseStrictString = (definition) => {
|
|
25
|
+
const tokens = getPathTokens(definition);
|
|
26
|
+
if (tokens.length === 0) return "";
|
|
27
|
+
let pattern = "";
|
|
28
|
+
for (const token of tokens) {
|
|
29
|
+
if (token.kind === "static") {
|
|
30
|
+
pattern += `/${escapeRegex(token.value)}`;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (token.kind === "param") {
|
|
34
|
+
pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (token.prefix.length > 0) {
|
|
38
|
+
pattern += `/${escapeRegex(token.prefix)}(.*)`;
|
|
39
|
+
} else {
|
|
40
|
+
pattern += "(?:/(.*))?";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return pattern;
|
|
44
|
+
};
|
|
45
|
+
const getPathCaptureKeys = (definition) => {
|
|
46
|
+
const keys = [];
|
|
47
|
+
for (const token of getPathTokens(definition)) {
|
|
48
|
+
if (token.kind === "param") keys.push(token.name);
|
|
49
|
+
if (token.kind === "wildcard") keys.push("*");
|
|
50
|
+
}
|
|
51
|
+
return keys;
|
|
52
|
+
};
|
|
53
|
+
const getPathParamsDefinition = (definition) => {
|
|
54
|
+
const entries = getPathTokens(definition).filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
|
|
55
|
+
return Object.fromEntries(entries);
|
|
56
|
+
};
|
|
57
|
+
const validatePathDefinition = (definition) => {
|
|
58
|
+
const segments = getPathSegments(definition);
|
|
59
|
+
const wildcardSegments = segments.filter((segment) => segment.includes("*"));
|
|
60
|
+
if (wildcardSegments.length === 0) return;
|
|
61
|
+
if (wildcardSegments.length > 1) {
|
|
62
|
+
throw new Error(`Invalid route definition "${definition}": only one wildcard segment is allowed`);
|
|
63
|
+
}
|
|
64
|
+
const wildcardSegmentIndex = segments.findIndex((segment) => segment.includes("*"));
|
|
65
|
+
const wildcardSegment = segments[wildcardSegmentIndex];
|
|
66
|
+
if (!wildcardSegment.match(/^(?:\*|\*\?|[^*]+\*|\S+\*\?)$/)) {
|
|
67
|
+
throw new Error(`Invalid route definition "${definition}": wildcard must be trailing in its segment`);
|
|
68
|
+
}
|
|
69
|
+
if (wildcardSegmentIndex !== segments.length - 1) {
|
|
70
|
+
throw new Error(`Invalid route definition "${definition}": wildcard segment is allowed only at the end`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const stripTrailingWildcard = (definition) => definition.replace(/\*\??$/, "");
|
|
1
74
|
class Route0 {
|
|
2
75
|
definition;
|
|
3
|
-
|
|
4
|
-
paramsDefinition;
|
|
5
|
-
searchDefinition;
|
|
6
|
-
hasLooseSearch;
|
|
76
|
+
params;
|
|
7
77
|
_origin;
|
|
78
|
+
_callable;
|
|
79
|
+
Infer = null;
|
|
8
80
|
/** Base URL used when generating absolute URLs (`abs: true`). */
|
|
9
81
|
get origin() {
|
|
10
82
|
if (!this._origin) {
|
|
@@ -18,11 +90,9 @@ class Route0 {
|
|
|
18
90
|
this._origin = origin;
|
|
19
91
|
}
|
|
20
92
|
constructor(definition, config = {}) {
|
|
93
|
+
validatePathDefinition(definition);
|
|
21
94
|
this.definition = definition;
|
|
22
|
-
this.
|
|
23
|
-
this.paramsDefinition = Route0._getParamsDefinitionBydefinition(definition);
|
|
24
|
-
this.searchDefinition = Route0._getSearchDefinitionBydefinition(definition);
|
|
25
|
-
this.hasLooseSearch = Route0._hasLooseSearch(definition);
|
|
95
|
+
this.params = Route0._getParamsDefinitionByDefinition(definition);
|
|
26
96
|
const { origin } = config;
|
|
27
97
|
if (origin && typeof origin === "string" && origin.length) {
|
|
28
98
|
this._origin = origin;
|
|
@@ -34,6 +104,12 @@ class Route0 {
|
|
|
34
104
|
this._origin = void 0;
|
|
35
105
|
}
|
|
36
106
|
}
|
|
107
|
+
const callable = this.get.bind(this);
|
|
108
|
+
Object.setPrototypeOf(callable, this);
|
|
109
|
+
Object.defineProperty(callable, Symbol.toStringTag, {
|
|
110
|
+
value: this.definition
|
|
111
|
+
});
|
|
112
|
+
this._callable = callable;
|
|
37
113
|
}
|
|
38
114
|
/**
|
|
39
115
|
* Creates a callable route instance.
|
|
@@ -41,19 +117,11 @@ class Route0 {
|
|
|
41
117
|
* If an existing route/callable route is provided, it is cloned.
|
|
42
118
|
*/
|
|
43
119
|
static create(definition, config) {
|
|
44
|
-
if (typeof definition === "function") {
|
|
45
|
-
return definition.clone(config);
|
|
46
|
-
}
|
|
47
|
-
if (typeof definition === "object") {
|
|
120
|
+
if (typeof definition === "function" || typeof definition === "object") {
|
|
48
121
|
return definition.clone(config);
|
|
49
122
|
}
|
|
50
123
|
const original = new Route0(definition, config);
|
|
51
|
-
|
|
52
|
-
Object.setPrototypeOf(callable, original);
|
|
53
|
-
Object.defineProperty(callable, Symbol.toStringTag, {
|
|
54
|
-
value: original.definition
|
|
55
|
-
});
|
|
56
|
-
return callable;
|
|
124
|
+
return original._callable;
|
|
57
125
|
}
|
|
58
126
|
/**
|
|
59
127
|
* Normalizes a definition/route into a callable route.
|
|
@@ -65,61 +133,25 @@ class Route0 {
|
|
|
65
133
|
return definition;
|
|
66
134
|
}
|
|
67
135
|
const original = typeof definition === "object" ? definition : new Route0(definition);
|
|
68
|
-
|
|
69
|
-
Object.setPrototypeOf(callable, original);
|
|
70
|
-
Object.defineProperty(callable, Symbol.toStringTag, {
|
|
71
|
-
value: original.definition
|
|
72
|
-
});
|
|
73
|
-
return callable;
|
|
74
|
-
}
|
|
75
|
-
static _splitPathDefinitionAndSearchTailDefinition(definition) {
|
|
76
|
-
const i = definition.indexOf("&");
|
|
77
|
-
if (i === -1) return { pathDefinition: definition, searchTailDefinition: "" };
|
|
78
|
-
return {
|
|
79
|
-
pathDefinition: definition.slice(0, i),
|
|
80
|
-
searchTailDefinition: definition.slice(i)
|
|
81
|
-
};
|
|
136
|
+
return original._callable;
|
|
82
137
|
}
|
|
83
|
-
static _getAbsPath(origin,
|
|
84
|
-
return new URL(
|
|
138
|
+
static _getAbsPath(origin, url) {
|
|
139
|
+
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
85
140
|
}
|
|
86
|
-
static
|
|
87
|
-
|
|
88
|
-
return pathDefinition;
|
|
89
|
-
}
|
|
90
|
-
static _getParamsDefinitionBydefinition(definition) {
|
|
91
|
-
const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
|
|
92
|
-
const matches = Array.from(pathDefinition.matchAll(/:([A-Za-z0-9_]+)/g));
|
|
93
|
-
const paramsDefinition = Object.fromEntries(matches.map((m) => [m[1], true]));
|
|
94
|
-
const keysCount = Object.keys(paramsDefinition).length;
|
|
95
|
-
if (keysCount === 0) {
|
|
96
|
-
return void 0;
|
|
97
|
-
}
|
|
98
|
-
return paramsDefinition;
|
|
99
|
-
}
|
|
100
|
-
static _getSearchDefinitionBydefinition(definition) {
|
|
101
|
-
const { searchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
|
|
102
|
-
if (!searchTailDefinition) {
|
|
103
|
-
return void 0;
|
|
104
|
-
}
|
|
105
|
-
const keys = searchTailDefinition.split("&").filter(Boolean);
|
|
106
|
-
const searchDefinition = Object.fromEntries(keys.map((k) => [k, true]));
|
|
107
|
-
const keysCount = Object.keys(searchDefinition).length;
|
|
108
|
-
if (keysCount === 0) {
|
|
109
|
-
return void 0;
|
|
110
|
-
}
|
|
111
|
-
return searchDefinition;
|
|
141
|
+
static _getParamsDefinitionByDefinition(definition) {
|
|
142
|
+
return getPathParamsDefinition(definition);
|
|
112
143
|
}
|
|
113
|
-
|
|
114
|
-
return
|
|
144
|
+
search() {
|
|
145
|
+
return this._callable;
|
|
115
146
|
}
|
|
116
147
|
/** Extends the current route definition by appending a suffix route. */
|
|
117
148
|
extend(suffixDefinition) {
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
149
|
+
const sourceDefinitionWithoutWildcard = stripTrailingWildcard(this.definition);
|
|
150
|
+
const definition = `${sourceDefinitionWithoutWildcard}/${suffixDefinition}`.replace(/\/{2,}/g, "/");
|
|
151
|
+
return Route0.create(
|
|
152
|
+
definition,
|
|
153
|
+
{ origin: this._origin }
|
|
154
|
+
);
|
|
123
155
|
}
|
|
124
156
|
// implementation
|
|
125
157
|
get(...args) {
|
|
@@ -133,37 +165,63 @@ class Route0 {
|
|
|
133
165
|
hashInput: void 0
|
|
134
166
|
};
|
|
135
167
|
}
|
|
136
|
-
const input =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
const [input, abs] = (() => {
|
|
169
|
+
if (typeof args[0] === "object" && args[0] !== null) {
|
|
170
|
+
return [args[0], args[1]];
|
|
171
|
+
}
|
|
172
|
+
if (typeof args[1] === "object" && args[1] !== null) {
|
|
173
|
+
return [args[1], args[0]];
|
|
174
|
+
}
|
|
175
|
+
if (typeof args[0] === "boolean" || typeof args[0] === "string") {
|
|
176
|
+
return [{}, args[0]];
|
|
177
|
+
}
|
|
178
|
+
if (typeof args[1] === "boolean" || typeof args[1] === "string") {
|
|
179
|
+
return [{}, args[1]];
|
|
180
|
+
}
|
|
181
|
+
return [{}, void 0];
|
|
182
|
+
})();
|
|
183
|
+
let searchInput2 = {};
|
|
184
|
+
let hashInput2 = void 0;
|
|
185
|
+
const paramsInput2 = {};
|
|
186
|
+
for (const [key, value] of Object.entries(input)) {
|
|
187
|
+
if (key === "?" && typeof value === "object" && value !== null) {
|
|
188
|
+
searchInput2 = value;
|
|
189
|
+
} else if (key === "#" && (typeof value === "string" || typeof value === "number")) {
|
|
190
|
+
hashInput2 = String(value);
|
|
191
|
+
} else if (key in this.params && (typeof value === "string" || typeof value === "number")) {
|
|
192
|
+
Object.assign(paramsInput2, { [key]: String(value) });
|
|
193
|
+
}
|
|
145
194
|
}
|
|
146
|
-
const { search, abs, hash, ...params } = input;
|
|
147
195
|
const absOriginInput2 = typeof abs === "string" && abs.length > 0 ? abs : void 0;
|
|
148
196
|
return {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
paramsInput: params,
|
|
197
|
+
searchInput: searchInput2,
|
|
198
|
+
paramsInput: paramsInput2,
|
|
152
199
|
absInput: absOriginInput2 !== void 0 || abs === true,
|
|
153
200
|
absOriginInput: absOriginInput2,
|
|
154
|
-
hashInput:
|
|
201
|
+
hashInput: hashInput2
|
|
155
202
|
};
|
|
156
203
|
})();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
url = url.replace(
|
|
165
|
-
|
|
166
|
-
|
|
204
|
+
let url = this.definition;
|
|
205
|
+
url = url.replace(/\/:([A-Za-z0-9_]+)\?/g, (_m, k) => {
|
|
206
|
+
const value = paramsInput[k];
|
|
207
|
+
if (value === void 0) return "";
|
|
208
|
+
return `/${encodeURIComponent(String(value))}`;
|
|
209
|
+
});
|
|
210
|
+
url = url.replace(/:([A-Za-z0-9_]+)(?!\?)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "undefined")));
|
|
211
|
+
url = url.replace(/\/\*\?/g, () => {
|
|
212
|
+
const value = paramsInput["*"];
|
|
213
|
+
if (value === void 0) return "";
|
|
214
|
+
const stringValue = String(value);
|
|
215
|
+
return stringValue.startsWith("/") ? stringValue : `/${stringValue}`;
|
|
216
|
+
});
|
|
217
|
+
url = url.replace(/\/\*/g, () => {
|
|
218
|
+
const value = String(paramsInput["*"] ?? "");
|
|
219
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
220
|
+
});
|
|
221
|
+
url = url.replace(/\*\?/g, () => String(paramsInput["*"] ?? ""));
|
|
222
|
+
url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
|
|
223
|
+
const searchString = stringifySearchQuery(searchInput);
|
|
224
|
+
url = [url, searchString].filter(Boolean).join("?");
|
|
167
225
|
url = url.replace(/\/{2,}/g, "/");
|
|
168
226
|
url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
|
|
169
227
|
if (hashInput !== void 0) {
|
|
@@ -171,92 +229,19 @@ class Route0 {
|
|
|
171
229
|
}
|
|
172
230
|
return url;
|
|
173
231
|
}
|
|
174
|
-
// implementation
|
|
175
|
-
flat(...args) {
|
|
176
|
-
const { searchInput, paramsInput, absInput, hashInput } = (() => {
|
|
177
|
-
if (args.length === 0) {
|
|
178
|
-
return {
|
|
179
|
-
searchInput: {},
|
|
180
|
-
paramsInput: {},
|
|
181
|
-
absInput: false,
|
|
182
|
-
hashInput: void 0
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
const input = args[0];
|
|
186
|
-
if (typeof input !== "object" || input === null) {
|
|
187
|
-
return {
|
|
188
|
-
searchInput: {},
|
|
189
|
-
paramsInput: {},
|
|
190
|
-
absInput: args[1] ?? false,
|
|
191
|
-
hashInput: void 0
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
const loose = args[2] ?? this.hasLooseSearch;
|
|
195
|
-
const paramsKeys = this.getParamsKeys();
|
|
196
|
-
const paramsInput2 = paramsKeys.reduce((acc, key) => {
|
|
197
|
-
if (input[key] !== void 0) {
|
|
198
|
-
acc[key] = input[key];
|
|
199
|
-
}
|
|
200
|
-
return acc;
|
|
201
|
-
}, {});
|
|
202
|
-
const searchKeys = this.getSearchKeys();
|
|
203
|
-
const searchInput2 = Object.keys(input).filter((k) => {
|
|
204
|
-
if (k === "hash") {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
if (searchKeys.includes(k)) {
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
if (paramsKeys.includes(k)) {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
return loose;
|
|
214
|
-
}).reduce((acc, key) => {
|
|
215
|
-
acc[key] = input[key];
|
|
216
|
-
return acc;
|
|
217
|
-
}, {});
|
|
218
|
-
const hashInput2 = input.hash;
|
|
219
|
-
return {
|
|
220
|
-
searchInput: searchInput2,
|
|
221
|
-
paramsInput: paramsInput2,
|
|
222
|
-
absInput: args[1] ?? false,
|
|
223
|
-
hashInput: hashInput2
|
|
224
|
-
};
|
|
225
|
-
})();
|
|
226
|
-
return this.get({
|
|
227
|
-
...paramsInput,
|
|
228
|
-
search: searchInput,
|
|
229
|
-
abs: absInput,
|
|
230
|
-
hash: hashInput
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
flatLoose(...args) {
|
|
234
|
-
return this.flat(args[0], args[1], true);
|
|
235
|
-
}
|
|
236
|
-
flatStrict(...args) {
|
|
237
|
-
return this.flat(args[0], args[1], false);
|
|
238
|
-
}
|
|
239
232
|
/** Returns path param keys extracted from route definition. */
|
|
240
233
|
getParamsKeys() {
|
|
241
|
-
return Object.keys(this.
|
|
242
|
-
}
|
|
243
|
-
/** Returns named search keys extracted from route definition. */
|
|
244
|
-
getSearchKeys() {
|
|
245
|
-
return Object.keys(this.searchDefinition || {});
|
|
234
|
+
return Object.keys(this.params);
|
|
246
235
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return [...this.getSearchKeys(), ...this.getParamsKeys()];
|
|
250
|
-
}
|
|
251
|
-
getDefinition() {
|
|
252
|
-
return this.pathDefinition;
|
|
236
|
+
getPathTokens() {
|
|
237
|
+
return getPathTokens(this.definition);
|
|
253
238
|
}
|
|
254
239
|
/** Clones route with optional config override. */
|
|
255
240
|
clone(config) {
|
|
256
241
|
return Route0.create(this.definition, config);
|
|
257
242
|
}
|
|
258
243
|
getRegexBaseStrictString() {
|
|
259
|
-
return this.
|
|
244
|
+
return getPathRegexBaseStrictString(this.definition);
|
|
260
245
|
}
|
|
261
246
|
getRegexBaseString() {
|
|
262
247
|
return this.getRegexBaseStrictString().replace(/\/+$/, "") + "/?";
|
|
@@ -329,7 +314,7 @@ class Route0 {
|
|
|
329
314
|
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
330
315
|
const base = abs ? void 0 : "http://example.com";
|
|
331
316
|
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
332
|
-
const
|
|
317
|
+
const search = parseSearchQuery(url.search);
|
|
333
318
|
let pathname = url.pathname;
|
|
334
319
|
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
335
320
|
pathname = pathname.slice(0, -1);
|
|
@@ -337,7 +322,8 @@ class Route0 {
|
|
|
337
322
|
const hrefRel = pathname + url.search + url.hash;
|
|
338
323
|
const location = {
|
|
339
324
|
pathname,
|
|
340
|
-
search
|
|
325
|
+
search,
|
|
326
|
+
searchString: url.search,
|
|
341
327
|
hash: url.hash,
|
|
342
328
|
origin: abs ? url.origin : void 0,
|
|
343
329
|
href: abs ? url.href : void 0,
|
|
@@ -348,7 +334,6 @@ class Route0 {
|
|
|
348
334
|
hostname: abs ? url.hostname : void 0,
|
|
349
335
|
port: abs ? url.port || void 0 : void 0,
|
|
350
336
|
// specific to UnknownLocation
|
|
351
|
-
searchParams,
|
|
352
337
|
params: void 0,
|
|
353
338
|
route: void 0,
|
|
354
339
|
known: false,
|
|
@@ -370,12 +355,8 @@ class Route0 {
|
|
|
370
355
|
location.route = this.definition;
|
|
371
356
|
location.params = {};
|
|
372
357
|
const pathname = location.pathname.length > 1 && location.pathname.endsWith("/") ? location.pathname.slice(0, -1) : location.pathname;
|
|
373
|
-
const paramNames =
|
|
374
|
-
const def = this.
|
|
375
|
-
def.replace(/:([A-Za-z0-9_]+)/g, (_m, name) => {
|
|
376
|
-
paramNames.push(String(name));
|
|
377
|
-
return "";
|
|
378
|
-
});
|
|
358
|
+
const paramNames = getPathCaptureKeys(this.definition);
|
|
359
|
+
const def = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
|
|
379
360
|
const exactRe = new RegExp(`^${this.getRegexBaseString()}$`);
|
|
380
361
|
const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`);
|
|
381
362
|
const exactMatch = pathname.match(exactRe);
|
|
@@ -385,7 +366,12 @@ class Route0 {
|
|
|
385
366
|
const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
|
|
386
367
|
if (paramsMatch) {
|
|
387
368
|
const values = paramsMatch.slice(1, 1 + paramNames.length);
|
|
388
|
-
const params = Object.fromEntries(
|
|
369
|
+
const params = Object.fromEntries(
|
|
370
|
+
paramNames.map((n, i) => {
|
|
371
|
+
const value = values[i];
|
|
372
|
+
return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
|
|
373
|
+
})
|
|
374
|
+
);
|
|
389
375
|
location.params = params;
|
|
390
376
|
} else {
|
|
391
377
|
location.params = {};
|
|
@@ -405,6 +391,12 @@ class Route0 {
|
|
|
405
391
|
break;
|
|
406
392
|
}
|
|
407
393
|
if (defPart.startsWith(":")) continue;
|
|
394
|
+
if (defPart.includes("*")) {
|
|
395
|
+
const prefix = defPart.replace(/\*\??$/, "");
|
|
396
|
+
if (pathPart.startsWith(prefix)) continue;
|
|
397
|
+
isPrefix = false;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
408
400
|
if (defPart !== pathPart) {
|
|
409
401
|
isPrefix = false;
|
|
410
402
|
break;
|
|
@@ -420,7 +412,9 @@ class Route0 {
|
|
|
420
412
|
const pathPart = pathParts[i];
|
|
421
413
|
if (!defPart || !pathPart) continue;
|
|
422
414
|
if (defPart.startsWith(":")) {
|
|
423
|
-
descendantParams[defPart.
|
|
415
|
+
descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
|
|
416
|
+
} else if (defPart.includes("*")) {
|
|
417
|
+
descendantParams["*"] = decodeURIComponent(pathPart);
|
|
424
418
|
}
|
|
425
419
|
}
|
|
426
420
|
location.params = descendantParams;
|
|
@@ -435,13 +429,16 @@ class Route0 {
|
|
|
435
429
|
};
|
|
436
430
|
}
|
|
437
431
|
_validateParamsInput(input) {
|
|
438
|
-
const
|
|
432
|
+
const paramsEntries = Object.entries(this.params);
|
|
433
|
+
const paramsMap = this.params;
|
|
434
|
+
const requiredParamsKeys = paramsEntries.filter(([, required]) => required).map(([k]) => k);
|
|
435
|
+
const paramsKeys = paramsEntries.map(([k]) => k);
|
|
439
436
|
if (input === void 0) {
|
|
440
|
-
if (
|
|
437
|
+
if (requiredParamsKeys.length) {
|
|
441
438
|
return {
|
|
442
439
|
issues: [
|
|
443
440
|
{
|
|
444
|
-
message: `Missing params: ${
|
|
441
|
+
message: `Missing params: ${requiredParamsKeys.map((k) => `"${k}"`).join(", ")}`
|
|
445
442
|
}
|
|
446
443
|
]
|
|
447
444
|
};
|
|
@@ -450,12 +447,12 @@ class Route0 {
|
|
|
450
447
|
}
|
|
451
448
|
if (typeof input !== "object" || input === null) {
|
|
452
449
|
return {
|
|
453
|
-
issues: [{ message: "Invalid
|
|
450
|
+
issues: [{ message: "Invalid route params: expected object" }]
|
|
454
451
|
};
|
|
455
452
|
}
|
|
456
453
|
const inputObj = input;
|
|
457
454
|
const inputKeys = Object.keys(inputObj);
|
|
458
|
-
const notDefinedKeys =
|
|
455
|
+
const notDefinedKeys = requiredParamsKeys.filter((k) => !inputKeys.includes(k));
|
|
459
456
|
if (notDefinedKeys.length) {
|
|
460
457
|
return {
|
|
461
458
|
issues: [
|
|
@@ -468,45 +465,16 @@ class Route0 {
|
|
|
468
465
|
const data = {};
|
|
469
466
|
for (const k of paramsKeys) {
|
|
470
467
|
const v = inputObj[k];
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
} else {
|
|
476
|
-
return {
|
|
477
|
-
issues: [{ message: `Invalid input: expected string, number, got ${typeof v} for "${k}"` }]
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return {
|
|
482
|
-
value: data
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
_validateSearchInput(input, loose) {
|
|
486
|
-
if (input === void 0) {
|
|
487
|
-
input = {};
|
|
488
|
-
}
|
|
489
|
-
if (typeof input !== "object" || input === null) {
|
|
490
|
-
return {
|
|
491
|
-
issues: [{ message: "Invalid input: expected object" }]
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
const inputObj = input;
|
|
495
|
-
const paramsKeys = this.getParamsKeys();
|
|
496
|
-
const searchKeys = this.getSearchKeys();
|
|
497
|
-
const data = {};
|
|
498
|
-
for (const [k, v] of Object.entries(inputObj)) {
|
|
499
|
-
if (k === "hash") continue;
|
|
500
|
-
if (paramsKeys.includes(k)) continue;
|
|
501
|
-
if (!loose && !searchKeys.includes(k)) continue;
|
|
502
|
-
if (v === void 0) continue;
|
|
503
|
-
if (typeof v === "string") {
|
|
468
|
+
const required = paramsMap[k];
|
|
469
|
+
if (v === void 0 && !required) {
|
|
470
|
+
data[k] = void 0;
|
|
471
|
+
} else if (typeof v === "string") {
|
|
504
472
|
data[k] = v;
|
|
505
473
|
} else if (typeof v === "number") {
|
|
506
474
|
data[k] = String(v);
|
|
507
475
|
} else {
|
|
508
476
|
return {
|
|
509
|
-
issues: [{ message: `Invalid
|
|
477
|
+
issues: [{ message: `Invalid route params: expected string, number, got ${typeof v} for "${k}"` }]
|
|
510
478
|
};
|
|
511
479
|
}
|
|
512
480
|
}
|
|
@@ -514,26 +482,6 @@ class Route0 {
|
|
|
514
482
|
value: data
|
|
515
483
|
};
|
|
516
484
|
}
|
|
517
|
-
_validateFlatInput(input, loose) {
|
|
518
|
-
const paramsResult = this._validateParamsInput(input);
|
|
519
|
-
if ("issues" in paramsResult) {
|
|
520
|
-
return {
|
|
521
|
-
issues: paramsResult.issues ?? []
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
const searchResult = this._validateSearchInput(input, loose);
|
|
525
|
-
if ("issues" in searchResult) {
|
|
526
|
-
return {
|
|
527
|
-
issues: searchResult.issues ?? []
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
return {
|
|
531
|
-
value: {
|
|
532
|
-
...searchResult.value,
|
|
533
|
-
...paramsResult.value
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
485
|
_safeParseSchemaResult(result) {
|
|
538
486
|
if ("issues" in result) {
|
|
539
487
|
return {
|
|
@@ -556,7 +504,7 @@ class Route0 {
|
|
|
556
504
|
return safeResult.data;
|
|
557
505
|
}
|
|
558
506
|
/** Standard Schema for route params input. */
|
|
559
|
-
|
|
507
|
+
paramsSchema = {
|
|
560
508
|
"~standard": {
|
|
561
509
|
version: 1,
|
|
562
510
|
vendor: "route0",
|
|
@@ -566,42 +514,17 @@ class Route0 {
|
|
|
566
514
|
parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
|
|
567
515
|
safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
|
|
568
516
|
};
|
|
569
|
-
/** Standard Schema for strict search input. */
|
|
570
|
-
strictSearchInputSchema = {
|
|
571
|
-
"~standard": {
|
|
572
|
-
version: 1,
|
|
573
|
-
vendor: "route0",
|
|
574
|
-
validate: (value) => this._validateSearchInput(value, false),
|
|
575
|
-
types: void 0
|
|
576
|
-
},
|
|
577
|
-
parse: (value) => this._parseSchemaResult(this._validateSearchInput(value, false)),
|
|
578
|
-
safeParse: (value) => this._safeParseSchemaResult(this._validateSearchInput(value, false))
|
|
579
|
-
};
|
|
580
|
-
/** Standard Schema for loose search input. */
|
|
581
|
-
looseSearchInputSchema = {
|
|
582
|
-
"~standard": {
|
|
583
|
-
version: 1,
|
|
584
|
-
vendor: "route0",
|
|
585
|
-
validate: (value) => this._validateSearchInput(value, true),
|
|
586
|
-
types: void 0
|
|
587
|
-
},
|
|
588
|
-
parse: (value) => this._parseSchemaResult(this._validateSearchInput(value, true)),
|
|
589
|
-
safeParse: (value) => this._safeParseSchemaResult(this._validateSearchInput(value, true))
|
|
590
|
-
};
|
|
591
|
-
/** Standard Schema for route flat input (uses route default strict/loose mode). */
|
|
592
|
-
flatInputSchema = {
|
|
593
|
-
"~standard": {
|
|
594
|
-
version: 1,
|
|
595
|
-
vendor: "route0",
|
|
596
|
-
validate: (value) => this._validateFlatInput(value, this.hasLooseSearch),
|
|
597
|
-
types: void 0
|
|
598
|
-
},
|
|
599
|
-
parse: (value) => this._parseSchemaResult(this._validateFlatInput(value, this.hasLooseSearch)),
|
|
600
|
-
safeParse: (value) => this._safeParseSchemaResult(this._validateFlatInput(value, this.hasLooseSearch))
|
|
601
|
-
};
|
|
602
517
|
/** True when path structure is equal (param names are ignored). */
|
|
603
518
|
isSame(other) {
|
|
604
|
-
return this.
|
|
519
|
+
return getPathTokens(this.definition).map((t) => {
|
|
520
|
+
if (t.kind === "static") return `s:${t.value}`;
|
|
521
|
+
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
522
|
+
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
523
|
+
}).join("/") === getPathTokens(other.definition).map((t) => {
|
|
524
|
+
if (t.kind === "static") return `s:${t.value}`;
|
|
525
|
+
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
526
|
+
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
527
|
+
}).join("/");
|
|
605
528
|
}
|
|
606
529
|
/** Static convenience wrapper for `isSame`. */
|
|
607
530
|
static isSame(a, b) {
|
|
@@ -619,11 +542,11 @@ class Route0 {
|
|
|
619
542
|
if (!other) return false;
|
|
620
543
|
other = Route0.create(other);
|
|
621
544
|
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
622
|
-
if (other.
|
|
545
|
+
if (other.definition === "/" && this.definition !== "/") {
|
|
623
546
|
return true;
|
|
624
547
|
}
|
|
625
|
-
const thisParts = getParts(this.
|
|
626
|
-
const otherParts = getParts(other.
|
|
548
|
+
const thisParts = getParts(this.definition);
|
|
549
|
+
const otherParts = getParts(other.definition);
|
|
627
550
|
if (thisParts.length <= otherParts.length) return false;
|
|
628
551
|
for (let i = 0; i < otherParts.length; i++) {
|
|
629
552
|
const otherPart = otherParts[i];
|
|
@@ -638,11 +561,11 @@ class Route0 {
|
|
|
638
561
|
if (!other) return false;
|
|
639
562
|
other = Route0.create(other);
|
|
640
563
|
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
641
|
-
if (this.
|
|
564
|
+
if (this.definition === "/" && other.definition !== "/") {
|
|
642
565
|
return true;
|
|
643
566
|
}
|
|
644
|
-
const thisParts = getParts(this.
|
|
645
|
-
const otherParts = getParts(other.
|
|
567
|
+
const thisParts = getParts(this.definition);
|
|
568
|
+
const otherParts = getParts(other.definition);
|
|
646
569
|
if (thisParts.length >= otherParts.length) return false;
|
|
647
570
|
for (let i = 0; i < thisParts.length; i++) {
|
|
648
571
|
const thisPart = thisParts[i];
|
|
@@ -656,29 +579,46 @@ class Route0 {
|
|
|
656
579
|
isConflict(other) {
|
|
657
580
|
if (!other) return false;
|
|
658
581
|
other = Route0.create(other);
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
582
|
+
const thisRegex = this.getRegex();
|
|
583
|
+
const otherRegex = other.getRegex();
|
|
584
|
+
const makeCandidates = (definition) => {
|
|
585
|
+
const tokens = getPathTokens(definition);
|
|
586
|
+
const values = (token) => {
|
|
587
|
+
if (token.kind === "static") return [token.value];
|
|
588
|
+
if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
|
|
589
|
+
if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
|
|
590
|
+
return ["", "x", "x/y"];
|
|
591
|
+
};
|
|
592
|
+
let acc = [""];
|
|
593
|
+
for (const token of tokens) {
|
|
594
|
+
const next = [];
|
|
595
|
+
for (const base of acc) {
|
|
596
|
+
for (const value of values(token)) {
|
|
597
|
+
if (value === "") {
|
|
598
|
+
next.push(base);
|
|
599
|
+
} else if (value.startsWith("/")) {
|
|
600
|
+
next.push(`${base}${value}`);
|
|
601
|
+
} else {
|
|
602
|
+
next.push(`${base}/${value}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
acc = next;
|
|
679
607
|
}
|
|
680
|
-
|
|
681
|
-
|
|
608
|
+
if (acc.length === 0) return ["/"];
|
|
609
|
+
return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
|
|
610
|
+
};
|
|
611
|
+
const thisCandidates = makeCandidates(this.definition);
|
|
612
|
+
const otherCandidates = makeCandidates(other.definition);
|
|
613
|
+
if (thisCandidates.some((path) => otherRegex.test(path))) return true;
|
|
614
|
+
if (otherCandidates.some((path) => thisRegex.test(path))) return true;
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
/** True when paths are same or can overlap when optional parts are omitted. */
|
|
618
|
+
isMayBeSame(other) {
|
|
619
|
+
if (!other) return false;
|
|
620
|
+
other = Route0.create(other);
|
|
621
|
+
return this.isSame(other) || this.isConflict(other);
|
|
682
622
|
}
|
|
683
623
|
/** Specificity comparator used for deterministic route ordering. */
|
|
684
624
|
isMoreSpecificThan(other) {
|
|
@@ -688,15 +628,21 @@ class Route0 {
|
|
|
688
628
|
if (path === "/") return ["/"];
|
|
689
629
|
return path.split("/").filter(Boolean);
|
|
690
630
|
};
|
|
691
|
-
const
|
|
692
|
-
|
|
631
|
+
const rank = (part) => {
|
|
632
|
+
if (part.includes("*")) return -1;
|
|
633
|
+
if (part.startsWith(":") && part.endsWith("?")) return 0;
|
|
634
|
+
if (part.startsWith(":")) return 1;
|
|
635
|
+
return 2;
|
|
636
|
+
};
|
|
637
|
+
const thisParts = getParts(this.definition);
|
|
638
|
+
const otherParts = getParts(other.definition);
|
|
693
639
|
for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
|
|
694
|
-
const
|
|
695
|
-
const
|
|
696
|
-
if (
|
|
697
|
-
if (
|
|
640
|
+
const thisRank = rank(thisParts[i]);
|
|
641
|
+
const otherRank = rank(otherParts[i]);
|
|
642
|
+
if (thisRank > otherRank) return true;
|
|
643
|
+
if (thisRank < otherRank) return false;
|
|
698
644
|
}
|
|
699
|
-
return this.
|
|
645
|
+
return this.definition < other.definition;
|
|
700
646
|
}
|
|
701
647
|
}
|
|
702
648
|
class Routes {
|
|
@@ -779,16 +725,16 @@ class Routes {
|
|
|
779
725
|
return path.split("/").filter(Boolean);
|
|
780
726
|
};
|
|
781
727
|
entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
|
|
782
|
-
const partsA = getParts(routeA.
|
|
783
|
-
const partsB = getParts(routeB.
|
|
784
|
-
if (
|
|
785
|
-
return partsA.length - partsB.length;
|
|
786
|
-
}
|
|
787
|
-
if (routeA.isConflict(routeB)) {
|
|
728
|
+
const partsA = getParts(routeA.definition);
|
|
729
|
+
const partsB = getParts(routeB.definition);
|
|
730
|
+
if (routeA.isMayBeSame(routeB)) {
|
|
788
731
|
if (routeA.isMoreSpecificThan(routeB)) return -1;
|
|
789
732
|
if (routeB.isMoreSpecificThan(routeA)) return 1;
|
|
790
733
|
}
|
|
791
|
-
|
|
734
|
+
if (partsA.length !== partsB.length) {
|
|
735
|
+
return partsA.length - partsB.length;
|
|
736
|
+
}
|
|
737
|
+
return routeA.definition.localeCompare(routeB.definition);
|
|
792
738
|
});
|
|
793
739
|
const pathsOrdering = entries.map(([_key, route]) => route.definition);
|
|
794
740
|
const keysOrdering = entries.map(([_key]) => _key);
|