@sveltejs/kit 1.0.0-next.456 → 1.0.0-next.459

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.456",
3
+ "version": "1.0.0-next.459",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -44,10 +44,10 @@ if (import.meta.env.DEV) {
44
44
  export function initial_fetch(resource, opts) {
45
45
  const url = JSON.stringify(typeof resource === 'string' ? resource : resource.url);
46
46
 
47
- let selector = `script[sveltekit\\:data-type="data"][sveltekit\\:data-url=${url}]`;
47
+ let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
48
48
 
49
49
  if (opts && typeof opts.body === 'string') {
50
- selector += `[sveltekit\\:data-body="${hash(opts.body)}"]`;
50
+ selector += `[data-hash="${hash(opts.body)}"]`;
51
51
  }
52
52
 
53
53
  const script = document.querySelector(selector);
@@ -1,4 +1,4 @@
1
- import { HttpError, Redirect } from '../control.js';
1
+ import { Redirect } from '../control.js';
2
2
  import { check_method_names, method_not_allowed } from './utils.js';
3
3
 
4
4
  /**
@@ -39,9 +39,8 @@ export async function render_endpoint(event, mod, state) {
39
39
  );
40
40
 
41
41
  if (!(response instanceof Response)) {
42
- return new Response(
43
- `Invalid response from route ${event.url.pathname}: handler should return a Response object`,
44
- { status: 500 }
42
+ throw new Error(
43
+ `Invalid response from route ${event.url.pathname}: handler should return a Response object`
45
44
  );
46
45
  }
47
46
 
@@ -52,15 +51,13 @@ export async function render_endpoint(event, mod, state) {
52
51
 
53
52
  return response;
54
53
  } catch (error) {
55
- if (error instanceof HttpError) {
56
- return new Response(error.message, { status: error.status });
57
- } else if (error instanceof Redirect) {
54
+ if (error instanceof Redirect) {
58
55
  return new Response(undefined, {
59
56
  status: error.status,
60
- headers: { Location: error.location }
57
+ headers: { location: error.location }
61
58
  });
62
- } else {
63
- throw error;
64
59
  }
60
+
61
+ throw error;
65
62
  }
66
63
  }
@@ -3,10 +3,9 @@ import { render_page } from './page/index.js';
3
3
  import { render_response } from './page/render.js';
4
4
  import { respond_with_error } from './page/respond_with_error.js';
5
5
  import { coalesce_to_error } from '../../utils/error.js';
6
- import { serialize_error, GENERIC_ERROR, static_error_page } from './utils.js';
6
+ import { GENERIC_ERROR, handle_fatal_error } from './utils.js';
7
7
  import { decode_params, disable_search, normalize_path } from '../../utils/url.js';
8
8
  import { exec } from '../../utils/routing.js';
9
- import { negotiate } from '../../utils/http.js';
10
9
  import { render_data } from './data/index.js';
11
10
  import { DATA_SUFFIX } from '../../constants.js';
12
11
 
@@ -190,178 +189,151 @@ export async function respond(request, options, state) {
190
189
  transformPageChunk: default_transform
191
190
  };
192
191
 
193
- // TODO match route before calling handle?
194
-
195
- try {
196
- const response = await options.hooks.handle({
197
- event,
198
- resolve: async (event, opts) => {
199
- if (opts) {
200
- // TODO remove for 1.0
201
- // @ts-expect-error
202
- if (opts.transformPage) {
203
- throw new Error(
204
- 'transformPage has been replaced by transformPageChunk — see https://github.com/sveltejs/kit/pull/5657 for more information'
205
- );
206
- }
207
- // @ts-expect-error
208
- if (opts.ssr) {
209
- throw new Error(
210
- 'ssr has been removed, set it in the appropriate +layout.js instead. See the PR for more information: https://github.com/sveltejs/kit/pull/6197'
211
- );
212
- }
213
-
214
- resolve_opts = {
215
- transformPageChunk: opts.transformPageChunk || default_transform
216
- };
192
+ /**
193
+ *
194
+ * @param {import('types').RequestEvent} event
195
+ * @param {import('types').ResolveOptions} [opts]
196
+ */
197
+ async function resolve(event, opts) {
198
+ try {
199
+ if (opts) {
200
+ // TODO remove for 1.0
201
+ if ('transformPage' in opts) {
202
+ throw new Error(
203
+ 'transformPage has been replaced by transformPageChunk — see https://github.com/sveltejs/kit/pull/5657 for more information'
204
+ );
217
205
  }
218
206
 
219
- if (state.prerendering?.fallback) {
220
- return await render_response({
221
- event,
222
- options,
223
- state,
224
- page_config: { ssr: false, csr: true },
225
- status: 200,
226
- error: null,
227
- branch: [],
228
- fetched: [],
229
- validation_errors: undefined,
230
- cookies: [],
231
- resolve_opts
232
- });
207
+ if ('ssr' in opts) {
208
+ throw new Error(
209
+ 'ssr has been removed, set it in the appropriate +layout.js instead. See the PR for more information: https://github.com/sveltejs/kit/pull/6197'
210
+ );
233
211
  }
234
212
 
235
- if (route) {
236
- /** @type {Response} */
237
- let response;
238
-
239
- if (is_data_request) {
240
- response = await render_data(event, route, options, state);
241
- } else if (route.page) {
242
- response = await render_page(event, route, route.page, options, state, resolve_opts);
243
- } else if (route.endpoint) {
244
- response = await render_endpoint(event, await route.endpoint(), state);
245
- } else {
246
- // a route will always have a page or an endpoint, but TypeScript
247
- // doesn't know that
248
- throw new Error('This should never happen');
249
- }
213
+ resolve_opts = {
214
+ transformPageChunk: opts.transformPageChunk || default_transform
215
+ };
216
+ }
250
217
 
251
- if (!is_data_request) {
252
- // we only want to set cookies on __data.js requests, we don't
253
- // want to cache stuff erroneously etc
254
- for (const key in headers) {
255
- const value = headers[key];
256
- response.headers.set(key, /** @type {string} */ (value));
257
- }
258
- }
218
+ if (state.prerendering?.fallback) {
219
+ return await render_response({
220
+ event,
221
+ options,
222
+ state,
223
+ page_config: { ssr: false, csr: true },
224
+ status: 200,
225
+ error: null,
226
+ branch: [],
227
+ fetched: [],
228
+ validation_errors: undefined,
229
+ cookies: [],
230
+ resolve_opts
231
+ });
232
+ }
233
+
234
+ if (route) {
235
+ /** @type {Response} */
236
+ let response;
259
237
 
260
- for (const cookie of cookies) {
261
- response.headers.append('set-cookie', cookie);
238
+ if (is_data_request) {
239
+ response = await render_data(event, route, options, state);
240
+ } else if (route.page) {
241
+ response = await render_page(event, route, route.page, options, state, resolve_opts);
242
+ } else if (route.endpoint) {
243
+ response = await render_endpoint(event, await route.endpoint(), state);
244
+ } else {
245
+ // a route will always have a page or an endpoint, but TypeScript
246
+ // doesn't know that
247
+ throw new Error('This should never happen');
248
+ }
249
+
250
+ if (!is_data_request) {
251
+ // we only want to set cookies on __data.js requests, we don't
252
+ // want to cache stuff erroneously etc
253
+ for (const key in headers) {
254
+ const value = headers[key];
255
+ response.headers.set(key, /** @type {string} */ (value));
262
256
  }
257
+ }
263
258
 
264
- // respond with 304 if etag matches
265
- if (response.status === 200 && response.headers.has('etag')) {
266
- let if_none_match_value = request.headers.get('if-none-match');
259
+ for (const cookie of cookies) {
260
+ response.headers.append('set-cookie', cookie);
261
+ }
267
262
 
268
- // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
269
- if (if_none_match_value?.startsWith('W/"')) {
270
- if_none_match_value = if_none_match_value.substring(2);
271
- }
263
+ // respond with 304 if etag matches
264
+ if (response.status === 200 && response.headers.has('etag')) {
265
+ let if_none_match_value = request.headers.get('if-none-match');
272
266
 
273
- const etag = /** @type {string} */ (response.headers.get('etag'));
267
+ // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
268
+ if (if_none_match_value?.startsWith('W/"')) {
269
+ if_none_match_value = if_none_match_value.substring(2);
270
+ }
274
271
 
275
- if (if_none_match_value === etag) {
276
- const headers = new Headers({ etag });
272
+ const etag = /** @type {string} */ (response.headers.get('etag'));
277
273
 
278
- // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
279
- for (const key of ['cache-control', 'content-location', 'date', 'expires', 'vary']) {
280
- const value = response.headers.get(key);
281
- if (value) headers.set(key, value);
282
- }
274
+ if (if_none_match_value === etag) {
275
+ const headers = new Headers({ etag });
283
276
 
284
- return new Response(undefined, {
285
- status: 304,
286
- headers
287
- });
277
+ // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
278
+ for (const key of ['cache-control', 'content-location', 'date', 'expires', 'vary']) {
279
+ const value = response.headers.get(key);
280
+ if (value) headers.set(key, value);
288
281
  }
289
- }
290
282
 
291
- return response;
283
+ return new Response(undefined, {
284
+ status: 304,
285
+ headers
286
+ });
287
+ }
292
288
  }
293
289
 
294
- if (state.initiator === GENERIC_ERROR) {
295
- return new Response('Internal Server Error', {
296
- status: 500
297
- });
298
- }
290
+ return response;
291
+ }
299
292
 
300
- // if this request came direct from the user, rather than
301
- // via a `fetch` in a `load`, render a 404 page
302
- if (!state.initiator) {
303
- return await respond_with_error({
304
- event,
305
- options,
306
- state,
307
- status: 404,
308
- error: new Error(`Not found: ${event.url.pathname}`),
309
- resolve_opts
310
- });
311
- }
293
+ if (state.initiator === GENERIC_ERROR) {
294
+ return new Response('Internal Server Error', {
295
+ status: 500
296
+ });
297
+ }
312
298
 
313
- if (state.prerendering) {
314
- return new Response('not found', { status: 404 });
315
- }
299
+ // if this request came direct from the user, rather than
300
+ // via a `fetch` in a `load`, render a 404 page
301
+ if (!state.initiator) {
302
+ return await respond_with_error({
303
+ event,
304
+ options,
305
+ state,
306
+ status: 404,
307
+ error: new Error(`Not found: ${event.url.pathname}`),
308
+ resolve_opts
309
+ });
310
+ }
316
311
 
317
- // we can't load the endpoint from our own manifest,
318
- // so we need to make an actual HTTP request
319
- return await fetch(request);
320
- },
312
+ if (state.prerendering) {
313
+ return new Response('not found', { status: 404 });
314
+ }
321
315
 
316
+ // we can't load the endpoint from our own manifest,
317
+ // so we need to make an actual HTTP request
318
+ return await fetch(request);
319
+ } catch (e) {
320
+ const error = coalesce_to_error(e);
321
+ return handle_fatal_error(event, options, error);
322
+ }
323
+ }
324
+
325
+ try {
326
+ return await options.hooks.handle({
327
+ event,
328
+ resolve,
322
329
  // TODO remove for 1.0
323
330
  // @ts-expect-error
324
331
  get request() {
325
332
  throw new Error('request in handle has been replaced with event' + details);
326
333
  }
327
334
  });
328
-
329
- // TODO for 1.0, change the error message to point to docs rather than PR
330
- if (response && !(response instanceof Response)) {
331
- throw new Error('handle must return a Response object' + details);
332
- }
333
-
334
- return response;
335
335
  } catch (/** @type {unknown} */ e) {
336
336
  const error = coalesce_to_error(e);
337
-
338
- options.handle_error(error, event);
339
-
340
- const type = negotiate(event.request.headers.get('accept') || 'text/html', [
341
- 'text/html',
342
- 'application/json'
343
- ]);
344
-
345
- if (is_data_request || type === 'application/json') {
346
- return new Response(serialize_error(error, options.get_stack), {
347
- status: 500,
348
- headers: { 'content-type': 'application/json; charset=utf-8' }
349
- });
350
- }
351
-
352
- // TODO is this necessary? should we just return a plain 500 at this point?
353
- try {
354
- return await respond_with_error({
355
- event,
356
- options,
357
- state,
358
- status: 500,
359
- error,
360
- resolve_opts
361
- });
362
- } catch (/** @type {unknown} */ e) {
363
- const error = coalesce_to_error(e);
364
- return static_error_page(options, 500, error.message);
365
- }
337
+ return handle_fatal_error(event, options, error);
366
338
  }
367
339
  }
@@ -192,6 +192,7 @@ export function create_fetch({ event, options, state, route, prerender_default }
192
192
  async function text() {
193
193
  const body = await response.text();
194
194
 
195
+ // TODO just pass `response.headers`, for processing inside `serialize_data`
195
196
  /** @type {import('types').ResponseHeaders} */
196
197
  const headers = {};
197
198
  for (const [key, value] of response.headers) {
@@ -334,7 +334,7 @@ export async function render_page(event, route, page, options, state, resolve_op
334
334
  });
335
335
  } catch (error) {
336
336
  // if we end up here, it means the data loaded successfull
337
- // but the page failed to render
337
+ // but the page failed to render, or that a prerendering error occurred
338
338
  options.handle_error(/** @type {Error} */ (error), event);
339
339
 
340
340
  return await respond_with_error({
@@ -2,7 +2,7 @@ import { devalue } from 'devalue';
2
2
  import { readable, writable } from 'svelte/store';
3
3
  import * as cookie from 'cookie';
4
4
  import { hash } from '../../hash.js';
5
- import { render_json_payload_script } from '../../../utils/escape.js';
5
+ import { serialize_data } from './serialize_data.js';
6
6
  import { s } from '../../../utils/misc.js';
7
7
  import { Csp } from './csp.js';
8
8
  import { serialize_error } from '../utils.js';
@@ -284,25 +284,7 @@ export async function render_response({
284
284
  }
285
285
 
286
286
  if (page_config.ssr && page_config.csr) {
287
- /** @type {string[]} */
288
- const serialized_data = [];
289
-
290
- for (const { url, body, response } of fetched) {
291
- serialized_data.push(
292
- render_json_payload_script(
293
- { type: 'data', url, body: typeof body === 'string' ? hash(body) : undefined },
294
- response
295
- )
296
- );
297
- }
298
-
299
- if (validation_errors) {
300
- serialized_data.push(
301
- render_json_payload_script({ type: 'validation_errors' }, validation_errors)
302
- );
303
- }
304
-
305
- body += `\n\t${serialized_data.join('\n\t')}`;
287
+ body += `\n\t${fetched.map(serialize_data).join('\n\t')}`;
306
288
  }
307
289
 
308
290
  if (options.service_worker) {
@@ -0,0 +1,58 @@
1
+ import { escape_html_attr } from '../../../utils/escape.js';
2
+ import { hash } from '../../hash.js';
3
+
4
+ /**
5
+ * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
6
+ *
7
+ * The first closes the script element, so everything after is treated as raw HTML.
8
+ * The second disables further parsing until `-->`, so the script element might be unexpectedly
9
+ * kept open until until an unrelated HTML comment in the page.
10
+ *
11
+ * U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018
12
+ * browsers.
13
+ *
14
+ * @see tests for unsafe parsing examples.
15
+ * @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
16
+ * @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
17
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
18
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
19
+ * @see https://github.com/tc39/proposal-json-superset
20
+ * @type {Record<string, string>}
21
+ */
22
+ const replacements = {
23
+ '<': '\\u003C',
24
+ '\u2028': '\\u2028',
25
+ '\u2029': '\\u2029'
26
+ };
27
+
28
+ const pattern = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g');
29
+
30
+ /**
31
+ * Generates a raw HTML string containing a safe script element carrying data and associated attributes.
32
+ *
33
+ * It escapes all the special characters needed to guarantee the element is unbroken, but care must
34
+ * be taken to ensure it is inserted in the document at an acceptable position for a script element,
35
+ * and that the resulting string isn't further modified.
36
+ *
37
+ * @param {import('./types.js').Fetched} fetched
38
+ * @returns {string} The raw HTML of a script element carrying the JSON payload.
39
+ * @example const html = serialize_data('/data.json', null, { foo: 'bar' });
40
+ */
41
+ export function serialize_data(fetched) {
42
+ const safe_payload = JSON.stringify(fetched.response).replace(
43
+ pattern,
44
+ (match) => replacements[match]
45
+ );
46
+
47
+ const attrs = [
48
+ 'type="application/json"',
49
+ 'data-sveltekit-fetched',
50
+ `data-url=${escape_html_attr(fetched.url)}`
51
+ ];
52
+
53
+ if (fetched.body) {
54
+ attrs.push(`data-hash=${escape_html_attr(hash(fetched.body))}`);
55
+ }
56
+
57
+ return `<script ${attrs.join(' ')}>${safe_payload}</script>`;
58
+ }
@@ -1,4 +1,6 @@
1
1
  import { devalue } from 'devalue';
2
+ import { DATA_SUFFIX } from '../../constants.js';
3
+ import { negotiate } from '../../utils/http.js';
2
4
  import { HttpError } from '../control.js';
3
5
 
4
6
  /** @param {any} body */
@@ -175,3 +177,33 @@ export function static_error_page(options, status, message) {
175
177
  status
176
178
  });
177
179
  }
180
+
181
+ /**
182
+ * @param {import('types').RequestEvent} event
183
+ * @param {import('types').SSROptions} options
184
+ * @param {Error} error
185
+ */
186
+ export function handle_fatal_error(event, options, error) {
187
+ let status = 500;
188
+
189
+ if (error instanceof HttpError) {
190
+ status = error.status;
191
+ } else {
192
+ options.handle_error(error, event);
193
+ }
194
+
195
+ // ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it
196
+ const type = negotiate(event.request.headers.get('accept') || 'text/html', [
197
+ 'text/html',
198
+ 'application/json'
199
+ ]);
200
+
201
+ if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') {
202
+ return new Response(serialize_error(error, options.get_stack), {
203
+ status,
204
+ headers: { 'content-type': 'application/json; charset=utf-8' }
205
+ });
206
+ }
207
+
208
+ return static_error_page(options, status, error.message);
209
+ }
@@ -1,61 +1,3 @@
1
- /**
2
- * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
3
- *
4
- * The first closes the script element, so everything after is treated as raw HTML.
5
- * The second disables further parsing until `-->`, so the script element might be unexpectedly
6
- * kept open until until an unrelated HTML comment in the page.
7
- *
8
- * U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018
9
- * browsers.
10
- *
11
- * @see tests for unsafe parsing examples.
12
- * @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
13
- * @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
14
- * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
15
- * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
16
- * @see https://github.com/tc39/proposal-json-superset
17
- * @type {Record<string, string>}
18
- */
19
- const render_json_payload_script_dict = {
20
- '<': '\\u003C',
21
- '\u2028': '\\u2028',
22
- '\u2029': '\\u2029'
23
- };
24
-
25
- const render_json_payload_script_regex = new RegExp(
26
- `[${Object.keys(render_json_payload_script_dict).join('')}]`,
27
- 'g'
28
- );
29
-
30
- /**
31
- * Generates a raw HTML string containing a safe script element carrying JSON data and associated attributes.
32
- *
33
- * It escapes all the special characters needed to guarantee the element is unbroken, but care must
34
- * be taken to ensure it is inserted in the document at an acceptable position for a script element,
35
- * and that the resulting string isn't further modified.
36
- *
37
- * Attribute names must be type-checked so we don't need to escape them.
38
- *
39
- * @param {import('types').PayloadScriptAttributes} attrs A list of attributes to be added to the element.
40
- * @param {any} payload The data to be carried by the element. Must be serializable to JSON.
41
- * @returns {string} The raw HTML of a script element carrying the JSON payload.
42
- * @example const html = render_json_payload_script({ type: 'data', url: '/data.json' }, { foo: 'bar' });
43
- */
44
- export function render_json_payload_script(attrs, payload) {
45
- const safe_payload = JSON.stringify(payload).replace(
46
- render_json_payload_script_regex,
47
- (match) => render_json_payload_script_dict[match]
48
- );
49
-
50
- let safe_attrs = '';
51
- for (const [key, value] of Object.entries(attrs)) {
52
- if (value === undefined) continue;
53
- safe_attrs += ` sveltekit:data-${key}=${escape_html_attr(value)}`;
54
- }
55
-
56
- return `<script type="application/json"${safe_attrs}>${safe_payload}</script>`;
57
- }
58
-
59
1
  /**
60
2
  * When inside a double-quoted attribute value, only `&` and `"` hold special meaning.
61
3
  * @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
@@ -135,10 +135,6 @@ export interface PageNode {
135
135
  child_pages?: PageNode[];
136
136
  }
137
137
 
138
- export type PayloadScriptAttributes =
139
- | { type: 'data'; url: string; body?: string }
140
- | { type: 'validation_errors' };
141
-
142
138
  export interface PrerenderDependency {
143
139
  response: Response;
144
140
  body: null | string | Uint8Array;