@sveltejs/kit 1.0.0-next.273 → 1.0.0-next.277

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.
@@ -45,8 +45,8 @@ async function invalidate_(resource) {
45
45
  /**
46
46
  * @type {import('$app/navigation').prefetch}
47
47
  */
48
- function prefetch_(href) {
49
- return router.prefetch(new URL(href, get_base_uri(document)));
48
+ async function prefetch_(href) {
49
+ await router.prefetch(new URL(href, get_base_uri(document)));
50
50
  }
51
51
 
52
52
  /**
@@ -22,6 +22,21 @@ function normalize_path(path, trailing_slash) {
22
22
  return path;
23
23
  }
24
24
 
25
+ // We track the scroll position associated with each history entry in sessionStorage,
26
+ // rather than on history.state itself, because when navigation is driven by
27
+ // popstate it's too late to update the scroll position associated with the
28
+ // state we're navigating from
29
+ const SCROLL_KEY = 'sveltekit:scroll';
30
+
31
+ /** @typedef {{ x: number, y: number }} ScrollPosition */
32
+ /** @type {Record<number, ScrollPosition>} */
33
+ let scroll_positions = {};
34
+ try {
35
+ scroll_positions = JSON.parse(sessionStorage[SCROLL_KEY]);
36
+ } catch {
37
+ // do nothing
38
+ }
39
+
25
40
  function scroll_state() {
26
41
  return {
27
42
  x: pageXOffset,
@@ -71,6 +86,7 @@ class Router {
71
86
  renderer.router = this;
72
87
 
73
88
  this.enabled = true;
89
+ this.initialized = false;
74
90
 
75
91
  // make it possible to reset focus
76
92
  document.body.setAttribute('tabindex', '-1');
@@ -83,6 +99,11 @@ class Router {
83
99
  history.replaceState({ ...history.state, 'sveltekit:index': 0 }, '', location.href);
84
100
  }
85
101
 
102
+ // if we reload the page, or Cmd-Shift-T back to it,
103
+ // recover scroll position
104
+ const scroll = scroll_positions[this.current_history_index];
105
+ if (scroll) scrollTo(scroll.x, scroll.y);
106
+
86
107
  this.hash_navigating = false;
87
108
 
88
109
  this.callbacks = {
@@ -95,9 +116,7 @@ class Router {
95
116
  }
96
117
 
97
118
  init_listeners() {
98
- if ('scrollRestoration' in history) {
99
- history.scrollRestoration = 'manual';
100
- }
119
+ history.scrollRestoration = 'manual';
101
120
 
102
121
  // Adopted from Nuxt.js
103
122
  // Reset scrollRestoration to auto when leaving page, allowing page reload
@@ -122,28 +141,16 @@ class Router {
122
141
  }
123
142
  });
124
143
 
125
- // Setting scrollRestoration to manual again when returning to this page.
126
- addEventListener('load', () => {
127
- history.scrollRestoration = 'manual';
128
- });
129
-
130
- // There's no API to capture the scroll location right before the user
131
- // hits the back/forward button, so we listen for scroll events
144
+ addEventListener('visibilitychange', () => {
145
+ if (document.visibilityState === 'hidden') {
146
+ this.#update_scroll_positions();
132
147
 
133
- /** @type {NodeJS.Timeout} */
134
- let scroll_timer;
135
- addEventListener('scroll', () => {
136
- clearTimeout(scroll_timer);
137
- scroll_timer = setTimeout(() => {
138
- // Store the scroll location in the history
139
- // This will persist even if we navigate away from the site and come back
140
- const new_state = {
141
- ...(history.state || {}),
142
- 'sveltekit:scroll': scroll_state()
143
- };
144
- history.replaceState(new_state, document.title, window.location.href);
145
- // iOS scroll event intervals happen between 30-150ms, sometimes around 200ms
146
- }, 200);
148
+ try {
149
+ sessionStorage[SCROLL_KEY] = JSON.stringify(scroll_positions);
150
+ } catch {
151
+ // do nothing
152
+ }
153
+ }
147
154
  });
148
155
 
149
156
  /** @param {Event} event */
@@ -216,10 +223,9 @@ class Router {
216
223
  // clicking a hash link and those triggered by popstate
217
224
  this.hash_navigating = true;
218
225
 
219
- const info = this.parse(url);
220
- if (info) {
221
- return this.renderer.update(info, [], false);
222
- }
226
+ this.#update_scroll_positions();
227
+ this.renderer.update_page_store(new URL(url.href));
228
+
223
229
  return;
224
230
  }
225
231
 
@@ -245,7 +251,7 @@ class Router {
245
251
 
246
252
  this._navigate({
247
253
  url: new URL(location.href),
248
- scroll: event.state['sveltekit:scroll'],
254
+ scroll: scroll_positions[event.state['sveltekit:index']],
249
255
  keepfocus: false,
250
256
  chain: [],
251
257
  details: null,
@@ -272,6 +278,12 @@ class Router {
272
278
  );
273
279
  }
274
280
  });
281
+
282
+ this.initialized = true;
283
+ }
284
+
285
+ #update_scroll_positions() {
286
+ scroll_positions[this.current_history_index] = scroll_state();
275
287
  }
276
288
 
277
289
  /**
@@ -294,7 +306,8 @@ class Router {
294
306
  id: url.pathname + url.search,
295
307
  routes: this.routes.filter(([pattern]) => pattern.test(path)),
296
308
  url,
297
- path
309
+ path,
310
+ initial: !this.initialized
298
311
  };
299
312
  }
300
313
  }
@@ -344,7 +357,7 @@ class Router {
344
357
 
345
358
  /**
346
359
  * @param {URL} url
347
- * @returns {Promise<import('./types').NavigationResult>}
360
+ * @returns {Promise<import('./types').NavigationResult | undefined>}
348
361
  */
349
362
  async prefetch(url) {
350
363
  const info = this.parse(url);
@@ -421,6 +434,8 @@ class Router {
421
434
  });
422
435
  }
423
436
 
437
+ this.#update_scroll_positions();
438
+
424
439
  accepted();
425
440
 
426
441
  if (!this.navigating) {
@@ -690,7 +705,7 @@ class Renderer {
690
705
  /** @type {Map<string, import('./types').NavigationResult>} */
691
706
  this.cache = new Map();
692
707
 
693
- /** @type {{id: string | null, promise: Promise<import('./types').NavigationResult> | null}} */
708
+ /** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
694
709
  this.loading = {
695
710
  id: null,
696
711
  promise: null
@@ -736,11 +751,12 @@ class Renderer {
736
751
  * status: number;
737
752
  * error: Error;
738
753
  * nodes: Array<Promise<CSRComponent>>;
739
- * url: URL;
740
754
  * params: Record<string, string>;
741
755
  * }} selected
742
756
  */
743
- async start({ status, error, nodes, url, params }) {
757
+ async start({ status, error, nodes, params }) {
758
+ const url = new URL(location.href);
759
+
744
760
  /** @type {Array<import('./types').BranchNode | undefined>} */
745
761
  const branch = [];
746
762
 
@@ -752,9 +768,6 @@ class Renderer {
752
768
 
753
769
  let error_args;
754
770
 
755
- // url.hash is empty when coming from the server
756
- url.hash = window.location.hash;
757
-
758
771
  try {
759
772
  for (let i = 0; i < nodes.length; i += 1) {
760
773
  const is_leaf = i === nodes.length - 1;
@@ -780,6 +793,7 @@ class Renderer {
780
793
 
781
794
  if (props) {
782
795
  node.uses.dependencies.add(url.href);
796
+ node.uses.url = true;
783
797
  }
784
798
 
785
799
  branch.push(node);
@@ -858,6 +872,11 @@ class Renderer {
858
872
  const token = (this.token = {});
859
873
  let navigation_result = await this._get_navigation_result(info, no_cache);
860
874
 
875
+ if (!navigation_result) {
876
+ location.href = info.url.href;
877
+ return;
878
+ }
879
+
861
880
  // abort if user navigated during update
862
881
  if (token !== this.token) return;
863
882
 
@@ -937,6 +956,10 @@ class Renderer {
937
956
  this.autoscroll = true;
938
957
  this.updating = false;
939
958
 
959
+ if (navigation_result.props.page) {
960
+ this.page = navigation_result.props.page;
961
+ }
962
+
940
963
  if (!this.router) return;
941
964
 
942
965
  const leaf_node = navigation_result.state.branch[navigation_result.state.branch.length - 1];
@@ -949,7 +972,7 @@ class Renderer {
949
972
 
950
973
  /**
951
974
  * @param {import('./types').NavigationInfo} info
952
- * @returns {Promise<import('./types').NavigationResult>}
975
+ * @returns {Promise<import('./types').NavigationResult | undefined>}
953
976
  */
954
977
  load(info) {
955
978
  this.loading.promise = this._get_navigation_result(info, false);
@@ -974,6 +997,12 @@ class Renderer {
974
997
  return this.invalidating;
975
998
  }
976
999
 
1000
+ /** @param {URL} url */
1001
+ update_page_store(url) {
1002
+ this.stores.page.set({ ...this.page, url });
1003
+ this.stores.page.notify();
1004
+ }
1005
+
977
1006
  /** @param {import('./types').NavigationResult} result */
978
1007
  _init(result) {
979
1008
  this.current = result.state;
@@ -981,6 +1010,8 @@ class Renderer {
981
1010
  const style = document.querySelector('style[data-svelte]');
982
1011
  if (style) style.remove();
983
1012
 
1013
+ this.page = result.props.page;
1014
+
984
1015
  this.root = new this.Root({
985
1016
  target: this.target,
986
1017
  props: {
@@ -1001,7 +1032,7 @@ class Renderer {
1001
1032
  /**
1002
1033
  * @param {import('./types').NavigationInfo} info
1003
1034
  * @param {boolean} no_cache
1004
- * @returns {Promise<import('./types').NavigationResult>}
1035
+ * @returns {Promise<import('./types').NavigationResult | undefined>}
1005
1036
  */
1006
1037
  async _get_navigation_result(info, no_cache) {
1007
1038
  if (this.loading.id === info.id && this.loading.promise) {
@@ -1034,11 +1065,13 @@ class Renderer {
1034
1065
  if (result) return result;
1035
1066
  }
1036
1067
 
1037
- return await this._load_error({
1038
- status: 404,
1039
- error: new Error(`Not found: ${info.url.pathname}`),
1040
- url: info.url
1041
- });
1068
+ if (info.initial) {
1069
+ return await this._load_error({
1070
+ status: 404,
1071
+ error: new Error(`Not found: ${info.url.pathname}`),
1072
+ url: info.url
1073
+ });
1074
+ }
1042
1075
  }
1043
1076
 
1044
1077
  /**
@@ -1293,7 +1326,9 @@ class Renderer {
1293
1326
  /** @type {Record<string, any>} */
1294
1327
  let props = {};
1295
1328
 
1296
- if (has_shadow && i === a.length - 1) {
1329
+ const is_shadow_page = has_shadow && i === a.length - 1;
1330
+
1331
+ if (is_shadow_page) {
1297
1332
  const res = await fetch(
1298
1333
  `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
1299
1334
  {
@@ -1331,25 +1366,31 @@ class Renderer {
1331
1366
  });
1332
1367
  }
1333
1368
 
1334
- if (node && node.loaded) {
1335
- if (node.loaded.fallthrough) {
1336
- return;
1337
- }
1338
- if (node.loaded.error) {
1339
- status = node.loaded.status;
1340
- error = node.loaded.error;
1369
+ if (node) {
1370
+ if (is_shadow_page) {
1371
+ node.uses.url = true;
1341
1372
  }
1342
1373
 
1343
- if (node.loaded.redirect) {
1344
- return {
1345
- redirect: node.loaded.redirect,
1346
- props: {},
1347
- state: this.current
1348
- };
1349
- }
1374
+ if (node.loaded) {
1375
+ if (node.loaded.fallthrough) {
1376
+ return;
1377
+ }
1378
+ if (node.loaded.error) {
1379
+ status = node.loaded.status;
1380
+ error = node.loaded.error;
1381
+ }
1382
+
1383
+ if (node.loaded.redirect) {
1384
+ return {
1385
+ redirect: node.loaded.redirect,
1386
+ props: {},
1387
+ state: this.current
1388
+ };
1389
+ }
1350
1390
 
1351
- if (node.loaded.stuff) {
1352
- stuff_changed = true;
1391
+ if (node.loaded.stuff) {
1392
+ stuff_changed = true;
1393
+ }
1353
1394
  }
1354
1395
  }
1355
1396
  } else {
@@ -1483,7 +1524,6 @@ class Renderer {
1483
1524
  * status: number;
1484
1525
  * error: Error;
1485
1526
  * nodes: Array<Promise<import('types/internal').CSRComponent>>;
1486
- * url: URL;
1487
1527
  * params: Record<string, string>;
1488
1528
  * };
1489
1529
  * }} opts
@@ -1209,7 +1209,6 @@ async function render_response({
1209
1209
  .map(({ node }) => `import(${s(options.prefix + node.entry)})`)
1210
1210
  .join(',\n\t\t\t\t\t\t')}
1211
1211
  ],
1212
- url: new URL(${s(url.href)}),
1213
1212
  params: ${devalue(params)}
1214
1213
  }` : 'null'}
1215
1214
  });
@@ -1222,12 +1221,15 @@ async function render_response({
1222
1221
  `;
1223
1222
 
1224
1223
  if (options.amp) {
1224
+ // inline_style contains CSS files (i.e. `import './styles.css'`)
1225
+ // rendered.css contains the CSS from `<style>` tags in Svelte components
1226
+ const styles = `${inlined_style}\n${rendered.css.code}`;
1225
1227
  head += `
1226
1228
  <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style>
1227
1229
  <noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
1228
1230
  <script async src="https://cdn.ampproject.org/v0.js"></script>
1229
1231
 
1230
- <style amp-custom>${inlined_style}\n${rendered.css.code}</style>`;
1232
+ <style amp-custom>${styles}</style>`;
1231
1233
 
1232
1234
  if (options.service_worker) {
1233
1235
  head +=
@@ -1259,6 +1261,8 @@ async function render_response({
1259
1261
  }
1260
1262
 
1261
1263
  if (styles.has(dep)) {
1264
+ // don't load stylesheets that are already inlined
1265
+ // include them in disabled state so that Vite can detect them and doesn't try to add them
1262
1266
  attributes.push('disabled', 'media="(max-width: 0)"');
1263
1267
  }
1264
1268
 
@@ -108,6 +108,7 @@ async function create_plugin(config, cwd) {
108
108
  entry: url.endsWith('.svelte') ? url : url + '?import',
109
109
  css: [],
110
110
  js: [],
111
+ // in dev we inline all styles to avoid FOUC
111
112
  styles
112
113
  };
113
114
  };
@@ -422,6 +423,11 @@ async function dev({ cwd, port, host, https, config }) {
422
423
  plugins: [
423
424
  svelte({
424
425
  extensions: config.extensions,
426
+ // In AMP mode, we know that there are no conditional component imports. In that case, we
427
+ // don't need to include CSS for components that are imported but unused, so we can just
428
+ // include rendered CSS.
429
+ // This would also apply if hydrate and router are both false, but we don't know if one
430
+ // has been enabled at the page level, so we don't do anything there.
425
431
  emitCss: !config.kit.amp,
426
432
  compilerOptions: {
427
433
  hydratable: !!config.kit.browser.hydrate
@@ -211,6 +211,11 @@ async function build_client({
211
211
  plugins: [
212
212
  svelte({
213
213
  extensions: config.extensions,
214
+ // In AMP mode, we know that there are no conditional component imports. In that case, we
215
+ // don't need to include CSS for components that are imported but unused, so we can just
216
+ // include rendered CSS.
217
+ // This would also apply if hydrate and router are both false, but we don't know if one
218
+ // has been enabled at the page level, so we don't do anything there.
214
219
  emitCss: !config.kit.amp,
215
220
  compilerOptions: {
216
221
  hydratable: !!config.kit.browser.hydrate
@@ -13,6 +13,8 @@ function generate_manifest(
13
13
  routes = build_data.manifest_data.routes,
14
14
  format = 'esm'
15
15
  ) {
16
+ /** @typedef {{ index: number, path: string }} LookupEntry */
17
+ /** @type {Map<string, LookupEntry>} */
16
18
  const bundled_nodes = new Map();
17
19
 
18
20
  // 0 and 1 are special, they correspond to the root layout and root error nodes
@@ -52,6 +54,9 @@ function generate_manifest(
52
54
  assets.push(build_data.service_worker);
53
55
  }
54
56
 
57
+ /** @param {string} id */
58
+ const get_index = (id) => id && /** @type {LookupEntry} */ (bundled_nodes.get(id)).index;
59
+
55
60
  // prettier-ignore
56
61
  return `{
57
62
  appDir: ${s(build_data.app_dir)},
@@ -71,8 +76,8 @@ function generate_manifest(
71
76
  params: ${get_params(route.params)},
72
77
  path: ${route.path ? s(route.path) : null},
73
78
  shadow: ${route.shadow ? importer(`${relative_path}/${build_data.server.vite_manifest[route.shadow].file}`) : null},
74
- a: ${s(route.a.map(component => component && bundled_nodes.get(component).index))},
75
- b: ${s(route.b.map(component => component && bundled_nodes.get(component).index))}
79
+ a: ${s(route.a.map(get_index))},
80
+ b: ${s(route.b.map(get_index))}
76
81
  }`.replace(/^\t\t/gm, '');
77
82
  } else {
78
83
  if (!build_data.server.vite_manifest[route.file]) {
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.273');
1001
+ const prog = sade('svelte-kit').version('1.0.0-next.277');
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.273'}\n`));
1159
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.277'}\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.273",
3
+ "version": "1.0.0-next.277",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",