ripple 0.2.44 → 0.2.46

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
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is a TypeScript UI framework for the web",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.44",
6
+ "version": "0.2.46",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -38,6 +38,84 @@ function RipplePlugin(config) {
38
38
  return null;
39
39
  }
40
40
 
41
+ // Override getTokenFromCode to handle @ as an identifier prefix
42
+ getTokenFromCode(code) {
43
+ if (code === 64) { // '@' character
44
+ // Look ahead to see if this is followed by a valid identifier character
45
+ if (this.pos + 1 < this.input.length) {
46
+ const nextChar = this.input.charCodeAt(this.pos + 1);
47
+ // Check if the next character can start an identifier
48
+ if ((nextChar >= 65 && nextChar <= 90) || // A-Z
49
+ (nextChar >= 97 && nextChar <= 122) || // a-z
50
+ nextChar === 95 || nextChar === 36) { // _ or $
51
+
52
+ // Check if we're in an expression context
53
+ // In JSX expressions, inside parentheses, assignments, etc.
54
+ // we want to treat @ as an identifier prefix rather than decorator
55
+ const currentType = this.type;
56
+ const inExpression = this.exprAllowed ||
57
+ currentType === tt.braceL || // Inside { }
58
+ currentType === tt.parenL || // Inside ( )
59
+ currentType === tt.eq || // After =
60
+ currentType === tt.comma || // After ,
61
+ currentType === tt.colon || // After :
62
+ currentType === tt.question || // After ?
63
+ currentType === tt.logicalOR || // After ||
64
+ currentType === tt.logicalAND || // After &&
65
+ currentType === tt.dot || // After . (for member expressions like obj.@prop)
66
+ currentType === tt.questionDot; // After ?. (for optional chaining like obj?.@prop)
67
+
68
+ if (inExpression) {
69
+ return this.readAtIdentifier();
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return super.getTokenFromCode(code);
75
+ }
76
+
77
+ // Read an @ prefixed identifier
78
+ readAtIdentifier() {
79
+ const start = this.pos;
80
+ this.pos++; // skip '@'
81
+
82
+ // Read the identifier part manually
83
+ let word = '';
84
+ while (this.pos < this.input.length) {
85
+ const ch = this.input.charCodeAt(this.pos);
86
+ if ((ch >= 65 && ch <= 90) || // A-Z
87
+ (ch >= 97 && ch <= 122) || // a-z
88
+ (ch >= 48 && ch <= 57) || // 0-9
89
+ ch === 95 || ch === 36) { // _ or $
90
+ word += this.input[this.pos++];
91
+ } else {
92
+ break;
93
+ }
94
+ }
95
+
96
+ if (word === '') {
97
+ this.raise(start, 'Invalid @ identifier');
98
+ }
99
+
100
+ // Return the full identifier including @
101
+ return this.finishToken(tt.name, '@' + word);
102
+ }
103
+
104
+ // Override parseIdent to mark @ identifiers as tracked
105
+ parseIdent(liberal) {
106
+ const node = super.parseIdent(liberal);
107
+ if (node.name && node.name.startsWith('@')) {
108
+ node.name = node.name.slice(1); // Remove the '@' for internal use
109
+ node.tracked = true;
110
+ node.start++;
111
+ const prev_pos = this.pos;
112
+ this.pos = node.start;
113
+ node.loc.start = this.curPosition();
114
+ this.pos = prev_pos;
115
+ }
116
+ return node;
117
+ }
118
+
41
119
  parseExportDefaultDeclaration() {
42
120
  // Check if this is "export default component"
43
121
  if (this.value === 'component') {
@@ -159,6 +237,11 @@ function RipplePlugin(config) {
159
237
  return this.finishNode(node, 'SpreadAttribute');
160
238
  } else {
161
239
  const id = this.parseIdentNode();
240
+ node.tracked = false;
241
+ if (id.name.startsWith('@')) {
242
+ node.tracked = true;
243
+ id.name = id.name.slice(1);
244
+ }
162
245
  this.finishNode(id, 'Identifier');
163
246
  node.name = id;
164
247
  node.value = id;
@@ -79,6 +79,10 @@ function visit_function(node, context) {
79
79
  function_depth: context.state.function_depth + 1,
80
80
  expression: null,
81
81
  });
82
+
83
+ if (node.metadata.tracked) {
84
+ mark_as_tracked(context.path);
85
+ }
82
86
  }
83
87
 
84
88
  function mark_as_tracked(path) {
@@ -117,11 +121,35 @@ const visitors = {
117
121
 
118
122
  if (
119
123
  is_reference(node, /** @type {Node} */ (parent)) &&
120
- context.state.metadata?.tracking === false &&
121
- is_tracked_name(node.name) &&
124
+ (is_tracked_name(node) || node.tracked) &&
122
125
  binding?.node !== node
123
126
  ) {
124
- context.state.metadata.tracking = true;
127
+ mark_as_tracked(context.path);
128
+ if (context.state.metadata?.tracking === false) {
129
+ context.state.metadata.tracking = true;
130
+ }
131
+ }
132
+
133
+ if (
134
+ is_reference(node, /** @type {Node} */ (parent)) &&
135
+ node.tracked &&
136
+ binding?.node !== node
137
+ ) {
138
+ if (context.state.metadata?.tracking === false) {
139
+ context.state.metadata.tracking = true;
140
+ }
141
+ binding.transform = {
142
+ read_tracked: (node) => b.call('$.get_tracked', node),
143
+ assign_tracked: (node, value) => b.call('$.set', node, value, b.id('__block')),
144
+ update_tracked: (node) => {
145
+ return b.call(
146
+ node.prefix ? '$.update_pre' : '$.update',
147
+ node.argument,
148
+ b.id('__block'),
149
+ node.operator === '--' && b.literal(-1),
150
+ );
151
+ },
152
+ };
125
153
  }
126
154
 
127
155
  context.next();
@@ -247,7 +275,7 @@ const visitors = {
247
275
  if (!computed) {
248
276
  return node;
249
277
  }
250
- return b.call('$.set_property', node, computed, value, b.id('__block'));
278
+ return b.call('$.old_set_property', node, computed, value, b.id('__block'));
251
279
  },
252
280
  };
253
281
  break;
@@ -264,6 +292,15 @@ const visitors = {
264
292
  const has_tracked = paths.some(
265
293
  (path) => path.node.type === 'Identifier' && is_tracked_name(path.node.name),
266
294
  );
295
+ for (const path of paths) {
296
+ if (path.node.tracked) {
297
+ error(
298
+ 'Variables cannot be reactively referenced using @',
299
+ state.analysis.module.filename,
300
+ path.node,
301
+ );
302
+ }
303
+ }
267
304
 
268
305
  if (has_tracked) {
269
306
  const tmp = state.scope.generate('tmp');
@@ -290,7 +327,7 @@ const visitors = {
290
327
  } else if (metadata.tracking && !metadata.await) {
291
328
  if (is_tracked_name(path.node.name) && value.type === 'MemberExpression') {
292
329
  return b.call(
293
- '$.get_property',
330
+ '$.old_get_property',
294
331
  b.call('$.get_computed', value.object),
295
332
  value.property.type === 'Identifier'
296
333
  ? b.literal(value.property.name)
@@ -312,7 +349,7 @@ const visitors = {
312
349
 
313
350
  if (is_tracked_name(path.node.name) && value.type === 'MemberExpression') {
314
351
  return b.call(
315
- '$.get_property',
352
+ '$.old_get_property',
316
353
  value.object,
317
354
  value.property.type === 'Identifier'
318
355
  ? b.literal(value.property.name)
@@ -375,10 +412,10 @@ const visitors = {
375
412
  };
376
413
  } else {
377
414
  binding.transform = {
378
- read: (_) => b.call('$.get_property', b.id('__props'), b.literal(name)),
415
+ read: (_) => b.call('$.old_get_property', b.id('__props'), b.literal(name)),
379
416
  assign: (node, value) => {
380
417
  return b.call(
381
- '$.set_property',
418
+ '$.old_set_property',
382
419
  b.id('__props'),
383
420
  b.literal(name),
384
421
  value,
@@ -387,7 +424,7 @@ const visitors = {
387
424
  },
388
425
  update: (_) =>
389
426
  b.call(
390
- node.prefix ? '$.update_property_pre' : '$.update_property',
427
+ node.prefix ? '$.old_update_property_pre' : '$.old_update_property',
391
428
  b.id('__props'),
392
429
  b.literal(name),
393
430
  b.id('__block'),
@@ -508,6 +545,8 @@ const visitors = {
508
545
 
509
546
  attr.metadata.delegated = delegated_event;
510
547
  }
548
+ } else {
549
+ visit(attr.value, state);
511
550
  }
512
551
  }
513
552
  } else if (attr.type === 'AccessorAttribute') {
@@ -405,7 +405,9 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
405
405
  if (attribute.type === 'SpreadAttribute') return true;
406
406
 
407
407
  if (attribute.type !== 'Attribute') continue;
408
- if (attribute.name.name.toLowerCase() !== name.toLowerCase()) continue;
408
+
409
+ const lowerCaseName = name.toLowerCase();
410
+ if (![lowerCaseName, `$${lowerCaseName}`].includes(attribute.name.name.toLowerCase())) continue;
409
411
 
410
412
  if (expected_value === null) return true;
411
413
 
@@ -17,16 +17,25 @@ import {
17
17
  is_boolean_attribute,
18
18
  is_dom_property,
19
19
  is_ripple_import,
20
- is_declared_within_component,
20
+ is_declared_function_within_component,
21
21
  is_inside_call_expression,
22
22
  is_tracked_computed_property,
23
23
  is_value_static,
24
24
  is_void_element,
25
+ is_component_level_function,
25
26
  } from '../../utils.js';
26
27
  import is_reference from 'is-reference';
27
28
  import { extract_paths, object } from '../../../utils/ast.js';
28
29
  import { render_stylesheets } from './stylesheet.js';
29
30
 
31
+ function add_ripple_internal_import(context) {
32
+ if (!context.state.to_ts) {
33
+ if (!context.state.imports.has(`import * as $ from 'ripple/internal/client'`)) {
34
+ context.state.imports.add(`import * as $ from 'ripple/internal/client'`);
35
+ }
36
+ }
37
+ }
38
+
30
39
  function visit_function(node, context) {
31
40
  if (context.state.to_ts) {
32
41
  context.next(context.state);
@@ -54,13 +63,18 @@ function visit_function(node, context) {
54
63
  let body = context.visit(node.body, state);
55
64
 
56
65
  if (metadata?.tracked === true) {
66
+ const new_body = [];
67
+
68
+ if (!is_inside_component(context, true) && is_component_level_function(context)) {
69
+ add_ripple_internal_import(context);
70
+ new_body.push(b.var('__block', b.call('$.scope')));
71
+ }
72
+ new_body.push(...body.body);
73
+
57
74
  return /** @type {FunctionExpression} */ ({
58
75
  ...node,
59
76
  params: node.params.map((param) => context.visit(param, state)),
60
- body:
61
- body.type === 'BlockStatement'
62
- ? { ...body, body: [b.var('__block', b.call('$.scope')), ...body.body] }
63
- : body,
77
+ body: body.type === 'BlockStatement' ? { ...body, body: new_body } : body,
64
78
  });
65
79
  }
66
80
 
@@ -72,10 +86,17 @@ function build_getter(node, context) {
72
86
 
73
87
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
74
88
  const binding = state.scope.get(node.name);
89
+ const transform = binding?.transform;
75
90
 
76
91
  // don't transform the declaration itself
77
- if (node !== binding?.node && binding?.transform?.read) {
78
- return binding.transform.read(node, context.state?.metadata?.spread, context.visit);
92
+ if (node !== binding?.node) {
93
+ const read_fn = transform?.read || (node.tracked && transform?.read_tracked);
94
+
95
+ if (read_fn) {
96
+ add_ripple_internal_import(context);
97
+
98
+ return read_fn(node, context.state?.metadata?.spread, context.visit);
99
+ }
79
100
  }
80
101
  }
81
102
 
@@ -100,7 +121,7 @@ const visitors = {
100
121
  const binding = context.state.scope.get(node.name);
101
122
  if (
102
123
  context.state.metadata?.tracking === false &&
103
- is_tracked_name(node.name) &&
124
+ (is_tracked_name(node.name) || node.tracked) &&
104
125
  binding?.node !== node
105
126
  ) {
106
127
  context.state.metadata.tracking = true;
@@ -135,6 +156,18 @@ const visitors = {
135
156
  context.state.metadata.tracking = true;
136
157
  }
137
158
 
159
+ if (
160
+ !context.state.to_ts &&
161
+ callee.type === 'Identifier' &&
162
+ callee.name === 'tracked' &&
163
+ is_ripple_import(callee, context)
164
+ ) {
165
+ return {
166
+ ...node,
167
+ arguments: [...node.arguments.map((arg) => context.visit(arg)), b.id('__block')],
168
+ };
169
+ }
170
+
138
171
  if (
139
172
  !is_inside_component(context, true) ||
140
173
  context.state.to_ts ||
@@ -144,7 +177,7 @@ const visitors = {
144
177
  (is_ripple_import(callee, context) &&
145
178
  (callee.type !== 'Identifier' ||
146
179
  (callee.name !== 'array' && callee.name !== 'deferred'))) ||
147
- is_declared_within_component(callee, context)
180
+ is_declared_function_within_component(callee, context)
148
181
  ) {
149
182
  return context.next();
150
183
  }
@@ -199,8 +232,8 @@ const visitors = {
199
232
  callee.optional ? b.true : undefined,
200
233
  node.optional ? b.true : undefined,
201
234
  ...node.arguments.map((arg) => context.visit(arg)),
202
- )
203
- )
235
+ ),
236
+ ),
204
237
  );
205
238
  }
206
239
  }
@@ -267,6 +300,18 @@ const visitors = {
267
300
  MemberExpression(node, context) {
268
301
  const parent = context.path.at(-1);
269
302
 
303
+ if (node.property.type === 'Identifier' && node.property.tracked) {
304
+ add_ripple_internal_import(context);
305
+
306
+ context.state.metadata.tracking = true;
307
+ return b.call(
308
+ '$.get_property',
309
+ context.visit(node.object),
310
+ node.computed ? context.visit(node.property) : b.literal(node.property.name),
311
+ node.optional ? b.true : undefined,
312
+ );
313
+ }
314
+
270
315
  if (parent.type !== 'AssignmentExpression') {
271
316
  const object = node.object;
272
317
  const property = node.property;
@@ -286,14 +331,14 @@ const visitors = {
286
331
 
287
332
  if (tracked_name) {
288
333
  return b.call(
289
- '$.get_property',
334
+ '$.old_get_property',
290
335
  context.visit(object),
291
336
  property.type === 'Identifier' ? b.literal(property.name) : property,
292
337
  node.optional ? b.true : undefined,
293
338
  );
294
339
  } else {
295
340
  return b.call(
296
- '$.get_property',
341
+ '$.old_get_property',
297
342
  context.visit(object),
298
343
  context.visit(property),
299
344
  node.optional ? b.true : undefined,
@@ -508,9 +553,10 @@ const visitors = {
508
553
 
509
554
  if (is_spreading) {
510
555
  // For spread attributes, store just the actual value, not the full attribute string
511
- const actual_value = is_boolean_attribute(name) && value === true
512
- ? b.literal(true)
513
- : b.literal(value === true ? '' : value);
556
+ const actual_value =
557
+ is_boolean_attribute(name) && value === true
558
+ ? b.literal(true)
559
+ : b.literal(value === true ? '' : value);
514
560
  spread_attributes.push(b.prop('init', b.literal(name), actual_value));
515
561
  } else {
516
562
  state.template.push(attr_value);
@@ -542,9 +588,10 @@ const visitors = {
542
588
 
543
589
  if (name === 'value' || name === '$value') {
544
590
  const id = state.flush_node();
545
- const expression = visit(attr.value, state);
591
+ const metadata = { tracking: false, await: false };
592
+ const expression = visit(attr.value, { ...state, metadata });
546
593
 
547
- if (name === '$value') {
594
+ if (name === '$value' || metadata.tracking) {
548
595
  local_updates.push(b.stmt(b.call('$.set_value', id, expression)));
549
596
  } else {
550
597
  state.init.push(b.stmt(b.call('$.set_value', id, expression)));
@@ -555,9 +602,10 @@ const visitors = {
555
602
 
556
603
  if (name === 'checked' || name === '$checked') {
557
604
  const id = state.flush_node();
558
- const expression = visit(attr.value, state);
605
+ const metadata = { tracking: false, await: false };
606
+ const expression = visit(attr.value, { ...state, metadata });
559
607
 
560
- if (name === '$checked') {
608
+ if (name === '$checked' || metadata.tracking) {
561
609
  local_updates.push(b.stmt(b.call('$.set_checked', id, expression)));
562
610
  } else {
563
611
  state.init.push(b.stmt(b.call('$.set_checked', id, expression)));
@@ -567,9 +615,10 @@ const visitors = {
567
615
 
568
616
  if (name === 'selected' || name === '$selected') {
569
617
  const id = state.flush_node();
570
- const expression = visit(attr.value, state);
618
+ const metadata = { tracking: false, await: false };
619
+ const expression = visit(attr.value, { ...state, metadata });
571
620
 
572
- if (name === '$selected') {
621
+ if (name === '$selected' || metadata.tracking) {
573
622
  local_updates.push(b.stmt(b.call('$.set_selected', id, expression)));
574
623
  } else {
575
624
  state.init.push(b.stmt(b.call('$.set_selected', id, expression)));
@@ -607,7 +656,7 @@ const visitors = {
607
656
  delegated_assignment = b.array(args);
608
657
  } else if (
609
658
  handler.type === 'Identifier' &&
610
- is_declared_within_component(handler, context)
659
+ is_declared_function_within_component(handler, context)
611
660
  ) {
612
661
  delegated_assignment = handler;
613
662
  } else {
@@ -639,11 +688,12 @@ const visitors = {
639
688
  continue;
640
689
  }
641
690
 
691
+ const metadata = { tracking: false, await: false };
692
+ const expression = visit(attr.value, { ...state, metadata });
642
693
  // All other attributes
643
- if (is_tracked_name(name)) {
644
- const attribute = name.slice(1);
694
+ if (is_tracked_name(name) || metadata.tracking) {
695
+ const attribute = is_tracked_name(name) ? name.slice(1) : name;
645
696
  const id = state.flush_node();
646
- const expression = visit(attr.value, state);
647
697
 
648
698
  if (is_dom_property(attribute)) {
649
699
  local_updates.push(b.stmt(b.assignment('=', b.member(id, attribute), expression)));
@@ -654,7 +704,6 @@ const visitors = {
654
704
  }
655
705
  } else {
656
706
  const id = state.flush_node();
657
- const expression = visit(attr.value, state);
658
707
 
659
708
  if (is_dom_property(name)) {
660
709
  state.init.push(b.stmt(b.assignment('=', b.member(id, name), expression)));
@@ -682,13 +731,14 @@ const visitors = {
682
731
  handle_static_attr(class_attribute.name.name, value);
683
732
  } else {
684
733
  const id = state.flush_node();
685
- let expression = visit(class_attribute.value, state);
734
+ const metadata = { tracking: false, await: false };
735
+ let expression = visit(class_attribute.value, { ...state, metadata });
686
736
 
687
737
  if (node.metadata.scoped && state.component.css) {
688
738
  expression = b.binary('+', b.literal(state.component.css.hash + ' '), expression);
689
739
  }
690
740
 
691
- if (class_attribute.name.name === '$class') {
741
+ if (class_attribute.name.name === '$class' || metadata.tracking) {
692
742
  local_updates.push(b.stmt(b.call('$.set_class', id, expression)));
693
743
  } else {
694
744
  state.init.push(b.stmt(b.call('$.set_class', id, expression)));
@@ -880,9 +930,7 @@ const visitors = {
880
930
 
881
931
  Fragment(node, context) {
882
932
  if (!context.state.to_ts) {
883
- if (!context.state.imports.has(`import * as $ from 'ripple/internal/client'`)) {
884
- context.state.imports.add(`import * as $ from 'ripple/internal/client'`);
885
- }
933
+ add_ripple_internal_import(context);
886
934
  }
887
935
 
888
936
  const metadata = { await: false };
@@ -906,11 +954,7 @@ const visitors = {
906
954
  Component(node, context) {
907
955
  let prop_statements;
908
956
 
909
- if (!context.state.to_ts) {
910
- if (!context.state.imports.has(`import * as $ from 'ripple/internal/client'`)) {
911
- context.state.imports.add(`import * as $ from 'ripple/internal/client'`);
912
- }
913
- }
957
+ add_ripple_internal_import(context);
914
958
 
915
959
  const metadata = { await: false };
916
960
 
@@ -1000,12 +1044,43 @@ const visitors = {
1000
1044
 
1001
1045
  const left = node.left;
1002
1046
 
1047
+ if (
1048
+ left.type === 'MemberExpression' &&
1049
+ left.property.type === 'Identifier' &&
1050
+ left.property.tracked
1051
+ ) {
1052
+ add_ripple_internal_import(context);
1053
+ const operator = node.operator;
1054
+ const right = node.right;
1055
+
1056
+ if (operator !== '=') {
1057
+ context.state.metadata.tracking = true;
1058
+ }
1059
+
1060
+ return b.call(
1061
+ '$.set_property',
1062
+ context.visit(left.object),
1063
+ left.computed ? context.visit(left.property) : b.literal(left.property.name),
1064
+ operator === '='
1065
+ ? context.visit(right)
1066
+ : b.binary(
1067
+ operator === '+=' ? '+' : operator === '-=' ? '-' : operator === '*=' ? '*' : '/',
1068
+ /** @type {Pattern} */ (context.visit(left)),
1069
+ /** @type {Expression} */ (context.visit(right)),
1070
+ ),
1071
+ b.id('__block'),
1072
+ );
1073
+ }
1074
+
1003
1075
  if (left.type === 'MemberExpression') {
1004
1076
  // need to capture setting length of array to throw a runtime error
1005
- if (left.property.type === 'Identifier' && (is_tracked_name(left.property.name) || left.property.name === 'length')) {
1077
+ if (
1078
+ left.property.type === 'Identifier' &&
1079
+ (is_tracked_name(left.property.name) || left.property.name === 'length')
1080
+ ) {
1006
1081
  if (left.property.name !== '$length') {
1007
1082
  return b.call(
1008
- '$.set_property',
1083
+ '$.old_set_property',
1009
1084
  context.visit(left.object),
1010
1085
  left.computed ? context.visit(left.property) : b.literal(left.property.name),
1011
1086
  visit_assignment_expression(node, context, build_assignment) ?? context.next(),
@@ -1038,6 +1113,23 @@ const visitors = {
1038
1113
  }
1039
1114
  const argument = node.argument;
1040
1115
 
1116
+ if (
1117
+ argument.type === 'MemberExpression' &&
1118
+ argument.property.type === 'Identifier' &&
1119
+ argument.property.tracked
1120
+ ) {
1121
+ add_ripple_internal_import(context);
1122
+ context.state.metadata.tracking = true;
1123
+
1124
+ return b.call(
1125
+ node.prefix ? '$.update_pre_property' : '$.update_property',
1126
+ context.visit(argument.object),
1127
+ argument.computed ? context.visit(argument.property) : b.literal(argument.property.name),
1128
+ b.id('__block'),
1129
+ node.operator === '--' ? b.literal(-1) : undefined,
1130
+ );
1131
+ }
1132
+
1041
1133
  if (
1042
1134
  argument.type === 'MemberExpression' &&
1043
1135
  ((argument.property.type === 'Identifier' && is_tracked_name(argument.property.name)) ||
@@ -1045,7 +1137,7 @@ const visitors = {
1045
1137
  is_tracked_computed_property(argument.object, argument.property, context)))
1046
1138
  ) {
1047
1139
  return b.call(
1048
- node.prefix ? '$.update_pre_property' : '$.update_property',
1140
+ node.prefix ? '$.old_update_pre_property' : '$.old_update_property',
1049
1141
  context.visit(argument.object),
1050
1142
  argument.computed ? context.visit(argument.property) : b.literal(argument.property.name),
1051
1143
  b.id('__block'),
@@ -1058,8 +1150,9 @@ const visitors = {
1058
1150
  const transformers = left && binding?.transform;
1059
1151
 
1060
1152
  if (left === argument) {
1061
- if (transformers?.update) {
1062
- return transformers.update(node);
1153
+ const update_fn = transformers?.update || transformers?.update_tracked;
1154
+ if (update_fn) {
1155
+ return update_fn(node);
1063
1156
  }
1064
1157
  }
1065
1158
 
@@ -1303,7 +1396,7 @@ const visitors = {
1303
1396
  return b.literal(node.quasis[0].value.cooked);
1304
1397
  }
1305
1398
 
1306
- const expressions = node.expressions.map(expr => context.visit(expr));
1399
+ const expressions = node.expressions.map((expr) => context.visit(expr));
1307
1400
  return b.template(node.quasis, expressions);
1308
1401
  },
1309
1402