@sveltejs/kit 1.0.0-next.304 → 1.0.0-next.307

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 ? '/?' : ''}$`
@@ -2081,38 +2081,46 @@ async function respond_with_error({
2081
2081
  resolve_opts
2082
2082
  }) {
2083
2083
  try {
2084
- const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
2085
- const default_error = await options.manifest._.nodes[1](); // 1 is always the root error
2084
+ const branch = [];
2085
+ let stuff = {};
2086
+
2087
+ if (resolve_opts.ssr) {
2088
+ const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
2089
+ const default_error = await options.manifest._.nodes[1](); // 1 is always the root error
2090
+
2091
+ const layout_loaded = /** @type {Loaded} */ (
2092
+ await load_node({
2093
+ event,
2094
+ options,
2095
+ state,
2096
+ route: null,
2097
+ node: default_layout,
2098
+ $session,
2099
+ stuff: {},
2100
+ is_error: false,
2101
+ is_leaf: false
2102
+ })
2103
+ );
2086
2104
 
2087
- const layout_loaded = /** @type {Loaded} */ (
2088
- await load_node({
2089
- event,
2090
- options,
2091
- state,
2092
- route: null,
2093
- node: default_layout,
2094
- $session,
2095
- stuff: {},
2096
- is_error: false,
2097
- is_leaf: false
2098
- })
2099
- );
2105
+ const error_loaded = /** @type {Loaded} */ (
2106
+ await load_node({
2107
+ event,
2108
+ options,
2109
+ state,
2110
+ route: null,
2111
+ node: default_error,
2112
+ $session,
2113
+ stuff: layout_loaded ? layout_loaded.stuff : {},
2114
+ is_error: true,
2115
+ is_leaf: false,
2116
+ status,
2117
+ error
2118
+ })
2119
+ );
2100
2120
 
2101
- const error_loaded = /** @type {Loaded} */ (
2102
- await load_node({
2103
- event,
2104
- options,
2105
- state,
2106
- route: null,
2107
- node: default_error,
2108
- $session,
2109
- stuff: layout_loaded ? layout_loaded.stuff : {},
2110
- is_error: true,
2111
- is_leaf: false,
2112
- status,
2113
- error
2114
- })
2115
- );
2121
+ branch.push(layout_loaded, error_loaded);
2122
+ stuff = error_loaded.stuff;
2123
+ }
2116
2124
 
2117
2125
  return await render_response({
2118
2126
  options,
@@ -2122,10 +2130,10 @@ async function respond_with_error({
2122
2130
  hydrate: options.hydrate,
2123
2131
  router: options.router
2124
2132
  },
2125
- stuff: error_loaded.stuff,
2133
+ stuff,
2126
2134
  status,
2127
2135
  error,
2128
- branch: [layout_loaded, error_loaded],
2136
+ branch,
2129
2137
  event,
2130
2138
  resolve_opts
2131
2139
  });
@@ -2181,7 +2189,8 @@ async function respond$1(opts) {
2181
2189
 
2182
2190
  try {
2183
2191
  nodes = await Promise.all(
2184
- route.a.map((n) => options.manifest._.nodes[n] && options.manifest._.nodes[n]())
2192
+ // we use == here rather than === because [undefined] serializes as "[null]"
2193
+ route.a.map((n) => (n == undefined ? n : options.manifest._.nodes[n]()))
2185
2194
  );
2186
2195
  } catch (err) {
2187
2196
  const error = coalesce_to_error(err);
@@ -2280,7 +2289,8 @@ async function respond$1(opts) {
2280
2289
  if (error) {
2281
2290
  while (i--) {
2282
2291
  if (route.b[i]) {
2283
- const error_node = await options.manifest._.nodes[route.b[i]]();
2292
+ const index = /** @type {number} */ (route.b[i]);
2293
+ const error_node = await options.manifest._.nodes[index]();
2284
2294
 
2285
2295
  /** @type {Loaded} */
2286
2296
  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
124
130
  * @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
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
133
140
  */
134
141
 
135
- const specials = new Set(['__layout', '__layout.reset', '__error', '__tests__', '__test__']);
142
+ /**
143
+ * @typedef {{
144
+ * error: string | undefined;
145
+ * layouts: Record<string, { file: string, name: string }>
146
+ * }} Node
147
+ */
148
+
149
+ /**
150
+ * @typedef {Map<string, Node>} Tree
151
+ */
152
+
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,201 @@ 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`));
167
-
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 = [];
174
+ /** @type {Map<string, Unit>} */
175
+ const units = new Map();
177
176
 
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();
177
+ /** @type {Tree} */
178
+ const tree = new Map();
182
179
 
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
199
+ .slice(0, -extension.length)
200
+ .replace(/(?:^|\/)index((?:@[a-zA-Z0-9_-]+)?(?:\.[a-z]+)?)?$/, '$1');
201
+ const project_relative = `${routes_base}/${file}`;
197
202
 
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
- });
203
+ const segments = id.split('/');
204
+ const name = /** @type {string} */ (segments.pop());
208
205
 
