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
@@ -0,0 +1,185 @@
1
+ /** @import { Effect, TemplateNode } from '#client' */
2
+ import { is_runes } from '../../context.js';
3
+ import { Batch, current_batch } from '../../reactivity/batch.js';
4
+ import {
5
+ branch,
6
+ destroy_effect,
7
+ move_effect,
8
+ pause_effect,
9
+ resume_effect
10
+ } from '../../reactivity/effects.js';
11
+ import { set_should_intro, should_intro } from '../../render.js';
12
+ import { hydrate_node, hydrating } from '../hydration.js';
13
+ import { create_text, should_defer_append } from '../operations.js';
14
+
15
+ /**
16
+ * @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch
17
+ */
18
+
19
+ /**
20
+ * @template Key
21
+ */
22
+ export class BranchManager {
23
+ /** @type {TemplateNode} */
24
+ anchor;
25
+
26
+ /** @type {Map<Batch, Key>} */
27
+ #batches = new Map();
28
+
29
+ /** @type {Map<Key, Effect>} */
30
+ #onscreen = new Map();
31
+
32
+ /** @type {Map<Key, Branch>} */
33
+ #offscreen = new Map();
34
+
35
+ /**
36
+ * Whether to pause (i.e. outro) on change, or destroy immediately.
37
+ * This is necessary for `<svelte:element>`
38
+ */
39
+ #transition = true;
40
+
41
+ /**
42
+ * @param {TemplateNode} anchor
43
+ * @param {boolean} transition
44
+ */
45
+ constructor(anchor, transition = true) {
46
+ this.anchor = anchor;
47
+ this.#transition = transition;
48
+ }
49
+
50
+ #commit = () => {
51
+ var batch = /** @type {Batch} */ (current_batch);
52
+
53
+ // if this batch was made obsolete, bail
54
+ if (!this.#batches.has(batch)) return;
55
+
56
+ var key = /** @type {Key} */ (this.#batches.get(batch));
57
+
58
+ var onscreen = this.#onscreen.get(key);
59
+
60
+ if (onscreen) {
61
+ // effect is already in the DOM — abort any current outro
62
+ resume_effect(onscreen);
63
+ } else {
64
+ // effect is currently offscreen. put it in the DOM
65
+ var offscreen = this.#offscreen.get(key);
66
+
67
+ if (offscreen) {
68
+ this.#onscreen.set(key, offscreen.effect);
69
+ this.#offscreen.delete(key);
70
+
71
+ // remove the anchor...
72
+ /** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove();
73
+
74
+ // ...and append the fragment
75
+ this.anchor.before(offscreen.fragment);
76
+ onscreen = offscreen.effect;
77
+ }
78
+ }
79
+
80
+ for (const [b, k] of this.#batches) {
81
+ this.#batches.delete(b);
82
+
83
+ if (b === batch) {
84
+ // keep values for newer batches
85
+ break;
86
+ }
87
+
88
+ const offscreen = this.#offscreen.get(k);
89
+
90
+ if (offscreen) {
91
+ // for older batches, destroy offscreen effects
92
+ // as they will never be committed
93
+ destroy_effect(offscreen.effect);
94
+ this.#offscreen.delete(k);
95
+ }
96
+ }
97
+
98
+ // outro/destroy all onscreen effects...
99
+ for (const [k, effect] of this.#onscreen) {
100
+ // ...except the one that was just committed
101
+ if (k === key) continue;
102
+
103
+ const on_destroy = () => {
104
+ const keys = Array.from(this.#batches.values());
105
+
106
+ if (keys.includes(k)) {
107
+ // keep the effect offscreen, as another batch will need it
108
+ var fragment = document.createDocumentFragment();
109
+ move_effect(effect, fragment);
110
+
111
+ fragment.append(create_text()); // TODO can we avoid this?
112
+
113
+ this.#offscreen.set(k, { effect, fragment });
114
+ } else {
115
+ destroy_effect(effect);
116
+ }
117
+
118
+ this.#onscreen.delete(k);
119
+ };
120
+
121
+ if (this.#transition || !onscreen) {
122
+ pause_effect(effect, on_destroy, false);
123
+ } else {
124
+ on_destroy();
125
+ }
126
+ }
127
+ };
128
+
129
+ /**
130
+ *
131
+ * @param {any} key
132
+ * @param {null | ((target: TemplateNode) => void)} fn
133
+ */
134
+ ensure(key, fn) {
135
+ var batch = /** @type {Batch} */ (current_batch);
136
+ var defer = should_defer_append();
137
+
138
+ if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) {
139
+ if (defer) {
140
+ var fragment = document.createDocumentFragment();
141
+ var target = create_text();
142
+
143
+ fragment.append(target);
144
+
145
+ this.#offscreen.set(key, {
146
+ effect: branch(() => fn(target)),
147
+ fragment
148
+ });
149
+ } else {
150
+ this.#onscreen.set(
151
+ key,
152
+ branch(() => fn(this.anchor))
153
+ );
154
+ }
155
+ }
156
+
157
+ this.#batches.set(batch, key);
158
+
159
+ if (defer) {
160
+ for (const [k, effect] of this.#onscreen) {
161
+ if (k === key) {
162
+ batch.skipped_effects.delete(effect);
163
+ } else {
164
+ batch.skipped_effects.add(effect);
165
+ }
166
+ }
167
+
168
+ for (const [k, branch] of this.#offscreen) {
169
+ if (k === key) {
170
+ batch.skipped_effects.delete(branch.effect);
171
+ } else {
172
+ batch.skipped_effects.add(branch.effect);
173
+ }
174
+ }
175
+
176
+ batch.add_callback(this.#commit);
177
+ } else {
178
+ if (hydrating) {
179
+ this.anchor = hydrate_node;
180
+ }
181
+
182
+ this.#commit();
183
+ }
184
+ }
185
+ }
@@ -1,19 +1,16 @@
1
- /** @import { Effect, TemplateNode } from '#client' */
2
- /** @import { Batch } from '../../reactivity/batch.js'; */
1
+ /** @import { TemplateNode } from '#client' */
3
2
  import { EFFECT_TRANSPARENT } from '#client/constants';
4
3
  import {
5
4
  hydrate_next,
6
- hydrate_node,
7
5
  hydrating,
8
6
  read_hydration_instruction,
9
7
  skip_nodes,
10
8
  set_hydrate_node,
11
9
  set_hydrating
12
10
  } from '../hydration.js';
13
- import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
14
- import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
15
- import { create_text, should_defer_append } from '../operations.js';
16
- import { current_batch } from '../../reactivity/batch.js';
11
+ import { block } from '../../reactivity/effects.js';
12
+ import { HYDRATION_START_ELSE } from '../../../../constants.js';
13
+ import { BranchManager } from './branches.js';
17
14
 
18
15
  // TODO reinstate https://github.com/sveltejs/svelte/pull/15250
19
16
 
@@ -28,122 +25,46 @@ export function if_block(node, fn, elseif = false) {
28
25
  hydrate_next();
29
26
  }
30
27
 
31
- var anchor = node;
32
-
33
- /** @type {Effect | null} */
34
- var consequent_effect = null;
35
-
36
- /** @type {Effect | null} */
37
- var alternate_effect = null;
38
-
39
- /** @type {typeof UNINITIALIZED | boolean | null} */
40
- var condition = UNINITIALIZED;
41
-
28
+ var branches = new BranchManager(node);
42
29
  var flags = elseif ? EFFECT_TRANSPARENT : 0;
43
30
 
44
- var has_branch = false;
45
-
46
- const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => {
47
- has_branch = true;
48
- update_branch(flag, fn);
49
- };
50
-
51
- /** @type {DocumentFragment | null} */
52
- var offscreen_fragment = null;
53
-
54
- function commit() {
55
- if (offscreen_fragment !== null) {
56
- // remove the anchor
57
- /** @type {Text} */ (offscreen_fragment.lastChild).remove();
58
-
59
- anchor.before(offscreen_fragment);
60
- offscreen_fragment = null;
61
- }
62
-
63
- var active = condition ? consequent_effect : alternate_effect;
64
- var inactive = condition ? alternate_effect : consequent_effect;
65
-
66
- if (active) {
67
- resume_effect(active);
68
- }
69
-
70
- if (inactive) {
71
- pause_effect(inactive, () => {
72
- if (condition) {
73
- alternate_effect = null;
74
- } else {
75
- consequent_effect = null;
76
- }
77
- });
78
- }
79
- }
80
-
81
- const update_branch = (
82
- /** @type {boolean | null} */ new_condition,
83
- /** @type {null | ((anchor: Node) => void)} */ fn
84
- ) => {
85
- if (condition === (condition = new_condition)) return;
86
-
87
- /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
88
- let mismatch = false;
89
-
31
+ /**
32
+ * @param {boolean} condition,
33
+ * @param {null | ((anchor: Node) => void)} fn
34
+ */
35
+ function update_branch(condition, fn) {
90
36
  if (hydrating) {
91
- const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
37
+ const is_else = read_hydration_instruction(node) === HYDRATION_START_ELSE;
92
38
 
93
- if (!!condition === is_else) {
39
+ if (condition === is_else) {
94
40
  // Hydration mismatch: remove everything inside the anchor and start fresh.
95
41
  // This could happen with `{#if browser}...{/if}`, for example
96
- anchor = skip_nodes();
42
+ var anchor = skip_nodes();
97
43
 
98
44
  set_hydrate_node(anchor);
99
- set_hydrating(false);
100
- mismatch = true;
101
- }
102
- }
45
+ branches.anchor = anchor;
103
46
 
104
- var defer = should_defer_append();
105
- var target = anchor;
106
-
107
- if (defer) {
108
- offscreen_fragment = document.createDocumentFragment();
109
- offscreen_fragment.append((target = create_text()));
110
- }
47
+ set_hydrating(false);
48
+ branches.ensure(condition, fn);
49
+ set_hydrating(true);
111
50
 
112
- if (condition) {
113
- consequent_effect ??= fn && branch(() => fn(target));
114
- } else {
115
- alternate_effect ??= fn && branch(() => fn(target));
51
+ return;
52
+ }
116
53
  }
117
54
 
118
- if (defer) {
119
- var batch = /** @type {Batch} */ (current_batch);
120
-
121
- var active = condition ? consequent_effect : alternate_effect;
122
- var inactive = condition ? alternate_effect : consequent_effect;
123
-
124
- if (active) batch.skipped_effects.delete(active);
125
- if (inactive) batch.skipped_effects.add(inactive);
55
+ branches.ensure(condition, fn);
56
+ }
126
57
 
127
- batch.add_callback(commit);
128
- } else {
129
- commit();
130
- }
58
+ block(() => {
59
+ var has_branch = false;
131
60
 
132
- if (mismatch) {
133
- // continue in hydration mode
134
- set_hydrating(true);
135
- }
136
- };
61
+ fn((fn, flag = true) => {
62
+ has_branch = true;
63
+ update_branch(flag, fn);
64
+ });
137
65
 
138
- block(() => {
139
- has_branch = false;
140
- fn(set_branch);
141
66
  if (!has_branch) {
142
- update_branch(null, null);
67
+ update_branch(false, null);
143
68
  }
144
69
  }, flags);
145
-
146
- if (hydrating) {
147
- anchor = hydrate_node;
148
- }
149
70
  }
@@ -1,12 +1,8 @@
1
- /** @import { Effect, TemplateNode } from '#client' */
2
- /** @import { Batch } from '../../reactivity/batch.js'; */
3
- import { UNINITIALIZED } from '../../../../constants.js';
4
- import { block, branch, pause_effect } from '../../reactivity/effects.js';
5
- import { not_equal, safe_not_equal } from '../../reactivity/equality.js';
1
+ /** @import { TemplateNode } from '#client' */
6
2
  import { is_runes } from '../../context.js';
7
- import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
8
- import { create_text, should_defer_append } from '../operations.js';
9
- import { current_batch } from '../../reactivity/batch.js';
3
+ import { block } from '../../reactivity/effects.js';
4
+ import { hydrate_next, hydrating } from '../hydration.js';
5
+ import { BranchManager } from './branches.js';
10
6
 
11
7
  /**
12
8
  * @template V
@@ -20,60 +16,18 @@ export function key(node, get_key, render_fn) {
20
16
  hydrate_next();
21
17
  }
22
18
 
23
- var anchor = node;
19
+ var branches = new BranchManager(node);
24
20
 
25
- /** @type {V | typeof UNINITIALIZED} */
26
- var key = UNINITIALIZED;
27
-
28
- /** @type {Effect} */
29
- var effect;
30
-
31
- /** @type {Effect} */
32
- var pending_effect;
33
-
34
- /** @type {DocumentFragment | null} */
35
- var offscreen_fragment = null;
36
-
37
- var changed = is_runes() ? not_equal : safe_not_equal;
38
-
39
- function commit() {
40
- if (effect) {
41
- pause_effect(effect);
42
- }
43
-
44
- if (offscreen_fragment !== null) {
45
- // remove the anchor
46
- /** @type {Text} */ (offscreen_fragment.lastChild).remove();
47
-
48
- anchor.before(offscreen_fragment);
49
- offscreen_fragment = null;
50
- }
51
-
52
- effect = pending_effect;
53
- }
21
+ var legacy = !is_runes();
54
22
 
55
23
  block(() => {
56
- if (changed(key, (key = get_key()))) {
57
- var target = anchor;
58
-
59
- var defer = should_defer_append();
60
-
61
- if (defer) {
62
- offscreen_fragment = document.createDocumentFragment();
63
- offscreen_fragment.append((target = create_text()));
64
- }
65
-
66
- pending_effect = branch(() => render_fn(target));
24
+ var key = get_key();
67
25
 
68
- if (defer) {
69
- /** @type {Batch} */ (current_batch).add_callback(commit);
70
- } else {
71
- commit();
72
- }
26
+ // key blocks in Svelte <5 had stupid semantics
27
+ if (legacy && key !== null && typeof key === 'object') {
28
+ key = /** @type {V} */ ({});
73
29
  }
74
- });
75
30
 
76
- if (hydrating) {
77
- anchor = hydrate_node;
78
- }
31
+ branches.ensure(key, render_fn);
32
+ });
79
33
  }
