svelte 5.46.0 → 5.46.3

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.
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.46.0",
5
+ "version": "5.46.3",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -6,6 +6,7 @@ import { regex_not_newline_characters } from '../../patterns.js';
6
6
  import * as e from '../../../errors.js';
7
7
  import * as w from '../../../warnings.js';
8
8
  import { is_text_attribute } from '../../../utils/ast.js';
9
+ import { locator } from '../../../state.js';
9
10
 
10
11
  const regex_closing_script_tag = /<\/script\s*>/;
11
12
  const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
@@ -39,9 +40,15 @@ export function read_script(parser, start, attributes) {
39
40
  parser.acorn_error(err);
40
41
  }
41
42
 
42
- // TODO is this necessary?
43
43
  ast.start = script_start;
44
44
 
45
+ if (ast.loc) {
46
+ // Acorn always uses `0` as the start of a `Program`, but for sourcemap purposes
47
+ // we need it to be the start of the `<script>` contents
48
+ ({ line: ast.loc.start.line, column: ast.loc.start.column } = locator(start));
49
+ ({ line: ast.loc.end.line, column: ast.loc.end.column } = locator(parser.index));
50
+ }
51
+
45
52
  /** @type {'default' | 'module'} */
46
53
  let context = 'default';
47
54
 
@@ -657,7 +657,8 @@ export function analyze_component(root, source, options) {
657
657
  if (
658
658
  binding &&
659
659
  binding.kind === 'normal' &&
660
- binding.declaration_kind !== 'import'
660
+ binding.declaration_kind !== 'import' &&
661
+ binding.declaration_kind !== 'function'
661
662
  ) {
662
663
  binding.kind = 'state';
663
664
  binding.mutated = true;
@@ -54,7 +54,9 @@ export function EachBlock(node, context) {
54
54
 
55
55
  // collect transitive dependencies...
56
56
  for (const binding of node.metadata.expression.dependencies) {
57
- collect_transitive_dependencies(binding, node.metadata.transitive_deps);
57
+ if (binding.declaration_kind !== 'function') {
58
+ collect_transitive_dependencies(binding, node.metadata.transitive_deps);
59
+ }
58
60
  }
59
61
 
60
62
  // ...and ensure they are marked as state, so they can be turned
@@ -519,14 +519,9 @@ export function client_component(analysis, options) {
519
519
 
520
520
  if (options.hmr) {
521
521
  const id = b.id(analysis.name);
522
- const HMR = b.id('$.HMR');
523
-
524
- const existing = b.member(id, HMR, true);
525
- const incoming = b.member(b.id('module.default'), HMR, true);
526
522
 
527
523
  const accept_fn_body = [
528
- b.stmt(b.assignment('=', b.member(incoming, 'source'), b.member(existing, 'source'))),
529
- b.stmt(b.call('$.set', b.member(existing, 'source'), b.member(incoming, 'original')))
524
+ b.stmt(b.call(b.member(b.member(id, b.id('$.HMR'), true), 'update'), b.id('module.default')))
530
525
  ];
531
526
 
532
527
  if (analysis.css.hash) {
@@ -535,8 +530,7 @@ export function client_component(analysis, options) {
535
530
  }
536
531
 
537
532
  const hmr = b.block([
538
- b.stmt(b.assignment('=', id, b.call('$.hmr', id, b.thunk(b.member(existing, 'source'))))),
539
-
533
+ b.stmt(b.assignment('=', id, b.call('$.hmr', id))),
540
534
  b.stmt(b.call('import.meta.hot.accept', b.arrow([b.id('module')], b.block(accept_fn_body))))
541
535
  ]);
542
536
 
@@ -336,7 +336,9 @@ export function RegularElement(node, context) {
336
336
  trimmed.every(
337
337
  (node) =>
338
338
  node.type === 'Text' ||
339
- (!node.metadata.expression.has_state && !node.metadata.expression.has_await)
339
+ (!node.metadata.expression.has_state &&
340
+ !node.metadata.expression.has_await &&
341
+ !node.metadata.expression.has_blockers())
340
342
  ) &&
341
343
  trimmed.some((node) => node.type === 'ExpressionTag');
342
344
 
@@ -209,11 +209,11 @@ export function VariableDeclaration(node, context) {
209
209
  let call = b.call(
210
210
  '$.async_derived',
211
211
  b.thunk(expression, true),
212
+ dev && b.literal(declarator.id.name),
212
213
  location ? b.literal(location) : undefined
213
214
  );
214
215
 
215
216
  call = should_save ? save(call) : b.await(call);
216
- if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
217
217
 
218
218
  declarations.push(b.declarator(declarator.id, call));
219
219
  } else {
@@ -244,17 +244,16 @@ export function VariableDeclaration(node, context) {
244
244
  call = b.call(
245
245
  '$.async_derived',
246
246
  b.thunk(expression, true),
247
+ dev &&
248
+ b.literal(
249
+ `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`
250
+ ),
247
251
  location ? b.literal(location) : undefined
248
252
  );
249
253
 
250
254
  call = should_save ? save(call) : b.await(call);
251
255
  }
252
256
 
253
- if (dev) {
254
- const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`;
255
- call = b.call('$.tag', call, b.literal(label));
256
- }
257
-
258
257
  declarations.push(b.declarator(id, call));
259
258
  }
260
259
 
@@ -134,7 +134,7 @@ export function build_template_chunk(
134
134
 
135
135
  const evaluated = state.scope.evaluate(value);
136
136
 
137
- has_await ||= node.metadata.expression.has_await;
137
+ has_await ||= node.metadata.expression.has_await || node.metadata.expression.has_blockers();
138
138
  has_state ||= has_await || (node.metadata.expression.has_state && !evaluated.is_known);
139
139
 
140
140
  if (values.length === 1) {
@@ -86,7 +86,14 @@ export function transform_body(instance_body, runner, transform) {
86
86
  }
87
87
 
88
88
  if (s.node.type === 'ExpressionStatement') {
89
- const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression));
89
+ // the expression may be a $inspect call, which will be transformed into an empty statement
90
+ const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ (
91
+ transform(s.node.expression)
92
+ );
93
+
94
+ if (expression.type === 'EmptyStatement') {
95
+ return null;
96
+ }
90
97
 
91
98
  return expression.type === 'AwaitExpression'
92
99
  ? b.thunk(expression, true)
@@ -1,23 +1,25 @@
1
- /** @import { Source, Effect, TemplateNode } from '#client' */
1
+ /** @import { Effect, TemplateNode } from '#client' */
2
2
  import { FILENAME, HMR } from '../../../constants.js';
3
3
  import { EFFECT_TRANSPARENT } from '#client/constants';
4
4
  import { hydrate_node, hydrating } from '../dom/hydration.js';
5
5
  import { block, branch, destroy_effect } from '../reactivity/effects.js';
6
- import { source } from '../reactivity/sources.js';
6
+ import { set, source } from '../reactivity/sources.js';
7
7
  import { set_should_intro } from '../render.js';
8
8
  import { get } from '../runtime.js';
9
9
 
10
10
  /**
11
11
  * @template {(anchor: Comment, props: any) => any} Component
12
- * @param {Component} original
13
- * @param {() => Source<Component>} get_source
12
+ * @param {Component} fn
14
13
  */
15
- export function hmr(original, get_source) {
14
+ export function hmr(fn) {
15
+ const current = source(fn);
16
+
16
17
  /**
17
18
  * @param {TemplateNode} anchor
18
19
  * @param {any} props
19
20
  */
20
21
  function wrapper(anchor, props) {
22
+ let component = {};
21
23
  let instance = {};
22
24
 
23
25
  /** @type {Effect} */
@@ -26,8 +28,9 @@ export function hmr(original, get_source) {
26
28
  let ran = false;
27
29
 
28
30
  block(() => {
29
- const source = get_source();
30
- const component = get(source);
31
+ if (component === (component = get(current))) {
32
+ return;
33
+ }
31
34
 
32
35
  if (effect) {
33
36
  // @ts-ignore
@@ -62,16 +65,24 @@ export function hmr(original, get_source) {
62
65
  }
63
66
 
64
67
  // @ts-expect-error
65
- wrapper[FILENAME] = original[FILENAME];
68
+ wrapper[FILENAME] = fn[FILENAME];
66
69
 
67
70
  // @ts-ignore
68
71
  wrapper[HMR] = {
69
- // When we accept an update, we set the original source to the new component
70
- original,
71
- // The `get_source` parameter reads `wrapper[HMR].source`, but in the `accept`
72
- // function we always replace it with `previous[HMR].source`, which in practice
73
- // means we only ever update the original
74
- source: source(original)
72
+ fn,
73
+ current,
74
+ update: (/** @type {any} */ incoming) => {
75
+ // This logic ensures that the first version of the component is the one
76
+ // whose update function and therefore block effect is preserved across updates.
77
+ // If we don't do this dance and instead just use `incoming` as the new component
78
+ // and then update, we'll create an ever-growing stack of block effects.
79
+
80
+ // Trigger the original block effect
81
+ set(wrapper[HMR].current, incoming[HMR].fn);
82
+
83
+ // Replace the incoming source with the original one
84
+ incoming[HMR].current = wrapper[HMR].current;
85
+ }
75
86
  };
76
87
 
77
88
  return wrapper;
@@ -21,7 +21,7 @@ import { get_boundary } from './boundary.js';
21
21
  export function async(node, blockers = [], expressions = [], fn) {
22
22
  var boundary = get_boundary();
23
23
  var batch = /** @type {Batch} */ (current_batch);
24
- var blocking = !boundary.is_pending();
24
+ var blocking = boundary.is_rendered();
25
25
 
26
26
  boundary.update_pending_count(1);
27
27
  batch.increment(blocking);
@@ -2,8 +2,10 @@
2
2
  import {
3
3
  BOUNDARY_EFFECT,
4
4
  COMMENT_NODE,
5
+ DIRTY,
5
6
  EFFECT_PRESERVED,
6
- EFFECT_TRANSPARENT
7
+ EFFECT_TRANSPARENT,
8
+ MAYBE_DIRTY
7
9
  } from '#client/constants';
8
10
  import { HYDRATION_START_ELSE } from '../../../../constants.js';
9
11
  import { component_context, set_component_context } from '../../context.js';
@@ -34,11 +36,13 @@ import { queue_micro_task } from '../task.js';
34
36
  import * as e from '../../errors.js';
35
37
  import * as w from '../../warnings.js';
36
38
  import { DEV } from 'esm-env';
37
- import { Batch } from '../../reactivity/batch.js';
39
+ import { Batch, schedule_effect } from '../../reactivity/batch.js';
38
40
  import { internal_set, source } from '../../reactivity/sources.js';
39
41
  import { tag } from '../../dev/tracing.js';
40
42
  import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
41
43
  import { create_text } from '../operations.js';
44
+ import { defer_effect } from '../../reactivity/utils.js';
45
+ import { set_signal_status } from '../../reactivity/status.js';
42
46
 
43
47
  /**
44
48
  * @typedef {{
@@ -64,7 +68,7 @@ export class Boundary {
64
68
  /** @type {Boundary | null} */
65
69
  parent;
66
70
 
67
- #pending = false;
71
+ is_pending = false;
68
72
 
69
73
  /** @type {TemplateNode} */
70
74
  #anchor;
@@ -101,6 +105,12 @@ export class Boundary {
101
105
 
102
106
  #is_creating_fallback = false;
103
107
 
108
+ /** @type {Set<Effect>} */
109
+ #dirty_effects = new Set();
110
+
111
+ /** @type {Set<Effect>} */
112
+ #maybe_dirty_effects = new Set();
113
+
104
114
  /**
105
115
  * A source containing the number of pending async deriveds/expressions.
106
116
  * Only created if `$effect.pending()` is used inside the boundary,
@@ -134,7 +144,7 @@ export class Boundary {
134
144
 
135
145
  this.parent = /** @type {Effect} */ (active_effect).b;
136
146
 
137
- this.#pending = !!this.#props.pending;
147
+ this.is_pending = !!this.#props.pending;
138
148
 
139
149
  this.#effect = block(() => {
140
150
  /** @type {Effect} */ (active_effect).b = this;
@@ -151,6 +161,10 @@ export class Boundary {
151
161
  this.#hydrate_pending_content();
152
162
  } else {
153
163
  this.#hydrate_resolved_content();
164
+
165
+ if (this.#pending_count === 0) {
166
+ this.is_pending = false;
167
+ }
154
168
  }
155
169
  } else {
156
170
  var anchor = this.#get_anchor();
@@ -164,7 +178,7 @@ export class Boundary {
164
178
  if (this.#pending_count > 0) {
165
179
  this.#show_pending_snippet();
166
180
  } else {
167
- this.#pending = false;
181
+ this.is_pending = false;
168
182
  }
169
183
  }
170
184
 
@@ -184,10 +198,6 @@ export class Boundary {
184
198
  } catch (error) {
185
199
  this.error(error);
186
200
  }
187
-
188
- // Since server rendered resolved content, we never show pending state
189
- // Even if client-side async operations are still running, the content is already displayed
190
- this.#pending = false;
191
201
  }
192
202
 
193
203
  #hydrate_pending_content() {
@@ -212,7 +222,7 @@ export class Boundary {
212
222
  this.#pending_effect = null;
213
223
  });
214
224
 
215
- this.#pending = false;
225
+ this.is_pending = false;
216
226
  }
217
227
  });
218
228
  }
@@ -220,7 +230,7 @@ export class Boundary {
220
230
  #get_anchor() {
221
231
  var anchor = this.#anchor;
222
232
 
223
- if (this.#pending) {
233
+ if (this.is_pending) {
224
234
  this.#pending_anchor = create_text();
225
235
  this.#anchor.before(this.#pending_anchor);
226
236
 
@@ -231,11 +241,19 @@ export class Boundary {
231
241
  }
232
242
 
233
243
  /**
234
- * Returns `true` if the effect exists inside a boundary whose pending snippet is shown
244
+ * Defer an effect inside a pending boundary until the boundary resolves
245
+ * @param {Effect} effect
246
+ */
247
+ defer_effect(effect) {
248
+ defer_effect(effect, this.#dirty_effects, this.#maybe_dirty_effects);
249
+ }
250
+
251
+ /**
252
+ * Returns `false` if the effect exists inside a boundary whose pending snippet is shown
235
253
  * @returns {boolean}
236
254
  */
237
- is_pending() {
238
- return this.#pending || (!!this.parent && this.parent.is_pending());
255
+ is_rendered() {
256
+ return !this.is_pending && (!this.parent || this.parent.is_rendered());
239
257
  }
240
258
 
241
259
  has_pending_snippet() {
@@ -298,7 +316,24 @@ export class Boundary {
298
316
  this.#pending_count += d;
299
317
 
300
318
  if (this.#pending_count === 0) {
301
- this.#pending = false;
319
+ this.is_pending = false;
320
+
321
+ // any effects that were encountered and deferred during traversal
322
+ // should be rescheduled — after the next traversal (which will happen
323
+ // immediately, due to the same update that brought us here)
324
+ // the effects will be flushed
325
+ for (const e of this.#dirty_effects) {
326
+ set_signal_status(e, DIRTY);
327
+ schedule_effect(e);
328
+ }
329
+
330
+ for (const e of this.#maybe_dirty_effects) {
331
+ set_signal_status(e, MAYBE_DIRTY);
332
+ schedule_effect(e);
333
+ }
334
+
335
+ this.#dirty_effects.clear();
336
+ this.#maybe_dirty_effects.clear();
302
337
 
303
338
  if (this.#pending_effect) {
304
339
  pause_effect(this.#pending_effect, () => {
@@ -394,7 +429,7 @@ export class Boundary {
394
429
 
395
430
  // we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
396
431
  // but it would be really weird to show the parent's boundary on a child reset.
397
- this.#pending = this.has_pending_snippet();
432
+ this.is_pending = this.has_pending_snippet();
398
433
 
399
434
  this.#main_effect = this.#run(() => {
400
435
  this.#is_creating_fallback = false;
@@ -404,7 +439,7 @@ export class Boundary {
404
439
  if (this.#pending_count > 0) {
405
440
  this.#show_pending_snippet();
406
441
  } else {
407
- this.#pending = false;
442
+ this.is_pending = false;
408
443
  }
409
444
  };
410
445
 
@@ -98,7 +98,6 @@ export {
98
98
  with_script
99
99
  } from './dom/template.js';
100
100
  export {
101
- async_body,
102
101
  for_await_track_reactivity_loss,
103
102
  run,
104
103
  save,
@@ -25,7 +25,6 @@ import {
25
25
  set_from_async_derived
26
26
  } from './deriveds.js';
27
27
  import { aborted } from './effects.js';
28
- import { hydrate_next, hydrating, set_hydrate_node, skip_nodes } from '../dom/hydration.js';
29
28
 
30
29
  /**
31
30
  * @param {Array<Promise<void>>} blockers
@@ -211,51 +210,6 @@ export function unset_context() {
211
210
  }
212
211
  }
213
212
 
214
- /**
215
- * @param {TemplateNode} anchor
216
- * @param {(target: TemplateNode) => Promise<void>} fn
217
- */
218
- export async function async_body(anchor, fn) {
219
- var boundary = get_boundary();
220
- var batch = /** @type {Batch} */ (current_batch);
221
- var blocking = !boundary.is_pending();
222
-
223
- boundary.update_pending_count(1);
224
- batch.increment(blocking);
225
-
226
- var active = /** @type {Effect} */ (active_effect);
227
-
228
- var was_hydrating = hydrating;
229
- var next_hydrate_node = undefined;
230
-
231
- if (was_hydrating) {
232
- hydrate_next();
233
- next_hydrate_node = skip_nodes(false);
234
- }
235
-
236
- try {
237
- var promise = fn(anchor);
238
- } finally {
239
- if (next_hydrate_node) {
240
- set_hydrate_node(next_hydrate_node);
241
- hydrate_next();
242
- }
243
- }
244
-
245
- try {
246
- await promise;
247
- } catch (error) {
248
- if (!aborted(active)) {
249
- invoke_error_boundary(error, active);
250
- }
251
- } finally {
252
- boundary.update_pending_count(-1);
253
- batch.decrement(blocking);
254
-
255
- unset_context();
256
- }
257
- }
258
-
259
213
  /**
260
214
  * @param {Array<() => void | Promise<void>>} thunks
261
215
  */
@@ -264,7 +218,7 @@ export function run(thunks) {
264
218
 
265
219
  var boundary = get_boundary();
266
220
  var batch = /** @type {Batch} */ (current_batch);
267
- var blocking = !boundary.is_pending();
221
+ var blocking = boundary.is_rendered();
268
222
 
269
223
  boundary.update_pending_count(1);
270
224
  batch.increment(blocking);
@@ -298,17 +252,13 @@ export function run(thunks) {
298
252
  throw STALE_REACTION;
299
253
  }
300
254
 
301
- try {
302
- restore();
303
- return fn();
304
- } finally {
305
- // TODO do we need it here as well as below?
306
- unset_context();
307
- }
255
+ restore();
256
+ return fn();
308
257
  })
309
258
  .catch(handle_error)
310
259
  .finally(() => {
311
260
  unset_context();
261
+ current_batch?.deactivate();
312
262
  });
313
263
 
314
264
  promises.push(promise);