209
- items.sort(comparator);
206
+ if (name === '__layout.reset') {
207
+ throw new Error(
208
+ '__layout.reset has been removed in favour of named layouts: https://kit.svelte.dev/docs/layouts#named-layouts'
209
+ );
210
+ }
210
211
 
211
- items.forEach((item) => {
212
- const id_parts = parent_id.slice();
212
+ if (name === '__error' || layout_pattern.test(name)) {
213
+ const dir = segments.join('/');
213
214
 
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);
215
+ if (!tree.has(dir)) {
216
+ tree.set(dir, {
217
+ error: undefined,
218
+ layouts: {}
219
+ });
220
220
  }
221
221
 
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);
222
+ const group = /** @type {Node} */ (tree.get(dir));
226
223
 
227
- if (layout_reset && layout) {
228
- throw new Error(`Cannot have __layout next to __layout.reset: ${layout_reset}`);
224
+ if (name === '__error') {
225
+ group.error = project_relative;
226
+ } else {
227
+ const match = /** @type {RegExpMatchArray} */ (layout_pattern.exec(name));
228
+
229
+ if (match[1] === DEFAULT) {
230
+ throw new Error(`${project_relative} cannot use reserved "${DEFAULT}" name`);
229
231
  }
230
232
 
231
- if (layout_reset) components.push(layout_reset);
232
- if (layout) components.push(layout);
233
- if (error) components.push(error);
233
+ const layout_id = match[1] || DEFAULT;
234
234
 
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);
235
+ const defined = group.layouts[layout_id];
236
+ if (defined && defined !== default_layout) {
237
+ throw new Error(
238
+ `Duplicate layout ${project_relative} already defined at ${defined.file}`
239
+ );
240
+ }
244
241
 
245
- if (item.is_page) {
246
- components.push(item.file);
242
+ group.layouts[layout_id] = {
243
+ file: project_relative,
244
+ name
245
+ };
246
+ }
247
247
 
248
- const concatenated = layout_stack.concat(item.file);
249
- const errors = error_stack.slice();
248
+ return;
249
+ } else if (dunder_pattern.test(file)) {
250
+ throw new Error(
251
+ `Files and directories prefixed with __ are reserved (saw ${project_relative})`
252
+ );
253
+ }
250
254
 
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
- }
255
+ if (!config.kit.routes(file)) return;
258
256
 
259
- i = errors.length;
260
- while (i--) {
261
- if (errors[i]) break;
262
- }
257
+ if (/\]\[/.test(id)) {
258
+ throw new Error(`Invalid route ${project_relative} — parameters must be separated`);
259
+ }
263
260
 
264
- errors.splice(i + 1);
261
+ if (count_occurrences('[', id) !== count_occurrences(']', id)) {
262
+ throw new Error(`Invalid route ${project_relative} — brackets are unbalanced`);
263
+ }
265
264
 
266
- const path = id.includes('[') ? '' : `/${id}`;
265
+ if (!units.has(id)) {
266
+ units.set(id, {
267
+ id,
268
+ pattern: parse_route_id(id).pattern,
269
+ segments: id
270
+ .split('/')
271
+ .filter(Boolean)
272
+ .map((segment) => {
273
+ /** @type {Part[]} */
274
+ const parts = [];
275
+ segment.split(/\[(.+?)\]/).map((content, i) => {
276
+ const dynamic = !!(i % 2);
277
+
278
+ if (!content) return;
279
+
280
+ parts.push({
281
+ content,
282
+ dynamic,
283
+ rest: dynamic && content.startsWith('...'),
284
+ type: (dynamic && content.split('=')[1]) || null
285
+ });
286
+ });
287
+ return parts;
288
+ }),
289
+ page: undefined,
290
+ endpoint: undefined
291
+ });
292
+ }
267
293
 
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
- }
294
+ const unit = /** @type {Unit} */ (units.get(id));
288
295
 
