@tsrx/core 0.0.26 → 0.0.28

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": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.26",
6
+ "version": "0.0.28",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -27,10 +27,11 @@
27
27
  "./types/acorn": {
28
28
  "types": "./types/acorn.d.ts"
29
29
  },
30
- "./runtime/merge-refs": {
31
- "types": "./types/runtime/merge-refs.d.ts",
32
- "default": "./src/runtime/merge-refs.js"
30
+ "./runtime/ref": {
31
+ "types": "./types/runtime/ref.d.ts",
32
+ "default": "./src/runtime/ref.js"
33
33
  },
34
+ "./runtime/*": "./src/runtime/*.js",
34
35
  "./test-harness/source-mappings": "./tests/shared/source-mappings.js",
35
36
  "./test-harness/compile": "./tests/shared/compile.js"
36
37
  },
@@ -19,6 +19,10 @@ export const COMPONENT_WHILE_STATEMENT_ERROR =
19
19
  'While loops are not supported in components. Move the while loop into a function.';
20
20
  export const COMPONENT_DO_WHILE_STATEMENT_ERROR =
21
21
  'Do...while loops are not supported in components. Move the do...while loop into a function.';
22
+ export const CLASS_COMPONENT_AS_NON_ARROW_PROPERTY_ERROR =
23
+ 'Components declared inside a class must be defined as an arrow function class property (e.g. `Foo = component() => { ... }`). Non-arrow component property values are not allowed.';
24
+ export const COMPONENT_MULTIPLE_PARAMS_ERROR =
25
+ 'Components accept a single props parameter. Move additional inputs into the props object instead.';
22
26
 
