@tsrx/core 0.0.1 → 0.0.2

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/src/plugin.js ADDED
@@ -0,0 +1,2248 @@
1
+ /**
2
+ @import * as AST from 'estree'
3
+ @import * as ESTreeJSX from 'estree-jsx'
4
+ @import { Parse } from '@tsrx/core/types'
5
+ */
6
+
7
+ import * as acorn from 'acorn';
8
+ import { parse_style } from './parse/style.js';
9
+ import {
10
+ convert_from_jsx,
11
+ skipWhitespace,
12
+ isWhitespaceTextNode,
13
+ BINDING_TYPES,
14
+ DestructuringErrors,
15
+ } from './parse/index.js';
16
+ import { regex_newline_characters } from './utils/patterns.js';
17
+ import { error } from './errors.js';
18
+
19
+ /**
20
+ * Acorn parser plugin for Ripple syntax extensions.
21
+ * Adds support for: component declarations, &[]/&{} lazy destructuring,
22
+ * #server blocks, #style identifiers, and enhanced JSX handling.
23
+ *
24
+ * @param {import('../types/index').TSRXPluginConfig} [config] - Plugin configuration
25
+ * @returns {(Parser: Parse.ParserConstructor) => Parse.ParserConstructor} Parser extension function
26
+ */
27
+ export function TSRXPlugin(config) {
28
+ return (/** @type {Parse.ParserConstructor} */ Parser) => {
29
+ const original = acorn.Parser.prototype;
30
+ const tt = Parser.tokTypes || acorn.tokTypes;
31
+ const tc = Parser.tokContexts || acorn.tokContexts;
32
+ // Some parser constructors (e.g. via TS plugins) expose `tokContexts` without `b_stat`.
33
+ // If we push an undefined context, Acorn's tokenizer will later crash reading `.override`.
34
+ const b_stat = tc.b_stat || acorn.tokContexts.b_stat;
35
+ const tstt = Parser.acornTypeScript.tokTypes;
36
+ const tstc = Parser.acornTypeScript.tokContexts;
37
+
38
+ class TSRXParser extends Parser {
39
+ /** @type {AST.Node[]} */
40
+ #path = [];
41
+ #commentContextId = 0;
42
+ #loose = false;
43
+ /** @type {import('../types/index').CompileError[] | undefined} */
44
+ #errors = undefined;
45
+ /** @type {string | null} */
46
+ #filename = null;
47
+
48
+ /**
49
+ * @param {Parse.Options} options
50
+ * @param {string} input
51
+ */
52
+ constructor(options, input) {
53
+ super(options, input);
54
+ this.#loose = options?.rippleOptions.loose === true;
55
+ this.#errors = options?.rippleOptions.errors;
56
+ this.#filename = options?.rippleOptions.filename || null;
57
+ }
58
+
59
+ /**
60
+ * @param {number} position
61
+ * @param {string} message
62
+ */
63
+ #report_recoverable_error(position, message) {
64
+ const start = Math.max(0, Math.min(position, this.input.length));
65
+ const end = Math.min(this.input.length, start + 1);
66
+ const start_loc = acorn.getLineInfo(this.input, start);
67
+ const end_loc = acorn.getLineInfo(this.input, end);
68
+
69
+ error(
70
+ message,
71
+ this.#filename,
72
+ /** @type {AST.NodeWithLocation} */ ({
73
+ start,
74
+ end,
75
+ loc: {
76
+ start: start_loc,
77
+ end: end_loc,
78
+ },
79
+ }),
80
+ this.#loose ? this.#errors : undefined,
81
+ );
82
+ }
83
+
84
+ /**
85
+ * In loose mode, keep parsing after duplicate declaration diagnostics so
86
+ * editor tooling can continue producing AST and mappings.
87
+ * @param {number} position
88
+ * @param {string | { message?: string }} message
89
+ */
90
+ raiseRecoverable(position, message) {
91
+ const error_message =
92
+ typeof message === 'string'
93
+ ? message
94
+ : typeof message?.message === 'string'
95
+ ? message.message
96
+ : String(message);
97
+
98
+ if (error_message.includes('has already been declared')) {
99
+ this.#report_recoverable_error(position, error_message);
100
+ return;
101
+ }
102
+
103
+ return super.raiseRecoverable(position, error_message);
104
+ }
105
+
106
+ /**
107
+ * Override to allow single-parameter generic arrow functions without trailing comma.
108
+ * By default, @sveltejs/acorn-typescript throws an error for `<T>() => {}` when JSX is enabled
109
+ * because it can't disambiguate from JSX. However, the parser still parses it correctly
110
+ * using tryParse - it just throws afterwards. By overriding this to do nothing, we allow
111
+ * the valid parse to succeed.
112
+ * @param {AST.TSTypeParameterDeclaration} node
113
+ */
114
+ reportReservedArrowTypeParam(node) {
115
+ // Allow <T>() => {} syntax without requiring trailing comma
116
+ if (this.#loose && node.params.length === 1 && node.extra?.trailingComma === undefined) {
117
+ error(
118
+ 'This syntax is reserved in files with the .mts or .cts extension. Add a trailing comma, as in `<T,>() => ...`.',
119
+ this.#filename,
120
+ node,
121
+ this.#errors,
122
+ );
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Override to allow `readonly` type modifier on any type in loose mode.
128
+ * By default, @sveltejs/acorn-typescript throws an error for `readonly { ... }`
129
+ * because TypeScript only permits `readonly` on array and tuple types.
130
+ * Suppress the error in the strict mode as ts is compiled away.
131
+ * @param {AST.TSTypeOperator} node
132
+ */
133
+ tsCheckTypeAnnotationForReadOnly(node) {
134
+ const typeAnnotation = /** @type {AST.TypeNode} */ (node.typeAnnotation);
135
+ if (typeAnnotation.type === 'TSTupleType' || typeAnnotation.type === 'TSArrayType') {
136
+ // Valid readonly usage, no error needed
137
+ return;
138
+ }
139
+
140
+ if (this.#loose) {
141
+ error(
142
+ "'readonly' type modifier is only permitted on array and tuple literal types.",
143
+ this.#filename,
144
+ typeAnnotation,
145
+ this.#errors,
146
+ );
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Override parseProperty to support component methods in object literals.
152
+ * Handles syntax like `{ component something() { <div /> } }`
153
+ * Also supports computed names: `{ component ['something']() { <div /> } }`
154
+ * @type {Parse.Parser['parseProperty']}
155
+ */
156
+ parseProperty(isPattern, refDestructuringErrors) {
157
+ // Check if this is a component method: component name( ... ) { ... }
158
+ if (!isPattern && this.type === tt.name && this.value === 'component') {
159
+ // Look ahead to see if this is "component identifier(", "component identifier<", "component [", or "component 'string'"
160
+ const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
161
+ if (lookahead) {
162
+ // This is a component method definition
163
+ const prop = /** @type {AST.Property} */ (this.startNode());
164
+ const isComputed = lookahead[0].trim().startsWith('[');
165
+ const isStringLiteral = /^['"]/.test(lookahead[0].trim());
166
+
167
+ if (isComputed) {
168
+ // For computed names, consume 'component'
169
+ // parse the key, then parse component without name
170
+ this.next(); // consume 'component'
171
+ this.next(); // consume '['
172
+ prop.key = this.parseExpression();
173
+ this.expect(tt.bracketR);
174
+ prop.computed = true;
175
+
176
+ // Parse component without name (skipName: true)
177
+ const component_node = this.parseComponent({ skipName: true });
178
+ /** @type {AST.TSRXProperty} */ (prop).value = component_node;
179
+ } else if (isStringLiteral) {
180
+ // For string literal names, consume 'component'
181
+ // parse the string key, then parse component without name
182
+ this.next(); // consume 'component'
183
+ prop.key = /** @type {AST.Literal} */ (this.parseExprAtom());
184
+ prop.computed = false;
185
+
186
+ // Parse component without name (skipName: true)
187
+ const component_node = this.parseComponent({ skipName: true });
188
+ /** @type {AST.TSRXProperty} */ (prop).value = component_node;
189
+ } else {
190
+ const component_node = this.parseComponent({ requireName: true });
191
+
192
+ prop.key = /** @type {AST.Identifier} */ (component_node.id);
193
+ /** @type {AST.TSRXProperty} */ (prop).value = component_node;
194
+ prop.computed = false;
195
+ }
196
+
197
+ prop.shorthand = false;
198
+ prop.method = true;
199
+ prop.kind = 'init';
200
+
201
+ return this.finishNode(prop, 'Property');
202
+ }
203
+ }
204
+
205
+ return super.parseProperty(isPattern, refDestructuringErrors);
206
+ }
207
+
208
+ /**
209
+ * Override parseClassElement to support component methods in classes.
210
+ * Handles syntax like `class Foo { component something() { <div /> } }`
211
+ * Also supports computed names: `class Foo { component ['something']() { <div /> } }`
212
+ * @type {Parse.Parser['parseClassElement']}
213
+ */
214
+ parseClassElement(constructorAllowsSuper) {
215
+ // Check if this is a component method: component name( ... ) { ... }
216
+ if (this.type === tt.name && this.value === 'component') {
217
+ // Look ahead to see if this is "component identifier(",
218
+ // "component identifier<", "component [", or "component 'string'"
219
+ const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
220
+ if (lookahead) {
221
+ // This is a component method definition
222
+ const node = /** @type {AST.MethodDefinition} */ (this.startNode());
223
+ const isComputed = lookahead[0].trim().startsWith('[');
224
+ const isStringLiteral = /^['"]/.test(lookahead[0].trim());
225
+
226
+ if (isComputed) {
227
+ // For computed names, consume 'component'
228
+ // parse the key, then parse component without name
229
+ this.next(); // consume 'component'
230
+ this.next(); // consume '['
231
+ node.key = this.parseExpression();
232
+ this.expect(tt.bracketR);
233
+ node.computed = true;
234
+
235
+ // Parse component without name (skipName: true)
236
+ const component_node = this.parseComponent({ skipName: true });
237
+ /** @type {AST.TSRXMethodDefinition} */ (node).value = component_node;
238
+ } else if (isStringLiteral) {
239
+ // For string literal names, consume 'component'
240
+ // parse the string key, then parse component without name
241
+ this.next(); // consume 'component'
242
+ node.key = /** @type {AST.Literal} */ (this.parseExprAtom());
243
+ node.computed = false;
244
+
245
+ // Parse component without name (skipName: true)
246
+ const component_node = this.parseComponent({ skipName: true });
247
+ /** @type {AST.TSRXMethodDefinition} */ (node).value = component_node;
248
+ } else {
249
+ // Use parseComponent which handles consuming 'component', parsing name, params, and body
250
+ const component_node = this.parseComponent({ requireName: true });
251
+
252
+ node.key = /** @type {AST.Identifier} */ (component_node.id);
253
+ /** @type {AST.TSRXMethodDefinition} */ (node).value = component_node;
254
+ node.computed = false;
255
+ }
256
+
257
+ node.static = false;
258
+ node.kind = 'method';
259
+
260
+ return this.finishNode(node, 'MethodDefinition');
261
+ }
262
+ }
263
+
264
+ return super.parseClassElement(constructorAllowsSuper);
265
+ }
266
+
267
+ /**
268
+ * Override parsePropertyValue to support TypeScript generic methods in object literals.
269
+ * By default, acorn-typescript doesn't handle `{ method<T>() {} }` syntax.
270
+ * This override checks for type parameters before parsing the method.
271
+ * @type {Parse.Parser['parsePropertyValue']}
272
+ */
273
+ parsePropertyValue(
274
+ prop,
275
+ isPattern,
276
+ isGenerator,
277
+ isAsync,
278
+ startPos,
279
+ startLoc,
280
+ refDestructuringErrors,
281
+ containsEsc,
282
+ ) {
283
+ // Check if this is a method with type parameters (e.g., `method<T>() {}`)
284
+ // We need to parse type parameters before the parentheses
285
+ if (
286
+ !isPattern &&
287
+ !isGenerator &&
288
+ !isAsync &&
289
+ this.type === tt.relational &&
290
+ this.value === '<'
291
+ ) {
292
+ // Try to parse type parameters
293
+ const typeParameters = this.tsTryParseTypeParameters();
294
+ if (typeParameters && this.type === tt.parenL) {
295
+ // This is a method with type parameters
296
+ /** @type {AST.Property} */ (prop).method = true;
297
+ /** @type {AST.Property} */ (prop).kind = 'init';
298
+ /** @type {AST.Property} */ (prop).value = this.parseMethod(false, false);
299
+ /** @type {AST.FunctionExpression} */ (
300
+ /** @type {AST.Property} */ (prop).value
301
+ ).typeParameters = typeParameters;
302
+ return;
303
+ }
304
+ }
305
+
306
+ return super.parsePropertyValue(
307
+ prop,
308
+ isPattern,
309
+ isGenerator,
310
+ isAsync,
311
+ startPos,
312
+ startLoc,
313
+ refDestructuringErrors,
314
+ containsEsc,
315
+ );
316
+ }
317
+
318
+ /**
319
+ * Acorn expects `this.context` to always contain at least one tokContext.
320
+ * Some of our template/JSX escape hatches can pop contexts aggressively;
321
+ * if the stack becomes empty, Acorn will crash reading `curContext().override`.
322
+ * @type {Parse.Parser['nextToken']}
323
+ */
324
+ nextToken() {
325
+ while (this.context.length && this.context[this.context.length - 1] == null) {
326
+ this.context.pop();
327
+ }
328
+ if (this.context.length === 0) {
329
+ this.context.push(b_stat);
330
+ }
331
+ return super.nextToken();
332
+ }
333
+
334
+ /**
335
+ * @returns {Parse.CommentMetaData | null}
336
+ */
337
+ #createCommentMetadata() {
338
+ if (this.#path.length === 0) {
339
+ return null;
340
+ }
341
+
342
+ const container = this.#path[this.#path.length - 1];
343
+ if (!container || container.type !== 'Element') {
344
+ return null;
345
+ }
346
+
347
+ const children = Array.isArray(container.children) ? container.children : [];
348
+ const hasMeaningfulChildren = children.some(
349
+ (child) => child && !isWhitespaceTextNode(child),
350
+ );
351
+
352
+ if (hasMeaningfulChildren) {
353
+ return null;
354
+ }
355
+
356
+ container.metadata ??= { path: [] };
357
+ if (container.metadata.commentContainerId === undefined) {
358
+ container.metadata.commentContainerId = ++this.#commentContextId;
359
+ }
360
+
361
+ return /*** @type {Parse.CommentMetaData} */ ({
362
+ containerId: container.metadata.commentContainerId,
363
+ childIndex: children.length,
364
+ beforeMeaningfulChild: !hasMeaningfulChildren,
365
+ });
366
+ }
367
+
368
+ /**
369
+ * Helper method to get the element name from a JSX identifier or member expression
370
+ * @type {Parse.Parser['getElementName']}
371
+ */
372
+ getElementName(node) {
373
+ if (!node) return null;
374
+ if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
375
+ return node.name;
376
+ } else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
377
+ // For components like <Foo.Bar>, return "Foo.Bar"
378
+ return this.getElementName(node.object) + '.' + this.getElementName(node.property);
379
+ }
380
+ return null;
381
+ }
382
+
383
+ /**
384
+ * Get token from character code - handles Ripple-specific tokens
385
+ * @type {Parse.Parser['getTokenFromCode']}
386
+ */
387
+ getTokenFromCode(code) {
388
+ if (code === 60) {
389
+ // < character
390
+ const inComponent = this.#path.findLast((n) => n.type === 'Component');
391
+ /** @type {number | null} */
392
+ let prevNonWhitespaceChar = null;
393
+
394
+ // Check if this could be TypeScript generics instead of JSX
395
+ // TypeScript generics appear after: identifiers, closing parens, 'new' keyword
396
+ // For example: Array<T>, func<T>(), new Map<K,V>(), method<T>()
397
+ // This check applies everywhere, not just inside components
398
+
399
+ // Look back to see what precedes the <
400
+ let lookback = this.pos - 1;
401
+
402
+ // Skip whitespace backwards
403
+ while (lookback >= 0) {
404
+ const ch = this.input.charCodeAt(lookback);
405
+ if (ch !== 32 && ch !== 9) break; // not space or tab
406
+ lookback--;
407
+ }
408
+
409
+ // Check what character/token precedes the <
410
+ if (lookback >= 0) {
411
+ const prevChar = this.input.charCodeAt(lookback);
412
+ prevNonWhitespaceChar = prevChar;
413
+
414
+ // If preceded by identifier character (letter, digit, _, $) or closing paren,
415
+ // this is likely TypeScript generics, not JSX
416
+ const isIdentifierChar =
417
+ (prevChar >= 65 && prevChar <= 90) || // A-Z
418
+ (prevChar >= 97 && prevChar <= 122) || // a-z
419
+ (prevChar >= 48 && prevChar <= 57) || // 0-9
420
+ prevChar === 95 || // _
421
+ prevChar === 36 || // $
422
+ prevChar === 41; // )
423
+
424
+ if (isIdentifierChar) {
425
+ return super.getTokenFromCode(code);
426
+ }
427
+ }
428
+
429
+ // Support parsing standalone template markup at the top-level (outside `component`)
430
+ // for tooling like Prettier, e.g.:
431
+ // <Something>...</Something>\n\n<Child />
432
+ // <head><style>...</style></head>
433
+ // We only do this when '<' is in a tag-like position.
434
+ const nextChar =
435
+ this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
436
+ const isWhitespaceAfterLt =
437
+ nextChar === 32 || nextChar === 9 || nextChar === 10 || nextChar === 13;
438
+ const isTagLikeAfterLt =
439
+ !isWhitespaceAfterLt &&
440
+ (nextChar === 47 || // '/'
441
+ (nextChar >= 65 && nextChar <= 90) || // A-Z
442
+ (nextChar >= 97 && nextChar <= 122)); // a-z
443
+ const prevAllowsTagStart =
444
+ prevNonWhitespaceChar === null ||
445
+ prevNonWhitespaceChar === 10 || // '\n'
446
+ prevNonWhitespaceChar === 13 || // '\r'
447
+ prevNonWhitespaceChar === 123 || // '{'
448
+ prevNonWhitespaceChar === 125 || // '}'
449
+ prevNonWhitespaceChar === 62; // '>'
450
+
451
+ if (!inComponent && prevAllowsTagStart && isTagLikeAfterLt) {
452
+ ++this.pos;
453
+ return this.finishToken(tstt.jsxTagStart);
454
+ }
455
+
456
+ if (inComponent) {
457
+ // Inside component template bodies, allow adjacent tags without requiring
458
+ // a newline/indentation before the next '<'. This is important for inputs
459
+ // like `<div />` and `</div><style>...</style>` which Prettier formats.
460
+ if (prevNonWhitespaceChar === 123 /* '{' */ || prevNonWhitespaceChar === 62 /* '>' */) {
461
+ if (!isWhitespaceAfterLt) {
462
+ ++this.pos;
463
+ return this.finishToken(tstt.jsxTagStart);
464
+ }
465
+ }
466
+
467
+ // Check if we're inside a nested function (arrow function, function expression, etc.)
468
+ // We need to distinguish between being inside a function vs just being in nested scopes
469
+ // (like for loops, if blocks, JSX elements, etc.)
470
+ const nestedFunctionContext = this.context.some((ctx) => ctx.token === 'function');
471
+
472
+ // Inside nested functions, treat < as relational/generic operator
473
+ // BUT: if the < is followed by /, it's a closing JSX tag, not a less-than operator
474
+ const nextChar =
475
+ this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
476
+ const isClosingTag = nextChar === 47; // '/'
477
+
478
+ if (nestedFunctionContext && !isClosingTag) {
479
+ // Inside function - treat as TypeScript generic, not JSX
480
+ ++this.pos;
481
+ return this.finishToken(tt.relational, '<');
482
+ }
483
+
484
+ // Check if everything before this position on the current line is whitespace
485
+ let lineStart = this.pos - 1;
486
+ while (
487
+ lineStart >= 0 &&
488
+ this.input.charCodeAt(lineStart) !== 10 &&
489
+ this.input.charCodeAt(lineStart) !== 13
490
+ ) {
491
+ lineStart--;
492
+ }
493
+ lineStart++; // Move past the newline character
494
+
495
+ // Check if all characters from line start to current position are whitespace
496
+ let allWhitespace = true;
497
+ for (let i = lineStart; i < this.pos; i++) {
498
+ const ch = this.input.charCodeAt(i);
499
+ if (ch !== 32 && ch !== 9) {
500
+ allWhitespace = false;
501
+ break;
502
+ }
503
+ }
504
+
505
+ // Check if the character after < is not whitespace
506
+ if (allWhitespace && this.pos + 1 < this.input.length) {
507
+ const nextChar = this.input.charCodeAt(this.pos + 1);
508
+ if (nextChar !== 32 && nextChar !== 9 && nextChar !== 10 && nextChar !== 13) {
509
+ ++this.pos;
510
+ return this.finishToken(tstt.jsxTagStart);
511
+ }
512
+ }
513
+ }
514
+ }
515
+
516
+ if (code === 35) {
517
+ // # character
518
+ if (this.pos + 1 < this.input.length) {
519
+ /** @param {string} value */
520
+ const startsWith = (value) =>
521
+ this.input.slice(this.pos, this.pos + value.length) === value;
522
+ /** @param {number} length */
523
+ const char_after = (length) =>
524
+ this.pos + length < this.input.length ? this.input.charCodeAt(this.pos + length) : -1;
525
+ /** @param {number} ch */
526
+ const is_ripple_delimiter = (ch) =>
527
+ ch === 40 || // (
528
+ ch === 41 || // )
529
+ ch === 60 || // <
530
+ ch === 46 || // .
531
+ ch === 44 || // ,
532
+ ch === 59 || // ;
533
+ ch === 91 || // [
534
+ ch === 93 || // ]
535
+ ch === 123 || // {
536
+ ch === 125 || // }
537
+ ch === 32 || // space
538
+ ch === 9 || // tab
539
+ ch === 10 || // newline
540
+ ch === 13 || // carriage return
541
+ ch === -1; // EOF
542
+
543
+ if (startsWith('#server') && is_ripple_delimiter(char_after(7))) {
544
+ this.pos += 7;
545
+ return this.finishToken(tt.name, '#server');
546
+ }
547
+
548
+ if (startsWith('#style') && is_ripple_delimiter(char_after(6))) {
549
+ this.pos += 6;
550
+ return this.finishToken(tt.name, '#style');
551
+ }
552
+ }
553
+ }
554
+ return super.getTokenFromCode(code);
555
+ }
556
+
557
+ /**
558
+ * Override isLet to recognize `let &{` and `let &[` as variable declarations.
559
+ * Acorn's isLet checks the char after `let` and only recognizes `{`, `[`, or identifiers.
560
+ * The `&` char (38) is not in that set, so `let &{...}` would not be parsed as a declaration.
561
+ * @type {Parse.Parser['isLet']}
562
+ */
563
+ isLet(context) {
564
+ if (!this.isContextual('let')) return false;
565
+ const skip = /\s*/y;
566
+ skip.lastIndex = this.pos;
567
+ const match = skip.exec(this.input);
568
+ if (!match) return super.isLet(context);
569
+ const next = this.pos + match[0].length;
570
+ const nextCh = this.input.charCodeAt(next);
571
+ // If next char is &, check if char after & is { or [
572
+ if (nextCh === 38) {
573
+ const afterAmp = this.input.charCodeAt(next + 1);
574
+ if (afterAmp === 123 || afterAmp === 91) return true;
575
+ }
576
+ return super.isLet(context);
577
+ }
578
+
579
+ /**
580
+ * Parse binding atom - handles lazy destructuring patterns (&{...} and &[...])
581
+ * When & is directly followed by { or [, parse as a lazy destructuring pattern.
582
+ * The resulting ObjectPattern/ArrayPattern node gets a `lazy: true` flag.
583
+ */
584
+ parseBindingAtom() {
585
+ if (this.type === tt.bitwiseAND) {
586
+ // Check that the char immediately after & is { or [ (no whitespace)
587
+ const charAfterAmp = this.input.charCodeAt(this.end);
588
+ if (charAfterAmp === 123 || charAfterAmp === 91) {
589
+ // & directly followed by { or [ — lazy destructuring
590
+ this.next(); // consume &, now current token is { or [
591
+ const pattern = super.parseBindingAtom();
592
+ /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (pattern).lazy = true;
593
+ return pattern;
594
+ }
595
+ }
596
+ return super.parseBindingAtom();
597
+ }
598
+
599
+ /**
600
+ * Parse expression atom - handles RippleArray and RippleObject literals
601
+ * @type {Parse.Parser['parseExprAtom']}
602
+ */
603
+ parseExprAtom(refDestructuringErrors, forNew, forInit) {
604
+ const lookahead_type = this.lookahead().type;
605
+ const is_next_call_token = lookahead_type === tt.parenL || lookahead_type === tt.relational;
606
+
607
+ // Check if this is #server identifier for server function calls
608
+ if (this.type === tt.name && this.value === '#server') {
609
+ const node = this.startNode();
610
+ this.next();
611
+ return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
612
+ }
613
+
614
+ if (this.type === tt.name && this.value === '#style') {
615
+ const node = this.startNode();
616
+ this.next();
617
+ return /** @type {AST.StyleIdentifier} */ (this.finishNode(node, 'StyleIdentifier'));
618
+ }
619
+
620
+ // Check if this is a component expression (e.g., in object literal values)
621
+ if (this.type === tt.name && this.value === 'component') {
622
+ return this.parseComponent();
623
+ }
624
+
625
+ return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
626
+ }
627
+
628
+ /**
629
+ * Override to track parenthesized expressions in metadata
630
+ * This allows the prettier plugin to preserve parentheses where they existed
631
+ * @type {Parse.Parser['parseParenAndDistinguishExpression']}
632
+ */
633
+ parseParenAndDistinguishExpression(canBeArrow, forInit) {
634
+ const startPos = this.start;
635
+ const expr = super.parseParenAndDistinguishExpression(canBeArrow, forInit);
636
+
637
+ // If the expression's start position is after the opening paren,
638
+ // it means it was wrapped in parentheses. Mark it in metadata.
639
+ if (expr && /** @type {AST.NodeWithLocation} */ (expr).start > startPos) {
640
+ expr.metadata ??= { path: [] };
641
+ expr.metadata.parenthesized = true;
642
+ }
643
+
644
+ return expr;
645
+ }
646
+
647
+ /**
648
+ * Override checkLocalExport to check all scopes in the scope stack.
649
+ * This is needed because server blocks create nested scopes, but exports
650
+ * from within server blocks should still be valid if the identifier is
651
+ * declared in the server block's scope (not just the top-level module scope).
652
+ * @type {Parse.Parser['checkLocalExport']}
653
+ */
654
+ checkLocalExport(id) {
655
+ const { name } = id;
656
+ if (this.hasImport(name)) return;
657
+ // Check all scopes in the scope stack, not just the top-level scope
658
+ for (let i = this.scopeStack.length - 1; i >= 0; i--) {
659
+ const scope = this.scopeStack[i];
660
+ if (scope.lexical.indexOf(name) !== -1 || scope.var.indexOf(name) !== -1) {
661
+ // Found in a scope, remove from undefinedExports if it was added
662
+ delete this.undefinedExports[name];
663
+ return;
664
+ }
665
+ }
666
+ // Not found in any scope, add to undefinedExports for later error
667
+ this.undefinedExports[name] = id;
668
+ }
669
+
670
+ /**
671
+ * @type {Parse.Parser['parseServerBlock']}
672
+ */
673
+ parseServerBlock() {
674
+ const node = /** @type {AST.ServerBlock} */ (this.startNode());
675
+ this.next();
676
+
677
+ const body = /** @type {AST.ServerBlockStatement} */ (this.startNode());
678
+ node.body = body;
679
+ body.body = [];
680
+
681
+ this.expect(tt.braceL);
682
+ this.enterScope(0);
683
+ while (this.type !== tt.braceR) {
684
+ const stmt = /** @type {AST.Statement} */ (this.parseStatement(null, true));
685
+ body.body.push(stmt);
686
+ }
687
+ this.next();
688
+ this.exitScope();
689
+ this.finishNode(body, 'BlockStatement');
690
+
691
+ this.awaitPos = 0;
692
+ return this.finishNode(node, 'ServerBlock');
693
+ }
694
+
695
+ /**
696
+ * Parse a component - common implementation used by statements, expressions, and export defaults
697
+ * @type {Parse.Parser['parseComponent']}
698
+ */
699
+ parseComponent({
700
+ requireName = false,
701
+ isDefault = false,
702
+ declareName = false,
703
+ skipName = false,
704
+ } = {}) {
705
+ const node = /** @type {AST.Component} */ (this.startNode());
706
+ node.type = 'Component';
707
+ node.css = null;
708
+ node.default = isDefault;
709
+
710
+ // skipName is used for computed property names where 'component' and the key
711
+ // have already been consumed before calling parseComponent
712
+ if (!skipName) {
713
+ this.next(); // consume 'component'
714
+ }
715
+ this.enterScope(0);
716
+
717
+ if (skipName) {
718
+ // For computed names, the key is parsed separately, so id is null
719
+ node.id = null;
720
+ } else if (requireName) {
721
+ node.id = this.parseIdent();
722
+ if (declareName) {
723
+ this.declareName(
724
+ node.id.name,
725
+ BINDING_TYPES.BIND_FUNCTION,
726
+ /** @type {AST.NodeWithLocation} */ (node.id).start,
727
+ );
728
+ }
729
+ } else {
730
+ node.id = this.type.label === 'name' ? this.parseIdent() : null;
731
+ if (declareName && node.id) {
732
+ this.declareName(
733
+ node.id.name,
734
+ BINDING_TYPES.BIND_FUNCTION,
735
+ /** @type {AST.NodeWithLocation} */ (node.id).start,
736
+ );
737
+ }
738
+ }
739
+
740
+ this.parseFunctionParams(node);
741
+ this.eat(tt.braceL);
742
+ node.body = [];
743
+ this.#path.push(node);
744
+
745
+ this.parseTemplateBody(node.body);
746
+ this.#path.pop();
747
+ this.exitScope();
748
+
749
+ this.next();
750
+ skipWhitespace(this);
751
+ this.finishNode(node, 'Component');
752
+ this.awaitPos = 0;
753
+
754
+ return node;
755
+ }
756
+
757
+ /**
758
+ * @type {Parse.Parser['parseExportDefaultDeclaration']}
759
+ */
760
+ parseExportDefaultDeclaration() {
761
+ // Check if this is "export default component"
762
+ if (this.value === 'component') {
763
+ return this.parseComponent({ isDefault: true });
764
+ }
765
+
766
+ return super.parseExportDefaultDeclaration();
767
+ }
768
+
769
+ /** @type {Parse.Parser['parseForStatement']} */
770
+ parseForStatement(node) {
771
+ this.next();
772
+ let awaitAt =
773
+ this.options.ecmaVersion >= 9 && this.canAwait && this.eatContextual('await')
774
+ ? this.lastTokStart
775
+ : -1;
776
+ this.labels.push({ kind: 'loop' });
777
+ this.enterScope(0);
778
+ this.expect(tt.parenL);
779
+
780
+ if (this.type === tt.semi) {
781
+ if (awaitAt > -1) this.unexpected(awaitAt);
782
+ return this.parseFor(node, null);
783
+ }
784
+
785
+ // @ts-ignore — acorn internal: isLet accepts 0 args at runtime
786
+ let isLet = this.isLet();
787
+ if (this.type === tt._var || this.type === tt._const || isLet) {
788
+ let init = /** @type {AST.VariableDeclaration} */ (this.startNode()),
789
+ kind = isLet ? 'let' : /** @type {AST.VariableDeclaration['kind']} */ (this.value);
790
+ this.next();
791
+ this.parseVar(init, true, kind);
792
+ this.finishNode(init, 'VariableDeclaration');
793
+ return this.parseForAfterInitWithIndex(
794
+ /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
795
+ init,
796
+ awaitAt,
797
+ );
798
+ }
799
+
800
+ // Handle other cases like using declarations if they exist
801
+ let startsWithLet = this.isContextual('let'),
802
+ isForOf = false;
803
+ let usingKind =
804
+ this.isUsing && this.isUsing(true)
805
+ ? 'using'
806
+ : this.isAwaitUsing && this.isAwaitUsing(true)
807
+ ? 'await using'
808
+ : null;
809
+ if (usingKind) {
810
+ let init = /** @type {AST.VariableDeclaration} */ (this.startNode());
811
+ this.next();
812
+ if (usingKind === 'await using') {
813
+ if (!this.canAwait) {
814
+ this.raise(this.start, 'Await using cannot appear outside of async function');
815
+ }
816
+ this.next();
817
+ }
818
+ this.parseVar(init, true, usingKind);
819
+ this.finishNode(init, 'VariableDeclaration');
820
+ return this.parseForAfterInitWithIndex(
821
+ /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
822
+ init,
823
+ awaitAt,
824
+ );
825
+ }
826
+
827
+ let containsEsc = this.containsEsc;
828
+ let refDestructuringErrors = new /** @type {new () => Parse.DestructuringErrors} */ (
829
+ /** @type {unknown} */ (DestructuringErrors)
830
+ )();
831
+ let initPos = this.start;
832
+ let init_expr =
833
+ awaitAt > -1
834
+ ? this.parseExprSubscripts(refDestructuringErrors, 'await')
835
+ : this.parseExpression(true, refDestructuringErrors);
836
+
837
+ if (
838
+ this.type === tt._in ||
839
+ (isForOf = this.options.ecmaVersion >= 6 && this.isContextual('of'))
840
+ ) {
841
+ if (awaitAt > -1) {
842
+ // implies `ecmaVersion >= 9`
843
+ if (this.type === tt._in) this.unexpected(awaitAt);
844
+ /** @type {AST.ForOfStatement} */ (node).await = true;
845
+ } else if (isForOf && this.options.ecmaVersion >= 8) {
846
+ if (
847
+ init_expr.start === initPos &&
848
+ !containsEsc &&
849
+ init_expr.type === 'Identifier' &&
850
+ init_expr.name === 'async'
851
+ )
852
+ this.unexpected();
853
+ else if (this.options.ecmaVersion >= 9)
854
+ /** @type {AST.ForOfStatement} */ (node).await = false;
855
+ }
856
+ if (startsWithLet && isForOf)
857
+ this.raise(
858
+ /** @type {AST.NodeWithLocation} */ (init_expr).start,
859
+ "The left-hand side of a for-of loop may not start with 'let'.",
860
+ );
861
+ const init = this.toAssignable(init_expr, false, refDestructuringErrors);
862
+ this.checkLValPattern(init);
863
+ return this.parseForInWithIndex(
864
+ /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
865
+ init,
866
+ );
867
+ } else {
868
+ this.checkExpressionErrors(refDestructuringErrors, true);
869
+ }
870
+
871
+ if (awaitAt > -1) this.unexpected(awaitAt);
872
+ return this.parseFor(node, init_expr);
873
+ }
874
+
875
+ /** @type {Parse.Parser['parseForAfterInitWithIndex']} */
876
+ parseForAfterInitWithIndex(node, init, awaitAt) {
877
+ if (
878
+ (this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual('of'))) &&
879
+ init.declarations.length === 1
880
+ ) {
881
+ if (this.options.ecmaVersion >= 9) {
882
+ if (this.type === tt._in) {
883
+ if (awaitAt > -1) {
884
+ this.unexpected(awaitAt);
885
+ }
886
+ } else {
887
+ /** @type {AST.ForOfStatement} */ (node).await = awaitAt > -1;
888
+ }
889
+ }
890
+ return this.parseForInWithIndex(
891
+ /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
892
+ init,
893
+ );
894
+ }
895
+ if (awaitAt > -1) {
896
+ this.unexpected(awaitAt);
897
+ }
898
+ return this.parseFor(node, init);
899
+ }
900
+
901
+ /** @type {Parse.Parser['parseForInWithIndex']} */
902
+ parseForInWithIndex(node, init) {
903
+ const isForIn = this.type === tt._in;
904
+ this.next();
905
+
906
+ if (
907
+ init.type === 'VariableDeclaration' &&
908
+ init.declarations[0].init != null &&
909
+ (!isForIn ||
910
+ this.options.ecmaVersion < 8 ||
911
+ this.strict ||
912
+ init.kind !== 'var' ||
913
+ init.declarations[0].id.type !== 'Identifier')
914
+ ) {
915
+ this.raise(
916
+ /** @type {AST.NodeWithLocation} */ (init).start,
917
+ `${isForIn ? 'for-in' : 'for-of'} loop variable declaration may not have an initializer`,
918
+ );
919
+ }
920
+
921
+ node.left = init;
922
+ node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign();
923
+
924
+ // Check for our extended syntax: "; index varName"
925
+ if (!isForIn && this.type === tt.semi) {
926
+ this.next(); // consume ';'
927
+
928
+ if (this.isContextual('index')) {
929
+ this.next(); // consume 'index'
930
+ /** @type {AST.ForOfStatement} */ (node).index = /** @type {AST.Identifier} */ (
931
+ this.parseExpression()
932
+ );
933
+ if (
934
+ /** @type {AST.Identifier} */ (/** @type {AST.ForOfStatement} */ (node).index)
935
+ .type !== 'Identifier'
936
+ ) {
937
+ this.raise(this.start, 'Expected identifier after "index" keyword');
938
+ }
939
+ this.eat(tt.semi);
940
+ }
941
+
942
+ if (this.isContextual('key')) {
943
+ this.next(); // consume 'key'
944
+ /** @type {AST.ForOfStatement} */ (node).key = this.parseExpression();
945
+ }
946
+
947
+ if (this.isContextual('index')) {
948
+ this.raise(this.start, '"index" must come before "key" in for-of loop');
949
+ }
950
+ } else if (!isForIn) {
951
+ // Set index to null for standard for-of loops
952
+ /** @type {AST.ForOfStatement} */ (node).index = null;
953
+ }
954
+
955
+ this.expect(tt.parenR);
956
+ node.body = /** @type {AST.BlockStatement} */ (this.parseStatement('for'));
957
+ this.exitScope();
958
+ this.labels.pop();
959
+ return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
960
+ }
961
+
962
+ /**
963
+ * @type {Parse.Parser['checkUnreserved']}
964
+ */
965
+ checkUnreserved(ref) {
966
+ if (ref.name === 'component') {
967
+ // Allow 'component' when it's followed by an identifier and '(' or '<' (component method in object literal or class)
968
+ // e.g., { component something() { ... } } or class Foo { component something<T>() { ... } }
969
+ // Also allow computed names: { component ['name']() { ... } }
970
+ // Also allow string literal names: { component 'name'() { ... } }
971
+ const nextChars = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
972
+ if (!nextChars) {
973
+ this.raise(
974
+ ref.start,
975
+ '"component" is a Ripple keyword and cannot be used as an identifier',
976
+ );
977
+ }
978
+ }
979
+ return super.checkUnreserved(ref);
980
+ }
981
+
982
+ /** @type {Parse.Parser['shouldParseExportStatement']} */
983
+ shouldParseExportStatement() {
984
+ if (super.shouldParseExportStatement()) {
985
+ return true;
986
+ }
987
+ if (this.value === 'component') {
988
+ return true;
989
+ }
990
+ return this.type.keyword === 'var';
991
+ }
992
+
993
+ /**
994
+ * @return {ESTreeJSX.JSXExpressionContainer}
995
+ */
996
+ jsx_parseExpressionContainer() {
997
+ let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
998
+ this.next();
999
+
1000
+ if (this.type === tt.name && this.value === 'html') {
1001
+ node.html = true;
1002
+ this.next();
1003
+ if (this.type === tt.braceR) {
1004
+ this.raise(
1005
+ this.start,
1006
+ '"html" is a Ripple keyword and must be used in the form {html some_content}',
1007
+ );
1008
+ }
1009
+ } else if (this.type === tt.name && this.value === 'text') {
1010
+ node.text = true;
1011
+ this.next();
1012
+ if (this.type === tt.braceR) {
1013
+ this.raise(
1014
+ this.start,
1015
+ '"text" is a Ripple keyword and must be used in the form {text some_value}',
1016
+ );
1017
+ }
1018
+ }
1019
+
1020
+ node.expression =
1021
+ this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1022
+ this.expect(tt.braceR);
1023
+
1024
+ return this.finishNode(node, 'JSXExpressionContainer');
1025
+ }
1026
+
1027
+ /**
1028
+ * @type {Parse.Parser['jsx_parseEmptyExpression']}
1029
+ */
1030
+ jsx_parseEmptyExpression() {
1031
+ // Override to properly handle the range for JSXEmptyExpression
1032
+ // The range should be from after { to before }
1033
+ const node = /** @type {ESTreeJSX.JSXEmptyExpression} */ (
1034
+ this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc)
1035
+ );
1036
+ node.end = this.start;
1037
+ node.loc.end = this.startLoc;
1038
+ return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
1039
+ }
1040
+
1041
+ /**
1042
+ * @type {Parse.Parser['jsx_parseTupleContainer']}
1043
+ */
1044
+ jsx_parseTupleContainer() {
1045
+ const t = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1046
+ return (
1047
+ this.next(),
1048
+ (t.expression =
1049
+ this.type === tt.bracketR ? this.jsx_parseEmptyExpression() : this.parseExpression()),
1050
+ this.expect(tt.bracketR),
1051
+ this.finishNode(t, 'JSXExpressionContainer')
1052
+ );
1053
+ }
1054
+
1055
+ /**
1056
+ * @type {Parse.Parser['jsx_parseAttribute']}
1057
+ */
1058
+ jsx_parseAttribute() {
1059
+ let node = /** @type {AST.TSRXAttribute | ESTreeJSX.JSXAttribute} */ (this.startNode());
1060
+
1061
+ if (this.eat(tt.braceL)) {
1062
+ if (this.value === 'ref') {
1063
+ this.next();
1064
+ if (this.type === tt.braceR) {
1065
+ this.raise(
1066
+ this.start,
1067
+ '"ref" is a Ripple keyword and must be used in the form {ref fn}',
1068
+ );
1069
+ }
1070
+ /** @type {AST.RefAttribute} */ (node).argument = this.parseMaybeAssign();
1071
+ this.expect(tt.braceR);
1072
+ return /** @type {AST.RefAttribute} */ (this.finishNode(node, 'RefAttribute'));
1073
+ } else if (this.type === tt.ellipsis) {
1074
+ this.expect(tt.ellipsis);
1075
+ /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
1076
+ this.expect(tt.braceR);
1077
+ return this.finishNode(node, 'SpreadAttribute');
1078
+ } else if (this.lookahead().type === tt.ellipsis) {
1079
+ this.expect(tt.ellipsis);
1080
+ /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
1081
+ this.expect(tt.braceR);
1082
+ return this.finishNode(node, 'SpreadAttribute');
1083
+ } else {
1084
+ const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
1085
+ id.tracked = false;
1086
+ this.finishNode(id, 'Identifier');
1087
+ /** @type {AST.Attribute} */ (node).name = id;
1088
+ /** @type {AST.Attribute} */ (node).value = id;
1089
+ /** @type {AST.Attribute} */ (node).shorthand = true; // Mark as shorthand since name and value are the same
1090
+ this.next();
1091
+ this.expect(tt.braceR);
1092
+ return this.finishNode(node, 'Attribute');
1093
+ }
1094
+ }
1095
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
1096
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).value =
1097
+ /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
1098
+ this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
1099
+ );
1100
+ return this.finishNode(node, 'JSXAttribute');
1101
+ }
1102
+
1103
+ /**
1104
+ * @type {Parse.Parser['jsx_parseNamespacedName']}
1105
+ */
1106
+ jsx_parseNamespacedName() {
1107
+ const base = this.jsx_parseIdentifier();
1108
+ if (!this.eat(tt.colon)) return base;
1109
+ const node = /** @type {ESTreeJSX.JSXNamespacedName} */ (
1110
+ this.startNodeAt(
1111
+ /** @type {AST.NodeWithLocation} */ (base).start,
1112
+ /** @type {AST.NodeWithLocation} */ (base).loc.start,
1113
+ )
1114
+ );
1115
+ node.namespace = base;
1116
+ node.name = this.jsx_parseIdentifier();
1117
+ return this.finishNode(node, 'JSXNamespacedName');
1118
+ }
1119
+
1120
+ /**
1121
+ * @type {Parse.Parser['jsx_parseIdentifier']}
1122
+ */
1123
+ jsx_parseIdentifier() {
1124
+ const node = /** @type {ESTreeJSX.JSXIdentifier} */ (this.startNode());
1125
+
1126
+ if (this.type.label === '@') {
1127
+ this.next(); // consume @
1128
+
1129
+ if (this.type === tt.name || this.type === tstt.jsxName) {
1130
+ node.name = /** @type {string} */ (this.value);
1131
+ node.tracked = true;
1132
+ this.next();
1133
+ } else {
1134
+ // Unexpected token after @
1135
+ this.unexpected();
1136
+ }
1137
+ } else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
1138
+ node.name = /** @type {string} */ (this.value);
1139
+ node.tracked = false; // Explicitly mark as not tracked
1140
+ this.next();
1141
+ } else {
1142
+ return super.jsx_parseIdentifier();
1143
+ }
1144
+
1145
+ return this.finishNode(node, 'JSXIdentifier');
1146
+ }
1147
+
1148
+ /**
1149
+ * @type {Parse.Parser['jsx_parseElementName']}
1150
+ */
1151
+ jsx_parseElementName() {
1152
+ if (this.type === tstt.jsxTagEnd) {
1153
+ return '';
1154
+ }
1155
+
1156
+ let node = this.jsx_parseNamespacedName();
1157
+
1158
+ if (node.type === 'JSXNamespacedName') {
1159
+ return node;
1160
+ }
1161
+
1162
+ if (this.eat(tt.dot)) {
1163
+ let memberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
1164
+ this.startNodeAt(
1165
+ /** @type {AST.NodeWithLocation} */ (node).start,
1166
+ /** @type {AST.NodeWithLocation} */ (node).loc.start,
1167
+ )
1168
+ );
1169
+ memberExpr.object = node;
1170
+ memberExpr.property = this.jsx_parseIdentifier();
1171
+ memberExpr.computed = false;
1172
+ memberExpr = this.finishNode(memberExpr, 'JSXMemberExpression');
1173
+ while (this.eat(tt.dot)) {
1174
+ let newMemberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
1175
+ this.startNodeAt(
1176
+ /** @type {AST.NodeWithLocation} */ (memberExpr).start,
1177
+ /** @type {AST.NodeWithLocation} */ (memberExpr).loc.start,
1178
+ )
1179
+ );
1180
+ newMemberExpr.object = memberExpr;
1181
+ newMemberExpr.property = this.jsx_parseIdentifier();
1182
+ newMemberExpr.computed = false;
1183
+ memberExpr = this.finishNode(newMemberExpr, 'JSXMemberExpression');
1184
+ }
1185
+ return memberExpr;
1186
+ }
1187
+ return node;
1188
+ }
1189
+
1190
+ /** @type {Parse.Parser['jsx_parseAttributeValue']} */
1191
+ jsx_parseAttributeValue() {
1192
+ switch (this.type) {
1193
+ case tt.braceL:
1194
+ return this.jsx_parseExpressionContainer();
1195
+ case tstt.jsxTagStart:
1196
+ case tt.string:
1197
+ return this.parseExprAtom();
1198
+ default:
1199
+ this.raise(this.start, 'value should be either an expression or a quoted text');
1200
+ }
1201
+ }
1202
+
1203
+ /**
1204
+ * @type {Parse.Parser['parseTryStatement']}
1205
+ */
1206
+ parseTryStatement(node) {
1207
+ this.next();
1208
+ node.block = this.parseBlock();
1209
+ node.handler = null;
1210
+
1211
+ if (this.value === 'pending') {
1212
+ this.next();
1213
+ node.pending = this.parseBlock();
1214
+ } else {
1215
+ node.pending = null;
1216
+ }
1217
+
1218
+ if (this.type === tt._catch) {
1219
+ const clause = /** @type {AST.CatchClause} */ (this.startNode());
1220
+ this.next();
1221
+ if (this.eat(tt.parenL)) {
1222
+ // Parse first param (error) manually to support optional second param (reset).
1223
+ // We can't use parseCatchClauseParam() because it eats the closing paren.
1224
+ const param = this.parseBindingAtom();
1225
+ const simple = param.type === 'Identifier';
1226
+ this.enterScope(simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : 0);
1227
+ this.checkLValPattern(
1228
+ param,
1229
+ simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : BINDING_TYPES.BIND_LEXICAL,
1230
+ );
1231
+ const type = this.tsTryParseTypeAnnotation();
1232
+ if (type) {
1233
+ param.typeAnnotation = type;
1234
+ this.resetEndLocation(param);
1235
+ }
1236
+ clause.param = param;
1237
+
1238
+ // Optional second parameter: reset function
1239
+ if (this.eat(tt.comma)) {
1240
+ const reset_param = this.parseBindingAtom();
1241
+ this.checkLValSimple(reset_param, BINDING_TYPES.BIND_LEXICAL);
1242
+ const reset_type = this.tsTryParseTypeAnnotation();
1243
+ if (reset_type) {
1244
+ reset_param.typeAnnotation = reset_type;
1245
+ this.resetEndLocation(reset_param);
1246
+ }
1247
+ clause.resetParam = reset_param;
1248
+ } else {
1249
+ clause.resetParam = null;
1250
+ }
1251
+
1252
+ this.expect(tt.parenR);
1253
+ } else {
1254
+ clause.param = null;
1255
+ clause.resetParam = null;
1256
+ this.enterScope(0);
1257
+ }
1258
+ clause.body = this.parseBlock(false);
1259
+ this.exitScope();
1260
+ node.handler = this.finishNode(clause, 'CatchClause');
1261
+ }
1262
+ node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
1263
+
1264
+ if (!node.handler && !node.finalizer && !node.pending) {
1265
+ this.raise(
1266
+ /** @type {AST.NodeWithLocation} */ (node).start,
1267
+ 'Missing catch or finally clause',
1268
+ );
1269
+ }
1270
+ return this.finishNode(node, 'TryStatement');
1271
+ }
1272
+
1273
+ /** @type {Parse.Parser['jsx_readToken']} */
1274
+ jsx_readToken() {
1275
+ const inside_tsx_compat = this.#path.findLast(
1276
+ (n) => n.type === 'TsxCompat' || n.type === 'Tsx',
1277
+ );
1278
+ if (inside_tsx_compat) {
1279
+ return super.jsx_readToken();
1280
+ }
1281
+ let out = '',
1282
+ chunkStart = this.pos;
1283
+
1284
+ while (true) {
1285
+ if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
1286
+ let ch = this.input.charCodeAt(this.pos);
1287
+
1288
+ switch (ch) {
1289
+ case 60: // '<'
1290
+ case 123: // '{'
1291
+ // In JSX text mode, '<' and '{' always start a tag/expression container.
1292
+ // `exprAllowed` can be false here due to surrounding parser state, but
1293
+ // throwing breaks valid templates (e.g. sibling tags after a close).
1294
+ if (ch === 60) {
1295
+ ++this.pos;
1296
+ return this.finishToken(tstt.jsxTagStart);
1297
+ }
1298
+ return this.getTokenFromCode(ch);
1299
+
1300
+ case 47: // '/'
1301
+ // Check if this is a comment (// or /*)
1302
+ if (this.input.charCodeAt(this.pos + 1) === 47) {
1303
+ // '//'
1304
+ // Line comment - handle it properly
1305
+ const commentStart = this.pos;
1306
+ const startLoc = this.curPosition();
1307
+ this.pos += 2;
1308
+
1309
+ let commentText = '';
1310
+ while (this.pos < this.input.length) {
1311
+ const nextCh = this.input.charCodeAt(this.pos);
1312
+ if (acorn.isNewLine(nextCh)) break;
1313
+ commentText += this.input[this.pos];
1314
+ this.pos++;
1315
+ }
1316
+
1317
+ const commentEnd = this.pos;
1318
+ const endLoc = this.curPosition();
1319
+
1320
+ // Call onComment if it exists
1321
+ if (this.options.onComment) {
1322
+ const metadata = this.#createCommentMetadata();
1323
+ this.options.onComment(
1324
+ false,
1325
+ commentText,
1326
+ commentStart,
1327
+ commentEnd,
1328
+ startLoc,
1329
+ endLoc,
1330
+ metadata,
1331
+ );
1332
+ }
1333
+
1334
+ // Continue processing from current position
1335
+ break;
1336
+ } else if (this.input.charCodeAt(this.pos + 1) === 42) {
1337
+ // '/*'
1338
+ // Block comment - handle it properly
1339
+ const commentStart = this.pos;
1340
+ const startLoc = this.curPosition();
1341
+ this.pos += 2;
1342
+
1343
+ let commentText = '';
1344
+ while (this.pos < this.input.length - 1) {
1345
+ if (
1346
+ this.input.charCodeAt(this.pos) === 42 &&
1347
+ this.input.charCodeAt(this.pos + 1) === 47
1348
+ ) {
1349
+ this.pos += 2;
1350
+ break;
1351
+ }
1352
+ commentText += this.input[this.pos];
1353
+ this.pos++;
1354
+ }
1355
+
1356
+ const commentEnd = this.pos;
1357
+ const endLoc = this.curPosition();
1358
+
1359
+ // Call onComment if it exists
1360
+ if (this.options.onComment) {
1361
+ const metadata = this.#createCommentMetadata();
1362
+ this.options.onComment(
1363
+ true,
1364
+ commentText,
1365
+ commentStart,
1366
+ commentEnd,
1367
+ startLoc,
1368
+ endLoc,
1369
+ metadata,
1370
+ );
1371
+ }
1372
+
1373
+ // Continue processing from current position
1374
+ break;
1375
+ }
1376
+ // If not a comment, fall through to default case
1377
+ this.context.push(b_stat);
1378
+ this.exprAllowed = true;
1379
+ return original.readToken.call(this, ch);
1380
+
1381
+ case 38: // '&'
1382
+ out += this.input.slice(chunkStart, this.pos);
1383
+ out += this.jsx_readEntity();
1384
+ chunkStart = this.pos;
1385
+ break;
1386
+
1387
+ case 62: // '>'
1388
+ case 125: {
1389
+ // '}'
1390
+ if (
1391
+ ch === 125 &&
1392
+ (this.#path.length === 0 ||
1393
+ this.#path.at(-1)?.type === 'Component' ||
1394
+ this.#path.at(-1)?.type === 'Element')
1395
+ ) {
1396
+ return original.readToken.call(this, ch);
1397
+ }
1398
+ this.raise(
1399
+ this.pos,
1400
+ 'Unexpected token `' +
1401
+ this.input[this.pos] +
1402
+ '`. Did you mean `' +
1403
+ (ch === 62 ? '&gt;' : '&rbrace;') +
1404
+ '` or ' +
1405
+ '`{"' +
1406
+ this.input[this.pos] +
1407
+ '"}' +
1408
+ '`?',
1409
+ );
1410
+ }
1411
+
1412
+ default:
1413
+ if (acorn.isNewLine(ch)) {
1414
+ out += this.input.slice(chunkStart, this.pos);
1415
+ out += this.jsx_readNewLine(true);
1416
+ chunkStart = this.pos;
1417
+ } else if (ch === 32 || ch === 9) {
1418
+ ++this.pos;
1419
+ } else {
1420
+ this.context.push(b_stat);
1421
+ this.exprAllowed = true;
1422
+ return original.readToken.call(this, ch);
1423
+ }
1424
+ }
1425
+ }
1426
+ }
1427
+
1428
+ /**
1429
+ * Override jsx_parseElement to intercept expression-level JSX.
1430
+ * This is called by acorn-jsx's parseExprAtom when it encounters <
1431
+ * in expression position. Only <tsx> and <tsx:*> are allowed.
1432
+ * @type {Parse.Parser['jsx_parseElement']}
1433
+ */
1434
+ jsx_parseElement() {
1435
+ const inside_tsx = this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
1436
+ if (inside_tsx) {
1437
+ // Inside tsx/tsx:*, let acorn-jsx handle it normally
1438
+ return super.jsx_parseElement();
1439
+ }
1440
+
1441
+ // Check if the element being parsed IS a <tsx> or <tsx:*> tag
1442
+ // Current token is jsxTagStart, this.end is position after '<'
1443
+ const tag_name_start = this.end;
1444
+ const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
1445
+ const is_tsx_tag =
1446
+ this.input.startsWith('tsx', tag_name_start) &&
1447
+ (tag_name_start + 3 >= this.input.length ||
1448
+ char_after_tsx === 62 || // >
1449
+ char_after_tsx === 47 || // / (self-closing)
1450
+ char_after_tsx === 32 || // space
1451
+ char_after_tsx === 9 || // tab
1452
+ char_after_tsx === 10 || // newline
1453
+ char_after_tsx === 13 || // carriage return
1454
+ char_after_tsx === 58); // : (tsx:react)
1455
+
1456
+ if (is_tsx_tag) {
1457
+ // Use Ripple's parseElement to create a Tsx/TsxCompat node
1458
+ this.next();
1459
+ return /** @type {import('estree-jsx').JSXElement} */ (
1460
+ /** @type {unknown} */ (this.parseElement())
1461
+ );
1462
+ }
1463
+
1464
+ this.raise(
1465
+ this.start,
1466
+ 'JSX elements cannot be used as expressions. Wrap with `<tsx>...</tsx>` or use elements as statements within a component.',
1467
+ );
1468
+ }
1469
+
1470
+ /**
1471
+ * @type {Parse.Parser['parseElement']}
1472
+ */
1473
+ parseElement() {
1474
+ const inside_head = this.#path.findLast(
1475
+ (n) => n.type === 'Element' && n.id.type === 'Identifier' && n.id.name === 'head',
1476
+ );
1477
+ // Adjust the start so we capture the `<` as part of the element
1478
+ const start = this.start - 1;
1479
+ const position = new acorn.Position(this.curLine, start - this.lineStart);
1480
+
1481
+ const element = /** @type {AST.Element | AST.Tsx | AST.TsxCompat} */ (this.startNode());
1482
+ element.start = start;
1483
+ /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
1484
+ element.metadata = { path: [] };
1485
+ element.children = [];
1486
+
1487
+ const open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
1488
+ this.jsx_parseOpeningElementAt(start, position)
1489
+ );
1490
+
1491
+ // Always attach the concrete opening element node for accurate source mapping
1492
+ element.openingElement = open;
1493
+
1494
+ // Check if this is a namespaced element (tsx:react)
1495
+ const is_tsx_compat = open.name.type === 'JSXNamespacedName';
1496
+ const is_tsx =
1497
+ !is_tsx_compat && open.name.type === 'JSXIdentifier' && open.name.name === 'tsx';
1498
+
1499
+ if (is_tsx_compat) {
1500
+ const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
1501
+ /** @type {AST.TsxCompat} */ (element).type = 'TsxCompat';
1502
+ /** @type {AST.TsxCompat} */ (element).kind = namespace_node.name.name; // e.g., "react" from "tsx:react"
1503
+
1504
+ if (open.selfClosing) {
1505
+ const tagName = namespace_node.namespace.name + ':' + namespace_node.name.name;
1506
+ this.raise(
1507
+ open.start,
1508
+ `TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
1509
+ );
1510
+ }
1511
+ } else if (is_tsx) {
1512
+ /** @type {AST.Tsx} */ (element).type = 'Tsx';
1513
+
1514
+ if (open.selfClosing) {
1515
+ this.raise(
1516
+ open.start,
1517
+ `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
1518
+ );
1519
+ }
1520
+ } else {
1521
+ element.type = 'Element';
1522
+ }
1523
+
1524
+ this.#path.push(element);
1525
+
1526
+ for (const attr of open.attributes) {
1527
+ if (attr.type === 'JSXAttribute') {
1528
+ /** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
1529
+ if (attr.name.type === 'JSXIdentifier') {
1530
+ /** @type {AST.Identifier} */ (/** @type {unknown} */ (attr.name)).type =
1531
+ 'Identifier';
1532
+ }
1533
+ if (attr.value !== null) {
1534
+ if (attr.value.type === 'JSXExpressionContainer') {
1535
+ const expression = attr.value.expression;
1536
+ if (expression.type === 'Literal') {
1537
+ expression.was_expression = true;
1538
+ }
1539
+ // @ts-ignore — intentional AST node conversion from JSX to Ripple
1540
+ /** @type {ESTreeJSX.JSXAttribute} */ (attr).value =
1541
+ /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (expression);
1542
+ }
1543
+ }
1544
+ }
1545
+ }
1546
+
1547
+ if (!is_tsx_compat && !is_tsx) {
1548
+ /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
1549
+ convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
1550
+ );
1551
+ element.selfClosing = open.selfClosing;
1552
+ }
1553
+
1554
+ element.attributes = open.attributes;
1555
+ element.metadata ??= { path: [] };
1556
+ element.metadata.commentContainerId = ++this.#commentContextId;
1557
+
1558
+ if (element.selfClosing) {
1559
+ this.#path.pop();
1560
+
1561
+ if (this.type.label === '</>/<=/>=') {
1562
+ this.pos--;
1563
+ this.next();
1564
+ }
1565
+ } else {
1566
+ if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
1567
+ let content = '';
1568
+
1569
+ // TODO implement this where we get a string for content of the content of the script tag
1570
+ // This is a temporary workaround to get the content of the script tag
1571
+ const start = open.end;
1572
+ const input = this.input.slice(start);
1573
+ const end = input.indexOf('</script>');
1574
+ content = end === -1 ? input : input.slice(0, end);
1575
+
1576
+ const newLines = content.match(regex_newline_characters)?.length;
1577
+ if (newLines) {
1578
+ this.curLine = open.loc.end.line + newLines;
1579
+ this.lineStart = start + content.lastIndexOf('\n') + 1;
1580
+ }
1581
+ if (end !== -1) {
1582
+ const closingStart = start + content.length;
1583
+ const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
1584
+ const closingStartLoc = new acorn.Position(
1585
+ closingLineInfo.line,
1586
+ closingLineInfo.column,
1587
+ );
1588
+
1589
+ // Ensure `</script>` can't be tokenized as `<` followed by a regexp
1590
+ // start when we manually advance to the `/`.
1591
+ this.exprAllowed = false;
1592
+
1593
+ // Position after '<' (so next() reads '/')
1594
+ this.pos = closingStart + 1;
1595
+ this.type = tstt.jsxTagStart;
1596
+ this.start = closingStart;
1597
+ this.startLoc = closingStartLoc;
1598
+ this.next();
1599
+
1600
+ // Consume '/'
1601
+ this.next();
1602
+
1603
+ const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
1604
+ element.closingElement = closingElement;
1605
+ this.exprAllowed = false;
1606
+
1607
+ const contentStartLineInfo = acorn.getLineInfo(this.input, start);
1608
+ const contentStartLoc = new acorn.Position(
1609
+ contentStartLineInfo.line,
1610
+ contentStartLineInfo.column,
1611
+ );
1612
+
1613
+ const contentEndLineInfo = acorn.getLineInfo(this.input, closingStart);
1614
+ const contentEndLoc = new acorn.Position(
1615
+ contentEndLineInfo.line,
1616
+ contentEndLineInfo.column,
1617
+ );
1618
+
1619
+ element.children = [
1620
+ /** @type {AST.ScriptContent} */ (
1621
+ /** @type {unknown} */ ({
1622
+ type: 'ScriptContent',
1623
+ content,
1624
+ start,
1625
+ end: closingStart,
1626
+ loc: { start: contentStartLoc, end: contentEndLoc },
1627
+ })
1628
+ ),
1629
+ ];
1630
+
1631
+ this.#path.pop();
1632
+ } else {
1633
+ // No closing tag
1634
+ if (!this.#loose) {
1635
+ this.raise(
1636
+ open.end,
1637
+ "Unclosed tag '<script>'. Expected '</script>' before end of component.",
1638
+ );
1639
+ }
1640
+ /** @type {AST.Element} */ (element).unclosed = true;
1641
+ this.#path.pop();
1642
+ }
1643
+ } else if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'style') {
1644
+ // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
1645
+ // So backtrack to the end of the <style> tag to make sure everything is included
1646
+ const start = open.end;
1647
+ const input = this.input.slice(start);
1648
+ const end = input.indexOf('</style>');
1649
+ const content = end === -1 ? input : input.slice(0, end);
1650
+
1651
+ const component = /** @type {AST.Component} */ (
1652
+ this.#path.findLast((n) => n.type === 'Component')
1653
+ );
1654
+ const parsed_css = parse_style(content, { loose: this.#loose });
1655
+
1656
+ if (!inside_head) {
1657
+ if (component.css !== null) {
1658
+ throw new Error('Components can only have one style tag');
1659
+ }
1660
+ component.css = parsed_css;
1661
+ /** @type {AST.Element} */ (element).metadata.styleScopeHash = parsed_css.hash;
1662
+ }
1663
+
1664
+ const newLines = content.match(regex_newline_characters)?.length;
1665
+ if (newLines) {
1666
+ this.curLine = open.loc.end.line + newLines;
1667
+ this.lineStart = start + content.lastIndexOf('\n') + 1;
1668
+ }
1669
+ if (end !== -1) {
1670
+ const closingStart = start + content.length;
1671
+ const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
1672
+ const closingStartLoc = new acorn.Position(
1673
+ closingLineInfo.line,
1674
+ closingLineInfo.column,
1675
+ );
1676
+
1677
+ // Ensure `</style>` can't be tokenized as `<` followed by a regexp
1678
+ // start when we manually advance to the `/`.
1679
+ this.exprAllowed = false;
1680
+
1681
+ // Position after '<' (so next() reads '/')
1682
+ this.pos = closingStart + 1;
1683
+ this.type = tstt.jsxTagStart;
1684
+ this.start = closingStart;
1685
+ this.startLoc = closingStartLoc;
1686
+ this.next();
1687
+
1688
+ // Consume '/'
1689
+ this.next();
1690
+
1691
+ const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
1692
+ element.closingElement = closingElement;
1693
+ this.exprAllowed = false;
1694
+ this.#path.pop();
1695
+ } else {
1696
+ if (!this.#loose) {
1697
+ this.raise(
1698
+ open.end,
1699
+ "Unclosed tag '<style>'. Expected '</style>' before end of component.",
1700
+ );
1701
+ }
1702
+ /** @type {AST.Element} */ (element).unclosed = true;
1703
+ this.#path.pop();
1704
+ }
1705
+ // This node is used for Prettier - always add parsed CSS as children
1706
+ // for proper formatting, regardless of whether it's inside head or not
1707
+ /** @type {AST.Element} */ (element).children = [
1708
+ /** @type {AST.Node} */ (/** @type {unknown} */ (parsed_css)),
1709
+ ];
1710
+
1711
+ // Ensure we escape JSX <tag></tag> context
1712
+ const curContext = this.curContext();
1713
+ const parent = this.#path.at(-1);
1714
+ const insideTemplate =
1715
+ parent?.type === 'Component' ||
1716
+ parent?.type === 'Element' ||
1717
+ parent?.type === 'Tsx' ||
1718
+ parent?.type === 'TsxCompat';
1719
+
1720
+ if (curContext === tstc.tc_expr && !insideTemplate) {
1721
+ this.context.pop();
1722
+ }
1723
+
1724
+ /** @type {AST.Element} */ (element).css = content;
1725
+ } else {
1726
+ this.enterScope(0);
1727
+ this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
1728
+ this.exitScope();
1729
+
1730
+ if (element.type === 'Tsx') {
1731
+ this.#path.pop();
1732
+
1733
+ if (!element.unclosed) {
1734
+ const raise_error = () => {
1735
+ this.raise(this.start, `Expected closing tag '</tsx>'`);
1736
+ };
1737
+
1738
+ this.next();
1739
+ // we should expect to see </tsx>
1740
+ if (this.value !== '/') {
1741
+ raise_error();
1742
+ }
1743
+ this.next();
1744
+ if (this.value !== 'tsx') {
1745
+ raise_error();
1746
+ }
1747
+ this.next();
1748
+ if (this.type !== tstt.jsxTagEnd) {
1749
+ raise_error();
1750
+ }
1751
+ this.next();
1752
+ }
1753
+ } else if (element.type === 'TsxCompat') {
1754
+ this.#path.pop();
1755
+
1756
+ if (!element.unclosed) {
1757
+ const raise_error = () => {
1758
+ this.raise(this.start, `Expected closing tag '</tsx:${element.kind}>'`);
1759
+ };
1760
+
1761
+ this.next();
1762
+ // we should expect to see </tsx:kind>
1763
+ if (this.value !== '/') {
1764
+ raise_error();
1765
+ }
1766
+ this.next();
1767
+ if (this.value !== 'tsx') {
1768
+ raise_error();
1769
+ }
1770
+ this.next();
1771
+ if (this.type.label !== ':') {
1772
+ raise_error();
1773
+ }
1774
+ this.next();
1775
+ if (this.value !== element.kind) {
1776
+ raise_error();
1777
+ }
1778
+ this.next();
1779
+ if (this.type !== tstt.jsxTagEnd) {
1780
+ raise_error();
1781
+ }
1782
+ this.next();
1783
+ }
1784
+ } else if (this.#path[this.#path.length - 1] === element) {
1785
+ // Check if this element was properly closed
1786
+ if (!this.#loose) {
1787
+ const tagName = this.getElementName(element.id);
1788
+ this.raise(
1789
+ this.start,
1790
+ `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
1791
+ );
1792
+ } else {
1793
+ element.unclosed = true;
1794
+ element.loc.end = {
1795
+ .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
1796
+ };
1797
+ element.end = element.openingElement.end;
1798
+ this.#path.pop();
1799
+ }
1800
+ }
1801
+ }
1802
+
1803
+ // Ensure we escape JSX <tag></tag> context
1804
+ const curContext = this.curContext();
1805
+ const parent = this.#path.at(-1);
1806
+ const insideTemplate =
1807
+ parent?.type === 'Component' ||
1808
+ parent?.type === 'Element' ||
1809
+ parent?.type === 'Tsx' ||
1810
+ parent?.type === 'TsxCompat';
1811
+
1812
+ if (curContext === tstc.tc_expr && !insideTemplate) {
1813
+ this.context.pop();
1814
+ }
1815
+ }
1816
+
1817
+ if (element.closingElement && !is_tsx_compat && !is_tsx) {
1818
+ /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
1819
+ element.closingElement.name,
1820
+ );
1821
+ }
1822
+
1823
+ this.finishNode(element, element.type);
1824
+ return element;
1825
+ }
1826
+
1827
+ /**
1828
+ * @type {Parse.Parser['parseTemplateBody']}
1829
+ */
1830
+ parseTemplateBody(body) {
1831
+ const inside_func =
1832
+ this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
1833
+ const inside_tsx = this.#path.findLast((n) => n.type === 'Tsx');
1834
+ const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
1835
+
1836
+ if (!inside_func) {
1837
+ if (this.type.label === 'continue') {
1838
+ throw new Error('`continue` statements are not allowed in components');
1839
+ }
1840
+ if (this.type.label === 'break') {
1841
+ throw new Error('`break` statements are not allowed in components');
1842
+ }
1843
+ }
1844
+
1845
+ if (inside_tsx) {
1846
+ this.exprAllowed = true;
1847
+
1848
+ while (true) {
1849
+ if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
1850
+ if (!this.#loose) {
1851
+ this.raise(
1852
+ this.start,
1853
+ `Unclosed tag '<tsx>'. Expected '</tsx>' before end of component.`,
1854
+ );
1855
+ } else {
1856
+ inside_tsx.unclosed = true;
1857
+ /** @type {AST.NodeWithLocation} */ (inside_tsx).loc.end = {
1858
+ .../** @type {AST.SourceLocation} */ (inside_tsx.openingElement.loc).end,
1859
+ };
1860
+ inside_tsx.end = inside_tsx.openingElement.end;
1861
+ }
1862
+ return;
1863
+ }
1864
+
1865
+ if (this.input.slice(this.pos, this.pos + 4) === '/tsx') {
1866
+ const after = this.input.charCodeAt(this.pos + 4);
1867
+ // Make sure it's </tsx> and not </tsx:...>
1868
+ if (after === 62 /* > */) {
1869
+ return;
1870
+ }
1871
+ }
1872
+
1873
+ if (this.type === tt.braceL) {
1874
+ const node = this.jsx_parseExpressionContainer();
1875
+ body.push(node);
1876
+ } else if (this.type === tstt.jsxTagStart) {
1877
+ // Parse JSX element
1878
+ const node = super.parseExpression();
1879
+ body.push(node);
1880
+ } else {
1881
+ const start = this.start;
1882
+ this.pos = start;
1883
+ let text = '';
1884
+
1885
+ while (this.pos < this.input.length) {
1886
+ const ch = this.input.charCodeAt(this.pos);
1887
+
1888
+ // Stop at opening tag, expression, or the component-closing brace
1889
+ if (ch === 60 || ch === 123 || ch === 125) {
1890
+ // < or { or }
1891
+ break;
1892
+ }
1893
+
1894
+ text += this.input[this.pos];
1895
+ this.pos++;
1896
+ }
1897
+
1898
+ if (text) {
1899
+ const node = /** @type {ESTreeJSX.JSXText} */ ({
1900
+ type: 'JSXText',
1901
+ value: text,
1902
+ raw: text,
1903
+ start,
1904
+ end: this.pos,
1905
+ });
1906
+ body.push(node);
1907
+ }
1908
+
1909
+ // Always call next() to ensure parser makes progress
1910
+ this.next();
1911
+ }
1912
+ }
1913
+ }
1914
+ if (inside_tsx_compat) {
1915
+ this.exprAllowed = true;
1916
+
1917
+ while (true) {
1918
+ if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
1919
+ if (!this.#loose) {
1920
+ this.raise(
1921
+ this.start,
1922
+ `Unclosed tag '<tsx:${inside_tsx_compat.kind}>'. Expected '</tsx:${inside_tsx_compat.kind}>' before end of component.`,
1923
+ );
1924
+ } else {
1925
+ inside_tsx_compat.unclosed = true;
1926
+ /** @type {AST.NodeWithLocation} */ (inside_tsx_compat).loc.end = {
1927
+ .../** @type {AST.SourceLocation} */ (inside_tsx_compat.openingElement.loc).end,
1928
+ };
1929
+ inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
1930
+ }
1931
+ return;
1932
+ }
1933
+
1934
+ if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
1935
+ return;
1936
+ }
1937
+
1938
+ if (this.type === tt.braceL) {
1939
+ const node = this.jsx_parseExpressionContainer();
1940
+ body.push(node);
1941
+ } else if (this.type === tstt.jsxTagStart) {
1942
+ // Parse JSX element
1943
+ const node = super.parseExpression();
1944
+ body.push(node);
1945
+ } else {
1946
+ const start = this.start;
1947
+ this.pos = start;
1948
+ let text = '';
1949
+
1950
+ while (this.pos < this.input.length) {
1951
+ const ch = this.input.charCodeAt(this.pos);
1952
+
1953
+ // Stop at opening tag, expression, or the component-closing brace
1954
+ if (ch === 60 || ch === 123 || ch === 125) {
1955
+ // < or { or }
1956
+ break;
1957
+ }
1958
+
1959
+ text += this.input[this.pos];
1960
+ this.pos++;
1961
+ }
1962
+
1963
+ if (text) {
1964
+ const node = /** @type {ESTreeJSX.JSXText} */ ({
1965
+ type: 'JSXText',
1966
+ value: text,
1967
+ raw: text,
1968
+ start,
1969
+ end: this.pos,
1970
+ });
1971
+ body.push(node);
1972
+ }
1973
+
1974
+ this.next();
1975
+ }
1976
+ }
1977
+ }
1978
+ if (this.type === tt.braceL) {
1979
+ const node = this.jsx_parseExpressionContainer();
1980
+ // Keep JSXEmptyExpression as-is (for prettier to handle comments)
1981
+ // but convert other expressions to Html/TSRXExpression/Text nodes
1982
+ if (node.expression.type !== 'JSXEmptyExpression') {
1983
+ /** @type {AST.TSRXExpression | AST.Html | AST.TextNode} */ (
1984
+ /** @type {unknown} */ (node)
1985
+ ).type = node.html ? 'Html' : node.text ? 'Text' : 'TSRXExpression';
1986
+ delete node.html;
1987
+ delete node.text;
1988
+ }
1989
+ body.push(node);
1990
+ } else if (this.type === tt.braceR) {
1991
+ // Leaving a component/template body. We may still be in TSX/JSX tokenization
1992
+ // context (e.g. after parsing markup), but the closing `}` is a JS token.
1993
+ // If we don't reset this here, the following `next()` can read EOF using
1994
+ // `jsx_readToken()` and throw "Unterminated JSX contents".
1995
+ while (this.curContext() === tstc.tc_expr) {
1996
+ this.context.pop();
1997
+ }
1998
+ return;
1999
+ } else if (this.type === tstt.jsxTagStart) {
2000
+ const startPos = this.start;
2001
+ const startLoc = this.startLoc;
2002
+ this.next();
2003
+ if (this.value === '/' || this.type === tt.slash) {
2004
+ // Consume '/'
2005
+ this.next();
2006
+
2007
+ const closingElement =
2008
+ /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
2009
+ this.jsx_parseClosingElementAt(startPos, startLoc)
2010
+ );
2011
+ this.exprAllowed = false;
2012
+
2013
+ // Validate that the closing tag matches the opening tag
2014
+ const currentElement = this.#path[this.#path.length - 1];
2015
+ if (
2016
+ !currentElement ||
2017
+ (currentElement.type !== 'Element' &&
2018
+ currentElement.type !== 'Tsx' &&
2019
+ currentElement.type !== 'TsxCompat')
2020
+ ) {
2021
+ this.raise(this.start, 'Unexpected closing tag');
2022
+ }
2023
+
2024
+ /** @type {string | null} */
2025
+ let openingTagName;
2026
+ /** @type {string | null} */
2027
+ let closingTagName;
2028
+
2029
+ if (currentElement.type === 'TsxCompat') {
2030
+ openingTagName = 'tsx:' + currentElement.kind;
2031
+ closingTagName =
2032
+ closingElement.name?.type === 'JSXNamespacedName'
2033
+ ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2034
+ : this.getElementName(closingElement.name);
2035
+ } else if (currentElement.type === 'Tsx') {
2036
+ openingTagName = 'tsx';
2037
+ closingTagName =
2038
+ closingElement.name?.type === 'JSXNamespacedName'
2039
+ ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2040
+ : this.getElementName(closingElement.name);
2041
+ } else {
2042
+ // Regular Element node
2043
+ openingTagName = this.getElementName(currentElement.id);
2044
+ closingTagName =
2045
+ closingElement.name?.type === 'JSXNamespacedName'
2046
+ ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2047
+ : this.getElementName(closingElement.name);
2048
+ }
2049
+
2050
+ if (openingTagName !== closingTagName) {
2051
+ if (!this.#loose) {
2052
+ this.raise(
2053
+ closingElement.start,
2054
+ `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
2055
+ );
2056
+ } else {
2057
+ // Loop through all unclosed elements on the stack
2058
+ while (this.#path.length > 0) {
2059
+ const elem = this.#path[this.#path.length - 1];
2060
+
2061
+ // Stop at non-Element boundaries (Component, etc.)
2062
+ if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
2063
+ break;
2064
+ }
2065
+
2066
+ const elemName =
2067
+ elem.type === 'TsxCompat'
2068
+ ? 'tsx:' + elem.kind
2069
+ : elem.type === 'Tsx'
2070
+ ? 'tsx'
2071
+ : this.getElementName(elem.id);
2072
+
2073
+ // Found matching opening tag
2074
+ if (elemName === closingTagName) {
2075
+ break;
2076
+ }
2077
+
2078
+ // Mark as unclosed and adjust location
2079
+ elem.unclosed = true;
2080
+ /** @type {AST.NodeWithLocation} */ (elem).loc.end = {
2081
+ .../** @type {AST.SourceLocation} */ (elem.openingElement.loc).end,
2082
+ };
2083
+ elem.end = elem.openingElement.end;
2084
+
2085
+ this.#path.pop(); // Remove from stack
2086
+ }
2087
+ }
2088
+ }
2089
+
2090
+ const elementToClose = this.#path[this.#path.length - 1];
2091
+ if (elementToClose && elementToClose.type === 'Element') {
2092
+ const elementToCloseName = this.getElementName(
2093
+ /** @type {AST.Element} */ (elementToClose).id,
2094
+ );
2095
+ if (elementToCloseName === closingTagName) {
2096
+ /** @type {AST.Element} */ (elementToClose).closingElement = closingElement;
2097
+ }
2098
+ }
2099
+
2100
+ this.#path.pop();
2101
+ skipWhitespace(this);
2102
+ return;
2103
+ }
2104
+ const node = this.parseElement();
2105
+ if (node !== null) {
2106
+ body.push(node);
2107
+ }
2108
+ } else {
2109
+ skipWhitespace(this);
2110
+ const node = this.parseStatement(null);
2111
+ body.push(node);
2112
+
2113
+ // Ensure we're not in JSX context before recursing
2114
+ // This is important when elements are parsed at statement level
2115
+ if (this.curContext() === tstc.tc_expr) {
2116
+ this.context.pop();
2117
+ }
2118
+ }
2119
+
2120
+ this.parseTemplateBody(body);
2121
+ }
2122
+
2123
+ /**
2124
+ * @type {Parse.Parser['parseStatement']}
2125
+ */
2126
+ parseStatement(context, topLevel, exports) {
2127
+ if (
2128
+ context !== 'for' &&
2129
+ context !== 'if' &&
2130
+ this.context.at(-1) === b_stat &&
2131
+ this.type === tt.braceL &&
2132
+ this.context.some((c) => c === tstc.tc_expr)
2133
+ ) {
2134
+ const node = this.jsx_parseExpressionContainer();
2135
+ // Keep JSXEmptyExpression as-is (don't convert to TSRXExpression/Text/Html)
2136
+ if (node.expression.type !== 'JSXEmptyExpression') {
2137
+ /** @type {AST.TSRXExpression | AST.Html | AST.TextNode} */ (
2138
+ /** @type {unknown} */ (node)
2139
+ ).type = node.html ? 'Html' : node.text ? 'Text' : 'TSRXExpression';
2140
+ delete node.html;
2141
+ delete node.text;
2142
+ }
2143
+
2144
+ return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2145
+ /** @type {unknown} */ (node)
2146
+ );
2147
+ }
2148
+
2149
+ if (this.value === '#server') {
2150
+ // Peek ahead to see if this is a server block (#server { ... }) vs
2151
+ // a server identifier expression (#server.fn(), #server.fn().then())
2152
+ let peek_pos = this.end;
2153
+ while (peek_pos < this.input.length && /\s/.test(this.input[peek_pos])) peek_pos++;
2154
+ if (peek_pos < this.input.length && this.input.charCodeAt(peek_pos) === 123) {
2155
+ // Next non-whitespace character is '{' — parse as server block
2156
+ return this.parseServerBlock();
2157
+ }
2158
+ // Otherwise fall through to parse as expression statement (e.g., #server.fn().then(...))
2159
+ }
2160
+
2161
+ if (this.value === 'component') {
2162
+ this.awaitPos = 0;
2163
+ return this.parseComponent({ requireName: true, declareName: true });
2164
+ }
2165
+
2166
+ if (this.type === tstt.jsxTagStart) {
2167
+ this.next();
2168
+ if (this.value === '/') {
2169
+ this.unexpected();
2170
+ }
2171
+ const node = this.parseElement();
2172
+
2173
+ if (!node) {
2174
+ this.unexpected();
2175
+ }
2176
+ return node;
2177
+ }
2178
+
2179
+ // &[ or &{ at statement level — lazy destructuring assignment
2180
+ // e.g., &[data] = track(0); or &{x, y} = obj;
2181
+ if (this.type === tt.bitwiseAND) {
2182
+ const charAfterAmp = this.input.charCodeAt(this.end);
2183
+ if (charAfterAmp === 123 || charAfterAmp === 91) {
2184
+ const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
2185
+ const assign_node = /** @type {AST.AssignmentExpression} */ (this.startNode());
2186
+ this.next(); // consume &
2187
+ // Parse the left-hand side (array or object expression)
2188
+ const left = /** @type {AST.ArrayPattern | AST.ObjectPattern} */ (
2189
+ /** @type {unknown} */ (this.parseExprAtom())
2190
+ );
2191
+ // Convert expression to destructuring pattern
2192
+ this.toAssignable(left, false);
2193
+ left.lazy = true;
2194
+ // Expect = operator
2195
+ this.expect(tt.eq);
2196
+ // Parse the right-hand side
2197
+ assign_node.operator = '=';
2198
+ assign_node.left = left;
2199
+ assign_node.right = /** @type {AST.Expression} */ (this.parseMaybeAssign());
2200
+ node.expression = /** @type {AST.AssignmentExpression} */ (
2201
+ this.finishNode(assign_node, 'AssignmentExpression')
2202
+ );
2203
+ this.semicolon();
2204
+ return /** @type {AST.ExpressionStatement} */ (
2205
+ this.finishNode(node, 'ExpressionStatement')
2206
+ );
2207
+ }
2208
+ }
2209
+
2210
+ return super.parseStatement(context, topLevel, exports);
2211
+ }
2212
+
2213
+ /**
2214
+ * @type {Parse.Parser['parseBlock']}
2215
+ */
2216
+ parseBlock(createNewLexicalScope, node, exitStrict) {
2217
+ const parent = this.#path.at(-1);
2218
+
2219
+ if (parent?.type === 'Component' || parent?.type === 'Element') {
2220
+ if (createNewLexicalScope === void 0) createNewLexicalScope = true;
2221
+ if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
2222
+
2223
+ node.body = [];
2224
+ this.expect(tt.braceL);
2225
+ if (createNewLexicalScope) {
2226
+ this.enterScope(0);
2227
+ }
2228
+ this.parseTemplateBody(node.body);
2229
+
2230
+ if (exitStrict) {
2231
+ this.strict = false;
2232
+ }
2233
+ this.exprAllowed = true;
2234
+
2235
+ this.next();
2236
+ if (createNewLexicalScope) {
2237
+ this.exitScope();
2238
+ }
2239
+ return this.finishNode(node, 'BlockStatement');
2240
+ }
2241
+
2242
+ return super.parseBlock(createNewLexicalScope, node, exitStrict);
2243
+ }
2244
+ }
2245
+
2246
+ return /** @type {Parse.ParserConstructor} */ (TSRXParser);
2247
+ };
2248
+ }