289
- const routes_base = path__default.relative(cwd, config.kit.files.routes);
296
+ if (config.extensions.find((ext) => file.endsWith(ext))) {
297
+ const { layouts, errors } = trace(project_relative, file, tree, config.extensions);
298
+ unit.page = {
299
+ a: layouts.concat(project_relative),
300
+ b: errors
301
+ };
302
+ } else {
303
+ unit.endpoint = project_relative;
304
+ }
305
+ });
290
306
 
291
- const layout = find_layout('__layout', routes_base) || default_layout;
292
- const error = find_layout('__error', routes_base) || default_error;
307
+ /** @type {string[]} */
308
+ const components = [];
293
309
 
294
- components.push(layout, error);
310
+ tree.forEach(({ layouts, error }) => {
311
+ // we do [default, error, ...other_layouts] so that components[0] and [1]
312
+ // are the root layout/error. kinda janky, there's probably a nicer way
313
+ if (layouts[DEFAULT]) {
314
+ components.push(layouts[DEFAULT].file);
315
+ }
295
316
 
296
- walk(config.kit.files.routes, [], [layout], [error]);
317
+ if (error) {
318
+ components.push(error);
319
+ }
297
320
 
298
- const lookup = new Map();
299
- for (const route of routes) {
300
- if (route.type === 'page') {
301
- lookup.set(route.id, route);
321
+ for (const id in layouts) {
322
+ if (id !== DEFAULT) components.push(layouts[id].file);
302
323
  }
303
- }
324
+ });
304
325
 
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);
326
+ units.forEach((unit) => {
327
+ if (unit.page) {
328
+ const leaf = /** @type {string} */ (unit.page.a[unit.page.a.length - 1]);
329
+ components.push(leaf);
311
330
  }
312
- }
331
+ });
332
+
333
+ Array.from(units.values())
334
+ .sort(compare)
335
+ .forEach((unit) => {
336
+ // TODO when we introduce layout endpoints and scoped middlewares, we
337
+ // will probably want to have a single unified route type here
338
+ // (created in the list_files(...).forEach(...) callback)
339
+ if (unit.page) {
340
+ routes.push({
341
+ type: 'page',
342
+ id: unit.id,
343
+ pattern: unit.pattern,
344
+ path: unit.id.includes('[') ? '' : `/${unit.id.replace(/@(?:[a-zA-Z0-9_-]+)/g, '')}`,
345
+ shadow: unit.endpoint || null,
346
+ a: unit.page.a,
347
+ b: unit.page.b
348
+ });
349
+ } else if (unit.endpoint) {
350
+ routes.push({
351
+ type: 'endpoint',
352
+ id: unit.id,
353
+ pattern: unit.pattern,
354
+ file: unit.endpoint
355
+ });
356
+ }
357
+ });
313
358
 
359
+ /** @type {import('types').Asset[]} */
314
360
  const assets = fs__default.existsSync(config.kit.files.assets)
315
- ? list_files({ config, dir: config.kit.files.assets, path: '' })
361
+ ? list_files(config.kit.files.assets).map((file) => ({
362
+ file,
363
+ size: fs__default.statSync(`${config.kit.files.assets}/${file}`).size,
364
+ type: mime.getType(file)
365
+ }))
316
366
  : [];
317
367
 
318
368
  const params_base = path__default.relative(cwd, config.kit.files.params);
