@sveltejs/kit 1.0.0-next.259 → 1.0.0-next.262

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.
@@ -189,8 +189,11 @@ class Router {
189
189
  // Ignore if <a> has a target
190
190
  if (a instanceof SVGAElement ? a.target.baseVal : a.target) return;
191
191
 
192
- // Check if new url only differs by hash
193
- if (url.href.split('#')[0] === location.href.split('#')[0]) {
192
+ // Check if new url only differs by hash and use the browser default behavior in that case
193
+ // This will ensure the `hashchange` event is fired
194
+ // Removing the hash does a full page navigation in the browser, so make sure a hash is present
195
+ const [base, hash] = url.href.split('#');
196
+ if (hash !== undefined && base === location.href.split('#')[0]) {
194
197
  // Call `pushState` to add url to history so going back works.
195
198
  // Also make a delay, otherwise the browser default behaviour would not kick in
196
199
  setTimeout(() => history.pushState({}, '', url.href));
@@ -731,15 +734,29 @@ class Renderer {
731
734
  for (let i = 0; i < nodes.length; i += 1) {
732
735
  const is_leaf = i === nodes.length - 1;
733
736
 
737
+ let props;
738
+
739
+ if (is_leaf) {
740
+ const serialized = document.querySelector('[data-type="svelte-props"]');
741
+ if (serialized) {
742
+ props = JSON.parse(/** @type {string} */ (serialized.textContent));
743
+ }
744
+ }
745
+
734
746
  const node = await this._load_node({
735
747
  module: await nodes[i],
736
748
  url,
737
749
  params,
738
750
  stuff,
739
751
  status: is_leaf ? status : undefined,
740
- error: is_leaf ? error : undefined
752
+ error: is_leaf ? error : undefined,
753
+ props
741
754
  });
742
755
 
756
+ if (props) {
757
+ node.uses.dependencies.add(url.href);
758
+ }
759
+
743
760
  branch.push(node);
744
761
 
745
762
  if (node && node.loaded) {
@@ -1092,10 +1109,11 @@ class Renderer {
1092
1109
  * url: URL;
1093
1110
  * params: Record<string, string>;
1094
1111
  * stuff: Record<string, any>;
1112
+ * props?: Record<string, any>;
1095
1113
  * }} options
1096
1114
  * @returns
1097
1115
  */
1098
- async _load_node({ status, error, module, url, params, stuff }) {
1116
+ async _load_node({ status, error, module, url, params, stuff, props }) {
1099
1117
  /** @type {import('./types').BranchNode} */
1100
1118
  const node = {
1101
1119
  module,
@@ -1104,12 +1122,17 @@ class Renderer {
1104
1122
  url: false,
1105
1123
  session: false,
1106
1124
  stuff: false,
1107
- dependencies: []
1125
+ dependencies: new Set()
1108
1126
  },
1109
1127
  loaded: null,
1110
1128
  stuff
1111
1129
  };
1112
1130
 
1131
+ if (props) {
1132
+ // shadow endpoint props means we need to mark this URL as a dependency of itself
1133
+ node.uses.dependencies.add(url.href);
1134
+ }
1135
+
1113
1136
  /** @type {Record<string, string>} */
1114
1137
  const uses_params = {};
1115
1138
  for (const key in params) {
@@ -1130,6 +1153,7 @@ class Renderer {
1130
1153
  /** @type {import('types/page').LoadInput | import('types/page').ErrorLoadInput} */
1131
1154
  const load_input = {
1132
1155
  params: uses_params,
1156
+ props: props || {},
1133
1157
  get url() {
1134
1158
  node.uses.url = true;
1135
1159
  return url;
@@ -1145,7 +1169,7 @@ class Renderer {
1145
1169
  fetch(resource, info) {
1146
1170
  const requested = typeof resource === 'string' ? resource : resource.url;
1147
1171
  const { href } = new URL(requested, url);
1148
- node.uses.dependencies.push(href);
1172
+ node.uses.dependencies.add(href);
1149
1173
 
1150
1174
  return started ? fetch(resource, info) : initial_fetch(resource, info);
1151
1175
  }
@@ -1173,6 +1197,8 @@ class Renderer {
1173
1197
 
1174
1198
  node.loaded = normalize(loaded);
1175
1199
  if (node.loaded.stuff) node.stuff = node.loaded.stuff;
1200
+ } else if (props) {
1201
+ node.loaded = normalize({ props });
1176
1202
  }
1177
1203
 
1178
1204
  return node;
@@ -1191,7 +1217,7 @@ class Renderer {
1191
1217
  if (cached) return cached;
1192
1218
  }
1193
1219
 
1194
- const [pattern, a, b, get_params] = route;
1220
+ const [pattern, a, b, get_params, has_shadow] = route;
1195
1221
  const params = get_params
1196
1222
  ? // the pattern is for the route which we've already matched to this path
1197
1223
  get_params(/** @type {RegExpExecArray} */ (pattern.exec(path)))
@@ -1235,16 +1261,50 @@ class Renderer {
1235
1261
  (changed.url && previous.uses.url) ||
1236
1262
  changed.params.some((param) => previous.uses.params.has(param)) ||
1237
1263
  (changed.session && previous.uses.session) ||
1238
- previous.uses.dependencies.some((dep) => this.invalid.has(dep)) ||
1264
+ Array.from(previous.uses.dependencies).some((dep) => this.invalid.has(dep)) ||
1239
1265
  (stuff_changed && previous.uses.stuff);
1240
1266
 
1241
1267
  if (changed_since_last_render) {
1242
- node = await this._load_node({
1243
- module,
1244
- url,
1245
- params,
1246
- stuff
1247
- });
1268
+ /** @type {Record<string, any>} */
1269
+ let props = {};
1270
+
1271
+ if (has_shadow && i === a.length - 1) {
1272
+ const res = await fetch(
1273
+ `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json`,
1274
+ {
1275
+ headers: {
1276
+ 'x-sveltekit-noredirect': 'true'
1277
+ }
1278
+ }
1279
+ );
1280
+
1281
+ if (res.ok) {
1282
+ const redirect = res.headers.get('x-sveltekit-location');
1283
+
1284
+ if (redirect) {
1285
+ return {
1286
+ redirect,
1287
+ props: {},
1288
+ state: this.current
1289
+ };
1290
+ }
1291
+
1292
+ props = await res.json();
1293
+ } else {
1294
+ status = res.status;
1295
+ error = new Error('Failed to load data');
1296
+ }
1297
+ }
1298
+
1299
+ if (!error) {
1300
+ node = await this._load_node({
1301
+ module,
1302
+ url,
1303
+ params,
1304
+ props,
1305
+ stuff
1306
+ });
1307
+ }
1248
1308
 
1249
1309
  if (node && node.loaded) {
1250
1310
  if (node.loaded.fallthrough) {
@@ -61,6 +61,24 @@ function decode_params(params) {
61
61
  return params;
62
62
  }
63
63
 
64
+ /** @param {any} body */
65
+ function is_pojo(body) {
66
+ if (typeof body !== 'object') return false;
67
+
68
+ if (body) {
69
+ if (body instanceof Uint8Array) return false;
70
+
71
+ // body could be a node Readable, but we don't want to import
72
+ // node built-ins, so we use duck typing
73
+ if (body._readableState && body._writableState && body._events) return false;
74
+
75
+ // similarly, it could be a web ReadableStream
76
+ if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) return false;
77
+ }
78
+
79
+ return true;
80
+ }
81
+
64
82
  /** @param {string} body */
65
83
  function error(body) {
66
84
  return new Response(body, {
@@ -95,13 +113,10 @@ function is_text(content_type) {
95
113
 
96
114
  /**
97
115
  * @param {import('types/hooks').RequestEvent} event
98
- * @param {import('types/internal').SSREndpoint} route
99
- * @param {RegExpExecArray} match
116
+ * @param {{ [method: string]: import('types/endpoint').RequestHandler }} mod
100
117
  * @returns {Promise<Response | undefined>}
101
118
  */
102
- async function render_endpoint(event, route, match) {
103
- const mod = await route.load();
104
-
119
+ async function render_endpoint(event, mod) {
105
120
  /** @type {import('types/endpoint').RequestHandler} */
106
121
  const handler = mod[event.request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word
107
122
 
@@ -109,11 +124,6 @@ async function render_endpoint(event, route, match) {
109
124
  return;
110
125
  }
111
126
 
112
- // we're mutating `request` so that we don't have to do { ...request, params }
113
- // on the next line, since that breaks the getters that replace path, query and
114
- // origin. We could revert that once we remove the getters
115
- event.params = route.params ? decode_params(route.params(match)) : {};
116
-
117
127
  const response = await handler(event);
118
128
  const preface = `Invalid response from route ${event.url.pathname}`;
119
129
 
@@ -163,24 +173,6 @@ async function render_endpoint(event, route, match) {
163
173
  });
164
174
  }
165
175
 
166
- /** @param {any} body */
167
- function is_pojo(body) {
168
- if (typeof body !== 'object') return false;
169
-
170
- if (body) {
171
- if (body instanceof Uint8Array) return false;
172
-
173
- // body could be a node Readable, but we don't want to import
174
- // node built-ins, so we use duck typing
175
- if (body._readableState && body._writableState && body._events) return false;
176
-
177
- // similarly, it could be a web ReadableStream
178
- if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) return false;
179
- }
180
-
181
- return true;
182
- }
183
-
184
176
  var chars$1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
185
177
  var unsafeChars = /[<>\b\f\n\r\t\0\u2028\u2029]/g;
186
178
  var reserved = /^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/;
@@ -482,8 +474,7 @@ function coalesce_to_error(err) {
482
474
  }
483
475
 
484
476
  /** @type {Record<string, string>} */
485
- const escape_json_string_in_html_dict = {
486
- '"': '\\"',
477
+ const escape_json_in_html_dict = {
487
478
  '<': '\\u003C',
488
479
  '>': '\\u003E',
489
480
  '/': '\\u002F',
@@ -498,7 +489,24 @@ const escape_json_string_in_html_dict = {
498
489
  '\u2029': '\\u2029'
499
490
  };
500
491
 
501
- /** @param {string} str */
492
+ /** @type {Record<string, string>} */
493
+ const escape_json_string_in_html_dict = {
494
+ '"': '\\"',
495
+ ...escape_json_in_html_dict
496
+ };
497
+
498
+ /**
499
+ * Escape a stringified JSON object that's going to be embedded in a `<script>` tag
500
+ * @param {string} str
501
+ */
502
+ function escape_json_in_html(str) {
503
+ return escape(str, escape_json_in_html_dict, (code) => `\\u${code.toString(16).toUpperCase()}`);
504
+ }
505
+
506
+ /**
507
+ * Escape a string JSON value to be embedded into a `<script>` tag
508
+ * @param {string} str
509
+ */
502
510
  function escape_json_string_in_html(str) {
503
511
  return escape(
504
512
  str,
@@ -1076,6 +1084,8 @@ async function render_response({
1076
1084
  /** @type {Array<{ url: string, body: string, json: string }>} */
1077
1085
  const serialized_data = [];
1078
1086
 
1087
+ let shadow_props;
1088
+
1079
1089
  let rendered;
1080
1090
 
1081
1091
  let is_private = false;
@@ -1086,13 +1096,14 @@ async function render_response({
1086
1096
  }
1087
1097
 
1088
1098
  if (ssr) {
1089
- branch.forEach(({ node, loaded, fetched, uses_credentials }) => {
1099
+ branch.forEach(({ node, props, loaded, fetched, uses_credentials }) => {
1090
1100
  if (node.css) node.css.forEach((url) => stylesheets.add(url));
1091
1101
  if (node.js) node.js.forEach((url) => modulepreloads.add(url));
1092
1102
  if (node.styles) Object.entries(node.styles).forEach(([k, v]) => styles.set(k, v));
1093
1103
 
1094
1104
  // TODO probably better if `fetched` wasn't populated unless `hydrate`
1095
1105
  if (fetched && page_config.hydrate) serialized_data.push(...fetched);
1106
+ if (props) shadow_props = props;
1096
1107
 
1097
1108
  if (uses_credentials) is_private = true;
1098
1109
 
@@ -1271,6 +1282,11 @@ async function render_response({
1271
1282
  return `<script ${attributes}>${json}</script>`;
1272
1283
  })
1273
1284
  .join('\n\t');
1285
+
1286
+ if (shadow_props) {
1287
+ // prettier-ignore
1288
+ body += `<script type="application/json" data-type="svelte-props">${escape_json_in_html(s(shadow_props))}</script>`;
1289
+ }
1274
1290
  }
1275
1291
 
1276
1292
  if (options.service_worker) {
@@ -1476,6 +1492,7 @@ function is_root_relative(path) {
1476
1492
  * $session: any;
1477
1493
  * stuff: Record<string, any>;
1478
1494
  * is_error: boolean;
1495
+ * is_leaf: boolean;
1479
1496
  * status?: number;
1480
1497
  * error?: Error;
1481
1498
  * }} opts
@@ -1492,6 +1509,7 @@ async function load_node({
1492
1509
  $session,
1493
1510
  stuff,
1494
1511
  is_error,
1512
+ is_leaf,
1495
1513
  status,
1496
1514
  error
1497
1515
  }) {
@@ -1513,13 +1531,40 @@ async function load_node({
1513
1531
  */
1514
1532
  let set_cookie_headers = [];
1515
1533
 
1534
+ /** @type {import('types/helper').Either<import('types/endpoint').Fallthrough, import('types/page').LoadOutput>} */
1516
1535
  let loaded;
1517
1536
 
1518
- if (module.load) {
1537
+ /** @type {import('types/endpoint').ShadowData} */
1538
+ const shadow = is_leaf
1539
+ ? await load_shadow_data(
1540
+ /** @type {import('types/internal').SSRPage} */ (route),
1541
+ event,
1542
+ !!state.prerender
1543
+ )
1544
+ : {};
1545
+
1546
+ if (shadow.fallthrough) return;
1547
+
1548
+ if (shadow.cookies) {
1549
+ set_cookie_headers.push(...shadow.cookies);
1550
+ }
1551
+
1552
+ if (shadow.error) {
1553
+ loaded = {
1554
+ status: shadow.status,
1555
+ error: shadow.error
1556
+ };
1557
+ } else if (shadow.redirect) {
1558
+ loaded = {
1559
+ status: shadow.status,
1560
+ redirect: shadow.redirect
1561
+ };
1562
+ } else if (module.load) {
1519
1563
  /** @type {import('types/page').LoadInput | import('types/page').ErrorLoadInput} */
1520
1564
  const load_input = {
1521
1565
  url: state.prerender ? create_prerendering_url_proxy(url) : url,
1522
1566
  params,
1567
+ props: shadow.body || {},
1523
1568
  get session() {
1524
1569
  uses_credentials = true;
1525
1570
  return $session;
@@ -1751,6 +1796,10 @@ async function load_node({
1751
1796
  if (!loaded) {
1752
1797
  throw new Error(`load function must return a value${options.dev ? ` (${node.entry})` : ''}`);
1753
1798
  }
1799
+ } else if (shadow.body) {
1800
+ loaded = {
1801
+ props: shadow.body
1802
+ };
1754
1803
  } else {
1755
1804
  loaded = {};
1756
1805
  }
@@ -1759,8 +1808,21 @@ async function load_node({
1759
1808
  return;
1760
1809
  }
1761
1810
 
1811
+ // generate __data.json files when prerendering
1812
+ if (shadow.body && state.prerender) {
1813
+ const pathname = `${event.url.pathname}/__data.json`;
1814
+
1815
+ const dependency = {
1816
+ response: new Response(undefined),
1817
+ body: JSON.stringify(shadow.body)
1818
+ };
1819
+
1820
+ state.prerender.dependencies.set(pathname, dependency);
1821
+ }
1822
+
1762
1823
  return {
1763
1824
  node,
1825
+ props: shadow.body,
1764
1826
  loaded: normalize(loaded),
1765
1827
  stuff: loaded.stuff || stuff,
1766
1828
  fetched,
@@ -1769,6 +1831,127 @@ async function load_node({
1769
1831
  };
1770
1832
  }
1771
1833
 
1834
+ /**
1835
+ *
1836
+ * @param {import('types/internal').SSRPage} route
1837
+ * @param {import('types/hooks').RequestEvent} event
1838
+ * @param {boolean} prerender
1839
+ * @returns {Promise<import('types/endpoint').ShadowData>}
1840
+ */
1841
+ async function load_shadow_data(route, event, prerender) {
1842
+ if (!route.shadow) return {};
1843
+
1844
+ try {
1845
+ const mod = await route.shadow();
1846
+
1847
+ if (prerender && (mod.post || mod.put || mod.del || mod.patch)) {
1848
+ throw new Error('Cannot prerender pages that have shadow endpoints with mutative methods');
1849
+ }
1850
+
1851
+ const method = event.request.method.toLowerCase().replace('delete', 'del');
1852
+ const handler = mod[method];
1853
+
1854
+ if (!handler) {
1855
+ return {
1856
+ status: 405,
1857
+ error: new Error(`${method} method not allowed`)
1858
+ };
1859
+ }
1860
+
1861
+ /** @type {import('types/endpoint').ShadowData} */
1862
+ const data = {
1863
+ status: 200,
1864
+ cookies: [],
1865
+ body: {}
1866
+ };
1867
+
1868
+ if (method !== 'get') {
1869
+ const result = await handler(event);
1870
+
1871
+ if (result.fallthrough) return result;
1872
+
1873
+ const { status = 200, headers = {}, body = {} } = result;
1874
+
1875
+ validate_shadow_output(headers, body);
1876
+
1877
+ if (headers['set-cookie']) {
1878
+ /** @type {string[]} */ (data.cookies).push(...headers['set-cookie']);
1879
+ }
1880
+
1881
+ // Redirects are respected...
1882
+ if (status >= 300 && status < 400) {
1883
+ return {
1884
+ status,
1885
+ redirect: /** @type {string} */ (
1886
+ headers instanceof Headers ? headers.get('location') : headers.location
1887
+ )
1888
+ };
1889
+ }
1890
+
1891
+ // ...but 4xx and 5xx status codes _don't_ result in the error page
1892
+ // rendering for non-GET requests — instead, we allow the page
1893
+ // to render with any validation errors etc that were returned
1894
+ data.status = status;
1895
+ data.body = body;
1896
+ }
1897
+
1898
+ if (mod.get) {
1899
+ const result = await mod.get.call(null, event);
1900
+
1901
+ if (result.fallthrough) return result;
1902
+
1903
+ const { status = 200, headers = {}, body = {} } = result;
1904
+
1905
+ validate_shadow_output(headers, body);
1906
+
1907
+ if (headers['set-cookie']) {
1908
+ /** @type {string[]} */ (data.cookies).push(...headers['set-cookie']);
1909
+ }
1910
+
1911
+ if (status >= 400) {
1912
+ return {
1913
+ status,
1914
+ error: new Error('Failed to load data')
1915
+ };
1916
+ }
1917
+
1918
+ if (status >= 300) {
1919
+ return {
1920
+ status,
1921
+ redirect: /** @type {string} */ (
1922
+ headers instanceof Headers ? headers.get('location') : headers.location
1923
+ )
1924
+ };
1925
+ }
1926
+
1927
+ data.body = { ...body, ...data.body };
1928
+ }
1929
+
1930
+ return data;
1931
+ } catch (e) {
1932
+ return {
1933
+ status: 500,
1934
+ error: coalesce_to_error(e)
1935
+ };
1936
+ }
1937
+ }
1938
+
1939
+ /**
1940
+ * @param {Headers | Partial<import('types/helper').ResponseHeaders>} headers
1941
+ * @param {import('types/helper').JSONValue} body
1942
+ */
1943
+ function validate_shadow_output(headers, body) {
1944
+ if (headers instanceof Headers && headers.has('set-cookie')) {
1945
+ throw new Error(
1946
+ 'Shadow endpoint request handler cannot use Headers interface with Set-Cookie headers'
1947
+ );
1948
+ }
1949
+
1950
+ if (!is_pojo(body)) {
1951
+ throw new Error('Body returned from shadow endpoint request handler must be a plain object');
1952
+ }
1953
+ }
1954
+
1772
1955
  /**
1773
1956
  * @typedef {import('./types.js').Loaded} Loaded
1774
1957
  * @typedef {import('types/internal').SSROptions} SSROptions
@@ -1805,7 +1988,8 @@ async function respond_with_error({ event, options, state, $session, status, err
1805
1988
  node: default_layout,
1806
1989
  $session,
1807
1990
  stuff: {},
1808
- is_error: false
1991
+ is_error: false,
1992
+ is_leaf: false
1809
1993
  })
1810
1994
  );
1811
1995
 
@@ -1821,6 +2005,7 @@ async function respond_with_error({ event, options, state, $session, status, err
1821
2005
  $session,
1822
2006
  stuff: layout_loaded ? layout_loaded.stuff : {},
1823
2007
  is_error: true,
2008
+ is_leaf: false,
1824
2009
  status,
1825
2010
  error
1826
2011
  })
@@ -1953,7 +2138,8 @@ async function respond$1(opts) {
1953
2138
  url: event.url,
1954
2139
  node,
1955
2140
  stuff,
1956
- is_error: false
2141
+ is_error: false,
2142
+ is_leaf: i === nodes.length - 1
1957
2143
  });
1958
2144
 
1959
2145
  if (!loaded) return;
@@ -2008,6 +2194,7 @@ async function respond$1(opts) {
2008
2194
  node: error_node,
2009
2195
  stuff: node_loaded.stuff,
2010
2196
  is_error: true,
2197
+ is_leaf: false,
2011
2198
  status,
2012
2199
  error
2013
2200
  })
@@ -2121,13 +2308,12 @@ function with_cookies(response, set_cookie_headers) {
2121
2308
  /**
2122
2309
  * @param {import('types/hooks').RequestEvent} event
2123
2310
  * @param {import('types/internal').SSRPage} route
2124
- * @param {RegExpExecArray} match
2125
2311
  * @param {import('types/internal').SSROptions} options
2126
2312
  * @param {import('types/internal').SSRState} state
2127
2313
  * @param {boolean} ssr
2128
2314
  * @returns {Promise<Response | undefined>}
2129
2315
  */
2130
- async function render_page(event, route, match, options, state, ssr) {
2316
+ async function render_page(event, route, options, state, ssr) {
2131
2317
  if (state.initiator === route) {
2132
2318
  // infinite request cycle detected
2133
2319
  return new Response(`Not found: ${event.url.pathname}`, {
@@ -2135,7 +2321,16 @@ async function render_page(event, route, match, options, state, ssr) {
2135
2321
  });
2136
2322
  }
2137
2323
 
2138
- const params = route.params ? decode_params(route.params(match)) : {};
2324
+ if (route.shadow) {
2325
+ const type = negotiate(event.request.headers.get('accept') || 'text/html', [
2326
+ 'text/html',
2327
+ 'application/json'
2328
+ ]);
2329
+
2330
+ if (type === 'application/json') {
2331
+ return render_endpoint(event, await route.shadow());
2332
+ }
2333
+ }
2139
2334
 
2140
2335
  const $session = await options.hooks.getSession(event);
2141
2336
 
@@ -2145,7 +2340,7 @@ async function render_page(event, route, match, options, state, ssr) {
2145
2340
  state,
2146
2341
  $session,
2147
2342
  route,
2148
- params,
2343
+ params: event.params, // TODO this is redundant
2149
2344
  ssr
2150
2345
  });
2151
2346
 
@@ -2164,6 +2359,60 @@ async function render_page(event, route, match, options, state, ssr) {
2164
2359
  }
2165
2360
  }
2166
2361
 
2362
+ /**
2363
+ * @param {string} accept
2364
+ * @param {string[]} types
2365
+ */
2366
+ function negotiate(accept, types) {
2367
+ const parts = accept
2368
+ .split(',')
2369
+ .map((str, i) => {
2370
+ const match = /([^/]+)\/([^;]+)(?:;q=([0-9.]+))?/.exec(str);
2371
+ if (match) {
2372
+ const [, type, subtype, q = '1'] = match;
2373
+ return { type, subtype, q: +q, i };
2374
+ }
2375
+
2376
+ throw new Error(`Invalid Accept header: ${accept}`);
2377
+ })
2378
+ .sort((a, b) => {
2379
+ if (a.q !== b.q) {
2380
+ return b.q - a.q;
2381
+ }
2382
+
2383
+ if ((a.subtype === '*') !== (b.subtype === '*')) {
2384
+ return a.subtype === '*' ? 1 : -1;
2385
+ }
2386
+
2387
+ if ((a.type === '*') !== (b.type === '*')) {
2388
+ return a.type === '*' ? 1 : -1;
2389
+ }
2390
+
2391
+ return a.i - b.i;
2392
+ });
2393
+
2394
+ let accepted;
2395
+ let min_priority = Infinity;
2396
+
2397
+ for (const mimetype of types) {
2398
+ const [type, subtype] = mimetype.split('/');
2399
+ const priority = parts.findIndex(
2400
+ (part) =>
2401
+ (part.type === type || part.type === '*') &&
2402
+ (part.subtype === subtype || part.subtype === '*')
2403
+ );
2404
+
2405
+ if (priority !== -1 && priority < min_priority) {
2406
+ accepted = mimetype;
2407
+ min_priority = priority;
2408
+ }
2409
+ }
2410
+
2411
+ return accepted;
2412
+ }
2413
+
2414
+ const DATA_SUFFIX = '/__data.json';
2415
+
2167
2416
  /** @type {import('types/internal').Respond} */
2168
2417
  async function respond(request, options, state = {}) {
2169
2418
  const url = new URL(request.url);
@@ -2290,14 +2539,46 @@ async function respond(request, options, state = {}) {
2290
2539
  decoded = decoded.slice(options.paths.base.length) || '/';
2291
2540
  }
2292
2541
 
2542
+ const is_data_request = decoded.endsWith(DATA_SUFFIX);
2543
+ if (is_data_request) decoded = decoded.slice(0, -DATA_SUFFIX.length) || '/';
2544
+
2293
2545
  for (const route of options.manifest._.routes) {
2294
2546
  const match = route.pattern.exec(decoded);
2295
2547
  if (!match) continue;
2296
2548
 
2297
- const response =
2298
- route.type === 'endpoint'
2299
- ? await render_endpoint(event, route, match)
2300
- : await render_page(event, route, match, options, state, ssr);
2549
+ event.params = route.params ? decode_params(route.params(match)) : {};
2550
+
2551
+ /** @type {Response | undefined} */
2552
+ let response;
2553
+
2554
+ if (is_data_request && route.type === 'page' && route.shadow) {
2555
+ response = await render_endpoint(event, await route.shadow());
2556
+
2557
+ // since redirects are opaque to the browser, we need to repackage
2558
+ // 3xx responses as 200s with a custom header
2559
+ if (
2560
+ response &&
2561
+ response.status >= 300 &&
2562
+ response.status < 400 &&
2563
+ request.headers.get('x-sveltekit-noredirect') === 'true'
2564
+ ) {
2565
+ const location = response.headers.get('location');
2566
+
2567
+ if (location) {
2568
+ response = new Response(undefined, {
2569
+ status: 204,
2570
+ headers: {
2571
+ 'x-sveltekit-location': location
2572
+ }
2573
+ });
2574
+ }
2575
+ }
2576
+ } else {
2577
+ response =
2578
+ route.type === 'endpoint'
2579
+ ? await render_endpoint(event, await route.load())
2580
+ : await render_page(event, route, options, state, ssr);
2581
+ }
2301
2582
 
2302
2583
  if (response) {
2303
2584
  // respond with 304 if etag matches
@@ -118,6 +118,12 @@ async function create_plugin(config, cwd) {
118
118
  type: 'page',
119
119
  pattern: route.pattern,
120
120
  params: get_params(route.params),
121
+ shadow: route.shadow
122
+ ? async () => {
123
+ const url = path__default.resolve(cwd, /** @type {string} */ (route.shadow));
124
+ return await vite.ssrLoadModule(url);
125
+ }
126
+ : null,
121
127
  a: route.a.map((id) => manifest_data.components.indexOf(id)),
122
128
  b: route.b.map((id) => manifest_data.components.indexOf(id))
123
129
  };
@@ -165,7 +165,10 @@ function generate_client_manifest(manifest_data, base) {
165
165
  '})';
166
166
 
167
167
  const tuple = [route.pattern, get_indices(route.a), get_indices(route.b)];
168
- if (params) tuple.push(params);
168
+
169
+ // optional items
170
+ if (params || route.shadow) tuple.push(params || 'null');
171
+ if (route.shadow) tuple.push('1');
169
172
 
170
173
  return `// ${route.a[route.a.length - 1]}\n\t\t[${tuple.join(', ')}]`;
171
174
  }
@@ -382,6 +385,7 @@ var mime = new Mime(standard, other);
382
385
  * }} Part
383
386
  * @typedef {{
384
387
  * basename: string;
388
+ * name: string;
385
389
  * ext: string;
386
390
  * parts: Part[],
387
391
  * file: string;
@@ -427,12 +431,13 @@ function create_manifest_data({
427
431
 
428
432
  /**
429
433
  * @param {string} dir
434
+ * @param {string[]} parent_key
430
435
  * @param {Part[][]} parent_segments
431
436
  * @param {string[]} parent_params
432
437
  * @param {Array<string|undefined>} layout_stack // accumulated __layout.svelte components
433
438
  * @param {Array<string|undefined>} error_stack // accumulated __error.svelte components
434
439
  */
435
- function walk(dir, parent_segments, parent_params, layout_stack, error_stack) {
440
+ function walk(dir, parent_key, parent_segments, parent_params, layout_stack, error_stack) {
436
441
  /** @type {Item[]} */
437
442
  let items = [];
438
443
  fs__default.readdirSync(dir).forEach((basename) => {
@@ -482,6 +487,7 @@ function create_manifest_data({
482
487
 
483
488
  items.push({
484
489
  basename,
490
+ name,
485
491
  ext,
486
492
  parts,
487
493
  file,
@@ -494,6 +500,7 @@ function create_manifest_data({
494
500
  items = items.sort(comparator);
495
501
 
496
502
  items.forEach((item) => {
503
+ const key = parent_key.slice();
497
504
  const segments = parent_segments.slice();
498
505
 
499
506
  if (item.is_index) {
@@ -517,11 +524,13 @@ function create_manifest_data({
517
524
  }
518
525
 
519
526
  segments[segments.length - 1] = last_segment;
527
+ key[key.length - 1] += item.route_suffix;
520
528
  } else {
521
529
  segments.push(item.parts);
522
530
  }
523
531
  }
524
532
  } else {
533
+ key.push(item.name);
525
534
  segments.push(item.parts);
526
535
  }
527
536
 
@@ -555,6 +564,7 @@ function create_manifest_data({
555
564
 
556
565
  walk(
557
566
  path__default.join(dir, item.basename),
567
+ key,
558
568
  segments,
559
569
  params,
560
570
  layout_reset ? [layout_reset] : layout_stack.concat(layout),
@@ -589,10 +599,12 @@ function create_manifest_data({
589
599
 
590
600
  routes.push({
591
601
  type: 'page',
602
+ key: key.join('/'),
592
603
  segments: simple_segments,
593
604
  pattern,
594
605
  params,
595
606
  path,
607
+ shadow: null,
596
608
  a: /** @type {string[]} */ (concatenated),
597
609
  b: /** @type {string[]} */ (errors)
598
610
  });
@@ -601,6 +613,7 @@ function create_manifest_data({
601
613
 
602
614
  routes.push({
603
615
  type: 'endpoint',
616
+ key: key.join('/'),
604
617
  segments: simple_segments,
605
618
  pattern,
606
619
  file: item.file,
@@ -617,7 +630,24 @@ function create_manifest_data({
617
630
 
618
631
  components.push(layout, error);
619
632
 
620
- walk(config.kit.files.routes, [], [], [layout], [error]);
633
+ walk(config.kit.files.routes, [], [], [], [layout], [error]);
634
+
635
+ // merge matching page/endpoint pairs into shadowed pages
636
+ let i = routes.length;
637
+ while (i--) {
638
+ const route = routes[i];
639
+ const prev = routes[i - 1];
640
+
641
+ if (prev && prev.key === route.key) {
642
+ if (prev.type !== 'endpoint' || route.type !== 'page') {
643
+ const relative = path__default.relative(cwd, path__default.resolve(config.kit.files.routes, prev.key));
644
+ throw new Error(`Duplicate route files: ${relative}`);
645
+ }
646
+
647
+ route.shadow = prev.file;
648
+ routes.splice(--i, 1);
649
+ }
650
+ }
621
651
 
622
652
  const assets = fs__default.existsSync(config.kit.files.assets)
623
653
  ? list_files({ config, dir: config.kit.files.assets, path: '' })
@@ -383,8 +383,10 @@ async function build_server(
383
383
 
384
384
  // add entry points for every endpoint...
385
385
  manifest_data.routes.forEach((route) => {
386
- if (route.type === 'endpoint') {
387
- const resolved = path__default.resolve(cwd, route.file);
386
+ const file = route.type === 'endpoint' ? route.file : route.shadow;
387
+
388
+ if (file) {
389
+ const resolved = path__default.resolve(cwd, file);
388
390
  const relative = path__default.relative(config.kit.files.routes, resolved);
389
391
  const name = posixify(path__default.join('entries/endpoints', relative.replace(/\.js$/, '')));
390
392
  input[name] = resolved;
@@ -468,24 +470,6 @@ async function build_server(
468
470
 
469
471
  const { chunks } = await create_build(merged_config);
470
472
 
471
- /** @type {Record<string, string[]>} */
472
- const lookup = {};
473
- chunks.forEach((chunk) => {
474
- if (!chunk.facadeModuleId) return;
475
- const id = chunk.facadeModuleId.slice(cwd.length + 1);
476
- lookup[id] = chunk.exports;
477
- });
478
-
479
- /** @type {Record<string, import('types/internal').HttpMethod[]>} */
480
- const methods = {};
481
- manifest_data.routes.forEach((route) => {
482
- if (route.type === 'endpoint' && lookup[route.file]) {
483
- methods[route.file] = lookup[route.file]
484
- .map((x) => /** @type {import('types/internal').HttpMethod} */ (method_names[x]))
485
- .filter(Boolean);
486
- }
487
- });
488
-
489
473
  /** @type {import('vite').Manifest} */
490
474
  const vite_manifest = JSON.parse(fs__default.readFileSync(`${output_dir}/server/manifest.json`, 'utf-8'));
491
475
 
@@ -576,8 +560,10 @@ function get_methods(cwd, output, manifest_data) {
576
560
  /** @type {Record<string, import('types/internal').HttpMethod[]>} */
577
561
  const methods = {};
578
562
  manifest_data.routes.forEach((route) => {
579
- if (route.type === 'endpoint' && lookup[route.file]) {
580
- methods[route.file] = lookup[route.file]
563
+ const file = route.type === 'endpoint' ? route.file : route.shadow;
564
+
565
+ if (file && lookup[file]) {
566
+ methods[file] = lookup[file]
581
567
  .map((x) => /** @type {import('types/internal').HttpMethod} */ (method_names[x]))
582
568
  .filter(Boolean);
583
569
  }
@@ -70,6 +70,7 @@ function generate_manifest(
70
70
  pattern: ${route.pattern},
71
71
  params: ${get_params(route.params)},
72
72
  path: ${route.path ? s(route.path) : null},
73
+ shadow: ${route.shadow ? importer(`${relative_path}/${build_data.server.vite_manifest[route.shadow].file}`) : null},
73
74
  a: ${s(route.a.map(component => component && bundled_nodes.get(component).index))},
74
75
  b: ${s(route.b.map(component => component && bundled_nodes.get(component).index))}
75
76
  }`.replace(/^\t\t/gm, '');
package/dist/cli.js CHANGED
@@ -995,7 +995,7 @@ async function launch(port, https) {
995
995
  exec(`${cmd} ${https ? 'https' : 'http'}://localhost:${port}`);
996
996
  }
997
997
 
998
- const prog = sade('svelte-kit').version('1.0.0-next.259');
998
+ const prog = sade('svelte-kit').version('1.0.0-next.262');
999
999
 
1000
1000
  prog
1001
1001
  .command('dev')
@@ -1153,7 +1153,7 @@ async function check_port(port) {
1153
1153
  function welcome({ port, host, https, open, loose, allow, cwd }) {
1154
1154
  if (open) launch(port, https);
1155
1155
 
1156
- console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.259'}\n`));
1156
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.262'}\n`));
1157
1157
 
1158
1158
  const protocol = https ? 'https:' : 'http:';
1159
1159
  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.259",
3
+ "version": "1.0.0-next.262",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -12,7 +12,7 @@
12
12
  "dependencies": {
13
13
  "@sveltejs/vite-plugin-svelte": "^1.0.0-next.32",
14
14
  "sade": "^1.7.4",
15
- "vite": "^2.7.2"
15
+ "vite": "^2.8.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@playwright/test": "^1.17.1",
@@ -1,5 +1,5 @@
1
1
  import { RequestEvent } from './hooks';
2
- import { Either, JSONValue, MaybePromise, ResponseHeaders } from './helper';
2
+ import { Either, JSONObject, JSONValue, MaybePromise, ResponseHeaders } from './helper';
3
3
 
4
4
  type Body = JSONValue | Uint8Array | ReadableStream | import('stream').Readable;
5
5
 
@@ -14,5 +14,26 @@ export interface Fallthrough {
14
14
  }
15
15
 
16
16
  export interface RequestHandler<Output extends Body = Body> {
17
- (event: RequestEvent): MaybePromise<Either<Response | EndpointOutput<Output>, Fallthrough>>;
17
+ (event: RequestEvent): MaybePromise<
18
+ Either<Output extends Response ? Response : EndpointOutput<Output>, Fallthrough>
19
+ >;
20
+ }
21
+
22
+ export interface ShadowEndpointOutput<Output extends JSONObject = JSONObject> {
23
+ status?: number;
24
+ headers?: Partial<ResponseHeaders>;
25
+ body?: Output;
26
+ }
27
+
28
+ export interface ShadowRequestHandler<Output extends JSONObject = JSONObject> {
29
+ (event: RequestEvent): MaybePromise<Either<ShadowEndpointOutput<Output>, Fallthrough>>;
30
+ }
31
+
32
+ export interface ShadowData {
33
+ fallthrough?: boolean;
34
+ status?: number;
35
+ error?: Error;
36
+ redirect?: string;
37
+ cookies?: string[];
38
+ body?: JSONObject;
18
39
  }
package/types/helper.d.ts CHANGED
@@ -1,13 +1,7 @@
1
1
  type ToJSON = { toJSON(...args: any[]): Exclude<JSONValue, ToJSON> };
2
2
 
3
- export type JSONValue =
4
- | string
5
- | number
6
- | boolean
7
- | null
8
- | ToJSON
9
- | JSONValue[]
10
- | { [key: string]: JSONValue };
3
+ export type JSONObject = { [key: string]: JSONValue };
4
+ export type JSONValue = string | number | boolean | null | ToJSON | JSONValue[] | JSONObject;
11
5
 
12
6
  /** `string[]` is only for set-cookie, everything else must be type of `string` */
13
7
  export type ResponseHeaders = Record<string, string | string[]>;
@@ -1,7 +1,7 @@
1
1
  import { OutputAsset, OutputChunk } from 'rollup';
2
2
  import { ValidatedConfig } from './config';
3
3
  import { InternalApp, SSRManifest } from './app';
4
- import { Fallthrough, RequestHandler } from './endpoint';
4
+ import { Fallthrough, RequestHandler, ShadowRequestHandler } from './endpoint';
5
5
  import { Either } from './helper';
6
6
  import { ExternalFetch, GetSession, Handle, HandleError, RequestEvent } from './hooks';
7
7
  import { Load } from './page';
@@ -74,6 +74,11 @@ export interface SSRPage {
74
74
  type: 'page';
75
75
  pattern: RegExp;
76
76
  params: GetParams;
77
+ shadow:
78
+ | null
79
+ | (() => Promise<{
80
+ [method: string]: ShadowRequestHandler;
81
+ }>);
77
82
  /**
78
83
  * plan a is to render 1 or more layout components followed by a leaf component.
79
84
  */
@@ -96,7 +101,8 @@ export interface SSREndpoint {
96
101
 
97
102
  export type SSRRoute = SSREndpoint | SSRPage;
98
103
 
99
- export type CSRRoute = [RegExp, CSRComponentLoader[], CSRComponentLoader[], GetParams?];
104
+ type HasShadow = 1;
105
+ export type CSRRoute = [RegExp, CSRComponentLoader[], CSRComponentLoader[], GetParams?, HasShadow?];
100
106
 
101
107
  export type SSRNodeLoader = () => Promise<SSRNode>;
102
108
 
@@ -179,6 +185,8 @@ export type HttpMethod = 'get' | 'head' | 'post' | 'put' | 'delete' | 'patch';
179
185
 
180
186
  export interface PageData {
181
187
  type: 'page';
188
+ key: string;
189
+ shadow: string | null;
182
190
  segments: RouteSegment[];
183
191
  pattern: RegExp;
184
192
  params: string[];
@@ -189,6 +197,7 @@ export interface PageData {
189
197
 
190
198
  export interface EndpointData {
191
199
  type: 'endpoint';
200
+ key: string;
192
201
  segments: RouteSegment[];
193
202
  pattern: RegExp;
194
203
  params: string[];
package/types/page.d.ts CHANGED
@@ -4,6 +4,7 @@ import { Either, MaybePromise } from './helper';
4
4
  export interface LoadInput<Params = Record<string, string>> {
5
5
  url: URL;
6
6
  params: Params;
7
+ props: Record<string, any>;
7
8
  fetch(info: RequestInfo, init?: RequestInit): Promise<Response>;
8
9
  session: App.Session;
9
10
  stuff: Partial<App.Stuff>;