@sveltejs/kit 1.0.0-next.421 → 1.0.0-next.422

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.421",
3
+ "version": "1.0.0-next.422",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -139,12 +139,23 @@ function get_groups(manifest_data, routes_dir) {
139
139
  return group;
140
140
  }
141
141
 
142
- // first, sort nodes by path length (necessary for finding the nearest layout more efficiently)...
143
- const nodes = [...manifest_data.nodes].sort(
144
- (n1, n2) =>
142
+ // first, sort nodes (necessary for finding the nearest layout more efficiently)...
143
+ const nodes = [...manifest_data.nodes].sort((n1, n2) => {
144
+ // Sort by path length first...
145
+ const path_length_diff =
145
146
  /** @type {string} */ (n1.component ?? n1.shared ?? n1.server).split('/').length -
146
- /** @type {string} */ (n2.component ?? n2.shared ?? n2.server).split('/').length
147
- );
147
+ /** @type {string} */ (n2.component ?? n2.shared ?? n2.server).split('/').length;
148
+
149
+ return (
150
+ path_length_diff ||
151
+ // ...on ties, sort named layouts first
152
+ (path.basename(n1.component || '').includes('-')
153
+ ? -1
154
+ : path.basename(n2.component || '').includes('-')
155
+ ? 1
156
+ : 0)
157
+ );
158
+ });
148
159
 
149
160
  // ...then, populate `directories` with +page/+layout files...
