@sveltejs/kit 2.17.3 → 2.19.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.
@@ -13,9 +13,7 @@ import {
13
13
  import { load_data, load_server_data } from './load_data.js';
14
14
  import { render_response } from './render.js';
15
15
  import { respond_with_error } from './respond_with_error.js';
16
- import { get_option } from '../../../utils/options.js';
17
16
  import { get_data_json } from '../data/index.js';
18
- import { load_page_nodes } from './load_page_nodes.js';
19
17
  import { DEV } from 'esm-env';
20
18
 
21
19
  /**
@@ -29,10 +27,11 @@ const MAX_DEPTH = 10;
29
27
  * @param {import('types').SSROptions} options
30
28
  * @param {import('@sveltejs/kit').SSRManifest} manifest
31
29
  * @param {import('types').SSRState} state
30
+ * @param {import('../../../utils/page_nodes.js').PageNodes} nodes
32
31
  * @param {import('types').RequiredResolveOptions} resolve_opts
33
32
  * @returns {Promise<Response>}
34
33
  */
35
- export async function render_page(event, page, options, manifest, state, resolve_opts) {
34
+ export async function render_page(event, page, options, manifest, state, nodes, resolve_opts) {
36
35
  if (state.depth > MAX_DEPTH) {
37
36
  // infinite request cycle detected
38
37
  return text(`Not found: ${event.url.pathname}`, {
@@ -46,9 +45,7 @@ export async function render_page(event, page, options, manifest, state, resolve
46
45
  }
47
46
 
48
47
  try {
49
- const nodes = await load_page_nodes(page, manifest);
50
-
51
- const leaf_node = /** @type {import('types').SSRNode} */ (nodes.at(-1));
48
+ const leaf_node = /** @type {import('types').SSRNode} */ (nodes.page());
52
49
 
53
50
  let status = 200;
54
51
 
@@ -70,16 +67,10 @@ export async function render_page(event, page, options, manifest, state, resolve
70
67
  }
71
68
  }
72
69
 
73
- const should_prerender_data = nodes.some(
74
- // prerender in case of trailingSlash because the client retrieves that value from the server
75
- (node) => node?.server?.load || node?.server?.trailingSlash !== undefined
76
- );
77
- const data_pathname = add_data_suffix(event.url.pathname);
78
-
79
70
  // it's crucial that we do this before returning the non-SSR response, otherwise
80
71
  // SvelteKit will erroneously believe that the path has been prerendered,
81
72
  // causing functions to be omitted from the manifest generated later
82
- const should_prerender = get_option(nodes, 'prerender') ?? false;
73
+ const should_prerender = nodes.prerender();
83
74
  if (should_prerender) {
84
75
  const mod = leaf_node.server;
85
76
  if (mod?.actions) {
@@ -96,13 +87,16 @@ export async function render_page(event, page, options, manifest, state, resolve
96
87
  // inherit the prerender option of the page
97
88
  state.prerender_default = should_prerender;
98
89
 
90
+ const should_prerender_data = nodes.should_prerender_data();
91
+ const data_pathname = add_data_suffix(event.url.pathname);
92
+
99
93
  /** @type {import('./types.js').Fetched[]} */
100
94
  const fetched = [];
101
95
 
102
96
  // renders an empty 'shell' page if SSR is turned off and if there is
103
97
  // no server data to prerender. As a result, the load functions and rendering
104
98
  // only occur client-side.
105
- if (get_option(nodes, 'ssr') === false && !(state.prerendering && should_prerender_data)) {
99
+ if (nodes.ssr() === false && !(state.prerendering && should_prerender_data)) {
106
100
  // if the user makes a request through a non-enhanced form, the returned value is lost
107
101
  // because there is no SSR or client-side handling of the response
108
102
  if (DEV && action_result && !event.request.headers.has('x-sveltekit-action')) {
@@ -123,7 +117,7 @@ export async function render_page(event, page, options, manifest, state, resolve
123
117
  fetched,
124
118
  page_config: {
125
119
  ssr: false,
126
- csr: get_option(nodes, 'csr') ?? true
120
+ csr: nodes.csr()
127
121
  },
128
122
  status,
129
123
  error: null,
@@ -142,7 +136,7 @@ export async function render_page(event, page, options, manifest, state, resolve
142
136
  let load_error = null;
143
137
 
144
138
  /** @type {Array<Promise<import('types').ServerDataNode | null>>} */
145
- const server_promises = nodes.map((node, i) => {
139
+ const server_promises = nodes.data.map((node, i) => {
146
140
  if (load_error) {
147
141
  // if an error happens immediately, don't bother with the rest of the nodes
148
142
  throw load_error;
@@ -177,10 +171,10 @@ export async function render_page(event, page, options, manifest, state, resolve
177
171
  });
178
172
  });
179
173
 
180
- const csr = get_option(nodes, 'csr') ?? true;
174
+ const csr = nodes.csr();
181
175
 
182
176
  /** @type {Array<Promise<Record<string, any> | null>>} */
183
- const load_promises = nodes.map((node, i) => {
177
+ const load_promises = nodes.data.map((node, i) => {
184
178
  if (load_error) throw load_error;
185
179
  return Promise.resolve().then(async () => {
186
180
  try {
@@ -211,8 +205,8 @@ export async function render_page(event, page, options, manifest, state, resolve
211
205
  for (const p of server_promises) p.catch(() => {});
212
206
  for (const p of load_promises) p.catch(() => {});
213
207
 
214
- for (let i = 0; i < nodes.length; i += 1) {
215
- const node = nodes[i];
208
+ for (let i = 0; i < nodes.data.length; i += 1) {
209
+ const node = nodes.data[i];
216
210
 
217
211
  if (node) {
218
212
  try {
@@ -300,7 +294,7 @@ export async function render_page(event, page, options, manifest, state, resolve
300
294
  });
301
295
  }
302
296
 
303
- const ssr = get_option(nodes, 'ssr') ?? true;
297
+ const ssr = nodes.ssr();
304
298
 
305
299
  return await render_response({
306
300
  event,
@@ -309,7 +303,7 @@ export async function render_page(event, page, options, manifest, state, resolve
309
303
  state,
310
304
  resolve_opts,
311
305
  page_config: {
312
- csr: get_option(nodes, 'csr') ?? true,
306
+ csr: nodes.csr(),
313
307
  ssr
314
308
  },
315
309
  status,
@@ -78,7 +78,7 @@ export async function load_server_data({ event, state, node, parent }) {
78
78
  const { href } = new URL(dep, event.url);
79
79
 
80
80
  if (DEV) {
81
- validate_depends(node.server_id, dep);
81
+ validate_depends(node.server_id || 'missing route ID', dep);
82
82
 
83
83
  if (done && !uses.dependencies.has(href)) {
84
84
  console.warn(
@@ -116,11 +116,6 @@ export async function render_response({
116
116
  }
117
117
 
118
118
  if (page_config.ssr) {
119
- if (__SVELTEKIT_DEV__ && !branch.at(-1)?.node.component) {
120
- // Can only be the leaf, layouts have a fallback component generated
121
- throw new Error(`Missing +page.svelte component for route ${event.route.id}`);
122
- }
123
-
124
119
  /** @type {Record<string, any>} */
125
120
  const props = {
126
121
  stores: {
@@ -128,7 +123,15 @@ export async function render_response({
128
123
  navigating: writable(null),
129
124
  updated
130
125
  },
131
- constructors: await Promise.all(branch.map(({ node }) => node.component())),
126
+ constructors: await Promise.all(
127
+ branch.map(({ node }) => {
128
+ if (!node.component) {
129
+ // Can only be the leaf, layouts have a fallback component generated
130
+ throw new Error(`Missing +page.svelte component for route ${event.route.id}`);
131
+ }
132
+ return node.component();
133
+ })
134
+ ),
132
135
  form: form_value
133
136
  };
134
137
 
@@ -1,9 +1,9 @@
1
1
  import { render_response } from './render.js';
2
2
  import { load_data, load_server_data } from './load_data.js';
3
3
  import { handle_error_and_jsonify, static_error_page, redirect_response } from '../utils.js';
4
- import { get_option } from '../../../utils/options.js';
5
4
  import { Redirect } from '../../control.js';
6
5
  import { get_status } from '../../../utils/error.js';
6
+ import { PageNodes } from '../../../utils/page_nodes.js';
7
7
 
8
8
  /**
9
9
  * @typedef {import('./types.js').Loaded} Loaded
@@ -40,8 +40,9 @@ export async function respond_with_error({
40
40
  try {
41
41
  const branch = [];
42
42
  const default_layout = await manifest._.nodes[0](); // 0 is always the root layout
43
- const ssr = get_option([default_layout], 'ssr') ?? true;
44
- const csr = get_option([default_layout], 'csr') ?? true;
43
+ const nodes = new PageNodes([default_layout]);
44
+ const ssr = nodes.ssr();
45
+ const csr = nodes.csr();
45
46
 
46
47
  if (ssr) {
47
48
  state.error = true;
@@ -11,21 +11,13 @@ import { exec } from '../../utils/routing.js';
11
11
  import { redirect_json_response, render_data } from './data/index.js';
12
12
  import { add_cookies_to_headers, get_cookies } from './cookie.js';
13
13
  import { create_fetch } from './fetch.js';
14
+ import { PageNodes } from '../../utils/page_nodes.js';
14
15
  import { HttpError, Redirect, SvelteKitError } from '../control.js';
15
- import {
16
- validate_layout_exports,
17
- validate_layout_server_exports,
18
- validate_page_exports,
19
- validate_page_server_exports,
20
- validate_server_exports
21
- } from '../../utils/exports.js';
22
- import { get_option } from '../../utils/options.js';
16
+ import { validate_server_exports } from '../../utils/exports.js';
23
17
  import { json, text } from '../../exports/index.js';
24
18
  import { action_json_redirect, is_action_json_request } from './page/actions.js';
25
19
  import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js';
26
20
  import { get_public_env } from './env_module.js';
27
- import { load_page_nodes } from './page/load_page_nodes.js';
28
- import { get_page_config } from '../../utils/route_config.js';
29
21
  import { resolve_route } from './page/server_routing.js';
30
22
  import { validateHeaders } from './validate-headers.js';
31
23
  import {
@@ -111,11 +103,82 @@ export async function respond(request, options, manifest, state) {
111
103
  url.searchParams.delete(INVALIDATED_PARAM);
112
104
  }
113
105
 
106
+ /** @type {Record<string, string>} */
107
+ const headers = {};
108
+
109
+ const { cookies, new_cookies, get_cookie_header, set_internal, set_trailing_slash } = get_cookies(
110
+ request,
111
+ url
112
+ );
113
+
114
+ /** @type {import('@sveltejs/kit').RequestEvent} */
115
+ const event = {
116
+ cookies,
117
+ // @ts-expect-error `fetch` needs to be created after the `event` itself
118
+ fetch: null,
119
+ getClientAddress:
120
+ state.getClientAddress ||
121
+ (() => {
122
+ throw new Error(
123
+ `${__SVELTEKIT_ADAPTER_NAME__} does not specify getClientAddress. Please raise an issue`
124
+ );
125
+ }),
126
+ locals: {},
127
+ params: {},
128
+ platform: state.platform,
129
+ request,
130
+ route: { id: null },
131
+ setHeaders: (new_headers) => {
132
+ if (__SVELTEKIT_DEV__) {
133
+ validateHeaders(new_headers);
134
+ }
135
+
136
+ for (const key in new_headers) {
137
+ const lower = key.toLowerCase();
138
+ const value = new_headers[key];
139
+
140
+ if (lower === 'set-cookie') {
141
+ throw new Error(
142
+ 'Use `event.cookies.set(name, value, options)` instead of `event.setHeaders` to set cookies'
143
+ );
144
+ } else if (lower in headers) {
145
+ throw new Error(`"${key}" header is already set`);
146
+ } else {
147
+ headers[lower] = value;
148
+
149
+ if (state.prerendering && lower === 'cache-control') {
150
+ state.prerendering.cache = /** @type {string} */ (value);
151
+ }
152
+ }
153
+ }
154
+ },
155
+ url,
156
+ isDataRequest: is_data_request,
157
+ isSubRequest: state.depth > 0
158
+ };
159
+
160
+ event.fetch = create_fetch({
161
+ event,
162
+ options,
163
+ manifest,
164
+ state,
165
+ get_cookie_header,
166
+ set_internal
167
+ });
168
+
169
+ if (state.emulator?.platform) {
170
+ event.platform = await state.emulator.platform({
171
+ config: {},
172
+ prerender: !!state.prerendering?.fallback
173
+ });
174
+ }
175
+
114
176
  let resolved_path;
115
177
 
116
178
  try {
117
179
  // reroute could alter the given URL, so we pass a copy
118
- resolved_path = options.hooks.reroute({ url: new URL(url) }) ?? url.pathname;
180
+ resolved_path =
181
+ (await options.hooks.reroute({ url: new URL(url), fetch: event.fetch })) ?? url.pathname;
119
182
  } catch {
120
183
  return text('Internal Server Error', {
121
184
  status: 500
@@ -131,9 +194,6 @@ export async function respond(request, options, manifest, state) {
131
194
  /** @type {import('types').SSRRoute | null} */
132
195
  let route = null;
133
196
 
134
- /** @type {Record<string, string>} */
135
- let params = {};
136
-
137
197
  if (base && !state.prerendering?.fallback) {
138
198
  if (!resolved_path.startsWith(base)) {
139
199
  return text('Not found', { status: 404 });
@@ -167,68 +227,13 @@ export async function respond(request, options, manifest, state) {
167
227
  const matched = exec(match, candidate.params, matchers);
168
228
  if (matched) {
169
229
  route = candidate;
170
- params = decode_params(matched);
230
+ event.route = { id: route.id };
231
+ event.params = decode_params(matched);
171
232
  break;
172
233
  }
173
234
  }
174
235
  }
175
236
 
176
- /** @type {import('types').TrailingSlash | void} */
177
- let trailing_slash = undefined;
178
-
179
- /** @type {Record<string, string>} */
180
- const headers = {};
181
-
182
- /** @type {Record<string, import('./page/types.js').Cookie>} */
183
- let cookies_to_add = {};
184
-
185
- /** @type {import('@sveltejs/kit').RequestEvent} */
186
- const event = {
187
- // @ts-expect-error `cookies` and `fetch` need to be created after the `event` itself
188
- cookies: null,
189
- // @ts-expect-error
190
- fetch: null,
191
- getClientAddress:
192
- state.getClientAddress ||
193
- (() => {
194
- throw new Error(
195
- `${__SVELTEKIT_ADAPTER_NAME__} does not specify getClientAddress. Please raise an issue`
196
- );
197
- }),
198
- locals: {},
199
- params,
200
- platform: state.platform,
201
- request,
202
- route: { id: route?.id ?? null },
203
- setHeaders: (new_headers) => {
204
- if (__SVELTEKIT_DEV__) {
205
- validateHeaders(new_headers);
206
- }
207
-
208
- for (const key in new_headers) {
209
- const lower = key.toLowerCase();
210
- const value = new_headers[key];
211
-
212
- if (lower === 'set-cookie') {
213
- throw new Error(
214
- 'Use `event.cookies.set(name, value, options)` instead of `event.setHeaders` to set cookies'
215
- );
216
- } else if (lower in headers) {
217
- throw new Error(`"${key}" header is already set`);
218
- } else {
219
- headers[lower] = value;
220
-
221
- if (state.prerendering && lower === 'cache-control') {
222
- state.prerendering.cache = /** @type {string} */ (value);
223
- }
224
- }
225
- }
226
- },
227
- url,
228
- isDataRequest: is_data_request,
229
- isSubRequest: state.depth > 0
230
- };
231
-
232
237
  /** @type {import('types').RequiredResolveOptions} */
233
238
  let resolve_opts = {
234
239
  transformPageChunk: default_transform,
@@ -236,51 +241,36 @@ export async function respond(request, options, manifest, state) {
236
241
  preload: default_preload
237
242
  };
238
243
 
244
+ /** @type {import('types').TrailingSlash} */
245
+ let trailing_slash = 'never';
246
+
239
247
  try {
248
+ /** @type {PageNodes|undefined} */
249
+ const page_nodes = route?.page
250
+ ? new PageNodes(await load_page_nodes(route.page, manifest))
251
+ : undefined;
252
+
240
253
  // determine whether we need to redirect to add/remove a trailing slash
241
254
  if (route) {
242
255
  // if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`,
243
256
  // regardless of the `trailingSlash` route option
244
257
  if (url.pathname === base || url.pathname === base + '/') {
245
258
  trailing_slash = 'always';
246
- } else if (route.page) {
247
- const nodes = await load_page_nodes(route.page, manifest);
248
-
259
+ } else if (page_nodes) {
249
260
  if (DEV) {
250
- const layouts = nodes.slice(0, -1);
251
- const page = nodes.at(-1);
252
-
253
- for (const layout of layouts) {
254
- if (layout) {
255
- validate_layout_server_exports(
256
- layout.server,
257
- /** @type {string} */ (layout.server_id)
258
- );
259
- validate_layout_exports(
260
- layout.universal,
261
- /** @type {string} */ (layout.universal_id)
262
- );
263
- }
264
- }
265
-
266
- if (page) {
267
- validate_page_server_exports(page.server, /** @type {string} */ (page.server_id));
268
- validate_page_exports(page.universal, /** @type {string} */ (page.universal_id));
269
- }
261
+ page_nodes.validate();
270
262
  }
271
-
272
- trailing_slash = get_option(nodes, 'trailingSlash');
263
+ trailing_slash = page_nodes.trailing_slash();
273
264
  } else if (route.endpoint) {
274
265
  const node = await route.endpoint();
275
- trailing_slash = node.trailingSlash;
276
-
266
+ trailing_slash = node.trailingSlash ?? 'never';
277
267
  if (DEV) {
278
268
  validate_server_exports(node, /** @type {string} */ (route.endpoint_id));
279
269
  }
280
270
  }
281
271
 
282
272
  if (!is_data_request) {
283
- const normalized = normalize_path(url.pathname, trailing_slash ?? 'never');
273
+ const normalized = normalize_path(url.pathname, trailing_slash);
284
274
 
285
275
  if (normalized !== url.pathname && !state.prerendering?.fallback) {
286
276
  return new Response(undefined, {
@@ -306,10 +296,9 @@ export async function respond(request, options, manifest, state) {
306
296
  const node = await route.endpoint();
307
297
  config = node.config ?? config;
308
298
  prerender = node.prerender ?? prerender;
309
- } else if (route.page) {
310
- const nodes = await load_page_nodes(route.page, manifest);
311
- config = get_page_config(nodes) ?? config;
312
- prerender = get_option(nodes, 'prerender') ?? false;
299
+ } else if (page_nodes) {
300
+ config = page_nodes.get_config() ?? config;
301
+ prerender = page_nodes.prerender();
313
302
  }
314
303
 
315
304
  if (state.before_handle) {
@@ -320,36 +309,16 @@ export async function respond(request, options, manifest, state) {
320
309
  event.platform = await state.emulator.platform({ config, prerender });
321
310
  }
322
311
  }
323
- } else if (state.emulator?.platform) {
324
- event.platform = await state.emulator.platform({
325
- config: {},
326
- prerender: !!state.prerendering?.fallback
327
- });
328
312
  }
329
313
 
330
- const { cookies, new_cookies, get_cookie_header, set_internal } = get_cookies(
331
- request,
332
- url,
333
- trailing_slash ?? 'never'
334
- );
335
-
336
- cookies_to_add = new_cookies;
337
- event.cookies = cookies;
338
- event.fetch = create_fetch({
339
- event,
340
- options,
341
- manifest,
342
- state,
343
- get_cookie_header,
344
- set_internal
345
- });
314
+ set_trailing_slash(trailing_slash);
346
315
 
347
316
  if (state.prerendering && !state.prerendering.fallback) disable_search(url);
348
317
 
349
318
  const response = await options.hooks.handle({
350
319
  event,
351
320
  resolve: (event, opts) =>
352
- resolve(event, opts).then((response) => {
321
+ resolve(event, page_nodes, opts).then((response) => {
353
322
  // add headers/cookies here, rather than inside `resolve`, so that we
354
323
  // can do it once for all responses instead of once per `return`
355
324
  for (const key in headers) {
@@ -357,7 +326,7 @@ export async function respond(request, options, manifest, state) {
357
326
  response.headers.set(key, /** @type {string} */ (value));
358
327
  }
359
328
 
360
- add_cookies_to_headers(response.headers, Object.values(cookies_to_add));
329
+ add_cookies_to_headers(response.headers, Object.values(new_cookies));
361
330
 
362
331
  if (state.prerendering && event.route.id !== null) {
363
332
  response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id));
@@ -418,7 +387,7 @@ export async function respond(request, options, manifest, state) {
418
387
  : route?.page && is_action_json_request(event)
419
388
  ? action_json_redirect(e)
420
389
  : redirect_response(e.status, e.location);
421
- add_cookies_to_headers(response.headers, Object.values(cookies_to_add));
390
+ add_cookies_to_headers(response.headers, Object.values(new_cookies));
422
391
  return response;
423
392
  }
424
393
  return await handle_fatal_error(event, options, e);
@@ -426,9 +395,10 @@ export async function respond(request, options, manifest, state) {
426
395
 
427
396
  /**
428
397
  * @param {import('@sveltejs/kit').RequestEvent} event
398
+ * @param {PageNodes | undefined} page_nodes
429
399
  * @param {import('@sveltejs/kit').ResolveOptions} [opts]
430
400
  */
431
- async function resolve(event, opts) {
401
+ async function resolve(event, page_nodes, opts) {
432
402
  try {
433
403
  if (opts) {
434
404
  resolve_opts = {
@@ -467,13 +437,23 @@ export async function respond(request, options, manifest, state) {
467
437
  manifest,
468
438
  state,
469
439
  invalidated_data_nodes,
470
- trailing_slash ?? 'never'
440
+ trailing_slash
471
441
  );
472
442
  } else if (route.endpoint && (!route.page || is_endpoint_request(event))) {
473
443
  response = await render_endpoint(event, await route.endpoint(), state);
474
444
  } else if (route.page) {
475
- if (page_methods.has(method)) {
476
- response = await render_page(event, route.page, options, manifest, state, resolve_opts);
445
+ if (!page_nodes) {
446
+ throw new Error('page_nodes not found. This should never happen');
447
+ } else if (page_methods.has(method)) {
448
+ response = await render_page(
449
+ event,
450
+ route.page,
451
+ options,
452
+ manifest,
453
+ state,
454
+ page_nodes,
455
+ resolve_opts
456
+ );
477
457
  } else {
478
458
  const allowed_methods = new Set(allowed_page_methods);
479
459
  const node = await manifest._.nodes[route.page.leaf]();
@@ -499,9 +479,8 @@ export async function respond(request, options, manifest, state) {
499
479
  }
500
480
  }
501
481
  } else {
502
- // a route will always have a page or an endpoint, but TypeScript
503
- // doesn't know that
504
- throw new Error('This should never happen');
482
+ // a route will always have a page or an endpoint, but TypeScript doesn't know that
483
+ throw new Error('Route is neither page nor endpoint. This should never happen');
505
484
  }
506
485
 
507
486
  // If the route contains a page and an endpoint, we need to add a
@@ -578,3 +557,15 @@ export async function respond(request, options, manifest, state) {
578
557
  }
579
558
  }
580
559
  }
560
+
561
+ /**
562
+ * @param {import('types').PageNodeIndexes} page
563
+ * @param {import('@sveltejs/kit').SSRManifest} manifest
564
+ */
565
+ export function load_page_nodes(page, manifest) {
566
+ return Promise.all([
567
+ // we use == here rather than === because [undefined] serializes as "[null]"
568
+ ...page.layouts.map((n) => (n == undefined ? n : manifest._.nodes[n]())),
569
+ manifest._.nodes[page.leaf]()
570
+ ]);
571
+ }