svelte 5.40.2 → 5.41.1

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 (31) hide show
  1. package/compiler/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/compiler/errors.js +11 -2
  4. package/src/compiler/phases/2-analyze/visitors/CallExpression.js +7 -0
  5. package/src/compiler/phases/2-analyze/visitors/EachBlock.js +5 -0
  6. package/src/compiler/phases/3-transform/client/transform-client.js +4 -1
  7. package/src/compiler/phases/3-transform/client/visitors/CallExpression.js +6 -0
  8. package/src/compiler/phases/3-transform/client/visitors/Fragment.js +7 -2
  9. package/src/compiler/phases/3-transform/client/visitors/LetDirective.js +21 -17
  10. package/src/compiler/phases/3-transform/client/visitors/RegularElement.js +1 -1
  11. package/src/compiler/phases/3-transform/client/visitors/SlotElement.js +1 -1
  12. package/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js +1 -1
  13. package/src/compiler/phases/3-transform/client/visitors/shared/component.js +2 -2
  14. package/src/compiler/phases/3-transform/server/visitors/CallExpression.js +4 -0
  15. package/src/internal/client/dom/blocks/await.js +71 -137
  16. package/src/internal/client/dom/blocks/boundary.js +7 -19
  17. package/src/internal/client/dom/blocks/branches.js +185 -0
  18. package/src/internal/client/dom/blocks/if.js +28 -107
  19. package/src/internal/client/dom/blocks/key.js +12 -58
  20. package/src/internal/client/dom/blocks/snippet.js +6 -22
  21. package/src/internal/client/dom/blocks/svelte-component.js +7 -63
  22. package/src/internal/client/dom/blocks/svelte-element.js +34 -45
  23. package/src/internal/client/dom/elements/bindings/select.js +21 -0
  24. package/src/internal/client/index.js +1 -1
  25. package/src/internal/client/reactivity/async.js +24 -8
  26. package/src/internal/client/reactivity/batch.js +66 -24
  27. package/src/internal/client/reactivity/effects.js +20 -2
  28. package/src/utils.js +1 -0
  29. package/src/version.js +1 -1
  30. package/types/index.d.ts +12 -0
  31. 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.40.2",
5
+ "version": "5.41.1",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -986,13 +986,13 @@ export function const_tag_invalid_placement(node) {
986
986
  }
987
987
 
988
988
  /**
989
- * The `{@const %name% = ...}` declaration is not available in this snippet
989
+ * The `{@const %name% = ...}` declaration is not available in this snippet
990
990
  * @param {null | number | NodeLike} node
991
991
  * @param {string} name
992
992
  * @returns {never}
993
993
  */
994
994
  export function const_tag_invalid_reference(node, name) {
995
- e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`);
995
+ e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet\nhttps://svelte.dev/e/const_tag_invalid_reference`);
996
996
  }
997
997
 
998
998
  /**
@@ -1023,6 +1023,15 @@ export function directive_missing_name(node, type) {
1023
1023
  e(node, 'directive_missing_name', `\`${type}\` name cannot be empty\nhttps://svelte.dev/e/directive_missing_name`);
1024
1024
  }
1025
1025
 
1026
+ /**
1027
+ * An `{#each ...}` block without an `as` clause cannot have a key
1028
+ * @param {null | number | NodeLike} node
1029
+ * @returns {never}
1030
+ */
1031
+ export function each_key_without_as(node) {
1032
+ e(node, 'each_key_without_as', `An \`{#each ...}\` block without an \`as\` clause cannot have a key\nhttps://svelte.dev/e/each_key_without_as`);
1033
+ }
1034
+
1026
1035
  /**
1027
1036
  * `</%name%>` attempted to close an element that was not open
1028
1037
  * @param {null | number | NodeLike} node
@@ -226,6 +226,13 @@ export function CallExpression(node, context) {
226
226
  break;
227
227
  }
228
228
 
229
+ case '$state.eager':
230
+ if (node.arguments.length !== 1) {
231
+ e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
232
+ }
233
+
234
+ break;
235
+
229
236
  case '$state.snapshot':
230
237
  if (node.arguments.length !== 1) {
231
238
  e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
@@ -1,3 +1,4 @@
1
+ /** @import { Expression } from 'estree' */
1
2
  /** @import { AST, Binding } from '#compiler' */
