svelte 5.56.8 → 5.56.10

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 (34) hide show
  1. package/compiler/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/compiler/index.js +2 -1
  4. package/src/compiler/migrate/index.js +6 -1
  5. package/src/compiler/phases/1-parse/index.js +4 -0
  6. package/src/compiler/phases/1-parse/read/style.js +75 -29
  7. package/src/compiler/phases/2-analyze/css/css-analyze.js +3 -0
  8. package/src/compiler/phases/2-analyze/css/css-warn.js +3 -0
  9. package/src/compiler/phases/2-analyze/index.js +19 -15
  10. package/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +16 -3
  11. package/src/compiler/phases/3-transform/client/visitors/RegularElement.js +13 -2
  12. package/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js +0 -1
  13. package/src/compiler/phases/3-transform/css/index.js +8 -1
  14. package/src/compiler/phases/3-transform/server/visitors/LabeledStatement.js +1 -0
  15. package/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +4 -2
  16. package/src/compiler/phases/3-transform/server/visitors/shared/element.js +48 -43
  17. package/src/compiler/phases/3-transform/server/visitors/shared/utils.js +1 -1
  18. package/src/compiler/print/index.js +372 -177
  19. package/src/internal/client/context.js +20 -53
  20. package/src/internal/client/dom/blocks/css-props.js +3 -3
  21. package/src/internal/client/dom/blocks/each.js +6 -2
  22. package/src/internal/client/dom/css.js +6 -1
  23. package/src/internal/client/dom/elements/attributes.js +2 -1
  24. package/src/internal/client/dom/elements/events.js +19 -1
  25. package/src/internal/client/reactivity/batch.js +1 -0
  26. package/src/internal/client/reactivity/sources.js +7 -1
  27. package/src/internal/server/context.js +8 -47
  28. package/src/internal/server/renderer.js +1 -0
  29. package/src/internal/shared/attributes.js +2 -1
  30. package/src/internal/shared/context.js +52 -0
  31. package/src/internal/shared/errors.js +2 -2
  32. package/src/version.js +1 -1
  33. package/types/index.d.ts +23 -9
  34. package/types/index.d.ts.map +1 -1
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "svelte",
3
3
  "description": "Cybernetically enhanced web apps",
4
4
  "license": "MIT",
5
- "version": "5.56.8",
5
+ "version": "5.56.10",
6
6
  "type": "module",
7
7
  "types": "./types/index.d.ts",
8
8
  "engines": {
@@ -141,7 +141,8 @@ export function parseCss(source) {
141
141
  type: 'StyleSheetFile',
142
142
  start: 0,
143
143
  end: source.length,
144
- children
144
+ children,
145
+ comments: parser.css_comments
145
146
  };
146
147
  }
147
148
 
