@sveltejs/kit 2.16.0 → 2.17.0

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.
Files changed (36) hide show
  1. package/package.json +5 -3
  2. package/src/core/adapt/builder.js +2 -0
  3. package/src/core/config/index.js +16 -1
  4. package/src/core/config/options.js +2 -1
  5. package/src/core/generate_manifest/index.js +14 -5
  6. package/src/core/postbuild/analyse.js +2 -2
  7. package/src/core/sync/write_client_manifest.js +33 -17
  8. package/src/core/sync/write_server.js +2 -1
  9. package/src/exports/public.d.ts +29 -6
  10. package/src/exports/vite/build/build_server.js +5 -0
  11. package/src/exports/vite/dev/index.js +29 -1
  12. package/src/exports/vite/index.js +51 -5
  13. package/src/runtime/app/state/index.js +11 -0
  14. package/src/runtime/client/client.js +80 -46
  15. package/src/runtime/client/parse.js +20 -0
  16. package/src/runtime/client/types.d.ts +16 -1
  17. package/src/runtime/client/utils.js +6 -0
  18. package/src/runtime/pathname.js +54 -0
  19. package/src/runtime/server/cookie.js +2 -1
  20. package/src/runtime/server/endpoint.js +0 -5
  21. package/src/runtime/server/fetch.js +12 -0
  22. package/src/runtime/server/page/index.js +1 -1
  23. package/src/runtime/server/page/render.js +25 -3
  24. package/src/runtime/server/page/server_routing.js +110 -0
  25. package/src/runtime/server/respond.js +45 -26
  26. package/src/runtime/server/validate-headers.js +64 -0
  27. package/src/runtime/utils.js +21 -0
  28. package/src/types/ambient-private.d.ts +1 -0
  29. package/src/types/global-private.d.ts +2 -0
  30. package/src/types/internal.d.ts +47 -9
  31. package/src/utils/filesystem.js +4 -3
  32. package/src/utils/routing.js +8 -0
  33. package/src/utils/url.js +0 -23
  34. package/src/version.js +1 -1
  35. package/types/index.d.ts +72 -13
  36. package/types/index.d.ts.map +2 -1
@@ -1,7 +1,6 @@
1
1
  import { BROWSER, DEV } from 'esm-env';
2
2
  import { onMount, tick } from 'svelte';
