@sveltejs/kit 1.0.0-next.287 → 1.0.0-next.288

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.
@@ -93,9 +93,6 @@ class Router {
93
93
  this.enabled = true;
94
94
  this.initialized = false;
95
95
 
96
- // make it possible to reset focus
97
- document.body.setAttribute('tabindex', '-1');
98
-
99
96
  // keeping track of the history index in order to prevent popstate navigation events if needed
100
97
  this.current_history_index = history.state?.['sveltekit:index'] ?? 0;
101
98
 
@@ -658,12 +655,12 @@ function create_updated_store() {
658
655
  * @param {RequestInit} [opts]
659
656
  */
660
657
  function initial_fetch(resource, opts) {
661
- const url = typeof resource === 'string' ? resource : resource.url;
658
+ const url = JSON.stringify(typeof resource === 'string' ? resource : resource.url);
662
659
 
663
- let selector = `script[data-type="svelte-data"][data-url=${JSON.stringify(url)}]`;
660
+ let selector = `script[sveltekit\\:data-type="data"][sveltekit\\:data-url=${url}]`;
664
661
 
665
662
  if (opts && typeof opts.body === 'string') {
666
- selector += `[data-body="${hash(opts.body)}"]`;
663
+ selector += `[sveltekit\\:data-body="${hash(opts.body)}"]`;
667
664
  }
668
665
 
669
666
  const script = document.querySelector(selector);
@@ -782,7 +779,7 @@ class Renderer {
782
779
  let props;
783
780
 
784
781
  if (is_leaf) {
785
- const serialized = document.querySelector('[data-type="svelte-props"]');
782
+ const serialized = document.querySelector('script[sveltekit\\:data-type="props"]');
786
783
  if (serialized) {
787
784
  props = JSON.parse(/** @type {string} */ (serialized.textContent));
788
785
  }
@@ -932,8 +929,24 @@ class Renderer {
932
929
  const { scroll, keepfocus } = opts;
933
930
 
934
931
  if (!keepfocus) {
932
+ // Reset page selection and focus
933
+ // We try to mimick browsers' behaviour as closely as possible by targeting the
934
+ // viewport, but unfortunately it's not a perfect match — e.g. shift-tabbing won't
935
+ // immediately cycle from the end of the page
936
+ // See https://html.spec.whatwg.org/multipage/interaction.html#get-the-focusable-area
937
+ const root = document.documentElement;
938
+ const tabindex = root.getAttribute('tabindex');
939
+
935
940
  getSelection()?.removeAllRanges();
936
- document.body.focus();
941
+ root.tabIndex = -1;
942
+ root.focus();
943
+
944
+ // restore `tabindex` as to prevent the document from stealing input from elements
945
+ if (tabindex !== null) {
946
+ root.setAttribute('tabindex', tabindex);
947
+ } else {
948
+ root.removeAttribute('tabindex');
949
+ }
937
950
  }
938
951
 
939
952
  // need to render the DOM before we can scroll to the rendered elements
@@ -498,30 +498,62 @@ function coalesce_to_error(err) {
498
498
  : new Error(JSON.stringify(err));
499
499
  }
500
500
 
501
- // dict from https://github.com/yahoo/serialize-javascript/blob/183c18a776e4635a379fdc620f81771f219832bb/index.js#L25
502
- /** @type {Record<string, string>} */
503
- const escape_json_in_html_dict = {
501
+ /**
502
+ * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
503
+ *
504
+ * The first closes the script element, so everything after is treated as raw HTML.
505
+ * The second disables further parsing until `-->`, so the script element might be unexpectedly
506
+ * kept open until until an unrelated HTML comment in the page.
507
+ *
508
+ * U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018
509
+ * browsers.
510
+ *
511
+ * @see tests for unsafe parsing examples.
512
+ * @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
513
+ * @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
514
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
515
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
516
+ * @see https://github.com/tc39/proposal-json-superset
517
+ * @type {Record<string, string>}
518
+ */
519
+ const render_json_payload_script_dict = {
504
520
  '<': '\\u003C',
505
- '>': '\\u003E',
506
- '/': '\\u002F',
507
521
  '\u2028': '\\u2028',
508
522
  '\u2029': '\\u2029'
509
523
  };
510
524
 
511
- const escape_json_in_html_regex = new RegExp(
512
- `[${Object.keys(escape_json_in_html_dict).join('')}]`,
525
+ const render_json_payload_script_regex = new RegExp(
526
+ `[${Object.keys(render_json_payload_script_dict).join('')}]`,
513
527
  'g'
514
528
  );
515
529
 
516
530
  /**
517
- * Escape a JSONValue that's going to be embedded in a `<script>` tag
518
- * @param {import('types').JSONValue} val
531
+ * Generates a raw HTML string containing a safe script element carrying JSON data and associated attributes.
532
+ *
533
+ * It escapes all the special characters needed to guarantee the element is unbroken, but care must
534
+ * be taken to ensure it is inserted in the document at an acceptable position for a script element,
535
+ * and that the resulting string isn't further modified.
536
+ *
537
+ * Attribute names must be type-checked so we don't need to escape them.
538
+ *
539
+ * @param {import('types').PayloadScriptAttributes} attrs A list of attributes to be added to the element.
540
+ * @param {import('types').JSONValue} payload The data to be carried by the element. Must be serializable to JSON.
541
+ * @returns {string} The raw HTML of a script element carrying the JSON payload.
542
+ * @example const html = render_json_payload_script({ type: 'data', url: '/data.json' }, { foo: 'bar' });
519
543
  */
520
- function escape_json_in_html(val) {
521
- return JSON.stringify(val).replace(
522
- escape_json_in_html_regex,
523
- (match) => escape_json_in_html_dict[match]
544
+ function render_json_payload_script(attrs, payload) {
545
+ const safe_payload = JSON.stringify(payload).replace(
546
+ render_json_payload_script_regex,
547
+ (match) => render_json_payload_script_dict[match]
524
548
  );
549
+
550
+ let safe_attrs = '';
551
+ for (const [key, value] of Object.entries(attrs)) {
552
+ if (value === undefined) continue;
553
+ safe_attrs += ` sveltekit:data-${key}=${escape_html_attr(value)}`;
554
+ }
555
+
556
+ return `<script type="application/json"${safe_attrs}>${safe_payload}</script>`;
525
557
  }
526
558
 
527
559
  /**
@@ -1084,7 +1116,7 @@ async function render_response({
1084
1116
  /** @type {Map<string, string>} */
1085
1117
  const styles = new Map();
1086
1118
 
1087
- /** @type {Array<{ url: string, body: string, json: string }>} */
1119
+ /** @type {Array<import('./types').Fetched>} */
1088
1120
  const serialized_data = [];
1089
1121
 
1090
1122
  let shadow_props;
@@ -1280,19 +1312,17 @@ async function render_response({
1280
1312
 
1281
1313
  body += `\n\t\t<script ${attributes.join(' ')}>${init_app}</script>`;
1282
1314
 
1283
- // prettier-ignore
1284
1315
  body += serialized_data
1285
- .map(({ url, body, json }) => {
1286
- let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr(url)}`;
1287
- if (body) attributes += ` data-body="${hash(body)}"`;
1288
-
1289
- return `<script ${attributes}>${json}</script>`;
1290
- })
1316
+ .map(({ url, body, response }) =>
1317
+ render_json_payload_script(
1318
+ { type: 'data', url, body: typeof body === 'string' ? hash(body) : undefined },
1319
+ response
1320
+ )
1321
+ )
1291
1322
  .join('\n\t');
1292
1323
 
1293
1324
  if (shadow_props) {
1294
- // prettier-ignore
1295
- body += `<script type="application/json" data-type="svelte-props">${escape_json_in_html(shadow_props)}</script>`;
1325
+ body += render_json_payload_script({ type: 'props' }, shadow_props);
1296
1326
  }
1297
1327
  }
1298
1328
 
@@ -1542,13 +1572,7 @@ async function load_node({
1542
1572
 
1543
1573
  let uses_credentials = false;
1544
1574
 
1545
- /**
1546
- * @type {Array<{
1547
- * url: string;
1548
- * body: string;
1549
- * json: string;
1550
- * }>}
1551
- */
1575
+ /** @type {Array<import('./types').Fetched>} */
1552
1576
  const fetched = [];
1553
1577
 
1554
1578
  /**
@@ -1748,8 +1772,6 @@ async function load_node({
1748
1772
  }
1749
1773
 
1750
1774
  if (!opts.body || typeof opts.body === 'string') {
1751
- // the json constructed below is later added to the dom in a script tag
1752
- // make sure the used values are safe
1753
1775
  const status_number = Number(response.status);
1754
1776
  if (isNaN(status_number)) {
1755
1777
  throw new Error(
@@ -1758,11 +1780,16 @@ async function load_node({
1758
1780
  }" type: ${typeof response.status}`
1759
1781
  );
1760
1782
  }
1761
- // prettier-ignore
1783
+
1762
1784
  fetched.push({
1763
1785
  url: requested,
1764
- body: /** @type {string} */ (opts.body),
1765
- json: `{"status":${status_number},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":${escape_json_in_html(body)}}`
1786
+ body: opts.body,
1787
+ response: {
1788
+ status: status_number,
1789
+ statusText: response.statusText,
1790
+ headers,
1791
+ body
1792
+ }
1766
1793
  });
1767
1794
  }
1768
1795
 
@@ -342,18 +342,32 @@ function crawl(html) {
342
342
  return hrefs;
343
343
  }
344
344
 
345
- // dict from https://github.com/yahoo/serialize-javascript/blob/183c18a776e4635a379fdc620f81771f219832bb/index.js#L25
346
- /** @type {Record<string, string>} */
347
- const escape_json_in_html_dict = {
345
+ /**
346
+ * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
347
+ *
348
+ * The first closes the script element, so everything after is treated as raw HTML.
349
+ * The second disables further parsing until `-->`, so the script element might be unexpectedly
350
+ * kept open until until an unrelated HTML comment in the page.
351
+ *
352
+ * U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018
353
+ * browsers.
354
+ *
355
+ * @see tests for unsafe parsing examples.
356
+ * @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
357
+ * @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
358
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
359
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
360
+ * @see https://github.com/tc39/proposal-json-superset
361
+ * @type {Record<string, string>}
362
+ */
363
+ const render_json_payload_script_dict = {
348
364
  '<': '\\u003C',
349
- '>': '\\u003E',
350
- '/': '\\u002F',
351
365
  '\u2028': '\\u2028',
352
366
  '\u2029': '\\u2029'
353
367
  };
354
368
 
355
369
  new RegExp(
356
- `[${Object.keys(escape_json_in_html_dict).join('')}]`,
370
+ `[${Object.keys(render_json_payload_script_dict).join('')}]`,
357
371
  'g'
358
372
  );
359
373
 
package/dist/cli.js CHANGED
@@ -998,7 +998,7 @@ async function launch(port, https) {
998
998
  exec(`${cmd} ${https ? 'https' : 'http'}://localhost:${port}`);
999
999
  }
1000
1000
 
1001
- const prog = sade('svelte-kit').version('1.0.0-next.287');
1001
+ const prog = sade('svelte-kit').version('1.0.0-next.288');
1002
1002
 
1003
1003
  prog
1004
1004
  .command('dev')
@@ -1156,7 +1156,7 @@ async function check_port(port) {
1156
1156
  function welcome({ port, host, https, open, loose, allow, cwd }) {
1157
1157
  if (open) launch(port, https);
1158
1158
 
1159
- console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.287'}\n`));
1159
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.288'}\n`));
1160
1160
 
1161
1161
  const protocol = https ? 'https:' : 'http:';
1162
1162
  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.287",
3
+ "version": "1.0.0-next.288",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -125,7 +125,7 @@ declare module '$app/navigation' {
125
125
  * If no argument is given, all routes will be fetched, otherwise you can specify routes by any matching pathname
126
126
  * such as `/about` (to match `src/routes/about.svelte`) or `/blog/*` (to match `src/routes/blog/[slug].svelte`).
127
127
  *
128
- * Unlike prefetch, this won't call preload for individual pages.
128
+ * Unlike prefetch, this won't call load for individual pages.
129
129
  * Returns a Promise that resolves when the routes have been prefetched.
130
130
  */
131
131
  export function prefetchRoutes(routes?: string[]): Promise<void>;
@@ -208,7 +208,15 @@ export type HttpMethod = 'get' | 'head' | 'post' | 'put' | 'delete' | 'patch';
208
208
 
209
209
  export type JSONObject = { [key: string]: JSONValue };
210
210
 
211
- export type JSONValue = string | number | boolean | null | ToJSON | JSONValue[] | JSONObject;
211
+ export type JSONValue =
212
+ | string
213
+ | number
214
+ | boolean
215
+ | null
216
+ | undefined
217
+ | ToJSON
218
+ | JSONValue[]
219
+ | JSONObject;
212
220
 
213
221
  export interface LoadInput<Params = Record<string, string>> {
214
222
  url: URL;
@@ -241,6 +249,12 @@ export type MaybePromise<T> = T | Promise<T>;
241
249
 
242
250
  export type Only<T, U> = { [P in keyof T]: T[P] } & { [P in Exclude<keyof U, keyof T>]?: never };
243
251
 
252
+ export type PayloadScriptAttributes = PayloadScriptAttributesData | PayloadScriptAttributesProps;
253
+
254
+ type PayloadScriptAttributesData = { type: 'data'; url: string; body?: string };
255
+
256
+ type PayloadScriptAttributesProps = { type: 'props' };
257
+
244
258
  export interface Prerendered {
245
259
  pages: Map<
246
260
  string,