@@ -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);
@@ -50,6 +50,9 @@ export class Parser {
50
50
  /** */
51
51
  index = 0;
52
52
 
53
+ /** @type {AST.CSS.CSSComment[]} */
54
+ css_comments = [];
55
+
53
56
  /**
54
57
  * Creates a minimal parser instance for CSS-only parsing.
55
58
  * Skips Svelte component parsing setup.
@@ -61,6 +64,7 @@ export class Parser {
61
64
  parser.template = source;
62
65
  parser.index = 0;
63
66
  parser.loose = false;
67
+ parser.css_comments = [];
64
68
  return parser;
65
69
  }
66
70
 
@@ -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_-]/;
@@ -24,6 +27,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
24
27
  */
25
28
  export default function read_style(parser, start, attributes) {
26
29
  const content_start = parser.index;
30
+ parser.css_comments = [];
27
31
  const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
28
32
  const content_end = parser.index;
29
33
 
@@ -36,6 +40,7 @@ export default function read_style(parser, start, attributes) {
36
40
  end: parser.index,
37
41
  attributes,
38
42
  children,
43
+ comments: parser.css_comments,
39
44
  content: {
40
45
  start: content_start,
41
46
  end: content_end,
@@ -202,15 +207,18 @@ function read_selector(parser, inside_pseudo_class = false) {
202
207
  });
203
208
  } else if (parser.eat('*')) {
204
209
  let name = '*';
210
+ /** @type {string | undefined} */
211
+ let namespace;
205
212
 
206
213
  if (parser.eat('|')) {
207
- // * is the namespace (which we ignore)
208
- name = read_identifier(parser);
214
+ namespace = name;
215
+ name = parser.eat('*') ? '*' : read_identifier(parser);
209
216
  }
210
217
 
211
218
  relative_selector.selectors.push({
212
219
  type: 'TypeSelector',
213
220
  name,
221
+ ...(namespace !== undefined && { namespace }),
214
222
  start,
215
223
  end: parser.index
216
224
  });
@@ -229,18 +237,22 @@ function read_selector(parser, inside_pseudo_class = false) {
229
237
  end: parser.index
230
238
  });
231
239
  } else if (parser.eat('::')) {
240
+ const name = read_identifier(parser);
241
+ /** @type {AST.CSS.SelectorList | null} */
242
+ let args = null;
243
+
244
+ if (parser.eat('(')) {
245
+ args = read_selector_list(parser, true);
246
+ parser.eat(')', true);
247
+ }
248
+
232
249
  relative_selector.selectors.push({
233
250
  type: 'PseudoElementSelector',
234
- name: read_identifier(parser),
251
+ name,
235
252
  start,
236
- end: parser.index
253
+ end: parser.index,
254
+ ...(args && { args })
237
255
  });
238
- // We read the inner selectors of a pseudo element to ensure it parses correctly,
239
- // but we don't do anything with the result.
240
- if (parser.eat('(')) {
241
- read_selector_list(parser, true);
242
- parser.eat(')', true);
243
- }
244
256
  } else if (parser.eat(':')) {
245
257
  const name = read_identifier(parser);
246
258
 
@@ -308,22 +320,25 @@ function read_selector(parser, inside_pseudo_class = false) {
308
320
  });
309
321
  } else if (!parser.match_regex(REGEX_COMBINATOR)) {
310
322
  let name = read_identifier(parser);
323
+ /** @type {string | undefined} */
324
+ let namespace;
311
325
 
312
326
  if (parser.eat('|')) {
313
- // we ignore the namespace when trying to find matching element classes
314
- name = read_identifier(parser);
327
+ namespace = name;
328
+ name = parser.eat('*') ? '*' : read_identifier(parser);
315
329
  }
316
330
 
317
331
  relative_selector.selectors.push({
318
332
  type: 'TypeSelector',
319
333
  name,
334
+ ...(namespace !== undefined && { namespace }),
320
335
  start,
321
336
  end: parser.index
322
337
  });
323
338
  }
324
339
 
325
340
  const index = parser.index;
326
- allow_comment_or_whitespace(parser);
341
+ allow_comment_or_whitespace(parser, false);
327
342
 
328
343
  if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
329
344
  // rewind, so we know whether to continue building the selector list
