@sveltejs/kit 2.26.0 → 2.27.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 (56) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -2
  3. package/src/core/adapt/builder.js +6 -1
  4. package/src/core/config/options.js +4 -0
  5. package/src/core/generate_manifest/index.js +3 -0
  6. package/src/core/postbuild/analyse.js +25 -1
  7. package/src/core/postbuild/fallback.js +2 -1
  8. package/src/core/postbuild/prerender.js +41 -10
  9. package/src/core/sync/create_manifest_data/index.js +35 -1
  10. package/src/core/sync/write_server.js +4 -2
  11. package/src/core/sync/write_types/index.js +12 -5
  12. package/src/exports/index.js +1 -1
  13. package/src/exports/internal/index.js +3 -1
  14. package/src/exports/internal/remote-functions.js +21 -0
  15. package/src/exports/public.d.ts +162 -2
  16. package/src/exports/vite/build/build_remote.js +129 -0
  17. package/src/exports/vite/dev/index.js +7 -0
  18. package/src/exports/vite/index.js +123 -8
  19. package/src/exports/vite/module_ids.js +3 -2
  20. package/src/exports/vite/preview/index.js +3 -1
  21. package/src/runtime/app/navigation.js +1 -0
  22. package/src/runtime/app/server/index.js +2 -0
  23. package/src/runtime/app/server/remote/command.js +91 -0
  24. package/src/runtime/app/server/remote/form.js +124 -0
  25. package/src/runtime/app/server/remote/index.js +4 -0
  26. package/src/runtime/app/server/remote/prerender.js +163 -0
  27. package/src/runtime/app/server/remote/query.js +115 -0
  28. package/src/runtime/app/server/remote/shared.js +153 -0
  29. package/src/runtime/client/client.js +107 -39
  30. package/src/runtime/client/fetcher.js +1 -1
  31. package/src/runtime/client/remote-functions/command.js +71 -0
  32. package/src/runtime/client/remote-functions/form.svelte.js +312 -0
  33. package/src/runtime/client/remote-functions/index.js +4 -0
  34. package/src/runtime/client/remote-functions/prerender.svelte.js +166 -0
  35. package/src/runtime/client/remote-functions/query.svelte.js +219 -0
  36. package/src/runtime/client/remote-functions/shared.svelte.js +143 -0
  37. package/src/runtime/client/types.d.ts +2 -0
  38. package/src/runtime/server/data/index.js +6 -4
  39. package/src/runtime/server/event-state.js +41 -0
  40. package/src/runtime/server/index.js +12 -3
  41. package/src/runtime/server/page/actions.js +1 -1
  42. package/src/runtime/server/page/index.js +10 -3
  43. package/src/runtime/server/page/load_data.js +18 -12
  44. package/src/runtime/server/page/render.js +31 -5
  45. package/src/runtime/server/page/serialize_data.js +1 -1
  46. package/src/runtime/server/remote.js +237 -0
  47. package/src/runtime/server/respond.js +57 -36
  48. package/src/runtime/shared.js +61 -0
  49. package/src/types/global-private.d.ts +2 -0
  50. package/src/types/internal.d.ts +51 -4
  51. package/src/types/synthetic/$env+static+private.md +1 -1
  52. package/src/utils/routing.js +1 -1
  53. package/src/version.js +1 -1
  54. package/types/index.d.ts +266 -3
  55. package/types/index.d.ts.map +14 -1
  56. /package/src/{runtime → utils}/hash.js +0 -0
