@sveltejs/kit 1.0.0-next.556 → 1.0.0-next.559

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.556",
3
+ "version": "1.0.0-next.559",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -99,8 +99,8 @@ export function generate_manifest({ build_data, relative_path, routes, format =
99
99
  ],
100
100
  routes: [
101
101
  ${routes.map(route => {
102
- route.types.forEach(type => {
103
- if (type) matchers.add(type);
102
+ route.params.forEach(param => {
103
+ if (param.matcher) matchers.add(param.matcher);
104
104
  });
105
105
 
106
106
  if (!route.page && !route.endpoint) return;
@@ -108,9 +108,7 @@ export function generate_manifest({ build_data, relative_path, routes, format =
108
108
  return `{
109
109
  id: ${s(route.id)},
110
110
  pattern: ${route.pattern},
111
- names: ${s(route.names)},
112
- types: ${s(route.types)},
113
- optional: ${s(route.optional)},
111
+ params: ${s(route.params)},
114
112
  page: ${route.page ? `{ layouts: ${get_nodes(route.page.layouts)}, errors: ${get_nodes(route.page.errors)}, leaf: ${reindexed.get(route.page.leaf)} }` : 'null'},
115
113
  endpoint: ${route.endpoint ? loader(join_relative(relative_path, resolve_symlinks(build_data.server.vite_manifest, route.endpoint.file).chunk.file)) : 'null'}
116
114
  }`;
@@ -24,6 +24,14 @@ export default function create_manifest_data({
24
24
  const matchers = create_matchers(config, cwd);
25
25
  const { nodes, routes } = create_routes_and_nodes(cwd, config, fallback);
26
26
 
27
+ for (const route of routes) {
28
+ for (const param of route.params) {
29
+ if (param.matcher && !matchers[param.matcher]) {
30
+ throw new Error(`No matcher found for parameter '${param.matcher}' in route ${route.id}`);
31
+ }
32
+ }
33
+ }
34
+
27
35
  return {
28
36
  assets,
29
37
  matchers,
@@ -153,7 +161,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
153
161
  );
154
162
  }
155
163
 
156
- const { pattern, names, types, optional } = parse_route_id(id);
164
+ const { pattern, params } = parse_route_id(id);
157
165
 
158
166
  /** @type {import('types').RouteData} */
159
167
  const route = {
@@ -162,9 +170,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
162
170
 
163
171
  segment,
164
172
  pattern,
165
- names,
166
- types,
167
- optional,
173
+ params,
168
174
 
169
175
  layout: null,
170
176
  error: null,
@@ -273,9 +279,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
273
279
  id: '/',
274
280
  segment: '',
275
281
  pattern: /^$/,
276
- names: [],
277
- types: [],
278
- optional: [],
282
+ params: [],
279
283
  parent: null,
280
284
  layout: null,
281
285
  error: null,
@@ -195,8 +195,8 @@ function update_types(config, routes, route, to_delete = new Set()) {
195
195
  // Makes sure a type is "repackaged" and therefore more readable
196
196
  declarations.push('type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;');
197
197
  declarations.push(
198
- `type RouteParams = { ${route.names
199
- .map((param, idx) => `${param}${route.optional[idx] ? '?' : ''}: string`)
198
+ `type RouteParams = { ${route.params
199
+ .map((param) => `${param.name}${param.optional ? '?' : ''}: string`)
200
200
  .join('; ')} }`
201
201
  );
202
202
 
@@ -270,8 +270,8 @@ function update_types(config, routes, route, to_delete = new Set()) {
270
270
  if (leaf) {
271
271
  if (leaf.route.page) ids.push(`"${leaf.route.id}"`);
272
272
 
273
- for (const name of leaf.route.names) {
274
- layout_params.add(name);
273
+ for (const param of leaf.route.params) {
274
+ layout_params.add(param.name);
275
275
  }
276
276
 
277
277
  ensureProxies(page, leaf.proxies);
@@ -8,12 +8,22 @@ import { HttpError, Redirect, ValidationError } from '../runtime/control.js';
8
8
  * @param {any} message
9
9
  */
10
10
  export function error(status, message) {
11
+ if (
12
+ (!__SVELTEKIT_BROWSER__ || __SVELTEKIT_DEV__) &&
13
+ (isNaN(status) || status < 400 || status > 599)
14
+ ) {
15
+ throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
16
+ }
17
+
11
18
  return new HttpError(status, message);
12
19
  }
13
20
 
14
21
  /** @type {import('@sveltejs/kit').redirect} */
15
22
  export function redirect(status, location) {
16
- if (isNaN(status) || status < 300 || status > 308) {
23
+ if (
24
+ (!__SVELTEKIT_BROWSER__ || __SVELTEKIT_DEV__) &&
25
+ (isNaN(status) || status < 300 || status > 308)
26
+ ) {
17
27
  throw new Error('Invalid status code');
18
28
  }
19
29
 
@@ -127,9 +127,6 @@ export function get_default_build_config({ config, input, ssr, outDir }) {
127
127
  cssCodeSplit: true,
128
128
  // don't use the default name to avoid collisions with 'static/manifest.json'
129
129
  manifest: 'vite-manifest.json',
130
- modulePreload: {
131
- polyfill: false
132
- },
133
130
  outDir,
134
131
  rollupOptions: {
135
132
  input,
@@ -156,9 +156,7 @@ export async function dev(vite, vite_config, svelte_config) {
156
156
  return {
157
157
  id: route.id,
158
158
  pattern: route.pattern,
159
- names: route.names,
160
- types: route.types,
161
- optional: route.optional,
159
+ params: route.params,
162
160
  page: route.page,
163
161
  endpoint: endpoint
164
162
  ? async () => {
@@ -35,9 +35,6 @@ const enforced_config = {
35
35
  formats: true
36
36
  },
37
37
  manifest: true,
38
- modulePreload: {
39
- polyfill: true
40
- },
41
38
  outDir: true,
42
39
  rollupOptions: {
43
40
  input: true,
@@ -75,7 +75,7 @@ function check_for_removed_attributes() {
75
75
 
76
76
  /**
77
77
  * @param {{
78
- * target: Element;
78
+ * target: HTMLElement;
79
79
  * base: string;
80
80
  * }} opts
81
81
  * @returns {import('./types').Client}
@@ -1385,12 +1385,12 @@ export function create_client({ target, base }) {
1385
1385
  }, 20);
1386
1386
  };
1387
1387
 
1388
- addEventListener('touchstart', trigger_prefetch);
1389
- addEventListener('mousemove', handle_mousemove);
1390
- addEventListener('sveltekit:trigger_prefetch', trigger_prefetch);
1388
+ target.addEventListener('touchstart', trigger_prefetch);
1389
+ target.addEventListener('mousemove', handle_mousemove);
1390
+ target.addEventListener('sveltekit:trigger_prefetch', trigger_prefetch);
1391
1391
 
1392
1392
  /** @param {MouseEvent} event */
1393
- addEventListener('click', (event) => {
1393
+ target.addEventListener('click', (event) => {
1394
1394
  // Adapted from https://github.com/visionmedia/page.js
1395
1395
  // MIT license https://github.com/visionmedia/page.js#license
1396
1396
  if (event.button || event.which !== 1) return;
@@ -11,14 +11,14 @@ export function parse(nodes, server_loads, dictionary, matchers) {
11
11
  const layouts_with_server_load = new Set(server_loads);
12
12
 
13
13
  return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
14
- const { pattern, names, types, optional } = parse_route_id(id);
14
+ const { pattern, params } = parse_route_id(id);
15
15
 
16
16
  const route = {
17
17
  id,
18
18
  /** @param {string} path */
19
19
  exec: (path) => {
20
20
  const match = pattern.exec(path);
21
- if (match) return exec(match, { names, types, optional }, matchers);
21
+ if (match) return exec(match, params, matchers);
22
22
  },
23
23
  errors: [1, ...(errors || [])].map((n) => nodes[n]),
24
24
  layouts: [0, ...(layouts || [])].map(create_layout_loader),
@@ -12,7 +12,7 @@ import { set_version } from '../env.js';
12
12
  * assets: string;
13
13
  * base: string;
14
14
  * },
15
- * target: Element;
15
+ * target: HTMLElement;
16
16
  * version: string;
17
17
  * }} opts
18
18
  */
@@ -43,56 +43,3 @@ export class ValidationError {
43
43
  this.data = data;
44
44
  }
45
45
  }
46
-
47
- /**
48
- * Creates an `HttpError` object with an HTTP status code and an optional message.
49
- * This object, if thrown during request handling, will cause SvelteKit to
50
- * return an error response without invoking `handleError`
51
- * @param {number} status
52
- * @param {string | undefined} [message]
53
- */
54
- export function error(status, message) {
55
- return new HttpError(status, message);
56
- }
57
-
58
- /**
59
- * Creates a `Redirect` object. If thrown during request handling, SvelteKit will
60
- * return a redirect response.
61
- * @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status
62
- * @param {string} location
63
- */
64
- export function redirect(status, location) {
65
- if (isNaN(status) || status < 300 || status > 308) {
66
- throw new Error('Invalid status code');
67
- }
68
-
69
- return new Redirect(status, location);
70
- }
71
-
72
- /**
73
- * Generates a JSON `Response` object from the supplied data.
74
- * @param {any} data
75
- * @param {ResponseInit} [init]
76
- */
77
- export function json(data, init) {
78
- // TODO deprecate this in favour of `Response.json` when it's
79
- // more widely supported
80
- const headers = new Headers(init?.headers);
81
- if (!headers.has('content-type')) {
82
- headers.set('content-type', 'application/json');
83
- }
84
-
85
- return new Response(JSON.stringify(data), {
86
- ...init,
87
- headers
88
- });
89
- }
90
-
91
- /**
92
- * Generates a `ValidationError` object.
93
- * @param {number} status
94
- * @param {Record<string, any> | undefined} [data]
95
- */
96
- export function invalid(status, data) {
97
- return new ValidationError(status, data);
98
- }
@@ -8,6 +8,10 @@ import { has_data_suffix, normalize_path, strip_data_suffix } from '../../utils/
8
8
  * @type {Record<string, Set<string>>} */
9
9
  const cookie_paths = {};
10
10
 
11
+ // default encoding functions for header cookie values
12
+ const encode = encodeURIComponent;
13
+ const decode = decodeURIComponent;
14
+
11
15
  /**
12
16
  * @param {Request} request
13
17
  * @param {URL} url
@@ -16,7 +20,7 @@ const cookie_paths = {};
16
20
  */
17
21
  export function get_cookies(request, url, dev, trailing_slash) {
18
22
  const header = request.headers.get('cookie') ?? '';
19
- const initial_cookies = parse(header);
23
+ const initial_cookies = parse(header, { decode });
20
24
 
21
25
  const normalized_url = normalize_path(
22
26
  // Remove suffix: 'foo/__data.json' would mean the cookie path is '/foo',
@@ -76,8 +80,8 @@ export function get_cookies(request, url, dev, trailing_slash) {
76
80
  return c.value;
77
81
  }
78
82
 
79
- const decode = opts?.decode || decodeURIComponent;
80
- const req_cookies = parse(header, { decode });
83
+ const decoder = opts?.decode || decode;
84
+ const req_cookies = parse(header, { decode: decoder });
81
85
  const cookie = req_cookies[name]; // the decoded string or undefined
82
86
 
83
87
  if (!dev || cookie) {
@@ -166,7 +170,7 @@ export function get_cookies(request, url, dev, trailing_slash) {
166
170
 
167
171
  // cookies sent by the user agent have lowest precedence
168
172
  for (const name in initial_cookies) {
169
- combined_cookies[name] = initial_cookies[name];
173
+ combined_cookies[name] = encode(initial_cookies[name]);
170
174
  }
171
175
 
172
176
  // cookies previous set during this event with cookies.set have higher precedence
@@ -175,14 +179,15 @@ export function get_cookies(request, url, dev, trailing_slash) {
175
179
  if (!domain_matches(destination.hostname, cookie.options.domain)) continue;
176
180
  if (!path_matches(destination.pathname, cookie.options.path)) continue;
177
181
 
178
- combined_cookies[cookie.name] = cookie.value;
182
+ const encoder = cookie.options.encode || encode;
183
+ combined_cookies[cookie.name] = encoder(cookie.value);
179
184
  }
180
185
 
181
186
  // explicit header has highest precedence
182
187
  if (header) {
183
- const parsed = parse(header);
188
+ const parsed = parse(header, { decode });
184
189
  for (const name in parsed) {
185
- combined_cookies[name] = parsed[name];
190
+ combined_cookies[name] = encode(parsed[name]);
186
191
  }
187
192
  }
188
193
 
@@ -77,7 +77,7 @@ export async function respond(request, options, state) {
77
77
  const match = candidate.pattern.exec(decoded);
78
78
  if (!match) continue;
79
79
 
80
- const matched = exec(match, candidate, matchers);
80
+ const matched = exec(match, candidate.params, matchers);
81
81
  if (matched) {
82
82
  route = candidate;
83
83
  params = decode_params(matched);
@@ -5,44 +5,40 @@ const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;
5
5
  * @param {string} id
6
6
  */
7
7
  export function parse_route_id(id) {
8
- /** @type {string[]} */
9
- const names = [];
10
-
11
- /** @type {string[]} */
12
- const types = [];
13
-
14
- /** @type {boolean[]} */
15
- const optional = [];
16
-
17
- // `/foo` should get an optional trailing slash, `/foo.json` should not
18
- // const add_trailing_slash = !/\.[a-z]+$/.test(key);
19
- let add_trailing_slash = true;
8
+ /** @type {import('types').RouteParam[]} */
9
+ const params = [];
20
10
 
21
11
  const pattern =
22
12
  id === '/'
23
13
  ? /^\/$/
24
14
  : new RegExp(
25
15
  `^${get_route_segments(id)
26
- .map((segment, i, segments) => {
16
+ .map((segment) => {
27
17
  // special case — /[...rest]/ could contain zero segments
28
18
  const rest_match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
29
19
  if (rest_match) {
30
- names.push(rest_match[1]);
31
- types.push(rest_match[2]);
32
- optional.push(false);
20
+ params.push({
21
+ name: rest_match[1],
22
+ matcher: rest_match[2],
23
+ optional: false,
24
+ rest: true,
25
+ chained: true
26
+ });
33
27
  return '(?:/(.*))?';
34
28
  }
35
29
  // special case — /[[optional]]/ could contain zero segments
36
30
  const optional_match = /^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(segment);
37
31
  if (optional_match) {
38
- names.push(optional_match[1]);
39
- types.push(optional_match[2]);
40
- optional.push(true);
32
+ params.push({
33
+ name: optional_match[1],
34
+ matcher: optional_match[2],
35
+ optional: true,
36
+ rest: false,
37
+ chained: true
38
+ });
41
39
  return '(?:/([^/]+))?';
42
40
  }
43
41
 
44
- const is_last = i === segments.length - 1;
45
-
46
42
  if (!segment) {
47
43
  return;
48
44
  }
@@ -73,29 +69,31 @@ export function parse_route_id(id) {
73
69
  );
74
70
  }
75
71
 
76
- const [, is_optional, is_rest, name, type] = match;
72
+ const [, is_optional, is_rest, name, matcher] = match;
77
73
  // It's assumed that the following invalid route id cases are already checked
78
74
  // - unbalanced brackets
79
75
  // - optional param following rest param
80
76
 
81
- names.push(name);
82
- types.push(type);
83
- optional.push(!!is_optional);
77
+ params.push({
78
+ name,
79
+ matcher,
80
+ optional: !!is_optional,
81
+ rest: !!is_rest,
82
+ chained: is_rest ? i === 1 && parts[0] === '' : false
83
+ });
84
84
  return is_rest ? '(.*?)' : is_optional ? '([^/]*)?' : '([^/]+?)';
85
85
  }
86
86
 
87
- if (is_last && content.includes('.')) add_trailing_slash = false;
88
-
89
87
  return escape(content);
90
88
  })
91
89
  .join('');
92
90
 
93
91
  return '/' + result;
94
92
  })
95
- .join('')}${add_trailing_slash ? '/?' : ''}$`
93
+ .join('')}/?$`
96
94
  );
97
95
 
98
- return { pattern, names, types, optional };
96
+ return { pattern, params };
99
97
  }
100
98
 
101
99
  /**
@@ -119,35 +117,70 @@ export function get_route_segments(route) {
119
117
 
120
118
  /**
121
119
  * @param {RegExpMatchArray} match
122
- * @param {{
123
- * names: string[];
124
- * types: string[];
125
- * optional: boolean[];
126
- * }} candidate
120
+ * @param {import('types').RouteParam[]} params
127
121
  * @param {Record<string, import('types').ParamMatcher>} matchers
128
122
  */
129
- export function exec(match, { names, types, optional }, matchers) {
123
+ export function exec(match, params, matchers) {
130
124
  /** @type {Record<string, string>} */
131
- const params = {};
125
+ const result = {};
126
+
127
+ const values = match.slice(1);
132
128
 
133
- for (let i = 0; i < names.length; i += 1) {
134
- const name = names[i];
135
- const type = types[i];
136
- let value = match[i + 1];
129
+ let buffered = '';
137
130
 
138
- if (value || !optional[i]) {
139
- if (type) {
140
- const matcher = matchers[type];
141
- if (!matcher) throw new Error(`Missing "${type}" param matcher`); // TODO do this ahead of time?
131
+ for (let i = 0; i < params.length; i += 1) {
132
+ const param = params[i];
133
+ let value = values[i];
134
+
135
+ if (param.chained && param.rest && buffered) {
136
+ // in the `[[lang=lang]]/[...rest]` case, if `lang` didn't
137
+ // match, we roll it over into the rest value
138
+ value = value ? buffered + '/' + value : buffered;
139
+ }
142
140
 
143
- if (!matcher(value)) return;
141
+ buffered = '';
142
+
143
+ if (value === undefined) {
144
+ // if `value` is undefined, it means this is
145
+ // an optional or rest parameter
146
+ if (param.rest) result[param.name] = '';
147
+ } else {
148
+ if (param.matcher && !matchers[param.matcher](value)) {
149
+ // in the `/[[a=b]]/[[c=d]]` case, if the value didn't satisfy the `b` matcher,
150
+ // try again with the next segment by shifting values rightwards
151
+ if (param.optional && param.chained) {
152
+ // @ts-expect-error TypeScript is... wrong
153
+ let j = values.indexOf(undefined, i);
154
+
155
+ if (j === -1) {
156
+ // we can't shift values any further, so hang on to this value
157
+ // so it can be rolled into a subsequent `[...rest]` param
158
+ const next = params[i + 1];
159
+ if (next?.rest && next.chained) {
160
+ buffered = value;
161
+ } else {
162
+ return;
163
+ }
164
+ }
165
+
166
+ while (j >= i) {
167
+ values[j] = values[j - 1];
168
+ j -= 1;
169
+ }
170
+
171
+ continue;
172
+ }
173
+
174
+ // otherwise, if the matcher returns `false`, the route did not match
175
+ return;
144
176
  }
145
177
 
146
- params[name] = value ?? '';
178
+ result[param.name] = value;
147
179
  }
148
180
  }
149
181
 
150
- return params;
182
+ if (buffered) return;
183
+ return result;
151
184
  }
152
185
 
153
186
  /** @param {string} str */
@@ -158,6 +158,14 @@ export interface Respond {
158
158
  (request: Request, options: SSROptions, state: SSRState): Promise<Response>;
159
159
  }
160
160
 
161
+ export interface RouteParam {
162
+ name: string;
163
+ matcher: string;
164
+ optional: boolean;
165
+ rest: boolean;
166
+ chained: boolean;
167
+ }
168
+
161
169
  /**
162
170
  * Represents a route segment in the app. It can either be an intermediate node
163
171
  * with only layout/error pages, or a leaf, at which point either `page` and `leaf`
@@ -169,9 +177,7 @@ export interface RouteData {
169
177
 
170
178
  segment: string;
171
179
  pattern: RegExp;
172
- names: string[];
173
- types: string[];
174
- optional: boolean[];
180
+ params: RouteParam[];
175
181
 
176
182
  layout: PageNode | null;
177
183
  error: PageNode | null;
@@ -337,12 +343,8 @@ export type SSREndpoint = Partial<Record<HttpMethod, RequestHandler>> & {
337
343
  export interface SSRRoute {
338
344
  id: string;
339
345
  pattern: RegExp;
340
- names: string[];
341
- types: string[];
342
- optional: boolean[];
343
-
346
+ params: RouteParam[];
344
347
  page: PageNodeIndexes | null;
345
-
346
348
  endpoint: (() => Promise<SSREndpoint>) | null;
347
349
  }
348
350