@@ -449,7 +464,7 @@ function read_block_item(parser) {
449
464
  // read ahead to understand whether we're dealing with a declaration or a nested rule.
450
465
  // this involves some duplicated work, but avoids a try-catch that would disguise errors
451
466
  const start = parser.index;
452
- read_value(parser);
467
+ read_value(parser, false);
453
468
  const char = parser.template[parser.index];
454
469
  parser.index = start;
455
470
 
@@ -492,10 +507,13 @@ function read_declaration(parser) {
492
507
 
493
508
  /**
494
509
  * @param {Parser} parser
510
+ * @param {boolean} [capture_comments]
495
511
  * @returns {string}
496
512
  */
497
- function read_value(parser) {
513
+ function read_value(parser, capture_comments = true) {
498
514
  let value = '';
515
+ /** @type {AST.CSS.CSSComment[]} */
516
+ const value_comments = [];
499
517
  let escaped = false;
500
518
  let in_url = false;
501
519
 
@@ -523,6 +541,13 @@ function read_value(parser) {
523
541
  } else if (char === '(' && value.slice(-3) === 'url') {
524
542
  in_url = true;
525
543
  } else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
544
+ const leading_whitespace = value.length - value.trimStart().length;
545
+ for (const comment of value_comments) {
546
+ comment.position = Math.max(
547
+ 0,
548
+ /** @type {number} */ (comment.position) - leading_whitespace
549
+ );
550
+ }
526
551
  return value.trim();
527
552
  } else if (
528
553
  char === '/' &&
@@ -530,13 +555,11 @@ function read_value(parser) {
530
555
  !quote_mark &&
531
556
  parser.template[parser.index + 1] === '*'
532
557
  ) {
533
- parser.index += 2;
534
- while (parser.index < parser.template.length) {
535
- if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') {
536
- parser.index += 2;
537
- break;
538
- }
539
- parser.index++;
558
+ const comment = read_comment(parser);
559
+ if (capture_comments) {
560
+ comment.position = value.length;
561
+ parser.css_comments.push(comment);
562
+ value_comments.push(comment);
540
563
  }
541
564
  continue;
542
565
  }
@@ -600,7 +623,8 @@ function read_identifier(parser) {
600
623
  if (char === '\\') {
601
624
  const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE);
602
625
  if (sequence) {
603
- identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16));
626
+ const character = String.fromCodePoint(parseInt(sequence.slice(1), 16));
627
+ identifier += character === '\\' ? '\\\\' : character;
604
628
  parser.index += sequence.length;
605
629
  } else {
606
630
  identifier += '\\' + parser.template[parser.index + 1];
@@ -624,13 +648,16 @@ function read_identifier(parser) {
624
648
  return identifier;
625
649
  }
626
650
 
627
- /** @param {Parser} parser */
628
- function allow_comment_or_whitespace(parser) {
651
+ /**
652
+ * @param {Parser} parser
653
+ * @param {boolean} [capture_comments]
654
+ */
655
+ function allow_comment_or_whitespace(parser, capture_comments = true) {
629
656
  parser.allow_whitespace();
630
657
  while (parser.match('/*') || parser.match('<!--')) {
631
- if (parser.eat('/*')) {
632
- parser.read_until(REGEX_COMMENT_CLOSE);
633
- parser.eat('*/', true);
658
+ if (parser.match('/*')) {
659
+ const comment = read_comment(parser);
660
+ if (capture_comments) parser.css_comments.push(comment);
634
661
  }
635
662
 
636
663
  if (parser.eat('<!--')) {
@@ -642,6 +669,25 @@ function allow_comment_or_whitespace(parser) {
642
669
  }
643
670
  }
644
671
 
672
+ /**
673
+ * @param {Parser} parser
674
+ * @returns {AST.CSS.CSSComment}
675
+ */
676
+ function read_comment(parser) {
677
+ const start = parser.index;
678
+ parser.eat('/*', true);
679
+ const value = parser.read_until(REGEX_COMMENT_CLOSE);
680
+ parser.eat('*/', true);
681
+ const end = parser.index;
682
+
683
+ return {
684
+ type: 'CSSComment',
685
+ value,
686
+ start,
687
+ end
688
+ };
689
+ }
690
+
645
691
  /**
646
692
  * Parse standalone CSS content (not wrapped in `<style>`).
647
693
  * @param {Parser} parser
@@ -312,6 +312,9 @@ const css_visitors = {
312
312
  }
313
313
 
314
314
  context.next();
315
+ },
316
+ PseudoElementSelector() {
317
+ // Don't analyse these
315
318
  }
316
319
  };
317
320
 
@@ -23,6 +23,9 @@ const visitors = {
23
23
  context.next();
24
24
  }
25
25
  },
26
+ PseudoElementSelector() {
27
+ // Don't analyse these
28
+ },
26
29
  ComplexSelector(node, context) {
27
30
  if (
28
31
  !node.metadata.used &&
@@ -958,7 +958,7 @@ function calculate_blockers(instance, analysis) {
958
958
  * @param {Set<Binding>} touched
959
959
  * @param {Set<ESTree.Node>} seen
960
960
  */
961
- const touch = (expression, scope, touched, seen = new Set()) => {
961
+ const touch = (expression, scope, touched, seen) => {
962
962
  if (seen.has(expression)) return;
963
963
  seen.add(expression);
964
964
 
@@ -1015,6 +1015,13 @@ function calculate_blockers(instance, analysis) {
1015
1015
  }
1016
1016
  }
1017
1017
 
1018
+ // Share seen nodes across calls so transitive assignments are only visited once.
1019
+ // Keep separate read/write state because the target sets can differ.
1020
+ /** @type {Set<ESTree.Node>} */
1021
+ const writes_seen = new Set();
1022
+ /** @type {Set<ESTree.Node>} */
1023
+ const reads_seen = new Set();
1024
+
1018
1025
  walk(
1019
1026
  node,
1020
1027
  { scope },
@@ -1044,13 +1051,7 @@ function calculate_blockers(instance, analysis) {
1044
1051
  const rune = get_rune(node, context.state.scope);
1045
1052
  if (rune === '$effect') return;
1046
1053
 
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
- }
1054
+ touch(node, context.state.scope, writes, writes_seen);
1054
1055
  },
1055
1056
  Identifier(node, context) {
1056
1057
  const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
@@ -1066,7 +1067,7 @@ function calculate_blockers(instance, analysis) {
1066
1067
  // might be called immediately, so we have to touch all references within it. Example:
1067
1068
  // function foo() { return () => blocker; } foo(); // blocker is touched
1068
1069
  if (node.argument) {
1069
- touch(node.argument, context.state.scope, reads);
1070
+ touch(node.argument, context.state.scope, reads, reads_seen);
1070
1071
  }
1071
1072
  },
1072
1073
  // don't look inside functions until they are called
@@ -1235,12 +1236,15 @@ function calculate_blockers(instance, analysis) {
1235
1236
  ? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init)
1236
1237
  : fn;
1237
1238
 
1238
- trace_references(
1239
- init.body,
1240
- reads_writes,
1241
- reads_writes,
1242
- /** @type {Scope} */ (instance.scopes.get(init))
1243
- );
1239
+ const fn_scope = /** @type {Scope} */ (instance.scopes.get(init));
1240
+
1241
+ if (init.body.type === 'BlockStatement') {
1242
+ trace_references(init.body, reads_writes, reads_writes, fn_scope);
1243
+ } else {
1244
+ // A concise arrow body is an implicit return, so treat it like the
1245
+ // `ReturnStatement` visitor in `trace_references` would.
1246
+ touch(init.body, fn_scope, reads_writes, new Set());
1247
+ }
1244
1248
 
1245
1249
  const max = [...reads_writes].reduce((max, binding) => {
1246
1250
  if (binding.blocker) {
@@ -1,4 +1,4 @@
1
- /** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */
1
+ /** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, LogicalOperator, Pattern } from 'estree' */
2
2
  /** @import { AST } from '#compiler' */
3
3
  /** @import { Context } from '../types.js' */
4
4
  import * as b from '#compiler/builders';
@@ -79,8 +79,13 @@ function build_assignment(operator, left, right, context) {
79
79
 
80
80
  // special case — assignment to private state field
81
81
  if (left.property.type === 'PrivateIdentifier') {
82
+ const logical_operator = ['||=', '&&=', '??='].includes(operator)
83
+ ? /** @type {LogicalOperator} */ (operator.slice(0, -1))
84
+ : null;
82
85
  let value = /** @type {Expression} */ (
83
- context.visit(build_assignment_value(operator, left, right))
86
+ context.visit(
87
+ logical_operator === null ? build_assignment_value(operator, left, right) : right
88
+ )
84
89
  );
85
90
 
86
91
  const needs_proxy =
@@ -88,7 +93,15 @@ function build_assignment(operator, left, right, context) {
88
93
  is_non_coercive_operator(operator) &&
89
94
  should_proxy(value, context.state.scope);
90
95
 
91
- return b.call('$.set', left, value, needs_proxy && b.true);
96
+ const assignment = b.call('$.set', left, value, needs_proxy && b.true);
97
+
98
+ return logical_operator === null
99
+ ? assignment
100
+ : b.logical(
101
+ logical_operator,
102
+ /** @type {Expression} */ (context.visit(left)),
103
+ assignment
104
+ );
92
105
  }
93
106
  }
94
107
  }
@@ -664,14 +664,25 @@ function build_element_attribute_update(element, node_id, name, value, attribute
664
664
  * @param {ComponentContext} context
665
665
  */
666
666
  function build_custom_element_attribute_update_assignment(node_id, attribute, context) {
667
- const { value, has_state } = build_attribute_value(attribute.value, context);
667
+ const memoizer = new Memoizer();
668
+ const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
669
+ memoizer.add(value, metadata)
670
+ );
668
671
 
669
672
  // don't lowercase name, as we set the element's property, which might be case sensitive
670
673
  const call = b.call('$.set_custom_element_data', node_id, b.literal(attribute.name), value);
671
674
 
672
675
  // this is different from other updates — it doesn't get grouped,
673
676
  // because set_custom_element_data may not be idempotent
674
- const update = has_state ? b.call('$.template_effect', b.thunk(call)) : call;
677
+ const update = has_state
678
+ ? b.call(
679
+ '$.template_effect',
680
+ b.arrow(memoizer.apply(), call),
681
+ memoizer.sync_values(),
682
+ memoizer.async_values(),
683
+ memoizer.blockers()
684
+ )
685
+ : call;
675
686
 
676
687
  context.state.init.push(b.stmt(update));
677
688
  }
@@ -13,7 +13,6 @@ export function UpdateExpression(node, context) {
13
13
 
14
14
  if (
15
15
  argument.type === 'MemberExpression' &&
16
- argument.object.type === 'ThisExpression' &&
17
16
  argument.property.type === 'PrivateIdentifier' &&
18
17
  context.state.state_fields.has('#' + argument.property.name)
19
18
  ) {
@@ -355,7 +355,11 @@ const visitors = {
355
355
  continue;
356
356
  }
357
357
 
358
- if (selector.type === 'TypeSelector' && selector.name === '*') {
358
+ if (
359
+ selector.type === 'TypeSelector' &&
360
+ selector.name === '*' &&
361
+ selector.namespace === undefined
362
+ ) {
359
363
  context.state.code.update(selector.start, selector.end, modifier);
360
364
  } else {
361
365
  context.state.code.appendLeft(selector.end, modifier);
@@ -374,6 +378,9 @@ const visitors = {
374
378
  if (node.name === 'is' || node.name === 'where' || node.name === 'has' || node.name === 'not') {
375
379
  context.next();
376
380
  }
381
+ },
382
+ PseudoElementSelector() {
383
+ // Functional pseudo-element arguments are not scoped as selectors.
377
384
  }
378
385
  };
379
386
 
@@ -8,6 +8,7 @@ import * as b from '#compiler/builders';
8
8
  */
9
9
  export function LabeledStatement(node, context) {
10
10
  if (context.state.analysis.runes || context.path.length > 1 || node.label.name !== '$') {
11
+ context.next();
11
12
  return;
12
13
  }
13
14
 
@@ -102,9 +102,11 @@ export function VariableDeclaration(node, context) {
102
102
  } else {
103
103
  const call = /** @type {CallExpression} */ (declarator.init);
104
104
 
105
- let rhs = value;
105
+ // - cannot be a SpreadElement because refused during analysis
106
+ // - use args[0] rather than value to avoid visiting twice (above in const value = ... and below in for-ofs)
107
+ let rhs = /** @type {Expression} */ (call.arguments[0]);
106
108
 
107
- if (rune !== '$derived' || call.arguments[0].type !== 'Identifier') {
109
+ if (rune === '$derived.by' || call.arguments[0].type !== 'Identifier') {
108
110
  const id = b.id(context.state.scope.generate('$$d'));
109
111
 
110
112
  rhs = b.call(id);
@@ -3,7 +3,7 @@
3
3
  /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
4
4
  import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
5
5
  import { binding_properties } from '../../../../bindings.js';
6
- import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../../nodes.js';
6
+ import { ExpressionMetadata, is_custom_element_node } from '../../../../nodes.js';
7
7
  import { regex_starts_with_newline } from '../../../../patterns.js';
8
8
  import * as b from '#compiler/builders';
9
9
  import {
@@ -21,6 +21,11 @@ import { escape_html } from '../../../../../../escaping.js';
21
21
 
22
22
  const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
23
23
 
24
+ /**
25
+ * @typedef {{ type: 'transformed', name: string, expression: Expression }} TransformedAttribute
26
+ * An attribute whose expression has already been transformed and must not be visited again.
27
+ */
28
+
24
29
  /**
25
30
  * Writes the output to the template output. Some elements may have attributes on them that require the
26
31
  * their output to be the child content instead. In this case, an object is returned.
@@ -29,7 +34,7 @@ const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
29
34
  * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
30
35
  */
31
36
  export function build_element_attributes(node, context, transform) {
32
- /** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
37
+ /** @type {Array<AST.Attribute | AST.SpreadAttribute | TransformedAttribute>} */
33
38
  const attributes = [];
34
39
 
35
40
  /** @type {AST.ClassDirective[]} */
@@ -145,42 +150,26 @@ export function build_element_attributes(node, context, transform) {
145
150
  attr.value[0].data === 'checkbox'
146
151
  );
147
152
 
148
- attributes.push(
149
- create_attribute('checked', null, -1, -1, [
150
- {
151
- type: 'ExpressionTag',
152
- start: -1,
153
- end: -1,
154
- expression: is_checkbox
155
- ? b.call(
156
- b.member(attribute.expression, 'includes'),
157
- build_attribute_value(value_attribute.value, context, transform)
158
- )
159
- : b.binary(
160
- '===',
161
- attribute.expression,
162
- build_attribute_value(value_attribute.value, context, transform)
163
- ),
164
- metadata: {
165
- expression: new ExpressionMetadata()
166
- }
167
- }
168
- ])
169
- );
153
+ attributes.push({
154
+ type: 'transformed',
155
+ name: 'checked',
156
+ expression: is_checkbox
157
+ ? b.call(
158
+ b.member(expression, 'includes'),
159
+ build_attribute_value(value_attribute.value, context, transform)
160
+ )
161
+ : b.binary(
162
+ '===',
163
+ expression,
164
+ build_attribute_value(value_attribute.value, context, transform)
165
+ )
166
+ });
170
167
  } else {
171
- attributes.push(
172
- create_attribute(attribute.name, null, -1, -1, [
173
- {
174
- type: 'ExpressionTag',
175
- start: -1,
176
- end: -1,
177
- expression,
178
- metadata: {
179
- expression: new ExpressionMetadata()
180
- }
181
- }
182
- ])
183
- );
168
+ attributes.push({
169
+ type: 'transformed',
170
+ name: get_attribute_name(node, attribute),
171
+ expression
172
+ });
184
173
  }
185
174
  } else if (attribute.type === 'SpreadAttribute') {
186
175
  attributes.push(attribute);
@@ -217,7 +206,21 @@ export function build_element_attributes(node, context, transform) {
217
206
  } else {
218
207
  const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null;
219
208
 
220
- for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
209
+ for (const attribute of /** @type {Array<AST.Attribute | TransformedAttribute>} */ (
210
+ attributes
211
+ )) {
212
+ if (attribute.type === 'transformed') {
213
+ context.state.template.push(
214
+ b.call(
215
+ '$.attr',
216
+ b.literal(attribute.name),
217
+ attribute.expression,
218
+ is_boolean_attribute(attribute.name) && b.true
219
+ )
220
+ );
221
+ continue;
222
+ }
223
+
221
224
  const name = get_attribute_name(node, attribute);
222
225
  const can_use_literal =
223
226
  (name !== 'class' || class_directives.length === 0) &&
@@ -298,14 +301,16 @@ function get_attribute_name(element, attribute) {
298
301
 
299
302
  /**
300
303
  * @param {AST.RegularElement | AST.SvelteElement} element
301
- * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
304
+ * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective | TransformedAttribute>} attributes
302
305
  * @param {ComponentContext} context
303
306
  * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
304
307
  */
305
308
  export function build_spread_object(element, attributes, context, transform) {
306
309
  const object = b.object(
307
310
  attributes.map((attribute) => {
308
- if (attribute.type === 'Attribute') {
311
+ if (attribute.type === 'transformed') {
312
+ return b.prop('init', b.key(attribute.name), attribute.expression);
313
+ } else if (attribute.type === 'Attribute') {
309
314
  const name = get_attribute_name(element, attribute);
310
315
  const value = build_attribute_value(
311
316
  attribute.value,
@@ -340,7 +345,7 @@ export function build_spread_object(element, attributes, context, transform) {
340
345
  /**
341
346
  *
342
347
  * @param {AST.RegularElement | AST.SvelteElement} element
343
- * @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
348
+ * @param {Array<AST.Attribute | AST.SpreadAttribute | TransformedAttribute>} attributes
344
349
  * @param {AST.StyleDirective[]} style_directives
345
350
  * @param {AST.ClassDirective[]} class_directives
346
351
  * @param {ComponentContext} context
@@ -356,7 +361,7 @@ function build_element_spread_attributes(
356
361
  ) {
357
362
  const args = prepare_element_spread(
358
363
  element,
359
- /** @type {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} */ (attributes),
364
+ attributes,
360
365
  style_directives,
361
366
  class_directives,
362
367
  context,
@@ -410,7 +415,7 @@ export function prepare_element_spread_object(element, context, transform) {
410
415
  /**
411
416
  * Prepare args for $.attributes(...): compute object, css_hash, classes, styles and flags.
412
417
  * @param {AST.RegularElement | AST.SvelteElement} element
413
- * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
418
+ * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective | TransformedAttribute>} attributes
414
419
  * @param {AST.StyleDirective[]} style_directives
415
420
  * @param {AST.ClassDirective[]} class_directives
416
421
  * @param {ComponentContext} context
@@ -286,7 +286,7 @@ export function build_getter(node, state) {
286
286
  }
287
287
 
288
288
  if (binding.kind === 'derived') {
289
- return (binding.declaration_kind === 'var' ? b.maybe_call : b.call)(binding.node);
289
+ return (binding.declaration_kind === 'var' ? b.maybe_call : b.call)(node);
290
290
  }
291
291
 
292
292
  return node;