@@ -336,8 +386,6 @@ function create_manifest_data({
336
386
 
337
387
  return {
338
388
  assets,
339
- layout,
340
- error,
341
389
  components,
342
390
  routes,
343
391
  matchers
@@ -345,142 +393,162 @@ function create_manifest_data({
345
393
  }
346
394
 
347
395
  /**
348
- * @param {string} needle
349
- * @param {string} haystack
396
+ * @param {string} file
397
+ * @param {string} path
398
+ * @param {Tree} tree
399
+ * @param {string[]} extensions
350
400
  */
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}/;
401
+ function trace(file, path, tree, extensions) {
402
+ /** @type {Array<string | undefined>} */
403
+ const layouts = [];
404
+
405
+ /** @type {Array<string | undefined>} */
406
+ const errors = [];
407
+
408
+ const parts = path.split('/');
409
+ const filename = /** @type {string} */ (parts.pop());
410
+ const extension = /** @type {string} */ (extensions.find((ext) => path.endsWith(ext)));
411
+ const base = filename.slice(0, -extension.length);
412
+
413
+ let layout_id = base.includes('@') ? base.split('@')[1] : DEFAULT;
414
+
415
+ // walk up the tree, find which __layout and __error components
416
+ // apply to this page
417
+ // eslint-disable-next-line
418
+ while (true) {
419
+ const node = tree.get(parts.join('/'));
420
+ const layout = node?.layouts[layout_id];
421
+
422
+ // any segment that has neither a __layout nor an __error can be discarded.
423
+ // in other words these...
424
+ // layouts: [a, , b, c]
425
+ // errors: [d, , e, ]
426
+ //
427
+ // ...can be compacted to these:
428
+ // layouts: [a, b, c]
429
+ // errors: [d, e, ]
430
+ if (node?.error || layout?.file) {
431
+ errors.unshift(node?.error);
432
+ layouts.unshift(layout?.file);
433
+ }
360
434
 
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;
435
+ if (layout?.name.includes('@')) {
436
+ layout_id = layout.name.split('@')[1];
437
+ } else {
438
+ if (layout) layout_id = DEFAULT;
439
+ if (parts.length === 0) break;
440
+ parts.pop();
441
+ }
369
442
  }
370
443
 
371
- const max = Math.max(a.parts.length, b.parts.length);
444
+ if (layout_id !== DEFAULT) {
445
+ throw new Error(`${file} references missing layout "${layout_id}"`);
446
+ }
372
447
 
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];
448
+ // trim empty space off the end of the errors array
449
+ let i = errors.length;
450
+ while (i--) if (errors[i]) break;
451
+ errors.length = i + 1;
376
452
 
377
- if (!a_sub_part) return 1; // b is more specific, so goes first
378
- if (!b_sub_part) return -1;
453
+ return { layouts, errors };
454
+ }
379
455
 
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;
456
+ /**
457
+ * @param {Unit} a
458
+ * @param {Unit} b
459
+ */
460
+ function compare(a, b) {
461
+ const max_segments = Math.max(a.segments.length, b.segments.length);
462
+ for (let i = 0; i < max_segments; i += 1) {
463
+ const sa = a.segments[i];
464
+ const sb = b.segments[i];
465
+
466
+ // /x < /x/y, but /[...x]/y < /[...x]
467
+ if (!sa) return a.id.includes('[...') ? +1 : -1;
468
+ if (!sb) return b.id.includes('[...') ? -1 : +1;
469
+
470
+ const max_parts = Math.max(sa.length, sb.length);
471
+ for (let i = 0; i < max_parts; i += 1) {
472
+ const pa = sa[i];
473
+ const pb = sb[i];
474
+
475
+ // xy < x[y], but [x].json < [x]
476
+ if (pa === undefined) return pb.dynamic ? -1 : +1;
477
+ if (pb === undefined) return pa.dynamic ? +1 : -1;
478
+
479
+ // x < [x]
480
+ if (pa.dynamic !== pb.dynamic) {
481
+ return pa.dynamic ? +1 : -1;
383
482
  }
384
- // sort alphabetically
385
- return a_sub_part.content < b_sub_part.content ? -1 : 1;
386
- }
387
-
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
483
 
395
- if (a_sub_part.dynamic && !!a_sub_part.type !== !!b_sub_part.type) {
396
- return a_sub_part.type ? -1 : 1;
397
- }
484
+ if (pa.dynamic) {
485
+ // [x] < [...x]
486
+ if (pa.rest !== pb.rest) {
487
+ return pa.rest ? +1 : -1;
488
+ }
398
489
 
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
- );
490
+ // [x=type] < [x]
491
+ if (!!pa.type !== !!pb.type) {
492
+ return pa.type ? -1 : +1;
493
+ }
494
+ }
404
495
  }
405
496
  }
406
497
 
407
- if (a.is_page !== b.is_page) {
408
- return a.is_page ? 1 : -1;
498
+ const a_is_endpoint = !a.page && a.endpoint;
499
+ const b_is_endpoint = !b.page && b.endpoint;
500
+
501
+ if (a_is_endpoint !== b_is_endpoint) {
502
+ return a_is_endpoint ? -1 : +1;
409
503
  }
410
504
 
411
- // otherwise sort alphabetically
412
- return a.file < b.file ? -1 : 1;
505
+ return a < b ? -1 : 1;
413
506
  }
414
507
 
415
508
  /**
416
- * @param {string} part
417
- * @param {string} file
509
+ * @param {string} needle
510
+ * @param {string} haystack
418
511
  */
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`);
512
+ function count_occurrences(needle, haystack) {
513
+ let count = 0;
514
+ for (let i = 0; i < haystack.length; i += 1) {
515
+ if (haystack[i] === needle) count += 1;
426
516
  }
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;
517
+ return count;
457
518
  }
458
519
 
459
520
  /**
460
- * @param {{
461
- * config: import('types').ValidatedConfig;
462
- * dir: string;
463
- * path: string;
464
- * files?: import('types').Asset[]
465
- * }} args
521
+ * @param {string} dir
522
+ * @param {string} [path]
523
+ * @param {string[]} [files]
466
524
  */
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
- });
525
+ function list_files(dir, path = '', files = []) {
526
+ fs__default.readdirSync(dir, { withFileTypes: true })
527
+ .sort(({ name: a }, { name: b }) => {
528
+ // sort each directory in (__layout, __error, everything else) order
529
+ // so that we can trace layouts/errors immediately
530
+
531
+ if (a.startsWith('__layout')) {
532
+ if (!b.startsWith('__layout')) return -1;
533
+ } else if (b.startsWith('__layout')) {
534
+ return 1;
535
+ } else if (a.startsWith('__')) {
536
+ if (!b.startsWith('__')) return -1;
537
+ } else if (b.startsWith('__')) {
538
+ return 1;
539
+ }
540
+
541
+ return a < b ? -1 : 1;
542
+ })
543
+ .forEach((file) => {
544
+ const joined = path ? `${path}/${file.name}` : file.name;
545
+
546
+ if (file.isDirectory()) {
547
+ list_files(`${dir}/${file.name}`, joined, files);
548
+ } else {
549
+ files.push(joined);
550
+ }
551
+ });
484
552
 
485
553
  return files;
486
554
  }
@@ -549,7 +617,7 @@ function write_manifest(manifest_data, base, output) {
549
617
  .join(',\n\t\t\t\t\t')}
550
618
  ]`.replace(/^\t/gm, '');
551
619
 
552
- /** @param {string[]} parts */
620
+ /** @param {Array<string | undefined>} parts */
553
621
  const get_indices = (parts) =>
554
622
  `[${parts.map((part) => (part ? component_indexes[part] : '')).join(', ')}]`;
555
623
 
@@ -833,21 +901,6 @@ function write_types(config, manifest_data) {
833
901
  /** @type {Map<string, { params: string[], type: 'page' | 'endpoint' | 'both' }>} */
834
902
  const shadow_types = new Map();
835
903
 
836
- /** @param {string} key */
837
- function extract_params(key) {
838
- /** @type {string[]} */
839
- const params = [];
840
-
841
- const pattern = /\[(?:\.{3})?([^\]]+)\]/g;
842
- let match;
843
-
844
- while ((match = pattern.exec(key))) {
845
- params.push(match[1]);
846
- }
847
-
848
- return params;
849
- }
850
-
851
904
  manifest_data.routes.forEach((route) => {
852
905
  const file = route.type === 'endpoint' ? route.file : route.shadow;
853
906
 
@@ -857,7 +910,7 @@ function write_types(config, manifest_data) {
857
910
  );
858
911
  const key = file.slice(0, -ext.length);
859
912
  shadow_types.set(key, {
860
- params: extract_params(key),
913
+ params: parse_route_id(key).names,
861
914
  type: route.type === 'endpoint' ? 'endpoint' : 'both'
862
915
  });
863
916
  }
@@ -870,7 +923,7 @@ function write_types(config, manifest_data) {
870
923
  const key = component.slice(0, -ext.length);
871
924
 
872
925
  if (!shadow_types.has(key)) {
873
- shadow_types.set(key, { params: extract_params(key), type: 'page' });
926
+ shadow_types.set(key, { params: parse_route_id(key).names, type: 'page' });
874
927
  }
875
928
  });
876
929
 
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.304');
873
+ const prog = sade('svelte-kit').version('1.0.0-next.307');
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
  }
