@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/cjs/index.cjs
CHANGED
|
@@ -22,13 +22,85 @@ __export(index_exports, {
|
|
|
22
22
|
Routes: () => Routes
|
|
23
23
|
});
|
|
24
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(/\*\??$/, "");
|
|
25
98
|
class Route0 {
|
|
26
99
|
definition;
|
|
27
|
-
|
|
28
|
-
paramsDefinition;
|
|
29
|
-
searchDefinition;
|
|
30
|
-
hasLooseSearch;
|
|
100
|
+
params;
|
|
31
101
|
_origin;
|
|
102
|
+
_callable;
|
|
103
|
+
Infer = null;
|
|
32
104
|
/** Base URL used when generating absolute URLs (`abs: true`). */
|
|
33
105
|
get origin() {
|
|
34
106
|
if (!this._origin) {
|
|
@@ -42,11 +114,9 @@ class Route0 {
|
|
|
42
114
|
this._origin = origin;
|
|
43
115
|
}
|
|
44
116
|
constructor(definition, config = {}) {
|
|
117
|
+
validatePathDefinition(definition);
|
|
45
118
|
this.definition = definition;
|
|
46
|
-
this.
|
|
47
|
-
this.paramsDefinition = Route0._getParamsDefinitionBydefinition(definition);
|
|
48
|
-
this.searchDefinition = Route0._getSearchDefinitionBydefinition(definition);
|
|
49
|
-
this.hasLooseSearch = Route0._hasLooseSearch(definition);
|
|
119
|
+
this.params = Route0._getParamsDefinitionByDefinition(definition);
|
|
50
120
|
const { origin } = config;
|
|
51
121
|
if (origin && typeof origin === "string" && origin.length) {
|
|
52
122
|
this._origin = origin;
|
|
@@ -58,6 +128,12 @@ class Route0 {
|
|
|
58
128
|
this._origin = void 0;
|
|
59
129
|
}
|
|
60
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;
|
|
61
137
|
}
|
|
62
138
|
/**
|
|
63
139
|
* Creates a callable route instance.
|
|
@@ -65,19 +141,11 @@ class Route0 {
|
|
|
65
141
|
* If an existing route/callable route is provided, it is cloned.
|
|
66
142
|
*/
|
|
67
143
|
static create(definition, config) {
|
|
68
|
-
if (typeof definition === "function") {
|
|
69
|
-
return definition.clone(config);
|
|
70
|
-
}
|
|
71
|
-
if (typeof definition === "object") {
|
|
144
|
+
if (typeof definition === "function" || typeof definition === "object") {
|
|
72
145
|
return definition.clone(config);
|
|
73
146
|
}
|
|
74
147
|
const original = new Route0(definition, config);
|
|
75
|
-
|
|
76
|
-
Object.setPrototypeOf(callable, original);
|
|
77
|
-
Object.defineProperty(callable, Symbol.toStringTag, {
|
|
78
|
-
value: original.definition
|
|
79
|
-
});
|
|
80
|
-
return callable;
|
|
148
|
+
return original._callable;
|
|
81
149
|
}
|
|
82
150
|
/**
|
|
83
151
|
* Normalizes a definition/route into a callable route.
|
|
@@ -89,61 +157,25 @@ class Route0 {
|
|
|
89
157
|
return definition;
|
|
90
158
|
}
|
|
91
159
|
const original = typeof definition === "object" ? definition : new Route0(definition);
|
|
92
|
-
|
|
93
|
-
Object.setPrototypeOf(callable, original);
|
|
94
|
-
Object.defineProperty(callable, Symbol.toStringTag, {
|
|
95
|
-
value: original.definition
|
|
96
|
-
});
|
|
97
|
-
return callable;
|
|
98
|
-
}
|
|
99
|
-
static _splitPathDefinitionAndSearchTailDefinition(definition) {
|
|
100
|
-
const i = definition.indexOf("&");
|
|
101
|
-
if (i === -1) return { pathDefinition: definition, searchTailDefinition: "" };
|
|
102
|
-
return {
|
|
103
|
-
pathDefinition: definition.slice(0, i),
|
|
104
|
-
searchTailDefinition: definition.slice(i)
|
|
105
|
-
};
|
|
160
|
+
return original._callable;
|
|
106
161
|
}
|
|
107
|
-
static _getAbsPath(origin,
|
|
108
|
-
return new URL(
|
|
162
|
+
static _getAbsPath(origin, url) {
|
|
163
|
+
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
109
164
|
}
|
|
110
|
-
static
|
|
111
|
-
|
|
112
|
-
return pathDefinition;
|
|
113
|
-
}
|
|
114
|
-
static _getParamsDefinitionBydefinition(definition) {
|
|
115
|
-
const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
|
|
116
|
-
const matches = Array.from(pathDefinition.matchAll(/:([A-Za-z0-9_]+)/g));
|
|
117
|
-
const paramsDefinition = Object.fromEntries(matches.map((m) => [m[1], true]));
|
|
118
|
-
const keysCount = Object.keys(paramsDefinition).length;
|
|
119
|
-
if (keysCount === 0) {
|
|
120
|
-
return void 0;
|
|
121
|
-
}
|
|
122
|
-
return paramsDefinition;
|
|
123
|
-
}
|
|
124
|
-
static _getSearchDefinitionBydefinition(definition) {
|
|
125
|
-
const { searchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
|
|
126
|
-
if (!searchTailDefinition) {
|
|
127
|
-
return void 0;
|
|
128
|
-
}
|
|
129
|
-
const keys = searchTailDefinition.split("&").filter(Boolean);
|
|
130
|
-
const searchDefinition = Object.fromEntries(keys.map((k) => [k, true]));
|
|
131
|
-
const keysCount = Object.keys(searchDefinition).length;
|
|
132
|
-
if (keysCount === 0) {
|
|
133
|
-
return void 0;
|
|
134
|
-
}
|
|
135
|
-
return searchDefinition;
|
|
165
|
+
static _getParamsDefinitionByDefinition(definition) {
|
|
166
|
+
return getPathParamsDefinition(definition);
|
|
136
167
|
}
|
|
137
|
-
|
|
138
|
-
return
|
|
168
|
+
search() {
|
|
169
|
+
return this._callable;
|
|
139
170
|
}
|
|
140
171
|
/** Extends the current route definition by appending a suffix route. */
|
|
141
172
|
extend(suffixDefinition) {
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
);
|
|
147
179
|
}
|
|
148
180
|
// implementation
|
|
149
181
|
get(...args) {
|
|
@@ -157,37 +189,63 @@ class Route0 {
|
|
|
157
189
|
hashInput: void 0
|
|
158
190
|
};
|
|
159
191
|
}
|
|
160
|
-
const input =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
218
|
}
|
|
170
|
-
const { search, abs, hash, ...params } = input;
|
|
171
219
|
const absOriginInput2 = typeof abs === "string" && abs.length > 0 ? abs : void 0;
|
|
172
220
|
return {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
paramsInput: params,
|
|
221
|
+
searchInput: searchInput2,
|
|
222
|
+
paramsInput: paramsInput2,
|
|
176
223
|
absInput: absOriginInput2 !== void 0 || abs === true,
|
|
177
224
|
absOriginInput: absOriginInput2,
|
|
178
|
-
hashInput:
|
|
225
|
+
hashInput: hashInput2
|
|
179
226
|
};
|
|
180
227
|
})();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
url = url.replace(
|
|
189
|
-
|
|
190
|
-
|
|
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("?");
|
|
191
249
|
url = url.replace(/\/{2,}/g, "/");
|
|
192
250
|
url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
|
|
193
251
|
if (hashInput !== void 0) {
|
|
@@ -195,92 +253,19 @@ class Route0 {
|
|
|
195
253
|
}
|
|
196
254
|
return url;
|
|
197
255
|
}
|
|
198
|
-
// implementation
|
|
199
|
-
flat(...args) {
|
|
200
|
-
const { searchInput, paramsInput, absInput, hashInput } = (() => {
|
|
201
|
-
if (args.length === 0) {
|
|
202
|
-
return {
|
|
203
|
-
searchInput: {},
|
|
204
|
-
paramsInput: {},
|
|
205
|
-
absInput: false,
|
|
206
|
-
hashInput: void 0
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
const input = args[0];
|
|
210
|
-
if (typeof input !== "object" || input === null) {
|
|
211
|
-
return {
|
|
212
|
-
searchInput: {},
|
|
213
|
-
paramsInput: {},
|
|
214
|
-
absInput: args[1] ?? false,
|
|
215
|
-
hashInput: void 0
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
const loose = args[2] ?? this.hasLooseSearch;
|
|
219
|
-
const paramsKeys = this.getParamsKeys();
|
|
220
|
-
const paramsInput2 = paramsKeys.reduce((acc, key) => {
|
|
221
|
-
if (input[key] !== void 0) {
|
|
222
|
-
acc[key] = input[key];
|
|
223
|
-
}
|
|
224
|
-
return acc;
|
|
225
|
-
}, {});
|
|
226
|
-
const searchKeys = this.getSearchKeys();
|
|
227
|
-
const searchInput2 = Object.keys(input).filter((k) => {
|
|
228
|
-
if (k === "hash") {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
if (searchKeys.includes(k)) {
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
if (paramsKeys.includes(k)) {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
return loose;
|
|
238
|
-
}).reduce((acc, key) => {
|
|
239
|
-
acc[key] = input[key];
|
|
240
|
-
return acc;
|
|
241
|
-
}, {});
|
|
242
|
-
const hashInput2 = input.hash;
|
|
243
|
-
return {
|
|
244
|
-
searchInput: searchInput2,
|
|
245
|
-
paramsInput: paramsInput2,
|
|
246
|
-
absInput: args[1] ?? false,
|
|
247
|
-
hashInput: hashInput2
|
|
248
|
-
};
|
|
249
|
-
})();
|
|
250
|
-
return this.get({
|
|
251
|
-
...paramsInput,
|
|
252
|
-
search: searchInput,
|
|
253
|
-
abs: absInput,
|
|
254
|
-
hash: hashInput
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
flatLoose(...args) {
|
|
258
|
-
return this.flat(args[0], args[1], true);
|
|
259
|
-
}
|
|
260
|
-
flatStrict(...args) {
|
|
261
|
-
return this.flat(args[0], args[1], false);
|
|
262
|
-
}
|
|
263
256
|
/** Returns path param keys extracted from route definition. */
|
|
264
257
|
getParamsKeys() {
|
|
265
|
-
return Object.keys(this.
|
|
266
|
-
}
|
|
267
|
-
/** Returns named search keys extracted from route definition. */
|
|
268
|
-
getSearchKeys() {
|
|
269
|
-
return Object.keys(this.searchDefinition || {});
|
|
258
|
+
return Object.keys(this.params);
|
|
270
259
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return [...this.getSearchKeys(), ...this.getParamsKeys()];
|
|
274
|
-
}
|
|
275
|
-
getDefinition() {
|
|
276
|
-
return this.pathDefinition;
|
|
260
|
+
getPathTokens() {
|
|
261
|
+
return getPathTokens(this.definition);
|
|
277
262
|
}
|
|
278
263
|
/** Clones route with optional config override. */
|
|
279
264
|
clone(config) {
|
|
280
265
|
return Route0.create(this.definition, config);
|
|
281
266
|
}
|
|
282
267
|
getRegexBaseStrictString() {
|
|
283
|
-
return this.
|
|
268
|
+
return getPathRegexBaseStrictString(this.definition);
|
|
284
269
|
}
|
|
285
270
|
getRegexBaseString() {
|
|
286
271
|
return this.getRegexBaseStrictString().replace(/\/+$/, "") + "/?";
|
|
@@ -353,7 +338,7 @@ class Route0 {
|
|
|
353
338
|
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
354
339
|
const base = abs ? void 0 : "http://example.com";
|
|
355
340
|
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
356
|
-
const
|
|
341
|
+
const search = (0, import_flat0.parse)(url.search);
|
|
357
342
|
let pathname = url.pathname;
|
|
358
343
|
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
359
344
|
pathname = pathname.slice(0, -1);
|
|
@@ -361,7 +346,8 @@ class Route0 {
|
|
|
361
346
|
const hrefRel = pathname + url.search + url.hash;
|
|
362
347
|
const location = {
|
|
363
348
|
pathname,
|
|
364
|
-
search
|
|
349
|
+
search,
|
|
350
|
+
searchString: url.search,
|
|
365
351
|
hash: url.hash,
|
|
366
352
|
origin: abs ? url.origin : void 0,
|
|
367
353
|
href: abs ? url.href : void 0,
|
|
@@ -372,7 +358,6 @@ class Route0 {
|
|
|
372
358
|
hostname: abs ? url.hostname : void 0,
|
|
373
359
|
port: abs ? url.port || void 0 : void 0,
|
|
374
360
|
// specific to UnknownLocation
|
|
375
|
-
searchParams,
|
|
376
361
|
params: void 0,
|
|
377
362
|
route: void 0,
|
|
378
363
|
known: false,
|
|
@@ -394,12 +379,8 @@ class Route0 {
|
|
|
394
379
|
location.route = this.definition;
|
|
395
380
|
location.params = {};
|
|
396
381
|
const pathname = location.pathname.length > 1 && location.pathname.endsWith("/") ? location.pathname.slice(0, -1) : location.pathname;
|
|
397
|
-
const paramNames =
|
|
398
|
-
const def = this.
|
|
399
|
-
def.replace(/:([A-Za-z0-9_]+)/g, (_m, name) => {
|
|
400
|
-
paramNames.push(String(name));
|
|
401
|
-
return "";
|
|
402
|
-
});
|
|
382
|
+
const paramNames = getPathCaptureKeys(this.definition);
|
|
383
|
+
const def = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
|
|
403
384
|
const exactRe = new RegExp(`^${this.getRegexBaseString()}$`);
|
|
404
385
|
const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`);
|
|
405
386
|
const exactMatch = pathname.match(exactRe);
|
|
@@ -409,7 +390,12 @@ class Route0 {
|
|
|
409
390
|
const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
|
|
410
391
|
if (paramsMatch) {
|
|
411
392
|
const values = paramsMatch.slice(1, 1 + paramNames.length);
|
|
412
|
-
const params = Object.fromEntries(
|
|
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
|
+
);
|
|
413
399
|
location.params = params;
|
|
414
400
|
} else {
|
|
415
401
|
location.params = {};
|
|
@@ -429,6 +415,12 @@ class Route0 {
|
|
|
429
415
|
break;
|
|
430
416
|
}
|
|
431
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
|
+
}
|
|
432
424
|
if (defPart !== pathPart) {
|
|
433
425
|
isPrefix = false;
|
|
434
426
|
break;
|
|
@@ -444,7 +436,9 @@ class Route0 {
|
|
|
444
436
|
const pathPart = pathParts[i];
|
|
445
437
|
if (!defPart || !pathPart) continue;
|
|
446
438
|
if (defPart.startsWith(":")) {
|
|
447
|
-
descendantParams[defPart.
|
|
439
|
+
descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
|
|
440
|
+
} else if (defPart.includes("*")) {
|
|
441
|
+
descendantParams["*"] = decodeURIComponent(pathPart);
|
|
448
442
|
}
|
|
449
443
|
}
|
|
450
444
|
location.params = descendantParams;
|
|
@@ -459,13 +453,16 @@ class Route0 {
|
|
|
459
453
|
};
|
|
460
454
|
}
|
|
461
455
|
_validateParamsInput(input) {
|
|
462
|
-
const
|
|
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);
|
|
463
460
|
if (input === void 0) {
|
|
464
|
-
if (
|
|
461
|
+
if (requiredParamsKeys.length) {
|
|
465
462
|
return {
|
|
466
463
|
issues: [
|
|
467
464
|
{
|
|
468
|
-
message: `Missing params: ${
|
|
465
|
+
message: `Missing params: ${requiredParamsKeys.map((k) => `"${k}"`).join(", ")}`
|
|
469
466
|
}
|
|
470
467
|
]
|
|
471
468
|
};
|
|
@@ -474,12 +471,12 @@ class Route0 {
|
|
|
474
471
|
}
|
|
475
472
|
if (typeof input !== "object" || input === null) {
|
|
476
473
|
return {
|
|
477
|
-
issues: [{ message: "Invalid
|
|
474
|
+
issues: [{ message: "Invalid route params: expected object" }]
|
|
478
475
|
};
|
|
479
476
|
}
|
|
480
477
|
const inputObj = input;
|
|
481
478
|
const inputKeys = Object.keys(inputObj);
|
|
482
|
-
const notDefinedKeys =
|
|
479
|
+
const notDefinedKeys = requiredParamsKeys.filter((k) => !inputKeys.includes(k));
|
|
483
480
|
if (notDefinedKeys.length) {
|
|
484
481
|
return {
|
|
485
482
|
issues: [
|
|
@@ -492,45 +489,16 @@ class Route0 {
|
|
|
492
489
|
const data = {};
|
|
493
490
|
for (const k of paramsKeys) {
|
|
494
491
|
const v = inputObj[k];
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
} else {
|
|
500
|
-
return {
|
|
501
|
-
issues: [{ message: `Invalid input: expected string, number, got ${typeof v} for "${k}"` }]
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return {
|
|
506
|
-
value: data
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
_validateSearchInput(input, loose) {
|
|
510
|
-
if (input === void 0) {
|
|
511
|
-
input = {};
|
|
512
|
-
}
|
|
513
|
-
if (typeof input !== "object" || input === null) {
|
|
514
|
-
return {
|
|
515
|
-
issues: [{ message: "Invalid input: expected object" }]
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
const inputObj = input;
|
|
519
|
-
const paramsKeys = this.getParamsKeys();
|
|
520
|
-
const searchKeys = this.getSearchKeys();
|
|
521
|
-
const data = {};
|
|
522
|
-
for (const [k, v] of Object.entries(inputObj)) {
|
|
523
|
-
if (k === "hash") continue;
|
|
524
|
-
if (paramsKeys.includes(k)) continue;
|
|
525
|
-
if (!loose && !searchKeys.includes(k)) continue;
|
|
526
|
-
if (v === void 0) continue;
|
|
527
|
-
if (typeof v === "string") {
|
|
492
|
+
const required = paramsMap[k];
|
|
493
|
+
if (v === void 0 && !required) {
|
|
494
|
+
data[k] = void 0;
|
|
495
|
+
} else if (typeof v === "string") {
|
|
528
496
|
data[k] = v;
|
|
529
497
|
} else if (typeof v === "number") {
|
|
530
498
|
data[k] = String(v);
|
|
531
499
|
} else {
|
|
532
500
|
return {
|
|
533
|
-
issues: [{ message: `Invalid
|
|
501
|
+
issues: [{ message: `Invalid route params: expected string, number, got ${typeof v} for "${k}"` }]
|
|
534
502
|
};
|
|
535
503
|
}
|
|
536
504
|
}
|
|
@@ -538,26 +506,6 @@ class Route0 {
|
|
|
538
506
|
value: data
|
|
539
507
|
};
|
|
540
508
|
}
|
|
541
|
-
_validateFlatInput(input, loose) {
|
|
542
|
-
const paramsResult = this._validateParamsInput(input);
|
|
543
|
-
if ("issues" in paramsResult) {
|
|
544
|
-
return {
|
|
545
|
-
issues: paramsResult.issues ?? []
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
const searchResult = this._validateSearchInput(input, loose);
|
|
549
|
-
if ("issues" in searchResult) {
|
|
550
|
-
return {
|
|
551
|
-
issues: searchResult.issues ?? []
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
return {
|
|
555
|
-
value: {
|
|
556
|
-
...searchResult.value,
|
|
557
|
-
...paramsResult.value
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
509
|
_safeParseSchemaResult(result) {
|
|
562
510
|
if ("issues" in result) {
|
|
563
511
|
return {
|
|
@@ -580,7 +528,7 @@ class Route0 {
|
|
|
580
528
|
return safeResult.data;
|
|
581
529
|
}
|
|
582
530
|
/** Standard Schema for route params input. */
|
|
583
|
-
|
|
531
|
+
paramsSchema = {
|
|
584
532
|
"~standard": {
|
|
585
533
|
version: 1,
|
|
586
534
|
vendor: "route0",
|
|
@@ -590,42 +538,17 @@ class Route0 {
|
|
|
590
538
|
parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
|
|
591
539
|
safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
|
|
592
540
|
};
|
|
593
|
-
/** Standard Schema for strict search input. */
|
|
594
|
-
strictSearchInputSchema = {
|
|
595
|
-
"~standard": {
|
|
596
|
-
version: 1,
|
|
597
|
-
vendor: "route0",
|
|
598
|
-
validate: (value) => this._validateSearchInput(value, false),
|
|
599
|
-
types: void 0
|
|
600
|
-
},
|
|
601
|
-
parse: (value) => this._parseSchemaResult(this._validateSearchInput(value, false)),
|
|
602
|
-
safeParse: (value) => this._safeParseSchemaResult(this._validateSearchInput(value, false))
|
|
603
|
-
};
|
|
604
|
-
/** Standard Schema for loose search input. */
|
|
605
|
-
looseSearchInputSchema = {
|
|
606
|
-
"~standard": {
|
|
607
|
-
version: 1,
|
|
608
|
-
vendor: "route0",
|
|
609
|
-
validate: (value) => this._validateSearchInput(value, true),
|
|
610
|
-
types: void 0
|
|
611
|
-
},
|
|
612
|
-
parse: (value) => this._parseSchemaResult(this._validateSearchInput(value, true)),
|
|
613
|
-
safeParse: (value) => this._safeParseSchemaResult(this._validateSearchInput(value, true))
|
|
614
|
-
};
|
|
615
|
-
/** Standard Schema for route flat input (uses route default strict/loose mode). */
|
|
616
|
-
flatInputSchema = {
|
|
617
|
-
"~standard": {
|
|
618
|
-
version: 1,
|
|
619
|
-
vendor: "route0",
|
|
620
|
-
validate: (value) => this._validateFlatInput(value, this.hasLooseSearch),
|
|
621
|
-
types: void 0
|
|
622
|
-
},
|
|
623
|
-
parse: (value) => this._parseSchemaResult(this._validateFlatInput(value, this.hasLooseSearch)),
|
|
624
|
-
safeParse: (value) => this._safeParseSchemaResult(this._validateFlatInput(value, this.hasLooseSearch))
|
|
625
|
-
};
|
|
626
541
|
/** True when path structure is equal (param names are ignored). */
|
|
627
542
|
isSame(other) {
|
|
628
|
-
return this.
|
|
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("/");
|
|
629
552
|
}
|
|
630
553
|
/** Static convenience wrapper for `isSame`. */
|
|
631
554
|
static isSame(a, b) {
|
|
@@ -643,11 +566,11 @@ class Route0 {
|
|
|
643
566
|
if (!other) return false;
|
|
644
567
|
other = Route0.create(other);
|
|
645
568
|
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
646
|
-
if (other.
|
|
569
|
+
if (other.definition === "/" && this.definition !== "/") {
|
|
647
570
|
return true;
|
|
648
571
|
}
|
|
649
|
-
const thisParts = getParts(this.
|
|
650
|
-
const otherParts = getParts(other.
|
|
572
|
+
const thisParts = getParts(this.definition);
|
|
573
|
+
const otherParts = getParts(other.definition);
|
|
651
574
|
if (thisParts.length <= otherParts.length) return false;
|
|
652
575
|
for (let i = 0; i < otherParts.length; i++) {
|
|
653
576
|
const otherPart = otherParts[i];
|
|
@@ -662,11 +585,11 @@ class Route0 {
|
|
|
662
585
|
if (!other) return false;
|
|
663
586
|
other = Route0.create(other);
|
|
664
587
|
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
665
|
-
if (this.
|
|
588
|
+
if (this.definition === "/" && other.definition !== "/") {
|
|
666
589
|
return true;
|
|
667
590
|
}
|
|
668
|
-
const thisParts = getParts(this.
|
|
669
|
-
const otherParts = getParts(other.
|
|
591
|
+
const thisParts = getParts(this.definition);
|
|
592
|
+
const otherParts = getParts(other.definition);
|
|
670
593
|
if (thisParts.length >= otherParts.length) return false;
|
|
671
594
|
for (let i = 0; i < thisParts.length; i++) {
|
|
672
595
|
const thisPart = thisParts[i];
|
|
@@ -680,29 +603,46 @@ class Route0 {
|
|
|
680
603
|
isConflict(other) {
|
|
681
604
|
if (!other) return false;
|
|
682
605
|
other = Route0.create(other);
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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;
|
|
703
631
|
}
|
|
704
|
-
|
|
705
|
-
|
|
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);
|
|
706
646
|
}
|
|
707
647
|
/** Specificity comparator used for deterministic route ordering. */
|
|
708
648
|
isMoreSpecificThan(other) {
|
|
@@ -712,15 +652,21 @@ class Route0 {
|
|
|
712
652
|
if (path === "/") return ["/"];
|
|
713
653
|
return path.split("/").filter(Boolean);
|
|
714
654
|
};
|
|
715
|
-
const
|
|
716
|
-
|
|
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);
|
|
717
663
|
for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
if (
|
|
721
|
-
if (
|
|
664
|
+
const thisRank = rank(thisParts[i]);
|
|
665
|
+
const otherRank = rank(otherParts[i]);
|
|
666
|
+
if (thisRank > otherRank) return true;
|
|
667
|
+
if (thisRank < otherRank) return false;
|
|
722
668
|
}
|
|
723
|
-
return this.
|
|
669
|
+
return this.definition < other.definition;
|
|
724
670
|
}
|
|
725
671
|
}
|
|
726
672
|
class Routes {
|
|
@@ -803,16 +749,16 @@ class Routes {
|
|
|
803
749
|
return path.split("/").filter(Boolean);
|
|
804
750
|
};
|
|
805
751
|
entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
|
|
806
|
-
const partsA = getParts(routeA.
|
|
807
|
-
const partsB = getParts(routeB.
|
|
808
|
-
if (
|
|
809
|
-
return partsA.length - partsB.length;
|
|
810
|
-
}
|
|
811
|
-
if (routeA.isConflict(routeB)) {
|
|
752
|
+
const partsA = getParts(routeA.definition);
|
|
753
|
+
const partsB = getParts(routeB.definition);
|
|
754
|
+
if (routeA.isMayBeSame(routeB)) {
|
|
812
755
|
if (routeA.isMoreSpecificThan(routeB)) return -1;
|
|
813
756
|
if (routeB.isMoreSpecificThan(routeA)) return 1;
|
|
814
757
|
}
|
|
815
|
-
|
|
758
|
+
if (partsA.length !== partsB.length) {
|
|
759
|
+
return partsA.length - partsB.length;
|
|
760
|
+
}
|
|
761
|
+
return routeA.definition.localeCompare(routeB.definition);
|
|
816
762
|
});
|
|
817
763
|
const pathsOrdering = entries.map(([_key, route]) => route.definition);
|
|
818
764
|
const keysOrdering = entries.map(([_key]) => _key);
|