@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,71 @@
1
+ /** @import { RemoteCommand, RemoteQueryOverride } from '@sveltejs/kit' */
2
+ /** @import { RemoteFunctionResponse } from 'types' */
3
+ /** @import { Query } from './query.svelte.js' */
4
+ import { app_dir } from '__sveltekit/paths';
5
+ import * as devalue from 'devalue';
6
+ import { HttpError } from '@sveltejs/kit/internal';
7
+ import { app } from '../client.js';
8
+ import { stringify_remote_arg } from '../../shared.js';
9
+ import { refresh_queries, release_overrides } from './shared.svelte.js';
10
+
11
+ /**
12
+ * Client-version of the `command` function from `$app/server`.
13
+ * @param {string} id
14
+ * @returns {RemoteCommand<any, any>}
15
+ */
16
+ export function command(id) {
17
+ // Careful: This function MUST be synchronous (can't use the async keyword) because the return type has to be a promise with an updates() method.
18
+ // If we make it async, the return type will be a promise that resolves to a promise with an updates() method, which is not what we want.
19
+ return (arg) => {
20
+ /** @type {Array<Query<any> | RemoteQueryOverride>} */
21
+ let updates = [];
22
+
23
+ /** @type {Promise<any> & { updates: (...args: any[]) => any }} */
24
+ const promise = (async () => {
25
+ // Wait a tick to give room for the `updates` method to be called
26
+ await Promise.resolve();
27
+
28
+ const response = await fetch(`/${app_dir}/remote/${id}`, {
29
+ method: 'POST',
30
+ body: JSON.stringify({
31
+ payload: stringify_remote_arg(arg, app.hooks.transport),
32
+ refreshes: updates.map((u) => u._key)
33
+ }),
34
+ headers: {
35
+ 'Content-Type': 'application/json'
36
+ }
37
+ });
38
+
39
+ if (!response.ok) {
40
+ release_overrides(updates);
41
+ // We only end up here in case of a network error or if the server has an internal error
42
+ // (which shouldn't happen because we handle errors on the server and always send a 200 response)
43
+ throw new Error('Failed to execute remote function');
44
+ }
45
+
46
+ const result = /** @type {RemoteFunctionResponse} */ (await response.json());
47
+ if (result.type === 'redirect') {
48
+ release_overrides(updates);
49
+ throw new Error(
50
+ 'Redirects are not allowed in commands. Return a result instead and use goto on the client'
51
+ );
52
+ } else if (result.type === 'error') {
53
+ release_overrides(updates);
54
+ throw new HttpError(result.status ?? 500, result.error);
55
+ } else {
56
+ refresh_queries(result.refreshes, updates);
57
+
58
+ return devalue.parse(result.result, app.decoders);
59
+ }
60
+ })();
61
+
62
+ promise.updates = (/** @type {any} */ ...args) => {
63
+ updates = args;
64
+ // @ts-expect-error Don't allow updates to be called multiple times
65
+ delete promise.updates;
66
+ return promise;
67
+ };
68
+
69
+ return promise;
70
+ };
71
+ }
@@ -0,0 +1,312 @@
1
+ /** @import { RemoteForm, RemoteQueryOverride } from '@sveltejs/kit' */
2
+ /** @import { RemoteFunctionResponse } from 'types' */
3
+ /** @import { Query } from './query.svelte.js' */
4
+ import { app_dir } from '__sveltekit/paths';
5
+ import * as devalue from 'devalue';
6
+ import { DEV } from 'esm-env';
7
+ import { HttpError } from '@sveltejs/kit/internal';
8
+ import { app, remote_responses, started, goto, set_nearest_error_page } from '../client.js';
9
+ import { create_remote_cache_key } from '../../shared.js';
10
+ import { tick } from 'svelte';
11
+ import { refresh_queries, release_overrides } from './shared.svelte.js';
12
+
13
+ /**
14
+ * Client-version of the `form` function from `$app/server`.
15
+ * @template T
16
+ * @param {string} id
17
+ * @returns {RemoteForm<T>}
18
+ */
19
+ export function form(id) {
20
+ /** @type {Map<any, { count: number, instance: RemoteForm<T> }>} */
21
+ const instances = new Map();
22
+
23
+ /** @param {string | number | boolean} [key] */
24
+ function create_instance(key) {
25
+ const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
26
+ const action = '?/remote=' + encodeURIComponent(action_id);
27
+
28
+ /** @type {any} */
29
+ let result = $state(
30
+ !started ? (remote_responses[create_remote_cache_key(action, '')] ?? undefined) : undefined
31
+ );
32
+
33
+ /**
34
+ * @param {FormData} data
35
+ * @returns {Promise<any> & { updates: (...args: any[]) => any }}
36
+ */
37
+ function submit(data) {
38
+ // Store a reference to the current instance and increment the usage count for the duration
39
+ // of the request. This ensures that the instance is not deleted in case of an optimistic update
40
+ // (e.g. when deleting an item in a list) that fails and wants to surface an error to the user afterwards.
41
+ // If the instance would be deleted in the meantime, the error property would be assigned to the old,
42
+ // no-longer-visible instance, so it would never be shown to the user.
43
+ const entry = instances.get(key);
44
+ if (entry) {
45
+ entry.count++;
46
+ }
47
+
48
+ /** @type {Array<Query<any> | RemoteQueryOverride>} */
49
+ let updates = [];
50
+
51
+ /** @type {Promise<any> & { updates: (...args: any[]) => any }} */
52
+ const promise = (async () => {
53
+ try {
54
+ await Promise.resolve();
55
+
56
+ if (updates.length > 0) {
57
+ if (DEV) {
58
+ if (data.get('sveltekit:remote_refreshes')) {
59
+ throw new Error(
60
+ 'The FormData key `sveltekit:remote_refreshes` is reserved for internal use and should not be set manually'
61
+ );
62
+ }
63
+ }
64
+ data.set('sveltekit:remote_refreshes', JSON.stringify(updates.map((u) => u._key)));
65
+ }
66
+
67
+ const response = await fetch(`/${app_dir}/remote/${action_id}`, {
68
+ method: 'POST',
69
+ body: data
70
+ });
71
+
72
+ if (!response.ok) {
73
+ // We only end up here in case of a network error or if the server has an internal error
74
+ // (which shouldn't happen because we handle errors on the server and always send a 200 response)
75
+ result = undefined;
76
+ throw new Error('Failed to execute remote function');
77
+ }
78
+
79
+ const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
80
+
81
+ if (form_result.type === 'result') {
82
+ result = devalue.parse(form_result.result, app.decoders);
83
+
84
+ refresh_queries(form_result.refreshes, updates);
85
+ } else if (form_result.type === 'redirect') {
86
+ const refreshes = form_result.refreshes ?? '';
87
+ const invalidateAll = !refreshes && updates.length === 0;
88
+ if (!invalidateAll) {
89
+ refresh_queries(refreshes, updates);
90
+ }
91
+ void goto(form_result.location, { invalidateAll });
92
+ } else {
93
+ result = undefined;
94
+ throw new HttpError(500, form_result.error);
95
+ }
96
+ } catch (e) {
97
+ release_overrides(updates);
98
+ throw e;
99
+ } finally {
100
+ void tick().then(() => {
101
+ if (entry) {
102
+ entry.count--;
103
+ if (entry.count === 0) {
104
+ instances.delete(key);
105
+ }
106
+ }
107
+ });
108
+ }
109
+ })();
110
+
111
+ promise.updates = (...args) => {
112
+ updates = args;
113
+ return promise;
114
+ };
115
+
116
+ return promise;
117
+ }
118
+
119
+ /** @type {RemoteForm<T>} */
120
+ const instance = {};
121
+
122
+ instance.method = 'POST';
123
+ instance.action = action;
124
+
125
+ /**
126
+ * @param {HTMLFormElement} form_element
127
+ * @param {HTMLElement | null} submitter
128
+ */
129
+ function create_form_data(form_element, submitter) {
130
+ const form_data = new FormData(form_element);
131
+
132
+ if (DEV) {
133
+ const enctype = submitter?.hasAttribute('formenctype')
134
+ ? /** @type {HTMLButtonElement | HTMLInputElement} */ (submitter).formEnctype
135
+ : clone(form_element).enctype;
136
+ if (enctype !== 'multipart/form-data') {
137
+ for (const value of form_data.values()) {
138
+ if (value instanceof File) {
139
+ throw new Error(
140
+ 'Your form contains <input type="file"> fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.'
141
+ );
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ const submitter_name = submitter?.getAttribute('name');
148
+ if (submitter_name) {
149
+ form_data.append(submitter_name, submitter?.getAttribute('value') ?? '');
150
+ }
151
+
152
+ return form_data;
153
+ }
154
+
155
+ /** @param {Parameters<RemoteForm<any>['enhance']>[0]} callback */
156
+ const form_onsubmit = (callback) => {
157
+ /** @param {SubmitEvent} event */
158
+ return async (event) => {
159
+ const form = /** @type {HTMLFormElement} */ (event.target);
160
+ const method = event.submitter?.hasAttribute('formmethod')
161
+ ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod
162
+ : clone(form).method;
163
+
164
+ if (method !== 'post') return;
165
+
166
+ const action = new URL(
167
+ // We can't do submitter.formAction directly because that property is always set
168
+ event.submitter?.hasAttribute('formaction')
169
+ ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction
170
+ : clone(form).action
171
+ );
172
+
173
+ if (action.searchParams.get('/remote') !== action_id) {
174
+ return;
175
+ }
176
+
177
+ event.preventDefault();
178
+
179
+ const data = create_form_data(form, event.submitter);
180
+
181
+ try {
182
+ await callback({
183
+ form,
184
+ data,
185
+ submit: () => submit(data)
186
+ });
187
+ } catch (e) {
188
+ const error =
189
+ e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
190
+ const status = e instanceof HttpError ? e.status : 500;
191
+ void set_nearest_error_page(error, status);
192
+ }
193
+ };
194
+ };
195
+
196
+ instance.onsubmit = form_onsubmit(({ submit }) => submit());
197
+
198
+ /** @param {Parameters<RemoteForm<any>['buttonProps']['enhance']>[0]} callback */
199
+ const form_action_onclick = (callback) => {
200
+ /** @param {Event} event */
201
+ return async (event) => {
202
+ const target = /** @type {HTMLButtonElement} */ (event.target);
203
+ const form = target.form;
204
+ if (!form) return;
205
+
206
+ // Prevent this from firing the form's submit event
207
+ event.stopPropagation();
208
+ event.preventDefault();
209
+
210
+ const data = create_form_data(form, target);
211
+
212
+ try {
213
+ await callback({
214
+ form,
215
+ data,
216
+ submit: () => submit(data)
217
+ });
218
+ } catch (e) {
219
+ const error =
220
+ e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
221
+ const status = e instanceof HttpError ? e.status : 500;
222
+ void set_nearest_error_page(error, status);
223
+ }
224
+ };
225
+ };
226
+
227
+ /** @type {RemoteForm<any>['buttonProps']} */
228
+ // @ts-expect-error we gotta set enhance as a non-enumerable property
229
+ const button_props = {
230
+ type: 'submit',
231
+ formmethod: 'POST',
232
+ formaction: action,
233
+ onclick: form_action_onclick(({ submit }) => submit())
234
+ };
235
+
236
+ Object.defineProperty(button_props, 'enhance', {
237
+ /** @type {RemoteForm<any>['buttonProps']['enhance']} */
238
+ value: (callback) => {
239
+ return {
240
+ type: 'submit',
241
+ formmethod: 'POST',
242
+ formaction: action,
243
+ onclick: form_action_onclick(callback)
244
+ };
245
+ }
246
+ });
247
+
248
+ Object.defineProperties(instance, {
249
+ buttonProps: {
250
+ value: button_props
251
+ },
252
+ result: {
253
+ get: () => result
254
+ },
255
+ enhance: {
256
+ /** @type {RemoteForm<any>['enhance']} */
257
+ value: (callback) => {
258
+ return {
259
+ method: 'POST',
260
+ action,
261
+ onsubmit: form_onsubmit(callback)
262
+ };
263
+ }
264
+ }
265
+ });
266
+
267
+ return instance;
268
+ }
269
+
270
+ const instance = create_instance();
271
+
272
+ Object.defineProperty(instance, 'for', {
273
+ /** @type {RemoteForm<any>['for']} */
274
+ value: (key) => {
275
+ const entry = instances.get(key) ?? { count: 0, instance: create_instance(key) };
276
+
277
+ try {
278
+ $effect.pre(() => {
279
+ return () => {
280
+ entry.count--;
281
+
282
+ void tick().then(() => {
283
+ if (entry.count === 0) {
284
+ instances.delete(key);
285
+ }
286
+ });
287
+ };
288
+ });
289
+
290
+ entry.count += 1;
291
+ instances.set(key, entry);
292
+ } catch {
293
+ // not in an effect context
294
+ }
295
+
296
+ return entry.instance;
297
+ }
298
+ });
299
+
300
+ return instance;
301
+ }
302
+
303
+ /**
304
+ * Shallow clone an element, so that we can access e.g. `form.action` without worrying
305
+ * that someone has added an `<input name="action">` (https://github.com/sveltejs/kit/issues/7593)
306
+ * @template {HTMLElement} T
307
+ * @param {T} element
308
+ * @returns {T}
309
+ */
310
+ function clone(element) {
311
+ return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element));
312
+ }
@@ -0,0 +1,4 @@
1
+ export { command } from './command.js';
2
+ export { form } from './form.svelte.js';
3
+ export { prerender } from './prerender.svelte.js';
4
+ export { query } from './query.svelte.js';
@@ -0,0 +1,166 @@
1
+ /** @import { RemoteFunctionResponse } from 'types' */
2
+ import { app_dir } from '__sveltekit/paths';
3
+ import { version } from '__sveltekit/environment';
4
+ import * as devalue from 'devalue';
5
+ import { DEV } from 'esm-env';
6
+ import { app, remote_responses, started } from '../client.js';
7
+ import { create_remote_function, remote_request } from './shared.svelte.js';
8
+
9
+ // Initialize Cache API for prerender functions
10
+ const CACHE_NAME = `sveltekit:${version}`;
11
+ /** @type {Cache | undefined} */
12
+ let prerender_cache;
13
+
14
+ void (async () => {
15
+ if (!DEV && typeof caches !== 'undefined') {
16
+ try {
17
+ prerender_cache = await caches.open(CACHE_NAME);
18
+
19
+ // Clean up old cache versions
20
+ const cache_names = await caches.keys();
21
+ for (const cache_name of cache_names) {
22
+ if (cache_name.startsWith('sveltekit:') && cache_name !== CACHE_NAME) {
23
+ await caches.delete(cache_name);
24
+ }
25
+ }
26
+ } catch (error) {
27
+ console.warn('Failed to initialize SvelteKit cache:', error);
28
+ }
29
+ }
30
+ })();
31
+
32
+ /**
33
+ * @template T
34
+ * @implements {Partial<Promise<T>>}
35
+ */
36
+ class Prerender {
37
+ /** @type {Promise<T>} */
38
+ #promise;
39
+
40
+ #loading = $state(true);
41
+ #ready = $state(false);
42
+
43
+ /** @type {T | undefined} */
44
+ #current = $state.raw();
45
+
46
+ #error = $state.raw(undefined);
47
+
48
+ /**
49
+ * @param {() => Promise<T>} fn
50
+ */
51
+ constructor(fn) {
52
+ this.#promise = fn().then(
53
+ (value) => {
54
+ this.#loading = false;
55
+ this.#ready = true;
56
+ this.#current = value;
57
+ return value;
58
+ },
59
+ (error) => {
60
+ this.#loading = false;
61
+ this.#error = error;
62
+ throw error;
63
+ }
64
+ );
65
+ }
66
+
67
+ /**
68
+ *
69
+ * @param {((value: any) => any) | null | undefined} onfulfilled
70
+ * @param {((reason: any) => any) | null | undefined} [onrejected]
71
+ * @returns
72
+ */
73
+ then(onfulfilled, onrejected) {
74
+ return this.#promise.then(onfulfilled, onrejected);
75
+ }
76
+
77
+ /**
78
+ * @param {((reason: any) => any) | null | undefined} onrejected
79
+ */
80
+ catch(onrejected) {
81
+ return this.#promise.catch(onrejected);
82
+ }
83
+
84
+ /**
85
+ * @param {(() => any) | null | undefined} onfinally
86
+ */
87
+ finally(onfinally) {
88
+ return this.#promise.finally(onfinally);
89
+ }
90
+
91
+ get current() {
92
+ return this.#current;
93
+ }
94
+
95
+ get error() {
96
+ return this.#error;
97
+ }
98
+
99
+ /**
100
+ * Returns true if the resource is loading.
101
+ */
102
+ get loading() {
103
+ return this.#loading;
104
+ }
105
+
106
+ /**
107
+ * Returns true once the resource has been loaded.
108
+ */
109
+ get ready() {
110
+ return this.#ready;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @param {string} id
116
+ */
117
+ export function prerender(id) {
118
+ return create_remote_function(id, (cache_key, payload) => {
119
+ return new Prerender(async () => {
120
+ if (!started) {
121
+ const result = remote_responses[cache_key];
122
+ if (result) {
123
+ return result;
124
+ }
125
+ }
126
+
127
+ const url = `/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`;
128
+
129
+ // Check the Cache API first
130
+ if (prerender_cache) {
131
+ try {
132
+ const cached_response = await prerender_cache.match(url);
133
+ if (cached_response) {
134
+ const cached_result = /** @type { RemoteFunctionResponse & { type: 'result' } } */ (
135
+ await cached_response.json()
136
+ );
137
+ return devalue.parse(cached_result.result, app.decoders);
138
+ }
139
+ } catch {
140
+ // Nothing we can do here
141
+ }
142
+ }
143
+
144
+ const result = await remote_request(url);
145
+
146
+ // For successful prerender requests, save to cache
147
+ if (prerender_cache) {
148
+ try {
149
+ await prerender_cache.put(
150
+ url,
151
+ // We need to create a new response because the original response is already consumed
152
+ new Response(JSON.stringify(result), {
153
+ headers: {
154
+ 'Content-Type': 'application/json'
155
+ }
156
+ })
157
+ );
158
+ } catch {
159
+ // Nothing we can do here
160
+ }
161
+ }
162
+
163
+ return result;
164
+ });
165
+ });
166
+ }