svelte 5.57.1 → 5.57.2

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 (30) hide show
  1. package/compiler/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/compiler/errors.js +10 -0
  4. package/src/compiler/migrate/index.js +15 -4
  5. package/src/compiler/phases/1-parse/state/tag.js +2 -0
  6. package/src/compiler/phases/2-analyze/visitors/LetDirective.js +22 -0
  7. package/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +2 -2
  8. package/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +2 -1
  9. package/src/compiler/phases/3-transform/client/visitors/shared/events.js +3 -2
  10. package/src/compiler/phases/scope.js +8 -0
  11. package/src/compiler/print/index.js +3 -1
  12. package/src/index-client.js +1 -1
  13. package/src/internal/client/dev/tracing.js +3 -1
  14. package/src/internal/client/dom/blocks/boundary.js +39 -8
  15. package/src/internal/client/dom/blocks/each.js +8 -6
  16. package/src/internal/client/dom/blocks/svelte-element.js +3 -1
  17. package/src/internal/client/dom/css.js +1 -1
  18. package/src/internal/client/dom/elements/attributes.js +168 -148
  19. package/src/internal/client/dom/elements/bindings/document.js +3 -2
  20. package/src/internal/client/dom/elements/transitions.js +3 -3
  21. package/src/internal/client/error-handling.js +5 -1
  22. package/src/internal/client/reactivity/async.js +5 -0
  23. package/src/internal/client/reactivity/batch.js +12 -4
  24. package/src/internal/client/reactivity/sources.js +3 -3
  25. package/src/internal/client/runtime.js +13 -16
  26. package/src/internal/server/index.js +1 -1
  27. package/src/reactivity/date.js +2 -2
  28. package/src/reactivity/url-search-params.js +1 -1
  29. package/src/version.js +1 -1
  30. package/types/index.d.ts.map +1 -1
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "svelte",
3
3
  "description": "Cybernetically enhanced web apps",
4
4
  "license": "MIT",
5
- "version": "5.57.1",
5
+ "version": "5.57.2",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -1213,6 +1213,16 @@ export function let_directive_invalid_placement(node) {
1213
1213
  e(node, 'let_directive_invalid_placement', `\`let:\` directive at invalid position\nhttps://svelte.dev/e/let_directive_invalid_placement`);
1214
1214
  }
1215
1215
 
1216
+ /**
1217
+ * Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
1218
+ * @param {null | number | NodeLike} node
1219
+ * @param {string} name
1220
+ * @returns {never}
1221
+ */
1222
+ export function let_directive_snippet_conflict(node, name) {
1223
+ e(node, 'let_directive_snippet_conflict', `Cannot use \`let:\` directives on a component that has a \`children\` snippet. Use snippet parameters instead (e.g. \`{#snippet children({ ${name} })}\`)\nhttps://svelte.dev/e/let_directive_snippet_conflict`);
1224
+ }
1225
+
1216
1226
  /**
1217
1227
  * Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax
1218
1228
  * @param {null | number | NodeLike} node
@@ -910,8 +910,10 @@ const instance_script = {
910
910
  start = /** @type {number} */ (parent.start);
911
911
  end = /** @type {number} */ (parent.end);
912
912
  }
913
- while (state.str.original[start] !== '\n') start--;
914
- while (state.str.original[end] !== '\n') end++;
913
+ // remove the declaration's whole line, or what there is of it inside the script
914
+ const script = /** @type {{ start: number; end: number }} */ (state.analysis.instance.ast);
915
+ while (start > script.start && state.str.original[start] !== '\n') start--;
916
+ while (end < script.end && state.str.original[end] !== '\n') end++;
915
917
  state.str.update(start, end, '');
916
918
  }
917
919
  },
@@ -987,8 +989,11 @@ const instance_script = {
987
989
  return;
988
990
  }
989
991
 
