@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "1.0.0-next.518",
3
+ "version": "1.0.0-next.519",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
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, affects_path } from '../../../utils/routing.js';
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 {Map<string, import('types').RouteData>} */
86
- const route_map = new Map();
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
- const { pattern, names, types } = parse_route_id(id);
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
- const segments = id.split('/');
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
- segment_map.set(
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
- route_map.set(id, route);
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
- const root = /** @type {import('types').RouteData} */ (route_map.get(''));
228
- if (route_map.size === 1) {
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
- route_map.set('', {
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
- const root = /** @type {import('types').RouteData} */ (route_map.get(''));
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
- route_map.forEach((route) => {
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
- conflicts.set(normalized, route.id);
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
- route_map.forEach((route) => {
295
- if (!route.leaf) return;
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 { nodes, routes };
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
+ }
@@ -1,8 +1,8 @@
1
1
  import { PageNode } from 'types';
2
2
 
3
3
  interface Part {
4
- content: string;
5
4
  dynamic: boolean;
5
+ optional: boolean;
6
6
  rest: boolean;
7
7
  type: string | null;
8
8
  }
@@ -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 match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(decoded_segment);
26
- if (match) {
27
- names.push(match[1]);
28
- types.push(match[2]);
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
- return (
35
- decoded_segment &&
36
- '/' +
37
- decoded_segment
38
- .split(/\[(.+?)\]/)
39
- .map((content, i) => {
40
- if (i % 2) {
41
- const match = param_pattern.exec(content);
42
- if (!match) {
43
- throw new Error(
44
- `Invalid param: ${content}. Params and matcher names can only have underscores and alphanumeric characters.`
45
- );
46
- }
47
-
48
- const [, rest, name, type] = match;
49
- names.push(name);
50
- types.push(type);
51
- return rest ? '(.*?)' : '([^/]+?)';
52
- }
53
-
54
- if (is_last && content.includes('.')) add_trailing_slash = false;
55
-
56
- return (
57
- content // allow users to specify characters on the file system in an encoded manner
58
- .normalize()
59
- // We use [ and ] to denote parameters, so users must encode these on the file
60
- // system to match against them. We don't decode all characters since others
61
- // can already be epressed and so that '%' can be easily used directly in filenames
62
- .replace(/%5[Bb]/g, '[')
63
- .replace(/%5[Dd]/g, ']')
64
- // '#', '/', and '?' can only appear in URL path segments in an encoded manner.
65
- // They will not be touched by decodeURI so need to be encoded here, so
66
- // that we can match against them.
67
- // We skip '/' since you can't create a file with it on any OS
68
- .replace(/#/g, '%23')
69
- .replace(/\?/g, '%3F')
70
- // escape characters that have special meaning in regex
71
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
72
- ); // TODO handle encoding
73
- })
74
- .join('')
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
- const value = match[i + 1] || '';
117
+ let value = match[i + 1] || '';
105
118
 
106
119
  if (type) {
107
120
  const matcher = matchers[type];