@sveltejs/kit 3.0.0-next.2 → 3.0.0-next.4

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 (33) hide show
  1. package/package.json +2 -2
  2. package/src/core/postbuild/analyse.js +0 -8
  3. package/src/core/postbuild/prerender.js +2 -0
  4. package/src/exports/public.d.ts +1 -1
  5. package/src/exports/vite/build/build_server.js +43 -58
  6. package/src/exports/vite/build/remote.js +18 -11
  7. package/src/exports/vite/build/utils.js +0 -8
  8. package/src/exports/vite/index.js +220 -216
  9. package/src/runtime/app/server/remote/command.js +0 -3
  10. package/src/runtime/app/server/remote/form.js +18 -13
  11. package/src/runtime/app/server/remote/prerender.js +28 -34
  12. package/src/runtime/app/server/remote/query.js +105 -94
  13. package/src/runtime/app/server/remote/requested.js +14 -10
  14. package/src/runtime/app/server/remote/shared.js +25 -19
  15. package/src/runtime/client/client.js +25 -15
  16. package/src/runtime/client/remote-functions/command.svelte.js +5 -30
  17. package/src/runtime/client/remote-functions/form.svelte.js +62 -82
  18. package/src/runtime/client/remote-functions/prerender.svelte.js +14 -6
  19. package/src/runtime/client/remote-functions/query/index.js +6 -14
  20. package/src/runtime/client/remote-functions/query/instance.svelte.js +37 -8
  21. package/src/runtime/client/remote-functions/query/proxy.js +3 -3
  22. package/src/runtime/client/remote-functions/query-batch.svelte.js +59 -68
  23. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +21 -6
  24. package/src/runtime/client/remote-functions/shared.svelte.js +76 -59
  25. package/src/runtime/server/page/render.js +20 -80
  26. package/src/runtime/server/page/server_routing.js +20 -15
  27. package/src/runtime/server/remote.js +296 -204
  28. package/src/runtime/server/respond.js +4 -2
  29. package/src/types/global-private.d.ts +3 -3
  30. package/src/types/internal.d.ts +53 -34
  31. package/src/version.js +1 -1
  32. package/types/index.d.ts +6 -5
  33. package/types/index.d.ts.map +1 -1
@@ -1,12 +1,12 @@
1
1
  /** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest } from '@sveltejs/kit' */
2
- /** @import { RemoteFormInternals, RemoteFunctionResponse, RemoteInternals, RequestState, SSROptions } from 'types' */
2
+ /** @import { RemoteFormInternals, RemoteFunctionData, RemoteFunctionResponse, RemoteInternals, RequestState, SSROptions } from 'types' */
3
3
 
4
4
  import { json, error } from '@sveltejs/kit';
5
5
  import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
6
6
  import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server';
7
7
  import { app_dir, base } from '$app/paths/internal/server';
8
8
  import { is_form_content_type } from '../../utils/http.js';
9
- import { parse_remote_arg, split_remote_key, stringify } from '../shared.js';
9
+ import { create_remote_key, parse_remote_arg, split_remote_key, stringify } from '../shared.js';
10
10
  import { handle_error_and_jsonify } from './utils.js';
11
11
  import { normalize_error } from '../../utils/error.js';
12
12
  import { check_incorrect_fail_use } from './page/actions.js';
@@ -58,225 +58,233 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
58
58
  });
59
59
 