2
3
  /** @import { Context } from '../types' */
3
4
  /** @import { Scope } from '../../scope' */
@@ -28,6 +29,10 @@ export function EachBlock(node, context) {
28
29
  node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
29
30
  }
30
31
 
32
+ if (node.metadata.keyed && !node.context) {
33
+ e.each_key_without_as(/** @type {Expression} */ (node.key));
34
+ }
35
+
31
36
  // evaluate expression in parent scope
32
37
  context.visit(node.expression, {
33
38
  ...context.state,
@@ -172,6 +172,7 @@ export function client_component(analysis, options) {
172
172
  // these are set inside the `Fragment` visitor, and cannot be used until then
173
173
  init: /** @type {any} */ (null),
174
174
  consts: /** @type {any} */ (null),
175
+ let_directives: /** @type {any} */ (null),
175
176
  update: /** @type {any} */ (null),
176
177
  after_update: /** @type {any} */ (null),
177
178
  template: /** @type {any} */ (null),
@@ -384,7 +385,9 @@ export function client_component(analysis, options) {
384
385
  .../** @type {ESTree.Statement[]} */ (template.body)
385
386
  ]);
386
387
 
387
- component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true))));
388
+ component_block.body.push(
389
+ b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true)))
390
+ );
388
391
  } else {
389
392
  component_block.body.push(
390
393
  ...state.instance_level_snippets,
@@ -49,6 +49,12 @@ export function CallExpression(node, context) {
49
49
  return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
50
50
  }
51
51
 
52
+ case '$state.eager':
53
+ return b.call(
54
+ '$.eager',
55
+ b.thunk(/** @type {Expression} */ (context.visit(node.arguments[0])))
56
+ );
57
+
52
58
  case '$state.snapshot':
53
59
  return b.call(
54
60
  '$.snapshot',
@@ -63,6 +63,7 @@ export function Fragment(node, context) {
63
63
  ...context.state,
64
64
  init: [],
65
65
  consts: [],
66
+ let_directives: [],
66
67
  update: [],
67
68
  after_update: [],
68
69
  memoizer: new Memoizer(),
@@ -150,7 +151,7 @@ export function Fragment(node, context) {
150
151
  }
151
152
  }
152
153
 
153
- body.push(...state.consts);
154
+ body.push(...state.let_directives, ...state.consts);
154
155
 
155
156
  if (has_await) {
156
157
  body.push(b.if(b.call('$.aborted'), b.return()));
@@ -177,7 +178,11 @@ export function Fragment(node, context) {
177
178
  }
178
179
 
179
180
  if (has_await) {
180
- return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]);
181
+ return b.block([
182
+ b.stmt(
183
+ b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true))
184
+ )
185
+ ]);
181
186
  } else {
182
187
  return b.block(body);
183
188
  }
@@ -21,22 +21,24 @@ export function LetDirective(node, context) {
21
21
  };
22
22
  }
23
23
 
24
- return b.const(
25
- name,
26
- b.call(
27
- '$.derived',
28
- b.thunk(
29
- b.block([
30
- b.let(
31
- /** @type {Expression} */ (node.expression).type === 'ObjectExpression'
32
- ? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
33
- b.object_pattern(node.expression.properties)
34
- : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
35
- b.array_pattern(node.expression.elements),
36
- b.member(b.id('$$slotProps'), node.name)
37
- ),
38
- b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
39
- ])
24
+ context.state.let_directives.push(
25
+ b.const(
26
+ name,
27
+ b.call(
28
+ '$.derived',
29
+ b.thunk(
30
+ b.block([
31
+ b.let(
32
+ /** @type {Expression} */ (node.expression).type === 'ObjectExpression'
33
+ ? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
34
+ b.object_pattern(node.expression.properties)
35
+ : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
36
+ b.array_pattern(node.expression.elements),
37
+ b.member(b.id('$$slotProps'), node.name)
38
+ ),
39
+ b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
40
+ ])
41
+ )
40
42
  )
41
43
  )
42
44
  );
@@ -46,6 +48,8 @@ export function LetDirective(node, context) {
46
48
  read: (node) => b.call('$.get', node)
47
49
  };
48
50
 
49
- return b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)));
51
+ context.state.let_directives.push(
52
+ b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name)))
53
+ );
50
54
  }