992
+ let declared = 0;
990
993
  for (const binding of bindings) {
991
- if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) {
994
+ // a binding this statement declares needs its `let` whether or not it is reassigned
995
+ if (ids.includes(binding.node) || (binding.reassigned && expression_ids.length === 0)) {
996
+ declared++;
992
997
  check_rune_binding('state');
993
998
  const init =
994
999
  binding.kind === 'state'
@@ -1004,7 +1009,13 @@ const instance_script = {
1004
1009
  }
1005
1010
  }
1006
1011
 
1007
- if (expression_ids.length === 0 && bindings.every((b) => b.kind !== 'store_sub')) {
1012
+ // the statement goes only once every target has a declaration standing in for it
1013
+ if (
1014
+ expression_ids.length === 0 &&
1015
+ bindings.length > 0 &&
1016
+ declared === bindings.length &&
1017
+ bindings.every((b) => b.kind !== 'store_sub')
1018
+ ) {
1008
1019
  state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
1009
1020
  return;
1010
1021
  }
@@ -532,6 +532,7 @@ function next(parser) {
532
532
  if (block.type === 'IfBlock') {
533
533
  if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
534
534
  if (parser.eat('if')) e.block_invalid_elseif(start);
535
+ if (block.alternate) e.block_duplicate_clause(start, '{:else}');
535
536
 
536
537
  parser.allow_whitespace();
537
538
 
@@ -580,6 +581,7 @@ function next(parser) {
580
581
 
581
582
  if (block.type === 'EachBlock') {
582
583
  if (!parser.eat('else')) e.expected_token(start, '{:else}');
584
+ if (block.fallback) e.block_duplicate_clause(start, '{:else}');
583
585
 
584
586
  parser.allow_whitespace();
585
587
  parser.eat('}', true);
@@ -1,6 +1,7 @@
1
1
  /** @import { AST } from '#compiler' */
2
2
  /** @import { Context } from '../types' */
3
3
  import * as e from '../../../errors.js';
4
+ import { determine_slot } from '../../../utils/slot.js';
4
5
 
5
6
  /**
6
7
  * @param {AST.LetDirective} node
@@ -21,4 +22,25 @@ export function LetDirective(node, context) {
21
22
  ) {
22
23
  e.let_directive_invalid_placement(node);
23
24
  }
25
+
26
+ // an explicit `children` snippet replaces the default slot content, so the `let:`
27
+ // directive would never be provided. if the component has a `slot` attribute, the
28
+ // directive applies to the component itself instead, which is fine
29
+ if (
30
+ (parent.type === 'Component' ||
31
+ parent.type === 'SvelteComponent' ||
32
+ parent.type === 'SvelteSelf') &&
33
+ !determine_slot(parent) &&
34
+ parent.fragment.nodes.some(
35
+ (child) => child.type === 'SnippetBlock' && child.expression.name === 'children'
36
+ )
37
+ ) {
38
+ const { expression } = node;
39
+ const pattern =
40
+ expression === null || (expression.type === 'Identifier' && expression.name === node.name)
41
+ ? node.name
42
+ : `${node.name}: ${context.state.analysis.source.slice(expression.start, expression.end)}`;
43
+
44
+ e.let_directive_snippet_conflict(node, pattern);
45
+ }
24
46
  }
@@ -173,8 +173,8 @@ export function check_element(node, context) {
173
173
  current_role === get_implicit_role(node.name, attribute_map) &&
174
174
  // <ul role="list"> is ok because CSS list-style:none removes the semantics and this is a way to bring them back
175
175
  !['ul', 'ol', 'li', 'menu'].includes(node.name) &&
176
- // <a role="link" /> is ok because without href the a tag doesn't have a role of link
177
- !(node.name === 'a' && !attribute_map.has('href'))
176
+ // <a role="link" /> and <area role="link" /> are ok because without href they don't have a role of link
177
+ !((node.name === 'a' || node.name === 'area') && !attribute_map.has('href'))
178
178
  ) {
179
179
  w.a11y_no_redundant_roles(attribute, current_role);
180
180
  }
@@ -18,7 +18,8 @@ export function AwaitExpression(node, context) {
18
18
  // in dev, note which values are read inside a reactive expression,
19
19
  // but don't track them
20
20
  else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
21
- return b.call(b.await(b.call('$.track_reactivity_loss', argument)));
21
+ // don't use b.await here, this way we preserve location information
22
+ return b.call({ ...node, argument: b.call('$.track_reactivity_loss', argument) });
22
23
  }
23
24
 
24
25
  return argument === node.argument ? node : { ...node, argument };
@@ -119,8 +119,9 @@ export function build_event_handler(node, metadata, context) {
119
119
 
120
120
  // local variable can be assigned directly
121
121
  // except in dev mode where when need $.apply()
122
- // in order to handle warnings.
123
- if (!dev && binding?.declaration_kind !== 'import') {
122
+ // in order to handle warnings, or when it is
123
+ // assigned after a top-level `await`
124
+ if (!dev && binding?.declaration_kind !== 'import' && !binding?.blocker) {
124
125
  return handler;
125
126
  }
126
127
  }
@@ -1343,6 +1343,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
1343
1343
  }
1344
1344
 
1345
1345
  context.next({ scope: child_scope });
1346
+
1347
+ // the parameters and what the body declares end up in the same function
1348
+ const body_scope = /** @type {Scope} */ (scopes.get(node.body));
1349
+ for (const [name, binding] of body_scope.declarations) {
1350
+ if (child_scope.declarations.has(name)) {
1351
+ e.declaration_duplicate(binding.node, name);
1352
+ }
1353
+ }
1346
1354
  },
1347
1355
 
1348
1356
  Fragment: (node, context) => {
@@ -754,7 +754,9 @@ const svelte_visitors = (comments, state) => ({
754
754
  if (node.pending) {
755
755
  context.write('}');
756
756
  block(context, node.pending, state.preserve_whitespace > 0);
757
- context.write('{:');
757
+ if (node.then || node.catch) {
758
+ context.write('{:');
759
+ }
758
760
  } else {
759
761
  context.write(' ');
760
762
  }
@@ -17,7 +17,7 @@ if (DEV) {
17
17
  if (!(rune in globalThis)) {
18
18
  // TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
19
19
  /** @type {any} */
20
- let value; // let's hope noone modifies this global, but belts and braces
20
+ let value; // let's hope no one modifies this global, but belts and braces
21
21
  Object.defineProperty(globalThis, rune, {
22
22
  configurable: true,
23
23
  // eslint-disable-next-line getter-return
@@ -131,8 +131,10 @@ export function trace(label, fn) {
131
131
  }
132
132
 
133
133
  /**
134
- * @param {Value} source
134
+ * @template {Value} T
135
+ * @param {T} source
135
136
  * @param {string} label
137
+ * @returns {T}
136
138
  */
137
139
  export function tag(source, label) {
138
140
  source.label = label;
@@ -1,6 +1,16 @@
1
1
  /** @import { Effect, Source, TemplateNode, } from '#client' */
2
- import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
3
- import { HYDRATION_START_ELSE, HYDRATION_START_FAILED } from '../../../../constants.js';
2
+ import {
3
+ BOUNDARY_EFFECT,
4
+ DESTROYED,
5
+ DESTROYING,
6
+ EFFECT_PRESERVED,
7
+ EFFECT_TRANSPARENT
8
+ } from '#client/constants';
9
+ import {
10
+ HYDRATION_ERROR,
11
+ HYDRATION_START_ELSE,
12
+ HYDRATION_START_FAILED
13
+ } from '../../../../constants.js';
4
14
  import { component_context, set_component_context } from '../../context.js';
5
15
  import { invoke_error_boundary } from '../../error-handling.js';
6
16
  import {
@@ -22,6 +32,7 @@ import {
22
32
  hydrate_node,
23
33
  hydrating,
24
34
  next,
35
+ read_hydration_instruction,
25
36
  skip_nodes,
26
37
  set_hydrate_node
27
38
  } from '../hydration.js';
@@ -154,16 +165,18 @@ export class Boundary {
154
165
 
155
166
  this.#effect = block(() => {
156
167
  if (hydrating) {
157
- const comment = /** @type {Comment} */ (this.#hydrate_open);
168
+ const comment_data = read_hydration_instruction(
169
+ /** @type {TemplateNode} */ (this.#hydrate_open)
170
+ );
158
171
  hydrate_next();
159
172
 
160
- const server_rendered_pending = comment.data === HYDRATION_START_ELSE;
161
- const server_rendered_failed = comment.data.startsWith(HYDRATION_START_FAILED);
173
+ const server_rendered_pending = comment_data === HYDRATION_START_ELSE;
174
+ const server_rendered_failed = comment_data.startsWith(HYDRATION_START_FAILED);
162
175
 
163
176
  if (server_rendered_failed) {
164
177
  // Server rendered the failed snippet - hydrate it.
165
178
  // The serialized error is embedded in the comment: <!--[?<json>-->
166
- const serialized_error = JSON.parse(comment.data.slice(HYDRATION_START_FAILED.length));
179
+ const serialized_error = JSON.parse(comment_data.slice(HYDRATION_START_FAILED.length));
167
180
  this.#hydrate_failed_content(serialized_error);
168
181
  } else if (server_rendered_pending) {
169
182
  this.#hydrate_pending_content();
@@ -220,6 +233,8 @@ export class Boundary {
220
233
  var calling_on_error = false;
221
234
 
222
235
  const reset = () => {
236
+ if (this.#is_destroyed()) return;
237
+
223
238
  if (did_reset) {
224
239
  w.svelte_boundary_reset_noop();
225
240
  return;
@@ -243,6 +258,8 @@ export class Boundary {
243
258
  };
244
259
 
245
260
  const invoke_onerror = () => {
261
+ if (this.#is_destroyed()) return;
262
+
246
263
  try {
247
264
  calling_on_error = true;
248
265
  this.#props.onerror?.(error, reset);
@@ -255,6 +272,10 @@ export class Boundary {
255
272
  return { reset, invoke_onerror };
256
273
  }
257
274
 
275
+ #is_destroyed() {
276
+ return (this.#effect.f & (DESTROYED | DESTROYING)) !== 0;
277
+ }
278
+
258
279
  #hydrate_pending_content() {
259
280
  const pending = this.#props.pending;
260
281
  if (!pending) return;
@@ -263,6 +284,8 @@ export class Boundary {
263
284
  this.#pending_effect = branch(() => pending(this.#anchor));
264
285
 
265
286
  queue_micro_task(() => {
287
+ if (this.#is_destroyed()) return;
288
+
266
289
  var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
267
290
  var anchor = create_text();
268
291
  var handled = false;
@@ -445,6 +468,10 @@ export class Boundary {
445
468
 
446
469
  /** @param {unknown} error */
447
470
  error(error) {
471
+ if (error === HYDRATION_ERROR) {
472
+ throw error;
473
+ }
474
+
448
475
  // If we have nothing to capture the error, or if we hit an error while
449
476
  // rendering the fallback, re-throw for another boundary to handle
450
477
  if (!this.#props.onerror && !this.#props.failed) {
@@ -457,7 +484,7 @@ export class Boundary {
457
484
  if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
458
485
 
459
486
  current_batch.oncommit(() => {
460
- this.#handle_error(error);
487
+ if (!this.#is_destroyed()) this.#handle_error(error);
461
488
  });
462
489
  } else {
463
490
  this.#handle_error(error);
@@ -493,11 +520,13 @@ export class Boundary {
493
520
 
494
521
  /** @param {unknown} transformed_error */
495
522
  const handle_error_result = (transformed_error) => {
523
+ if (this.#is_destroyed()) return;
524
+
496
525
  const { reset, invoke_onerror } = this.#create_reset(transformed_error);
497
526
 
498
527
  invoke_onerror();
499
528
 
500
- if (failed) {
529
+ if (failed && !this.#is_destroyed()) {
501
530
  this.#failed_effect = this.#run(() => {
502
531
  try {
503
532
  return branch(() => {
@@ -523,6 +552,8 @@ export class Boundary {
523
552
  };
524
553
 
525
554
  queue_micro_task(() => {
555
+ if (this.#is_destroyed()) return;
556
+
526
557
  // Run the error through the API-level transformError transform (e.g. SvelteKit's handleError)
527
558
  /** @type {unknown} */
528
559
  var result;
@@ -1,4 +1,4 @@
1
- /** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
1
+ /** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, TemplateNode, TransitionManager } from '#client' */
2
2
  /** @import { Batch } from '../../reactivity/batch.js'; */
3
3
  import {
4
4
  EACH_INDEX_REACTIVE,
@@ -216,9 +216,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
216
216
  tag(each_array, '{#each ...}');
217
217
  }
218
218
 
219
- /** @type {V[]} */
220
- var array;
221
-
222
219
  /** @type {Map<Batch, Set<any>>} */
223
220
  var pending = new Map();
224
221
 
@@ -234,6 +231,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
234
231
 
235
232
  state.pending.delete(batch);
236
233
 
234
+ // The effect doesn't necessarily re-run in a batch right before that batch commits
235
+ // (its view of the collection may not have changed), so we read the collection
236
+ // as the committing batch sees it rather than using the most recent block run's result
237
+ var array = get(each_array);
238
+
237
239
  state.fallback = fallback;
238
240
  reconcile(state, array, anchor, flags, get_key);
239
241
 
@@ -264,7 +266,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
264
266
  }
265
267
 
266
268
  var effect = block(() => {
267
- array = /** @type {V[]} */ (get(each_array));
269
+ var array = /** @type {V[]} */ (get(each_array));
268
270
  var length = array.length;
269
271
 
270
272
  /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@@ -664,7 +666,7 @@ function reconcile(state, array, anchor, flags, get_key) {
664
666
  * @param {V} value
665
667
  * @param {unknown} key
666
668
  * @param {number} index
667
- * @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>, collection: () => V[]) => void} render_fn
669
+ * @param {(anchor: Node, item: MaybeSource<V>, index: MaybeSource<number>, collection: () => V[]) => void} render_fn
668
670
  * @param {number} flags
669
671
  * @param {() => V[]} get_collection
670
672
  * @returns {EachItem}
@@ -71,6 +71,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
71
71
 
72
72
  branches.ensure(next_tag, (anchor) => {
73
73
  if (next_tag) {
74
+ var is_hydrating = hydrating;
74
75
  element = hydrating ? /** @type {Element} */ (element) : create_element(next_tag, ns);
75
76
 
76
77
  if (DEV && location) {
@@ -123,7 +124,8 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
123
124
  // we do this after calling `render_fn` so that child effects don't override `nodes.end`
124
125
  /** @type {Effect & { nodes: EffectNodes }} */ (active_effect).nodes.end = element;
125
126
 
126
- anchor.before(element);
127
+ // we only move the node if we are not hydrating since a claimed element is already in place
128
+ if (!is_hydrating) anchor.before(element);
127
129
  }
128
130
 
129
131
  if (hydrating) {
@@ -12,7 +12,7 @@ export function append_styles(anchor, css) {
12
12
  // Use an effect to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
13
13
  effect(() => {
14
14
  // Bit of a hack: branches.js/each.js use offscreen fragments with temporary text nodes that will
15
- // never be connected to the real dom. Therfore walk up to the branch that has created the component
15
+ // never be connected to the real dom. Therefore walk up to the branch that has created the component
16
16
  // whose styles we want to append, and check its node instead. It will be connected by the time we get here.
17
17
  anchor = active_effect?.parent?.nodes?.start ?? anchor;
18
18
  var root = anchor.getRootNode();