@sveltejs/kit 2.57.1 → 2.59.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 (35) hide show
  1. package/package.json +2 -2
  2. package/src/exports/internal/remote-functions.js +1 -1
  3. package/src/exports/public.d.ts +107 -16
  4. package/src/runtime/app/paths/client.js +7 -0
  5. package/src/runtime/app/paths/server.js +7 -0
  6. package/src/runtime/app/server/remote/command.js +7 -6
  7. package/src/runtime/app/server/remote/form.js +2 -1
  8. package/src/runtime/app/server/remote/prerender.js +3 -4
  9. package/src/runtime/app/server/remote/query.js +327 -113
  10. package/src/runtime/app/server/remote/requested.js +127 -32
  11. package/src/runtime/app/server/remote/shared.js +89 -20
  12. package/src/runtime/client/client.js +92 -62
  13. package/src/runtime/client/ndjson.js +42 -0
  14. package/src/runtime/client/remote-functions/command.svelte.js +8 -3
  15. package/src/runtime/client/remote-functions/form.svelte.js +24 -6
  16. package/src/runtime/client/remote-functions/index.js +3 -1
  17. package/src/runtime/client/remote-functions/query-batch.svelte.js +105 -0
  18. package/src/runtime/client/remote-functions/query-live.svelte.js +636 -0
  19. package/src/runtime/client/remote-functions/query.svelte.js +48 -148
  20. package/src/runtime/client/remote-functions/shared.svelte.js +76 -23
  21. package/src/runtime/form-utils.js +33 -12
  22. package/src/runtime/server/page/index.js +26 -16
  23. package/src/runtime/server/page/load_data.js +4 -2
  24. package/src/runtime/server/page/render.js +21 -18
  25. package/src/runtime/server/remote.js +117 -9
  26. package/src/runtime/server/respond.js +11 -8
  27. package/src/runtime/server/utils.js +10 -0
  28. package/src/runtime/shared.js +3 -3
  29. package/src/runtime/utils.js +0 -1
  30. package/src/types/internal.d.ts +54 -14
  31. package/src/utils/page_nodes.js +1 -0
  32. package/src/utils/url.js +3 -3
  33. package/src/version.js +1 -1
  34. package/types/index.d.ts +182 -30
  35. package/types/index.d.ts.map +8 -4
@@ -15,7 +15,7 @@ import { create_server_routing_response, generate_route_object } from './server_
15
15
  import { add_resolution_suffix } from '../../pathname.js';
16
16
  import { try_get_request_store, with_request_store } from '@sveltejs/kit/internal/server';
17
17
  import { text_encoder } from '../../utils.js';
18
- import { get_global_name, handle_error_and_jsonify } from '../utils.js';
18
+ import { count_non_ssi_comments, get_global_name, handle_error_and_jsonify } from '../utils.js';
19
19
  import { create_remote_key } from '../../shared.js';
20
20
  import { get_status } from '../../../utils/error.js';
21
21
 
@@ -267,25 +267,25 @@ export async function render_response({
267
267
 
268
268
  paths.reset(); // just in case `options.root.render(...)` failed
269
269
  }
270
+ } else {
271
+ rendered = { head: '', html: '', css: { code: '', map: null }, hashes: { script: [] } };
272
+ }
270
273
 