51
55
  }
@@ -106,7 +106,7 @@ export function RegularElement(node, context) {
106
106
 
107
107
  case 'LetDirective':
108
108
  // visit let directives before everything else, to set state
109
- lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
109
+ context.visit(attribute, { ...context.state, let_directives: lets });
110
110
  break;
111
111
 
112
112
  case 'OnDirective':
@@ -49,7 +49,7 @@ export function SlotElement(node, context) {
49
49
  }
50
50
  }
51
51
  } else if (attribute.type === 'LetDirective') {
52
- lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
52
+ context.visit(attribute, { ...context.state, let_directives: lets });
53
53
  }
54
54
  }
55
55
 
@@ -9,7 +9,7 @@
9
9
  export function SvelteFragment(node, context) {
10
10
  for (const attribute of node.attributes) {
11
11
  if (attribute.type === 'LetDirective') {
12
- context.state.init.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
12
+ context.visit(attribute);
13
13
  }
14
14
  }
15
15
 
@@ -101,7 +101,7 @@ export function build_component(node, component_name, context) {
101
101
  if (slot_scope_applies_to_itself) {
102
102
  for (const attribute of node.attributes) {
103
103
  if (attribute.type === 'LetDirective') {
104
- lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
104
+ context.visit(attribute, { ...context.state, let_directives: lets });
105
105
  }
106
106
  }
107
107
  }
@@ -109,7 +109,7 @@ export function build_component(node, component_name, context) {
109
109
  for (const attribute of node.attributes) {
110
110
  if (attribute.type === 'LetDirective') {
111
111
  if (!slot_scope_applies_to_itself) {
112
- lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default)));
112
+ context.visit(attribute, { ...states.default, let_directives: lets });
113
113
  }
114
114
  } else if (attribute.type === 'OnDirective') {
115
115
  if (!attribute.expression) {
@@ -38,6 +38,10 @@ export function CallExpression(node, context) {
38
38
  return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
39
39
  }
40
40
 
41
+ if (rune === '$state.eager') {
42
+ return node.arguments[0];
43
+ }
44
+
41
45
  if (rune === '$state.snapshot') {
42
46
  return b.call(
43
47
  '$.snapshot',
@@ -1,12 +1,9 @@
1
- /** @import { Effect, Source, TemplateNode } from '#client' */
2
- import { DEV } from 'esm-env';
1
+ /** @import { Source, TemplateNode } from '#client' */
3
2
  import { is_promise } from '../../../shared/utils.js';
4
- import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
3
+ import { block } from '../../reactivity/effects.js';
5
4
  import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
6
- import { set_active_effect, set_active_reaction } from '../../runtime.js';
7
5
  import {
8
6
  hydrate_next,
9
- hydrate_node,
10
7
  hydrating,
11
8
  skip_nodes,
12
9
  set_hydrate_node,
@@ -14,15 +11,10 @@ import {
14
11
  } from '../hydration.js';
15
12
  import { queue_micro_task } from '../task.js';
16
13
  import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
17
- import {
18
- component_context,
19
- dev_stack,
20
- is_runes,
21
- set_component_context,
22
- set_dev_current_component_function,
23
- set_dev_stack
24
- } from '../../context.js';
14
+ import { is_runes } from '../../context.js';
25
15
  import { flushSync, is_flushing_sync } from '../../reactivity/batch.js';
16
+ import { BranchManager } from './branches.js';
17
+ import { capture, unset_context } from '../../reactivity/async.js';
26
18
 
27
19
  const PENDING = 0;
28
20
  const THEN = 1;
@@ -33,7 +25,7 @@ const CATCH = 2;
33
25
  /**
34
26
  * @template V
35
27
  * @param {TemplateNode} node
36
- * @param {(() => Promise<V>)} get_input
28
+ * @param {(() => any)} get_input
37
29
  * @param {null | ((anchor: Node) => void)} pending_fn
38
30
  * @param {null | ((anchor: Node, value: Source<V>) => void)} then_fn
39
31
  * @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
@@ -44,149 +36,94 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
44
36
  hydrate_next();
45
37
  }
46
38
 
47
- var anchor = node;
48
39
  var runes = is_runes();
49
- var active_component_context = component_context;
50
-
51
- /** @type {any} */
52
- var component_function = DEV ? component_context?.function : null;
53
- var dev_original_stack = DEV ? dev_stack : null;
54
-
55
- /** @type {V | Promise<V> | typeof UNINITIALIZED} */
56
- var input = UNINITIALIZED;
57
-
58
- /** @type {Effect | null} */
59
- var pending_effect;
60
-
61
- /** @type {Effect | null} */
62
- var then_effect;
63
-
64
- /** @type {Effect | null} */
65
- var catch_effect;
66
-
67
- var input_source = runes
68
- ? source(/** @type {V} */ (undefined))
69
- : mutable_source(/** @type {V} */ (undefined), false, false);
70
- var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
71
- var resolved = false;
72
- /**
73
- * @param {AwaitState} state
74
- * @param {boolean} restore
75
- */
76
- function update(state, restore) {
77
- resolved = true;
78
-
79
- if (restore) {
80
- set_active_effect(effect);
81
- set_active_reaction(effect); // TODO do we need both?
82
- set_component_context(active_component_context);
83
- if (DEV) {
84
- set_dev_current_component_function(component_function);
85
- set_dev_stack(dev_original_stack);
86
- }
87
- }
88
-
89
- try {
90
- if (state === PENDING && pending_fn) {
91
- if (pending_effect) resume_effect(pending_effect);
92
- else pending_effect = branch(() => pending_fn(anchor));
93
- }
94
-
95
- if (state === THEN && then_fn) {
96
- if (then_effect) resume_effect(then_effect);
97
- else then_effect = branch(() => then_fn(anchor, input_source));
98
- }
99
-
100
- if (state === CATCH && catch_fn) {
101
- if (catch_effect) resume_effect(catch_effect);
102
- else catch_effect = branch(() => catch_fn(anchor, error_source));
103
- }
104
-
105
- if (state !== PENDING && pending_effect) {
106
- pause_effect(pending_effect, () => (pending_effect = null));
107
- }
108
-
109
- if (state !== THEN && then_effect) {
110
- pause_effect(then_effect, () => (then_effect = null));
111
- }
112
-
113
- if (state !== CATCH && catch_effect) {
114
- pause_effect(catch_effect, () => (catch_effect = null));
115
- }
116
- } finally {
117
- if (restore) {
118
- if (DEV) {
119
- set_dev_current_component_function(null);
120
- set_dev_stack(null);
121
- }
122
40
 
123
- set_component_context(null);
124
- set_active_reaction(null);
125
- set_active_effect(null);
41
+ var v = /** @type {V} */ (UNINITIALIZED);
42
+ var value = runes ? source(v) : mutable_source(v, false, false);
43
+ var error = runes ? source(v) : mutable_source(v, false, false);
126
44
 
127
- // without this, the DOM does not update until two ticks after the promise
128
- // resolves, which is unexpected behaviour (and somewhat irksome to test)
129
- if (!is_flushing_sync) flushSync();
130
- }
131
- }
132
- }
45
+ var branches = new BranchManager(node);
133
46
 
134
- var effect = block(() => {
135
- if (input === (input = get_input())) return;
47
+ block(() => {
48
+ var input = get_input();
49
+ var destroyed = false;
136
50
 
137
51
  /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
138
- // @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight
139
- let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE);
52
+ // @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight
53
+ let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE);
140
54
 
141
55
  if (mismatch) {
142
56
  // Hydration mismatch: remove everything inside the anchor and start fresh
143
- anchor = skip_nodes();
144
-
145
- set_hydrate_node(anchor);
57
+ set_hydrate_node(skip_nodes());
146
58
  set_hydrating(false);
147
- mismatch = true;
148
59
  }
149
60
 
150
61
  if (is_promise(input)) {
151
- var promise = input;
62
+ var restore = capture();
63
+ var resolved = false;
64
+
65
+ /**
66
+ * @param {() => void} fn
67
+ */
68
+ const resolve = (fn) => {
69
+ if (destroyed) return;
70
+
71
+ resolved = true;
72
+ restore();
73
+
74
+ if (hydrating) {
75
+ // `restore()` could set `hydrating` to `true`, which we very much
76
+ // don't want — we want to restore everything _except_ this
77
+ set_hydrating(false);
78
+ }
152
79
 
153
- resolved = false;
80
+ try {
81
+ fn();
82
+ } finally {
83
+ unset_context();
154
84
 
155
- promise.then(
156
- (value) => {
157
- if (promise !== input) return;
158
- // we technically could use `set` here since it's on the next microtick
159
- // but let's use internal_set for consistency and just to be safe
160
- internal_set(input_source, value);
161
- update(THEN, true);
85
+ // without this, the DOM does not update until two ticks after the promise
86
+ // resolves, which is unexpected behaviour (and somewhat irksome to test)
87
+ if (!is_flushing_sync) flushSync();
88
+ }
89
+ };
90
+
91
+ input.then(
92
+ (v) => {
93
+ resolve(() => {
94
+ internal_set(value, v);
95
+ branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
96
+ });
162
97
  },
163
- (error) => {
164
- if (promise !== input) return;
165
- // we technically could use `set` here since it's on the next microtick
166
- // but let's use internal_set for consistency and just to be safe
167
- internal_set(error_source, error);
168
- update(CATCH, true);
169
- if (!catch_fn) {
170
- // Rethrow the error if no catch block exists
171
- throw error_source.v;
172
- }
98
+ (e) => {
99
+ resolve(() => {
100
+ internal_set(error, e);
101
+ branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error)));
102
+
103
+ if (!catch_fn) {
104
+ // Rethrow the error if no catch block exists
105
+ throw error.v;
106
+ }
107
+ });
173
108
  }
174
109
  );
175
110
 
176
111
  if (hydrating) {
177
- if (pending_fn) {
178
- pending_effect = branch(() => pending_fn(anchor));
179
- }
112
+ branches.ensure(PENDING, pending_fn);
180
113
  } else {
181
114
  // Wait a microtask before checking if we should show the pending state as
182
- // the promise might have resolved by the next microtask.
115
+ // the promise might have resolved by then
183
116
  queue_micro_task(() => {
184
- if (!resolved) update(PENDING, true);
117
+ if (!resolved) {
118
+ resolve(() => {
119
+ branches.ensure(PENDING, pending_fn);
120
+ });
121
+ }
185
122
  });
186
123
  }
187
124
  } else {
188
- internal_set(input_source, input);
189
- update(THEN, false);
125
+ internal_set(value, input);
126
+ branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
190
127
  }
191
128
 
192
129
  if (mismatch) {
@@ -194,11 +131,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
194
131
  set_hydrating(true);
195
132
  }
196
133
 
197
- // Set the input to something else, in order to disable the promise callbacks
198
- return () => (input = UNINITIALIZED);
134
+ return () => {
135
+ destroyed = true;
136
+ };
199
137
  });
200
-
201
- if (hydrating) {
202
- anchor = hydrate_node;
203
- }
204
138
  }
@@ -8,7 +8,13 @@ import {
8
8
  import { HYDRATION_START_ELSE } from '../../../../constants.js';
9
9
  import { component_context, set_component_context } from '../../context.js';
10
10
  import { handle_error, invoke_error_boundary } from '../../error-handling.js';
11
- import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
11
+ import {
12
+ block,
13
+ branch,
14
+ destroy_effect,
15
+ move_effect,
16
+ pause_effect
17
+ } from '../../reactivity/effects.js';
12
18
  import {
13
19
  active_effect,
14
20
  active_reaction,
@@ -425,24 +431,6 @@ export class Boundary {
425
431
  }
426
432
  }
427
433
 
428
- /**
429
- *
430
- * @param {Effect} effect
431
- * @param {DocumentFragment} fragment
432
- */
433
- function move_effect(effect, fragment) {
434
- var node = effect.nodes_start;
435
- var end = effect.nodes_end;
436
-
437
- while (node !== null) {
438
- /** @type {TemplateNode | null} */
439
- var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
440
-
441
- fragment.append(node);
442
- node = next;
443
- }
444
- }
445
-
446
434
  export function get_boundary() {
447
435
  return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
448
436
  }