@@ -979,7 +981,6 @@ prog
979
981
  prog
980
982
  .command('package')
981
983
  .describe('Create a package')
982
- .option('-d, --dir', 'Destination directory', 'package')
983
984
  .action(async () => {
984
985
  try {
985
986
  const config = await load_config();
@@ -1039,15 +1040,16 @@ async function check_port(port) {
1039
1040
  * host: string;
1040
1041
  * https: boolean;
1041
1042
  * port: number;
1043
+ * base: string;
1042
1044
  * loose?: boolean;
1043
1045
  * allow?: string[];
1044
1046
  * cwd?: string;
1045
1047
  * }} param0
1046
1048
  */
1047
- function welcome({ port, host, https, open, loose, allow, cwd }) {
1048
- if (open) launch(port, https);
1049
+ function welcome({ port, host, https, open, base, loose, allow, cwd }) {
1050
+ if (open) launch(port, https, base);
1049
1051
 
1050
- console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.304'}\n`));
1052
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.307'}\n`));
1051
1053
 
1052
1054
  const protocol = https ? 'https:' : 'http:';
1053
1055
  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.304",
3
+ "version": "1.0.0-next.307",
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",
package/types/index.d.ts CHANGED
@@ -184,7 +184,7 @@ export interface HandleError {
184
184
  }
185
185
 
186
186
  /**
187
- * The type of a `load` function exported from `<script context="module">` in a page or layout.
187
+ * The `(input: LoadInput) => LoadOutput` `load` function exported from `<script context="module">` in a page or layout.
188
188
  *
189
189
  * Note that you can use [generated types](/docs/types#generated-types) instead of manually specifying the Params generic argument.
190
190
  */
@@ -215,26 +215,22 @@ export interface ParamMatcher {
215
215
  }
216
216
 
217
217
  /**
218
- * A function exported from an endpoint that corresponds to an
219
- * HTTP verb (`get`, `put`, `patch`, etc) and handles requests with
220
- * that method. Note that since 'delete' is a reserved word in
221
- * JavaScript, delete handles are called `del` instead.
218
+ * A `(event: RequestEvent) => RequestHandlerOutput` function exported from an endpoint that corresponds to an HTTP verb (`get`, `put`, `patch`, etc) and handles requests with that method. Note that since 'delete' is a reserved word in JavaScript, delete handles are called `del` instead.
222
219
  *
223
- * Note that you can use [generated types](/docs/types#generated-types)
224
- * instead of manually specifying the `Params` generic argument.
220
+ * Note that you can use [generated types](/docs/types#generated-types) instead of manually specifying the `Params` generic argument.
225
221
  */
226
222
  export interface RequestHandler<
227
223
  Params extends Record<string, string> = Record<string, string>,
228
224
  Output extends ResponseBody = ResponseBody
229
225
  > {
230
- (event: RequestEvent<Params>): RequestHandlerOutput<Output>;
226
+ (event: RequestEvent<Params>): MaybePromise<RequestHandlerOutput<Output>>;
231
227
  }
232
228
 
233
- export type RequestHandlerOutput<Output extends ResponseBody = ResponseBody> = MaybePromise<{
229
+ export interface RequestHandlerOutput<Output extends ResponseBody = ResponseBody> {
234
230
  status?: number;
235
231
  headers?: Headers | Partial<ResponseHeaders>;
236
232
  body?: Output;
237
- }>;
233
+ }
238
234
 
239
235
  export type ResponseBody = JSONValue | Uint8Array | ReadableStream | import('stream').Readable;
240
236
 
@@ -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 {