svelte 5.56.10 → 5.57.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 (81) hide show
  1. package/compiler/index.js +1 -1
  2. package/elements.d.ts +3 -0
  3. package/package.json +3 -3
  4. package/src/compiler/phases/1-parse/acorn.js +1 -1
  5. package/src/compiler/phases/1-parse/utils/html.js +12 -7
  6. package/src/compiler/phases/2-analyze/css/css-prune.js +10 -0
  7. package/src/compiler/phases/2-analyze/index.js +26 -3
  8. package/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +10 -4
  9. package/src/compiler/phases/2-analyze/visitors/CallExpression.js +1 -1
  10. package/src/compiler/phases/2-analyze/visitors/DeclarationTag.js +2 -1
  11. package/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js +10 -10
  12. package/src/compiler/phases/2-analyze/visitors/SvelteBody.js +4 -4
  13. package/src/compiler/phases/2-analyze/visitors/SvelteDocument.js +4 -4
  14. package/src/compiler/phases/2-analyze/visitors/SvelteWindow.js +4 -4
  15. package/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +14 -2
  16. package/src/compiler/phases/2-analyze/visitors/shared/element.js +2 -8
  17. package/src/compiler/phases/2-analyze/visitors/shared/utils.js +23 -1
  18. package/src/compiler/phases/3-transform/client/transform-client.js +2 -2
  19. package/src/compiler/phases/3-transform/client/transform-template/index.js +6 -0
  20. package/src/compiler/phases/3-transform/client/utils.js +35 -8
  21. package/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +6 -6
  22. package/src/compiler/phases/3-transform/client/visitors/BindDirective.js +10 -13
  23. package/src/compiler/phases/3-transform/client/visitors/ConstTag.js +19 -8
  24. package/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js +3 -2
  25. package/src/compiler/phases/3-transform/client/visitors/EachBlock.js +4 -1
  26. package/src/compiler/phases/3-transform/client/visitors/Fragment.js +2 -7
  27. package/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +2 -1
  28. package/src/compiler/phases/3-transform/client/visitors/IfBlock.js +2 -1
  29. package/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +2 -1
  30. package/src/compiler/phases/3-transform/client/visitors/RegularElement.js +106 -44
  31. package/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +2 -1
  32. package/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +11 -4
  33. package/src/compiler/phases/3-transform/client/visitors/shared/element.js +6 -1
  34. package/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +1 -6
  35. package/src/compiler/phases/3-transform/client/visitors/shared/utils.js +5 -5
  36. package/src/compiler/phases/3-transform/server/transform-server.js +20 -9
  37. package/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +1 -1
  38. package/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js +1 -1
  39. package/src/compiler/phases/3-transform/server/visitors/EachBlock.js +8 -2
  40. package/src/compiler/phases/3-transform/server/visitors/IfBlock.js +4 -4
  41. package/src/compiler/phases/3-transform/server/visitors/RegularElement.js +1 -1
  42. package/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +9 -4
  43. package/src/compiler/phases/3-transform/server/visitors/shared/element.js +5 -4
  44. package/src/compiler/phases/3-transform/server/visitors/shared/utils.js +30 -0
  45. package/src/compiler/phases/bindings.js +3 -1
  46. package/src/compiler/phases/nodes.js +4 -0
  47. package/src/compiler/print/index.js +21 -16
  48. package/src/easing/index.js +62 -0
  49. package/src/internal/client/constants.js +9 -0
  50. package/src/internal/client/context.js +15 -5
  51. package/src/internal/client/dev/hmr.js +15 -4
  52. package/src/internal/client/dom/blocks/boundary.js +21 -12
  53. package/src/internal/client/dom/blocks/slot.js +19 -0
  54. package/src/internal/client/dom/blocks/svelte-head.js +8 -0
  55. package/src/internal/client/dom/elements/attributes.js +35 -34
  56. package/src/internal/client/dom/elements/bindings/select.js +99 -9
  57. package/src/internal/client/dom/elements/bindings/this.js +7 -2
  58. package/src/internal/client/dom/elements/transitions.js +81 -68
  59. package/src/internal/client/dom/operations.js +21 -1
  60. package/src/internal/client/error-handling.js +10 -2
  61. package/src/internal/client/index.js +9 -2
  62. package/src/internal/client/proxy.js +8 -3
  63. package/src/internal/client/reactivity/async.js +23 -3
  64. package/src/internal/client/reactivity/effects.js +14 -1
  65. package/src/internal/client/render.js +4 -8
  66. package/src/internal/client/runtime.js +45 -33
  67. package/src/internal/client/warnings.js +0 -11
  68. package/src/internal/server/abort-signal.js +8 -9
  69. package/src/internal/server/context.js +2 -2
  70. package/src/internal/server/index.js +1 -1
  71. package/src/internal/server/renderer.js +163 -52
  72. package/src/internal/shared/attributes.js +2 -1
  73. package/src/internal/shared/clone.js +2 -0
  74. package/src/internal/shared/context.js +3 -2
  75. package/src/legacy/legacy-server.js +1 -1
  76. package/src/reactivity/map.js +22 -0
  77. package/src/reactivity/url.js +2 -0
  78. package/src/transition/index.js +18 -7
  79. package/src/version.js +1 -1
  80. package/types/index.d.ts +135 -37
  81. package/types/index.d.ts.map +2 -2