@@ -1,8 +1,8 @@
1
1
  /** @import { Snippet } from 'svelte' */
2
- /** @import { Effect, TemplateNode } from '#client' */
2
+ /** @import { TemplateNode } from '#client' */
3
3
  /** @import { Getters } from '#shared' */
4
4
  import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
5
- import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
5
+ import { block, teardown } from '../../reactivity/effects.js';
6
6
  import {
7
7
  dev_current_component_function,
8
8
  set_dev_current_component_function
@@ -14,8 +14,8 @@ import * as w from '../../warnings.js';
14
14
  import * as e from '../../errors.js';
15
15
  import { DEV } from 'esm-env';
16
16
  import { get_first_child, get_next_sibling } from '../operations.js';
17
- import { noop } from '../../../shared/utils.js';
18
17
  import { prevent_snippet_stringification } from '../../../shared/validate.js';
18
+ import { BranchManager } from './branches.js';
19
19
 
20
20
  /**
21
21
  * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@@ -25,33 +25,17 @@ import { prevent_snippet_stringification } from '../../../shared/validate.js';
25
25
  * @returns {void}
26
26
  */
27
27
  export function snippet(node, get_snippet, ...args) {
28
- var anchor = node;
29
-
30
- /** @type {SnippetFn | null | undefined} */
31
- // @ts-ignore
32
- var snippet = noop;
33
-
34
- /** @type {Effect | null} */
35
- var snippet_effect;
28
+ var branches = new BranchManager(node);
36
29
 
37
30
  block(() => {
38
- if (snippet === (snippet = get_snippet())) return;
39
-
40
- if (snippet_effect) {
41
- destroy_effect(snippet_effect);
42
- snippet_effect = null;
43
- }
31
+ const snippet = get_snippet() ?? null;
44
32
 
45
33
  if (DEV && snippet == null) {
46
34
  e.invalid_snippet();
47
35
  }
48
36
 
49
- snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
37
+ branches.ensure(snippet, snippet && ((anchor) => snippet(anchor, ...args)));
50
38
  }, EFFECT_TRANSPARENT);
51
-
52
- if (hydrating) {
53
- anchor = hydrate_node;
54
- }
55
39
  }
56
40
 
57
41
  /**
@@ -1,10 +1,8 @@
1
- /** @import { TemplateNode, Dom, Effect } from '#client' */
2
- /** @import { Batch } from '../../reactivity/batch.js'; */
1
+ /** @import { TemplateNode, Dom } from '#client' */
3
2
  import { EFFECT_TRANSPARENT } from '#client/constants';
4
- import { block, branch, pause_effect } from '../../reactivity/effects.js';
5
- import { current_batch } from '../../reactivity/batch.js';
6
- import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
7
- import { create_text, should_defer_append } from '../operations.js';
3
+ import { block } from '../../reactivity/effects.js';
4
+ import { hydrate_next, hydrating } from '../hydration.js';
5
+ import { BranchManager } from './branches.js';
8
6
 
9
7
  /**
10
8
  * @template P
@@ -19,64 +17,10 @@ export function component(node, get_component, render_fn) {
19
17
  hydrate_next();
20
18
  }
21
19
 
22
- var anchor = node;
23
-
24
- /** @type {C} */
25
- var component;
26
-
27
- /** @type {Effect | null} */
28
- var effect;
29
-
30
- /** @type {DocumentFragment | null} */
31
- var offscreen_fragment = null;
32
-
33
- /** @type {Effect | null} */
34
- var pending_effect = null;
35
-
36
- function commit() {
37
- if (effect) {
38
- pause_effect(effect);
39
- effect = null;
40
- }
41
-
42
- if (offscreen_fragment) {
43
- // remove the anchor
44
- /** @type {Text} */ (offscreen_fragment.lastChild).remove();
45
-
46
- anchor.before(offscreen_fragment);
47
- offscreen_fragment = null;
48
- }
49
-
50
- effect = pending_effect;
51
- pending_effect = null;
52
- }
20
+ var branches = new BranchManager(node);
53
21
 
54
22
  block(() => {
55
- if (component === (component = get_component())) return;
56
-
57
- var defer = should_defer_append();
58
-
59
- if (component) {
60
- var target = anchor;
61
-
62
- if (defer) {
63
- offscreen_fragment = document.createDocumentFragment();
64
- offscreen_fragment.append((target = create_text()));
65
- if (effect) {
66
- /** @type {Batch} */ (current_batch).skipped_effects.add(effect);
67
- }
68
- }
69
- pending_effect = branch(() => render_fn(target, component));
70
- }
71
-
72
- if (defer) {
73
- /** @type {Batch} */ (current_batch).add_callback(commit);
74
- } else {
75
- commit();
76
- }
23
+ var component = get_component() ?? null;
24
+ branches.ensure(component, component && ((target) => render_fn(target, component)));
77
25
  }, EFFECT_TRANSPARENT);
78
-
79
- if (hydrating) {
80
- anchor = hydrate_node;
81
- }
82
26
  }