@@ -0,0 +1,237 @@
1
+ /** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest } from '@sveltejs/kit' */
2
+ /** @import { RemoteFunctionResponse, RemoteInfo, SSROptions } from 'types' */
3
+
4
+ import { json, error } from '@sveltejs/kit';
5
+ import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
6
+ import { app_dir, base } from '__sveltekit/paths';
7
+ import { with_event } from '../app/server/event.js';
8
+ import { is_form_content_type } from '../../utils/http.js';
9
+ import { parse_remote_arg, stringify } from '../shared.js';
10
+ import { handle_error_and_jsonify } from './utils.js';
11
+ import { normalize_error } from '../../utils/error.js';
12
+ import { check_incorrect_fail_use } from './page/actions.js';
13
+ import { DEV } from 'esm-env';
14
+ import { get_event_state } from './event-state.js';
15
+
16
+ /**
17
+ * @param {RequestEvent} event
18
+ * @param {SSROptions} options
19
+ * @param {SSRManifest} manifest
20
+ * @param {string} id
21
+ */
22
+ export async function handle_remote_call(event, options, manifest, id) {
23
+ const [hash, name, prerender_args] = id.split('/');
24
+ const remotes = manifest._.remotes;
25
+
26
+ if (!remotes[hash]) error(404);
27
+
28
+ const module = await remotes[hash]();
29
+ const fn = module[name];
30
+
31
+ if (!fn) error(404);
32
+
33
+ /** @type {RemoteInfo} */
34
+ const info = fn.__;
35
+ const transport = options.hooks.transport;
36
+
37
+ /** @type {string[] | undefined} */
38
+ let form_client_refreshes;
39
+
40
+ try {
41
+ if (info.type === 'form') {
42
+ if (!is_form_content_type(event.request)) {
43
+ throw new SvelteKitError(
44
+ 415,
45
+ 'Unsupported Media Type',
46
+ `Form actions expect form-encoded data — received ${event.request.headers.get(
47
+ 'content-type'
48
+ )}`
49
+ );
50
+ }
51
+
52
+ const form_data = await event.request.formData();
53
+ form_client_refreshes = JSON.parse(
54
+ /** @type {string} */ (form_data.get('sveltekit:remote_refreshes')) ?? '[]'
55
+ );
56
+ form_data.delete('sveltekit:remote_refreshes');
57
+
58
+ const fn = info.fn;
59
+ const data = await with_event(event, () => fn(form_data));
60
+
61
+ return json(
62
+ /** @type {RemoteFunctionResponse} */ ({
63
+ type: 'result',
64
+ result: stringify(data, transport),
65
+ refreshes: stringify(
66
+ {
67
+ ...get_event_state(event).refreshes,
68
+ ...(await apply_client_refreshes(/** @type {string[]} */ (form_client_refreshes)))
69
+ },
70
+ transport
71
+ )
72
+ })
73
+ );
74
+ }
75
+
76
+ if (info.type === 'command') {
77
+ /** @type {{ payload: string, refreshes: string[] }} */
78
+ const { payload, refreshes } = await event.request.json();
79
+ const arg = parse_remote_arg(payload, transport);
80
+ const data = await with_event(event, () => fn(arg));
81
+ const refreshed = await apply_client_refreshes(refreshes);
82
+
83
+ return json(
84
+ /** @type {RemoteFunctionResponse} */ ({
85
+ type: 'result',
86
+ result: stringify(data, transport),
87
+ refreshes: stringify({ ...get_event_state(event).refreshes, ...refreshed }, transport)
88
+ })
89
+ );
90
+ }
91
+
92
+ const payload =
93
+ info.type === 'prerender'
94
+ ? prerender_args
95
+ : /** @type {string} */ (
96
+ // new URL(...) necessary because we're hiding the URL from the user in the event object
97
+ new URL(event.request.url).searchParams.get('payload')
98
+ );
99
+
100
+ const data = await with_event(event, () => fn(parse_remote_arg(payload, transport)));
101
+
102
+ return json(
103
+ /** @type {RemoteFunctionResponse} */ ({
104
+ type: 'result',
105
+ result: stringify(data, transport)
106
+ })
107
+ );
108
+ } catch (error) {
109
+ if (error instanceof Redirect) {
110
+ const refreshes = {
111
+ ...(get_event_state(event).refreshes ?? {}), // could be set by form actions
112
+ ...(await apply_client_refreshes(form_client_refreshes ?? []))
113
+ };
114
+ return json({
115
+ type: 'redirect',
116
+ location: error.location,
117
+ refreshes: Object.keys(refreshes).length > 0 ? stringify(refreshes, transport) : undefined
118
+ });
119
+ }
120
+
121
+ return json(
122
+ /** @type {RemoteFunctionResponse} */ ({
123
+ type: 'error',
124
+ error: await handle_error_and_jsonify(event, options, error),
125
+ status: error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500
126
+ }),
127
+ {
128
+ headers: {
129
+ 'cache-control': 'private, no-store'
130
+ }
131
+ }
132
+ );
133
+ }
134
+
135
+ /** @param {string[]} refreshes */
136
+ async function apply_client_refreshes(refreshes) {
137
+ return Object.fromEntries(
138
+ await Promise.all(
139
+ refreshes.map(async (key) => {
140
+ const [hash, name, payload] = key.split('/');
141
+ const loader = manifest._.remotes[hash];
142
+
143
+ // TODO what do we do in this case? erroring after the mutation has happened is not great
144
+ if (!loader) error(400, 'Bad Request');
145
+
146
+ const module = await loader();
147
+ const fn = module[name];
148
+
149
+ if (!fn) error(400, 'Bad Request');
150
+
151
+ return [key, await with_event(event, () => fn(parse_remote_arg(payload, transport)))];
152
+ })
153
+ )
154
+ );
155
+ }
156
+ }
157
+
158
+ /**
159
+ * @param {RequestEvent} event
160
+ * @param {SSRManifest} manifest
161
+ * @param {string} id
162
+ * @returns {Promise<ActionResult>}
163
+ */
164
+ export async function handle_remote_form_post(event, manifest, id) {
165
+ const [hash, name, action_id] = id.split('/');
166
+ const remotes = manifest._.remotes;
167
+ const module = await remotes[hash]?.();
168
+
169
+ let form = /** @type {RemoteForm<any>} */ (module?.[name]);
170
+
171
+ if (!form) {
172
+ event.setHeaders({
173
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
174
+ // "The server must generate an Allow header field in a 405 status code response"
175
+ allow: 'GET'
176
+ });
177
+ return {
178
+ type: 'error',
179
+ error: new SvelteKitError(
180
+ 405,
181
+ 'Method Not Allowed',
182
+ `POST method not allowed. No form actions exist for ${DEV ? `the page at ${event.route.id}` : 'this page'}`
183
+ )
184
+ };
185
+ }
186
+
187
+ if (action_id) {
188
+ // @ts-expect-error
189
+ form = with_event(event, () => form.for(JSON.parse(action_id)));
190
+ }
191
+
192
+ try {
193
+ const form_data = await event.request.formData();
194
+ const fn = /** @type {RemoteInfo & { type: 'form' }} */ (/** @type {any} */ (form).__).fn;
195
+
196
+ await with_event(event, () => fn(form_data));
197
+
198
+ // We don't want the data to appear on `let { form } = $props()`, which is why we're not returning it.
199
+ // It is instead available on `myForm.result`, setting of which happens within the remote `form` function.
200
+ return {
201
+ type: 'success',
202
+ status: 200
203
+ };
204
+ } catch (e) {
205
+ const err = normalize_error(e);
206
+
207
+ if (err instanceof Redirect) {
208
+ return {
209
+ type: 'redirect',
210
+ status: err.status,
211
+ location: err.location
212
+ };
213
+ }
214
+
215
+ return {
216
+ type: 'error',
217
+ error: check_incorrect_fail_use(err)
218
+ };
219
+ }
220
+ }
221
+
222
+ /**
223
+ * @param {URL} url
224
+ */
225
+ export function get_remote_id(url) {
226
+ return (
227
+ url.pathname.startsWith(`${base}/${app_dir}/remote/`) &&
228
+ url.pathname.replace(`${base}/${app_dir}/remote/`, '')
229
+ );
230
+ }
231
+
232
+ /**
233
+ * @param {URL} url
234
+ */
235
+ export function get_remote_action(url) {
236
+ return url.searchParams.get('/remote');
237
+ }
@@ -1,6 +1,6 @@
1
1
  import { DEV } from 'esm-env';
