@tsrx/core 0.1.61 → 0.1.63

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.1.61",
6
+ "version": "0.1.63",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -79,7 +79,7 @@
79
79
  "is-reference": "^3.0.3",
80
80
  "magic-string": "^0.30.18",
81
81
  "zimmerframe": "^1.1.2",
82
- "@tsrx/runtime": "0.1.1"
82
+ "@tsrx/runtime": "0.1.2"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/node": "^24.3.0",
@@ -809,6 +809,10 @@ export function get_comment_handlers(source, comments, index = 0) {
809
809
  const hasBlankLine = /\n\s*\n/.test(slice);
810
810
  const nodeEndLine = node.loc?.end?.line ?? null;
811
811
  const commentStartLine = comments[0].loc?.start?.line ?? null;
812
+ const commentOnSameLine =
813
+ nodeEndLine !== null &&
814
+ commentStartLine !== null &&
815
+ nodeEndLine === commentStartLine;
812
816
  const isImmediateNextLine =
813
817
  nodeEndLine !== null &&
814
818
  commentStartLine !== null &&
@@ -853,10 +857,7 @@ export function get_comment_handlers(source, comments, index = 0) {
853
857
  // For function parameters, only attach as trailing comment if it's on the same line
854
858
  // Comments on next line after comma should be leading comments of next parameter
855
859
  if (isParam) {
856
- // Check if comment is on same line as the node
857
- const nodeEndLine = source.slice(0, node.end).split('\n').length;
858
- const commentStartLine = source.slice(0, comments[0].start).split('\n').length;
859
- if (nodeEndLine === commentStartLine) {
860
+ if (commentOnSameLine) {
860
861
  node.trailingComments = [
861
862
  /** @type {AST.CommentWithLocation} */ (comments.shift()),
862
863
  ];
@@ -868,11 +869,6 @@ export function get_comment_handlers(source, comments, index = 0) {
868
869
  // Only attach as trailing if:
869
870
  // 1. It's on the same line as this node, OR
870
871
  // 2. This is the last item in the array (no next sibling to attach to)
871
- const commentOnSameLine =
872
- nodeEndLine !== null &&
873
- commentStartLine !== null &&
874
- nodeEndLine === commentStartLine;
875
-
876
872
  if (commentOnSameLine || is_last_in_array) {
877
873
  node.trailingComments = [
878
874
  /** @type {AST.CommentWithLocation} */ (comments.shift()),
package/src/plugin.js CHANGED
@@ -108,6 +108,88 @@ function get_argument_clash_reported_names(check_clashes) {
108
108
  return reported_names;
109
109
  }
110
110
 
111
+ /**
112
+ * The position of a pending `&{…}`/`&[…]` lazy binding pattern recorded on a
113
+ * DestructuringErrors context, or -1. Acorn's DestructuringErrors class knows
114
+ * nothing about the field, so absence means none was recorded.
115
+ *
116
+ * @param {Parse.DestructuringErrors | undefined | null} refDestructuringErrors
117
+ * @returns {number}
118
+ */
119
+ function get_lazy_binding_pos(refDestructuringErrors) {
120
+ return refDestructuringErrors?.lazyBindingPos ?? -1;
121
+ }
122
+
123
+ /** @type {ReadonlySet<string>} */
124
+ const lazy_target_wrapper_types = new Set([
125
+ 'ParenthesizedExpression',
126
+ 'TSAsExpression',
127
+ 'TSSatisfiesExpression',
128
+ 'TSNonNullExpression',
129
+ 'TSTypeAssertion',
130
+ 'TSTypeCastExpression',
131
+ ]);
132
+
133
+ /**
134
+ * Whether the lazy binding pattern recorded at `pos` (the position of its `&`,
135
+ * one character before the pattern node) sits in a pattern-forming position of
136
+ * `node` — a slot that toAssignable converts into a binding or assignment
137
+ * target. A lazy pattern reached only through an expression position (for
138
+ * example as a member expression's object in `&{ a }.b = x`) is an expression
139
+ * use and must keep its pending error. `node` is the pre-conversion tree, so
140
+ * both the expression and pattern spellings of each slot appear here.
141
+ *
142
+ * @param {AST.Node | null | undefined} node
143
+ * @param {number} pos
144
+ * @returns {boolean}
145
+ */
146
+ function pattern_position_contains_lazy(node, pos) {
147
+ if (!node || typeof node !== 'object') return false;
148
+ if (
149
+ /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (node).lazy &&
150
+ /** @type {number} */ (node.start) === pos + 1
151
+ ) {
152
+ return true;
153
+ }
154
+ // Parentheses and the TypeScript expression wrappers acorn-typescript's
155
+ // toAssignable unwraps when converting a target (`[&{ a }!] = arr`) — the
156
+ // wrapped node stays in a pattern-forming position. TSTypeCastExpression is
157
+ // internal to acorn-typescript, hence the string set rather than switch
158
+ // cases over the ESTree union.
159
+ if (lazy_target_wrapper_types.has(node.type)) {
160
+ return pattern_position_contains_lazy(
161
+ /** @type {{ expression: AST.Node }} */ (/** @type {unknown} */ (node)).expression,
162
+ pos,
163
+ );
164
+ }
165
+ switch (node.type) {
166
+ case 'ObjectExpression':
167
+ case 'ObjectPattern':
168
+ return node.properties.some((property) =>
169
+ pattern_position_contains_lazy(
170
+ property.type === 'Property'
171
+ ? /** @type {AST.Node} */ (property.value)
172
+ : property.argument,
173
+ pos,
174
+ ),
175
+ );
176
+ case 'ArrayExpression':
177
+ case 'ArrayPattern':
178
+ return node.elements.some(
179
+ (element) => element && pattern_position_contains_lazy(element, pos),
180
+ );
181
+ case 'AssignmentExpression':
182
+ return node.operator === '=' && pattern_position_contains_lazy(node.left, pos);
183
+ case 'AssignmentPattern':
184
+ return pattern_position_contains_lazy(node.left, pos);
185
+ case 'SpreadElement':
186
+ case 'RestElement':
187
+ return pattern_position_contains_lazy(node.argument, pos);
188
+ default:
189
+ return false;
190
+ }
191
+ }
192
+
111
193
  /**
112
194
  * A `<` opens a tag only when the character after it can begin one: `/` for a
113
195
  * closing tag, `>` for a fragment, `{` for a dynamic tag, or an element or
@@ -276,6 +358,8 @@ export function TSRXPlugin(config) {
276
358
  #errors = undefined;
277
359
  /** @type {string | null} */
278
360
  #filename = null;
361
+ /** @type {WeakMap<object, { names: Set<string>, lexicalLength: number, varLength: number }>} */
362
+ #localExportNamesByScope = new WeakMap();
279
363
  #functionBodyDepth = 0;
280
364
  #allowExpressionContainerTrailingSemicolon = false;
281
365
  #jsxAttributeValueExpressionDepth = 0;
@@ -1514,6 +1598,25 @@ export function TSRXPlugin(config) {
1514
1598
  * @type {Parse.Parser['parseExprAtom']}
1515
1599
  */
1516
1600
  parseExprAtom(refDestructuringErrors, forInit, forNew) {
1601
+ // A `&{…}`/`&[…]` lazy binding pattern is only meaningful where the
1602
+ // expression may still turn out to be a binding or assignment target —
1603
+ // an arrow parameter list, a destructuring assignment target, or a
1604
+ // for–of/for–in loop target. Those are exactly the positions Acorn
1605
+ // parses with a DestructuringErrors context, so parse the pattern there
1606
+ // and record it as a pending error the same way Acorn treats `{a = b}`
1607
+ // shorthand: pattern conversion accepts the node as-is, while contexts
1608
+ // that remain expressions raise in checkExpressionErrors.
1609
+ if (
1610
+ refDestructuringErrors &&
1611
+ this.type === tt.bitwiseAND &&
1612
+ (this.input.charCodeAt(this.end) === CharCode.openBrace ||
1613
+ this.input.charCodeAt(this.end) === CharCode.openBracket)
1614
+ ) {
1615
+ if (get_lazy_binding_pos(refDestructuringErrors) < 0) {
1616
+ refDestructuringErrors.lazyBindingPos = this.start;
1617
+ }
1618
+ return /** @type {AST.Expression} */ (/** @type {unknown} */ (this.parseBindingAtom()));
1619
+ }
1517
1620
  // A token already consumed as JSX text (a script-mode element child) must
1518
1621
  // stay text even when it happens to begin at an `@` — otherwise whether
1519
1622
  // `@if` parses as a directive would depend on leading whitespace.
@@ -3236,6 +3339,89 @@ export function TSRXPlugin(config) {
3236
3339
  return expr;
3237
3340
  }
3238
3341
 
3342
+ /**
3343
+ * A recorded lazy binding pattern that never became a binding or
3344
+ * assignment target is a pending error, exactly like `{a = b}` shorthand
3345
+ * outside a destructuring pattern. Acorn calls the throwing form at every
3346
+ * boundary where a context definitively stayed an expression
3347
+ * (parenthesized expressions, call arguments, plain assignments'
3348
+ * right-hand sides, …) — that is where the record is enforced.
3349
+ *
3350
+ * The non-throwing form is deliberately left untouched: Acorn uses it in
3351
+ * parseExprOps/parseMaybeConditional to stop parsing operators early, and
3352
+ * returning true there would cut the pattern off from the `as` /
3353
+ * `satisfies` operator lane before toAssignable can accept the wrapped
3354
+ * target (`[&{ a } as T] = arr`). Every path that keeps a stale record
3355
+ * still ends in a throwing check.
3356
+ *
3357
+ * @type {Parse.Parser['checkExpressionErrors']}
3358
+ */
3359
+ checkExpressionErrors(refDestructuringErrors, andThrow) {
3360
+ if (andThrow) {
3361
+ const lazy_binding_pos = get_lazy_binding_pos(refDestructuringErrors);
3362
+ if (lazy_binding_pos >= 0) {
3363
+ this.raise(
3364
+ lazy_binding_pos,
3365
+ 'Lazy binding patterns are only valid as binding or assignment targets',
3366
+ );
3367
+ }
3368
+ }
3369
+ return super.checkExpressionErrors(refDestructuringErrors, andThrow);
3370
+ }
3371
+
3372
+ /**
3373
+ * acorn-typescript unwraps TS expression wrappers in checkLValSimple,
3374
+ * which is only right for simple targets (`[b as any] = arr`). A wrapped
3375
+ * destructuring pattern (`[&{ a } as T] = arr`) reaches checkLValPattern
3376
+ * still wrapped — toAssignableList ignores return values, so the wrapper
3377
+ * survives conversion — and would fall through to checkLValSimple's
3378
+ * "Assigning to rvalue". Unwrap here so wrapped patterns take the
3379
+ * pattern lane.
3380
+ *
3381
+ * @type {Parse.Parser['checkLValPattern']}
3382
+ */
3383
+ checkLValPattern(expr, bindingType, checkClashes) {
3384
+ let node = expr;
3385
+ while (
3386
+ node.type === 'TSNonNullExpression' ||
3387
+ node.type === 'TSAsExpression' ||
3388
+ node.type === 'TSSatisfiesExpression' ||
3389
+ node.type === 'TSTypeAssertion'
3390
+ ) {
3391
+ node = /** @type {AST.Node} */ (
3392
+ /** @type {{ expression: AST.Node }} */ (/** @type {unknown} */ (node)).expression
3393
+ );
3394
+ }
3395
+ return super.checkLValPattern(node, bindingType, checkClashes);
3396
+ }
3397
+
3398
+ /**
3399
+ * Converting a node into an assignment target resolves any lazy binding
3400
+ * pattern recorded inside it — the pattern landed in a valid position,
3401
+ * so its pending error must not outlive the conversion (e.g.
3402
+ * `({ pair: &{ a } } = obj)` would otherwise still raise when the
3403
+ * enclosing parenthesized expression runs checkExpressionErrors). This
3404
+ * mirrors how Acorn resets `shorthandAssign` once the shorthand ends up
3405
+ * inside a converted left-hand side.
3406
+ *
3407
+ * @type {Parse.Parser['toAssignable']}
3408
+ */
3409
+ toAssignable(node, isBinding, refDestructuringErrors, preserveTypeScriptWrapper) {
3410
+ const lazy_binding_pos = get_lazy_binding_pos(refDestructuringErrors);
3411
+ // Only a pattern-forming position resolves the record: a lazy pattern
3412
+ // that is merely inside the target's span but reached through an
3413
+ // expression position (`&{ a }.b = x`) is still an expression use.
3414
+ if (lazy_binding_pos >= 0 && pattern_position_contains_lazy(node, lazy_binding_pos)) {
3415
+ /** @type {Parse.DestructuringErrors} */ (refDestructuringErrors).lazyBindingPos = -1;
3416
+ }
3417
+ return super.toAssignable(
3418
+ node,
3419
+ isBinding,
3420
+ refDestructuringErrors,
3421
+ preserveTypeScriptWrapper,
3422
+ );
3423
+ }
3424
+
3239
3425
  /**
3240
3426
  * Override checkLocalExport to check all scopes in the scope stack.
3241
3427
  * This is needed because submodules create nested scopes, but exports
@@ -3248,8 +3434,7 @@ export function TSRXPlugin(config) {
3248
3434
  if (this.hasImport(name)) return;
3249
3435
  // Check all scopes in the scope stack, not just the top-level scope
3250
3436
  for (let i = this.scopeStack.length - 1; i >= 0; i--) {
3251
- const scope = this.scopeStack[i];
3252
- if (scope.lexical.indexOf(name) !== -1 || scope.var.indexOf(name) !== -1) {
3437
+ if (this.#scopeDeclaredNames(this.scopeStack[i]).has(name)) {
3253
3438
  // Found in a scope, remove from undefinedExports if it was added
3254
3439
  delete this.undefinedExports[name];
3255
3440
  return;
@@ -3259,6 +3444,32 @@ export function TSRXPlugin(config) {
3259
3444
  this.undefinedExports[name] = id;
3260
3445
  }
3261
3446
 
3447
+ /**
3448
+ * The names declared in `scope`, as a cached Set. Acorn only ever
3449
+ * appends to a scope's `lexical` and `var` arrays during the scope's
3450
+ * lifetime, so syncing from the last-seen lengths is enough to keep the
3451
+ * Set current.
3452
+ *
3453
+ * @param {{ lexical: string[], var: string[] }} scope
3454
+ * @returns {Set<string>}
3455
+ */
3456
+ #scopeDeclaredNames(scope) {
3457
+ let cached = this.#localExportNamesByScope.get(scope);
3458
+ if (!cached) {
3459
+ cached = { names: new Set(), lexicalLength: 0, varLength: 0 };
3460
+ this.#localExportNamesByScope.set(scope, cached);
3461
+ }
3462
+ for (let i = cached.lexicalLength; i < scope.lexical.length; i++) {
3463
+ cached.names.add(scope.lexical[i]);
3464
+ }
3465
+ for (let i = cached.varLength; i < scope.var.length; i++) {
3466
+ cached.names.add(scope.var[i]);
3467
+ }
3468
+ cached.lexicalLength = scope.lexical.length;
3469
+ cached.varLength = scope.var.length;
3470
+ return cached.names;
3471
+ }
3472
+
3262
3473
  /** @type {Parse.Parser['parseForStatement']} */
3263
3474
  parseForStatement(node) {
3264
3475
  this.next();
@@ -391,14 +391,17 @@ export function flatten_switch_consequent(consequent) {
391
391
  export function clone_ast_node(node, with_locations = true) {
392
392
  if (!node || typeof node !== 'object') return node;
393
393
  if (Array.isArray(node)) {
394
- return /** @type {T} */ (node.map((child) => clone_ast_node(child, with_locations)));
394
+ const clone = new Array(node.length);
395
+ for (let i = 0; i < node.length; i++) {
396
+ clone[i] = clone_ast_node(node[i], with_locations);
397
+ }
398
+ return /** @type {T} */ (clone);
395
399
  }
396
- const clone = { ...node };
400
+ const clone = /** @type {T} */ (with_locations ? { ...node } : {});
397
401
  const clone_record = /** @type {Record<string, unknown>} */ (clone);
398
402
 
399
403
  for (const key of Object.keys(node)) {
400
404
  if (!with_locations && (key === 'loc' || key === 'start' || key === 'end')) {
401
- delete clone_record[key];
402
405
  continue;
403
406
  }
404
407
  if (key === 'metadata') {
@@ -517,6 +517,11 @@ export function convert_source_map_to_mappings(
517
517
  function set_bracket_computed_mapping(node, mappings) {
518
518
  if (has_location(node.key)) {
519
519
  const key = node.key;
520
+ const start_key = `${key.loc.start.line}:${key.loc.start.column - 1}`;
521
+ const end_key = `${key.loc.end.line}:${key.loc.end.column + 1}`;
522
+ if (!src_to_gen_map.get(start_key)?.length || !src_to_gen_map.get(end_key)?.length) {
523
+ return;
524
+ }
520
525
  mappings.push(
521
526
  get_mapping_from_node(
522
527
  /** @type {AST.NodeWithLocation} */ ({
package/types/parse.d.ts CHANGED
@@ -86,6 +86,11 @@ export namespace Parse {
86
86
  parenthesizedAssign: number;
87
87
  parenthesizedBind: number;
88
88
  doubleProto: number;
89
+ /**
90
+ * TSRX extension: position of a pending `&{…}`/`&[…]` lazy binding
91
+ * pattern parsed in expression position, set only by TSRXPlugin.
92
+ */
93
+ lazyBindingPos?: number;
89
94
  }
90
95
 
91
96
  /**
@@ -1605,11 +1610,13 @@ export namespace Parse {
1605
1610
  * @param node Expression to convert
1606
1611
  * @param isBinding Whether binding pattern
1607
1612
  * @param refDestructuringErrors Error collector
1613
+ * @param preserveTypeScriptWrapper Keep TS wrapper nodes (acorn-typescript extension)
1608
1614
  */
1609
1615
  toAssignable(
1610
1616
  node: AST.Node,
1611
1617
  isBinding?: boolean,
1612
1618
  refDestructuringErrors?: DestructuringErrors,
1619
+ preserveTypeScriptWrapper?: boolean,
1613
1620
  ): AST.Pattern;
1614
1621
 
1615
1622
  /**