@tsrx/core 0.0.1 → 0.0.3
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/LICENSE +21 -0
- package/README.md +249 -0
- package/package.json +18 -17
- package/src/index.js +56 -58
- package/src/parse/index.js +1 -1
- package/src/parse/parse-module.js +18 -0
- package/src/plugin.js +2248 -0
- package/src/scope.js +1 -1
- package/src/source-map-utils.js +19 -19
- package/types/index.d.ts +20 -20
- package/types/parse.d.ts +3 -3
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 ? '>' : '}') +
|
|
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
|
+
}
|