@sveltejs/kit 1.0.0-next.264 → 1.0.0-next.268

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;
@@ -1698,9 +1693,7 @@ async function load_node({
1698
1693
  } else {
1699
1694
  // external
1700
1695
  if (resolved.startsWith('//')) {
1701
- throw new Error(
1702
- `Cannot request protocol-relative URL (${requested}) in server-side fetch`
1703
- );
1696
+ requested = event.url.protocol + requested;
1704
1697
  }
1705
1698
 
1706
1699
  // external fetch
@@ -1742,11 +1735,21 @@ async function load_node({
1742
1735
  }
1743
1736
 
1744
1737
  if (!opts.body || typeof opts.body === 'string') {
1738
+ // the json constructed below is later added to the dom in a script tag
1739
+ // make sure the used values are safe
1740
+ const status_number = Number(response.status);
1741
+ if (isNaN(status_number)) {
1742
+ throw new Error(
1743
+ `response.status is not a number. value: "${
1744
+ response.status
1745
+ }" type: ${typeof response.status}`
1746
+ );
1747
+ }
1745
1748
  // prettier-ignore
1746
1749
  fetched.push({
1747
1750
  url: requested,
1748
1751
  body: /** @type {string} */ (opts.body),
1749
- json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":"${escape_json_value_in_html(body)}"}`
1752
+ json: `{"status":${status_number},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":${escape_json_in_html(body)}}`
1750
1753
  });
1751
1754
  }
1752
1755
 
@@ -1887,22 +1890,21 @@ async function load_shadow_data(route, event, prerender) {
1887
1890
  if (result.fallthrough) return result;
1888
1891
 
1889
1892
  const { status, headers, body } = validate_shadow_output(result);
1893
+ data.status = status;
1894
+
1890
1895
  add_cookies(/** @type {string[]} */ (data.cookies), headers);
1891
1896
 
1892
1897
  // Redirects are respected...
1893
1898
  if (status >= 300 && status < 400) {
1894
- return {
1895
- status,
1896
- redirect: /** @type {string} */ (
1897
- headers instanceof Headers ? headers.get('location') : headers.location
1898
- )
1899
- };
1899
+ data.redirect = /** @type {string} */ (
1900
+ headers instanceof Headers ? headers.get('location') : headers.location
1901
+ );
1902
+ return data;
1900
1903
  }
1901
1904
 
1902
1905
  // ...but 4xx and 5xx status codes _don't_ result in the error page
1903
1906
  // rendering for non-GET requests — instead, we allow the page
1904
1907
  // to render with any validation errors etc that were returned
1905
- data.status = status;
1906
1908
  data.body = body;
1907
1909
  }
1908
1910
 
@@ -1913,21 +1915,18 @@ async function load_shadow_data(route, event, prerender) {
1913
1915
 
1914
1916
  const { status, headers, body } = validate_shadow_output(result);
1915
1917
  add_cookies(/** @type {string[]} */ (data.cookies), headers);
1918
+ data.status = status;
1916
1919
 
1917
1920
  if (status >= 400) {
1918
- return {
1919
- status,
1920
- error: new Error('Failed to load data')
1921
- };
1921
+ data.error = new Error('Failed to load data');
1922
+ return data;
1922
1923
  }
1923
1924
 
1924
1925
  if (status >= 300) {
1925
- return {
1926
- status,
1927
- redirect: /** @type {string} */ (
1928
- headers instanceof Headers ? headers.get('location') : headers.location
1929
- )
1930
- };
1926
+ data.redirect = /** @type {string} */ (
1927
+ headers instanceof Headers ? headers.get('location') : headers.location
1928
+ );
1929
+ return data;
1931
1930
  }
1932
1931
 
1933
1932
  data.body = { ...body, ...data.body };
@@ -2446,26 +2445,15 @@ const DATA_SUFFIX = '/__data.json';
2446
2445
  async function respond(request, options, state = {}) {
2447
2446
  const url = new URL(request.url);
2448
2447
 
2449
- if (url.pathname !== '/' && options.trailing_slash !== 'ignore') {
2450
- const has_trailing_slash = url.pathname.endsWith('/');
2448
+ const normalized = normalize_path(url.pathname, options.trailing_slash);
2451
2449
 
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 = '';
2461
-
2462
- return new Response(undefined, {
2463
- status: 301,
2464
- headers: {
2465
- location: url.pathname + url.search
2466
- }
2467
- });
2468
- }
2450
+ if (normalized !== url.pathname) {
2451
+ return new Response(undefined, {
2452
+ status: 301,
2453
+ headers: {
2454
+ location: normalized + (url.search === '?' ? '' : url.search)
2455
+ }
2456
+ });
2469
2457
  }
2470
2458
 
2471
2459
  const { parameter, allowed } = options.method_override;
@@ -2594,11 +2582,11 @@ async function respond(request, options, state = {}) {
2594
2582
  const location = response.headers.get('location');
2595
2583
 
2596
2584
  if (location) {
2585
+ const headers = new Headers(response.headers);
2586
+ headers.set('x-sveltekit-location', location);
2597
2587
  response = new Response(undefined, {
2598
2588
  status: 204,
2599
- headers: {
2600
- 'x-sveltekit-location': location
2601
- }
2589
+ headers
2602
2590
  });
2603
2591
  }
2604
2592
  }
@@ -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
@@ -391,21 +420,21 @@ function escape_html_attr(str) {
391
420
  * @typedef {import('types/internal').Logger} Logger
392
421
  */
393
422
 
394
- /** @type {(errorDetails: Parameters<PrerenderErrorHandler>[0] ) => string} */
395
- function errorDetailsToString({ status, path, referrer, referenceType }) {
423
+ /** @type {(details: Parameters<PrerenderErrorHandler>[0] ) => string} */
424
+ function format_error({ status, path, referrer, referenceType }) {
396
425
  return `${status} ${path}${referrer ? ` (${referenceType} from ${referrer})` : ''}`;
397
426
  }
398
427
 
399
428
  /** @type {(log: Logger, onError: OnError) => PrerenderErrorHandler} */
400
- function chooseErrorHandler(log, onError) {
429
+ function normalise_error_handler(log, onError) {
401
430
  switch (onError) {
402
431
  case 'continue':
403
- return (errorDetails) => {
404
- log.error(errorDetailsToString(errorDetails));
432
+ return (details) => {
433
+ log.error(format_error(details));
405
434
  };
406
435
  case 'fail':
407
- return (errorDetails) => {
408
- throw new Error(errorDetailsToString(errorDetails));
436
+ return (details) => {
437
+ throw new Error(format_error(details));
409
438
  };
410
439
  default:
411
440
  return onError;
@@ -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,11 +482,9 @@ 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
- const error = chooseErrorHandler(log, config.kit.prerender.onError);
487
+ const error = normalise_error_handler(log, config.kit.prerender.onError);
459
488
 
460
489
  const files = new Set([
461
490
  ...build_data.static,
@@ -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,148 +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
- if (path === '/') {
496
- return '/index.html';
508
+ const file = path.slice(config.kit.paths.base.length + 1);
509
+
510
+ if (file === '') {
511
+ return 'index.html';
497
512
  }
498
- const parts = path.split('/');
499
- if (is_html && parts[parts.length - 1] !== 'index.html') {
500
- if (config.kit.prerender.createIndexFiles) {
501
- parts.push('index.html');
502
- } else {
503
- parts[parts.length - 1] += '.html';
504
- }
513
+
514
+ if (is_html && !file.endsWith('.html')) {
515
+ return file + (config.kit.trailingSlash === 'always' ? 'index.html' : '.html');
505
516
  }
506
- return parts.join('/');
517
+
518
+ return file;
507
519
  }
508
520
 
521
+ const seen = new Set();
522
+ const written = new Set();
523
+
509
524
  /**
510
- * @param {string} decoded_path
511
- * @param {string?} referrer
525
+ * @param {string | null} referrer
526
+ * @param {string} decoded
527
+ * @param {string} [encoded]
512
528
  */
513
- function enqueue(decoded_path, referrer) {
514
- const path = encodeURI(normalize(decoded_path));
529
+ function enqueue(referrer, decoded, encoded) {
530
+ if (seen.has(decoded)) return;
531
+ seen.add(decoded);
515
532
 
516
- if (seen.has(path)) return;
517
- seen.add(path);
533
+ const file = decoded.slice(config.kit.paths.base.length + 1);
534
+ if (files.has(file)) return;
518
535
 
519
- return q.add(() => visit(path, decoded_path, referrer));
536
+ return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer));
520
537
  }
521
538
 
522
539
  /**
523
- * @param {string} path
524
- * @param {string} decoded_path
540
+ * @param {string} decoded
541
+ * @param {string} encoded
525
542
  * @param {string?} referrer
526
543
  */
527
- async function visit(path, decoded_path, referrer) {
544
+ async function visit(decoded, encoded, referrer) {
545
+ if (!decoded.startsWith(config.kit.paths.base)) {
546
+ error({ status: 404, path: decoded, referrer, referenceType: 'linked' });
547
+ return;
548
+ }
549
+
528
550
  /** @type {Map<string, import('types/internal').PrerenderDependency>} */
529
551
  const dependencies = new Map();
530
552
 
531
- const render_path = config.kit.paths?.base
532
- ? `http://sveltekit-prerender${config.kit.paths.base}${path === '/' ? '' : path}`
533
- : `http://sveltekit-prerender${path}`;
534
-
535
- const rendered = await app.render(new Request(render_path), {
553
+ const response = await app.render(new Request(`http://sveltekit-prerender${encoded}`), {
536
554
  prerender: {
537
555
  all,
538
556
  dependencies
539
557
  }
540
558
  });
541
559
 
542
- if (rendered) {
543
- const response_type = Math.floor(rendered.status / 100);
544
- const type = rendered.headers.get('content-type');
545
- const is_html = response_type === REDIRECT || type === 'text/html';
560
+ const text = await response.text();
546
561
 
547
- const file = `${out}${output_filename(decoded_path, is_html)}`;
562
+ save(response, text, decoded, encoded, referrer, 'linked');
548
563
 
549
- if (response_type === REDIRECT) {
550
- const location = rendered.headers.get('location');
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);
551
569
 
552
- if (location) {
553
- mkdirp(dirname(file));
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
+ }
554
580
 
555
- log.warn(`${rendered.status} ${decoded_path} -> ${location}`);
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;
556
584
 
557
- writeFileSync(
558
- file,
559
- `<meta http-equiv="refresh" content=${escape_html_attr(`0;url=${location}`)}>`
560
- );
585
+ const resolved = resolve(encoded, href);
586
+ if (!is_root_relative(resolved)) continue;
561
587
 
562
- const resolved = resolve(path, location);
563
- if (is_root_relative(resolved)) {
564
- enqueue(resolved, path);
565
- }
566
- } else {
567
- log.warn(`location header missing on redirect received from ${decoded_path}`);
568
- }
588
+ const parsed = new URL(resolved, 'http://localhost');
569
589
 
570
- return;
571
- }
590
+ if (parsed.search) ;
572
591
 
573
- const text = await rendered.text();
592
+ const pathname = normalize_path(parsed.pathname, config.kit.trailingSlash);
593
+ enqueue(decoded, decodeURI(pathname), pathname);
594
+ }
595
+ }
596
+ }
574
597
 
575
- if (rendered.status === 200) {
576
- mkdirp(dirname(file));
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'));
609
+ const is_html = response_type === REDIRECT || type === 'text/html';
577
610
 
578
- log.info(`${rendered.status} ${decoded_path}`);
579
- writeFileSync(file, text);
580
- paths.push(normalize(decoded_path));
581
- } else if (response_type !== OK) {
582
- error({ status: rendered.status, path, referrer, referenceType: 'linked' });
583
- }
611
+ const file = output_filename(decoded, is_html);
612
+ const dest = `${out}/${file}`;
584
613
 
585
- for (const [dependency_path, result] of dependencies) {
586
- const { status, headers } = result.response;
614
+ if (written.has(file)) return;
615
+ written.add(file);
587
616
 
588
- const response_type = Math.floor(status / 100);
617
+ if (response_type === REDIRECT) {
618
+ const location = response.headers.get('location');
589
619
 
590
- const is_html = headers.get('content-type') === 'text/html';
620
+ if (location) {
621
+ mkdirp(dirname(dest));
591
622
 
592
- const file = `${out}${output_filename(dependency_path, is_html)}`;
593
- mkdirp(dirname(file));
623
+ log.warn(`${response.status} ${decoded} -> ${location}`);
594
624
 
595
625
  writeFileSync(
596
- file,
597
- result.body === null ? new Uint8Array(await result.response.arrayBuffer()) : result.body
626
+ dest,
627
+ `<meta http-equiv="refresh" content=${escape_html_attr(`0;url=${location}`)}>`
598
628
  );
599
- paths.push(dependency_path);
600
-
601
- if (response_type === OK) {
602
- log.info(`${status} ${dependency_path}`);
603
- } else {
604
- error({
605
- status,
606
- path: dependency_path,
607
- referrer: path,
608
- referenceType: 'fetched'
609
- });
610
- }
611
- }
612
629
 
613
- if (is_html && config.kit.prerender.crawl) {
614
- for (const href of crawl(text)) {
615
- if (href.startsWith('data:') || href.startsWith('#')) continue;
616
-
617
- const resolved = resolve(path, href);
618
- if (!is_root_relative(resolved)) continue;
630
+ let resolved = resolve(encoded, location);
631
+ if (is_root_relative(resolved)) {
632
+ resolved = normalize_path(resolved, config.kit.trailingSlash);
633
+ enqueue(decoded, decodeURI(resolved), resolved);
634
+ }
619
635
 
620
- const parsed = new URL(resolved, 'http://localhost');
636
+ if (!prerendered.redirects.has(decoded)) {
637
+ prerendered.redirects.set(decoded, {
638
+ status: response.status,
639
+ location: resolved
640
+ });
621
641
 
622
- let pathname = decodeURI(parsed.pathname);
642
+ prerendered.paths.push(normalize_path(decoded, 'never'));
643
+ }
644
+ } else {
645
+ log.warn(`location header missing on redirect received from ${decoded}`);
646
+ }
623
647
 
624
- if (config.kit.paths.base) {
625
- if (!pathname.startsWith(config.kit.paths.base)) continue;
626
- pathname = pathname.slice(config.kit.paths.base.length) || '/';
627
- }
648
+ return;
649
+ }
628
650
 
629
- const file = pathname.slice(1);
630
- if (files.has(file)) continue;
651
+ if (response.status === 200) {
652
+ mkdirp(dirname(dest));
631
653
 
632
- if (parsed.search) ;
654
+ log.info(`${response.status} ${decoded}`);
655
+ writeFileSync(dest, body);
633
656
 
634
- enqueue(pathname, path);
635
- }
657
+ if (is_html) {
658
+ prerendered.pages.set(decoded, {
659
+ file
660
+ });
661
+ } else {
662
+ prerendered.assets.set(decoded, {
663
+ type
664
+ });
636
665
  }
666
+
667
+ prerendered.paths.push(normalize_path(decoded, 'never'));
668
+ } else if (response_type !== OK) {
669
+ error({ status: response.status, path: decoded, referrer, referenceType });
637
670
  }
638
671
  }
639
672
 
@@ -641,10 +674,10 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
641
674
  for (const entry of config.kit.prerender.entries) {
642
675
  if (entry === '*') {
643
676
  for (const entry of build_data.entries) {
644
- enqueue(entry, null);
677
+ enqueue(null, normalize_path(config.kit.paths.base + entry, config.kit.trailingSlash)); // TODO can we pre-normalize these?
645
678
  }
646
679
  } else {
647
- enqueue(entry, null);
680
+ enqueue(null, normalize_path(config.kit.paths.base + entry, config.kit.trailingSlash));
648
681
  }
649
682
  }
650
683
 
@@ -665,9 +698,7 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
665
698
  writeFileSync(file, await rendered.text());
666
699
  }
667
700
 
668
- return {
669
- paths
670
- };
701
+ return prerendered;
671
702
  }
672
703
 
673
704
  /**
@@ -681,11 +712,14 @@ async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
681
712
  */
682
713
  function create_builder({ cwd, config, build_data, log }) {
683
714
  /** @type {Set<string>} */
684
- const prerendered_paths = new Set();
715
+ let prerendered_paths;
716
+
685
717
  let generated_manifest = false;
686
718
 
687
719
  /** @param {import('types/internal').RouteData} route */
688
720
  function not_prerendered(route) {
721
+ if (!prerendered_paths) return true;
722
+
689
723
  if (route.type === 'page' && route.path) {
690
724
  return !prerendered_paths.has(route.path);
691
725
  }
@@ -700,6 +734,7 @@ function create_builder({ cwd, config, build_data, log }) {
700
734
  copy,
701
735
 
702
736
  appDir: config.kit.appDir,
737
+ trailingSlash: config.kit.trailingSlash,
703
738
 
704
739
  createEntries(fn) {
705
740
  generated_manifest = true;
@@ -829,10 +864,7 @@ function create_builder({ cwd, config, build_data, log }) {
829
864
  log
830
865
  });
831
866
 
832
- prerendered.paths.forEach((path) => {
833
- prerendered_paths.add(path);
834
- prerendered_paths.add(path + '/');
835
- });
867
+ prerendered_paths = new Set(prerendered.paths);
836
868
 
837
869
  return prerendered;
838
870
  }
package/dist/cli.js CHANGED
@@ -619,7 +619,10 @@ const options = object(
619
619
  prerender: object({
620
620
  concurrency: number(1),
621
621
  crawl: boolean(true),
622
- createIndexFiles: boolean(true),
622
+ createIndexFiles: error(
623
+ (keypath) =>
624
+ `${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/configuration#trailingslash`
625
+ ),
623
626
  enabled: boolean(true),
624
627
  entries: validate(['*'], (input, keypath) => {
625
628
  if (!Array.isArray(input) || !input.every((page) => typeof page === 'string')) {
@@ -675,7 +678,7 @@ const options = object(
675
678
 
676
679
  serviceWorker: object({
677
680
  register: boolean(true),
678
- files: fun((filename) => !/\.DS_STORE/.test(filename))
681
+ files: fun((filename) => !/\.DS_Store/.test(filename))
679
682
  }),
680
683
 
681
684
  // TODO remove this for 1.0
@@ -995,7 +998,7 @@ async function launch(port, https) {
995
998
  exec(`${cmd} ${https ? 'https' : 'http'}://localhost:${port}`);
996
999
  }
997
1000
 
998
- const prog = sade('svelte-kit').version('1.0.0-next.264');
1001
+ const prog = sade('svelte-kit').version('1.0.0-next.268');
999
1002
 
1000
1003
  prog
1001
1004
  .command('dev')
@@ -1153,7 +1156,7 @@ async function check_port(port) {
1153
1156
  function welcome({ port, host, https, open, loose, allow, cwd }) {
1154
1157
  if (open) launch(port, https);
1155
1158
 
1156
- console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.264'}\n`));
1159
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.268'}\n`));
1157
1160
 
1158
1161
  const protocol = https ? 'https:' : 'http:';
1159
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.264",
3
+ "version": "1.0.0-next.268",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -84,8 +84,9 @@
84
84
  "check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",
85
85
  "test": "npm run test:unit && npm run test:packaging && npm run test:prerendering && npm run test:integration",
86
86
  "test:unit": "uvu src \"(spec\\.js|test[\\\\/]index\\.js)\" -i packaging",
87
- "test:prerendering": "pnpm test:prerendering:basics",
87
+ "test:prerendering": "pnpm test:prerendering:basics && pnpm test:prerendering:options",
88
88
  "test:prerendering:basics": "cd test/prerendering/basics && pnpm test",
89
+ "test:prerendering:options": "cd test/prerendering/options && pnpm test",
89
90
  "test:packaging": "uvu src/packaging \"(spec\\.js|test[\\\\/]index\\.js)\"",
90
91
  "test:integration": "pnpm test:integration:amp && pnpm test:integration:basics && pnpm test:integration:options && pnpm test:integration:options-2",
91
92
  "test:integration:amp": "cd test/apps/amp && pnpm test",
package/types/config.d.ts CHANGED
@@ -35,12 +35,39 @@ 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;
41
67
  mkdirp(dir: string): void;
42
68
 
43
69
  appDir: string;
70
+ trailingSlash: 'always' | 'never' | 'ignore';
44
71
 
45
72
  /**
46
73
  * Create entry points that map to individual functions
@@ -86,9 +113,7 @@ export interface Builder {
86
113
  }
87
114
  ): string[];
88
115
 
89
- prerender(options: { all?: boolean; dest: string; fallback?: string }): Promise<{
90
- paths: string[];
91
- }>;
116
+ prerender(options: { all?: boolean; dest: string; fallback?: string }): Promise<Prerendered>;
92
117
  }
93
118
 
94
119
  export interface Adapter {
@@ -153,7 +178,6 @@ export interface Config {
153
178
  prerender?: {
154
179
  concurrency?: number;
155
180
  crawl?: boolean;
156
- createIndexFiles?: boolean;
157
181
  enabled?: boolean;
158
182
  entries?: string[];
159
183
  onError?: PrerenderOnErrorValue;
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';