svelte 5.56.9 → 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 (94) 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/migrate/index.js +6 -1
  5. package/src/compiler/phases/1-parse/acorn.js +1 -1
  6. package/src/compiler/phases/1-parse/read/style.js +16 -6
  7. package/src/compiler/phases/1-parse/utils/html.js +12 -7
  8. package/src/compiler/phases/2-analyze/css/css-prune.js +10 -0
  9. package/src/compiler/phases/2-analyze/index.js +45 -18
  10. package/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +10 -4
  11. package/src/compiler/phases/2-analyze/visitors/CallExpression.js +1 -1
  12. package/src/compiler/phases/2-analyze/visitors/DeclarationTag.js +2 -1
  13. package/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js +10 -10
  14. package/src/compiler/phases/2-analyze/visitors/SvelteBody.js +4 -4
  15. package/src/compiler/phases/2-analyze/visitors/SvelteDocument.js +4 -4
  16. package/src/compiler/phases/2-analyze/visitors/SvelteWindow.js +4 -4
  17. package/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +14 -2
  18. package/src/compiler/phases/2-analyze/visitors/shared/element.js +2 -8
  19. package/src/compiler/phases/2-analyze/visitors/shared/utils.js +23 -1
  20. package/src/compiler/phases/3-transform/client/transform-client.js +2 -2
  21. package/src/compiler/phases/3-transform/client/transform-template/index.js +6 -0
  22. package/src/compiler/phases/3-transform/client/utils.js +35 -8
  23. package/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +16 -3
  24. package/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +6 -6
  25. package/src/compiler/phases/3-transform/client/visitors/BindDirective.js +10 -13
  26. package/src/compiler/phases/3-transform/client/visitors/ConstTag.js +19 -8
  27. package/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js +3 -2
  28. package/src/compiler/phases/3-transform/client/visitors/EachBlock.js +4 -1
  29. package/src/compiler/phases/3-transform/client/visitors/Fragment.js +2 -7
  30. package/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +2 -1
  31. package/src/compiler/phases/3-transform/client/visitors/IfBlock.js +2 -1
  32. package/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +2 -1
  33. package/src/compiler/phases/3-transform/client/visitors/RegularElement.js +119 -46
  34. package/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +2 -1
  35. package/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js +0 -1
  36. package/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +11 -4
  37. package/src/compiler/phases/3-transform/client/visitors/shared/element.js +6 -1
  38. package/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +1 -6
  39. package/src/compiler/phases/3-transform/client/visitors/shared/utils.js +5 -5
  40. package/src/compiler/phases/3-transform/css/index.js +5 -1
  41. package/src/compiler/phases/3-transform/server/transform-server.js +20 -9
  42. package/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +1 -1
  43. package/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js +1 -1
  44. package/src/compiler/phases/3-transform/server/visitors/EachBlock.js +8 -2
  45. package/src/compiler/phases/3-transform/server/visitors/IfBlock.js +4 -4
  46. package/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js +1 -0
  47. package/src/compiler/phases/3-transform/server/visitors/RegularElement.js +1 -1
  48. package/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +9 -4
  49. package/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +4 -2
  50. package/src/compiler/phases/3-transform/server/visitors/shared/element.js +53 -47
  51. package/src/compiler/phases/3-transform/server/visitors/shared/utils.js +31 -1
  52. package/src/compiler/phases/bindings.js +3 -1
  53. package/src/compiler/phases/nodes.js +4 -0
  54. package/src/compiler/print/index.js +95 -24
  55. package/src/easing/index.js +62 -0
  56. package/src/internal/client/constants.js +9 -0
  57. package/src/internal/client/context.js +33 -56
  58. package/src/internal/client/dev/hmr.js +15 -4
  59. package/src/internal/client/dom/blocks/boundary.js +21 -12
  60. package/src/internal/client/dom/blocks/css-props.js +3 -3
  61. package/src/internal/client/dom/blocks/slot.js +19 -0
  62. package/src/internal/client/dom/blocks/svelte-head.js +8 -0
  63. package/src/internal/client/dom/css.js +6 -1
  64. package/src/internal/client/dom/elements/attributes.js +37 -35
  65. package/src/internal/client/dom/elements/bindings/select.js +99 -9
  66. package/src/internal/client/dom/elements/bindings/this.js +7 -2
  67. package/src/internal/client/dom/elements/events.js +19 -1
  68. package/src/internal/client/dom/elements/transitions.js +81 -68
  69. package/src/internal/client/dom/operations.js +21 -1
  70. package/src/internal/client/error-handling.js +10 -2
  71. package/src/internal/client/index.js +9 -2
  72. package/src/internal/client/proxy.js +8 -3
  73. package/src/internal/client/reactivity/async.js +23 -3
  74. package/src/internal/client/reactivity/batch.js +1 -0
  75. package/src/internal/client/reactivity/effects.js +14 -1
  76. package/src/internal/client/reactivity/sources.js +7 -1
  77. package/src/internal/client/render.js +4 -8
  78. package/src/internal/client/runtime.js +45 -33
  79. package/src/internal/client/warnings.js +0 -11
  80. package/src/internal/server/abort-signal.js +8 -9
  81. package/src/internal/server/context.js +9 -48
  82. package/src/internal/server/index.js +1 -1
  83. package/src/internal/server/renderer.js +164 -52
  84. package/src/internal/shared/attributes.js +4 -2
  85. package/src/internal/shared/clone.js +2 -0
  86. package/src/internal/shared/context.js +53 -0
  87. package/src/internal/shared/errors.js +2 -2
  88. package/src/legacy/legacy-server.js +1 -1
  89. package/src/reactivity/map.js +22 -0
  90. package/src/reactivity/url.js +2 -0
  91. package/src/transition/index.js +18 -7
  92. package/src/version.js +1 -1
  93. package/types/index.d.ts +148 -46
  94. 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.9",
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",
@@ -1590,7 +1590,12 @@ function migrate_slot_usage(node, path, state) {
1590
1590
  */
1591
1591
  function extract_type_and_comment(declarator, state, path) {
1592
1592
  const str = state.str;
1593
- const parent = path.at(-1);
1593
+ let parent = path.at(-1);
1594
+
1595
+ if (parent?.type === 'Program') {
1596
+ // We don't want comments from the program node
1597
+ parent = undefined;
1598
+ }
1594
1599
 
1595
1600
  // Try to find jsdoc above the declaration
1596
1601
  let comment_node = /** @type {Node} */ (parent)?.leadingComments?.at(-1);
@@ -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 {
@@ -7,8 +7,11 @@ const REGEX_CLOSING_BRACKET = /[\s\]]/;
7
7
  const REGEX_ATTRIBUTE_FLAGS = /[a-zA-Z]+/y; // only `i` and `s` are valid today, but make it future-proof
8
8
  const REGEX_COMBINATOR = /(\+|~|>|\|\|)/y;
9
9
  const REGEX_PERCENTAGE = /\d+(\.\d+)?%/y;
10
+ // `of` must be preceded by whitespace, otherwise it would be part of the `<an+b>` token
11
+ // (`2nof` is a single dimension token). It does not need to be followed by whitespace,
12
+ // because a `.`, `#`, `[`, `*`, `:` or `&` already ends the `of` identifier — minifiers rely on that
10
13
  const REGEX_NTH_OF =
11
- /(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/y;
14
+ /(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of(\s+|(?=[.#[*:&])))/y;
12
15
  const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
13
16
  const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y;
14
17
  const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
@@ -204,15 +207,18 @@ function read_selector(parser, inside_pseudo_class = false) {
204
207
  });
205
208
  } else if (parser.eat('*')) {
206
209
  let name = '*';
210
+ /** @type {string | undefined} */
211
+ let namespace;
207
212
 
208
213
  if (parser.eat('|')) {
209
- // * is the namespace (which we ignore)
210
- name = read_identifier(parser);
214
+ namespace = name;
215
+ name = parser.eat('*') ? '*' : read_identifier(parser);
211
216
  }
212
217
 
213
218
  relative_selector.selectors.push({
214
219
  type: 'TypeSelector',
215
220
  name,
221
+ ...(namespace !== undefined && { namespace }),
216
222
  start,
217
223
  end: parser.index
218
224
  });
@@ -314,15 +320,18 @@ function read_selector(parser, inside_pseudo_class = false) {
314
320
  });
315
321
  } else if (!parser.match_regex(REGEX_COMBINATOR)) {
316
322
  let name = read_identifier(parser);
323
+ /** @type {string | undefined} */
324
+ let namespace;
317
325
 
318
326
  if (parser.eat('|')) {
319
- // we ignore the namespace when trying to find matching element classes
320
- name = read_identifier(parser);
327
+ namespace = name;
328
+ name = parser.eat('*') ? '*' : read_identifier(parser);
321
329
  }
322
330
 
323
331
  relative_selector.selectors.push({
324
332
  type: 'TypeSelector',
325
333
  name,
334
+ ...(namespace !== undefined && { namespace }),
326
335
  start,
327
336
  end: parser.index
328
337
  });
@@ -614,7 +623,8 @@ function read_identifier(parser) {
614
623
  if (char === '\\') {
615
624
  const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE);
616
625
  if (sequence) {
617
- identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16));
626
+ const character = String.fromCodePoint(parseInt(sequence.slice(1), 16));
627
+ identifier += character === '\\' ? '\\\\' : character;
618
628
  parser.index += sequence.length;
619
629
  } else {
620
630
  identifier += '\\' + parser.template[parser.index + 1];
@@ -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
  }
@@ -958,7 +963,7 @@ function calculate_blockers(instance, analysis) {
958
963
  * @param {Set<Binding>} touched
959
964
  * @param {Set<ESTree.Node>} seen
960
965
  */
961
- const touch = (expression, scope, touched, seen = new Set()) => {
966
+ const touch = (expression, scope, touched, seen) => {
962
967
  if (seen.has(expression)) return;
963
968
  seen.add(expression);
964
969
 
@@ -1015,6 +1020,13 @@ function calculate_blockers(instance, analysis) {
1015
1020
  }
1016
1021
  }
1017
1022
 
1023
+ // Share seen nodes across calls so transitive assignments are only visited once.
1024
+ // Keep separate read/write state because the target sets can differ.
1025
+ /** @type {Set<ESTree.Node>} */
1026
+ const writes_seen = new Set();
1027
+ /** @type {Set<ESTree.Node>} */
1028
+ const reads_seen = new Set();
1029
+
1018
1030
  walk(
1019
1031
  node,
1020
1032
  { scope },
@@ -1044,13 +1056,7 @@ function calculate_blockers(instance, analysis) {
1044
1056
  const rune = get_rune(node, context.state.scope);
1045
1057
  if (rune === '$effect') return;
1046
1058
 
1047
- /** @type {Set<Binding>} */
1048
- const touched = new Set();
1049
- touch(node, context.state.scope, touched);
1050
-
1051
- for (const b of touched) {
1052
- writes.add(b);
1053
- }
1059
+ touch(node, context.state.scope, writes, writes_seen);
1054
1060
  },
1055
1061
  Identifier(node, context) {
1056
1062
  const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
@@ -1066,7 +1072,7 @@ function calculate_blockers(instance, analysis) {
1066
1072
  // might be called immediately, so we have to touch all references within it. Example:
1067
1073
  // function foo() { return () => blocker; } foo(); // blocker is touched
1068
1074
  if (node.argument) {
1069
- touch(node.argument, context.state.scope, reads);
1075
+ touch(node.argument, context.state.scope, reads, reads_seen);
1070
1076
  }
1071
1077
  },
1072
1078
  // don't look inside functions until they are called
@@ -1227,6 +1233,23 @@ function calculate_blockers(instance, analysis) {
1227
1233
 
1228
1234
  flush_sync_group();
1229
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
+
1230
1253
  for (const fn of functions) {
1231
1254
  /** @type {Set<Binding>} */
1232
1255
  const reads_writes = new Set();
@@ -1235,12 +1258,15 @@ function calculate_blockers(instance, analysis) {
1235
1258
  ? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init)
1236
1259
  : fn;
1237
1260
 
1238
- trace_references(
1239
- init.body,
1240
- reads_writes,
1241
- reads_writes,
1242
- /** @type {Scope} */ (instance.scopes.get(init))
1243
- );
1261
+ const fn_scope = /** @type {Scope} */ (instance.scopes.get(init));
1262
+
1263
+ if (init.body.type === 'BlockStatement') {
1264
+ trace_references(init.body, reads_writes, reads_writes, fn_scope);
1265
+ } else {
1266
+ // A concise arrow body is an implicit return, so treat it like the
1267
+ // `ReturnStatement` visitor in `trace_references` would.
1268
+ touch(init.body, fn_scope, reads_writes, new Set());
1269
+ }
1244
1270
 
1245
1271
  const max = [...reads_writes].reduce((max, binding) => {
1246
1272
  if (binding.blocker) {
@@ -1314,7 +1340,8 @@ function order_reactive_statements(unsorted_reactive_declarations) {
1314
1340
  * @returns
1315
1341
  */
1316
1342
  const add_declaration = (node, declaration) => {
1317
- 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;
1318
1345
 
1319
1346
  for (const binding of declaration.dependencies) {
1320
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 =