271
- for (const { node } of branch) {
272
- for (const url of node.imports) modulepreloads.add(url);
273
- for (const url of node.stylesheets) stylesheets.add(url);
274
- for (const url of node.fonts) fonts.add(url);
274
+ for (const { node } of branch) {
275
+ for (const url of node.imports) modulepreloads.add(url);
276
+ for (const url of node.stylesheets) stylesheets.add(url);
277
+ for (const url of node.fonts) fonts.add(url);
275
278
 
276
- if (node.inline_styles && !client.inline) {
277
- Object.entries(await node.inline_styles()).forEach(([filename, css]) => {
278
- if (typeof css === 'string') {
279
- inline_styles.set(filename, css);
280
- return;
281
- }
279
+ if (node.inline_styles && !client.inline) {
280
+ Object.entries(await node.inline_styles()).forEach(([filename, css]) => {
281
+ if (typeof css === 'string') {
282
+ inline_styles.set(filename, css);
283
+ return;
284
+ }
282
285
 
283
- inline_styles.set(filename, css(`${assets}/${paths.app_dir}/immutable/assets`, assets));
284
- });
285
- }
286
+ inline_styles.set(filename, css(`${assets}/${paths.app_dir}/immutable/assets`, assets));
287
+ });
286
288
  }
287
- } else {
288
- rendered = { head: '', html: '', css: { code: '', map: null }, hashes: { script: [] } };
289
289
  }
290
290
 
291
291
  const head = new Head(rendered.head, !!state.prerendering);
@@ -529,7 +529,10 @@ export async function render_response({
529
529
 
530
530
  const store = internals.type === 'prerender' ? prerender : query;
531
531
 
532
- if (event_state.remote.refreshes?.[remote_key] !== undefined) {
532
+ if (
533
+ event_state.remote.refreshes?.has(remote_key) ||
534
+ event_state.remote.reconnects?.has(remote_key)
535
+ ) {
533
536
  // This entry was refreshed/set by a command or form action.
534
537
  // Always await it so the mutation result is serialized.
535
538
  store[remote_key] = await entry.data;
@@ -691,7 +694,7 @@ export async function render_response({
691
694
 
692
695
  if (DEV) {
693
696
  if (page_config.csr) {
694
- if (transformed.split('<!--').length < html.split('<!--').length) {
697
+ if (count_non_ssi_comments(transformed) < count_non_ssi_comments(html)) {
695
698
  // the \u001B stuff is ANSI codes, so that we don't need to add a library to the runtime
696
699
  // https://svelte.dev/playground/1b3f49696f0c44c881c34587f2537aa2?version=4.2.19
697
700
  console.warn(
@@ -121,7 +121,12 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
121
121
  /** @type {RemoteFunctionResponse} */ ({
122
122
  type: 'result',
123
123
  result: stringify(result, transport),
124
- refreshes: result.issues ? undefined : await serialize_refreshes()
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)
125
130
  })
126
131
  );
127
132
  }
@@ -137,11 +142,115 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
137
142
  /** @type {RemoteFunctionResponse} */ ({
138
143
  type: 'result',
139
144
  result: stringify(data, transport),
140
- refreshes: await serialize_refreshes()
145
+ refreshes: await serialize_singleflight(state.remote.refreshes),
146
+ reconnects: await serialize_singleflight(state.remote.reconnects)
141
147
  })
142
148
  );
143
149
  }
144
150
 
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}`
157
+ );
158
+ }
159
+
160
+ const payload = /** @type {string} */ (
161
+ new URL(event.request.url).searchParams.get('payload')
162
+ );
163
+
164
+ const generator = internals.run(event, state, parse_remote_arg(payload, transport));
165
+
166
+ const encoder = new TextEncoder();
167
+
168
+ /**
169
+ * @param {ReadableStreamDefaultController} controller
170
+ * @param {any} payload
171
+ */
172
+ function send(controller, payload) {
173
+ controller.enqueue(encoder.encode(JSON.stringify(payload) + '\n'));
174
+ }
175
+
176
+ let closed = false;
177
+
178
+ /** @type {string | undefined} */
179
+ let result = undefined;
180
+
181
+ async function cancel() {
182
+ if (closed) return;
183
+ closed = true;
184
+ await generator.return(undefined);
185
+ }
186
+
187
+ event.request.signal.addEventListener('abort', cancel, { once: true });
188
+
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
+ }
197
+
198
+ try {
199
+ while (true) {
200
+ const { value, done } = await generator.next();
201
+
202
+ if (done) {
203
+ await cancel();
204
+ controller.close();
205
+ return;
206
+ }
207
+
208
+ // only send changed data
209
+ if (result !== (result = stringify(value, transport))) {
210
+ send(controller, {
211
+ type: 'result',
212
+ result
213
+ });
214
+
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
+ }
238
+
239
+ await cancel();
240
+ controller.close();
241
+ }
242
+ },
243
+ cancel
244
+ }),
245
+ {
246
+ headers: {
247
+ 'cache-control': 'private, no-store',
248
+ 'content-type': 'application/x-ndjson'
249
+ }
250
+ }
251
+ );
252
+ }
253
+
145
254
  const payload =
146
255
  internals.type === 'prerender'
147
256
  ? additional_args
@@ -166,7 +275,8 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
166
275
  /** @type {RemoteFunctionResponse} */ ({
167
276
  type: 'redirect',
168
277
  location: error.location,
169
- refreshes: await serialize_refreshes()
278
+ refreshes: await serialize_singleflight(state.remote.refreshes),
279
+ reconnects: await serialize_singleflight(state.remote.reconnects)
170
280
  })
171
281
  );
172
282
  }
@@ -191,16 +301,14 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
191
301
  );
192
302
  }
193
303
 
194
- async function serialize_refreshes() {
195
- const refreshes = state.remote.refreshes ?? {};
196
-
197
- const entries = Object.entries(refreshes);
198
- if (entries.length === 0) {
304
+ /** @param {Map<string, Promise<any>> | null} map */
305
+ async function serialize_singleflight(map) {
306
+ if (!map || map.size === 0) {
199
307
  return undefined;
200
308
  }
201
309
 
202
310
  const results = await Promise.all(
203
- entries.map(async ([key, promise]) => {
311
+ Array.from(map, async ([key, promise]) => {
204
312
  try {
205
313
  return [key, { type: 'result', data: await promise }];
206
314
  } catch (error) {
@@ -1,4 +1,4 @@
1
- /** @import { RequestState } from 'types' */
1
+ /** @import { RequestState, SSRNode } from 'types' */
2
2
  import { DEV } from 'esm-env';
3
3
  import { json, text } from '@sveltejs/kit';
4
4
  import { Redirect, SvelteKitError } from '@sveltejs/kit/internal';
@@ -152,13 +152,9 @@ export async function internal_respond(request, options, manifest, state) {
152
152
  forms: null,
153
153
  /** A map of remote function key to corresponding single-flight-mutation promise */
154
154
  refreshes: null,
155
+ reconnects: null,
155
156
  /** A map of remote function ID to payloads requested for refreshing by the client */
156
- requested: null,
157
- /**
158
- * A map of remote function ID to objects that have passed validation;
159
- * used to prevent revalidating parameters returned from `requested`
160
- */
161
- validated: null
157
+ requested: null
162
158
  },
163
159
  is_in_remote_function: false,
164
160
  is_in_render: false,
@@ -566,7 +562,14 @@ export async function internal_respond(request, options, manifest, state) {
566
562
  page_config: { ssr: false, csr: true },
567
563
  status: 200,
568
564
  error: null,
569
- branch: [],
565
+ branch: [
566
+ // include the root layout because it applies to every page
567
+ {
568
+ node: /** @type {SSRNode} */ (await manifest._.nodes[0]()),
569
+ data: null,
570
+ server_data: null
571
+ }
572
+ ],
570
573
  fetched: [],
571
574
  resolve_opts,
572
575
  data_serializer: server_data_serializer(event, event_state, options)
@@ -251,3 +251,13 @@ export function get_node_type(node_id) {
251
251
  const dot_parts = filename.split('.');
252
252
  return dot_parts.slice(0, -1).join('.');
253
253
  }
254
+
255
+ /**
256
+ * Counts HTML comments that are not SSI directives (which start with `<!--#`).
257
+ * Used to detect when `transformPageChunk` removes comments that Svelte needs for hydration.
258
+ * @param {string} str
259
+ * @returns {number}
260
+ */
261
+ export function count_non_ssi_comments(str) {
262
+ return (str.match(/<!--(?!#)/g) ?? []).length;
263
+ }
@@ -1,6 +1,6 @@
1
1
  /** @import { Transport } from '@sveltejs/kit' */
2
2
  import * as devalue from 'devalue';
3
- import { base64_decode, base64_encode, text_decoder } from './utils.js';
3
+ import { base64_decode, base64_encode, text_encoder } from './utils.js';
4
4
  import * as svelte from 'svelte';
5
5
 
6
6
  /**
@@ -261,7 +261,7 @@ export function stringify_remote_arg(value, transport, sort = true) {
261
261
  create_remote_arg_reducers(transport, sort, new Map())
262
262
  );
263
263
 
264
- const bytes = new TextEncoder().encode(json_string);
264
+ const bytes = text_encoder.encode(json_string);
265
265
  return base64_encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
266
266
  }
267
267
 
@@ -273,7 +273,7 @@ export function stringify_remote_arg(value, transport, sort = true) {
273
273
  export function parse_remote_arg(string, transport) {
274
274
  if (!string) return undefined;
275
275
 
276
- const json_string = text_decoder.decode(
276
+ const json_string = new TextDecoder().decode(
277
277
  // no need to add back `=` characters, atob can handle it
278
278
  base64_decode(string.replaceAll('-', '+').replaceAll('_', '/'))
279
279
  );
@@ -1,7 +1,6 @@
1
1
  import { BROWSER } from 'esm-env';
2
2
 
3
3
  export const text_encoder = new TextEncoder();
4
- export const text_decoder = new TextDecoder();
5
4
 
6
5
  /**
7
6
  * Like node's path.relative, but without using node
@@ -22,7 +22,9 @@ import {
22
22
  ClientInit,
23
23
  Transport,
24
24
  HandleValidationError,
25
- RemoteFormIssue
25
+ RemoteFormIssue,
26
+ RemoteQuery,
27
+ RemoteLiveQuery
26
28
  } from '@sveltejs/kit';
27
29
  import {
28
30
  HttpMethod,
@@ -303,6 +305,8 @@ export type RemoteFunctionResponse =
303
305
  | (ServerRedirectNode & {
304
306
  /** devalue'd Record<string, any> */
305
307
  refreshes?: string;
308
+ /** devalue'd Record<string, any> */
309
+ reconnects?: string;
306
310
  })
307
311
  | ServerErrorNode
308
312
  | {
@@ -310,22 +314,33 @@ export type RemoteFunctionResponse =
310
314
  result: string;
311
315
  /** devalue'd Record<string, any> */
312
316
  refreshes: string | undefined;
317
+ /** devalue'd Record<string, any> */
318
+ reconnects: string | undefined;
313
319
  };
314
320
 
315
- export type RemoteRefreshResult = {
321
+ export type RemoteSingleflightResult = {
316
322
  type: 'result';
317
323
  data: any;
318
324
  };
319
325
 
320
- export type RemoteRefreshError = {
326
+ export type RemoteSingleflightError = {
321
327
  type: 'error';
322
328
  status?: number;
323
329
  error: App.Error;
324
330
  };
325
331
 
326
- export type RemoteRefreshEntry = RemoteRefreshResult | RemoteRefreshError;
332
+ export type RemoteSingleflightEntry = RemoteSingleflightResult | RemoteSingleflightError;
327
333
 
328
- export type RemoteRefreshMap = Record<string, RemoteRefreshEntry>;
334
+ export type RemoteSingleflightMap = Record<string, RemoteSingleflightEntry>;
335
+
336
+ export type RemoteLiveQueryUserFunctionReturnType<Output> = MaybePromise<
337
+ | AsyncGenerator<Output>
338
+ | AsyncIterator<Output>
339
+ | AsyncIterable<Output>
340
+ | Generator<Output>
341
+ | Iterator<Output>
342
+ | Iterable<Output>
343
+ >;
329
344
 
330
345
  /**
331
346
  * Signals a successful response of the server `load` function.
@@ -593,19 +608,41 @@ interface BaseRemoteInternals {
593
608
  export interface RemoteQueryInternals extends BaseRemoteInternals {
594
609
  type: 'query';
595
610
  validate: (arg?: any) => MaybePromise<any>;
611
+ /**
612
+ * Creates a `RemoteQuery` bound directly to a specific client payload (the
613
+ * stringified raw argument) and a pre-validated argument, skipping the query
614
+ * wrapper's re-validation step. Used by `requested(query)` to ensure
615
+ * `refresh()` / `set()` target the same cache key the client is listening on
616
+ * even when the schema transforms the input.
617
+ */
618
+ bind(payload: string, arg: any): RemoteQuery<any>;
596
619
  }
597
620
  export interface RemoteQueryLiveInternals extends BaseRemoteInternals {
598
621
  type: 'query_live';
599
- run(
600
- event: RequestEvent,
601
- state: RequestState,
602
- arg: any
603
- ): Promise<{ iterator: AsyncIterator<any>; cancel: () => void }>;
622
+ validate: (arg?: any) => MaybePromise<any>;
623
+ run(event: RequestEvent, state: RequestState, arg: any): AsyncGenerator<any>;
624
+ /**
625
+ * Creates a `RemoteLiveQuery` bound directly to a specific client payload (the
626
+ * stringified raw argument) and a pre-validated argument, skipping the query
627
+ * wrapper's re-validation step. Used by `requested(liveQuery)` to ensure
628
+ * `reconnect()` targets the same cache key the client is listening on even
629
+ * when the schema transforms the input.
630
+ */
631
+ bind(payload: string, arg: any): RemoteLiveQuery<any>;
604
632
  }
605
633
 
606
634
  export interface RemoteQueryBatchInternals extends BaseRemoteInternals {
607
635
  type: 'query_batch';
636
+ validate: (arg?: any) => MaybePromise<any>;
608
637
  run: (args: any[], options: SSROptions) => Promise<any[]>;
638
+ /**
639
+ * Creates a `RemoteQuery` bound directly to a specific client payload (the
640
+ * stringified raw argument) and a pre-validated argument, skipping the query
641
+ * wrapper's re-validation step. Used by `requested(batchQuery)` to ensure
642
+ * `refresh()` / `set()` target the same cache key the client is listening on
643
+ * even when the schema transforms the input.
644
+ */
645
+ bind(payload: string, arg: any): RemoteQuery<any>;
609
646
  }
610
647
 
611
648
  export interface RemoteCommandInternals extends BaseRemoteInternals {
@@ -624,10 +661,13 @@ export interface RemotePrerenderInternals extends BaseRemoteInternals {
624
661
  inputs?: RemotePrerenderInputsGenerator;
625
662
  }
626
663
 
627
- export type RemoteInternals =
664
+ export type RemoteAnyQueryInternals =
628
665
  | RemoteQueryInternals
629
- | RemoteQueryLiveInternals
630
666
  | RemoteQueryBatchInternals
667
+ | RemoteQueryLiveInternals;
668
+
669
+ export type RemoteInternals =
670
+ | RemoteAnyQueryInternals
631
671
  | RemoteCommandInternals
632
672
  | RemoteFormInternals
633
673
  | RemotePrerenderInternals;
@@ -661,9 +701,9 @@ export interface RequestState {
661
701
  Record<string, { serialize: boolean; data: MaybePromise<any> }>
662
702
  >;
663
703
  forms: null | Map<any, any>;
664
- refreshes: null | Record<string, Promise<any>>;
704
+ refreshes: null | Map<string, Promise<any>>;
705
+ reconnects: null | Map<string, Promise<any>>;
665
706
  requested: null | Map<string, string[]>;
666
- validated: null | Map<string, Set<any>>;
667
707
  };
668
708
  readonly is_in_remote_function: boolean;
669
709
  readonly is_in_render: boolean;
@@ -6,6 +6,7 @@ import {
6
6
  } from './exports.js';
7
7
 
8
8
  export class PageNodes {
9
+ /** All layout nodes and the page node, if any */
9
10
  data;
10
11
 
11
12
  /**
package/src/utils/url.js CHANGED
@@ -138,12 +138,12 @@ export function make_trackable(url, callback, search_params_callback, allow_hash
138
138
 
139
139
  if (!BROWSER) {
140
140
  // @ts-ignore
141
- tracked[Symbol.for('nodejs.util.inspect.custom')] = (depth, opts, inspect) => {
141
+ tracked[Symbol.for('nodejs.util.inspect.custom')] = (_depth, opts, inspect) => {
142
142
  return inspect(url, opts);
143
143
  };
144
144
 
145
145
  // @ts-ignore
146
- tracked.searchParams[Symbol.for('nodejs.util.inspect.custom')] = (depth, opts, inspect) => {
146
+ tracked.searchParams[Symbol.for('nodejs.util.inspect.custom')] = (_depth, opts, inspect) => {
147
147
  return inspect(url.searchParams, opts);
148
148
  };
149
149
  }
@@ -194,7 +194,7 @@ export function disable_search(url) {
194
194
  function allow_nodejs_console_log(url) {
195
195
  if (!BROWSER) {
196
196
  // @ts-ignore
197
- url[Symbol.for('nodejs.util.inspect.custom')] = (depth, opts, inspect) => {
197
+ url[Symbol.for('nodejs.util.inspect.custom')] = (_depth, opts, inspect) => {
198
198
  return inspect(new URL(url), opts);
199
199
  };
200
200
  }
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.57.1';
4
+ export const VERSION = '2.59.0';