@sveltejs/kit 1.0.0-next.257 → 1.0.0-next.260

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.
@@ -731,15 +731,29 @@ class Renderer {
731
731
  for (let i = 0; i < nodes.length; i += 1) {
732
732
  const is_leaf = i === nodes.length - 1;
733
733
 
734
+ let props;
735
+
736
+ if (is_leaf) {
737
+ const serialized = document.querySelector('[data-type="svelte-props"]');
738
+ if (serialized) {
739
+ props = JSON.parse(/** @type {string} */ (serialized.textContent));
740
+ }
741
+ }
742
+
734
743
  const node = await this._load_node({
735
744
  module: await nodes[i],
736
745
  url,
737
746
  params,
738
747
  stuff,
739
748
  status: is_leaf ? status : undefined,
740
- error: is_leaf ? error : undefined
749
+ error: is_leaf ? error : undefined,
750
+ props
741
751
  });
742
752
 
753
+ if (props) {
754
+ node.uses.dependencies.add(url.href);
755
+ }
756
+
743
757
  branch.push(node);
744
758
 
745
759
  if (node && node.loaded) {
@@ -1092,10 +1106,11 @@ class Renderer {
1092
1106
  * url: URL;
1093
1107
  * params: Record<string, string>;
1094
1108
  * stuff: Record<string, any>;
1109
+ * props?: Record<string, any>;
1095
1110
  * }} options
1096
1111
  * @returns
1097
1112
  */
1098
- async _load_node({ status, error, module, url, params, stuff }) {
1113
+ async _load_node({ status, error, module, url, params, stuff, props }) {
1099
1114
  /** @type {import('./types').BranchNode} */
1100
1115
  const node = {
1101
1116
  module,
@@ -1104,12 +1119,17 @@ class Renderer {
1104
1119
  url: false,
1105
1120
  session: false,
1106
1121
  stuff: false,
1107
- dependencies: []
1122
+ dependencies: new Set()
1108
1123
  },
1109
1124
  loaded: null,
1110
1125
  stuff
1111
1126
  };
1112
1127
 
1128
+ if (props) {
1129
+ // shadow endpoint props means we need to mark this URL as a dependency of itself
1130
+ node.uses.dependencies.add(url.href);
1131
+ }
1132
+
1113
1133
  /** @type {Record<string, string>} */
1114
1134
  const uses_params = {};
1115
1135
  for (const key in params) {
@@ -1130,6 +1150,7 @@ class Renderer {
1130
1150
  /** @type {import('types/page').LoadInput | import('types/page').ErrorLoadInput} */
1131
1151
  const load_input = {
1132
1152
  params: uses_params,
1153
+ props: props || {},
1133
1154
  get url() {
1134
1155
  node.uses.url = true;
1135
1156
  return url;
@@ -1145,7 +1166,7 @@ class Renderer {
1145
1166
  fetch(resource, info) {
1146
1167
  const requested = typeof resource === 'string' ? resource : resource.url;
1147
1168
  const { href } = new URL(requested, url);
1148
- node.uses.dependencies.push(href);
1169
+ node.uses.dependencies.add(href);
1149
1170
 
1150
1171
  return started ? fetch(resource, info) : initial_fetch(resource, info);
1151
1172
  }
@@ -1173,6 +1194,8 @@ class Renderer {
1173
1194
 
1174
1195
  node.loaded = normalize(loaded);
1175
1196
  if (node.loaded.stuff) node.stuff = node.loaded.stuff;
1197
+ } else if (props) {
1198
+ node.loaded = normalize({ props });
1176
1199
  }
1177
1200
 
1178
1201
  return node;
@@ -1191,7 +1214,7 @@ class Renderer {
1191
1214
  if (cached) return cached;
1192
1215
  }
1193
1216
 
1194
- const [pattern, a, b, get_params] = route;
1217
+ const [pattern, a, b, get_params, has_shadow] = route;
1195
1218
  const params = get_params
1196
1219
  ? // the pattern is for the route which we've already matched to this path
1197
1220
  get_params(/** @type {RegExpExecArray} */ (pattern.exec(path)))
@@ -1235,16 +1258,47 @@ class Renderer {
1235
1258
  (changed.url && previous.uses.url) ||
1236
1259
  changed.params.some((param) => previous.uses.params.has(param)) ||
1237
1260
  (changed.session && previous.uses.session) ||
1238
- previous.uses.dependencies.some((dep) => this.invalid.has(dep)) ||
1261
+ Array.from(previous.uses.dependencies).some((dep) => this.invalid.has(dep)) ||
1239
1262
  (stuff_changed && previous.uses.stuff);
1240
1263
 
1241
1264
  if (changed_since_last_render) {
1242
- node = await this._load_node({
1243
- module,
1244
- url,
1245
- params,
1246
- stuff
1247
- });
1265
+ /** @type {Record<string, any>} */
1266
+ let props = {};
1267
+
1268
+ if (has_shadow && i === a.length - 1) {
1269
+ const res = await fetch(`${url.pathname}/__data.json`, {
1270
+ headers: {
1271
+ 'x-sveltekit-noredirect': 'true'
1272
+ }
1273
+ });
1274
+
1275
+ if (res.ok) {
1276
+ const redirect = res.headers.get('x-sveltekit-location');
1277
+
1278
+ if (redirect) {
1279
+ return {
1280
+ redirect,
1281
+ props: {},
1282
+ state: this.current
1283
+ };
1284
+ }
1285
+
1286
+ props = await res.json();
1287
+ } else {
1288
+ status = res.status;
1289
+ error = new Error('Failed to load data');
1290
+ }
1291
+ }
1292
+
1293
+ if (!error) {
1294
+ node = await this._load_node({
1295
+ module,
1296
+ url,
1297
+ params,
1298
+ props,
1299
+ stuff
1300
+ });
1301
+ }
1248
1302
 
1249
1303
  if (node && node.loaded) {
1250
1304
  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)$/;
@@ -1076,6 +1068,8 @@ async function render_response({
1076
1068
  /** @type {Array<{ url: string, body: string, json: string }>} */
1077
1069
  const serialized_data = [];
1078
1070
 
1071
+ let shadow_props;
1072
+
1079
1073
  let rendered;
1080
1074
 
1081
1075
  let is_private = false;
@@ -1086,13 +1080,14 @@ async function render_response({
1086
1080
  }
1087
1081
 
1088
1082
  if (ssr) {
1089
- branch.forEach(({ node, loaded, fetched, uses_credentials }) => {
1083
+ branch.forEach(({ node, props, loaded, fetched, uses_credentials }) => {
1090
1084
  if (node.css) node.css.forEach((url) => stylesheets.add(url));
1091
1085
  if (node.js) node.js.forEach((url) => modulepreloads.add(url));
1092
1086
  if (node.styles) Object.entries(node.styles).forEach(([k, v]) => styles.set(k, v));
1093
1087
 
1094
1088
  // TODO probably better if `fetched` wasn't populated unless `hydrate`
1095
1089
  if (fetched && page_config.hydrate) serialized_data.push(...fetched);
1090
+ if (props) shadow_props = props;
1096
1091
 
1097
1092
  if (uses_credentials) is_private = true;
1098
1093
 
@@ -1271,6 +1266,11 @@ async function render_response({
1271
1266
  return `<script ${attributes}>${json}</script>`;
1272
1267
  })
1273
1268
  .join('\n\t');
1269
+
1270
+ if (shadow_props) {
1271
+ // prettier-ignore
1272
+ body += `<script type="application/json" data-type="svelte-props">${s(shadow_props)}</script>`;
1273
+ }
1274
1274
  }
1275
1275
 
1276
1276
  if (options.service_worker) {
@@ -1476,6 +1476,7 @@ function is_root_relative(path) {
1476
1476
  * $session: any;
1477
1477
  * stuff: Record<string, any>;
1478
1478
  * is_error: boolean;
1479
+ * is_leaf: boolean;
1479
1480
  * status?: number;
1480
1481
  * error?: Error;
1481
1482
  * }} opts
@@ -1492,6 +1493,7 @@ async function load_node({
1492
1493
  $session,
1493
1494
  stuff,
1494
1495
  is_error,
1496
+ is_leaf,
1495
1497
  status,
1496
1498
  error
1497
1499
  }) {
@@ -1513,13 +1515,40 @@ async function load_node({
1513
1515
  */
1514
1516
  let set_cookie_headers = [];
1515
1517
 
1518
+ /** @type {import('types/helper').Either<import('types/endpoint').Fallthrough, import('types/page').LoadOutput>} */
1516
1519
  let loaded;
1517
1520
 
1518
- if (module.load) {
1521
+ /** @type {import('types/endpoint').ShadowData} */
1522
+ const shadow = is_leaf
1523
+ ? await load_shadow_data(
1524
+ /** @type {import('types/internal').SSRPage} */ (route),
1525
+ event,
1526
+ !!state.prerender
1527
+ )
1528
+ : {};
1529
+
1530
+ if (shadow.fallthrough) return;
1531
+
1532
+ if (shadow.cookies) {
1533
+ set_cookie_headers.push(...shadow.cookies);
1534
+ }
1535
+
1536
+ if (shadow.error) {
1537
+ loaded = {
1538
+ status: shadow.status,
1539
+ error: shadow.error
1540
+ };
1541
+ } else if (shadow.redirect) {
1542
+ loaded = {
1543
+ status: shadow.status,
1544
+ redirect: shadow.redirect
1545
+ };
1546
+ } else if (module.load) {
1519
1547
  /** @type {import('types/page').LoadInput | import('types/page').ErrorLoadInput} */
1520
1548
  const load_input = {
1521
1549
  url: state.prerender ? create_prerendering_url_proxy(url) : url,
1522
1550
  params,
1551
+ props: shadow.body || {},
1523
1552
  get session() {
1524
1553
  uses_credentials = true;
1525
1554
  return $session;
@@ -1555,9 +1584,15 @@ async function load_node({
1555
1584
 
1556
1585
  // merge headers from request
1557
1586
  for (const [key, value] of event.request.headers) {
1558
- if (opts.headers.has(key)) continue;
1559
- if (key === 'cookie' || key === 'authorization' || key === 'if-none-match') continue;
1560
- opts.headers.set(key, value);
1587
+ if (
1588
+ key !== 'authorization' &&
1589
+ key !== 'cookie' &&
1590
+ key !== 'host' &&
1591
+ key !== 'if-none-match' &&
1592
+ !opts.headers.has(key)
1593
+ ) {
1594
+ opts.headers.set(key, value);
1595
+ }
1561
1596
  }
1562
1597
 
1563
1598
  opts.headers.set('referer', event.url.href);
@@ -1745,6 +1780,10 @@ async function load_node({
1745
1780
  if (!loaded) {
1746
1781
  throw new Error(`load function must return a value${options.dev ? ` (${node.entry})` : ''}`);
1747
1782
  }
1783
+ } else if (shadow.body) {
1784
+ loaded = {
1785
+ props: shadow.body
1786
+ };
1748
1787
  } else {
1749
1788
  loaded = {};
1750
1789
  }
@@ -1753,8 +1792,21 @@ async function load_node({
1753
1792
  return;
1754
1793
  }
1755
1794
 
1795
+ // generate __data.json files when prerendering
1796
+ if (shadow.body && state.prerender) {
1797
+ const pathname = `${event.url.pathname}/__data.json`;
1798
+
1799
+ const dependency = {
1800
+ response: new Response(undefined),
1801
+ body: JSON.stringify(shadow.body)
1802
+ };
1803
+
1804
+ state.prerender.dependencies.set(pathname, dependency);
1805
+ }
1806
+
1756
1807
  return {
1757
1808
  node,
1809
+ props: shadow.body,
1758
1810
  loaded: normalize(loaded),
1759
1811
  stuff: loaded.stuff || stuff,
1760
1812
  fetched,
@@ -1763,6 +1815,127 @@ async function load_node({
1763
1815
  };
1764
1816
  }
1765
1817
 
1818
+ /**
1819
+ *
1820
+ * @param {import('types/internal').SSRPage} route
1821
+ * @param {import('types/hooks').RequestEvent} event
1822
+ * @param {boolean} prerender
1823
+ * @returns {Promise<import('types/endpoint').ShadowData>}
1824
+ */
1825
+ async function load_shadow_data(route, event, prerender) {
1826
+ if (!route.shadow) return {};
1827
+
1828
+ try {
1829
+ const mod = await route.shadow();
1830
+
1831
+ if (prerender && (mod.post || mod.put || mod.del || mod.patch)) {
1832
+ throw new Error('Cannot prerender pages that have shadow endpoints with mutative methods');
1833
+ }
1834
+
1835
+ const method = event.request.method.toLowerCase().replace('delete', 'del');
1836
+ const handler = mod[method];
1837
+
1838
+ if (!handler) {
1839
+ return {
1840
+ status: 405,
1841
+ error: new Error(`${method} method not allowed`)
1842
+ };
1843
+ }
1844
+
1845
+ /** @type {import('types/endpoint').ShadowData} */
1846
+ const data = {
1847
+ status: 200,
1848
+ cookies: [],
1849
+ body: {}
1850
+ };
1851
+
1852
+ if (method !== 'get') {
1853
+ const result = await handler(event);
1854
+
1855
+ if (result.fallthrough) return result;
1856
+
1857
+ const { status = 200, headers = {}, body = {} } = result;
1858
+
1859
+ validate_shadow_output(headers, body);
1860
+
1861
+ if (headers['set-cookie']) {
1862
+ /** @type {string[]} */ (data.cookies).push(...headers['set-cookie']);
1863
+ }
1864
+
1865
+ // Redirects are respected...
1866
+ if (status >= 300 && status < 400) {
1867
+ return {
1868
+ status,
1869
+ redirect: /** @type {string} */ (
1870
+ headers instanceof Headers ? headers.get('location') : headers.location
1871
+ )
1872
+ };
1873
+ }
1874
+
1875
+ // ...but 4xx and 5xx status codes _don't_ result in the error page
1876
+ // rendering for non-GET requests — instead, we allow the page
1877
+ // to render with any validation errors etc that were returned
1878
+ data.status = status;
1879
+ data.body = body;
1880
+ }
1881
+
1882
+ if (mod.get) {
1883
+ const result = await mod.get.call(null, event);
1884
+
1885
+ if (result.fallthrough) return result;
1886
+
1887
+ const { status = 200, headers = {}, body = {} } = result;
1888
+
1889
+ validate_shadow_output(headers, body);
1890
+
1891
+ if (headers['set-cookie']) {
1892
+ /** @type {string[]} */ (data.cookies).push(...headers['set-cookie']);
1893
+ }
1894
+
1895
+ if (status >= 400) {
1896
+ return {
1897
+ status,
1898
+ error: new Error('Failed to load data')
1899
+ };
1900
+ }
1901
+
1902
+ if (status >= 300) {
1903
+ return {
1904
+ status,
1905
+ redirect: /** @type {string} */ (
1906
+ headers instanceof Headers ? headers.get('location') : headers.location
1907
+ )
1908
+ };
1909
+ }
1910
+
1911
+ data.body = { ...body, ...data.body };
1912
+ }
1913
+
1914
+ return data;
1915
+ } catch (e) {
1916
+ return {
1917
+ status: 500,
1918
+ error: coalesce_to_error(e)
1919
+ };
1920
+ }
1921
+ }
1922
+
1923
+ /**
1924
+ * @param {Headers | Partial<import('types/helper').ResponseHeaders>} headers
1925
+ * @param {import('types/helper').JSONValue} body
1926
+ */
1927
+ function validate_shadow_output(headers, body) {
1928
+ if (headers instanceof Headers && headers.has('set-cookie')) {
1929
+ throw new Error(
1930
+ 'Shadow endpoint request handler cannot use Headers interface with Set-Cookie headers'
1931
+ );
1932
+ }
1933
+
1934
+ if (!is_pojo(body)) {
1935
+ throw new Error('Body returned from shadow endpoint request handler must be a plain object');
1936
+ }
1937
+ }
1938
+
1766
1939
  /**
1767
1940
  * @typedef {import('./types.js').Loaded} Loaded
1768
1941
  * @typedef {import('types/internal').SSROptions} SSROptions
@@ -1799,7 +1972,8 @@ async function respond_with_error({ event, options, state, $session, status, err
1799
1972
  node: default_layout,
1800
1973
  $session,
1801
1974
  stuff: {},
1802
- is_error: false
1975
+ is_error: false,
1976
+ is_leaf: false
1803
1977
  })
1804
1978
  );
1805
1979
 
@@ -1815,6 +1989,7 @@ async function respond_with_error({ event, options, state, $session, status, err
1815
1989
  $session,
1816
1990
  stuff: layout_loaded ? layout_loaded.stuff : {},
1817
1991
  is_error: true,
1992
+ is_leaf: false,
1818
1993
  status,
1819
1994
  error
1820
1995
  })
@@ -1947,7 +2122,8 @@ async function respond$1(opts) {
1947
2122
  url: event.url,
1948
2123
  node,
1949
2124
  stuff,
1950
- is_error: false
2125
+ is_error: false,
2126
+ is_leaf: i === nodes.length - 1
1951
2127
  });
1952
2128
 
1953
2129
  if (!loaded) return;
@@ -2002,6 +2178,7 @@ async function respond$1(opts) {
2002
2178
  node: error_node,
2003
2179
  stuff: node_loaded.stuff,
2004
2180
  is_error: true,
2181
+ is_leaf: false,
2005
2182
  status,
2006
2183
  error
2007
2184
  })
@@ -2115,13 +2292,12 @@ function with_cookies(response, set_cookie_headers) {
2115
2292
  /**
2116
2293
  * @param {import('types/hooks').RequestEvent} event
2117
2294
  * @param {import('types/internal').SSRPage} route
2118
- * @param {RegExpExecArray} match
2119
2295
  * @param {import('types/internal').SSROptions} options
2120
2296
  * @param {import('types/internal').SSRState} state
2121
2297
  * @param {boolean} ssr
2122
2298
  * @returns {Promise<Response | undefined>}
2123
2299
  */
2124
- async function render_page(event, route, match, options, state, ssr) {
2300
+ async function render_page(event, route, options, state, ssr) {
2125
2301
  if (state.initiator === route) {
2126
2302
  // infinite request cycle detected
2127
2303
  return new Response(`Not found: ${event.url.pathname}`, {
@@ -2129,7 +2305,16 @@ async function render_page(event, route, match, options, state, ssr) {
2129
2305
  });
2130
2306
  }
2131
2307
 
2132
- const params = route.params ? decode_params(route.params(match)) : {};
2308
+ if (route.shadow) {
2309
+ const type = negotiate(event.request.headers.get('accept') || 'text/html', [
2310
+ 'text/html',
2311
+ 'application/json'
2312
+ ]);
2313
+
2314
+ if (type === 'application/json') {
2315
+ return render_endpoint(event, await route.shadow());
2316
+ }
2317
+ }
2133
2318
 
2134
2319
  const $session = await options.hooks.getSession(event);
2135
2320
 
@@ -2139,7 +2324,7 @@ async function render_page(event, route, match, options, state, ssr) {
2139
2324
  state,
2140
2325
  $session,
2141
2326
  route,
2142
- params,
2327
+ params: event.params, // TODO this is redundant
2143
2328
  ssr
2144
2329
  });
2145
2330
 
@@ -2158,6 +2343,60 @@ async function render_page(event, route, match, options, state, ssr) {
2158
2343
  }
2159
2344
  }
2160
2345
 
2346
+ /**
2347
+ * @param {string} accept
2348
+ * @param {string[]} types
2349
+ */
2350
+ function negotiate(accept, types) {
2351
+ const parts = accept
2352
+ .split(',')
2353
+ .map((str, i) => {
2354
+ const match = /([^/]+)\/([^;]+)(?:;q=([0-9.]+))?/.exec(str);
2355
+ if (match) {
2356
+ const [, type, subtype, q = '1'] = match;
2357
+ return { type, subtype, q: +q, i };
2358
+ }
2359
+
2360
+ throw new Error(`Invalid Accept header: ${accept}`);
2361
+ })
2362
+ .sort((a, b) => {
2363
+ if (a.q !== b.q) {
2364
+ return b.q - a.q;
2365
+ }
2366
+
2367
+ if ((a.subtype === '*') !== (b.subtype === '*')) {
2368
+ return a.subtype === '*' ? 1 : -1;
2369
+ }
2370
+
2371
+ if ((a.type === '*') !== (b.type === '*')) {
2372
+ return a.type === '*' ? 1 : -1;
2373
+ }
2374
+
2375
+ return a.i - b.i;
2376
+ });
2377
+
2378
+ let accepted;
2379
+ let min_priority = Infinity;
2380
+
2381
+ for (const mimetype of types) {
2382
+ const [type, subtype] = mimetype.split('/');
2383
+ const priority = parts.findIndex(
2384
+ (part) =>
2385
+ (part.type === type || part.type === '*') &&
2386
+ (part.subtype === subtype || part.subtype === '*')
2387
+ );
2388
+
2389
+ if (priority !== -1 && priority < min_priority) {
2390
+ accepted = mimetype;
2391
+ min_priority = priority;
2392
+ }
2393
+ }
2394
+
2395
+ return accepted;
2396
+ }
2397
+
2398
+ const DATA_SUFFIX = '/__data.json';
2399
+
2161
2400
  /** @type {import('types/internal').Respond} */
2162
2401
  async function respond(request, options, state = {}) {
2163
2402
  const url = new URL(request.url);
@@ -2284,14 +2523,46 @@ async function respond(request, options, state = {}) {
2284
2523
  decoded = decoded.slice(options.paths.base.length) || '/';
2285
2524
  }
2286
2525
 
2526
+ const is_data_request = decoded.endsWith(DATA_SUFFIX);
2527
+ if (is_data_request) decoded = decoded.slice(0, -DATA_SUFFIX.length) || '/';
2528
+
2287
2529
  for (const route of options.manifest._.routes) {
2288
2530
  const match = route.pattern.exec(decoded);
2289
2531
  if (!match) continue;
2290
2532
 
2291
- const response =
2292
- route.type === 'endpoint'
2293
- ? await render_endpoint(event, route, match)
2294
- : await render_page(event, route, match, options, state, ssr);
2533
+ event.params = route.params ? decode_params(route.params(match)) : {};
2534
+
2535
+ /** @type {Response | undefined} */
2536
+ let response;
2537
+
2538
+ if (is_data_request && route.type === 'page' && route.shadow) {
2539
+ response = await render_endpoint(event, await route.shadow());
2540
+
2541
+ // since redirects are opaque to the browser, we need to repackage
2542
+ // 3xx responses as 200s with a custom header
2543
+ if (
2544
+ response &&
2545
+ response.status >= 300 &&
2546
+ response.status < 400 &&
2547
+ request.headers.get('x-sveltekit-noredirect') === 'true'
2548
+ ) {
2549
+ const location = response.headers.get('location');
2550
+
2551
+ if (location) {
2552
+ response = new Response(undefined, {
2553
+ status: 204,
2554
+ headers: {
2555
+ 'x-sveltekit-location': location
2556
+ }
2557
+ });
2558
+ }
2559
+ }
2560
+ } else {
2561
+ response =
2562
+ route.type === 'endpoint'
2563
+ ? await render_endpoint(event, await route.load())
2564
+ : await render_page(event, route, options, state, ssr);
2565
+ }
2295
2566
 
2296
2567
  if (response) {
2297
2568
  // 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.257');
998
+ const prog = sade('svelte-kit').version('1.0.0-next.260');
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.257'}\n`));
1156
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.260'}\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.257",
3
+ "version": "1.0.0-next.260",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -125,7 +125,7 @@ declare module '$app/stores' {
125
125
  export const page: Readable<{
126
126
  url: URL;
127
127
  params: Record<string, string>;
128
- stuff: Record<string, any>;
128
+ stuff: App.Stuff;
129
129
  status: number;
130
130
  error: Error | null;
131
131
  }>;
@@ -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>;