23
27
  const invalid_nestings = {
24
28
  // <p> cannot contain block-level elements
@@ -247,6 +251,61 @@ export function validate_component_unsupported_loop_statement(node, filename, er
247
251
  error(message, filename ?? null, node, errors, comments);
248
252
  }
249
253
 
254
+ /**
255
+ * Validates that a component declares at most a single (props) parameter.
256
+ * Components have one slot for props; additional positional parameters are
257
+ * silently dropped or naively passed through depending on the target, so
258
+ * reject them at analysis time. Reports one error per extra parameter so
259
+ * every offending input gets its own TS diagnostic squiggle. In throwing
260
+ * mode the first call raises and aborts before the loop continues.
261
+ *
262
+ * @param {AST.Component} component
263
+ * @param {string | null | undefined} filename
264
+ * @param {CompileError[]} [errors]
265
+ * @param {AST.CommentWithLocation[]} [comments]
266
+ */
267
+ export function validate_component_params(component, filename, errors, comments) {
268
+ const params = /** @type {AST.Pattern[] | undefined} */ (component.params);
269
+ if (!params || params.length <= 1) {
270
+ return;
271
+ }
272
+
273
+ for (let i = 1; i < params.length; i++) {
274
+ error(COMPONENT_MULTIPLE_PARAMS_ERROR, filename ?? null, params[i], errors, comments);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Validates that components declared at the top level of a class body use the
280
+ * only allowed form: an arrow function class property (regular or static).
281
+ * Reports an error for non-arrow component property values such as
282
+ * `Foo = component() { ... }`. The method form (`component foo() {}` inside
283
+ * a class body) is rejected at parse time and never reaches this check.
284
+ *
285
+ * @param {AST.ClassBody} class_body
286
+ * @param {string | null | undefined} filename
287
+ * @param {CompileError[]} [errors]
288
+ * @param {AST.CommentWithLocation[]} [comments]
289
+ */
290
+ export function validate_class_component_declarations(class_body, filename, errors, comments) {
291
+ for (const member of class_body.body) {
292
+ if (member.type !== 'PropertyDefinition') {
293
+ continue;
294
+ }
295
+
296
+ const value = /** @type {any} */ (member).value;
297
+ if (value && value.type === 'Component' && !value.metadata?.arrow) {
298
+ error(
299
+ CLASS_COMPONENT_AS_NON_ARROW_PROPERTY_ERROR,
300
+ filename ?? null,
301
+ member,
302
+ errors,
303
+ comments,
304
+ );
305
+ }
306
+ }
307
+ }
308
+
250
309
  /**
251
310
  * @param {AST.Element} element
252
311
  * @param {AnalysisContext} context
package/src/index.js CHANGED
@@ -141,8 +141,14 @@ export { escape, escape_script as escapeScript } from './utils/escaping.js';
141
141
 
142
142
  // Transform
143
143
  export {
144
+ add_jsx_setup_declaration as addJsxSetupDeclaration,
144
145
  createJsxTransform,
146
+ CREATE_REF_PROP_INTERNAL_NAME,
147
+ extract_jsx_setup_declarations as extractJsxSetupDeclarations,
148
+ is_ref_prop_expression as isRefPropExpression,
149
+ MERGE_REFS_INTERNAL_NAME,
145
150
  merge_duplicate_refs as mergeDuplicateRefs,
151
+ NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
146
152
  rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
147
153
  to_jsx_attribute as toJsxAttribute,
148
154
  validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
@@ -164,12 +170,16 @@ export {
164
170
  flatten_switch_consequent,
165
171
  get_for_of_iteration_params,
166
172
  identifier_to_jsx_name,
173
+ is_bare_render_expression,
167
174
  is_dynamic_element_id,
168
175
  is_jsx_child,
169
176
  set_loc,
170
177
  to_text_expression,
171
178
  } from './transform/jsx/ast-builders.js';
172
- export { render_stylesheets as renderStylesheets } from './transform/stylesheet.js';
179
+ export {
180
+ render_stylesheets as renderStylesheets,
181
+ render_css_result as renderCssResult,
182
+ } from './transform/stylesheet.js';
173
183
  export {
174
184
  prepare_stylesheet_for_render as prepareStylesheetForRender,
175
185
  is_style_element as isStyleElement,
@@ -213,17 +223,21 @@ export {
213
223
  // Analyze
214
224
  export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
215
225
  export {
226
+ CLASS_COMPONENT_AS_NON_ARROW_PROPERTY_ERROR,
216
227
  COMPONENT_DO_WHILE_STATEMENT_ERROR,
217
228
  COMPONENT_FOR_IN_STATEMENT_ERROR,
218
229
  COMPONENT_FOR_STATEMENT_ERROR,
219
230
  COMPONENT_LOOP_BREAK_ERROR,
220
231
  COMPONENT_LOOP_RETURN_ERROR,
232
+ COMPONENT_MULTIPLE_PARAMS_ERROR,
221
233
  COMPONENT_RETURN_VALUE_ERROR,
222
234
  COMPONENT_WHILE_STATEMENT_ERROR,
223
235
  get_return_keyword_node as getReturnKeywordNode,
224
236
  get_statement_keyword_node as getStatementKeywordNode,
237
+ validate_class_component_declarations as validateClassComponentDeclarations,
225
238
  validate_component_loop_break_statement as validateComponentLoopBreakStatement,
226
239
  validate_component_loop_return_statement as validateComponentLoopReturnStatement,
240
+ validate_component_params as validateComponentParams,
227
241
  validate_component_return_statement as validateComponentReturnStatement,
228
242
  validate_component_unsupported_loop_statement as validateComponentUnsupportedLoopStatement,
229
243
  validate_nesting as validateNesting,
package/src/plugin.js CHANGED
@@ -559,65 +559,6 @@ export function TSRXPlugin(config) {
559
559
  return super.parseProperty(isPattern, refDestructuringErrors);
560
560
  }
561
561
 
562
- /**
563
- * Override parseClassElement to support component methods in classes.
564
- * Handles syntax like `class Foo { component something() { <div /> } }`
565
- * Also supports computed names: `class Foo { component ['something']() { <div /> } }`
566
- * @type {Parse.Parser['parseClassElement']}
567
- */
568
- parseClassElement(constructorAllowsSuper) {
569
- // Check if this is a component method: component name( ... ) { ... }
570
- if (this.type === tt.name && this.value === 'component') {
571
- // Look ahead to see if this is "component identifier(",
572
- // "component identifier<", "component [", or "component 'string'"
573
- const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
574
- if (lookahead) {
575
- // This is a component method definition
576
- const node = /** @type {AST.MethodDefinition} */ (this.startNode());
577
- const isComputed = lookahead[0].trim().startsWith('[');
578
- const isStringLiteral = /^['"]/.test(lookahead[0].trim());
579
-
580
- if (isComputed) {
581
- // For computed names, consume 'component'
582
- // parse the key, then parse component without name
583
- this.next(); // consume 'component'
584
- this.next(); // consume '['
585
- node.key = this.parseExpression();
586
- this.expect(tt.bracketR);
587
- node.computed = true;
588
-
589
- // Parse component without name (skipName: true)
590
- const component_node = this.parseComponent({ skipName: true });
591
- /** @type {AST.TSRXMethodDefinition} */ (node).value = component_node;
592
- } else if (isStringLiteral) {
593
- // For string literal names, consume 'component'
594
- // parse the string key, then parse component without name
595
- this.next(); // consume 'component'
596
- node.key = /** @type {AST.Literal} */ (this.parseExprAtom());
597
- node.computed = false;
598
-
599
- // Parse component without name (skipName: true)
600
- const component_node = this.parseComponent({ skipName: true });
601
- /** @type {AST.TSRXMethodDefinition} */ (node).value = component_node;
602
- } else {
603
- // Use parseComponent which handles consuming 'component', parsing name, params, and body
604
- const component_node = this.parseComponent({ requireName: true });
605
-
606
- node.key = /** @type {AST.Identifier} */ (component_node.id);
607
- /** @type {AST.TSRXMethodDefinition} */ (node).value = component_node;
608
- node.computed = false;
609
- }
610
-
611
- node.static = false;
612
- node.kind = 'method';
613
-
614
- return this.finishNode(node, 'MethodDefinition');
615
- }
616
- }
617
-
618
- return super.parseClassElement(constructorAllowsSuper);
619
- }
620
-
621
562
  /**
622
563
  * Override parsePropertyValue to support TypeScript generic methods in object literals.
623
564
  * By default, acorn-typescript doesn't handle `{ method<T>() {} }` syntax.
@@ -1371,15 +1312,15 @@ export function TSRXPlugin(config) {
1371
1312
  */
1372
1313
  checkUnreserved(ref) {
1373
1314
  if (ref.name === 'component') {
1374
- // Allow 'component' when it's followed by an identifier and '(' or '<' (component method in object literal or class)
1375
- // e.g., { component something() { ... } } or class Foo { component something<T>() { ... } }
1315
+ // Allow 'component' when it's followed by an identifier and '(' or '<' (component method in object literal)
1316
+ // e.g., { component something() { ... } }
1376
1317
  // Also allow computed names: { component ['name']() { ... } }
1377
1318
  // Also allow string literal names: { component 'name'() { ... } }
1378
1319
  const nextChars = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
1379
1320
  if (!nextChars) {
1380
1321
  this.raise(
1381
1322
  ref.start,
1382
- '"component" is a Ripple keyword and cannot be used as an identifier',
1323
+ '"component" is a TSRX keyword and cannot be used as an identifier',
1383
1324
  );
1384
1325
  }
1385
1326
  }
@@ -1404,6 +1345,21 @@ export function TSRXPlugin(config) {
1404
1345
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1405
1346
  this.next();
1406
1347
 
1348
+ if (this.type === tt.name && this.value === 'ref') {
1349
+ const ref_node = /** @type {AST.RefExpression} */ (this.startNode());
1350
+ this.next();
1351
+ if (this.type === tt.braceR) {
1352
+ this.raise(
1353
+ this.start,
1354
+ '"ref" is a TSRX keyword and must be used in the form {ref item}',
1355
+ );
1356
+ }
1357
+ ref_node.argument = this.parseMaybeAssign();
1358
+ node.expression = /** @type {any} */ (this.finishNode(ref_node, 'RefExpression'));
1359
+ this.expect(tt.braceR);
1360
+ return this.finishNode(node, 'JSXExpressionContainer');
1361
+ }
1362
+
1407
1363
  if (this.type === tt.name && this.value === 'html') {
1408
1364
  node.html = true;
1409
1365
  this.next();
@@ -0,0 +1,57 @@
1
+ /** @type {typeof Object.getOwnPropertyDescriptor} */
2
+ export var get_descriptor = Object.getOwnPropertyDescriptor;
3
+ /** @type {typeof Object.getOwnPropertyDescriptors} */
4
+ export var get_descriptors = Object.getOwnPropertyDescriptors;
5
+ /** @type {typeof Array.from} */
6
+ export var array_from = Array.from;
7
+ /** @type {typeof Array.isArray} */
8
+ export var is_array = Array.isArray;
9
+ /** @type {typeof Object.defineProperty} */
10
+ export var define_property = Object.defineProperty;
11
+ /** @type {typeof Object.getPrototypeOf} */
12
+ export var get_prototype_of = Object.getPrototypeOf;
13
+ /** @type {typeof Object.values} */
14
+ export var object_values = Object.values;
15
+ /** @type {typeof Object.entries} */
16
+ export var object_entries = Object.entries;
17
+ /** @type {typeof Object.keys} */
18
+ export var object_keys = Object.keys;
19
+ /** @type {typeof Object.getOwnPropertySymbols} */
20
+ export var get_own_property_symbols = Object.getOwnPropertySymbols;
21
+ /** @type {typeof structuredClone} */
22
+ export var structured_clone = structuredClone;
23
+ /** @type {typeof Object.prototype} */
24
+ export var object_prototype = Object.prototype;
25
+ /** @type {typeof Array.prototype} */
26
+ export var array_prototype = Array.prototype;
27
+ /** @type {typeof Object.prototype.hasOwnProperty} */
28
+ export var has_own_property = object_prototype.hasOwnProperty;
29
+
30
+ /**
31
+ * @param {object} value
32
+ * @param {PropertyKey} key
33
+ * @returns {boolean}
34
+ */
35
+ export function has_prototype_accessor(value, key) {
36
+ var proto = get_prototype_of(value);
37
+ while (proto != null) {
38
+ var descriptor = get_descriptor(proto, key);
39
+ if (descriptor !== undefined) {
40
+ return typeof descriptor.get === 'function' || typeof descriptor.set === 'function';
41
+ }
42
+ proto = get_prototype_of(proto);
43
+ }
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * Slice helper for arrays and array-like values.
49
+ * @param {ArrayLike<any>} array_like
50
+ * @param {...number} args
51
+ * @returns {any[]}
52
+ */
53
+ export function array_slice(array_like, ...args) {
54
+ return is_array(array_like)
55
+ ? array_like.slice(...args)
56
+ : array_prototype.slice.call(array_like, ...args);
57
+ }
@@ -0,0 +1,250 @@
1
+ import {
2
+ has_own_property,
3
+ get_descriptor,
4
+ has_prototype_accessor,
5
+ } from '@tsrx/core/runtime/language-helpers';
6
+
7
+ const REF_VALUE = Symbol();
8
+
9
+ /**
10
+ * Merge multiple refs (function refs and ref objects) into a single
11
+ * callback ref. Used by React, Preact, and Vue targets when an element has
12
+ * more than one `ref` attribute.
13
+ * This is a public method and also used by the compiler to unite any refs with
14
+ * any of the supported syntaxes. It does not process spreads, that is delegated to
15
+ * `normalize_spread_props`.
16
+ *
17
+ * @param {...((node: any) => void | (() => void)) | { current: any } | { value: any } | null | undefined} refs
18
+ * @returns {(node: any) => (() => void)}
19
+ */
20
+ export function mergeRefs(...refs) {
21
+ return (node) => {
22
+ /** @type {Array<() => void>} */
23
+ const cleanups = [];
24
+ for (const ref of refs) {
25
+ if (ref == null) continue;
26
+ if (typeof ref === 'function') {
27
+ const result = ref(node);
28
+ if (typeof result === 'function') {
29
+ cleanups.push(result);
30
+ } else {
31
+ cleanups.push(() => ref(null));
32
+ }
33
+ } else if (is_ref_object(ref, 'current')) {
34
+ /** @type {{ current: any }} */ (ref).current = node;
35
+ cleanups.push(() => {
36
+ /** @type {{ current: any }} */ (ref).current = null;
37
+ });
38
+ } else if (is_ref_object(ref, 'value')) {
39
+ /** @type {{ value: any }} */ (ref).value = node;
40
+ cleanups.push(() => {
41
+ /** @type {{ value: any }} */ (ref).value = null;
42
+ });
43
+ }
44
+ }
45
+ return () => {
46
+ for (const cleanup of cleanups) cleanup();
47
+ };
48
+ };
49
+ }
50
+
51
+ export { is_ref_prop as isRefProp };
52
+
53
+ /**
54
+ * @param {unknown} value
55
+ * @returns {boolean}
56
+ */
57
+ function is_ref_prop(value) {
58
+ return typeof value === 'function' && REF_VALUE in value;
59
+ }
60
+
61
+ /**
62
+ * @param {any} ref_value
63
+ * @param {any} node
64
+ * @param {(value: any) => void} [set_ref_value]
65
+ * @returns {void | (() => void)}
66
+ */
67
+ export function apply_ref_value(ref_value, node, set_ref_value) {
68
+ if (typeof ref_value === 'function') {
69
+ return ref_value(node);
70
+ }
71
+
72
+ if (ref_value && typeof ref_value === 'object') {
73
+ if (is_ref_object(ref_value, 'current')) {
74
+ ref_value.current = node;
75
+ return () => {
76
+ ref_value.current = null;
77
+ };
78
+ }
79
+
80
+ if (is_ref_object(ref_value, 'value')) {
81
+ ref_value.value = node;
82
+ return () => {
83
+ ref_value.value = null;
84
+ };
85
+ }
86
+ }
87
+
88
+ if (set_ref_value !== undefined) {
89
+ set_ref_value(node);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @param {() => any} get_ref_value
95
+ * @param {(value: any) => void} [set_ref_value]
96
+ * @returns {(node: any) => void | (() => void)}
97
+ */
98
+ export function create_ref_prop(get_ref_value, set_ref_value) {
99
+ /**
100
+ * @param {any} node
101
+ * @returns {void | (() => void)}
102
+ */
103
+ function ref_prop_callback(node) {
104
+ const ref_value = get_ref_value();
105
+ const cleanup = apply_ref_value(ref_value, node, set_ref_value);
106
+ if (typeof cleanup === 'function' || node === null) {
107
+ return cleanup;
108
+ }
109
+ return () => {
110
+ apply_ref_value(ref_value, null, set_ref_value);
111
+ };
112
+ }
113
+
114
+ Object.defineProperty(ref_prop_callback, REF_VALUE, {
115
+ value: 'ref_value',
116
+ enumerable: false,
117
+ });
118
+
119
+ return ref_prop_callback;
120
+ }
121
+
122
+ /**
123
+ * @param {...any} refs
124
+ * @returns {any}
125
+ */
126
+ export function merge_ref_props(...refs) {
127
+ const filtered = refs.filter((ref) => ref != null);
128
+
129
+ if (filtered.length === 0) {
130
+ return undefined;
131
+ }
132
+
133
+ if (filtered.length === 1) {
134
+ return filtered[0];
135
+ }
136
+
137
+ /**
138
+ * @param {any} node
139
+ * @returns {void | (() => void)}
140
+ */
141
+ function merged_ref_prop(node) {
142
+ /** @type {Array<() => void>} */
143
+ const cleanups = [];
144
+
145
+ for (const ref of filtered) {
146
+ const cleanup = apply_ref_value(ref, node);
147
+ if (typeof cleanup === 'function') {
148
+ cleanups.push(cleanup);
149
+ } else if (typeof ref === 'function' && node !== null) {
150
+ cleanups.push(() => ref(null));
151
+ }
152
+ }
153
+
154
+ return () => {
155
+ for (const cleanup of cleanups) {
156
+ cleanup();
157
+ }
158
+ };
159
+ }
160
+
161
+ return merged_ref_prop;
162
+ }
163
+
164
+ /**
165
+ * @param {Record<string | symbol, any> | null | undefined} props
166
+ * @param {...any} outer_refs
167
+ * @returns {Record<string | symbol, any> | null | undefined}
168
+ */
169
+ export function normalize_spread_props(props, ...outer_refs) {
170
+ if (props == null) {
171
+ return props;
172
+ }
173
+
174
+ /** @type {any[]} */
175
+ const refs = [];
176
+ /** @type {Record<string | symbol, any>} */
177
+ let next = {};
178
+ let changed = false;
179
+ let existing_ref;
180
+
181
+ for (const key of Reflect.ownKeys(props)) {
182
+ const descriptor = get_descriptor(props, key);
183
+ if (!descriptor?.enumerable) {
184
+ continue;
185
+ }
186
+
187
+ const value = /** @type {any} */ (props)[key];
188
+
189
+ if (key === 'ref') {
190
+ if (is_ref_prop(value)) {
191
+ refs.push(value);
192
+ changed = true;
193
+ } else {
194
+ existing_ref = value;
195
+ }
196
+ continue;
197
+ }
198
+
199
+ if (is_ref_prop(value)) {
200
+ refs.push(value);
201
+ changed = true;
202
+ continue;
203
+ }
204
+
205
+ next[key] = value;
206
+ }
207
+
208
+ if (!changed && outer_refs.length === 0) {
209
+ return props;
210
+ }
211
+
212
+ const merged_ref = merge_ref_props(existing_ref, ...refs, ...outer_refs);
213
+ if (merged_ref !== undefined) {
214
+ next.ref = merged_ref;
215
+ }
216
+
217
+ return next;
218
+ }
219
+
220
+ /**
221
+ * @param {object} value
222
+ * @param {'current' | 'value'} key
223
+ * @returns {boolean}
224
+ */
225
+ function is_ref_object(value, key) {
226
+ if (is_dom_node(value)) {
227
+ return false;
228
+ }
229
+ if (key === 'value' && '__v_isRef' in value) {
230
+ return true;
231
+ }
232
+ if (has_own_property.call(value, key)) {
233
+ return true;
234
+ }
235
+ return key === 'value' && has_prototype_accessor(value, 'value');
236
+ }
237
+
238
+ /**
239
+ * @param {object} value
240
+ * @returns {boolean}
241
+ */
242
+ function is_dom_node(value) {
243
+ return (
244
+ (typeof Node !== 'undefined' && value instanceof Node) ||
245
+ ('nodeType' in value &&
246
+ typeof (/** @type {{ nodeType?: unknown }} */ (value).nodeType) === 'number' &&
247
+ 'nodeName' in value &&
248
+ typeof (/** @type {{ nodeName?: unknown }} */ (value).nodeName) === 'string')
249
+ );
250
+ }
@@ -143,7 +143,7 @@ export function identifier_to_jsx_name(id) {
143
143
  /** @type {any} */ ({
144
144
  type: 'JSXIdentifier',
145
145
  name: id.name,
146
- metadata: { path: [], is_component: /^[A-Z]/.test(id.name) },
146
+ metadata: { ...(id.metadata || {}), path: [], is_component: /^[A-Z]/.test(id.name) },
147
147
  }),
148
148
  id,
149
149
  );
@@ -187,6 +187,57 @@ export function is_jsx_child(node) {
187
187
  );
188
188
  }
189
189
 
190
+ /**
191
+ * The parser represents `<>{expr}</>` / `<tsx>{expr}</tsx>` as a Tsx node,
192
+ * and expression-position lowering unwraps that to the inner expression.
193
+ * When such a node appears directly in a component or statement render body,
194
+ * the unwrapped expression is still render output rather than an executable
195
+ * statement.
196
+ *
197
+ * @param {any} node
198
+ * @returns {boolean}
199
+ */
200
+ export function is_bare_render_expression(node) {
201
+ if (!node || typeof node !== 'object') {
202
+ return false;
203
+ }
204
+
205
+ switch (node.type) {
206
+ case 'ArrayExpression':
207
+ case 'ArrowFunctionExpression':
208
+ case 'AssignmentExpression':
209
+ case 'AwaitExpression':
210
+ case 'BinaryExpression':
211
+ case 'CallExpression':
212
+ case 'ChainExpression':
213
+ case 'ClassExpression':
214
+ case 'ConditionalExpression':
215
+ case 'FunctionExpression':
216
+ case 'Identifier':
217
+ case 'ImportExpression':
218
+ case 'Literal':
219
+ case 'LogicalExpression':
220
+ case 'MemberExpression':
221
+ case 'MetaProperty':
222
+ case 'NewExpression':
223
+ case 'ObjectExpression':
224
+ case 'ParenthesizedExpression':
225
+ case 'SequenceExpression':
226
+ case 'TaggedTemplateExpression':
227
+ case 'TemplateLiteral':
228
+ case 'ThisExpression':
229
+ case 'TSAsExpression':
230
+ case 'TSSatisfiesExpression':
231
+ case 'TSNonNullExpression':
232
+ case 'UnaryExpression':
233
+ case 'UpdateExpression':
234
+ case 'YieldExpression':
235
+ return true;
236
+ default:
237
+ return false;
238
+ }
239
+ }
240
+
190
241
  /**
191
242
  * A dynamic element id is one whose identifier is `tracked` — i.e. it was
192
243
  * introduced by reactive destructuring so its value can change at runtime.
@@ -325,26 +376,26 @@ export function to_text_expression(expression, source_node = expression) {
325
376
  }
326
377
 
327
378
  /**
328
- * Deep-clone an AST subtree. `loc` / `start` / `end` are shallow-shared by
329
- * reference rather than recursed into — `loc` objects can contain back-refs
330
- * to sub-objects that would blow the stack with a naive deep clone, and
331
- * every other traversal in the targets treats these positional keys as
332
- * shared.
379
+ * Deep-clone an AST subtree.
333
380
  *
334
381
  * @param {any} node
382
+ * @param {boolean} with_locations
335
383
  * @returns {any}
336
384
  */
337
- export function clone_expression_node(node) {
385
+ export function clone_expression_node(node, with_locations = true) {
338
386
  if (!node || typeof node !== 'object') return node;
339
- if (Array.isArray(node)) return node.map(clone_expression_node);
340
- const clone = { ...node };
341
- for (const key of Object.keys(clone)) {
342
- if (key === 'loc' || key === 'start' || key === 'end') continue;
387
+ if (Array.isArray(node)) return node.map((child) => clone_expression_node(child, with_locations));
388
+ const clone = /** @type {Record<string, any>} */ ({});
389
+
390
+ for (const key of Object.keys(node)) {
391
+ if (!with_locations && (key === 'loc' || key === 'start' || key === 'end')) {
392
+ continue;
393
+ }
343
394
  if (key === 'metadata') {
344
- clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
395
+ clone.metadata = node.metadata ? { ...node.metadata } : { path: [] };
345
396
  continue;
346
397
  }
347
- clone[key] = clone_expression_node(clone[key]);
398
+ clone[key] = clone_expression_node(node[key], with_locations);
348
399
  }
349
400
  return clone;
350
401
  }