150
161
  for (let i = 0; i < nodes.length; i += 1) {
@@ -713,12 +724,12 @@ export function find_nearest_layout(routes_dir, nodes, start_idx) {
713
724
  }
714
725
 
715
726
  let common_path = path.dirname(start_file);
716
- if (match[1] === 'layout' && !name) {
727
+ if (match[1] === 'layout' && !match[2] && !name) {
717
728
  // We are a default layout, so we skip the current level
718
729
  common_path = path.dirname(common_path);
719
730
  }
720
731
 
721
- for (let i = start_idx; i >= 0; i -= 1) {
732
+ for (let i = start_idx - 1; i >= 0; i -= 1) {
722
733
  const node = nodes[i];
723
734
  const file = /** @type {string} */ (node.component || node.shared || node.server);
724
735
 
@@ -1,3 +1,4 @@
1
+ import { HttpError, Redirect } from '../../index/private.js';
1
2
  import { check_method_names, method_not_allowed } from './utils.js';
2
3
 
3
4
  /**
@@ -29,16 +30,29 @@ export async function render_endpoint(event, route) {
29
30
  return method_not_allowed(mod, method);
30
31
  }
31
32
 
32
- const response = await handler(
33
- /** @type {import('types').RequestEvent<Record<string, any>>} */ (event)
34
- );
35
-
36
- if (!(response instanceof Response)) {
37
- return new Response(
38
- `Invalid response from route ${event.url.pathname}: handler should return a Response object`,
39
- { status: 500 }
33
+ try {
34
+ const response = await handler(
35
+ /** @type {import('types').RequestEvent<Record<string, any>>} */ (event)
40
36
  );
41
- }
42
37
 
43
- return response;
38
+ if (!(response instanceof Response)) {
39
+ return new Response(
40
+ `Invalid response from route ${event.url.pathname}: handler should return a Response object`,
41
+ { status: 500 }
42
+ );
43
+ }
44
+
45
+ return response;
46
+ } catch (error) {
47
+ if (error instanceof HttpError) {
48
+ return new Response(error.message, { status: error.status });
49
+ } else if (error instanceof Redirect) {
50
+ return new Response(undefined, {
51
+ status: error.status,
52
+ headers: { Location: error.location }
53
+ });
54
+ } else {
55
+ throw error;
56
+ }
57
+ }
44
58
  }
@@ -118,6 +118,9 @@ export async function respond(request, options, state) {
118
118
  /** @type {import('types').ResponseHeaders} */
119
119
  const headers = {};
120
120
 
121
+ /** @type {string[]} */
122
+ const cookies = [];
123
+
121
124
  /** @type {import('types').RequestEvent} */
122
125
  const event = {
123
126
  get clientAddress() {
@@ -141,16 +144,26 @@ export async function respond(request, options, state) {
141
144
  setHeaders: (new_headers) => {
142
145
  for (const key in new_headers) {
143
146
  const lower = key.toLowerCase();
147
+ const value = new_headers[key];
144
148
 
145
- if (lower in headers) {
146
- throw new Error(`"${key}" header is already set`);
147
- }
149
+ if (lower === 'set-cookie') {
150
+ const new_cookies = /** @type {string[]} */ (Array.isArray(value) ? value : [value]);
148
151
 
149
- // TODO apply these headers to the response
150
- headers[lower] = new_headers[key];
152
+ for (const cookie of new_cookies) {
153
+ if (cookies.includes(cookie)) {
154
+ throw new Error(`"${key}" header already has cookie with same value`);
155
+ }
151
156
 
152
- if (state.prerendering && lower === 'cache-control') {
153
- state.prerendering.cache = /** @type {string} */ (new_headers[key]);
157
+ cookies.push(cookie);
158
+ }
159
+ } else if (lower in headers) {
160
+ throw new Error(`"${key}" header is already set`);
161
+ } else {
162
+ headers[lower] = value;
163
+
164
+ if (state.prerendering && lower === 'cache-control') {
165
+ state.prerendering.cache = /** @type {string} */ (value);
166
+ }
154
167
  }
155
168
  }
156
169
  },
@@ -254,6 +267,7 @@ export async function respond(request, options, state) {
254
267
  return {
255
268
  // TODO return `uses`, so we can reuse server data effectively
256
269
  data: await load_server_data({
270
+ dev: options.dev,
257
271
  event,
258
272
  node,
259
273
  parent: async () => {
@@ -312,19 +326,19 @@ export async function respond(request, options, state) {
312
326
  : await render_page(event, route, options, state, resolve_opts);
313
327
  }
314
328
 
315
- for (const key in headers) {
316
- const value = headers[key];
317
- if (key === 'set-cookie') {
318
- for (const cookie of Array.isArray(value) ? value : [value]) {
319
- response.headers.append(key, /** @type {string} */ (cookie));
320
- }
321
- } else if (!is_data_request) {
322
- // we only want to set cookies on __data.json requests, we don't
323
- // want to cache stuff erroneously etc
329
+ if (!is_data_request) {
330
+ // we only want to set cookies on __data.json requests, we don't
331
+ // want to cache stuff erroneously etc
332
+ for (const key in headers) {
333
+ const value = headers[key];
324
334
  response.headers.set(key, /** @type {string} */ (value));
325
335
  }
326
336
  }
327
337
 
338
+ for (const cookie of cookies) {
339
+ response.headers.append('set-cookie', cookie);
340
+ }
341
+
328
342
  // respond with 304 if etag matches
329
343
  if (response.status === 200 && response.headers.has('etag')) {
330
344
  let if_none_match_value = request.headers.get('if-none-match');
@@ -36,7 +36,11 @@ export async function render_page(event, route, options, state, resolve_opts) {
36
36
  'application/json'
37
37
  ]);
38
38
 
39
- if (accept === 'application/json') {
39
+ if (
40
+ accept === 'application/json' &&
41
+ event.request.method !== 'GET' &&
42
+ event.request.method !== 'HEAD'
43
+ ) {
40
44
  const node = await options.manifest._.nodes[route.leaf]();
41
45
  if (node.server) {
42
46
  return handle_json_request(event, options, node.server);
@@ -157,6 +161,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
157
161
  }
158
162
 
159
163
  return await load_server_data({
164
+ dev: options.dev,
160
165
  event,
161
166
  node,
162
167
  parent: async () => {
@@ -346,8 +351,8 @@ function get_page_config(leaf, options) {
346
351
  * @param {import('types').SSRNode['server']} mod
347
352
  */
348
353
  export async function handle_json_request(event, options, mod) {
349
- const method = /** @type {import('types').HttpMethod} */ (event.request.method);
350
- const handler = mod[method === 'HEAD' || method === 'GET' ? 'load' : method];
354
+ const method = /** @type {'POST' | 'PUT' | 'PATCH' | 'DELETE'} */ (event.request.method);
355
+ const handler = mod[method];
351
356
 
352
357
  if (!handler) {
353
358
  return method_not_allowed(mod, method);
@@ -357,14 +362,6 @@ export async function handle_json_request(event, options, mod) {
357
362
  // @ts-ignore
358
363
  const result = await handler.call(null, event);
359
364
 
360
- if (method === 'HEAD') {
361
- return new Response();
362
- }
363
-
364
- if (method === 'GET') {
365
- return json(result);
366
- }
367
-
368
365
  if (result?.errors) {
369
366
  // @ts-ignore
370
367
  return json({ errors: result.errors }, { status: result.status || 400 });
@@ -3,12 +3,13 @@ import { LoadURL, PrerenderingURL } from '../../../utils/url.js';
3
3
  /**
4
4
  * Calls the user's `load` function.
5
5
  * @param {{
6
+ * dev: boolean;
6
7
  * event: import('types').RequestEvent;
7
8
  * node: import('types').SSRNode | undefined;
8
9
  * parent: () => Promise<Record<string, any>>;
9
10
  * }} opts
10
11
  */
11
- export async function load_server_data({ event, node, parent }) {
12
+ export async function load_server_data({ dev, event, node, parent }) {
12
13
  if (!node?.server) return null;
13
14
 
14
15
  const server_data = await node.server.load?.call(null, {
@@ -27,7 +28,13 @@ export async function load_server_data({ event, node, parent }) {
27
28
  url: event.url
28
29
  });
29
30
 
30
- return server_data ? unwrap_promises(server_data) : null;
31
+ const result = server_data ? await unwrap_promises(server_data) : null;
32
+
33
+ if (dev) {
34
+ check_serializability(result, /** @type {string} */ (node.server_id), 'data');
35
+ }
36
+
37
+ return result;
31
38
  }
32
39
 
33
40
  /**
@@ -79,3 +86,42 @@ async function unwrap_promises(object) {
79
86
 
80
87
  return unwrapped;
81
88
  }
89
+
90
+ /**
91
+ * Check that the data can safely be serialized to JSON
92
+ * @param {any} value
93
+ * @param {string} id
94
+ * @param {string} path
95
+ */
96
+ function check_serializability(value, id, path) {
97
+ const type = typeof value;
98
+
99
+ if (type === 'string' || type === 'boolean' || type === 'number' || type === 'undefined') {
100
+ // primitives are fine
101
+ return;
102
+ }
103
+
104
+ if (type === 'object') {
105
+ // nulls are fine...
106
+ if (!value) return;
107
+
108
+ // ...so are plain arrays...
109
+ if (Array.isArray(value)) {
110
+ value.forEach((child, i) => {
111
+ check_serializability(child, id, `${path}[${i}]`);
112
+ });
113
+ return;
114
+ }
115
+
116
+ // ...and objects
117
+ const tag = Object.prototype.toString.call(value);
118
+ if (tag === '[object Object]') {
119
+ for (const key in value) {
120
+ check_serializability(value[key], id, `${path}.${key}`);
121
+ }
122
+ return;
123
+ }
124
+ }
125
+
126
+ throw new Error(`${path} returned from 'load' in ${id} cannot be serialized as JSON`);
127
+ }
@@ -35,6 +35,7 @@ export async function respond_with_error({ event, options, state, status, error,
35
35
  const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
36
36
 
37
37
  const server_data_promise = load_server_data({
38
+ dev: options.dev,
38
39
  event,
39
40
  node: default_layout,
40
41
  parent: async () => ({})
@@ -119,6 +119,7 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
119
119
  if (node.server) {
120
120
  const { module } = await resolve(node.server);
121
121
  result.server = module;
122
+ result.server_id = node.server;
122
123
  }
123
124
 
124
125
  // in dev we inline all styles to avoid FOUC. this gets populated lazily so that
package/src/vite/utils.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { loadConfigFromFile, loadEnv, normalizePath } from 'vite';
4
4
  import { runtime_directory } from '../core/utils.js';
5
+ import { posixify } from '../utils/filesystem.js';
5
6
 
6
7
  /**
7
8
  * @param {import('vite').ResolvedConfig} config
@@ -113,18 +114,22 @@ export function get_aliases(config) {
113
114
  ];
114
115
 
115
116
  for (let [key, value] of Object.entries(config.alias)) {
117
+ value = posixify(value);
116
118
  if (value.endsWith('/*')) {
117
119
  value = value.slice(0, -2);
118
120
  }
119
121
  if (key.endsWith('/*')) {
120
122
  // Doing just `{ find: key.slice(0, -2) ,..}` would mean `import .. from "key"` would also be matched, which we don't want
121
123
  alias.push({
122
- find: new RegExp(`^${key.slice(0, -2)}\\/(.+)$`),
124
+ find: new RegExp(`^${escape_for_regexp(key.slice(0, -2))}\\/(.+)$`),
123
125
  replacement: `${path.resolve(value)}/$1`
124
126
  });
125
127
  } else if (key + '/*' in config.alias) {
126
128
  // key and key/* both exist -> the replacement for key needs to happen _only_ on import .. from "key"
127
- alias.push({ find: new RegExp(`^${key}$`), replacement: path.resolve(value) });
129
+ alias.push({
130
+ find: new RegExp(`^${escape_for_regexp(key)}$`),
131
+ replacement: path.resolve(value)
132
+ });
128
133
  } else {
129
134
  alias.push({ find: key, replacement: path.resolve(value) });
130
135
  }
@@ -133,6 +138,13 @@ export function get_aliases(config) {
133
138
  return alias;
134
139
  }
135
140
 
141
+ /**
142
+ * @param {string} str
143
+ */
144
+ function escape_for_regexp(str) {
145
+ return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match);
146
+ }
147
+
136
148
  /**
137
149
  * Given an entry point like [cwd]/src/hooks, returns a filename like [cwd]/src/hooks.js or [cwd]/src/hooks/index.js
138
150
  * @param {string} entry
package/types/index.d.ts CHANGED
@@ -312,7 +312,7 @@ export interface Action<
312
312
  Params extends Partial<Record<string, string>> = Partial<Record<string, string>>
313
313
  > {
314
314
  (event: RequestEvent<Params>): MaybePromise<
315
- | { status?: number; errors: Record<string, string>; location?: never }
315
+ | { status?: number; errors: Record<string, any>; location?: never }
316
316
  | { status?: never; errors?: never; location: string }
317
317
  | void
318
318
  >;
@@ -221,6 +221,9 @@ export interface SSRNode {
221
221
  PUT?: Action;
222
222
  DELETE?: Action;
223
223
  };
224
+
225
+ // store this in dev so we can print serialization errors
226
+ server_id?: string;
224
227
  }
225
228
 
226
229
  export type SSRNodeLoader = () => Promise<SSRNode>;