@devp0nt/route0 1.0.0-next.67 → 1.0.0-next.68
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/flat0.cjs +2 -0
- package/dist/cjs/flat0.cjs.map +1 -0
- package/dist/cjs/flat0.d.cts +2 -0
- package/dist/cjs/index.cjs +234 -311
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +91 -181
- package/dist/esm/flat0.d.ts +2 -0
- package/dist/esm/flat0.js +1 -0
- package/dist/esm/flat0.js.map +1 -0
- package/dist/esm/index.d.ts +91 -181
- package/dist/esm/index.js +234 -311
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
- package/src/flat0.ts +0 -0
- package/src/index.test.ts +409 -621
- package/src/index.ts +419 -836
package/dist/cjs/index.cjs
CHANGED
|
@@ -22,13 +22,67 @@ __export(index_exports, {
|
|
|
22
22
|
Routes: () => Routes
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(index_exports);
|
|
25
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
+
const getPathSegments = (definition) => {
|
|
27
|
+
if (definition === "" || definition === "/") return [];
|
|
28
|
+
return definition.split("/").filter(Boolean);
|
|
29
|
+
};
|
|
30
|
+
const getRuntimePathTokens = (definition) => {
|
|
31
|
+
const segments = getPathSegments(definition);
|
|
32
|
+
return segments.map((segment) => {
|
|
33
|
+
const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
|
|
34
|
+
if (param) {
|
|
35
|
+
return { kind: "param", name: param[1], optional: param[2] === "?" };
|
|
36
|
+
}
|
|
37
|
+
if (segment === "*" || segment === "*?") {
|
|
38
|
+
return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
|
|
39
|
+
}
|
|
40
|
+
const wildcard = segment.match(/^(.*)\*(\?)?$/);
|
|
41
|
+
if (wildcard && !segment.includes("\\*")) {
|
|
42
|
+
return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
|
|
43
|
+
}
|
|
44
|
+
return { kind: "static", value: segment };
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const getPathRegexBaseStrictString = (definition) => {
|
|
48
|
+
const tokens = getRuntimePathTokens(definition);
|
|
49
|
+
if (tokens.length === 0) return "";
|
|
50
|
+
let pattern = "";
|
|
51
|
+
for (const token of tokens) {
|
|
52
|
+
if (token.kind === "static") {
|
|
53
|
+
pattern += `/${escapeRegex(token.value)}`;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (token.kind === "param") {
|
|
57
|
+
pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (token.prefix.length > 0) {
|
|
61
|
+
pattern += `/${escapeRegex(token.prefix)}(.*)`;
|
|
62
|
+
} else {
|
|
63
|
+
pattern += "(?:/(.*))?";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return pattern;
|
|
67
|
+
};
|
|
68
|
+
const getPathCaptureKeys = (definition) => {
|
|
69
|
+
const keys = [];
|
|
70
|
+
for (const token of getRuntimePathTokens(definition)) {
|
|
71
|
+
if (token.kind === "param") keys.push(token.name);
|
|
72
|
+
if (token.kind === "wildcard") keys.push("*");
|
|
73
|
+
}
|
|
74
|
+
return keys;
|
|
75
|
+
};
|
|
76
|
+
const getPathParamsDefinition = (definition) => {
|
|
77
|
+
const entries = getRuntimePathTokens(definition).filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
|
|
78
|
+
return Object.fromEntries(entries);
|
|
79
|
+
};
|
|
25
80
|
class Route0 {
|
|
26
81
|
definition;
|
|
27
|
-
|
|
28
|
-
paramsDefinition;
|
|
29
|
-
searchDefinition;
|
|
30
|
-
hasLooseSearch;
|
|
82
|
+
params;
|
|
31
83
|
_origin;
|
|
84
|
+
_callable;
|
|
85
|
+
Infer = null;
|
|
32
86
|
/** Base URL used when generating absolute URLs (`abs: true`). */
|
|
33
87
|
get origin() {
|
|
34
88
|
if (!this._origin) {
|
|
@@ -43,10 +97,7 @@ class Route0 {
|
|
|
43
97
|
}
|
|
44
98
|
constructor(definition, config = {}) {
|
|
45
99
|
this.definition = definition;
|
|
46
|
-
this.
|
|
47
|
-
this.paramsDefinition = Route0._getParamsDefinitionBydefinition(definition);
|
|
48
|
-
this.searchDefinition = Route0._getSearchDefinitionBydefinition(definition);
|
|
49
|
-
this.hasLooseSearch = Route0._hasLooseSearch(definition);
|
|
100
|
+
this.params = Route0._getParamsDefinitionByDefinition(definition);
|
|
50
101
|
const { origin } = config;
|
|
51
102
|
if (origin && typeof origin === "string" && origin.length) {
|
|
52
103
|
this._origin = origin;
|
|
@@ -58,6 +109,12 @@ class Route0 {
|
|
|
58
109
|
this._origin = void 0;
|
|
59
110
|
}
|
|
60
111
|
}
|
|
112
|
+
const callable = this.get.bind(this);
|
|
113
|
+
Object.setPrototypeOf(callable, this);
|
|
114
|
+
Object.defineProperty(callable, Symbol.toStringTag, {
|
|
115
|
+
value: this.definition
|
|
116
|
+
});
|
|
117
|
+
this._callable = callable;
|
|
61
118
|
}
|
|
62
119
|
/**
|
|
63
120
|
* Creates a callable route instance.
|
|
@@ -65,19 +122,11 @@ class Route0 {
|
|
|
65
122
|
* If an existing route/callable route is provided, it is cloned.
|
|
66
123
|
*/
|
|
67
124
|
static create(definition, config) {
|
|
68
|
-
if (typeof definition === "function") {
|
|
69
|
-
return definition.clone(config);
|
|
70
|
-
}
|
|
71
|
-
if (typeof definition === "object") {
|
|
125
|
+
if (typeof definition === "function" || typeof definition === "object") {
|
|
72
126
|
return definition.clone(config);
|
|
73
127
|
}
|
|
74
128
|
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;
|
|
129
|
+
return original._callable;
|
|
81
130
|
}
|
|
82
131
|
/**
|
|
83
132
|
* Normalizes a definition/route into a callable route.
|
|
@@ -89,61 +138,24 @@ class Route0 {
|
|
|
89
138
|
return definition;
|
|
90
139
|
}
|
|
91
140
|
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
|
-
};
|
|
106
|
-
}
|
|
107
|
-
static _getAbsPath(origin, pathWithSearch) {
|
|
108
|
-
return new URL(pathWithSearch, origin).toString().replace(/\/$/, "");
|
|
141
|
+
return original._callable;
|
|
109
142
|
}
|
|
110
|
-
static
|
|
111
|
-
|
|
112
|
-
return pathDefinition;
|
|
143
|
+
static _getAbsPath(origin, url) {
|
|
144
|
+
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
113
145
|
}
|
|
114
|
-
static
|
|
115
|
-
|
|
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;
|
|
146
|
+
static _getParamsDefinitionByDefinition(definition) {
|
|
147
|
+
return getPathParamsDefinition(definition);
|
|
136
148
|
}
|
|
137
|
-
|
|
138
|
-
return
|
|
149
|
+
search() {
|
|
150
|
+
return this._callable;
|
|
139
151
|
}
|
|
140
152
|
/** Extends the current route definition by appending a suffix route. */
|
|
141
153
|
extend(suffixDefinition) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
const definition = `${this.definition}/${suffixDefinition}`.replace(/\/{2,}/g, "/");
|
|
155
|
+
return Route0.create(
|
|
156
|
+
definition,
|
|
157
|
+
{ origin: this._origin }
|
|
158
|
+
);
|
|
147
159
|
}
|
|
148
160
|
// implementation
|
|
149
161
|
get(...args) {
|
|
@@ -157,35 +169,61 @@ class Route0 {
|
|
|
157
169
|
hashInput: void 0
|
|
158
170
|
};
|
|
159
171
|
}
|
|
160
|
-
const input =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
const [input, abs] = (() => {
|
|
173
|
+
if (typeof args[0] === "object" && args[0] !== null) {
|
|
174
|
+
return [args[0], args[1]];
|
|
175
|
+
}
|
|
176
|
+
if (typeof args[1] === "object" && args[1] !== null) {
|
|
177
|
+
return [args[1], args[0]];
|
|
178
|
+
}
|
|
179
|
+
if (typeof args[0] === "boolean" || typeof args[0] === "string") {
|
|
180
|
+
return [{}, args[0]];
|
|
181
|
+
}
|
|
182
|
+
if (typeof args[1] === "boolean" || typeof args[1] === "string") {
|
|
183
|
+
return [{}, args[1]];
|
|
184
|
+
}
|
|
185
|
+
return [{}, void 0];
|
|
186
|
+
})();
|
|
187
|
+
let searchInput2 = {};
|
|
188
|
+
let hashInput2 = void 0;
|
|
189
|
+
const paramsInput2 = {};
|
|
190
|
+
for (const [key, value] of Object.entries(input)) {
|
|
191
|
+
if (key === "?" && typeof value === "object" && value !== null) {
|
|
192
|
+
searchInput2 = value;
|
|
193
|
+
} else if (key === "#" && (typeof value === "string" || typeof value === "number")) {
|
|
194
|
+
hashInput2 = String(value);
|
|
195
|
+
} else if (key in this.params && (typeof value === "string" || typeof value === "number")) {
|
|
196
|
+
Object.assign(paramsInput2, { [key]: String(value) });
|
|
197
|
+
}
|
|
169
198
|
}
|
|
170
|
-
const { search, abs, hash, ...params } = input;
|
|
171
199
|
const absOriginInput2 = typeof abs === "string" && abs.length > 0 ? abs : void 0;
|
|
172
200
|
return {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
paramsInput: params,
|
|
201
|
+
searchInput: searchInput2,
|
|
202
|
+
paramsInput: paramsInput2,
|
|
176
203
|
absInput: absOriginInput2 !== void 0 || abs === true,
|
|
177
204
|
absOriginInput: absOriginInput2,
|
|
178
|
-
hashInput:
|
|
205
|
+
hashInput: hashInput2
|
|
179
206
|
};
|
|
180
207
|
})();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
url = url.replace(
|
|
208
|
+
let url = this.definition;
|
|
209
|
+
url = url.replace(/\/:([A-Za-z0-9_]+)\?/g, (_m, k) => {
|
|
210
|
+
const value = paramsInput[k];
|
|
211
|
+
if (value === void 0) return "";
|
|
212
|
+
return `/${encodeURIComponent(String(value))}`;
|
|
213
|
+
});
|
|
214
|
+
url = url.replace(/:([A-Za-z0-9_]+)(?!\?)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "undefined")));
|
|
215
|
+
url = url.replace(/\/\*\?/g, () => {
|
|
216
|
+
const value = paramsInput["*"];
|
|
217
|
+
if (value === void 0) return "";
|
|
218
|
+
const stringValue = String(value);
|
|
219
|
+
return stringValue.startsWith("/") ? stringValue : `/${stringValue}`;
|
|
220
|
+
});
|
|
221
|
+
url = url.replace(/\/\*/g, () => {
|
|
222
|
+
const value = String(paramsInput["*"] ?? "");
|
|
223
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
224
|
+
});
|
|
225
|
+
url = url.replace(/\*\?/g, () => String(paramsInput["*"] ?? ""));
|
|
226
|
+
url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
|
|
189
227
|
const searchInputStringified = Object.fromEntries(Object.entries(searchInput).map(([k, v]) => [k, String(v)]));
|
|
190
228
|
url = [url, new URLSearchParams(searchInputStringified).toString()].filter(Boolean).join("?");
|
|
191
229
|
url = url.replace(/\/{2,}/g, "/");
|
|
@@ -195,92 +233,16 @@ class Route0 {
|
|
|
195
233
|
}
|
|
196
234
|
return url;
|
|
197
235
|
}
|
|
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
236
|
/** Returns path param keys extracted from route definition. */
|
|
264
237
|
getParamsKeys() {
|
|
265
|
-
return Object.keys(this.
|
|
266
|
-
}
|
|
267
|
-
/** Returns named search keys extracted from route definition. */
|
|
268
|
-
getSearchKeys() {
|
|
269
|
-
return Object.keys(this.searchDefinition || {});
|
|
270
|
-
}
|
|
271
|
-
/** Returns all flat input keys (`search + params`). */
|
|
272
|
-
getFlatKeys() {
|
|
273
|
-
return [...this.getSearchKeys(), ...this.getParamsKeys()];
|
|
274
|
-
}
|
|
275
|
-
getDefinition() {
|
|
276
|
-
return this.pathDefinition;
|
|
238
|
+
return Object.keys(this.params);
|
|
277
239
|
}
|
|
278
240
|
/** Clones route with optional config override. */
|
|
279
241
|
clone(config) {
|
|
280
242
|
return Route0.create(this.definition, config);
|
|
281
243
|
}
|
|
282
244
|
getRegexBaseStrictString() {
|
|
283
|
-
return this.
|
|
245
|
+
return getPathRegexBaseStrictString(this.definition);
|
|
284
246
|
}
|
|
285
247
|
getRegexBaseString() {
|
|
286
248
|
return this.getRegexBaseStrictString().replace(/\/+$/, "") + "/?";
|
|
@@ -394,12 +356,8 @@ class Route0 {
|
|
|
394
356
|
location.route = this.definition;
|
|
395
357
|
location.params = {};
|
|
396
358
|
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
|
-
});
|
|
359
|
+
const paramNames = getPathCaptureKeys(this.definition);
|
|
360
|
+
const def = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
|
|
403
361
|
const exactRe = new RegExp(`^${this.getRegexBaseString()}$`);
|
|
404
362
|
const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`);
|
|
405
363
|
const exactMatch = pathname.match(exactRe);
|
|
@@ -409,7 +367,12 @@ class Route0 {
|
|
|
409
367
|
const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
|
|
410
368
|
if (paramsMatch) {
|
|
411
369
|
const values = paramsMatch.slice(1, 1 + paramNames.length);
|
|
412
|
-
const params = Object.fromEntries(
|
|
370
|
+
const params = Object.fromEntries(
|
|
371
|
+
paramNames.map((n, i) => {
|
|
372
|
+
const value = values[i];
|
|
373
|
+
return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
|
|
374
|
+
})
|
|
375
|
+
);
|
|
413
376
|
location.params = params;
|
|
414
377
|
} else {
|
|
415
378
|
location.params = {};
|
|
@@ -429,6 +392,12 @@ class Route0 {
|
|
|
429
392
|
break;
|
|
430
393
|
}
|
|
431
394
|
if (defPart.startsWith(":")) continue;
|
|
395
|
+
if (defPart.includes("*")) {
|
|
396
|
+
const prefix = defPart.replace(/\*\??$/, "");
|
|
397
|
+
if (pathPart.startsWith(prefix)) continue;
|
|
398
|
+
isPrefix = false;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
432
401
|
if (defPart !== pathPart) {
|
|
433
402
|
isPrefix = false;
|
|
434
403
|
break;
|
|
@@ -444,7 +413,9 @@ class Route0 {
|
|
|
444
413
|
const pathPart = pathParts[i];
|
|
445
414
|
if (!defPart || !pathPart) continue;
|
|
446
415
|
if (defPart.startsWith(":")) {
|
|
447
|
-
descendantParams[defPart.
|
|
416
|
+
descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
|
|
417
|
+
} else if (defPart.includes("*")) {
|
|
418
|
+
descendantParams["*"] = decodeURIComponent(pathPart);
|
|
448
419
|
}
|
|
449
420
|
}
|
|
450
421
|
location.params = descendantParams;
|
|
@@ -459,13 +430,16 @@ class Route0 {
|
|
|
459
430
|
};
|
|
460
431
|
}
|
|
461
432
|
_validateParamsInput(input) {
|
|
462
|
-
const
|
|
433
|
+
const paramsEntries = Object.entries(this.params);
|
|
434
|
+
const paramsMap = this.params;
|
|
435
|
+
const requiredParamsKeys = paramsEntries.filter(([, required]) => required).map(([k]) => k);
|
|
436
|
+
const paramsKeys = paramsEntries.map(([k]) => k);
|
|
463
437
|
if (input === void 0) {
|
|
464
|
-
if (
|
|
438
|
+
if (requiredParamsKeys.length) {
|
|
465
439
|
return {
|
|
466
440
|
issues: [
|
|
467
441
|
{
|
|
468
|
-
message: `Missing params: ${
|
|
442
|
+
message: `Missing params: ${requiredParamsKeys.map((k) => `"${k}"`).join(", ")}`
|
|
469
443
|
}
|
|
470
444
|
]
|
|
471
445
|
};
|
|
@@ -474,12 +448,12 @@ class Route0 {
|
|
|
474
448
|
}
|
|
475
449
|
if (typeof input !== "object" || input === null) {
|
|
476
450
|
return {
|
|
477
|
-
issues: [{ message: "Invalid
|
|
451
|
+
issues: [{ message: "Invalid route params: expected object" }]
|
|
478
452
|
};
|
|
479
453
|
}
|
|
480
454
|
const inputObj = input;
|
|
481
455
|
const inputKeys = Object.keys(inputObj);
|
|
482
|
-
const notDefinedKeys =
|
|
456
|
+
const notDefinedKeys = requiredParamsKeys.filter((k) => !inputKeys.includes(k));
|
|
483
457
|
if (notDefinedKeys.length) {
|
|
484
458
|
return {
|
|
485
459
|
issues: [
|
|
@@ -492,13 +466,16 @@ class Route0 {
|
|
|
492
466
|
const data = {};
|
|
493
467
|
for (const k of paramsKeys) {
|
|
494
468
|
const v = inputObj[k];
|
|
495
|
-
|
|
469
|
+
const required = paramsMap[k];
|
|
470
|
+
if (v === void 0 && !required) {
|
|
471
|
+
data[k] = void 0;
|
|
472
|
+
} else if (typeof v === "string") {
|
|
496
473
|
data[k] = v;
|
|
497
474
|
} else if (typeof v === "number") {
|
|
498
475
|
data[k] = String(v);
|
|
499
476
|
} else {
|
|
500
477
|
return {
|
|
501
|
-
issues: [{ message: `Invalid
|
|
478
|
+
issues: [{ message: `Invalid route params: expected string, number, got ${typeof v} for "${k}"` }]
|
|
502
479
|
};
|
|
503
480
|
}
|
|
504
481
|
}
|
|
@@ -506,58 +483,6 @@ class Route0 {
|
|
|
506
483
|
value: data
|
|
507
484
|
};
|
|
508
485
|
}
|
|
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") {
|
|
528
|
-
data[k] = v;
|
|
529
|
-
} else if (typeof v === "number") {
|
|
530
|
-
data[k] = String(v);
|
|
531
|
-
} else {
|
|
532
|
-
return {
|
|
533
|
-
issues: [{ message: `Invalid input: expected string, number, or undefined, got ${typeof v} for "${k}"` }]
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
return {
|
|
538
|
-
value: data
|
|
539
|
-
};
|
|
540
|
-
}
|
|
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
486
|
_safeParseSchemaResult(result) {
|
|
562
487
|
if ("issues" in result) {
|
|
563
488
|
return {
|
|
@@ -580,7 +505,7 @@ class Route0 {
|
|
|
580
505
|
return safeResult.data;
|
|
581
506
|
}
|
|
582
507
|
/** Standard Schema for route params input. */
|
|
583
|
-
|
|
508
|
+
paramsSchema = {
|
|
584
509
|
"~standard": {
|
|
585
510
|
version: 1,
|
|
586
511
|
vendor: "route0",
|
|
@@ -590,42 +515,17 @@ class Route0 {
|
|
|
590
515
|
parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
|
|
591
516
|
safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
|
|
592
517
|
};
|
|
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
518
|
/** True when path structure is equal (param names are ignored). */
|
|
627
519
|
isSame(other) {
|
|
628
|
-
return this.
|
|
520
|
+
return getRuntimePathTokens(this.definition).map((t) => {
|
|
521
|
+
if (t.kind === "static") return `s:${t.value}`;
|
|
522
|
+
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
523
|
+
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
524
|
+
}).join("/") === getRuntimePathTokens(other.definition).map((t) => {
|
|
525
|
+
if (t.kind === "static") return `s:${t.value}`;
|
|
526
|
+
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
527
|
+
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
528
|
+
}).join("/");
|
|
629
529
|
}
|
|
630
530
|
/** Static convenience wrapper for `isSame`. */
|
|
631
531
|
static isSame(a, b) {
|
|
@@ -643,11 +543,11 @@ class Route0 {
|
|
|
643
543
|
if (!other) return false;
|
|
644
544
|
other = Route0.create(other);
|
|
645
545
|
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
646
|
-
if (other.
|
|
546
|
+
if (other.definition === "/" && this.definition !== "/") {
|
|
647
547
|
return true;
|
|
648
548
|
}
|
|
649
|
-
const thisParts = getParts(this.
|
|
650
|
-
const otherParts = getParts(other.
|
|
549
|
+
const thisParts = getParts(this.definition);
|
|
550
|
+
const otherParts = getParts(other.definition);
|
|
651
551
|
if (thisParts.length <= otherParts.length) return false;
|
|
652
552
|
for (let i = 0; i < otherParts.length; i++) {
|
|
653
553
|
const otherPart = otherParts[i];
|
|
@@ -662,11 +562,11 @@ class Route0 {
|
|
|
662
562
|
if (!other) return false;
|
|
663
563
|
other = Route0.create(other);
|
|
664
564
|
const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
|
|
665
|
-
if (this.
|
|
565
|
+
if (this.definition === "/" && other.definition !== "/") {
|
|
666
566
|
return true;
|
|
667
567
|
}
|
|
668
|
-
const thisParts = getParts(this.
|
|
669
|
-
const otherParts = getParts(other.
|
|
568
|
+
const thisParts = getParts(this.definition);
|
|
569
|
+
const otherParts = getParts(other.definition);
|
|
670
570
|
if (thisParts.length >= otherParts.length) return false;
|
|
671
571
|
for (let i = 0; i < thisParts.length; i++) {
|
|
672
572
|
const thisPart = thisParts[i];
|
|
@@ -680,29 +580,46 @@ class Route0 {
|
|
|
680
580
|
isConflict(other) {
|
|
681
581
|
if (!other) return false;
|
|
682
582
|
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
|
-
|
|
583
|
+
const thisRegex = this.getRegex();
|
|
584
|
+
const otherRegex = other.getRegex();
|
|
585
|
+
const makeCandidates = (definition) => {
|
|
586
|
+
const tokens = getRuntimePathTokens(definition);
|
|
587
|
+
const values = (token) => {
|
|
588
|
+
if (token.kind === "static") return [token.value];
|
|
589
|
+
if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
|
|
590
|
+
if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
|
|
591
|
+
return ["", "x", "x/y"];
|
|
592
|
+
};
|
|
593
|
+
let acc = [""];
|
|
594
|
+
for (const token of tokens) {
|
|
595
|
+
const next = [];
|
|
596
|
+
for (const base of acc) {
|
|
597
|
+
for (const value of values(token)) {
|
|
598
|
+
if (value === "") {
|
|
599
|
+
next.push(base);
|
|
600
|
+
} else if (value.startsWith("/")) {
|
|
601
|
+
next.push(`${base}${value}`);
|
|
602
|
+
} else {
|
|
603
|
+
next.push(`${base}/${value}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
acc = next;
|
|
703
608
|
}
|
|
704
|
-
|
|
705
|
-
|
|
609
|
+
if (acc.length === 0) return ["/"];
|
|
610
|
+
return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
|
|
611
|
+
};
|
|
612
|
+
const thisCandidates = makeCandidates(this.definition);
|
|
613
|
+
const otherCandidates = makeCandidates(other.definition);
|
|
614
|
+
if (thisCandidates.some((path) => otherRegex.test(path))) return true;
|
|
615
|
+
if (otherCandidates.some((path) => thisRegex.test(path))) return true;
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
/** True when paths are same or can overlap when optional parts are omitted. */
|
|
619
|
+
isMayBeSame(other) {
|
|
620
|
+
if (!other) return false;
|
|
621
|
+
other = Route0.create(other);
|
|
622
|
+
return this.isSame(other) || this.isConflict(other);
|
|
706
623
|
}
|
|
707
624
|
/** Specificity comparator used for deterministic route ordering. */
|
|
708
625
|
isMoreSpecificThan(other) {
|
|
@@ -712,15 +629,21 @@ class Route0 {
|
|
|
712
629
|
if (path === "/") return ["/"];
|
|
713
630
|
return path.split("/").filter(Boolean);
|
|
714
631
|
};
|
|
715
|
-
const
|
|
716
|
-
|
|
632
|
+
const rank = (part) => {
|
|
633
|
+
if (part.includes("*")) return -1;
|
|
634
|
+
if (part.startsWith(":") && part.endsWith("?")) return 0;
|
|
635
|
+
if (part.startsWith(":")) return 1;
|
|
636
|
+
return 2;
|
|
637
|
+
};
|
|
638
|
+
const thisParts = getParts(this.definition);
|
|
639
|
+
const otherParts = getParts(other.definition);
|
|
717
640
|
for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
if (
|
|
721
|
-
if (
|
|
641
|
+
const thisRank = rank(thisParts[i]);
|
|
642
|
+
const otherRank = rank(otherParts[i]);
|
|
643
|
+
if (thisRank > otherRank) return true;
|
|
644
|
+
if (thisRank < otherRank) return false;
|
|
722
645
|
}
|
|
723
|
-
return this.
|
|
646
|
+
return this.definition < other.definition;
|
|
724
647
|
}
|
|
725
648
|
}
|
|
726
649
|
class Routes {
|
|
@@ -803,16 +726,16 @@ class Routes {
|
|
|
803
726
|
return path.split("/").filter(Boolean);
|
|
804
727
|
};
|
|
805
728
|
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)) {
|
|
729
|
+
const partsA = getParts(routeA.definition);
|
|
730
|
+
const partsB = getParts(routeB.definition);
|
|
731
|
+
if (routeA.isMayBeSame(routeB)) {
|
|
812
732
|
if (routeA.isMoreSpecificThan(routeB)) return -1;
|
|
813
733
|
if (routeB.isMoreSpecificThan(routeA)) return 1;
|
|
814
734
|
}
|
|
815
|
-
|
|
735
|
+
if (partsA.length !== partsB.length) {
|
|
736
|
+
return partsA.length - partsB.length;
|
|
737
|
+
}
|
|
738
|
+
return routeA.definition.localeCompare(routeB.definition);
|
|
816
739
|
});
|
|
817
740
|
const pathsOrdering = entries.map(([_key, route]) => route.definition);
|
|
818
741
|
const keysOrdering = entries.map(([_key]) => _key);
|