3
3
  import {
4
- add_data_suffix,
5
4
  decode_params,
6
5
  decode_pathname,
7
6
  strip_hash,
@@ -9,7 +8,7 @@ import {
9
8
  normalize_path
10
9
  } from '../../utils/url.js';
11
10
  import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js';
12
- import { parse } from './parse.js';
11
+ import { parse, parse_server_route } from './parse.js';
13
12
  import * as storage from './session-storage.js';
14
13
  import {
15
14
  find_anchor,
@@ -40,6 +39,7 @@ import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../sh
40
39
  import { get_message, get_status } from '../../utils/error.js';
41
40
  import { writable } from 'svelte/store';
42
41
  import { page, update, navigating } from './state.svelte.js';
42
+ import { add_data_suffix, add_resolution_prefix } from '../pathname.js';
43
43
 
44
44
  const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
45
45
 
@@ -158,7 +158,7 @@ async function update_service_worker() {
158
158
 
159
159
  function noop() {}
160
160
 
161
- /** @type {import('types').CSRRoute[]} */
161
+ /** @type {import('types').CSRRoute[]} All routes of the app. Only available when kit.router.resolution=client */
162
162
  let routes;
163
163
  /** @type {import('types').CSRPageNodeLoader} */
164
164
  let default_layout_loader;
@@ -265,7 +265,7 @@ export async function start(_app, _target, hydrate) {
265
265
 
266
266
  await _app.hooks.init?.();
267
267
 
268
- routes = parse(_app);
268
+ routes = __SVELTEKIT_CLIENT_ROUTING__ ? parse(_app) : [];
269
269
  container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement;
270
270
  target = _target;
271
271
 
@@ -322,7 +322,8 @@ async function _invalidate() {
322
322
  if (!pending_invalidate) return;
323
323
  pending_invalidate = null;
324
324
 
325
- const intent = get_navigation_intent(current.url, true);
325
+ const nav_token = (token = {});
326
+ const intent = await get_navigation_intent(current.url, true);
326
327
 
327
328
  // Clear preload, it might be affected by the invalidation.
328
329
  // Also solves an edge case where a preload is triggered, the navigation for it
@@ -330,7 +331,6 @@ async function _invalidate() {
330
331
  // at which point the invalidation should take over and "win".
331
332
  load_cache = null;
332
333
 
333
- const nav_token = (token = {});
334
334
  const navigation_result = intent && (await load_route(intent));
335
335
  if (!navigation_result || nav_token !== token) return;
336
336
 
@@ -433,10 +433,7 @@ async function _preload_data(intent) {
433
433
  * @returns {Promise<void>}
434
434
  */
435
435
  async function _preload_code(url) {
436
- const rerouted = get_rerouted_url(url);
437
- if (!rerouted) return;
438
-
439
- const route = routes.find((route) => route.exec(get_url_path(rerouted)));
436
+ const route = (await get_navigation_intent(url, false))?.route;
440
437
 
441
438
  if (route) {
442
439
  await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]()));
@@ -587,8 +584,7 @@ function get_navigation_result_from_branch({ url, params, branch, status, error,
587
584
  }
588
585
 
589
586
  /**
590
- * Call the load function of the given node, if it exists.
591
- * If `server_data` is passed, this is treated as the initial run and the page endpoint is not requested.
587
+ * Call the universal load function of the given node, if it exists.
592
588
  *
593
589
  * @param {{
594
590
  * loader: import('types').CSRPageNodeLoader;
@@ -1240,31 +1236,47 @@ function get_rerouted_url(url) {
1240
1236
  * returns undefined.
1241
1237
  * @param {URL | undefined} url
1242
1238
  * @param {boolean} invalidating
1239
+ * @returns {Promise<import('./types.js').NavigationIntent | undefined>}
1243
1240
  */
1244
- function get_navigation_intent(url, invalidating) {
1241
+ async function get_navigation_intent(url, invalidating) {
1245
1242
  if (!url) return;
1246
1243
  if (is_external_url(url, base, app.hash)) return;
1247
1244
 
1248
- const rerouted = get_rerouted_url(url);
1249
- if (!rerouted) return;
1245
+ if (__SVELTEKIT_CLIENT_ROUTING__) {
1246
+ const rerouted = get_rerouted_url(url);
1247
+ if (!rerouted) return;
1250
1248
 
1251
- const path = get_url_path(rerouted);
1249
+ const path = get_url_path(rerouted);
1252
1250
 
1253
- for (const route of routes) {
1254
- const params = route.exec(path);
1251
+ for (const route of routes) {
1252
+ const params = route.exec(path);
1255
1253
 
1256
- if (params) {
1257
- const id = get_page_key(url);
1258
- /** @type {import('./types.js').NavigationIntent} */
1259
- const intent = {
1260
- id,
1261
- invalidating,
1262
- route,
1263
- params: decode_params(params),
1264
- url
1265
- };
1266
- return intent;
1254
+ if (params) {
1255
+ return {
1256
+ id: get_page_key(url),
1257
+ invalidating,
1258
+ route,
1259
+ params: decode_params(params),
1260
+ url
1261
+ };
1262
+ }
1267
1263
  }
1264
+ } else {
1265
+ /** @type {{ route?: import('types').CSRRouteServer, params: Record<string, string>}} */
1266
+ const { route, params } = await import(
1267
+ /* @vite-ignore */
1268
+ add_resolution_prefix(url.pathname)
1269
+ );
1270
+
1271
+ if (!route) return;
1272
+
1273
+ return {
1274
+ id: get_page_key(url),
1275
+ invalidating,
1276
+ route: parse_server_route(route, app.nodes),
1277
+ params,
1278
+ url
1279
+ };
1268
1280
  }
1269
1281
  }
1270
1282
 
@@ -1347,11 +1359,15 @@ async function navigate({
1347
1359
  accept = noop,
1348
1360
  block = noop
1349
1361
  }) {
1350
- const intent = get_navigation_intent(url, false);
1362
+ const prev_token = token;
1363
+ token = nav_token;
1364
+
1365
+ const intent = await get_navigation_intent(url, false);
1351
1366
  const nav = _before_navigate({ url, type, delta: popped?.delta, intent });
1352
1367
 
1353
1368
  if (!nav) {
1354
1369
  block();
1370
+ if (token === nav_token) token = prev_token;
1355
1371
  return;
1356
1372
  }
1357
1373
 
@@ -1367,7 +1383,6 @@ async function navigate({
1367
1383
  stores.navigating.set((navigating.current = nav.navigation));
1368
1384
  }
1369
1385
 
1370
- token = nav_token;
1371
1386
  let navigation_result = intent && (await load_route(intent));
1372
1387
 
1373
1388
  if (!navigation_result) {
@@ -1621,6 +1636,8 @@ if (import.meta.hot) {
1621
1636
  function setup_preload() {
1622
1637
  /** @type {NodeJS.Timeout} */
1623
1638
  let mousemove_timeout;
1639
+ /** @type {Element} */
1640
+ let current_a;
1624
1641
 
1625
1642
  container.addEventListener('mousemove', (event) => {
1626
1643
  const target = /** @type {Element} */ (event.target);
@@ -1656,9 +1673,11 @@ function setup_preload() {
1656
1673
  * @param {Element} element
1657
1674
  * @param {number} priority
1658
1675
  */
1659
- function preload(element, priority) {
1676
+ async function preload(element, priority) {
1660
1677
  const a = find_anchor(element, container);
1661
- if (!a) return;
1678
+ if (!a || a === current_a) return;
1679
+
1680
+ current_a = a;
1662
1681
 
1663
1682
  const { url, external, download } = get_link_info(a, base, app.hash);
1664
1683
  if (external || download) return;
@@ -1670,7 +1689,7 @@ function setup_preload() {
1670
1689
 
1671
1690
  if (!options.reload && !same_url) {
1672
1691
  if (priority <= options.preload_data) {
1673
- const intent = get_navigation_intent(url, false);
1692
+ const intent = await get_navigation_intent(url, false);
1674
1693
  if (intent) {
1675
1694
  if (DEV) {
1676
1695
  _preload_data(intent).then((result) => {
@@ -1924,7 +1943,7 @@ export async function preloadData(href) {
1924
1943
  }
1925
1944
 
1926
1945
  const url = resolve_url(href);
1927
- const intent = get_navigation_intent(url, false);
1946
+ const intent = await get_navigation_intent(url, false);
1928
1947
 
1929
1948
  if (!intent) {
1930
1949
  throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`);
@@ -1954,7 +1973,7 @@ export async function preloadData(href) {
1954
1973
  * @param {string} pathname
1955
1974
  * @returns {Promise<void>}
1956
1975
  */
1957
- export function preloadCode(pathname) {
1976
+ export async function preloadCode(pathname) {
1958
1977
  if (!BROWSER) {
1959
1978
  throw new Error('Cannot call preloadCode(...) on the server');
1960
1979
  }
@@ -1974,9 +1993,11 @@ export function preloadCode(pathname) {
1974
1993
  );
1975
1994
  }
1976
1995
 
1977
- const rerouted = get_rerouted_url(url);
1978
- if (!rerouted || !routes.find((route) => route.exec(get_url_path(rerouted)))) {
1979
- throw new Error(`'${pathname}' did not match any routes`);
1996
+ if (__SVELTEKIT_CLIENT_ROUTING__) {
1997
+ const rerouted = get_rerouted_url(url);
1998
+ if (!rerouted || !routes.find((route) => route.exec(get_url_path(rerouted)))) {
1999
+ throw new Error(`'${pathname}' did not match any routes`);
2000
+ }
1980
2001
  }
1981
2002
  }
1982
2003
 
@@ -2474,16 +2495,31 @@ function _start_router() {
2474
2495
  */
2475
2496
  async function _hydrate(
2476
2497
  target,
2477
- { status = 200, error, node_ids, params, route, data: server_data_nodes, form }
2498
+ { status = 200, error, node_ids, params, route, server_route, data: server_data_nodes, form }
2478
2499
  ) {
2479
2500
  hydrated = true;
2480
2501
 
2481
2502
  const url = new URL(location.href);
2482
2503
 
2483
- if (!__SVELTEKIT_EMBEDDED__) {
2484
- // See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation
2485
- // of determining the params on the client side.
2486
- ({ params = {}, route = { id: null } } = get_navigation_intent(url, false) || {});
2504
+ /** @type {import('types').CSRRoute | undefined} */
2505
+ let parsed_route;
2506
+
2507
+ if (__SVELTEKIT_CLIENT_ROUTING__) {
2508
+ if (!__SVELTEKIT_EMBEDDED__) {
2509
+ // See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation
2510
+ // of determining the params on the client side.
2511
+ ({ params = {}, route = { id: null } } = (await get_navigation_intent(url, false)) || {});
2512
+ }
2513
+
2514
+ parsed_route = routes.find(({ id }) => id === route.id);
2515
+ } else {
2516
+ // undefined in case of 404
2517
+ if (server_route) {
2518
+ parsed_route = route = parse_server_route(server_route, app.nodes);
2519
+ } else {
2520
+ route = { id: null };
2521
+ params = {};
2522
+ }
2487
2523
  }
2488
2524
 
2489
2525
  /** @type {import('./types.js').NavigationFinished | undefined} */
@@ -2517,8 +2553,6 @@ async function _hydrate(
2517
2553
  /** @type {Array<import('./types.js').BranchNode | undefined>} */
2518
2554
  const branch = await Promise.all(branch_promises);
2519
2555
 
2520
- const parsed_route = routes.find(({ id }) => id === route.id);
2521
-
2522
2556
  // server-side will have compacted the branch, reinstate empty slots
2523
2557
  // so that error boundaries can be lined up correctly
2524
2558
  if (parsed_route) {
@@ -10,6 +10,7 @@ export function parse({ nodes, server_loads, dictionary, matchers }) {
10
10
  return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
11
11
  const { pattern, params } = parse_route_id(id);
12
12
 
13
+ /** @type {import('types').CSRRoute} */
13
14
  const route = {
14
15
  id,
15
16
  /** @param {string} path */
@@ -55,3 +56,22 @@ export function parse({ nodes, server_loads, dictionary, matchers }) {
55
56
  return id === undefined ? id : [layouts_with_server_load.has(id), nodes[id]];
56
57
  }
57
58
  }
59
+
60
+ /**
61
+ * @param {import('types').CSRRouteServer} input
62
+ * @param {import('types').CSRPageNodeLoader[]} app_nodes Will be modified if a new node is loaded that's not already in the array
63
+ * @returns {import('types').CSRRoute}
64
+ */
65
+ export function parse_server_route({ nodes, id, leaf, layouts, errors }, app_nodes) {
66
+ return {
67
+ id,
68
+ exec: () => ({}), // dummy function; exec already happened on the server
69
+ // By writing to app_nodes only when a loader at that index is not already defined,
70
+ // we ensure that loaders have referential equality when they load the same node.
71
+ // Code elsewhere in client.js relies on this referential equality to determine
72
+ // if a loader is different and should therefore (re-)run.
73
+ errors: errors.map((n) => (n ? (app_nodes[n] ||= nodes[n]) : undefined)),
74
+ layouts: layouts.map((n) => (n ? [n[0], (app_nodes[n[1]] ||= nodes[n[1]])] : undefined)),
75
+ leaf: [leaf[0], (app_nodes[leaf[1]] ||= nodes[leaf[1]])]
76
+ };
77
+ }
@@ -4,6 +4,7 @@ import {
4
4
  CSRPageNode,
5
5
  CSRPageNodeLoader,
6
6
  CSRRoute,
7
+ CSRRouteServer,
7
8
  ServerDataNode,
8
9
  TrailingSlash,
9
10
  Uses
@@ -12,13 +13,18 @@ import { Page, ParamMatcher } from '@sveltejs/kit';
12
13
 
13
14
  export interface SvelteKitApp {
14
15
  /**
15
- * A list of all the error/layout/page nodes used in the app
16
+ * A list of all the error/layout/page nodes used in the app.
17
+ * - In case of router.resolution=client, this is filled completely upfront.
18
+ * - In case of router.resolution=server, this is filled with the root layout and root error page
19
+ * at the beginning and then filled up as the user navigates around the app, loading new nodes
16
20
  */
17
21
  nodes: CSRPageNodeLoader[];
18
22
 
19
23
  /**
20
24
  * A list of all layout node ids that have a server load function.
21
25
  * Pages are not present because it's shorter to encode it on the leaf itself.
26
+ *
27
+ * In case of router.resolution=server, this only contains one entry for the root layout.
22
28
  */
23
29
  server_loads: number[];
24
30
 
@@ -27,9 +33,16 @@ export interface SvelteKitApp {
27
33
  * is parsed into an array of routes on startup. The numbers refer to the indices in `nodes`.
28
34
  * If the leaf number is negative, it means it does use a server load function and the complement is the node index.
29
35
  * The route layout and error nodes are not referenced, they are always number 0 and 1 and always apply.
36
+ *
37
+ * In case of router.resolution=server, this object is empty, as resolution happens on the server.
30
38
  */
31
39
  dictionary: Record<string, [leaf: number, layouts: number[], errors?: number[]]>;
32
40
 
41
+ /**
42
+ * A map of `[matcherName: string]: (..) => boolean`, which is used to match route parameters.
43
+ *
44
+ * In case of router.resolution=server, this object is empty, as resolution happens on the server.
45
+ */
33
46
  matchers: Record<string, ParamMatcher>;
34
47
 
35
48
  hooks: ClientHooks;
@@ -108,6 +121,8 @@ export interface HydrateOptions {
108
121
  node_ids: number[];
109
122
  params: Record<string, string>;
110
123
  route: { id: string | null };
124
+ /** Only used when `router.resolution=server`; can then still be undefined in case of 404 */
125
+ server_route?: CSRRouteServer;
111
126
  data: Array<ServerDataNode | null>;
112
127
  form: Record<string, any> | null;
113
128
  }
@@ -130,6 +130,12 @@ export function get_link_info(a, base, uses_hash_router) {
130
130
 
131
131
  try {
132
132
  url = new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);
133
+
134
+ // if the hash doesn't start with `#/` then it's probably linking to an id on the current page
135
+ if (uses_hash_router && url.hash.match(/^#[^/]/)) {
136
+ const route = location.hash.split('#')[1] || '/';
137
+ url.hash = `#${route}${url.hash}`;
138
+ }
133
139
  } catch {}
134
140
 
135
141
  const target = a instanceof SVGAElement ? a.target.baseVal : a.target;
@@ -0,0 +1,54 @@
1
+ import { base, app_dir } from '__sveltekit/paths';
2
+
3
+ const DATA_SUFFIX = '/__data.json';
4
+ const HTML_DATA_SUFFIX = '.html__data.json';
5
+
6
+ /** @param {string} pathname */
7
+ export function has_data_suffix(pathname) {
8
+ return pathname.endsWith(DATA_SUFFIX) || pathname.endsWith(HTML_DATA_SUFFIX);
9
+ }
10
+
11
+ /** @param {string} pathname */
12
+ export function add_data_suffix(pathname) {
13
+ if (pathname.endsWith('.html')) return pathname.replace(/\.html$/, HTML_DATA_SUFFIX);
14
+ return pathname.replace(/\/$/, '') + DATA_SUFFIX;
15
+ }
16
+
17
+ /** @param {string} pathname */
18
+ export function strip_data_suffix(pathname) {
19
+ if (pathname.endsWith(HTML_DATA_SUFFIX)) {
20
+ return pathname.slice(0, -HTML_DATA_SUFFIX.length) + '.html';
21
+ }
22
+
23
+ return pathname.slice(0, -DATA_SUFFIX.length);
24
+ }
25
+
26
+ const ROUTE_PREFIX = `${base}/${app_dir}/route`;
27
+
28
+ /**
29
+ * @param {string} pathname
30
+ * @returns {boolean}
31
+ */
32
+ export function has_resolution_prefix(pathname) {
33
+ return pathname === `${ROUTE_PREFIX}.js` || pathname.startsWith(`${ROUTE_PREFIX}/`);
34
+ }
35
+
36
+ /**
37
+ * Convert a regular URL to a route to send to SvelteKit's server-side route resolution endpoint
38
+ * @param {string} pathname
39
+ * @returns {string}
40
+ */
41
+ export function add_resolution_prefix(pathname) {
42
+ let normalized = pathname.slice(base.length);
43
+ if (normalized.endsWith('/')) normalized = normalized.slice(0, -1);
44
+
45
+ return `${ROUTE_PREFIX}${normalized}.js`;
46
+ }
47
+
48
+ /**
49
+ * @param {string} pathname
50
+ * @returns {string}
51
+ */
52
+ export function strip_resolution_prefix(pathname) {
53
+ return base + (pathname.slice(ROUTE_PREFIX.length, -3) || '/');
54
+ }
@@ -1,5 +1,6 @@
1
1
  import { parse, serialize } from 'cookie';
2
- import { add_data_suffix, normalize_path, resolve } from '../../utils/url.js';
2
+ import { normalize_path, resolve } from '../../utils/url.js';
3
+ import { add_data_suffix } from '../pathname.js';
3
4
 
4
5
  // eslint-disable-next-line no-control-regex -- control characters are invalid in cookie names
5
6
  const INVALID_COOKIE_CHARACTER_REGEX = /[\x00-\x1F\x7F()<>@,;:"/[\]?={} \t]/;
@@ -1,4 +1,3 @@
1
- import { DEV } from 'esm-env';
2
1
  import { ENDPOINT_METHODS, PAGE_METHODS } from '../../constants.js';
3
2
  import { negotiate } from '../../utils/http.js';
4
3
  import { Redirect } from '../control.js';
@@ -11,10 +10,6 @@ import { method_not_allowed } from './utils.js';
11
10
  * @returns {Promise<Response>}
12
11
  */
13
12
  export async function render_endpoint(event, mod, state) {
14
- if (DEV && event.request.headers.get('x-sveltekit-action') === 'true') {
15
- throw new Error('use:enhance should only be used with SvelteKit form actions');
16
- }
17
-
18
13
  const method = /** @type {import('types').HttpMethod} */ (event.request.method);
19
14
 
20
15
  let handler = mod[method] || mod.fallback;
@@ -112,6 +112,18 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
112
112
  return await fetch(request);
113
113
  }
114
114
 
115
+ if (
116
+ manifest._.prerendered_routes.has(decoded) ||
117
+ (decoded.at(-1) === '/' && manifest._.prerendered_routes.has(decoded.slice(0, -1)))
118
+ ) {
119
+ // The path of something prerendered could match a different route
120
+ // that is still in the manifest, leading to the wrong route being loaded.
121
+ // We therefore bail early here. The prerendered logic is different for
122
+ // each adapter, (except maybe for prerendered redirects)
123
+ // so we need to make an actual HTTP request.
124
+ return await fetch(request);
125
+ }
126
+
115
127
  if (credentials !== 'omit') {
116
128
  const cookie = get_cookie_header(url, request.headers.get('cookie'));
117
129
  if (cookie) {
@@ -1,7 +1,7 @@
1
1
  import { text } from '../../../exports/index.js';
2
2
  import { compact } from '../../../utils/array.js';
3
3
  import { get_status, normalize_error } from '../../../utils/error.js';
4
- import { add_data_suffix } from '../../../utils/url.js';
4
+ import { add_data_suffix } from '../../pathname.js';
5
5
  import { Redirect } from '../../control.js';
6
6
  import { redirect_response, static_error_page, handle_error_and_jsonify } from '../utils.js';
7
7
  import {
@@ -13,6 +13,8 @@ import { text } from '../../../exports/index.js';
13
13
  import { create_async_iterator } from '../../../utils/streaming.js';
14
14
  import { SVELTE_KIT_ASSETS } from '../../../constants.js';
15
15
  import { SCHEME } from '../../../utils/url.js';
16
+ import { create_server_routing_response, generate_route_object } from './server_routing.js';
17
+ import { add_resolution_prefix } from '../../pathname.js';
16
18
 
17
19
  // TODO rename this function/module
18
20
 
@@ -297,8 +299,10 @@ export async function render_response({
297
299
  }
298
300
 
299
301
  if (page_config.csr) {
302
+ const route = manifest._.client.routes?.find((r) => r.id === event.route.id) ?? null;
303
+
300
304
  if (client.uses_env_dynamic_public && state.prerendering) {
301
- modulepreloads.add(`${options.app_dir}/env.js`);
305
+ modulepreloads.add(`${paths.app_dir}/env.js`);
302
306
  }
303
307
 
304
308
  if (!client.inline) {
@@ -317,6 +321,16 @@ export async function render_response({
317
321
  }
318
322
  }
319
323
 
324
+ // prerender a `/_app/route/path/to/page.js` module
325
+ if (manifest._.client.routes && state.prerendering && !state.prerendering.fallback) {
326
+ const pathname = add_resolution_prefix(event.url.pathname);
327
+
328
+ state.prerendering.dependencies.set(
329
+ pathname,
330
+ create_server_routing_response(route, event.params, new URL(pathname, event.url), manifest)
331
+ );
332
+ }
333
+
320
334
  const blocks = [];
321
335
 
322
336
  // when serving a prerendered page in an app that uses $env/dynamic/public, we must
@@ -394,7 +408,15 @@ export async function render_response({
394
408
  hydrate.push(`status: ${status}`);
395
409
  }
396
410
 
397
- if (options.embedded) {
411
+ if (manifest._.client.routes) {
412
+ if (route) {
413
+ const stringified = generate_route_object(route, event.url, manifest).replaceAll(
414
+ '\n',
415
+ '\n\t\t\t\t\t\t\t'
416
+ ); // make output after it's put together with the rest more readable
417
+ hydrate.push(`params: ${devalue.uneval(event.params)}`, `server_route: ${stringified}`);
418
+ }
419
+ } else if (options.embedded) {
398
420
  hydrate.push(`params: ${devalue.uneval(event.params)}`, `route: ${s(event.route)}`);
399
421
  }
400
422
 
@@ -419,7 +441,7 @@ export async function render_response({
419
441
  });`;
420
442
 
421
443
  if (load_env_eagerly) {
422
- blocks.push(`import(${s(`${base}/${options.app_dir}/env.js`)}).then(({ env }) => {
444
+ blocks.push(`import(${s(`${base}/${paths.app_dir}/env.js`)}).then(({ env }) => {
423
445
  ${global}.env = env;
424
446
 
425
447
  ${boot.replace(/\n/g, '\n\t')}
@@ -0,0 +1,110 @@
1
+ import { base, assets } from '__sveltekit/paths';
2
+ import { text } from '../../../exports/index.js';
3
+ import { s } from '../../../utils/misc.js';
4
+ import { exec } from '../../../utils/routing.js';
5
+ import { decode_params } from '../../../utils/url.js';
6
+ import { get_relative_path } from '../../utils.js';
7
+
8
+ /**
9
+ * @param {import('types').SSRClientRoute} route
10
+ * @param {URL} url
11
+ * @param {import('@sveltejs/kit').SSRManifest} manifest
12
+ * @returns {string}
13
+ */
14
+ export function generate_route_object(route, url, manifest) {
15
+ const { errors, layouts, leaf } = route;
16
+
17
+ const nodes = [...errors, ...layouts.map((l) => l?.[1]), leaf[1]]
18
+ .filter((n) => typeof n === 'number')
19
+ .map((n) => `'${n}': () => ${create_client_import(manifest._.client.nodes?.[n], url)}`)
20
+ .join(',\n\t\t');
21
+
22
+ // stringified version of
23
+ /** @type {import('types').CSRRouteServer} */
24
+ return [
25
+ `{\n\tid: ${s(route.id)}`,
26
+ `errors: ${s(route.errors)}`,
27
+ `layouts: ${s(route.layouts)}`,
28
+ `leaf: ${s(route.leaf)}`,
29
+ `nodes: {\n\t\t${nodes}\n\t}\n}`
30
+ ].join(',\n\t');
31
+ }
32
+
33
+ /**
34
+ * @param {string | undefined} import_path
35
+ * @param {URL} url
36
+ */
37
+ function create_client_import(import_path, url) {
38
+ if (!import_path) return 'Promise.resolve({})';
39
+
40
+ // During DEV, Vite will make the paths absolute (e.g. /@fs/...)
41
+ if (import_path[0] === '/') {
42
+ return `import('${import_path}')`;
43
+ }
44
+
45
+ // During PROD, they're root-relative
46
+ if (assets !== '') {
47
+ return `import('${assets}/${import_path}')`;
48
+ }
49
+
50
+ // Else we make them relative to the server-side route resolution request
51
+ // to support IPFS, the internet archive, etc.
52
+ let path = get_relative_path(url.pathname, `${base}/${import_path}`);
53
+ if (path[0] !== '.') path = `./${path}`;
54
+ return `import('${path}')`;
55
+ }
56
+
57
+ /**
58
+ * @param {string} resolved_path
59
+ * @param {URL} url
60
+ * @param {import('@sveltejs/kit').SSRManifest} manifest
61
+ * @returns {Promise<Response>}
62
+ */
63
+ export async function resolve_route(resolved_path, url, manifest) {
64
+ if (!manifest._.client.routes) {
65
+ return text('Server-side route resolution disabled', { status: 400 });
66
+ }
67
+
68
+ /** @type {import('types').SSRClientRoute | null} */
69
+ let route = null;
70
+ /** @type {Record<string, string>} */
71
+ let params = {};
72
+
73
+ const matchers = await manifest._.matchers();
74
+
75
+ for (const candidate of manifest._.client.routes) {
76
+ const match = candidate.pattern.exec(resolved_path);
77
+ if (!match) continue;
78
+
79
+ const matched = exec(match, candidate.params, matchers);
80
+ if (matched) {
81
+ route = candidate;
82
+ params = decode_params(matched);
83
+ break;
84
+ }
85
+ }
86
+
87
+ return create_server_routing_response(route, params, url, manifest).response;
88
+ }
89
+
90
+ /**
91
+ * @param {import('types').SSRClientRoute | null} route
92
+ * @param {Partial<Record<string, string>>} params
93
+ * @param {URL} url
94
+ * @param {import('@sveltejs/kit').SSRManifest} manifest
95
+ * @returns {{response: Response, body: string}}
96
+ */
97
+ export function create_server_routing_response(route, params, url, manifest) {
98
+ const headers = new Headers({
99
+ 'content-type': 'application/javascript; charset=utf-8'
100
+ });
101
+
102
+ if (route) {
103
+ const csr_route = generate_route_object(route, url, manifest);
104
+ const body = `export const route = ${csr_route}; export const params = ${JSON.stringify(params)};`;
105
+
106
+ return { response: text(body, { headers }), body };
107
+ } else {
108
+ return { response: text('', { headers }), body: '' };
109
+ }
110
+ }