package/elements.d.ts CHANGED
@@ -1352,6 +1352,9 @@ export interface HTMLSelectAttributes extends HTMLAttributes<HTMLSelectElement>
1352
1352
  required?: boolean | undefined | null;
1353
1353
  size?: number | undefined | null;
1354
1354
  value?: any;
1355
+ // needs both casing variants because language tools does lowercase names of non-shorthand attributes
1356
+ defaultValue?: any;
1357
+ defaultvalue?: any;
1355
1358
 
1356
1359
  'on:change'?: ChangeEventHandler<HTMLSelectElement> | undefined | null;
1357
1360
  onchange?: ChangeEventHandler<HTMLSelectElement> | undefined | null;
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.56.10",
5
+ "version": "5.57.0",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -139,13 +139,14 @@
139
139
  ],
140
140
  "devDependencies": {
141
141
  "@jridgewell/trace-mapping": "^0.3.25",
142
- "@playwright/test": "^1.60.0",
142
+ "@playwright/test": "^1.62.0",
143
143
  "@rollup/plugin-commonjs": "^28.0.1",
144
144
  "@rollup/plugin-node-resolve": "^15.3.0",
145
145
  "@rollup/plugin-terser": "^0.4.4",
146
146
  "@rollup/plugin-virtual": "^3.0.2",
147
147
  "@types/aria-query": "^5.0.4",
148
148
  "@types/node": "^20.11.5",
149
+ "@types/trusted-types": "^2.0.7",
149
150
  "baseline-browser-mapping": "^2.10.32",
150
151
  "dts-buddy": "^0.5.5",
151
152
  "esbuild": "^0.28.1",
@@ -161,7 +162,6 @@
161
162
  "@jridgewell/sourcemap-codec": "^1.5.0",
162
163
  "@sveltejs/acorn-typescript": "^1.0.10",
163
164
  "@types/estree": "^1.0.5",
164
- "@types/trusted-types": "^2.0.7",
165
165
  "acorn": "^8.12.1",
166
166
  "aria-query": "5.3.1",
167
167
  "axobject-query": "^4.1.0",
@@ -59,7 +59,7 @@ export function parse(source, comments, typescript, is_script) {
59
59
 
60
60
  return /** @type {Program} */ (ast);
61
61
  } catch (err) {
62
- // TODO the `return` in necessary for TS<7 due to a bug; otherwise
62
+ // TODO the `return` is necessary for TS<7 due to a bug; otherwise
63
63
  // the `finally` block is regarded as unreachable
64
64
  return handle_parse_error(err);
65
65
  } finally {
@@ -20,7 +20,7 @@ function reg_exp_entity(entity_name, is_attribute_value) {
20
20
 
21
21
  /** @param {boolean} is_attribute_value */
22
22
  function get_entity_pattern(is_attribute_value) {
23
- const reg_exp_num = '#(?:x[a-fA-F\\d]+|\\d+)(?:;)?';
23
+ const reg_exp_num = '#(?:[xX][a-fA-F\\d]+|\\d+)(?:;)?';
24
24
  const reg_exp_entities = Object.keys(entities).map(
25
25
  /** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
26
26
  );
@@ -50,7 +50,7 @@ export function decode_character_references(html, is_attribute_value) {
50
50
  // Handle named entities
51
51
  if (entity[0] !== '#') {
52
52
  code = entities[entity];
53
- } else if (entity[1] === 'x') {
53
+ } else if (entity[1] === 'x' || entity[1] === 'X') {
54
54
  code = parseInt(entity.substring(2), 16);
55
55
  } else {
56
56
  code = parseInt(entity.substring(1), 10);
@@ -60,7 +60,7 @@ export function decode_character_references(html, is_attribute_value) {
60
60
  return match;
61
61
  }
62
62
 
63
- return String.fromCodePoint(validate_code(code));
63
+ return String.fromCodePoint(validate_code(code, is_attribute_value));
64
64
  }
65
65
  );
66
66
  }
@@ -75,10 +75,15 @@ const NUL = 0;
75
75
  // Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
76
76
  // Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
77
77
 
78
- /** @param {number} code */
79
- function validate_code(code) {
80
- // line feed becomes generic whitespace
81
- if (code === 10) {
78
+ /**
79
+ * @param {number} code
80
+ * @param {boolean} is_attribute_value
81
+ */
82
+ function validate_code(code, is_attribute_value) {
83
+ // line feed becomes generic whitespace, since it is collapsed along with the
84
+ // surrounding whitespace anyway. In an attribute value it is significant, so it
85
+ // is left alone there
86
+ if (code === 10 && !is_attribute_value) {
82
87
  return 32;
83
88
  }
84
89
 
@@ -122,6 +122,13 @@ const any_selector = {
122
122
  */
123
123
  const seen = new Set();
124
124
 
125
+ /**
126
+ * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
127
+ */
128
+ function is_inside_svelte_head(node) {
129
+ return node.metadata.path.some((ancestor) => ancestor.type === 'SvelteHead');
130
+ }
131
+
125
132
  /**
126
133
  *
127
134
  * @param {Compiler.AST.CSS.StyleSheet} stylesheet
@@ -143,6 +150,9 @@ export function prune(stylesheet, elements) {
143
150
  seen.clear();
144
151
 
145
152
  if (
153
+ // Elements rendered through <svelte:head> are not style-scopable.
154
+ // Prevent css hash injection (class="s-...") on tags like <meta>, <link>, <script>.
155
+ !is_inside_svelte_head(element) &&
146
156
  apply_selector(
147
157
  selectors,
148
158
  /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
@@ -285,7 +285,7 @@ export function analyze_module(source, options) {
285
285
  runes: true,
286
286
  immutable: true,
287
287
  tracing: false,
288
- async_deriveds: new Set(),
288
+ async_deriveds: new Map(),
289
289
  comments,
290
290
  classes: new Map(),
291
291
  pickled_awaits: new Set()
@@ -557,7 +557,7 @@ export function analyze_component(root, source, options) {
557
557
  source,
558
558
  snippet_renderers: new Map(),
559
559
  snippets: new Set(),
560
- async_deriveds: new Set(),
560
+ async_deriveds: new Map(),
561
561
  pickled_awaits: new Set(),
562
562
  instance_body: {
563
563
  sync: [],
@@ -832,6 +832,11 @@ export function analyze_component(root, source, options) {
832
832
  } else {
833
833
  e.export_undefined(specifier, name);
834
834
  }
835
+ } else if (binding.initial?.type === 'SnippetBlock') {
836
+ // If a snippet is exported, a consumer could only import this named export and not the default export (the component).
837
+ // In this case we need to set hasGlobal of our output to true so that e.g. vite-plugin-svelte does not tell Vite to
838
+ // tree-shake the CSS if the default export is not used.
839
+ analysis.css.has_global = true;
835
840
  }
836
841
  }
837
842
  }
@@ -1228,6 +1233,23 @@ function calculate_blockers(instance, analysis) {
1228
1233
 
1229
1234
  flush_sync_group();
1230
1235
 
1236
+ // a store subscription must wait on whatever blocks the store itself; this must happen
1237
+ // before function tracing so that functions reading `$store` inherit the blocker
1238
+ for (const [name, binding] of instance.scope.declarations) {
1239
+ if (binding.kind !== 'store_sub') continue;
1240
+
1241
+ const store_blocker = instance.scope.get(name.slice(1))?.blocker;
1242
+ if (!store_blocker) continue;
1243
+
1244
+ if (
1245
+ !binding.blocker ||
1246
+ /** @type {ESTree.SimpleLiteral & { value: number }} */ (binding.blocker.property).value <
1247
+ /** @type {ESTree.SimpleLiteral & { value: number }} */ (store_blocker.property).value
1248
+ ) {
1249
+ binding.blocker = store_blocker;
1250
+ }
1251
+ }
1252
+
1231
1253
  for (const fn of functions) {
1232
1254
  /** @type {Set<Binding>} */
1233
1255
  const reads_writes = new Set();
@@ -1318,7 +1340,8 @@ function order_reactive_statements(unsorted_reactive_declarations) {
1318
1340
  * @returns
1319
1341
  */
1320
1342
  const add_declaration = (node, declaration) => {
1321
- if ([...reactive_declarations.values()].includes(declaration)) return;
1343
+ // Visited set: each ReactiveStatement is stored under exactly one LabeledStatement node
1344
+ if (reactive_declarations.has(node)) return;
1322
1345
 
1323
1346
  for (const binding of declaration.dependencies) {
1324
1347
  if (declaration.assignments.has(binding)) continue;
@@ -1,6 +1,7 @@
1
1
  /** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
2
2
  /** @import { Context } from '../types' */
3
3
  /** @import { AST } from '#compiler' */
4
+ /** @import { ExpressionMetadata } from '../../nodes.js' */
4
5
  import * as e from '../../../errors.js';
5
6
 
6
7
  /**
@@ -10,15 +11,20 @@ import * as e from '../../../errors.js';
10
11
  export function AwaitExpression(node, context) {
11
12
  const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
12
13
 
13
- // preserve context for awaits that precede other expressions in template or `$derived(...)`
14
14
  if (
15
15
  is_reactive_expression(
16
16
  context.path,
17
17
  context.state.derived_function_depth === context.state.function_depth
18
- ) &&
19
- !is_last_evaluated_expression(context.path, node)
18
+ )
20
19
  ) {
21
- context.state.analysis.pickled_awaits.add(node);
20
+ const expression = /** @type {ExpressionMetadata} */ (context.state.expression);
21
+
22
+ // preserve context for awaits that precede other expressions in template or `$derived(...)`,
23
+ // and for any await that follows one, so the restored context ends at the next suspension
24
+ if (expression.has_pickled_await || !is_last_evaluated_expression(context.path, node)) {
25
+ context.state.analysis.pickled_awaits.add(node);
26
+ expression.has_pickled_await = true;
27
+ }
22
28
  }
23
29
 
24
30
  let suspend = tla;
@@ -253,7 +253,7 @@ export function CallExpression(node, context) {
253
253
  });
254
254
 
255
255
  if (expression.has_await) {
256
- context.state.analysis.async_deriveds.add(node);
256
+ context.state.analysis.async_deriveds.set(node, expression);
257
257
  }
258
258
 
259
259
  // Tell surrounding declaration tag about metadata for correct calculation of blockers etc
@@ -43,7 +43,8 @@ export function DeclarationTag(node, context) {
43
43
  */
44
44
  export function mark_async_declaration(context, metadata, declarations) {
45
45
  const has_await = metadata.expression.has_await;
46
- const blockers = [...metadata.expression.dependencies]
46
+ // reads inside closures must block too, like they do in template expressions
47
+ const blockers = [...metadata.expression.references]
47
48
  .map((dep) => dep.blocker)
48
49
  .filter((b) => b !== null && b.object !== context.state.async_consts?.id);
49
50
 
@@ -1,4 +1,4 @@
1
- /** @import { ExportNamedDeclaration, Identifier } from 'estree' */
1
+ /** @import { ExportNamedDeclaration, Identifier, VariableDeclaration } from 'estree' */
2
2
  /** @import { Context } from '../types' */
3
3
  import * as e from '../../../errors.js';
4
4
  import { extract_identifiers } from '../../../utils/ast.js';
@@ -23,15 +23,6 @@ export function ExportNamedDeclaration(node, context) {
23
23
  }
24
24
 
25
25
  if (node.declaration?.type === 'VariableDeclaration') {
26
- // in runes mode, forbid `export let`
27
- if (
28
- context.state.analysis.runes &&
29
- context.state.ast_type === 'instance' &&
30
- node.declaration.kind === 'let'
31
- ) {
32
- e.legacy_export_invalid(node);
33
- }
34
-
35
26
  for (const declarator of node.declaration.declarations) {
36
27
  for (const id of extract_identifiers(declarator.id)) {
37
28
  const binding = context.state.scope.get(id.name);
@@ -46,6 +37,15 @@ export function ExportNamedDeclaration(node, context) {
46
37
  }
47
38
  }
48
39
  }
40
+
41
+ // in runes mode, forbid `export let`
42
+ if (
43
+ context.state.analysis.runes &&
44
+ context.state.ast_type === 'instance' &&
45
+ node.declaration.kind === 'let'
46
+ ) {
47
+ e.legacy_export_invalid(node);
48
+ }
49
49
  }
50
50
 
51
51
  if (context.state.analysis.runes) {
@@ -3,6 +3,7 @@
3
3
  import * as e from '../../../errors.js';
4
4
  import { is_event_attribute } from '../../../utils/ast.js';
5
5
  import { disallow_children } from './shared/special-element.js';
6
+ import { check_global_event_reference } from './shared/utils.js';
6
7
 
7
8
  /**
8
9
  * @param {AST.SvelteBody} node
@@ -11,10 +12,9 @@ import { disallow_children } from './shared/special-element.js';
11
12
  export function SvelteBody(node, context) {
12
13
  disallow_children(node);
13
14
  for (const attribute of node.attributes) {
14
- if (
15
- attribute.type === 'SpreadAttribute' ||
16
- (attribute.type === 'Attribute' && !is_event_attribute(attribute))
17
- ) {
15
+ if (attribute.type === 'Attribute' && is_event_attribute(attribute)) {
16
+ check_global_event_reference(attribute, context);
17
+ } else if (attribute.type === 'SpreadAttribute' || attribute.type === 'Attribute') {
18
18
  e.svelte_body_illegal_attribute(attribute);
19
19
  }
20
20
  }
@@ -3,6 +3,7 @@
3
3
  import { disallow_children } from './shared/special-element.js';
4
4
  import * as e from '../../../errors.js';
5
5
  import { is_event_attribute } from '../../../utils/ast.js';
6
+ import { check_global_event_reference } from './shared/utils.js';
6
7
 
7
8
  /**
8
9
  * @param {AST.SvelteDocument} node
@@ -12,10 +13,9 @@ export function SvelteDocument(node, context) {
12
13
  disallow_children(node);
13
14
 
14
15
  for (const attribute of node.attributes) {
15
- if (
16
- attribute.type === 'SpreadAttribute' ||
17
- (attribute.type === 'Attribute' && !is_event_attribute(attribute))
18
- ) {
16
+ if (attribute.type === 'Attribute' && is_event_attribute(attribute)) {
17
+ check_global_event_reference(attribute, context);
18
+ } else if (attribute.type === 'SpreadAttribute' || attribute.type === 'Attribute') {
19
19
  e.illegal_element_attribute(attribute, 'svelte:document');
20
20
  }
21
21
  }
@@ -3,6 +3,7 @@
3
3
  import { disallow_children } from './shared/special-element.js';
4
4
  import * as e from '../../../errors.js';
5
5
  import { is_event_attribute } from '../../../utils/ast.js';
6
+ import { check_global_event_reference } from './shared/utils.js';
6
7
 
7
8
  /**
8
9
  * @param {AST.SvelteWindow} node
@@ -12,10 +13,9 @@ export function SvelteWindow(node, context) {
12
13
  disallow_children(node);
13
14
 
14
15
  for (const attribute of node.attributes) {
15
- if (
16
- attribute.type === 'SpreadAttribute' ||
17
- (attribute.type === 'Attribute' && !is_event_attribute(attribute))
18
- ) {
16
+ if (attribute.type === 'Attribute' && is_event_attribute(attribute)) {
17
+ check_global_event_reference(attribute, context);
18
+ } else if (attribute.type === 'SpreadAttribute' || attribute.type === 'Attribute') {
19
19
  e.illegal_element_attribute(attribute, 'svelte:window');
20
20
  }
21
21
  }
@@ -373,11 +373,23 @@ export function check_element(node, context) {
373
373
  }
374
374
  }
375
375
 
376
- if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) {
376
+ // `focus`/`blur` don't bubble, so an element that isn't focusable itself can only be notified
377
+ // about focus changes inside it through `focusin`/`focusout` — accept those as well
378
+ if (
379
+ !has_spread &&
380
+ handlers.has('mouseover') &&
381
+ !handlers.has('focus') &&
382
+ !handlers.has('focusin')
383
+ ) {
377
384
  w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus');
378
385
  }
379
386
 
380
- if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) {
387
+ if (
388
+ !has_spread &&
389
+ handlers.has('mouseout') &&
390
+ !handlers.has('blur') &&
391
+ !handlers.has('focusout')
392
+ ) {
381
393
  w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur');
382
394
  }
383
395
 
@@ -9,6 +9,7 @@ import {
9
9
  validate_attribute_name,
10
10
  validate_slot_attribute
11
11
  } from './attribute.js';
12
+ import { check_global_event_reference } from './utils.js';
12
13
 
13
14
  const EVENT_MODIFIERS = [
14
15
  'preventDefault',
@@ -64,14 +65,7 @@ export function validate_element(node, context) {
64
65
  e.attribute_invalid_event_handler(attribute);
65
66
  }
66
67
 
67
- const value = get_attribute_expression(attribute);
68
- if (
69
- value.type === 'Identifier' &&
70
- value.name === attribute.name &&
71
- !context.state.scope.get(value.name)
72
- ) {
73
- w.attribute_global_event_reference(attribute, attribute.name);
74
- }
68
+ check_global_event_reference(attribute, context);
75
69
  }
76
70
 
77
71
  if (attribute.name === 'slot') {
@@ -4,7 +4,11 @@
4
4
  /** @import { Scope } from '../../../scope' */
5
5
  /** @import { NodeLike } from '../../../../errors.js' */
6
6
  import * as e from '../../../../errors.js';
7
- import { extract_identifiers, get_parent } from '../../../../utils/ast.js';
7
+ import {
8
+ extract_identifiers,
9
+ get_attribute_expression,
10
+ get_parent
11
+ } from '../../../../utils/ast.js';
8
12
  import * as w from '../../../../warnings.js';
9
13
  import * as b from '#compiler/builders';
10
14
  import { get_rune } from '../../../scope.js';
@@ -298,3 +302,21 @@ export function validate_export(node, scope, name) {
298
302
  e.state_invalid_export(node);
299
303
  }
300
304
  }
305
+
306
+ /**
307
+ * Warns when an event attribute uses the shorthand form (`{onclick}`) but the
308
+ * referenced name isn't declared, so it silently resolves to the global handler.
309
+ * @param {AST.Attribute & { value: [AST.ExpressionTag] | AST.ExpressionTag }} attribute
310
+ * @param {Context} context
311
+ */
312
+ export function check_global_event_reference(attribute, context) {
313
+ const value = get_attribute_expression(attribute);
314
+
315
+ if (
316
+ value.type === 'Identifier' &&
317
+ value.name === attribute.name &&
318
+ !context.state.scope.get(value.name)
319
+ ) {
320
+ w.attribute_global_event_reference(attribute, attribute.name);
321
+ }
322
+ }
@@ -247,11 +247,11 @@ export function client_component(analysis, options) {
247
247
  }
248
248
 
249
249
  for (const [node] of analysis.reactive_statements) {
250
- const statement = [...state.legacy_reactive_statements].find(([n]) => n === node);
250
+ const statement = state.legacy_reactive_statements.get(node);
251
251
  if (statement === undefined) {
252
252
  throw new Error('Could not find reactive statement');
253
253
  }
254
- instance.body.push(statement[1]);
254
+ instance.body.push(statement);
255
255
  }
256
256
 
257
257
  if (analysis.reactive_statements.size > 0) {
@@ -39,6 +39,12 @@ export function transform_template(state, name, flags = 0) {
39
39
  const namespace = state.metadata.namespace;
40
40
  const tree = state.options.fragments === 'tree';
41
41
 
42
+ const { nodes } = state.template;
43
+ const is_lone_anchor = nodes.length === 1 && nodes[0].type === 'comment';
44
+
45
+ // special case - `$.comment` creates the anchor more cheaply than cloning a template
46
+ if (is_lone_anchor) return b.id('$.comment');
47
+
42
48
  const expression = tree ? state.template.as_tree() : state.template.as_html();
43
49
 
44
50
  const key =
@@ -2,6 +2,7 @@
2
2
  /** @import { Binding } from '#compiler' */
3
3
  /** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */
4
4
  /** @import { Analysis } from '../../types.js' */
5
+ /** @import { ExpressionMetadata } from '../../nodes.js' */
5
6
  /** @import { Scope } from '../../scope.js' */
6
7
  import * as b from '#compiler/builders';
7
8
  import { is_simple_expression, save } from '../../../utils/ast.js';
@@ -164,20 +165,46 @@ export function should_proxy(node, scope) {
164
165
  return true;
165
166
  }
166
167
 
168
+ /**
169
+ * An async thunk. If an `await` inside restores the reaction context via `$.save`,
170
+ * the body exits through `$.unsave` so the context cannot leak into foreign microtasks
171
+ * that run before the returned promise settles
172
+ * @param {Expression | BlockStatement} body
173
+ * @param {ExpressionMetadata} metadata
174
+ */
175
+ export function async_thunk(body, metadata) {
176
+ if (!metadata.has_pickled_await) {
177
+ return b.arrow([], body, true);
178
+ }
179
+
180
+ const block = body.type === 'BlockStatement' ? body : b.block([b.return(body)]);
181
+
182
+ return b.arrow(
183
+ [],
184
+ b.block([
185
+ {
186
+ type: 'TryStatement',
187
+ block,
188
+ handler: null,
189
+ finalizer: b.block([b.stmt(b.call('$.unsave'))])
190
+ }
191
+ ]),
192
+ true
193
+ );
194
+ }
195
+
167
196
  /**
168
197
  * Svelte legacy mode should use safe equals in most places, runes mode shouldn't
169
198
  * @param {ComponentClientTransformState} state
170
199
  * @param {Expression | BlockStatement} expression
171
- * @param {boolean} [async]
200
+ * @param {ExpressionMetadata} [metadata]
172
201
  */
173
- export function create_derived(state, expression, async = false) {
174
- const thunk = b.thunk(expression, async);
175
-
176
- if (async) {
177
- return save(b.call('$.async_derived', thunk));
178
- } else {
179
- return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
202
+ export function create_derived(state, expression, metadata) {
203
+ if (metadata?.has_await) {
204
+ return save(b.call('$.async_derived', async_thunk(expression, metadata)));
180
205
  }
206
+
207
+ return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', b.thunk(expression));
181
208
  }
182
209
 
183
210
  /**
@@ -1,9 +1,9 @@
1
1
  /** @import { BlockStatement, Pattern, Statement } from 'estree' */
2
2
  /** @import { AST } from '#compiler' */
3
3
  /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
4
- import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js';
4
+ import { extract_identifiers } from '../../../../utils/ast.js';
5
5
  import * as b from '#compiler/builders';
6
- import { create_derived } from '../utils.js';
6
+ import { async_thunk, create_derived } from '../utils.js';
7
7
  import { get_value } from './shared/declarations.js';
8
8
  import { build_expression, add_svelte_meta } from './shared/utils.js';
9
9
 
@@ -15,10 +15,10 @@ export function AwaitBlock(node, context) {
15
15
  context.state.template.push_comment();
16
16
 
17
17
  // Visit {#await <expression>} first to ensure that scopes are in the correct order
18
- const expression = b.thunk(
19
- build_expression(context, node.expression, node.metadata.expression),
20
- node.metadata.expression.has_await
21
- );
18
+ const input = build_expression(context, node.expression, node.metadata.expression);
19
+ const expression = node.metadata.expression.has_await
20
+ ? async_thunk(input, node.metadata.expression)
21
+ : b.thunk(input);
22
22
 
23
23
  let then_block;
24
24
  let catch_block;
@@ -1,7 +1,7 @@
1
1
  /** @import { CallExpression, Expression, Pattern } from 'estree' */
2
2
  /** @import { AST } from '#compiler' */
3
3
  /** @import { ComponentContext } from '../types' */
4
- import { dev, is_ignored } from '../../../../state.js';
4
+ import { dev, ignore_map, is_ignored } from '../../../../state.js';
5
5
  import { is_text_attribute } from '../../../../utils/ast.js';
6
6
  import * as b from '#compiler/builders';
7
7
  import { binding_properties } from '../../../bindings.js';
@@ -40,9 +40,15 @@ export function BindDirective(node, context) {
40
40
  validate_binding(context.state, node, expression);
41
41
  }
42
42
 
43
- const assignment = /** @type {Expression} */ (
44
- context.visit(b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value')))
43
+ const raw_assignment = b.assignment(
44
+ '=',
45
+ /** @type {Pattern} */ (node.expression),
46
+ b.id('$$value')
45
47
  );
48
+ // The assignment is generated, so inherit any ignores attached to the binding
49
+ ignore_map.set(raw_assignment, ignore_map.get(node) ?? []);
50
+
51
+ const assignment = /** @type {Expression} */ (context.visit(raw_assignment));
46
52
 
47
53
  if (dev) {
48
54
  // in dev, create named functions, so that `$inspect(...)` delivers
@@ -58,16 +64,7 @@ export function BindDirective(node, context) {
58
64
  get = b.thunk(expression);
59
65
 
60
66
  /** @type {Expression | undefined} */
61
- set = b.unthunk(
62
- b.arrow(
63
- [b.id('$$value')],
64
- /** @type {Expression} */ (
65
- context.visit(
66
- b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))
67
- )
68
- )
69
- )
70
- );
67
+ set = b.unthunk(b.arrow([b.id('$$value')], assignment));
71
68
 
72
69
  if (get === set) {
73
70
  set = undefined;
@@ -19,7 +19,7 @@ export function ConstTag(node, context) {
19
19
  if (declaration.id.type === 'Identifier') {
20
20
  const init = build_expression(context, declaration.init, node.metadata.expression);
21
21
 
22
- let expression = create_derived(context.state, init, node.metadata.expression.has_await);
22
+ let expression = create_derived(context.state, init, node.metadata.expression);
23
23
 
24
24
  if (dev) {
25
25
  expression = b.call('$.tag', expression, b.literal(declaration.id.name));
@@ -45,20 +45,31 @@ export function ConstTag(node, context) {
45
45
  transform
46
46
  });
47
47
 
48
- // TODO optimise the simple `{ x } = y` case — we can just return `y`
49
- // instead of destructuring it only to return a new object
48
+ const is_simple_object_pattern =
49
+ declaration.id.type === 'ObjectPattern' &&
50
+ declaration.id.properties.every(
51
+ (p) =>
52
+ p.type === 'Property' &&
53
+ !p.computed &&
54
+ p.key.type === 'Identifier' &&
55
+ p.value.type === 'Identifier' &&
56
+ p.key.name === p.value.name
57
+ );
58
+
50
59
  const init = build_expression(
51
60
  { ...context, state: child_state },
52
61
  declaration.init,
53
62
  node.metadata.expression
54
63
  );
55
64
 
56
- const block = b.block([
57
- b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
58
- b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
59
- ]);
65
+ const block = is_simple_object_pattern
66
+ ? b.block([b.return(init)])
67
+ : b.block([
68
+ b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
69
+ b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
70
+ ]);
60
71
 
61
- let expression = create_derived(context.state, block, node.metadata.expression.has_await);
72
+ let expression = create_derived(context.state, block, node.metadata.expression);
62
73
 
63
74
  if (dev) {
64
75
  expression = b.call('$.tag', expression, b.literal('[@const]'));