@sveltejs/kit 1.0.0-next.518 → 1.0.0-next.519
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/package.json +1 -1
- package/src/core/sync/create_manifest_data/conflict.js +0 -0
- package/src/core/sync/create_manifest_data/index.js +94 -112
- package/src/core/sync/create_manifest_data/sort.js +163 -0
- package/src/core/sync/create_manifest_data/types.d.ts +1 -1
- package/src/utils/routing.js +61 -48
package/package.json
CHANGED
|
File without changes
|
|
@@ -3,7 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import mime from 'mime';
|
|
4
4
|
import { runtime_directory } from '../../utils.js';
|
|
5
5
|
import { posixify } from '../../../utils/filesystem.js';
|
|
6
|
-
import { parse_route_id
|
|
6
|
+
import { parse_route_id } from '../../../utils/routing.js';
|
|
7
|
+
import { sort_routes } from './sort.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @param {{
|
|
@@ -82,11 +83,8 @@ function create_matchers(config, cwd) {
|
|
|
82
83
|
* @param {string} fallback
|
|
83
84
|
*/
|
|
84
85
|
function create_routes_and_nodes(cwd, config, fallback) {
|
|
85
|
-
/** @type {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
/** @type {Map<string, import('./types').Part[][]>} */
|
|
89
|
-
const segment_map = new Map();
|
|
86
|
+
/** @type {import('types').RouteData[]} */
|
|
87
|
+
const routes = [];
|
|
90
88
|
|
|
91
89
|
const routes_base = posixify(path.relative(cwd, config.kit.files.routes));
|
|
92
90
|
|
|
@@ -111,32 +109,19 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
111
109
|
throw new Error(`Invalid route ${id} — brackets are unbalanced`);
|
|
112
110
|
}
|
|
113
111
|
|
|
114
|
-
|
|
112
|
+
if (/\[\.\.\.\w+\]\/\[\[/.test(id)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Invalid route ${id} — an [[optional]] route segment cannot follow a [...rest] route segment`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
115
117
|
|
|
116
|
-
|
|
118
|
+
if (/\[\[\.\.\./.test(id)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Invalid route ${id} — a rest route segment is always optional, remove the outer square brackets`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
id,
|
|
120
|
-
segments
|
|
121
|
-
.filter((segment) => segment !== '' && affects_path(segment))
|
|
122
|
-
.map((segment) => {
|
|
123
|
-
/** @type {import('./types').Part[]} */
|
|
124
|
-
const parts = [];
|
|
125
|
-
segment.split(/\[(.+?)\]/).map((content, i) => {
|
|
126
|
-
const dynamic = !!(i % 2);
|
|
127
|
-
|
|
128
|
-
if (!content) return;
|
|
129
|
-
|
|
130
|
-
parts.push({
|
|
131
|
-
content,
|
|
132
|
-
dynamic,
|
|
133
|
-
rest: dynamic && content.startsWith('...'),
|
|
134
|
-
type: (dynamic && content.split('=')[1]) || null
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
return parts;
|
|
138
|
-
})
|
|
139
|
-
);
|
|
124
|
+
const { pattern, names, types } = parse_route_id(id);
|
|
140
125
|
|
|
141
126
|
/** @type {import('types').RouteData} */
|
|
142
127
|
const route = {
|
|
@@ -157,7 +142,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
157
142
|
|
|
158
143
|
// important to do this before walking children, so that child
|
|
159
144
|
// routes appear later
|
|
160
|
-
|
|
145
|
+
routes.push(route);
|
|
161
146
|
|
|
162
147
|
// if we don't do this, the route map becomes unwieldy to console.log
|
|
163
148
|
Object.defineProperty(route, 'parent', { enumerable: false });
|
|
@@ -224,8 +209,8 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
224
209
|
|
|
225
210
|
walk(0, '', '', null);
|
|
226
211
|
|
|
227
|
-
|
|
228
|
-
|
|
212
|
+
if (routes.length === 1) {
|
|
213
|
+
const root = routes[0];
|
|
229
214
|
if (!root.leaf && !root.error && !root.layout && !root.endpoint) {
|
|
230
215
|
throw new Error(
|
|
231
216
|
// TODO adjust this error message for 1.0
|
|
@@ -237,7 +222,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
237
222
|
} else {
|
|
238
223
|
// If there's no routes directory, we'll just create a single empty route. This ensures the root layout and
|
|
239
224
|
// error components are included in the manifest, which is needed for subsequent build/dev commands to work
|
|
240
|
-
|
|
225
|
+
routes.push({
|
|
241
226
|
id: '',
|
|
242
227
|
segment: '',
|
|
243
228
|
pattern: /^$/,
|
|
@@ -252,7 +237,9 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
252
237
|
});
|
|
253
238
|
}
|
|
254
239
|
|
|
255
|
-
|
|
240
|
+
prevent_conflicts(routes);
|
|
241
|
+
|
|
242
|
+
const root = routes[0];
|
|
256
243
|
|
|
257
244
|
if (!root.layout?.component) {
|
|
258
245
|
if (!root.layout) root.layout = { depth: 0, child_pages: [] };
|
|
@@ -267,32 +254,19 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
267
254
|
// we do layouts/errors first as they are more likely to be reused,
|
|
268
255
|
// and smaller indexes take fewer bytes. also, this guarantees that
|
|
269
256
|
// the default error/layout are 0/1
|
|
270
|
-
|
|
257
|
+
for (const route of routes) {
|
|
271
258
|
if (route.layout) nodes.push(route.layout);
|
|
272
259
|
if (route.error) nodes.push(route.error);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** @type {Map<string, string>} */
|
|
276
|
-
const conflicts = new Map();
|
|
277
|
-
|
|
278
|
-
route_map.forEach((route) => {
|
|
279
|
-
if (!route.leaf) return;
|
|
280
|
-
|
|
281
|
-
nodes.push(route.leaf);
|
|
282
|
-
|
|
283
|
-
const normalized = route.id.split('/').filter(affects_path).join('/');
|
|
284
|
-
|
|
285
|
-
if (conflicts.has(normalized)) {
|
|
286
|
-
throw new Error(`${conflicts.get(normalized)} and ${route.id} occupy the same route`);
|
|
287
|
-
}
|
|
260
|
+
}
|
|
288
261
|
|
|
289
|
-
|
|
290
|
-
|
|
262
|
+
for (const route of routes) {
|
|
263
|
+
if (route.leaf) nodes.push(route.leaf);
|
|
264
|
+
}
|
|
291
265
|
|
|
292
266
|
const indexes = new Map(nodes.map((node, i) => [node, i]));
|
|
293
267
|
|
|
294
|
-
|
|
295
|
-
if (!route.leaf)
|
|
268
|
+
for (const route of routes) {
|
|
269
|
+
if (!route.leaf) continue;
|
|
296
270
|
|
|
297
271
|
route.page = {
|
|
298
272
|
layouts: [],
|
|
@@ -333,11 +307,12 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
333
307
|
if (parent_id !== undefined) {
|
|
334
308
|
throw new Error(`${current_node.component} references missing segment "${parent_id}"`);
|
|
335
309
|
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const routes = Array.from(route_map.values()).sort((a, b) => compare(a, b, segment_map));
|
|
310
|
+
}
|
|
339
311
|
|
|
340
|
-
return {
|
|
312
|
+
return {
|
|
313
|
+
nodes,
|
|
314
|
+
routes: sort_routes(routes)
|
|
315
|
+
};
|
|
341
316
|
}
|
|
342
317
|
|
|
343
318
|
/**
|
|
@@ -400,59 +375,6 @@ function analyze(project_relative, file, component_extensions, module_extensions
|
|
|
400
375
|
throw new Error(`Files and directories prefixed with + are reserved (saw ${project_relative})`);
|
|
401
376
|
}
|
|
402
377
|
|
|
403
|
-
/**
|
|
404
|
-
* @param {import('types').RouteData} a
|
|
405
|
-
* @param {import('types').RouteData} b
|
|
406
|
-
* @param {Map<string, import('./types').Part[][]>} segment_map
|
|
407
|
-
*/
|
|
408
|
-
function compare(a, b, segment_map) {
|
|
409
|
-
const a_segments = /** @type {import('./types').Part[][]} */ (segment_map.get(a.id));
|
|
410
|
-
const b_segments = /** @type {import('./types').Part[][]} */ (segment_map.get(b.id));
|
|
411
|
-
|
|
412
|
-
const max_segments = Math.max(a_segments.length, b_segments.length);
|
|
413
|
-
for (let i = 0; i < max_segments; i += 1) {
|
|
414
|
-
const sa = a_segments[i];
|
|
415
|
-
const sb = b_segments[i];
|
|
416
|
-
|
|
417
|
-
// /x < /x/y, but /[...x]/y < /[...x]
|
|
418
|
-
if (!sa) return a.id.includes('[...') ? +1 : -1;
|
|
419
|
-
if (!sb) return b.id.includes('[...') ? -1 : +1;
|
|
420
|
-
|
|
421
|
-
const max_parts = Math.max(sa.length, sb.length);
|
|
422
|
-
for (let i = 0; i < max_parts; i += 1) {
|
|
423
|
-
const pa = sa[i];
|
|
424
|
-
const pb = sb[i];
|
|
425
|
-
|
|
426
|
-
// xy < x[y], but [x].json < [x]
|
|
427
|
-
if (pa === undefined) return pb.dynamic ? -1 : +1;
|
|
428
|
-
if (pb === undefined) return pa.dynamic ? +1 : -1;
|
|
429
|
-
|
|
430
|
-
// x < [x]
|
|
431
|
-
if (pa.dynamic !== pb.dynamic) {
|
|
432
|
-
return pa.dynamic ? +1 : -1;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (pa.dynamic) {
|
|
436
|
-
// [x] < [...x]
|
|
437
|
-
if (pa.rest !== pb.rest) {
|
|
438
|
-
return pa.rest ? +1 : -1;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// [x=type] < [x]
|
|
442
|
-
if (!!pa.type !== !!pb.type) {
|
|
443
|
-
return pa.type ? -1 : +1;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (!!a.endpoint !== !!b.endpoint) {
|
|
450
|
-
return a.endpoint ? -1 : +1;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return a < b ? -1 : 1;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
378
|
/** @param {string} dir */
|
|
457
379
|
function list_files(dir) {
|
|
458
380
|
/** @type {string[]} */
|
|
@@ -486,3 +408,63 @@ function count_occurrences(needle, haystack) {
|
|
|
486
408
|
}
|
|
487
409
|
return count;
|
|
488
410
|
}
|
|
411
|
+
|
|
412
|
+
/** @param {import('types').RouteData[]} routes */
|
|
413
|
+
function prevent_conflicts(routes) {
|
|
414
|
+
/** @type {Map<string, string>} */
|
|
415
|
+
const lookup = new Map();
|
|
416
|
+
|
|
417
|
+
for (const route of routes) {
|
|
418
|
+
if (!route.leaf && !route.endpoint) continue;
|
|
419
|
+
|
|
420
|
+
const normalized = normalize_route_id(route.id);
|
|
421
|
+
|
|
422
|
+
// find all permutations created by optional parameters
|
|
423
|
+
const split = normalized.split(/<\?(.+?)\>/g);
|
|
424
|
+
|
|
425
|
+
let permutations = [/** @type {string} */ (split[0])];
|
|
426
|
+
|
|
427
|
+
// turn `x/[[optional]]/y` into `x/y` and `x/[required]/y`
|
|
428
|
+
for (let i = 1; i < split.length; i += 2) {
|
|
429
|
+
const matcher = split[i];
|
|
430
|
+
const next = split[i + 1];
|
|
431
|
+
|
|
432
|
+
permutations = [
|
|
433
|
+
...permutations.map((x) => x + next),
|
|
434
|
+
...permutations.map((x) => x + `<${matcher}>${next}`)
|
|
435
|
+
];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const permutation of permutations) {
|
|
439
|
+
// remove leading/trailing/duplicated slashes caused by prior
|
|
440
|
+
// manipulation of optional parameters and (groups)
|
|
441
|
+
const key = permutation
|
|
442
|
+
.replace(/\/{2,}/, '/')
|
|
443
|
+
.replace(/^\//, '')
|
|
444
|
+
.replace(/\/$/, '');
|
|
445
|
+
|
|
446
|
+
if (lookup.has(key)) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`The "${lookup.get(key)}" and "${route.id}" routes conflict with each other`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
lookup.set(key, route.id);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** @param {string} id */
|
|
458
|
+
function normalize_route_id(id) {
|
|
459
|
+
return (
|
|
460
|
+
id
|
|
461
|
+
// remove groups
|
|
462
|
+
.replace(/(?<=^|\/)\(.+?\)(?=$|\/)/g, '')
|
|
463
|
+
|
|
464
|
+
// replace `[param]` with `<*>`, `[param=x]` with `<x>`, and `[[param]]` with `<?*>`
|
|
465
|
+
.replace(
|
|
466
|
+
/\[(?:(\[)|(\.\.\.))?.+?(=.+?)?\]\]?/g,
|
|
467
|
+
(_, optional, rest, matcher) => `<${optional ? '?' : ''}${rest ?? ''}${matcher ?? '*'}>`
|
|
468
|
+
)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { affects_path } from '../../../utils/routing.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{
|
|
5
|
+
* type: 'static' | 'required' | 'optional' | 'rest';
|
|
6
|
+
* content: string;
|
|
7
|
+
* matched: boolean;
|
|
8
|
+
* }} Part
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Part[]} Segment
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const EMPTY = { type: 'static', content: '', matched: false };
|
|
16
|
+
|
|
17
|
+
/** @param {import('types').RouteData[]} routes */
|
|
18
|
+
export function sort_routes(routes) {
|
|
19
|
+
/** @type {Map<string, Part[]>} */
|
|
20
|
+
const segment_cache = new Map();
|
|
21
|
+
|
|
22
|
+
/** @param {string} segment */
|
|
23
|
+
function get_parts(segment) {
|
|
24
|
+
if (!segment_cache.has(segment)) {
|
|
25
|
+
segment_cache.set(segment, split(segment));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return segment_cache.get(segment);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {string} id */
|
|
32
|
+
function split(id) {
|
|
33
|
+
/** @type {Part[]} */
|
|
34
|
+
const parts = [];
|
|
35
|
+
|
|
36
|
+
let i = 0;
|
|
37
|
+
while (i <= id.length) {
|
|
38
|
+
const start = id.indexOf('[', i);
|
|
39
|
+
if (start === -1) {
|
|
40
|
+
parts.push({ type: 'static', content: id.slice(i), matched: false });
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
parts.push({ type: 'static', content: id.slice(i, start), matched: false });
|
|
45
|
+
|
|
46
|
+
const type = id[start + 1] === '[' ? 'optional' : id[start + 1] === '.' ? 'rest' : 'required';
|
|
47
|
+
const delimiter = type === 'optional' ? ']]' : ']';
|
|
48
|
+
const end = id.indexOf(delimiter, start);
|
|
49
|
+
|
|
50
|
+
if (end === -1) {
|
|
51
|
+
throw new Error(`Invalid route ID ${id}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const content = id.slice(start, (i = end + delimiter.length));
|
|
55
|
+
|
|
56
|
+
parts.push({
|
|
57
|
+
type,
|
|
58
|
+
content,
|
|
59
|
+
matched: content.includes('=')
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return parts;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return routes.sort((route_a, route_b) => {
|
|
67
|
+
const segments_a = split_route_id(route_a.id).map(get_parts);
|
|
68
|
+
const segments_b = split_route_id(route_b.id).map(get_parts);
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < Math.max(segments_a.length, segments_b.length); i += 1) {
|
|
71
|
+
const segment_a = segments_a[i] ?? [EMPTY];
|
|
72
|
+
const segment_b = segments_b[i] ?? [EMPTY];
|
|
73
|
+
|
|
74
|
+
for (let j = 0; j < Math.max(segment_a.length, segment_b.length); j += 1) {
|
|
75
|
+
const a = segment_a[j];
|
|
76
|
+
const b = segment_b[j];
|
|
77
|
+
|
|
78
|
+
// first part of each segment is always static
|
|
79
|
+
// (though it may be the empty string), then
|
|
80
|
+
// it alternates between dynamic and static
|
|
81
|
+
// (i.e. [foo][bar] is disallowed)
|
|
82
|
+
const dynamic = j % 2 === 1;
|
|
83
|
+
|
|
84
|
+
if (dynamic) {
|
|
85
|
+
if (!a) return -1;
|
|
86
|
+
if (!b) return +1;
|
|
87
|
+
|
|
88
|
+
// get the next static chunk, so we can handle [...rest] edge cases
|
|
89
|
+
const next_a = segment_a[j + 1].content || segments_a[i + 1]?.[0].content;
|
|
90
|
+
const next_b = segment_b[j + 1].content || segments_b[i + 1]?.[0].content;
|
|
91
|
+
|
|
92
|
+
// `[...rest]/x` outranks `[...rest]`
|
|
93
|
+
if (a.type === 'rest' && b.type === 'rest') {
|
|
94
|
+
if (next_a && next_b) continue;
|
|
95
|
+
if (next_a) return -1;
|
|
96
|
+
if (next_b) return +1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// `[...rest]/x` outranks `[required]` or `[required]/[required]`
|
|
100
|
+
// but not `[required]/x`
|
|
101
|
+
if (a.type === 'rest') {
|
|
102
|
+
return next_a && !next_b ? -1 : +1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (b.type === 'rest') {
|
|
106
|
+
return next_b && !next_a ? +1 : -1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// part with matcher outranks one without
|
|
110
|
+
if (a.matched !== b.matched) {
|
|
111
|
+
return a.matched ? -1 : +1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (a.type !== b.type) {
|
|
115
|
+
// `[...rest]` has already been accounted for, so here
|
|
116
|
+
// we're comparing between `[required]` and `[[optional]]`
|
|
117
|
+
if (a.type === 'required') return -1;
|
|
118
|
+
if (b.type === 'required') return +1;
|
|
119
|
+
}
|
|
120
|
+
} else if (a.content !== b.content) {
|
|
121
|
+
// shallower path outranks deeper path
|
|
122
|
+
if (a === EMPTY) return -1;
|
|
123
|
+
if (b === EMPTY) return +1;
|
|
124
|
+
|
|
125
|
+
return sort_static(a.content, b.content);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return route_a.id < route_b.id ? +1 : -1;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @param {string} id */
|
|
135
|
+
function split_route_id(id) {
|
|
136
|
+
return (
|
|
137
|
+
id
|
|
138
|
+
// remove all [[optional]] parts unless they're at the very end
|
|
139
|
+
.replace(/\[\[[^\]]+\]\](?!$)/g, '')
|
|
140
|
+
.split('/')
|
|
141
|
+
.filter((segment) => segment !== '' && affects_path(segment))
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Sort two strings lexicographically, except `foobar` outranks `foo`
|
|
147
|
+
* @param {string} a
|
|
148
|
+
* @param {string} b
|
|
149
|
+
*/
|
|
150
|
+
function sort_static(a, b) {
|
|
151
|
+
if (a === b) return 0;
|
|
152
|
+
|
|
153
|
+
for (let i = 0; true; i += 1) {
|
|
154
|
+
const char_a = a[i];
|
|
155
|
+
const char_b = b[i];
|
|
156
|
+
|
|
157
|
+
if (char_a !== char_b) {
|
|
158
|
+
if (char_a === undefined) return +1;
|
|
159
|
+
if (char_b === undefined) return -1;
|
|
160
|
+
return char_a < char_b ? -1 : +1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/utils/routing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const param_pattern = /^(\.\.\.)?(\w+)(?:=(\w+))?$/;
|
|
1
|
+
const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;
|
|
2
2
|
|
|
3
3
|
/** @param {string} id */
|
|
4
4
|
export function parse_route_id(id) {
|
|
@@ -22,57 +22,70 @@ export function parse_route_id(id) {
|
|
|
22
22
|
.map((segment, i, segments) => {
|
|
23
23
|
const decoded_segment = decodeURIComponent(segment);
|
|
24
24
|
// special case — /[...rest]/ could contain zero segments
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
names.push(
|
|
28
|
-
types.push(
|
|
25
|
+
const rest_match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(decoded_segment);
|
|
26
|
+
if (rest_match) {
|
|
27
|
+
names.push(rest_match[1]);
|
|
28
|
+
types.push(rest_match[2]);
|
|
29
29
|
return '(?:/(.*))?';
|
|
30
30
|
}
|
|
31
|
+
// special case — /[[optional]]/ could contain zero segments
|
|
32
|
+
const optional_match = /^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(decoded_segment);
|
|
33
|
+
if (optional_match) {
|
|
34
|
+
names.push(optional_match[1]);
|
|
35
|
+
types.push(optional_match[2]);
|
|
36
|
+
return '(?:/([^/]+))?';
|
|
37
|
+
}
|
|
31
38
|
|
|
32
39
|
const is_last = i === segments.length - 1;
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
41
|
+
if (!decoded_segment) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parts = decoded_segment.split(/\[(.+?)\](?!\])/);
|
|
46
|
+
const result = parts
|
|
47
|
+
.map((content, i) => {
|
|
48
|
+
if (i % 2) {
|
|
49
|
+
const match = param_pattern.exec(content);
|
|
50
|
+
if (!match) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Invalid param: ${content}. Params and matcher names can only have underscores and alphanumeric characters.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [, optional, rest, name, type] = match;
|
|
57
|
+
// It's assumed that the following invalid route id cases are already checked
|
|
58
|
+
// - unbalanced brackets
|
|
59
|
+
// - optional param following rest param
|
|
60
|
+
|
|
61
|
+
names.push(name);
|
|
62
|
+
types.push(type);
|
|
63
|
+
return rest ? '(.*?)' : optional ? '([^/]*)?' : '([^/]+?)';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (is_last && content.includes('.')) add_trailing_slash = false;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
content // allow users to specify characters on the file system in an encoded manner
|
|
70
|
+
.normalize()
|
|
71
|
+
// We use [ and ] to denote parameters, so users must encode these on the file
|
|
72
|
+
// system to match against them. We don't decode all characters since others
|
|
73
|
+
// can already be epressed and so that '%' can be easily used directly in filenames
|
|
74
|
+
.replace(/%5[Bb]/g, '[')
|
|
75
|
+
.replace(/%5[Dd]/g, ']')
|
|
76
|
+
// '#', '/', and '?' can only appear in URL path segments in an encoded manner.
|
|
77
|
+
// They will not be touched by decodeURI so need to be encoded here, so
|
|
78
|
+
// that we can match against them.
|
|
79
|
+
// We skip '/' since you can't create a file with it on any OS
|
|
80
|
+
.replace(/#/g, '%23')
|
|
81
|
+
.replace(/\?/g, '%3F')
|
|
82
|
+
// escape characters that have special meaning in regex
|
|
83
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
84
|
+
); // TODO handle encoding
|
|
85
|
+
})
|
|
86
|
+
.join('');
|
|
87
|
+
|
|
88
|
+
return '/' + result;
|
|
76
89
|
})
|
|
77
90
|
.join('')}${add_trailing_slash ? '/?' : ''}$`
|
|
78
91
|
);
|
|
@@ -101,7 +114,7 @@ export function exec(match, names, types, matchers) {
|
|
|
101
114
|
for (let i = 0; i < names.length; i += 1) {
|
|
102
115
|
const name = names[i];
|
|
103
116
|
const type = types[i];
|
|
104
|
-
|
|
117
|
+
let value = match[i + 1] || '';
|
|
105
118
|
|
|
106
119
|
if (type) {
|
|
107
120
|
const matcher = matchers[type];
|