@sveltejs/kit 1.0.0-next.305 → 1.0.0-next.306

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.
@@ -269,7 +269,7 @@ function parse_route_id(id) {
269
269
  ? /^\/$/
270
270
  : new RegExp(
271
271
  `^${decodeURIComponent(id)
272
- .split('/')
272
+ .split(/(?:@(?:~|[a-zA-Z0-9_-]*))?(?:\/|$)/)
273
273
  .map((segment, i, segments) => {
274
274
  // special case — /[...rest]/ could contain zero segments
275
275
  const match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
@@ -282,40 +282,41 @@ function parse_route_id(id) {
282
282
  const is_last = i === segments.length - 1;
283
283
 
284
284
  return (
285
+ segment &&
285
286
  '/' +
286
- segment
287
- .split(/\[(.+?)\]/)
288
- .map((content, i) => {
289
- if (i % 2) {
290
- const [, rest, name, type] = /** @type {RegExpMatchArray} */ (
291
- param_pattern.exec(content)
292
- );
293
- names.push(name);
294
- types.push(type);
295
- return rest ? '(.*?)' : '([^/]+?)';
296
- }
297
-
298
- if (is_last && content.includes('.')) add_trailing_slash = false;
299
-
300
- return (
301
- content // allow users to specify characters on the file system in an encoded manner
302
- .normalize()
303
- // We use [ and ] to denote parameters, so users must encode these on the file
304
- // system to match against them. We don't decode all characters since others
305
- // can already be epressed and so that '%' can be easily used directly in filenames
306
- .replace(/%5[Bb]/g, '[')
307
- .replace(/%5[Dd]/g, ']')
308
- // '#', '/', and '?' can only appear in URL path segments in an encoded manner.
309
- // They will not be touched by decodeURI so need to be encoded here, so
310
- // that we can match against them.
311
- // We skip '/' since you can't create a file with it on any OS
312
- .replace(/#/g, '%23')
313
- .replace(/\?/g, '%3F')
314
- // escape characters that have special meaning in regex
315
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
316
- ); // TODO handle encoding
317
- })
318
- .join('')
287
+ segment
288
+ .split(/\[(.+?)\]/)
289
+ .map((content, i) => {
290
+ if (i % 2) {
291
+ const [, rest, name, type] = /** @type {RegExpMatchArray} */ (
292
+ param_pattern.exec(content)
293
+ );
294
+ names.push(name);
295
+ types.push(type);
296
+ return rest ? '(.*?)' : '([^/]+?)';
297
+ }
298
+
299
+ if (is_last && content.includes('.')) add_trailing_slash = false;
300
+
301
+ return (
302
+ content // allow users to specify characters on the file system in an encoded manner
303
+ .normalize()
304
+ // We use [ and ] to denote parameters, so users must encode these on the file
305
+ // system to match against them. We don't decode all characters since others
306
+ // can already be epressed and so that '%' can be easily used directly in filenames
307
+ .replace(/%5[Bb]/g, '[')
308
+ .replace(/%5[Dd]/g, ']')
309
+ // '#', '/', and '?' can only appear in URL path segments in an encoded manner.
310
+ // They will not be touched by decodeURI so need to be encoded here, so
311
+ // that we can match against them.
312
+ // We skip '/' since you can't create a file with it on any OS
313
+ .replace(/#/g, '%23')
314
+ .replace(/\?/g, '%3F')
315
+ // escape characters that have special meaning in regex
316
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
317
+ ); // TODO handle encoding
318
+ })
319
+ .join('')
319
320
  );
320
321
  })
321
322
  .join('')}${add_trailing_slash ? '/?' : ''}$`
@@ -2181,7 +2181,8 @@ async function respond$1(opts) {
2181
2181
 
2182
2182
  try {
2183
2183
  nodes = await Promise.all(
2184
- route.a.map((n) => options.manifest._.nodes[n] && options.manifest._.nodes[n]())
2184
+ // we use == here rather than === because [undefined] serializes as "[null]"
2185
+ route.a.map((n) => (n == undefined ? n : options.manifest._.nodes[n]()))
2185
2186
  );
2186
2187
  } catch (err) {
2187
2188
  const error = coalesce_to_error(err);
@@ -2280,7 +2281,8 @@ async function respond$1(opts) {
2280
2281
  if (error) {
2281
2282
  while (i--) {
2282
2283
  if (route.b[i]) {
2283
- const error_node = await options.manifest._.nodes[route.b[i]]();
2284
+ const index = /** @type {number} */ (route.b[i]);
2285
+ const error_node = await options.manifest._.nodes[index]();
2284
2286
 
2285
2287
  /** @type {Loaded} */
2286
2288
  let node_loaded;
@@ -12,6 +12,7 @@ import { getRequest, setResponse } from '../node.js';
12
12
  import { sequence } from '../hooks.js';
13
13
  import { p as posixify } from './filesystem.js';
14
14
  import { p as parse_route_id } from './misc.js';
15
+ import { n as normalize_path } from './url.js';
15
16
  import 'sade';
16
17
  import 'child_process';
17
18
  import 'net';
@@ -131,8 +132,8 @@ async function create_plugin(config, cwd) {
131
132
  return await vite.ssrLoadModule(url, { fixStacktrace: false });
132
133
  }
133
134
  : null,
134
- a: route.a.map((id) => manifest_data.components.indexOf(id)),
135
- b: route.b.map((id) => manifest_data.components.indexOf(id))
135
+ a: route.a.map((id) => (id ? manifest_data.components.indexOf(id) : undefined)),
136
+ b: route.b.map((id) => (id ? manifest_data.components.indexOf(id) : undefined))
136
137
  };
137
138
  }
138
139
 
@@ -214,7 +215,13 @@ async function create_plugin(config, cwd) {
214
215
 
215
216
  if (req.url === '/favicon.ico') return not_found(res);
216
217
 
217
- if (!decoded.startsWith(config.kit.paths.base)) return not_found(res);
218
+ if (!decoded.startsWith(config.kit.paths.base)) {
219
+ const suggestion = normalize_path(
220
+ config.kit.paths.base + req.url,
221
+ config.kit.trailingSlash
222
+ );
223
+ return not_found(res, `Not found (did you mean ${suggestion}?)`);
224
+ }
218
225
 
219
226
  /** @type {Partial<import('types').Hooks>} */
220
227
  const user_hooks = resolve_entry(config.kit.files.hooks)
@@ -370,9 +377,9 @@ async function create_plugin(config, cwd) {
370
377
  }
371
378
 
372
379
  /** @param {import('http').ServerResponse} res */
373
- function not_found(res) {
380
+ function not_found(res, message = 'Not found') {
374
381
  res.statusCode = 404;
375
- res.end('Not found');
382
+ res.end(message);
376
383
  }
377
384
 
378
385
  /**
@@ -71,6 +71,7 @@ async function build_service_worker(
71
71
 
72
72
  export const files = [
73
73
  ${manifest_data.assets
74
+ .filter((asset) => config.kit.serviceWorker.files(asset.file))
74
75
  .map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`)
75
76
  .join(',\n\t\t\t\t')}
76
77
  ];
@@ -54,7 +54,7 @@ function generate_manifest({ build_data, relative_path, routes, format = 'esm' }
54
54
  assets.push(build_data.service_worker);
55
55
  }
56
56
 
57
- /** @param {string} id */
57
+ /** @param {string | undefined} id */
58
58
  const get_index = (id) => id && /** @type {LookupEntry} */ (bundled_nodes.get(id)).index;
59
59
 
60
60
  const matchers = new Set();
@@ -17,7 +17,7 @@ function parse_route_id(id) {
17
17
  ? /^\/$/
18
18
  : new RegExp(
19
19
  `^${decodeURIComponent(id)
20
- .split('/')
20
+ .split(/(?:@(?:~|[a-zA-Z0-9_-]*))?(?:\/|$)/)
21
21
  .map((segment, i, segments) => {
22
22
  // special case — /[...rest]/ could contain zero segments
23
23
  const match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
@@ -30,40 +30,41 @@ function parse_route_id(id) {
30
30
  const is_last = i === segments.length - 1;
31
31
 
32
32
  return (
33
+ segment &&
33
34
  '/' +
34
- segment
35
- .split(/\[(.+?)\]/)
36
- .map((content, i) => {
37
- if (i % 2) {
38
- const [, rest, name, type] = /** @type {RegExpMatchArray} */ (
39
- param_pattern.exec(content)
40
- );
41
- names.push(name);
42
- types.push(type);
43
- return rest ? '(.*?)' : '([^/]+?)';
44
- }
35
+ segment
36
+ .split(/\[(.+?)\]/)
37
+ .map((content, i) => {
38
+ if (i % 2) {
39
+ const [, rest, name, type] = /** @type {RegExpMatchArray} */ (
40
+ param_pattern.exec(content)
41
+ );
42
+ names.push(name);
43
+ types.push(type);
44
+ return rest ? '(.*?)' : '([^/]+?)';
45
+ }
45
46
 
46
- if (is_last && content.includes('.')) add_trailing_slash = false;
47
+ if (is_last && content.includes('.')) add_trailing_slash = false;
47
48
 
48
- return (
49
- content // allow users to specify characters on the file system in an encoded manner
50
- .normalize()
51
- // We use [ and ] to denote parameters, so users must encode these on the file
52
- // system to match against them. We don't decode all characters since others
53
- // can already be epressed and so that '%' can be easily used directly in filenames
54
- .replace(/%5[Bb]/g, '[')
55
- .replace(/%5[Dd]/g, ']')
56
- // '#', '/', and '?' can only appear in URL path segments in an encoded manner.
57
- // They will not be touched by decodeURI so need to be encoded here, so
58
- // that we can match against them.
59
- // We skip '/' since you can't create a file with it on any OS
60
- .replace(/#/g, '%23')
61
- .replace(/\?/g, '%3F')
62
- // escape characters that have special meaning in regex
63
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
64
- ); // TODO handle encoding
65
- })
66
- .join('')
49
+ return (
50
+ content // allow users to specify characters on the file system in an encoded manner
51
+ .normalize()
52
+ // We use [ and ] to denote parameters, so users must encode these on the file
53
+ // system to match against them. We don't decode all characters since others
54
+ // can already be epressed and so that '%' can be easily used directly in filenames
55
+ .replace(/%5[Bb]/g, '[')
56
+ .replace(/%5[Dd]/g, ']')
57
+ // '#', '/', and '?' can only appear in URL path segments in an encoded manner.
58
+ // They will not be touched by decodeURI so need to be encoded here, so
59
+ // that we can match against them.
60
+ // We skip '/' since you can't create a file with it on any OS
61
+ .replace(/#/g, '%23')
62
+ .replace(/\?/g, '%3F')
63
+ // escape characters that have special meaning in regex
64
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
65
+ ); // TODO handle encoding
66
+ })
67
+ .join('')
67
68
  );
68
69
  })
69
70
  .join('')}${add_trailing_slash ? '/?' : ''}$`
@@ -121,18 +121,39 @@ var mime = new Mime(standard, other);
121
121
  * rest: boolean;
122
122
  * type: string | null;
123
123
  * }} Part
124
+ */
125
+
126
+ /**
127
+ * A route, consisting of an endpoint module and/or an array of components
128
+ * (n layouts and one leaf) for successful navigations and an array of
129
+ * n error components to render if navigation fails
130
+ * @typedef {{
131
+ * id: string;
132
+ * pattern: RegExp;
133
+ * segments: Part[][];
134
+ * page?: {
135
+ * a: Array<string | undefined>;
136
+ * b: Array<string | undefined>;
137
+ * };
138
+ * endpoint?: string;
139
+ * }} Unit
140
+ */
141
+
142
+ /**
124
143
  * @typedef {{
125
- * name: string;
126
- * parts: Part[],
127
- * file: string;
128
- * is_dir: boolean;
129
- * is_index: boolean;
130
- * is_page: boolean;
131
- * route_suffix: string
132
- * }} Item
144
+ * error: string | undefined;
145
+ * layouts: Record<string, { file: string, name: string }>
146
+ * }} Node
147
+ */
148
+
149
+ /**
150
+ * @typedef {Map<string, Node>} Tree
133
151
  */
134
152
 
135
- const specials = new Set(['__layout', '__layout.reset', '__error', '__tests__', '__test__']);
153
+ const layout_pattern = /^__layout(?:-([a-zA-Z0-9_-]+))?(?:@([a-zA-Z0-9_-]+))?$/;
154
+ const dunder_pattern = /(^|\/)__(?!tests?__)/; // forbid __-prefixed files/directories except __error, __layout[-...], __test__, __tests__
155
+
156
+ const DEFAULT = 'default';
136
157
 
137
158
  /**
138
159
  * @param {{
@@ -147,172 +168,199 @@ function create_manifest_data({
147
168
  fallback = `${get_runtime_path(config)}/components`,
148
169
  cwd = process.cwd()
149
170
  }) {
150
- /**
151
- * @param {string} file_name
152
- * @param {string} dir
153
- */
154
- function find_layout(file_name, dir) {
155
- const files = config.extensions.map((ext) => posixify(path__default.join(dir, `${file_name}${ext}`)));
156
- return files.find((file) => fs__default.existsSync(path__default.resolve(cwd, file)));
157
- }
158
-
159
- /** @type {string[]} */
160
- const components = [];
161
-
162
171
  /** @type {import('types').RouteData[]} */
163
172
  const routes = [];
164
173
 
165
- const default_layout = posixify(path__default.relative(cwd, `${fallback}/layout.svelte`));
166
- const default_error = posixify(path__default.relative(cwd, `${fallback}/error.svelte`));
174
+ /** @type {Map<string, Unit>} */
175
+ const units = new Map();
167
176
 
168
- /**
169
- * @param {string} dir
170
- * @param {string[]} parent_id
171
- * @param {Array<string|undefined>} layout_stack // accumulated __layout.svelte components
172
- * @param {Array<string|undefined>} error_stack // accumulated __error.svelte components
173
- */
174
- function walk(dir, parent_id, layout_stack, error_stack) {
175
- /** @type {Item[]} */
176
- const items = [];
177
+ /** @type {Tree} */
178
+ const tree = new Map();
177
179
 
178
- fs__default.readdirSync(dir).forEach((basename) => {
179
- const resolved = path__default.join(dir, basename);
180
- const file = posixify(path__default.relative(cwd, resolved));
181
- const is_dir = fs__default.statSync(resolved).isDirectory();
182
-
183
- const ext = is_dir
184
- ? ''
185
- : config.extensions.find((ext) => basename.endsWith(ext)) ||
186
- config.kit.endpointExtensions.find((ext) => basename.endsWith(ext));
180
+ const default_layout = {
181
+ file: posixify(path__default.relative(cwd, `${fallback}/layout.svelte`)),
182
+ name: DEFAULT
183
+ };
187
184
 
188
- if (ext === undefined) return;
185
+ // set default root layout/error
186
+ tree.set('', {
187
+ error: posixify(path__default.relative(cwd, `${fallback}/error.svelte`)),
188
+ layouts: { [DEFAULT]: default_layout }
189
+ });
189
190
 
190
- const name = basename.slice(0, basename.length - ext.length);
191
+ const routes_base = posixify(path__default.relative(cwd, config.kit.files.routes));
192
+ const valid_extensions = [...config.extensions, ...config.kit.endpointExtensions];
191
193
 
192
- if (name.startsWith('__') && !specials.has(name)) {
193
- throw new Error(`Files and directories prefixed with __ are reserved (saw ${file})`);
194
- }
194
+ list_files(config.kit.files.routes).forEach((file) => {
195
+ const extension = valid_extensions.find((ext) => file.endsWith(ext));
196
+ if (!extension) return;
195
197
 
196
- if (!config.kit.routes(file)) return;
198
+ const id = file.slice(0, -extension.length).replace(/\/?index(\.[a-z]+)?$/, '$1');
199
+ const project_relative = `${routes_base}/${file}`;
197
200
 
198
- items.push({
199
- file,
200
- name,
201
- parts: get_parts(name, file),
202
- route_suffix: basename.slice(basename.indexOf('.'), -ext.length),
203
- is_dir,
204
- is_index: !is_dir && basename.startsWith('index.'),
205
- is_page: config.extensions.includes(ext)
206
- });
207
- });
201
+ const segments = id.split('/');
202
+ const name = /** @type {string} */ (segments.pop());
208
203
 
209
- items.sort(comparator);
204
+ if (name === '__layout.reset') {
205
+ throw new Error(
206
+ '__layout.reset has been removed in favour of named layouts: https://kit.svelte.dev/docs/layouts#named-layouts'
207
+ );
208
+ }
210
209
 
211
- items.forEach((item) => {
212
- const id_parts = parent_id.slice();
210
+ if (name === '__error' || layout_pattern.test(name)) {
211
+ const dir = segments.join('/');
213
212
 
214
- if (item.is_index) {
215
- if (item.route_suffix && id_parts.length > 0) {
216
- id_parts[id_parts.length - 1] += item.route_suffix;
217
- }
218
- } else {
219
- id_parts.push(item.name);
213
+ if (!tree.has(dir)) {
214
+ tree.set(dir, {
215
+ error: undefined,
216
+ layouts: {}
217
+ });
220
218
  }
221
219
 
222
- if (item.is_dir) {
223
- const layout_reset = find_layout('__layout.reset', item.file);
224
- const layout = find_layout('__layout', item.file);
225
- const error = find_layout('__error', item.file);
220
+ const group = /** @type {Node} */ (tree.get(dir));
221
+
222
+ if (name === '__error') {
223
+ group.error = project_relative;
224
+ } else {
225
+ const match = /** @type {RegExpMatchArray} */ (layout_pattern.exec(name));
226
226
 
227
- if (layout_reset && layout) {
228
- throw new Error(`Cannot have __layout next to __layout.reset: ${layout_reset}`);
227
+ if (match[1] === DEFAULT) {
228
+ throw new Error(`${project_relative} cannot use reserved "${DEFAULT}" name`);
229
229
  }
230
230
 
231
- if (layout_reset) components.push(layout_reset);
232
- if (layout) components.push(layout);
233
- if (error) components.push(error);
231
+ const layout_id = match[1] || DEFAULT;
234
232
 
235
- walk(
236
- path__default.join(dir, item.name),
237
- id_parts,
238
- layout_reset ? [layout_reset] : layout_stack.concat(layout),
239
- layout_reset ? [error] : error_stack.concat(error)
240
- );
241
- } else {
242
- const id = id_parts.join('/');
243
- const { pattern } = parse_route_id(id);
233
+ const defined = group.layouts[layout_id];
234
+ if (defined && defined !== default_layout) {
235
+ throw new Error(
236
+ `Duplicate layout ${project_relative} already defined at ${defined.file}`
237
+ );
238
+ }
244
239
 
245
- if (item.is_page) {
246
- components.push(item.file);
240
+ group.layouts[layout_id] = {
241
+ file: project_relative,
242
+ name
243
+ };
244
+ }
247
245
 
248
- const concatenated = layout_stack.concat(item.file);
249
- const errors = error_stack.slice();
246
+ return;
247
+ } else if (dunder_pattern.test(file)) {
248
+ throw new Error(
249
+ `Files and directories prefixed with __ are reserved (saw ${project_relative})`
250
+ );
251
+ }
250
252
 
251
- let i = concatenated.length;
252
- while (i--) {
253
- if (!errors[i] && !concatenated[i]) {
254
- errors.splice(i, 1);
255
- concatenated.splice(i, 1);
256
- }
257
- }
253
+ if (!config.kit.routes(file)) return;
258
254
 
259
- i = errors.length;
260
- while (i--) {
261
- if (errors[i]) break;
262
- }
255
+ if (/\]\[/.test(id)) {
256
+ throw new Error(`Invalid route ${project_relative} — parameters must be separated`);
257
+ }
263
258
 
264
- errors.splice(i + 1);
259
+ if (count_occurrences('[', id) !== count_occurrences(']', id)) {
260
+ throw new Error(`Invalid route ${project_relative} — brackets are unbalanced`);
261
+ }
265
262
 
266
- const path = id.includes('[') ? '' : `/${id}`;
263
+ if (!units.has(id)) {
264
+ units.set(id, {
265
+ id,
266
+ pattern: parse_route_id(id).pattern,
267
+ segments: id
268
+ .split('/')
269
+ .filter(Boolean)
270
+ .map((segment) => {
271
+ /** @type {Part[]} */
272
+ const parts = [];
273
+ segment.split(/\[(.+?)\]/).map((content, i) => {
274
+ const dynamic = !!(i % 2);
275
+
276
+ if (!content) return;
277
+
278
+ parts.push({
279
+ content,
280
+ dynamic,
281
+ rest: dynamic && content.startsWith('...'),
282
+ type: (dynamic && content.split('=')[1]) || null
283
+ });
284
+ });
285
+ return parts;
286
+ }),
287
+ page: undefined,
288
+ endpoint: undefined
289
+ });
290
+ }
267
291
 
268
- routes.push({
269
- type: 'page',
270
- id,
271
- pattern,
272
- path,
273
- shadow: null,
274
- a: /** @type {string[]} */ (concatenated),
275
- b: /** @type {string[]} */ (errors)
276
- });
277
- } else {
278
- routes.push({
279
- type: 'endpoint',
280
- id,
281
- pattern,
282
- file: item.file
283
- });
284
- }
285
- }
286
- });
287
- }
292
+ const unit = /** @type {Unit} */ (units.get(id));
288
293
 
289
- const routes_base = path__default.relative(cwd, config.kit.files.routes);
294
+ if (config.extensions.find((ext) => file.endsWith(ext))) {
295
+ const { layouts, errors } = trace(project_relative, file, tree, config.extensions);
296
+ unit.page = {
297
+ a: layouts.concat(project_relative),
298
+ b: errors
299
+ };
300
+ } else {
301
+ unit.endpoint = project_relative;
302
+ }
303
+ });
290
304
 
291
- const layout = find_layout('__layout', routes_base) || default_layout;
292
- const error = find_layout('__error', routes_base) || default_error;
305
+ /** @type {string[]} */
306
+ const components = [];
293
307
 
294
- components.push(layout, error);
308
+ tree.forEach(({ layouts, error }) => {
309
+ // we do [default, error, ...other_layouts] so that components[0] and [1]
310
+ // are the root layout/error. kinda janky, there's probably a nicer way
311
+ if (layouts[DEFAULT]) {
312
+ components.push(layouts[DEFAULT].file);
313
+ }
295
314
 
296
- walk(config.kit.files.routes, [], [layout], [error]);
315
+ if (error) {
316
+ components.push(error);
317
+ }
297
318
 
298
- const lookup = new Map();
299
- for (const route of routes) {
300
- if (route.type === 'page') {
301
- lookup.set(route.id, route);
319
+ for (const id in layouts) {
320
+ if (id !== DEFAULT) components.push(layouts[id].file);
302
321
  }
303
- }
322
+ });
304
323
 
305
- let i = routes.length;
306
- while (i--) {
307
- const route = routes[i];
308
- if (route.type === 'endpoint' && lookup.has(route.id)) {
309
- lookup.get(route.id).shadow = route.file;
310
- routes.splice(i, 1);
324
+ units.forEach((unit) => {
325
+ if (unit.page) {
326
+ const leaf = /** @type {string} */ (unit.page.a[unit.page.a.length - 1]);
327
+ components.push(leaf);
311
328
  }
312
- }
329
+ });
313
330
 
331
+ Array.from(units.values())
332
+ .sort(compare)
333
+ .forEach((unit) => {
334
+ // TODO when we introduce layout endpoints and scoped middlewares, we
335
+ // will probably want to have a single unified route type here
336
+ // (created in the list_files(...).forEach(...) callback)
337
+ if (unit.page) {
338
+ routes.push({
339
+ type: 'page',
340
+ id: unit.id,
341
+ pattern: unit.pattern,
342
+ path: unit.id.includes('[') ? '' : `/${unit.id.replace(/@(?:[a-zA-Z0-9_-]+)/g, '')}`,
343
+ shadow: unit.endpoint || null,
344
+ a: unit.page.a,
345
+ b: unit.page.b
346
+ });
347
+ } else if (unit.endpoint) {
348
+ routes.push({
349
+ type: 'endpoint',
350
+ id: unit.id,
351
+ pattern: unit.pattern,
352
+ file: unit.endpoint
353
+ });
354
+ }
355
+ });
356
+
357
+ /** @type {import('types').Asset[]} */
314
358
  const assets = fs__default.existsSync(config.kit.files.assets)
315
- ? list_files({ config, dir: config.kit.files.assets, path: '' })
359
+ ? list_files(config.kit.files.assets).map((file) => ({
360
+ file,
361
+ size: fs__default.statSync(`${config.kit.files.assets}/${file}`).size,
362
+ type: mime.getType(file)
363
+ }))
316
364
  : [];
317
365
 
318
366
  const params_base = path__default.relative(cwd, config.kit.files.params);
@@ -336,8 +384,6 @@ function create_manifest_data({
336
384
 
337
385
  return {
338
386
  assets,
339
- layout,
340
- error,
341
387
  components,
342
388
  routes,
343
389
  matchers
@@ -345,142 +391,162 @@ function create_manifest_data({
345
391
  }
346
392
 
347
393
  /**
348
- * @param {string} needle
349
- * @param {string} haystack
394
+ * @param {string} file
395
+ * @param {string} path
396
+ * @param {Tree} tree
397
+ * @param {string[]} extensions
350
398
  */
351
- function count_occurrences(needle, haystack) {
352
- let count = 0;
353
- for (let i = 0; i < haystack.length; i += 1) {
354
- if (haystack[i] === needle) count += 1;
355
- }
356
- return count;
357
- }
358
-
359
- const spread_pattern = /\[\.{3}/;
399
+ function trace(file, path, tree, extensions) {
400
+ /** @type {Array<string | undefined>} */
401
+ const layouts = [];
402
+
403
+ /** @type {Array<string | undefined>} */
404
+ const errors = [];
405
+
406
+ const parts = path.split('/');
407
+ const filename = /** @type {string} */ (parts.pop());
408
+ const extension = /** @type {string} */ (extensions.find((ext) => path.endsWith(ext)));
409
+ const base = filename.slice(0, -extension.length);
410
+
411
+ let layout_id = base.includes('@') ? base.split('@')[1] : DEFAULT;
412
+
413
+ // walk up the tree, find which __layout and __error components
414
+ // apply to this page
415
+ // eslint-disable-next-line
416
+ while (true) {
417
+ const node = tree.get(parts.join('/'));
418
+ const layout = node?.layouts[layout_id];
419
+
420
+ // any segment that has neither a __layout nor an __error can be discarded.
421
+ // in other words these...
422
+ // layouts: [a, , b, c]
423
+ // errors: [d, , e, ]
424
+ //
425
+ // ...can be compacted to these:
426
+ // layouts: [a, b, c]
427
+ // errors: [d, e, ]
428
+ if (node?.error || layout?.file) {
429
+ errors.unshift(node?.error);
430
+ layouts.unshift(layout?.file);
431
+ }
360
432
 
361
- /**
362
- * @param {Item} a
363
- * @param {Item} b
364
- */
365
- function comparator(a, b) {
366
- if (a.is_index !== b.is_index) {
367
- if (a.is_index) return spread_pattern.test(a.file) ? 1 : -1;
368
- return spread_pattern.test(b.file) ? -1 : 1;
433
+ if (layout?.name.includes('@')) {
434
+ layout_id = layout.name.split('@')[1];
435
+ } else {
436
+ if (layout) layout_id = DEFAULT;
437
+ if (parts.length === 0) break;
438
+ parts.pop();
439
+ }
369
440
  }
370
441
 
371
- const max = Math.max(a.parts.length, b.parts.length);
442
+ if (layout_id !== DEFAULT) {
443
+ throw new Error(`${file} references missing layout "${layout_id}"`);
444
+ }
372
445
 
373
- for (let i = 0; i < max; i += 1) {
374
- const a_sub_part = a.parts[i];
375
- const b_sub_part = b.parts[i];
446
+ // trim empty space off the end of the errors array
447
+ let i = errors.length;
448
+ while (i--) if (errors[i]) break;
449
+ errors.length = i + 1;
376
450
 
377
- if (!a_sub_part) return 1; // b is more specific, so goes first
378
- if (!b_sub_part) return -1;
451
+ return { layouts, errors };
452
+ }
379
453
 
380
- if (a_sub_part.rest && b_sub_part.rest) {
381
- if (a.is_page !== b.is_page) {
382
- return a.is_page ? 1 : -1;
454
+ /**
455
+ * @param {Unit} a
456
+ * @param {Unit} b
457
+ */
458
+ function compare(a, b) {
459
+ const max_segments = Math.max(a.segments.length, b.segments.length);
460
+ for (let i = 0; i < max_segments; i += 1) {
461
+ const sa = a.segments[i];
462
+ const sb = b.segments[i];
463
+
464
+ // /x < /x/y, but /[...x]/y < /[...x]
465
+ if (!sa) return a.id.includes('[...') ? +1 : -1;
466
+ if (!sb) return b.id.includes('[...') ? -1 : +1;
467
+
468
+ const max_parts = Math.max(sa.length, sb.length);
469
+ for (let i = 0; i < max_parts; i += 1) {
470
+ const pa = sa[i];
471
+ const pb = sb[i];
472
+
473
+ // xy < x[y], but [x].json < [x]
474
+ if (pa === undefined) return pb.dynamic ? -1 : +1;
475
+ if (pb === undefined) return pa.dynamic ? +1 : -1;
476
+
477
+ // x < [x]
478
+ if (pa.dynamic !== pb.dynamic) {
479
+ return pa.dynamic ? +1 : -1;
383
480
  }
384
- // sort alphabetically
385
- return a_sub_part.content < b_sub_part.content ? -1 : 1;
386
- }
387
481
 
388
- // If one is ...rest order it later
389
- if (a_sub_part.rest !== b_sub_part.rest) return a_sub_part.rest ? 1 : -1;
390
-
391
- if (a_sub_part.dynamic !== b_sub_part.dynamic) {
392
- return a_sub_part.dynamic ? 1 : -1;
393
- }
394
-
395
- if (a_sub_part.dynamic && !!a_sub_part.type !== !!b_sub_part.type) {
396
- return a_sub_part.type ? -1 : 1;
397
- }
482
+ if (pa.dynamic) {
483
+ // [x] < [...x]
484
+ if (pa.rest !== pb.rest) {
485
+ return pa.rest ? +1 : -1;
486
+ }
398
487
 
399
- if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) {
400
- return (
401
- b_sub_part.content.length - a_sub_part.content.length ||
402
- (a_sub_part.content < b_sub_part.content ? -1 : 1)
403
- );
488
+ // [x=type] < [x]
489
+ if (!!pa.type !== !!pb.type) {
490
+ return pa.type ? -1 : +1;
491
+ }
492
+ }
404
493
  }
405
494
  }
406
495
 
407
- if (a.is_page !== b.is_page) {
408
- return a.is_page ? 1 : -1;
496
+ const a_is_endpoint = !a.page && a.endpoint;
497
+ const b_is_endpoint = !b.page && b.endpoint;
498
+
499
+ if (a_is_endpoint !== b_is_endpoint) {
500
+ return a_is_endpoint ? -1 : +1;
409
501
  }
410
502
 
411
- // otherwise sort alphabetically
412
- return a.file < b.file ? -1 : 1;
503
+ return a < b ? -1 : 1;
413
504
  }
414
505
 
415
506
  /**
416
- * @param {string} part
417
- * @param {string} file
507
+ * @param {string} needle
508
+ * @param {string} haystack
418
509
  */
419
- function get_parts(part, file) {
420
- if (/\]\[/.test(part)) {
421
- throw new Error(`Invalid route ${file} parameters must be separated`);
422
- }
423
-
424
- if (count_occurrences('[', part) !== count_occurrences(']', part)) {
425
- throw new Error(`Invalid route ${file} — brackets are unbalanced`);
510
+ function count_occurrences(needle, haystack) {
511
+ let count = 0;
512
+ for (let i = 0; i < haystack.length; i += 1) {
513
+ if (haystack[i] === needle) count += 1;
426
514
  }
427
-
428
- /** @type {Part[]} */
429
- const result = [];
430
- part.split(/\[(.+?\(.+?\)|.+?)\]/).map((str, i) => {
431
- if (!str) return;
432
- const dynamic = i % 2 === 1;
433
-
434
- const [, content, type] = dynamic
435
- ? /^((?:\.\.\.)?[a-zA-Z_][a-zA-Z0-9_]*)(?:=([a-zA-Z_][a-zA-Z0-9_]*))?$/.exec(str) || [
436
- null,
437
- null,
438
- null
439
- ]
440
- : [null, str, null];
441
-
442
- if (!content) {
443
- throw new Error(
444
- `Invalid route ${file} — parameter name and type must match /^[a-zA-Z_][a-zA-Z0-9_]*$/`
445
- );
446
- }
447
-
448
- result.push({
449
- content,
450
- dynamic,
451
- rest: dynamic && /^\.{3}.+$/.test(content),
452
- type
453
- });
454
- });
455
-
456
- return result;
515
+ return count;
457
516
  }
458
517
 
459
518
  /**
460
- * @param {{
461
- * config: import('types').ValidatedConfig;
462
- * dir: string;
463
- * path: string;
464
- * files?: import('types').Asset[]
465
- * }} args
519
+ * @param {string} dir
520
+ * @param {string} [path]
521
+ * @param {string[]} [files]
466
522
  */
467
- function list_files({ config, dir, path, files = [] }) {
468
- fs__default.readdirSync(dir).forEach((file) => {
469
- const full = `${dir}/${file}`;
470
-
471
- const stats = fs__default.statSync(full);
472
- const joined = path ? `${path}/${file}` : file;
473
-
474
- if (stats.isDirectory()) {
475
- list_files({ config, dir: full, path: joined, files });
476
- } else if (config.kit.serviceWorker.files(joined)) {
477
- files.push({
478
- file: joined,
479
- size: stats.size,
480
- type: mime.getType(joined)
481
- });
482
- }
483
- });
523
+ function list_files(dir, path = '', files = []) {
524
+ fs__default.readdirSync(dir, { withFileTypes: true })
525
+ .sort(({ name: a }, { name: b }) => {
526
+ // sort each directory in (__layout, __error, everything else) order
527
+ // so that we can trace layouts/errors immediately
528
+
529
+ if (a.startsWith('__layout')) {
530
+ if (!b.startsWith('__layout')) return -1;
531
+ } else if (b.startsWith('__layout')) {
532
+ return 1;
533
+ } else if (a.startsWith('__')) {
534
+ if (!b.startsWith('__')) return -1;
535
+ } else if (b.startsWith('__')) {
536
+ return 1;
537
+ }
538
+
539
+ return a < b ? -1 : 1;
540
+ })
541
+ .forEach((file) => {
542
+ const joined = path ? `${path}/${file.name}` : file.name;
543
+
544
+ if (file.isDirectory()) {
545
+ list_files(`${dir}/${file.name}`, joined, files);
546
+ } else {
547
+ files.push(joined);
548
+ }
549
+ });
484
550
 
485
551
  return files;
486
552
  }
@@ -549,7 +615,7 @@ function write_manifest(manifest_data, base, output) {
549
615
  .join(',\n\t\t\t\t\t')}
550
616
  ]`.replace(/^\t/gm, '');
551
617
 
552
- /** @param {string[]} parts */
618
+ /** @param {Array<string | undefined>} parts */
553
619
  const get_indices = (parts) =>
554
620
  `[${parts.map((part) => (part ? component_indexes[part] : '')).join(', ')}]`;
555
621
 
package/dist/cli.js CHANGED
@@ -853,8 +853,9 @@ function handle_error(e) {
853
853
  /**
854
854
  * @param {number} port
855
855
  * @param {boolean} https
856
+ * @param {string} base
856
857
  */
857
- async function launch(port, https) {
858
+ async function launch(port, https, base) {
858
859
  const { exec } = await import('child_process');
859
860
  let cmd = 'open';
860
861
  if (process.platform == 'win32') {
@@ -866,10 +867,10 @@ async function launch(port, https) {
866
867
  cmd = 'xdg-open';
867
868
  }
868
869
  }
869
- exec(`${cmd} ${https ? 'https' : 'http'}://localhost:${port}`);
870
+ exec(`${cmd} ${https ? 'https' : 'http'}://localhost:${port}${base}`);
870
871
  }
871
872
 
872
- const prog = sade('svelte-kit').version('1.0.0-next.305');
873
+ const prog = sade('svelte-kit').version('1.0.0-next.306');
873
874
 
874
875
  prog
875
876
  .command('dev')
@@ -903,6 +904,7 @@ prog
903
904
  host: address_info.address,
904
905
  https: !!(https || server_config.https),
905
906
  open: open || !!server_config.open,
907
+ base: config.kit.paths.base,
906
908
  loose: server_config.fs.strict === false,
907
909
  allow: server_config.fs.allow,
908
910
  cwd
@@ -970,7 +972,7 @@ prog
970
972
 
971
973
  await preview({ port, host, config, https });
972
974
 
973
- welcome({ port, host, https, open });
975
+ welcome({ port, host, https, open, base: config.kit.paths.base });
974
976
  } catch (error) {
975
977
  handle_error(error);
976
978
  }
@@ -1039,15 +1041,16 @@ async function check_port(port) {
1039
1041
  * host: string;
1040
1042
  * https: boolean;
1041
1043
  * port: number;
1044
+ * base: string;
1042
1045
  * loose?: boolean;
1043
1046
  * allow?: string[];
1044
1047
  * cwd?: string;
1045
1048
  * }} param0
1046
1049
  */
1047
- function welcome({ port, host, https, open, loose, allow, cwd }) {
1048
- if (open) launch(port, https);
1050
+ function welcome({ port, host, https, open, base, loose, allow, cwd }) {
1051
+ if (open) launch(port, https, base);
1049
1052
 
1050
- console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.305'}\n`));
1053
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.306'}\n`));
1051
1054
 
1052
1055
  const protocol = https ? 'https:' : 'http:';
1053
1056
  const exposed = typeof host !== 'undefined' && host !== 'localhost' && host !== '127.0.0.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "1.0.0-next.305",
3
+ "version": "1.0.0-next.306",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -15,7 +15,7 @@
15
15
  "vite": "^2.9.0"
16
16
  },
17
17
  "devDependencies": {
18
- "@playwright/test": "^1.19.1",
18
+ "@playwright/test": "^1.20.2",
19
19
  "@rollup/plugin-replace": "^4.0.0",
20
20
  "@types/amphtml-validator": "^1.0.1",
21
21
  "@types/cookie": "^0.4.1",
@@ -101,8 +101,6 @@ export class InternalServer extends Server {
101
101
 
102
102
  export interface ManifestData {
103
103
  assets: Asset[];
104
- layout: string;
105
- error: string;
106
104
  components: string[];
107
105
  routes: RouteData[];
108
106
  matchers: Record<string, string>;
@@ -128,8 +126,8 @@ export interface PageData {
128
126
  shadow: string | null;
129
127
  pattern: RegExp;
130
128
  path: string;
131
- a: string[];
132
- b: string[];
129
+ a: Array<string | undefined>;
130
+ b: Array<string | undefined>;
133
131
  }
134
132
 
135
133
  export type PayloadScriptAttributes =
@@ -285,12 +283,12 @@ export interface SSRPage {
285
283
  /**
286
284
  * plan a is to render 1 or more layout components followed by a leaf component.
287
285
  */
288
- a: number[];
286
+ a: Array<number | undefined>;
289
287
  /**
290
288
  * plan b — if one of them components fails in `load` we backtrack until we find
291
289
  * the nearest error component.
292
290
  */
293
- b: number[];
291
+ b: Array<number | undefined>;
294
292
  }
295
293
 
296
294
  export interface SSRPagePart {