60
60
  try {
61
- if (internals.type === 'query_batch') {
62
- if (event.request.method !== 'POST') {
63
- throw new SvelteKitError(
64
- 405,
65
- 'Method Not Allowed',
66
- `\`query.batch\` functions must be invoked via POST request, not ${event.request.method}`
61
+ /** @type {RemoteFunctionData} */
62
+ const data = {};
63
+
64
+ switch (internals.type) {
65
+ case 'query_live': {
66
+ if (event.request.method !== 'GET') {
67
+ throw new SvelteKitError(
68
+ 405,
69
+ 'Method Not Allowed',
70
+ `\`query.live\` functions must be invoked via GET request, not ${event.request.method}`
71
+ );
72
+ }
73
+
74
+ const payload = /** @type {string} */ (
75
+ new URL(event.request.url).searchParams.get('payload')
67
76
  );
68
- }
69
77
 
70
- /** @type {{ payloads: string[] }} */
71
- const { payloads } = await event.request.json();
78
+ const generator = internals.run(event, state, parse_remote_arg(payload, transport));
72
79
 
73
- const args = await Promise.all(
74
- payloads.map((payload) => parse_remote_arg(payload, transport))
75
- );
80
+ const encoder = new TextEncoder();
76
81
 
77
- const results = await with_request_store({ event, state }, () =>
78
- internals.run(args, options)
79
- );
82
+ /**
83
+ * @param {ReadableStreamDefaultController} controller
84
+ * @param {any} payload
85
+ */
86
+ function send(controller, payload) {
87
+ controller.enqueue(encoder.encode('data: ' + JSON.stringify(payload) + '\n\n'));
88
+ }
80
89
 
81
- return json(
82
- /** @type {RemoteFunctionResponse} */ ({
83
- type: 'result',
84
- result: stringify(results, transport)
85
- })
86
- );
87
- }
90
+ let closed = false;
88
91
 
89
- if (internals.type === 'form') {
90
- if (event.request.method !== 'POST') {
91
- throw new SvelteKitError(
92
- 405,
93
- 'Method Not Allowed',
94
- `\`form\` functions must be invoked via POST request, not ${event.request.method}`
95
- );
96
- }
92
+ /** @type {string | undefined} */
93
+ let result = undefined;
97
94
 
98
- if (!is_form_content_type(event.request)) {
99
- throw new SvelteKitError(
100
- 415,
101
- 'Unsupported Media Type',
102
- `\`form\` functions expect form-encoded data — received ${event.request.headers.get(
103
- 'content-type'
104
- )}`
105
- );
106
- }
95
+ async function cancel() {
96
+ if (closed) return;
97
+ closed = true;
98
+ await generator.return(undefined);
99
+ }
107
100
 
108
- const { data, meta, form_data } = await deserialize_binary_form(event.request);
109
- state.remote.requested = create_requested_map(meta.remote_refreshes);
101
+ event.request.signal.addEventListener('abort', cancel, { once: true });
110
102
 
111
- // If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set)
112
- // Note that additional_args will only be set if the form is not enhanced, as enhanced forms transfer the key inside `data`.
113
- if (additional_args && !('id' in data)) {
114
- data.id = JSON.parse(decodeURIComponent(additional_args));
115
- }
103
+ return new Response(
104
+ new ReadableStream({
105
+ async pull(controller) {
106
+ if (event.request.signal.aborted) {
107
+ await cancel();
108
+ controller.close();
109
+ return;
110
+ }
116
111
 
117
- const fn = internals.fn;
118
- const result = await with_request_store({ event, state }, () => fn(data, meta, form_data));
112
+ try {
113
+ while (true) {
114
+ const { value, done } = await generator.next();
115
+
116
+ if (done) {
117
+ await cancel();
118
+ controller.close();
119
+ return;
120
+ }
121
+
122
+ // only send changed data
123
+ if (result !== (result = stringify(value, transport))) {
124
+ send(controller, {
125
+ type: 'result',
126
+ result
127
+ });
128
+
129
+ return;
130
+ }
131
+ }
132
+ } catch (error) {
133
+ if (!event.request.signal.aborted) {
134
+ if (error instanceof Redirect) {
135
+ send(controller, {
136
+ type: 'redirect',
137
+ location: error.location
138
+ });
139
+ } else {
140
+ const status =
141
+ error instanceof HttpError || error instanceof SvelteKitError
142
+ ? error.status
143
+ : 500;
144
+
145
+ send(controller, {
146
+ type: 'error',
147
+ error: await handle_error_and_jsonify(event, state, options, error),
148
+ status
149
+ });
150
+ }
151
+ }
119
152
 
120
- return json(
121
- /** @type {RemoteFunctionResponse} */ ({
122
- type: 'result',
123
- result: stringify(result, transport),
124
- refreshes: result.issues
125
- ? undefined
126
- : await serialize_singleflight(state.remote.refreshes),
127
- reconnects: result.issues
128
- ? undefined
129
- : await serialize_singleflight(state.remote.reconnects)
130
- })
131
- );
132
- }
153
+ await cancel();
154
+ controller.close();
155
+ }
156
+ },
157
+ cancel
158
+ }),
159
+ {
160
+ headers: {
161
+ 'cache-control': 'private, no-store',
162
+ 'content-type': 'text/event-stream'
163
+ }
164
+ }
165
+ );
166
+ }
133
167
 
134
- if (internals.type === 'command') {
135
- /** @type {{ payload: string, refreshes?: string[] }} */
136
- const { payload, refreshes } = await event.request.json();
137
- state.remote.requested = create_requested_map(refreshes);
138
- const arg = parse_remote_arg(payload, transport);
139
- const data = await with_request_store({ event, state }, () => fn(arg));
168
+ case 'query_batch': {
169
+ if (event.request.method !== 'POST') {
170
+ throw new SvelteKitError(
171
+ 405,
172
+ 'Method Not Allowed',
173
+ `\`query.batch\` functions must be invoked via POST request, not ${event.request.method}`
174
+ );
175
+ }
140
176
 
141
- return json(
142
- /** @type {RemoteFunctionResponse} */ ({
143
- type: 'result',
144
- result: stringify(data, transport),
145
- refreshes: await serialize_singleflight(state.remote.refreshes),
146
- reconnects: await serialize_singleflight(state.remote.reconnects)
147
- })
148
- );
149
- }
177
+ /** @type {{ payloads: string[] }} */
178
+ const { payloads } = await event.request.json();
150
179
 
151
- if (internals.type === 'query_live') {
152
- if (event.request.method !== 'GET') {
153
- throw new SvelteKitError(
154
- 405,
155
- 'Method Not Allowed',
156
- `\`query.live\` functions must be invoked via GET request, not ${event.request.method}`
180
+ const args = await Promise.all(
181
+ payloads.map((payload) => parse_remote_arg(payload, transport))
157
182
  );
183
+
184
+ data._ = await with_request_store({ event, state }, () => internals.run(args, options));
185
+
186
+ break;
158
187
  }
159
188
 
160
- const payload = /** @type {string} */ (
161
- new URL(event.request.url).searchParams.get('payload')
162
- );
189
+ case 'form': {
190
+ if (event.request.method !== 'POST') {
191
+ throw new SvelteKitError(
192
+ 405,
193
+ 'Method Not Allowed',
194
+ `\`form\` functions must be invoked via POST request, not ${event.request.method}`
195
+ );
196
+ }
163
197
 
164
- const generator = internals.run(event, state, parse_remote_arg(payload, transport));
198
+ if (!is_form_content_type(event.request)) {
199
+ throw new SvelteKitError(
200
+ 415,
201
+ 'Unsupported Media Type',
202
+ `\`form\` functions expect form-encoded data — received ${event.request.headers.get(
203
+ 'content-type'
204
+ )}`
205
+ );
206
+ }
165
207
 
166
- const encoder = new TextEncoder();
208
+ const { data: input, meta, form_data } = await deserialize_binary_form(event.request);
209
+ state.remote.requested = create_requested_map(meta.remote_refreshes);
167
210
 
168
- /**
169
- * @param {ReadableStreamDefaultController} controller
170
- * @param {any} payload
171
- */
172
- function send(controller, payload) {
173
- controller.enqueue(encoder.encode('data: ' + JSON.stringify(payload) + '\n\n'));
174
- }
211
+ // If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set)
212
+ // Note that additional_args will only be set if the form is not enhanced, as enhanced forms transfer the key inside `data`.
213
+ if (additional_args && !('id' in input)) {
214
+ input.id = JSON.parse(decodeURIComponent(additional_args));
215
+ }
175
216
 
176
- let closed = false;
217
+ const fn = internals.fn;
218
+ data._ = await with_request_store(
219
+ { event, state: { ...state, is_in_remote_form_or_command: true } },
220
+ () => fn(input, meta, form_data)
221
+ );
177
222
 
178
- /** @type {string | undefined} */
179
- let result = undefined;
223
+ if (data._.issues) {
224
+ // special case — don't serialize refreshes/reconnects
225
+ return json(
226
+ /** @type {RemoteFunctionResponse} */ ({
227
+ type: 'result',
228
+ data: stringify(data, transport)
229
+ })
230
+ );
231
+ }
180
232
 
181
- async function cancel() {
182
- if (closed) return;
183
- closed = true;
184
- await generator.return(undefined);
233
+ break;
185
234
  }
186
235
 
187
- event.request.signal.addEventListener('abort', cancel, { once: true });
236
+ case 'command': {
237
+ /** @type {{ payload: string, refreshes?: string[] }} */
238
+ const { payload, refreshes } = await event.request.json();
239
+ state.remote.requested = create_requested_map(refreshes);
240
+ const arg = parse_remote_arg(payload, transport);
188
241
 
189
- return new Response(
190
- new ReadableStream({
191
- async pull(controller) {
192
- if (event.request.signal.aborted) {
193
- await cancel();
194
- controller.close();
195
- return;
196
- }
242
+ data._ = await with_request_store(
243
+ { event, state: { ...state, is_in_remote_form_or_command: true } },
244
+ () => fn(arg)
245
+ );
197
246
 
198
- try {
199
- while (true) {
200
- const { value, done } = await generator.next();
247
+ break;
248
+ }
201
249
 
202
- if (done) {
203
- await cancel();
204
- controller.close();
205
- return;
206
- }
250
+ case 'prerender': {
251
+ data._ = await with_request_store({ event, state }, () =>
252
+ fn(parse_remote_arg(additional_args, transport))
253
+ );
207
254
 
208
- // only send changed data
209
- if (result !== (result = stringify(value, transport))) {
210
- send(controller, {
211
- type: 'result',
212
- result
213
- });
255
+ break;
256
+ }
214
257
 
215
- return;
216
- }
217
- }
218
- } catch (error) {
219
- if (!event.request.signal.aborted) {
220
- if (error instanceof Redirect) {
221
- send(controller, {
222
- type: 'redirect',
223
- location: error.location
224
- });
225
- } else {
226
- const status =
227
- error instanceof HttpError || error instanceof SvelteKitError
228
- ? error.status
229
- : 500;
230
-
231
- send(controller, {
232
- type: 'error',
233
- error: await handle_error_and_jsonify(event, state, options, error),
234
- status
235
- });
236
- }
237
- }
258
+ case 'query': {
259
+ const payload = /** @type {string} */ (
260
+ // new URL(...) necessary because we're hiding the URL from the user in the event object
261
+ new URL(event.request.url).searchParams.get('payload')
262
+ );
238
263
 
239
- await cancel();
240
- controller.close();
241
- }
242
- },
243
- cancel
244
- }),
245
- {
246
- headers: {
247
- 'cache-control': 'private, no-store',
248
- 'content-type': 'text/event-stream'
249
- }
250
- }
251
- );
264
+ data._ = await with_request_store({ event, state }, () =>
265
+ fn(parse_remote_arg(payload, transport))
266
+ );
267
+
268
+ break;
269
+ }
252
270
  }
253
271
 
254
- const payload =
255
- internals.type === 'prerender'
256
- ? additional_args
257
- : /** @type {string} */ (
258
- // new URL(...) necessary because we're hiding the URL from the user in the event object
259
- new URL(event.request.url).searchParams.get('payload')
260
- );
261
-
262
- const data = await with_request_store({ event, state }, () =>
263
- fn(parse_remote_arg(payload, transport))
264
- );
272
+ await collect_remote_data(data, event, state, options);
265
273
 
266
274
  return json(
267
275
  /** @type {RemoteFunctionResponse} */ ({
268
276
  type: 'result',
269
- result: stringify(data, transport)
277
+ data: stringify(data, transport)
270
278
  })
271
279
  );
272
280
  } catch (error) {
273
281
  if (error instanceof Redirect) {
282
+ const data = await collect_remote_data({ redirect: error.location }, event, state, options);
283
+
274
284
  return json(
275
285
  /** @type {RemoteFunctionResponse} */ ({
276
- type: 'redirect',
277
- location: error.location,
278
- refreshes: await serialize_singleflight(state.remote.refreshes),
279
- reconnects: await serialize_singleflight(state.remote.reconnects)
286
+ type: 'result',
287
+ data: stringify(data, transport)
280
288
  })
281
289
  );
282
290
  }
@@ -300,35 +308,113 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
300
308
  }
301
309
  );
302
310
  }
311
+ }
303
312
 
304
- /** @param {Map<string, Promise<any>> | null} map */
305
- async function serialize_singleflight(map) {
306
- if (!map || map.size === 0) {
307
- return undefined;
308
- }
313
+ /**
314
+ * Collects all the query/prerender data that was retrieved
315
+ * during the request and adds it to `data`
316
+ * @param {RemoteFunctionData} data
317
+ * @param {RequestEvent} event
318
+ * @param {RequestState} state
319
+ * @param {SSROptions} options
320
+ */
321
+ export async function collect_remote_data(data, event, state, options) {
322
+ /**
323
+ *
324
+ * @param {unknown} error
325
+ * @returns {Promise<[status: number, error: App.Error]>}
326
+ */
327
+ async function convert_error(error) {
328
+ const status =
329
+ error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500;
309
330
 
310
- const results = await Promise.all(
311
- Array.from(map, async ([key, promise]) => {
312
- try {
313
- return [key, { type: 'result', data: await promise }];
314
- } catch (error) {
315
- const status =
316
- error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500;
317
-
318
- return [
319
- key,
320
- {
321
- type: 'error',
322
- status,
323
- error: await handle_error_and_jsonify(event, state, options, error)
324
- }
325
- ];
331
+ return [status, await handle_error_and_jsonify(event, state, options, error)];
332
+ }
333
+
334
+ /** @type {Promise<any>[]} */
335
+ const promises = [];
336
+
337
+ if (state.remote.explicit) {
338
+ for (const [remote_key, { internals, promise }] of state.remote.explicit) {
339
+ // there were explicit refreshes/reconnects (via `refresh()`/`set()`/`reconnect()`),
340
+ // so the client should apply these single-flight updates instead of calling `invalidateAll()`
341
+ data.r = true;
342
+
343
+ const type = /** @type {'p' | 'q' | 'l'} */ (
344
+ internals.type === 'query_live' ? 'l' : internals.type[0]
345
+ );
346
+
347
+ await promise.then(
348
+ (v) => {
349
+ ((data[type] ??= {})[remote_key] ??= {}).v = v;
350
+ },
351
+ async (e) => {
352
+ if (e instanceof Redirect) {
353
+ // already handled elsewhere
354
+ return;
355
+ }
356
+
357
+ ((data[type] ??= {})[remote_key] ??= {}).e = await convert_error(e);
326
358
  }
327
- })
328
- );
359
+ );
360
+ }
361
+ }
362
+
363
+ await Promise.all(promises);
364
+
365
+ if (state.remote.implicit) {
366
+ for (const [internals, record] of state.remote.implicit) {
367
+ // Private (non-exported) remote functions have no `id` and must never be
368
+ // serialized into the response — otherwise their (potentially private) result
369
+ // would be shipped to the client under a malformed `undefined/...` key.
370
+ if (!internals.id) continue;
371
+
372
+ for (const key in record) {
373
+ // form outputs are registered under the client-side action id directly
374
+ const remote_key = internals.type === 'form' ? key : create_remote_key(internals.id, key);
329
375
 
330
- return stringify(Object.fromEntries(results), transport);
376
+ const type = /** @type {'p' | 'q' | 'l' | 'f'} */ (
377
+ internals.type === 'query_live' ? 'l' : internals.type[0]
378
+ );
379
+
380
+ const promise = state.remote.data?.get(internals)?.[key] ?? record[key]();
381
+
382
+ // If the promise is still pending (e.g. the query was rendered in its loading
383
+ // state during SSR), omit it from the payload entirely so that the client
384
+ // fetches it itself — an entry without `v`/`e` would hydrate as `undefined`.
385
+ let resolved = true;
386
+
387
+ await Promise.race([
388
+ Promise.resolve(promise).then(
389
+ (v) => {
390
+ if (resolved) {
391
+ ((data[type] ??= {})[remote_key] ??= {}).v = v;
392
+ }
393
+ },
394
+ (e) => {
395
+ if (e instanceof Redirect) {
396
+ // already handled elsewhere
397
+ return;
398
+ }
399
+
400
+ if (resolved) {
401
+ promises.push(
402
+ convert_error(e).then((e) => {
403
+ ((data[type] ??= {})[remote_key] ??= {}).e = e;
404
+ })
405
+ );
406
+ }
407
+ }
408
+ ),
409
+ Promise.resolve().then(() => (resolved = false))
410
+ ]);
411
+ }
412
+ }
331
413
  }
414
+
415
+ await Promise.all(promises);
416
+
417
+ return data;
332
418
  }
333
419
 
334
420
  /**
@@ -377,7 +463,10 @@ export async function handle_remote_form_post(event, state, manifest, id) {
377
463
  * @returns {Promise<ActionResult>}
378
464
  */
379
465
  async function handle_remote_form_post_internal(event, state, manifest, id) {
380
- const [hash, name, action_id] = id.split('/');
466
+ // `hash` and `name` can never contain a `/`, but the JSON-stringified key of a
467
+ // keyed (`form.for(key)`) instance can — rejoin the remaining segments
468
+ const [hash, name, ...rest] = id.split('/');
469
+ const action_id = rest.join('/');
381
470
  const remotes = manifest._.remotes;
382
471
  const module = await remotes[hash]?.();
383
472
 
@@ -413,7 +502,10 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
413
502
  data.id = JSON.parse(decodeURIComponent(action_id));
414
503
  }
415
504
 
416
- await with_request_store({ event, state }, () => fn(data, meta, form_data));
505
+ await with_request_store(
506
+ { event, state: { ...state, is_in_remote_form_or_command: true } },
507
+ () => fn(data, meta, form_data)
508
+ );
417
509
 
418
510
  // We don't want the data to appear on `let { form } = $props()`, which is why we're not returning it.
419
511
  // It is instead available on `myForm.result`, setting of which happens within the remote `form` function.
@@ -147,14 +147,16 @@ export async function internal_respond(request, options, manifest, state) {
147
147
  },
148
148
  remote: {
149
149
  data: null,
150
+ explicit: null,
151
+ implicit: null,
150
152
  forms: null,
151
- refreshes: null,
152
153
  requested: null,
153
- reconnects: null,
154
154
  batches: null,
155
155
  live_iterators: null
156
156
  },
157
157
  is_in_remote_function: false,
158
+ is_in_remote_form_or_command: false,
159
+ is_in_remote_query: false,
158
160
  is_in_render: false,
159
161
  is_in_universal_load: false
160
162
  };
@@ -1,3 +1,5 @@
1
+ import { RemoteFunctionData } from 'types';
2
+
1
3
  declare global {
2
4
  const __SVELTEKIT_ADAPTER_NAME__: string;
3
5
  const __SVELTEKIT_APP_DIR__: string;
@@ -40,9 +42,7 @@ declare global {
40
42
  /** Public environment variables */
41
43
  env?: Record<string, string>;
42
44
  /** Serialized data from query/form/command functions */
43
- query?: Record<string, any>;
44
- /** Serialized data from prerender functions */
45
- prerender?: Record<string, any>;
45
+ data?: RemoteFunctionData;
46
46
  /** Create a placeholder promise */
47
47
  defer?: (id: number) => Promise<any>;
48
48
  /** Resolve a placeholder promise */