2
2
  import { json, text } from '@sveltejs/kit';
3
- import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
3
+ import { Redirect, SvelteKitError } from '@sveltejs/kit/internal';
4
4
  import { base, app_dir } from '__sveltekit/paths';
5
5
  import { is_endpoint_request, render_endpoint } from './endpoint.js';
6
6
  import { render_page } from './page/index.js';
@@ -33,7 +33,9 @@ import {
33
33
  strip_data_suffix,
34
34
  strip_resolution_suffix
35
35
  } from '../pathname.js';
36
+ import { get_remote_id, handle_remote_call } from './remote.js';
36
37
  import { with_event } from '../app/server/event.js';
38
+ import { create_event_state, EVENT_STATE } from './event-state.js';
37
39
 
38
40
  /* global __SVELTEKIT_ADAPTER_NAME__ */
39
41
  /* global __SVELTEKIT_DEV__ */
@@ -64,24 +66,35 @@ export async function respond(request, options, manifest, state) {
64
66
  /** URL but stripped from the potential `/__data.json` suffix and its search param */
65
67
  const url = new URL(request.url);
66
68
 
67
- if (options.csrf_check_origin) {
69
+ const is_route_resolution_request = has_resolution_suffix(url.pathname);
70
+ const is_data_request = has_data_suffix(url.pathname);
71
+ const remote_id = get_remote_id(url);
72
+
73
+ if (options.csrf_check_origin && request.headers.get('origin') !== url.origin) {
74
+ const opts = { status: 403 };
75
+
76
+ if (remote_id && request.method !== 'GET') {
77
+ return json(
78
+ {
79
+ message: 'Cross-site remote requests are forbidden'
80
+ },
81
+ opts
82
+ );
83
+ }
84
+
68
85
  const forbidden =
69
86
  is_form_content_type(request) &&
70
87
  (request.method === 'POST' ||
71
88
  request.method === 'PUT' ||
72
89
  request.method === 'PATCH' ||
73
- request.method === 'DELETE') &&
74
- request.headers.get('origin') !== url.origin;
90
+ request.method === 'DELETE');
75
91
 
76
92
  if (forbidden) {
77
- const csrf_error = new HttpError(
78
- 403,
79
- `Cross-site ${request.method} form submissions are forbidden`
80
- );
93
+ const message = `Cross-site ${request.method} form submissions are forbidden`;
81
94
  if (request.headers.get('accept') === 'application/json') {
82
- return json(csrf_error.body, { status: csrf_error.status });
95
+ return json({ message }, opts);
83
96
  }
84
- return text(csrf_error.body.message, { status: csrf_error.status });
97
+ return text(message, opts);
85
98
  }
86
99
  }
87
100
 
@@ -92,14 +105,11 @@ export async function respond(request, options, manifest, state) {
92
105
  /** @type {boolean[] | undefined} */
93
106
  let invalidated_data_nodes;
94
107
 
95
- /**
96
- * If the request is for a route resolution, first modify the URL, then continue as normal
97
- * for path resolution, then return the route object as a JS file.
98
- */
99
- const is_route_resolution_request = has_resolution_suffix(url.pathname);
100
- const is_data_request = has_data_suffix(url.pathname);
101
-
102
108
  if (is_route_resolution_request) {
109
+ /**
110
+ * If the request is for a route resolution, first modify the URL, then continue as normal
111
+ * for path resolution, then return the route object as a JS file.
112
+ */
103
113
  url.pathname = strip_resolution_suffix(url.pathname);
104
114
  } else if (is_data_request) {
105
115
  url.pathname =
@@ -111,6 +121,9 @@ export async function respond(request, options, manifest, state) {
111
121
  ?.split('')
112
122
  .map((node) => node === '1');
113
123
  url.searchParams.delete(INVALIDATED_PARAM);
124
+ } else if (remote_id) {
125
+ url.pathname = base;
126
+ url.search = '';
114
127
  }
115
128
 
116
129
  /** @type {Record<string, string>} */
@@ -123,6 +136,7 @@ export async function respond(request, options, manifest, state) {
123
136
 
124
137
  /** @type {import('@sveltejs/kit').RequestEvent} */
125
138
  const event = {
139
+ [EVENT_STATE]: create_event_state(state, options),
126
140
  cookies,
127
141
  // @ts-expect-error `fetch` needs to be created after the `event` itself
128
142
  fetch: null,
@@ -164,7 +178,8 @@ export async function respond(request, options, manifest, state) {
164
178
  },
165
179
  url,
166
180
  isDataRequest: is_data_request,
167
- isSubRequest: state.depth > 0
181
+ isSubRequest: state.depth > 0,
182
+ isRemoteRequest: !!remote_id
168
183
  };
169
184
 
170
185
  event.fetch = create_fetch({
@@ -183,23 +198,25 @@ export async function respond(request, options, manifest, state) {
183
198
  });
184
199
  }
185
200
 
186
- let resolved_path;
187
-
188
- const prerendering_reroute_state = state.prerendering?.inside_reroute;
189
- try {
190
- // For the duration or a reroute, disable the prerendering state as reroute could call API endpoints
191
- // which would end up in the wrong logic path if not disabled.
192
- if (state.prerendering) state.prerendering.inside_reroute = true;
201
+ let resolved_path = url.pathname;
193
202
 
194
- // reroute could alter the given URL, so we pass a copy
195
- resolved_path =
196
- (await options.hooks.reroute({ url: new URL(url), fetch: event.fetch })) ?? url.pathname;
197
- } catch {
198
- return text('Internal Server Error', {
199
- status: 500
200
- });
201
- } finally {
202
- if (state.prerendering) state.prerendering.inside_reroute = prerendering_reroute_state;
203
+ if (!remote_id) {
204
+ const prerendering_reroute_state = state.prerendering?.inside_reroute;
205
+ try {
206
+ // For the duration or a reroute, disable the prerendering state as reroute could call API endpoints
207
+ // which would end up in the wrong logic path if not disabled.
208
+ if (state.prerendering) state.prerendering.inside_reroute = true;
209
+
210
+ // reroute could alter the given URL, so we pass a copy
211
+ resolved_path =
212
+ (await options.hooks.reroute({ url: new URL(url), fetch: event.fetch })) ?? url.pathname;
213
+ } catch {
214
+ return text('Internal Server Error', {
215
+ status: 500
216
+ });
217
+ } finally {
218
+ if (state.prerendering) state.prerendering.inside_reroute = prerendering_reroute_state;
219
+ }
203
220
  }
204
221
 
205
222
  try {
@@ -254,14 +271,14 @@ export async function respond(request, options, manifest, state) {
254
271
  return get_public_env(request);
255
272
  }
256
273
 
257
- if (resolved_path.startsWith(`/${app_dir}`)) {
274
+ if (!remote_id && resolved_path.startsWith(`/${app_dir}`)) {
258
275
  // Ensure that 404'd static assets are not cached - some adapters might apply caching by default
259
276
  const headers = new Headers();
260
277
  headers.set('cache-control', 'public, max-age=0, must-revalidate');
261
278
  return text('Not found', { status: 404, headers });
262
279
  }
263
280
 
264
- if (!state.prerendering?.fallback) {
281
+ if (!state.prerendering?.fallback && !remote_id) {
265
282
  // TODO this could theoretically break — should probably be inside a try-catch
266
283
  const matchers = await manifest._.matchers();
267
284
 
@@ -476,6 +493,10 @@ export async function respond(request, options, manifest, state) {
476
493
  });
477
494
  }
478
495
 
496
+ if (remote_id) {
497
+ return await handle_remote_call(event, options, manifest, remote_id);
498
+ }
499
+
479
500
  if (route) {
480
501
  const method = /** @type {import('types').HttpMethod} */ (event.request.method);
481
502
 
@@ -1,3 +1,6 @@
1
+ /** @import { Transport } from '@sveltejs/kit' */
2
+ import * as devalue from 'devalue';
3
+
1
4
  /**
2
5
  * @param {string} route_id
3
6
  * @param {string} dep
@@ -14,3 +17,61 @@ export function validate_depends(route_id, dep) {
14
17
  export const INVALIDATED_PARAM = 'x-sveltekit-invalidated';
15
18
 
16
19
  export const TRAILING_SLASH_PARAM = 'x-sveltekit-trailing-slash';
20
+
21
+ /**
22
+ * Try to `devalue.stringify` the data object using the provided transport encoders.
23
+ * @param {any} data
24
+ * @param {Transport} transport
25
+ */
26
+ export function stringify(data, transport) {
27
+ const encoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.encode]));
28
+
29
+ return devalue.stringify(data, encoders);
30
+ }
31
+
32
+ /**
33
+ * Stringifies the argument (if any) for a remote function in such a way that
34
+ * it is both a valid URL and a valid file name (necessary for prerendering).
35
+ * @param {any} value
36
+ * @param {Transport} transport
37
+ */
38
+ export function stringify_remote_arg(value, transport) {
39
+ if (value === undefined) return '';
40
+
41
+ // If people hit file/url size limits, we can look into using something like compress_and_encode_text from svelte.dev beyond a certain size
42
+ const json_string = stringify(value, transport);
43
+
44
+ // Convert to UTF-8 bytes, then base64 - handles all Unicode properly (btoa would fail on exotic characters)
45
+ const utf8_bytes = new TextEncoder().encode(json_string);
46
+ return btoa(String.fromCharCode(...utf8_bytes))
47
+ .replace(/=/g, '')
48
+ .replace(/\+/g, '-')
49
+ .replace(/\//g, '_');
50
+ }
51
+
52
+ /**
53
+ * Parses the argument (if any) for a remote function
54
+ * @param {string} string
55
+ * @param {Transport} transport
56
+ */
57
+ export function parse_remote_arg(string, transport) {
58
+ if (!string) return undefined;
59
+
60
+ const decoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.decode]));
61
+
62
+ // We don't need to add back the `=`-padding because atob can handle it
63
+ const base64_restored = string.replace(/-/g, '+').replace(/_/g, '/');
64
+ const binary_string = atob(base64_restored);
65
+ const utf8_bytes = new Uint8Array([...binary_string].map((char) => char.charCodeAt(0)));
66
+ const json_string = new TextDecoder().decode(utf8_bytes);
67
+
68
+ return devalue.parse(json_string, decoders);
69
+ }
70
+
71
+ /**
72
+ * @param {string} id
73
+ * @param {string} payload
74
+ */
75
+ export function create_remote_cache_key(id, payload) {
76
+ return id + '/' + payload;
77
+ }
@@ -4,6 +4,8 @@ declare global {
4
4
  const __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: number;
5
5
  const __SVELTEKIT_DEV__: boolean;
6
6
  const __SVELTEKIT_EMBEDDED__: boolean;
7
+ /** true if corresponding config option is set to true */
8
+ const __SVELTEKIT_EXPERIMENTAL__REMOTE_FUNCTIONS__: boolean;
7
9
  /** True if `config.kit.router.resolution === 'client'` */
8
10
  const __SVELTEKIT_CLIENT_ROUTING__: boolean;
9
11
  /**
@@ -20,7 +20,8 @@ import {
20
20
  Adapter,
21
21
  ServerInit,
22
22
  ClientInit,
23
- Transporter
23
+ Transport,
24
+ HandleValidationError
24
25
  } from '@sveltejs/kit';
25
26
  import {
26
27
  HttpMethod,
@@ -45,6 +46,7 @@ export interface ServerInternalModule {
45
46
  set_safe_public_env(environment: Record<string, string>): void;
46
47
  set_version(version: string): void;
47
48
  set_fix_stack_trace(fix_stack_trace: (error: unknown) => string): void;
49
+ get_hooks: () => Promise<Record<string, any>>;
48
50
  }
49
51
 
50
52
  export interface Asset {
@@ -149,15 +151,16 @@ export interface ServerHooks {
149
151
  handleFetch: HandleFetch;
150
152
  handle: Handle;
151
153
  handleError: HandleServerError;
154
+ handleValidationError: HandleValidationError;
152
155
  reroute: Reroute;
153
- transport: Record<string, Transporter>;
156
+ transport: Transport;
154
157
  init?: ServerInit;
155
158
  }
156
159
 
157
160
  export interface ClientHooks {
158
161
  handleError: HandleClientError;
159
162
  reroute: Reroute;
160
- transport: Record<string, Transporter>;
163
+ transport: Transport;
161
164
  init?: ClientInit;
162
165
  }
163
166
 
@@ -189,6 +192,10 @@ export interface ManifestData {
189
192
  universal: string | null;
190
193
  };
191
194
  nodes: PageNode[];
195
+ remotes: Array<{
196
+ file: string;
197
+ hash: string;
198
+ }>;
192
199
  routes: RouteData[];
193
200
  matchers: Record<string, string>;
194
201
  }
@@ -216,6 +223,11 @@ export interface PrerenderOptions {
216
223
  cache?: string; // including this here is a bit of a hack, but it makes it easy to add <meta http-equiv>
217
224
  fallback?: boolean;
218
225
  dependencies: Map<string, PrerenderDependency>;
226
+ /**
227
+ * For each key the (possibly still pending) result of a prerendered remote function.
228
+ * Used to deduplicate requests to the same remote function with the same arguments.
229
+ */
230
+ remote_responses: Map<string, Promise<any>>;
219
231
  /** True for the duration of a call to the `reroute` hook */
220
232
  inside_reroute?: boolean;
221
233
  }
@@ -280,7 +292,18 @@ export type ServerNodesResponse = {
280
292
  nodes: Array<ServerDataNode | ServerDataSkippedNode | ServerErrorNode | null>;
281
293
  };
282
294
 
283
- export type ServerDataResponse = ServerRedirectNode | ServerNodesResponse;
295
+ export type RemoteFunctionResponse =
296
+ | (ServerRedirectNode & {
297
+ /** devalue'd Record<string, any> */
298
+ refreshes?: string;
299
+ })
300
+ | ServerErrorNode
301
+ | {
302
+ type: 'result';
303
+ result: string;
304
+ /** devalue'd Record<string, any> */
305
+ refreshes: string;
306
+ };
284
307
 
285
308
  /**
286
309
  * Signals a successful response of the server `load` function.
@@ -347,6 +370,8 @@ export interface ServerMetadata {
347
370
  has_server_load: boolean;
348
371
  }>;
349
372
  routes: Map<string, ServerMetadataRoute>;
373
+ /** For each hashed remote file, a map of export name -> { type, dynamic }, where `dynamic` is `false` for non-dynamic prerender functions */
374
+ remotes: Map<string, Map<string, { type: RemoteInfo['type']; dynamic: boolean }>>;
350
375
  }
351
376
 
352
377
  export interface SSRComponent {
@@ -445,6 +470,7 @@ export interface PageNodeIndexes {
445
470
  }
446
471
 
447
472
  export type PrerenderEntryGenerator = () => MaybePromise<Array<Record<string, string>>>;
473
+ export type RemotePrerenderInputsGenerator<Input = any> = () => MaybePromise<Input[]>;
448
474
 
449
475
  export type SSREndpoint = Partial<Record<HttpMethod, RequestHandler>> & {
450
476
  prerender?: PrerenderOption;
@@ -519,5 +545,26 @@ export type ValidatedKitConfig = Omit<RecursiveRequired<KitConfig>, 'adapter'> &
519
545
  adapter?: Adapter;
520
546
  };
521
547
 
548
+ export type RemoteInfo =
549
+ | {
550
+ type: 'query' | 'command';
551
+ id: string;
552
+ name: string;
553
+ }
554
+ | {
555
+ type: 'form';
556
+ id: string;
557
+ name: string;
558
+ fn: (data: FormData) => Promise<any>;
559
+ }
560
+ | {
561
+ type: 'prerender';
562
+ id: string;
563
+ name: string;
564
+ has_arg: boolean;
565
+ dynamic?: boolean;
566
+ inputs?: RemotePrerenderInputsGenerator;
567
+ };
568
+
522
569
  export * from '../exports/index.js';
523
570
  export * from './private.js';
@@ -14,6 +14,6 @@ MY_FEATURE_FLAG=""
14
14
 
15
15
  You can override `.env` values from the command line like so:
16
16
 
17
- ```bash
17
+ ```sh
18
18
  MY_FEATURE_FLAG="enabled" npm run dev
19
19
  ```
@@ -117,7 +117,7 @@ export function remove_optional_params(id) {
117
117
  * @param {string} segment
118
118
  */
119
119
  function affects_path(segment) {
120
- return !/^\([^)]+\)$/.test(segment);
120
+ return segment !== '' && !/^\([^)]+\)$/.test(segment);
121
121
  }
122
122
 
123
123
  /**
package/src/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // generated during release, do not modify
2
2
 
3
3
  /** @type {string} */
4
- export const VERSION = '2.26.0';
4
+ export const VERSION = '2.27.0';