@sveltejs/kit 1.0.0-next.265 → 1.0.0-next.269

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.
@@ -6,6 +6,22 @@ import { writable } from 'svelte/store';
6
6
  import { base, set_paths } from '../paths.js';
7
7
  import { init } from './singletons.js';
8
8
 
9
+ /**
10
+ * @param {string} path
11
+ * @param {'always' | 'never' | 'ignore'} trailing_slash
12
+ */
13
+ function normalize_path(path, trailing_slash) {
14
+ if (path === '/' || trailing_slash === 'ignore') return path;
15
+
16
+ if (trailing_slash === 'never') {
17
+ return path.endsWith('/') ? path.slice(0, -1) : path;
18
+ } else if (trailing_slash === 'always' && /\/[^./]+$/.test(path)) {
19
+ return path + '/';
20
+ }
21
+
22
+ return path;
23
+ }
24
+
9
25
  function scroll_state() {
10
26
  return {
11
27
  x: pageXOffset,
@@ -396,14 +412,7 @@ class Router {
396
412
  }
397
413
  this.navigating++;
398
414
 
399
- let { pathname } = url;
400
-
401
- if (this.trailing_slash === 'never') {
402
- if (pathname !== '/' && pathname.endsWith('/')) pathname = pathname.slice(0, -1);
403
- } else if (this.trailing_slash === 'always') {
404
- const is_file = /** @type {string} */ (url.pathname.split('/').pop()).includes('.');
405
- if (!is_file && !pathname.endsWith('/')) pathname += '/';
406
- }
415
+ const pathname = normalize_path(url.pathname, this.trailing_slash);
407
416
 
408
417
  info.url = new URL(url.origin + pathname + url.search + url.hash);
409
418
 
@@ -147,7 +147,9 @@ async function render_endpoint(event, mod) {
147
147
 
148
148
  const { status = 200, body = {} } = response;
149
149
  const headers =
150
- response.headers instanceof Headers ? response.headers : to_headers(response.headers);
150
+ response.headers instanceof Headers
151
+ ? new Headers(response.headers)
152
+ : to_headers(response.headers);
151
153
 
152
154
  const type = headers.get('content-type');
153
155
 
@@ -483,52 +485,29 @@ function coalesce_to_error(err) {
483
485
  : new Error(JSON.stringify(err));
484
486
  }
485
487
 
488
+ // dict from https://github.com/yahoo/serialize-javascript/blob/183c18a776e4635a379fdc620f81771f219832bb/index.js#L25
486
489
  /** @type {Record<string, string>} */
487
490
  const escape_json_in_html_dict = {
488
- '&': '\\u0026',
489
- '>': '\\u003e',
490
- '<': '\\u003c',
491
- '\u2028': '\\u2028',
492
- '\u2029': '\\u2029'
493
- };
494
-
495
- /** @type {Record<string, string>} */
496
- const escape_json_value_in_html_dict = {
497
- '"': '\\"',
498
491
  '<': '\\u003C',
499
492
  '>': '\\u003E',
500
493
  '/': '\\u002F',
501
- '\\': '\\\\',
502
- '\b': '\\b',
503
- '\f': '\\f',
504
- '\n': '\\n',
505
- '\r': '\\r',
506
- '\t': '\\t',
507
- '\0': '\\0',
508
494
  '\u2028': '\\u2028',
509
495
  '\u2029': '\\u2029'
510
496
  };
511
497
 
512
- /**
513
- * Escape a stringified JSON object that's going to be embedded in a `<script>` tag
514
- * @param {string} str
515
- */
516
- function escape_json_in_html(str) {
517
- // adapted from https://github.com/vercel/next.js/blob/694407450638b037673c6d714bfe4126aeded740/packages/next/server/htmlescape.ts
518
- // based on https://github.com/zertosh/htmlescape
519
- // License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
520
- return str.replace(/[&><\u2028\u2029]/g, (match) => escape_json_in_html_dict[match]);
521
- }
498
+ const escape_json_in_html_regex = new RegExp(
499
+ `[${Object.keys(escape_json_in_html_dict).join('')}]`,
500
+ 'g'
501
+ );
522
502
 
523
503
  /**
524
- * Escape a string JSON value to be embedded into a `<script>` tag
525
- * @param {string} str
504
+ * Escape a JSONValue that's going to be embedded in a `<script>` tag
505
+ * @param {import("@sveltejs/kit/types/helper").JSONValue} val
526
506
  */
527
- function escape_json_value_in_html(str) {
528
- return escape(
529
- str,
530
- escape_json_value_in_html_dict,
531
- (code) => `\\u${code.toString(16).toUpperCase()}`
507
+ function escape_json_in_html(val) {
508
+ return JSON.stringify(val).replace(
509
+ escape_json_in_html_regex,
510
+ (match) => escape_json_in_html_dict[match]
532
511
  );
533
512
  }
534
513
 
@@ -1301,7 +1280,7 @@ async function render_response({
1301
1280
 
1302
1281
  if (shadow_props) {
1303
1282
  // prettier-ignore
1304
- body += `<script type="application/json" data-type="svelte-props">${escape_json_in_html(s(shadow_props))}</script>`;
1283
+ body += `<script type="application/json" data-type="svelte-props">${escape_json_in_html(shadow_props)}</script>`;
1305
1284
  }
1306
1285
  }
1307
1286
 
@@ -1496,6 +1475,22 @@ function is_root_relative(path) {
1496
1475
  return path[0] === '/' && path[1] !== '/';
1497
1476
  }
1498
1477
 
1478
+ /**
1479
+ * @param {string} path
1480
+ * @param {'always' | 'never' | 'ignore'} trailing_slash
1481
+ */
1482
+ function normalize_path(path, trailing_slash) {
1483
+ if (path === '/' || trailing_slash === 'ignore') return path;
1484
+
1485
+ if (trailing_slash === 'never') {
1486
+ return path.endsWith('/') ? path.slice(0, -1) : path;
1487
+ } else if (trailing_slash === 'always' && /\/[^./]+$/.test(path)) {
1488
+ return path + '/';
1489
+ }
1490
+
1491
+ return path;
1492
+ }
1493
+
1499
1494
  /**
1500
1495
  * @param {{
1501
1496
  * event: import('types/hooks').RequestEvent;
@@ -1555,6 +1550,7 @@ async function load_node({
1555
1550
  ? await load_shadow_data(
1556
1551
  /** @type {import('types/internal').SSRPage} */ (route),
1557
1552
  event,
1553
+ options,
1558
1554
  !!state.prerender
1559
1555
  )
1560
1556
  : {};
@@ -1698,9 +1694,7 @@ async function load_node({
1698
1694
  } else {
1699
1695
  // external
1700
1696
  if (resolved.startsWith('//')) {
1701
- throw new Error(
1702
- `Cannot request protocol-relative URL (${requested}) in server-side fetch`
1703
- );
1697
+ requested = event.url.protocol + requested;
1704
1698
  }
1705
1699
 
1706
1700
  // external fetch
@@ -1742,11 +1736,21 @@ async function load_node({
1742
1736
  }
1743
1737
 
1744
1738
  if (!opts.body || typeof opts.body === 'string') {
1739
+ // the json constructed below is later added to the dom in a script tag
1740
+ // make sure the used values are safe
1741
+ const status_number = Number(response.status);
1742
+ if (isNaN(status_number)) {
1743
+ throw new Error(
1744
+ `response.status is not a number. value: "${
1745
+ response.status
1746
+ }" type: ${typeof response.status}`
1747
+ );
1748
+ }
1745
1749
  // prettier-ignore
1746
1750
  fetched.push({
1747
1751
  url: requested,
1748
1752
  body: /** @type {string} */ (opts.body),
1749
- json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":"${escape_json_value_in_html(body)}"}`
1753
+ json: `{"status":${status_number},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":${escape_json_in_html(body)}}`
1750
1754
  });
1751
1755
  }
1752
1756
 
@@ -1851,10 +1855,11 @@ async function load_node({
1851
1855
  *
1852
1856
  * @param {import('types/internal').SSRPage} route
1853
1857
  * @param {import('types/hooks').RequestEvent} event
1858
+ * @param {import('types/internal').SSROptions} options
1854
1859
  * @param {boolean} prerender
1855
1860
  * @returns {Promise<import('types/endpoint').ShadowData>}
1856
1861
  */
1857
- async function load_shadow_data(route, event, prerender) {
1862
+ async function load_shadow_data(route, event, options, prerender) {
1858
1863
  if (!route.shadow) return {};
1859
1864
 
1860
1865
  try {
@@ -1887,22 +1892,21 @@ async function load_shadow_data(route, event, prerender) {
1887
1892
  if (result.fallthrough) return result;
1888
1893
 
1889
1894
  const { status, headers, body } = validate_shadow_output(result);
1895
+ data.status = status;
1896
+
1890
1897
  add_cookies(/** @type {string[]} */ (data.cookies), headers);
1891
1898
 
1892
1899
  // Redirects are respected...
1893
1900
  if (status >= 300 && status < 400) {
1894
- return {
1895
- status,
1896
- redirect: /** @type {string} */ (
1897
- headers instanceof Headers ? headers.get('location') : headers.location
1898
- )
1899
- };
1901
+ data.redirect = /** @type {string} */ (
1902
+ headers instanceof Headers ? headers.get('location') : headers.location
1903
+ );
1904
+ return data;
1900
1905
  }
1901
1906
 
1902
1907
  // ...but 4xx and 5xx status codes _don't_ result in the error page
1903
1908
  // rendering for non-GET requests — instead, we allow the page
1904
1909
  // to render with any validation errors etc that were returned
1905
- data.status = status;
1906
1910
  data.body = body;
1907
1911
  }
1908
1912
 
@@ -1913,21 +1917,18 @@ async function load_shadow_data(route, event, prerender) {
1913
1917
 
1914
1918
  const { status, headers, body } = validate_shadow_output(result);
1915
1919
  add_cookies(/** @type {string[]} */ (data.cookies), headers);
1920
+ data.status = status;
1916
1921
 
1917
1922
  if (status >= 400) {
1918
- return {
1919
- status,
1920
- error: new Error('Failed to load data')
1921
- };
1923
+ data.error = new Error('Failed to load data');
1924
+ return data;
1922
1925
  }
1923
1926
 
1924
1927
  if (status >= 300) {
1925
- return {
1926
- status,
1927
- redirect: /** @type {string} */ (
1928
- headers instanceof Headers ? headers.get('location') : headers.location
1929
- )
1930
- };
1928
+ data.redirect = /** @type {string} */ (
1929
+ headers instanceof Headers ? headers.get('location') : headers.location
1930
+ );
1931
+ return data;
1931
1932
  }
1932
1933
 
1933
1934
  data.body = { ...body, ...data.body };
@@ -1935,9 +1936,12 @@ async function load_shadow_data(route, event, prerender) {
1935
1936
 
1936
1937
  return data;
1937
1938
  } catch (e) {
1939
+ const error = coalesce_to_error(e);
1940
+ options.handle_error(error, event);
1941
+
1938
1942
  return {
1939
1943
  status: 500,
1940
- error: coalesce_to_error(e)
1944
+ error
1941
1945
  };
1942
1946
  }
1943
1947
  }
@@ -2446,26 +2450,15 @@ const DATA_SUFFIX = '/__data.json';
2446
2450
  async function respond(request, options, state = {}) {
2447
2451
  const url = new URL(request.url);
2448
2452
 
2449
- if (url.pathname !== '/' && options.trailing_slash !== 'ignore') {
2450
- const has_trailing_slash = url.pathname.endsWith('/');
2451
-
2452
- if (
2453
- (has_trailing_slash && options.trailing_slash === 'never') ||
2454
- (!has_trailing_slash &&
2455
- options.trailing_slash === 'always' &&
2456
- !(url.pathname.split('/').pop() || '').includes('.'))
2457
- ) {
2458
- url.pathname = has_trailing_slash ? url.pathname.slice(0, -1) : url.pathname + '/';
2459
-
2460
- if (url.search === '?') url.search = '';
2453
+ const normalized = normalize_path(url.pathname, options.trailing_slash);
2461
2454
 
2462
- return new Response(undefined, {
2463
- status: 301,
2464
- headers: {
2465
- location: url.pathname + url.search
2466
- }
2467
- });
2468
- }
2455
+ if (normalized !== url.pathname) {
2456
+ return new Response(undefined, {
2457
+ status: 301,
2458
+ headers: {
2459
+ location: normalized + (url.search === '?' ? '' : url.search)
2460
+ }
2461
+ });
2469
2462
  }
2470
2463
 
2471
2464
  const { parameter, allowed } = options.method_override;
@@ -2594,11 +2587,11 @@ async function respond(request, options, state = {}) {
2594
2587
  const location = response.headers.get('location');
2595
2588
 
2596
2589
  if (location) {
2590
+ const headers = new Headers(response.headers);
2591
+ headers.set('x-sveltekit-location', location);
2597
2592
  response = new Response(undefined, {
2598
2593
  status: 204,
2599
- headers: {
2600
- 'x-sveltekit-location': location
2601
- }
2594
+ headers
2602
2595
  });
2603
2596
  }
2604
2597
  }
@@ -1,4 +1,4 @@
1
- import { m as mkdirp, b as SVELTE_KIT, h as rimraf, i as copy, $, j as logger } from '../cli.js';
1
+ import { b as SVELTE_KIT, m as mkdirp, h as rimraf, i as copy, $, j as logger } from '../cli.js';
2
2
  import { readFileSync, writeFileSync } from 'fs';
3
3
  import { resolve as resolve$1, join, dirname } from 'path';
4
4
  import { pathToFileURL, URL } from 'url';
@@ -55,6 +55,22 @@ function is_root_relative(path) {
55
55
  return path[0] === '/' && path[1] !== '/';
56
56
  }
57
57
 
58
+ /**
59
+ * @param {string} path
60
+ * @param {'always' | 'never' | 'ignore'} trailing_slash
61
+ */
62
+ function normalize_path(path, trailing_slash) {
63
+ if (path === '/' || trailing_slash === 'ignore') return path;
64
+
65
+ if (trailing_slash === 'never') {
66
+ return path.endsWith('/') ? path.slice(0, -1) : path;
67
+ } else if (trailing_slash === 'always' && /\/[^./]+$/.test(path)) {
68
+ return path + '/';
69
+ }
70
+
71
+ return path;
72
+ }
73
+
58
74
  /** @typedef {{
59
75
  * fn: () => Promise<any>,
60
76
  * fulfil: (value: any) => void,
@@ -331,7 +347,20 @@ function crawl(html) {
331
347
  return hrefs;
332
348
  }
333
349
 
350
+ // dict from https://github.com/yahoo/serialize-javascript/blob/183c18a776e4635a379fdc620f81771f219832bb/index.js#L25
334
351
  /** @type {Record<string, string>} */
352
+ const escape_json_in_html_dict = {
353
+ '<': '\\u003C',
354
+ '>': '\\u003E',
355
+ '/': '\\u002F',
356
+ '\u2028': '\\u2028',
357
+ '\u2029': '\\u2029'
358
+ };
359
+
360
+ new RegExp(
361
+ `[${Object.keys(escape_json_in_html_dict).join('')}]`,
362
+ 'g'
363
+ );
335
364
 
336
365
  /**
337
366
  * @param str {string} string to escape
@@ -425,25 +454,27 @@ const REDIRECT = 3;
425
454
  * fallback?: string;
426
455
  * all: boolean; // disregard `export const prerender = true`
427
456
  * }} opts
428
- * @returns {Promise<{ paths: string[] }>} returns a promise that resolves to an array of paths corresponding to the files that have been prerendered.
429
457
  */
430
458
  async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
459
+ /** @type {import('types/config').Prerendered} */
460
+ const prerendered = {
461
+ pages: new Map(),
462
+ assets: new Map(),
463
+ redirects: new Map(),
464
+ paths: []
465
+ };
466
+
431
467
  if (!config.kit.prerender.enabled && !fallback) {
432
- return { paths: [] };
468
+ return prerendered;
433
469
  }
434
470
 
435
471
  __fetch_polyfill();
436
472
 
437
- mkdirp(out);
438
-
439
- const dir = resolve$1(cwd, `${SVELTE_KIT}/output`);
440
-
441
- const seen = new Set();
442
-
443
- const server_root = resolve$1(dir);
473
+ const server_root = resolve$1(cwd, `${SVELTE_KIT}/output`);
444
474
 
445
475
  /** @type {import('types/internal').AppModule} */
446
476
  const { App, override } = await import(pathToFileURL(`${server_root}/server/app.js`).href);
477
+ const { manifest } = await import(pathToFileURL(`${server_root}/server/manifest.js`).href);
447
478
 
448
479
  override({
449
480
  paths: config.kit.paths,
@@ -451,8 +482,6 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
451
482
  read: (file) => readFileSync(join(config.kit.files.assets, file))
452
483
  });
453
484
 
454
- const { manifest } = await import(pathToFileURL(`${server_root}/server/manifest.js`).href);
455
-
456
485
  const app = new App(manifest);
457
486
 
458
487
  const error = normalise_error_handler(log, config.kit.prerender.onError);
@@ -463,28 +492,12 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
463
492
  ...build_data.client.assets.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`)
464
493
  ]);
465
494
 
466
- /** @type {string[]} */
467
- const paths = [];
468
-
469
495
  build_data.static.forEach((file) => {
470
496
  if (file.endsWith('/index.html')) {
471
497
  files.add(file.slice(0, -11));
472
498
  }
473
499
  });
474
500
 
475
- /**
476
- * @param {string} path
477
- */
478
- function normalize(path) {
479
- if (config.kit.trailingSlash === 'always') {
480
- return path.endsWith('/') ? path : `${path}/`;
481
- } else if (config.kit.trailingSlash === 'never') {
482
- return !path.endsWith('/') || path === '/' ? path : path.slice(0, -1);
483
- }
484
-
485
- return path;
486
- }
487
-
488
501
  const q = queue(config.kit.prerender.concurrency);
489
502
 
490
503
  /**
@@ -492,150 +505,168 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
492
505
  * @param {boolean} is_html
493
506
  */
494
507
  function output_filename(path, is_html) {
495
- path = path.slice(config.kit.paths.base.length) || '/';
508
+ const file = path.slice(config.kit.paths.base.length + 1);
496
509
 
497
- if (path === '/') {
498
- return '/index.html';
510
+ if (file === '') {
511
+ return 'index.html';
499
512
  }
500
513
 
501
- const parts = path.split('/');
502
- if (is_html && parts[parts.length - 1] !== 'index.html') {
503
- if (config.kit.trailingSlash === 'always') {
504
- parts.push('index.html');
505
- } else {
506
- parts[parts.length - 1] += '.html';
507
- }
514
+ if (is_html && !file.endsWith('.html')) {
515
+ return file + (config.kit.trailingSlash === 'always' ? 'index.html' : '.html');
508
516
  }
509
- return parts.join('/');
517
+
518
+ return file;
510
519
  }
511
520
 
521
+ const seen = new Set();
522
+ const written = new Set();
523
+
512
524
  /**
513
- * @param {string} decoded_path
514
- * @param {string?} referrer
525
+ * @param {string | null} referrer
526
+ * @param {string} decoded
527
+ * @param {string} [encoded]
515
528
  */
516
- function enqueue(decoded_path, referrer) {
517
- const path = encodeURI(normalize(decoded_path));
529
+ function enqueue(referrer, decoded, encoded) {
530
+ if (seen.has(decoded)) return;
531
+ seen.add(decoded);
518
532
 
519
- if (seen.has(path)) return;
520
- seen.add(path);
533
+ const file = decoded.slice(config.kit.paths.base.length + 1);
534
+ if (files.has(file)) return;
521
535
 
522
- return q.add(() => visit(path, decoded_path, referrer));
536
+ return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer));
523
537
  }
524
538
 
525
539
  /**
526
- * @param {string} path
527
- * @param {string} decoded_path
540
+ * @param {string} decoded
541
+ * @param {string} encoded
528
542
  * @param {string?} referrer
529
543
  */
530
- async function visit(path, decoded_path, referrer) {
531
- if (!path.startsWith(config.kit.paths.base)) {
532
- error({ status: 404, path, referrer, referenceType: 'linked' });
544
+ async function visit(decoded, encoded, referrer) {
545
+ if (!decoded.startsWith(config.kit.paths.base)) {
546
+ error({ status: 404, path: decoded, referrer, referenceType: 'linked' });
533
547
  return;
534
548
  }
535
549
 
536
550
  /** @type {Map<string, import('types/internal').PrerenderDependency>} */
537
551
  const dependencies = new Map();
538
552
 
539
- const rendered = await app.render(new Request(`http://sveltekit-prerender${path}`), {
553
+ const response = await app.render(new Request(`http://sveltekit-prerender${encoded}`), {
540
554
  prerender: {
541
555
  all,
542
556
  dependencies
543
557
  }
544
558
  });
545
559
 
546
- const response_type = Math.floor(rendered.status / 100);
547
- const type = rendered.headers.get('content-type');
560
+ const text = await response.text();
561
+
562
+ save(response, text, decoded, encoded, referrer, 'linked');
563
+
564
+ for (const [dependency_path, result] of dependencies) {
565
+ // this seems circuitous, but using new URL allows us to not care
566
+ // whether dependency_path is encoded or not
567
+ const encoded_dependency_path = new URL(dependency_path, 'http://localhost').pathname;
568
+ const decoded_dependency_path = decodeURI(encoded_dependency_path);
569
+
570
+ const body = result.body ?? new Uint8Array(await result.response.arrayBuffer());
571
+ save(
572
+ result.response,
573
+ body,
574
+ decoded_dependency_path,
575
+ encoded_dependency_path,
576
+ decoded,
577
+ 'fetched'
578
+ );
579
+ }
580
+
581
+ if (config.kit.prerender.crawl && response.headers.get('content-type') === 'text/html') {
582
+ for (const href of crawl(text)) {
583
+ if (href.startsWith('data:') || href.startsWith('#')) continue;
584
+
585
+ const resolved = resolve(encoded, href);
586
+ if (!is_root_relative(resolved)) continue;
587
+
588
+ const parsed = new URL(resolved, 'http://localhost');
589
+
590
+ if (parsed.search) ;
591
+
592
+ const pathname = normalize_path(parsed.pathname, config.kit.trailingSlash);
593
+ enqueue(decoded, decodeURI(pathname), pathname);
594
+ }
595
+ }
596
+ }
597
+
598
+ /**
599
+ * @param {Response} response
600
+ * @param {string | Uint8Array} body
601
+ * @param {string} decoded
602
+ * @param {string} encoded
603
+ * @param {string | null} referrer
604
+ * @param {'linked' | 'fetched'} referenceType
605
+ */
606
+ function save(response, body, decoded, encoded, referrer, referenceType) {
607
+ const response_type = Math.floor(response.status / 100);
608
+ const type = /** @type {string} */ (response.headers.get('content-type'));
548
609
  const is_html = response_type === REDIRECT || type === 'text/html';
549
610
 
550
- const file = `${out}${output_filename(decoded_path, is_html)}`;
611
+ const file = output_filename(decoded, is_html);
612
+ const dest = `${out}/${file}`;
613
+
614
+ if (written.has(file)) return;
615
+ written.add(file);
551
616
 
552
617
  if (response_type === REDIRECT) {
553
- const location = rendered.headers.get('location');
618
+ const location = response.headers.get('location');
554
619
 
555
620
  if (location) {
556
- mkdirp(dirname(file));
621
+ mkdirp(dirname(dest));
557
622
 
558
- log.warn(`${rendered.status} ${decoded_path} -> ${location}`);
623
+ log.warn(`${response.status} ${decoded} -> ${location}`);
559
624
 
560
625
  writeFileSync(
561
- file,
626
+ dest,
562
627
  `<meta http-equiv="refresh" content=${escape_html_attr(`0;url=${location}`)}>`
563
628
  );
564
629
 
565
- const resolved = resolve(path, location);
630
+ let resolved = resolve(encoded, location);
566
631
  if (is_root_relative(resolved)) {
567
- enqueue(resolved, path);
632
+ resolved = normalize_path(resolved, config.kit.trailingSlash);
633
+ enqueue(decoded, decodeURI(resolved), resolved);
634
+ }
635
+
636
+ if (!prerendered.redirects.has(decoded)) {
637
+ prerendered.redirects.set(decoded, {
638
+ status: response.status,
639
+ location: resolved
640
+ });
641
+
642
+ prerendered.paths.push(normalize_path(decoded, 'never'));
568
643
  }
569
644
  } else {
570
- log.warn(`location header missing on redirect received from ${decoded_path}`);
645
+ log.warn(`location header missing on redirect received from ${decoded}`);
571
646
  }
572
647
 
573
648
  return;
574
649
  }
575
650
 
576
- const text = await rendered.text();
577
-
578
- if (rendered.status === 200) {
579
- mkdirp(dirname(file));
580
-
581
- log.info(`${rendered.status} ${decoded_path}`);
582
- writeFileSync(file, text);
583
- paths.push(normalize(decoded_path));
584
- } else if (response_type !== OK) {
585
- error({ status: rendered.status, path, referrer, referenceType: 'linked' });
586
- }
587
-
588
- for (const [dependency_path, result] of dependencies) {
589
- const { status, headers } = result.response;
590
-
591
- const response_type = Math.floor(status / 100);
592
-
593
- const is_html = headers.get('content-type') === 'text/html';
594
-
595
- const file = `${out}${output_filename(dependency_path, is_html)}`;
596
- mkdirp(dirname(file));
651
+ if (response.status === 200) {
652
+ mkdirp(dirname(dest));
597
653
 
598
- writeFileSync(
599
- file,
600
- result.body === null ? new Uint8Array(await result.response.arrayBuffer()) : result.body
601
- );
602
- paths.push(dependency_path);
654
+ log.info(`${response.status} ${decoded}`);
655
+ writeFileSync(dest, body);
603
656
 
604
- if (response_type === OK) {
605
- log.info(`${status} ${dependency_path}`);
657
+ if (is_html) {
658
+ prerendered.pages.set(decoded, {
659
+ file
660
+ });
606
661
  } else {
607
- error({
608
- status,
609
- path: dependency_path,
610
- referrer: path,
611
- referenceType: 'fetched'
662
+ prerendered.assets.set(decoded, {
663
+ type
612
664
  });
613
665
  }
614
- }
615
-
616
- if (is_html && config.kit.prerender.crawl) {
617
- for (const href of crawl(text)) {
618
- if (href.startsWith('data:') || href.startsWith('#')) continue;
619
666
 
620
- const resolved = resolve(path, href);
621
- if (!is_root_relative(resolved)) continue;
622
-
623
- const parsed = new URL(resolved, 'http://localhost');
624
-
625
- let pathname = decodeURI(parsed.pathname);
626
-
627
- if (config.kit.paths.base) {
628
- if (!pathname.startsWith(config.kit.paths.base)) continue;
629
- pathname = pathname.slice(config.kit.paths.base.length) || '/';
630
- }
631
-
632
- const file = pathname.slice(1);
633
- if (files.has(file)) continue;
634
-
635
- if (parsed.search) ;
636
-
637
- enqueue(pathname, path);
638
- }
667
+ prerendered.paths.push(normalize_path(decoded, 'never'));
668
+ } else if (response_type !== OK) {
669
+ error({ status: response.status, path: decoded, referrer, referenceType });
639
670
  }
640
671
  }
641
672
 
@@ -643,10 +674,10 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
643
674
  for (const entry of config.kit.prerender.entries) {
644
675
  if (entry === '*') {
645
676
  for (const entry of build_data.entries) {
646
- enqueue(config.kit.paths.base + entry, null);
677
+ enqueue(null, normalize_path(config.kit.paths.base + entry, config.kit.trailingSlash)); // TODO can we pre-normalize these?
647
678
  }
648
679
  } else {
649
- enqueue(config.kit.paths.base + entry, null);
680
+ enqueue(null, normalize_path(config.kit.paths.base + entry, config.kit.trailingSlash));
650
681
  }
651
682
  }
652
683
 
@@ -667,9 +698,7 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
667
698
  writeFileSync(file, await rendered.text());
668
699
  }
669
700
 
670
- return {
671
- paths
672
- };
701
+ return prerendered;
673
702
  }
674
703
 
675
704
  /**
@@ -683,11 +712,14 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
683
712
  */
684
713
  function create_builder({ cwd, config, build_data, log }) {
685
714
  /** @type {Set<string>} */
686
- const prerendered_paths = new Set();
715
+ let prerendered_paths;
716
+
687
717
  let generated_manifest = false;
688
718
 
689
719
  /** @param {import('types/internal').RouteData} route */
690
720
  function not_prerendered(route) {
721
+ if (!prerendered_paths) return true;
722
+
691
723
  if (route.type === 'page' && route.path) {
692
724
  return !prerendered_paths.has(route.path);
693
725
  }
@@ -832,10 +864,7 @@ function create_builder({ cwd, config, build_data, log }) {
832
864
  log
833
865
  });
834
866
 
835
- prerendered.paths.forEach((path) => {
836
- prerendered_paths.add(path);
837
- prerendered_paths.add(path + '/');
838
- });
867
+ prerendered_paths = new Set(prerendered.paths);
839
868
 
840
869
  return prerendered;
841
870
  }
package/dist/cli.js CHANGED
@@ -620,7 +620,8 @@ const options = object(
620
620
  concurrency: number(1),
621
621
  crawl: boolean(true),
622
622
  createIndexFiles: error(
623
- (keypath) => `${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/configuration#trailingslash`
623
+ (keypath) =>
624
+ `${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/configuration#trailingslash`
624
625
  ),
625
626
  enabled: boolean(true),
626
627
  entries: validate(['*'], (input, keypath) => {
@@ -677,7 +678,7 @@ const options = object(
677
678
 
678
679
  serviceWorker: object({
679
680
  register: boolean(true),
680
- files: fun((filename) => !/\.DS_STORE/.test(filename))
681
+ files: fun((filename) => !/\.DS_Store/.test(filename))
681
682
  }),
682
683
 
683
684
  // TODO remove this for 1.0
@@ -997,7 +998,7 @@ async function launch(port, https) {
997
998
  exec(`${cmd} ${https ? 'https' : 'http'}://localhost:${port}`);
998
999
  }
999
1000
 
1000
- const prog = sade('svelte-kit').version('1.0.0-next.265');
1001
+ const prog = sade('svelte-kit').version('1.0.0-next.269');
1001
1002
 
1002
1003
  prog
1003
1004
  .command('dev')
@@ -1155,7 +1156,7 @@ async function check_port(port) {
1155
1156
  function welcome({ port, host, https, open, loose, allow, cwd }) {
1156
1157
  if (open) launch(port, https);
1157
1158
 
1158
- console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.265'}\n`));
1159
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.269'}\n`));
1159
1160
 
1160
1161
  const protocol = https ? 'https:' : 'http:';
1161
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.265",
3
+ "version": "1.0.0-next.269",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
package/types/config.d.ts CHANGED
@@ -35,6 +35,32 @@ export interface AdapterEntry {
35
35
  }) => void;
36
36
  }
37
37
 
38
+ export interface Prerendered {
39
+ pages: Map<
40
+ string,
41
+ {
42
+ /** The location of the .html file relative to the output directory */
43
+ file: string;
44
+ }
45
+ >;
46
+ assets: Map<
47
+ string,
48
+ {
49
+ /** The MIME type of the asset */
50
+ type: string;
51
+ }
52
+ >;
53
+ redirects: Map<
54
+ string,
55
+ {
56
+ status: number;
57
+ location: string;
58
+ }
59
+ >;
60
+ /** An array of prerendered paths (without trailing slashes, regardless of the trailingSlash config) */
61
+ paths: string[];
62
+ }
63
+
38
64
  export interface Builder {
39
65
  log: Logger;
40
66
  rimraf(dir: string): void;
@@ -87,9 +113,7 @@ export interface Builder {
87
113
  }
88
114
  ): string[];
89
115
 
90
- prerender(options: { all?: boolean; dest: string; fallback?: string }): Promise<{
91
- paths: string[];
92
- }>;
116
+ prerender(options: { all?: boolean; dest: string; fallback?: string }): Promise<Prerendered>;
93
117
  }
94
118
 
95
119
  export interface Adapter {
package/types/index.d.ts CHANGED
@@ -4,7 +4,14 @@
4
4
  import './ambient-modules';
5
5
 
6
6
  export { App, SSRManifest } from './app';
7
- export { Adapter, Builder, Config, PrerenderErrorHandler, ValidatedConfig } from './config';
7
+ export {
8
+ Adapter,
9
+ Builder,
10
+ Config,
11
+ Prerendered,
12
+ PrerenderErrorHandler,
13
+ ValidatedConfig
14
+ } from './config';
8
15
  export { EndpointOutput, RequestHandler } from './endpoint';
9
16
  export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput } from './page';
10
17
  export { ExternalFetch, GetSession, Handle, HandleError, RequestEvent, ResolveOpts } from './hooks';