ripple 0.3.13 → 0.3.15

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/package.json +5 -30
  3. package/src/runtime/array.js +38 -38
  4. package/src/runtime/create-subscriber.js +2 -2
  5. package/src/runtime/internal/client/bindings.js +4 -6
  6. package/src/runtime/internal/client/events.js +8 -3
  7. package/src/runtime/internal/client/hmr.js +5 -17
  8. package/src/runtime/internal/client/runtime.js +1 -0
  9. package/src/runtime/internal/server/blocks.js +7 -9
  10. package/src/runtime/internal/server/index.js +14 -22
  11. package/src/runtime/media-query.js +34 -33
  12. package/src/runtime/object.js +7 -10
  13. package/src/runtime/proxy.js +2 -3
  14. package/src/runtime/reactive-value.js +23 -21
  15. package/src/utils/ast.js +1 -1
  16. package/src/utils/attributes.js +43 -0
  17. package/src/utils/builders.js +2 -2
  18. package/tests/client/basic/basic.components.test.rsrx +103 -1
  19. package/tests/client/basic/basic.errors.test.rsrx +1 -1
  20. package/tests/client/basic/basic.styling.test.rsrx +1 -1
  21. package/tests/client/compiler/compiler.assignments.test.rsrx +1 -1
  22. package/tests/client/compiler/compiler.attributes.test.rsrx +1 -1
  23. package/tests/client/compiler/compiler.basic.test.rsrx +51 -14
  24. package/tests/client/compiler/compiler.tracked-access.test.rsrx +1 -1
  25. package/tests/client/compiler/compiler.try-in-function.test.rsrx +1 -1
  26. package/tests/client/compiler/compiler.typescript.test.rsrx +1 -1
  27. package/tests/client/css/global-additional-cases.test.rsrx +1 -1
  28. package/tests/client/css/global-advanced-selectors.test.rsrx +1 -1
  29. package/tests/client/css/global-at-rules.test.rsrx +1 -1
  30. package/tests/client/css/global-basic.test.rsrx +1 -1
  31. package/tests/client/css/global-classes-ids.test.rsrx +1 -1
  32. package/tests/client/css/global-combinators.test.rsrx +1 -1
  33. package/tests/client/css/global-complex-nesting.test.rsrx +1 -1
  34. package/tests/client/css/global-edge-cases.test.rsrx +1 -1
  35. package/tests/client/css/global-keyframes.test.rsrx +1 -1
  36. package/tests/client/css/global-nested.test.rsrx +1 -1
  37. package/tests/client/css/global-pseudo.test.rsrx +1 -1
  38. package/tests/client/css/global-scoping.test.rsrx +1 -1
  39. package/tests/client/css/style-identifier.test.rsrx +1 -1
  40. package/tests/client/return.test.rsrx +1 -1
  41. package/tests/hydration/build-components.js +1 -1
  42. package/tests/server/basic.components.test.rsrx +114 -0
  43. package/tests/server/compiler.test.rsrx +38 -1
  44. package/tests/server/style-identifier.test.rsrx +1 -1
  45. package/tests/setup-server.js +1 -1
  46. package/tests/utils/compiler-compat-config.test.js +1 -1
  47. package/types/index.d.ts +1 -1
  48. package/src/compiler/comment-utils.js +0 -91
  49. package/src/compiler/errors.js +0 -77
  50. package/src/compiler/identifier-utils.js +0 -80
  51. package/src/compiler/index.d.ts +0 -127
  52. package/src/compiler/index.js +0 -89
  53. package/src/compiler/phases/1-parse/index.js +0 -3007
  54. package/src/compiler/phases/1-parse/style.js +0 -704
  55. package/src/compiler/phases/2-analyze/css-analyze.js +0 -160
  56. package/src/compiler/phases/2-analyze/index.js +0 -2208
  57. package/src/compiler/phases/2-analyze/prune.js +0 -1131
  58. package/src/compiler/phases/2-analyze/validation.js +0 -168
  59. package/src/compiler/phases/3-transform/client/index.js +0 -5264
  60. package/src/compiler/phases/3-transform/segments.js +0 -2125
  61. package/src/compiler/phases/3-transform/server/index.js +0 -1749
  62. package/src/compiler/phases/3-transform/stylesheet.js +0 -545
  63. package/src/compiler/scope.js +0 -476
  64. package/src/compiler/source-map-utils.js +0 -358
  65. package/src/compiler/types/acorn.d.ts +0 -11
  66. package/src/compiler/types/estree-jsx.d.ts +0 -11
  67. package/src/compiler/types/estree.d.ts +0 -11
  68. package/src/compiler/types/index.d.ts +0 -1411
  69. package/src/compiler/types/parse.d.ts +0 -1723
  70. package/src/compiler/utils.js +0 -1258
@@ -1,3007 +0,0 @@
1
- /**
2
- @import * as AST from 'estree'
3
- @import * as ESTreeJSX from 'estree-jsx'
4
- @import { Parse } from '#parser'
5
- @import { RipplePluginConfig } from '#compiler';
6
- @import { ParseOptions, RippleCompileError } from 'ripple/compiler'
7
- */
8
-
9
- import * as acorn from 'acorn';
10
- import { tsPlugin } from '@sveltejs/acorn-typescript';
11
- import { parse_style } from './style.js';
12
- import { walk } from 'zimmerframe';
13
- import { regex_newline_characters } from '../../../utils/patterns.js';
14
- import { error } from '../../errors.js';
15
-
16
- /**
17
- * @typedef {(BaseParser: typeof acorn.Parser) => typeof acorn.Parser} AcornPlugin
18
- */
19
-
20
- const parser = /** @type {Parse.ParserConstructor} */ (
21
- /** @type {unknown} */ (
22
- acorn.Parser.extend(
23
- tsPlugin({ jsx: true }),
24
- /** @type {AcornPlugin} */ (/** @type {unknown} */ (RipplePlugin())),
25
- )
26
- )
27
- );
28
-
29
- /** @type {Parse.BindingType} */
30
- const BINDING_TYPES = {
31
- BIND_NONE: 0, // Not a binding
32
- BIND_VAR: 1, // Var-style binding
33
- BIND_LEXICAL: 2, // Let- or const-style binding
34
- BIND_FUNCTION: 3, // Function declaration
35
- BIND_SIMPLE_CATCH: 4, // Simple (identifier pattern) catch binding
36
- BIND_OUTSIDE: 5, // Special case for function names as bound inside the function
37
- };
38
-
39
- /**
40
- * @this {Parse.DestructuringErrors}
41
- * @returns {Parse.DestructuringErrors}
42
- */
43
- function DestructuringErrors() {
44
- if (!(this instanceof DestructuringErrors)) {
45
- throw new TypeError("'DestructuringErrors' must be invoked with 'new'");
46
- }
47
- this.shorthandAssign = -1;
48
- this.trailingComma = -1;
49
- this.parenthesizedAssign = -1;
50
- this.parenthesizedBind = -1;
51
- this.doubleProto = -1;
52
- return this;
53
- }
54
-
55
- /**
56
- * Convert JSX node types to regular JavaScript node types
57
- * @param {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXMemberExpression | AST.Node} node - The JSX node to convert
58
- * @returns {AST.Identifier | AST.MemberExpression | AST.Node} The converted node
59
- */
60
- function convert_from_jsx(node) {
61
- /** @type {AST.Identifier | AST.MemberExpression | AST.Node} */
62
- let converted_node;
63
- if (node.type === 'JSXIdentifier') {
64
- converted_node = /** @type {AST.Identifier} */ (/** @type {unknown} */ (node));
65
- converted_node.type = 'Identifier';
66
- } else if (node.type === 'JSXMemberExpression') {
67
- converted_node = /** @type {AST.MemberExpression} */ (/** @type {unknown} */ (node));
68
- converted_node.type = 'MemberExpression';
69
- converted_node.object = /** @type {AST.Identifier | AST.MemberExpression} */ (
70
- convert_from_jsx(converted_node.object)
71
- );
72
- converted_node.property = /** @type {AST.Identifier} */ (
73
- convert_from_jsx(converted_node.property)
74
- );
75
- } else {
76
- converted_node = node;
77
- }
78
- return converted_node;
79
- }
80
-
81
- const regex_whitespace_only = /\s/;
82
-
83
- /**
84
- * Skip whitespace characters without skipping comments.
85
- * This is needed because Acorn's skipSpace() also skips comments, which breaks
86
- * parsing in certain contexts. Updates parser position and line tracking.
87
- * @param {Parse.Parser} parser
88
- */
89
- function skipWhitespace(parser) {
90
- const originalStart = parser.start;
91
- /** @type {acorn.Position | undefined} */
92
- let lineInfo;
93
- while (
94
- parser.start < parser.input.length &&
95
- regex_whitespace_only.test(parser.input[parser.start])
96
- ) {
97
- parser.start++;
98
- }
99
- // Update line tracking if whitespace was skipped
100
- if (parser.start !== originalStart) {
101
- lineInfo = acorn.getLineInfo(parser.input, parser.start);
102
- // Only update curLine/lineStart if the tokenizer hasn't already
103
- // advanced past this position. When parser.pos > parser.start,
104
- // acorn's internal skipSpace() has already processed comments and
105
- // whitespace beyond where we stopped, so curLine/lineStart already
106
- // reflect a later (correct) position that we must not overwrite.
107
- if (parser.pos <= parser.start) {
108
- parser.curLine = lineInfo.line;
109
- parser.lineStart = parser.start - lineInfo.column;
110
- }
111
- }
112
-
113
- // After skipping whitespace, update startLoc to reflect our actual position
114
- // so the next node's start location is correct
115
- parser.startLoc = lineInfo || acorn.getLineInfo(parser.input, parser.start);
116
- }
117
-
118
- /**
119
- * @param {AST.Node | null | undefined} node
120
- * @returns {boolean}
121
- */
122
- function isWhitespaceTextNode(node) {
123
- if (!node || node.type !== 'Text') {
124
- return false;
125
- }
126
-
127
- const expr = node.expression;
128
- if (expr && expr.type === 'Literal' && typeof expr.value === 'string') {
129
- return /^\s*$/.test(expr.value);
130
- }
131
- return false;
132
- }
133
-
134
- /**
135
- * @param {AST.Element} element
136
- * @param {ESTreeJSX.JSXOpeningElement} open
137
- */
138
-
139
- /**
140
- * Acorn parser plugin for Ripple syntax extensions
141
- * @param {RipplePluginConfig} [config] - Plugin configuration
142
- * @returns {(Parser: Parse.ParserConstructor) => Parse.ParserConstructor} Parser extension function
143
- */
144
- function RipplePlugin(config) {
145
- return (/** @type {Parse.ParserConstructor} */ Parser) => {
146
- const original = acorn.Parser.prototype;
147
- const tt = Parser.tokTypes || acorn.tokTypes;
148
- const tc = Parser.tokContexts || acorn.tokContexts;
149
- // Some parser constructors (e.g. via TS plugins) expose `tokContexts` without `b_stat`.
150
- // If we push an undefined context, Acorn's tokenizer will later crash reading `.override`.
151
- const b_stat = tc.b_stat || acorn.tokContexts.b_stat;
152
- const tstt = Parser.acornTypeScript.tokTypes;
153
- const tstc = Parser.acornTypeScript.tokContexts;
154
-
155
- class RippleParser extends Parser {
156
- /** @type {AST.Node[]} */
157
- #path = [];
158
- #commentContextId = 0;
159
- #loose = false;
160
- /** @type {RippleCompileError[] | undefined} */
161
- #errors = undefined;
162
- /** @type {string | null} */
163
- #filename = null;
164
-
165
- /**
166
- * @param {Parse.Options} options
167
- * @param {string} input
168
- */
169
- constructor(options, input) {
170
- super(options, input);
171
- this.#loose = options?.rippleOptions.loose === true;
172
- this.#errors = options?.rippleOptions.errors;
173
- this.#filename = options?.rippleOptions.filename || null;
174
- }
175
-
176
- /**
177
- * @param {number} position
178
- * @param {string} message
179
- */
180
- #report_recoverable_error(position, message) {
181
- const start = Math.max(0, Math.min(position, this.input.length));
182
- const end = Math.min(this.input.length, start + 1);
183
- const start_loc = acorn.getLineInfo(this.input, start);
184
- const end_loc = acorn.getLineInfo(this.input, end);
185
-
186
- error(
187
- message,
188
- this.#filename,
189
- /** @type {AST.NodeWithLocation} */ ({
190
- start,
191
- end,
192
- loc: {
193
- start: start_loc,
194
- end: end_loc,
195
- },
196
- }),
197
- this.#loose ? this.#errors : undefined,
198
- );
199
- }
200
-
201
- /**
202
- * In loose mode, keep parsing after duplicate declaration diagnostics so
203
- * editor tooling can continue producing AST and mappings.
204
- * @param {number} position
205
- * @param {string | { message?: string }} message
206
- */
207
- raiseRecoverable(position, message) {
208
- const error_message =
209
- typeof message === 'string'
210
- ? message
211
- : typeof message?.message === 'string'
212
- ? message.message
213
- : String(message);
214
-
215
- if (error_message.includes('has already been declared')) {
216
- this.#report_recoverable_error(position, error_message);
217
- return;
218
- }
219
-
220
- return super.raiseRecoverable(position, error_message);
221
- }
222
-
223
- /**
224
- * Override to allow single-parameter generic arrow functions without trailing comma.
225
- * By default, @sveltejs/acorn-typescript throws an error for `<T>() => {}` when JSX is enabled
226
- * because it can't disambiguate from JSX. However, the parser still parses it correctly
227
- * using tryParse - it just throws afterwards. By overriding this to do nothing, we allow
228
- * the valid parse to succeed.
229
- * @param {AST.TSTypeParameterDeclaration} node
230
- */
231
- reportReservedArrowTypeParam(node) {
232
- // Allow <T>() => {} syntax without requiring trailing comma
233
- if (this.#loose && node.params.length === 1 && node.extra?.trailingComma === undefined) {
234
- error(
235
- 'This syntax is reserved in files with the .mts or .cts extension. Add a trailing comma, as in `<T,>() => ...`.',
236
- this.#filename,
237
- node,
238
- this.#errors,
239
- );
240
- }
241
- }
242
-
243
- /**
244
- * Override to allow `readonly` type modifier on any type in loose mode.
245
- * By default, @sveltejs/acorn-typescript throws an error for `readonly { ... }`
246
- * because TypeScript only permits `readonly` on array and tuple types.
247
- * Suppress the error in the strict mode as ts is compiled away.
248
- * @param {AST.TSTypeOperator} node
249
- */
250
- tsCheckTypeAnnotationForReadOnly(node) {
251
- const typeAnnotation = /** @type {AST.TypeNode} */ (node.typeAnnotation);
252
- if (typeAnnotation.type === 'TSTupleType' || typeAnnotation.type === 'TSArrayType') {
253
- // Valid readonly usage, no error needed
254
- return;
255
- }
256
-
257
- if (this.#loose) {
258
- error(
259
- "'readonly' type modifier is only permitted on array and tuple literal types.",
260
- this.#filename,
261
- typeAnnotation,
262
- this.#errors,
263
- );
264
- }
265
- }
266
-
267
- /**
268
- * Override parseProperty to support component methods in object literals.
269
- * Handles syntax like `{ component something() { <div /> } }`
270
- * Also supports computed names: `{ component ['something']() { <div /> } }`
271
- * @type {Parse.Parser['parseProperty']}
272
- */
273
- parseProperty(isPattern, refDestructuringErrors) {
274
- // Check if this is a component method: component name( ... ) { ... }
275
- if (!isPattern && this.type === tt.name && this.value === 'component') {
276
- // Look ahead to see if this is "component identifier(", "component identifier<", "component [", or "component 'string'"
277
- const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
278
- if (lookahead) {
279
- // This is a component method definition
280
- const prop = /** @type {AST.Property} */ (this.startNode());
281
- const isComputed = lookahead[0].trim().startsWith('[');
282
- const isStringLiteral = /^['"]/.test(lookahead[0].trim());
283
-
284
- if (isComputed) {
285
- // For computed names, consume 'component'
286
- // parse the key, then parse component without name
287
- this.next(); // consume 'component'
288
- this.next(); // consume '['
289
- prop.key = this.parseExpression();
290
- this.expect(tt.bracketR);
291
- prop.computed = true;
292
-
293
- // Parse component without name (skipName: true)
294
- const component_node = this.parseComponent({ skipName: true });
295
- /** @type {AST.RippleProperty} */ (prop).value = component_node;
296
- } else if (isStringLiteral) {
297
- // For string literal names, consume 'component'
298
- // parse the string key, then parse component without name
299
- this.next(); // consume 'component'
300
- prop.key = /** @type {AST.Literal} */ (this.parseExprAtom());
301
- prop.computed = false;
302
-
303
- // Parse component without name (skipName: true)
304
- const component_node = this.parseComponent({ skipName: true });
305
- /** @type {AST.RippleProperty} */ (prop).value = component_node;
306
- } else {
307
- const component_node = this.parseComponent({ requireName: true });
308
-
309
- prop.key = /** @type {AST.Identifier} */ (component_node.id);
310
- /** @type {AST.RippleProperty} */ (prop).value = component_node;
311
- prop.computed = false;
312
- }
313
-
314
- prop.shorthand = false;
315
- prop.method = true;
316
- prop.kind = 'init';
317
-
318
- return this.finishNode(prop, 'Property');
319
- }
320
- }
321
-
322
- return super.parseProperty(isPattern, refDestructuringErrors);
323
- }
324
-
325
- /**
326
- * Override parseClassElement to support component methods in classes.
327
- * Handles syntax like `class Foo { component something() { <div /> } }`
328
- * Also supports computed names: `class Foo { component ['something']() { <div /> } }`
329
- * @type {Parse.Parser['parseClassElement']}
330
- */
331
- parseClassElement(constructorAllowsSuper) {
332
- // Check if this is a component method: component name( ... ) { ... }
333
- if (this.type === tt.name && this.value === 'component') {
334
- // Look ahead to see if this is "component identifier(",
335
- // "component identifier<", "component [", or "component 'string'"
336
- const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
337
- if (lookahead) {
338
- // This is a component method definition
339
- const node = /** @type {AST.MethodDefinition} */ (this.startNode());
340
- const isComputed = lookahead[0].trim().startsWith('[');
341
- const isStringLiteral = /^['"]/.test(lookahead[0].trim());
342
-
343
- if (isComputed) {
344
- // For computed names, consume 'component'
345
- // parse the key, then parse component without name
346
- this.next(); // consume 'component'
347
- this.next(); // consume '['
348
- node.key = this.parseExpression();
349
- this.expect(tt.bracketR);
350
- node.computed = true;
351
-
352
- // Parse component without name (skipName: true)
353
- const component_node = this.parseComponent({ skipName: true });
354
- /** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
355
- } else if (isStringLiteral) {
356
- // For string literal names, consume 'component'
357
- // parse the string key, then parse component without name
358
- this.next(); // consume 'component'
359
- node.key = /** @type {AST.Literal} */ (this.parseExprAtom());
360
- node.computed = false;
361
-
362
- // Parse component without name (skipName: true)
363
- const component_node = this.parseComponent({ skipName: true });
364
- /** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
365
- } else {
366
- // Use parseComponent which handles consuming 'component', parsing name, params, and body
367
- const component_node = this.parseComponent({ requireName: true });
368
-
369
- node.key = /** @type {AST.Identifier} */ (component_node.id);
370
- /** @type {AST.RippleMethodDefinition} */ (node).value = component_node;
371
- node.computed = false;
372
- }
373
-
374
- node.static = false;
375
- node.kind = 'method';
376
-
377
- return this.finishNode(node, 'MethodDefinition');
378
- }
379
- }
380
-
381
- return super.parseClassElement(constructorAllowsSuper);
382
- }
383
-
384
- /**
385
- * Override parsePropertyValue to support TypeScript generic methods in object literals.
386
- * By default, acorn-typescript doesn't handle `{ method<T>() {} }` syntax.
387
- * This override checks for type parameters before parsing the method.
388
- * @type {Parse.Parser['parsePropertyValue']}
389
- */
390
- parsePropertyValue(
391
- prop,
392
- isPattern,
393
- isGenerator,
394
- isAsync,
395
- startPos,
396
- startLoc,
397
- refDestructuringErrors,
398
- containsEsc,
399
- ) {
400
- // Check if this is a method with type parameters (e.g., `method<T>() {}`)
401
- // We need to parse type parameters before the parentheses
402
- if (
403
- !isPattern &&
404
- !isGenerator &&
405
- !isAsync &&
406
- this.type === tt.relational &&
407
- this.value === '<'
408
- ) {
409
- // Try to parse type parameters
410
- const typeParameters = this.tsTryParseTypeParameters();
411
- if (typeParameters && this.type === tt.parenL) {
412
- // This is a method with type parameters
413
- /** @type {AST.Property} */ (prop).method = true;
414
- /** @type {AST.Property} */ (prop).kind = 'init';
415
- /** @type {AST.Property} */ (prop).value = this.parseMethod(false, false);
416
- /** @type {AST.FunctionExpression} */ (
417
- /** @type {AST.Property} */ (prop).value
418
- ).typeParameters = typeParameters;
419
- return;
420
- }
421
- }
422
-
423
- return super.parsePropertyValue(
424
- prop,
425
- isPattern,
426
- isGenerator,
427
- isAsync,
428
- startPos,
429
- startLoc,
430
- refDestructuringErrors,
431
- containsEsc,
432
- );
433
- }
434
-
435
- /**
436
- * Acorn expects `this.context` to always contain at least one tokContext.
437
- * Some of our template/JSX escape hatches can pop contexts aggressively;
438
- * if the stack becomes empty, Acorn will crash reading `curContext().override`.
439
- * @type {Parse.Parser['nextToken']}
440
- */
441
- nextToken() {
442
- while (this.context.length && this.context[this.context.length - 1] == null) {
443
- this.context.pop();
444
- }
445
- if (this.context.length === 0) {
446
- this.context.push(b_stat);
447
- }
448
- return super.nextToken();
449
- }
450
-
451
- /**
452
- * @returns {Parse.CommentMetaData | null}
453
- */
454
- #createCommentMetadata() {
455
- if (this.#path.length === 0) {
456
- return null;
457
- }
458
-
459
- const container = this.#path[this.#path.length - 1];
460
- if (!container || container.type !== 'Element') {
461
- return null;
462
- }
463
-
464
- const children = Array.isArray(container.children) ? container.children : [];
465
- const hasMeaningfulChildren = children.some(
466
- (child) => child && !isWhitespaceTextNode(child),
467
- );
468
-
469
- if (hasMeaningfulChildren) {
470
- return null;
471
- }
472
-
473
- container.metadata ??= { path: [] };
474
- if (container.metadata.commentContainerId === undefined) {
475
- container.metadata.commentContainerId = ++this.#commentContextId;
476
- }
477
-
478
- return /*** @type {Parse.CommentMetaData} */ ({
479
- containerId: container.metadata.commentContainerId,
480
- childIndex: children.length,
481
- beforeMeaningfulChild: !hasMeaningfulChildren,
482
- });
483
- }
484
-
485
- /**
486
- * Helper method to get the element name from a JSX identifier or member expression
487
- * @type {Parse.Parser['getElementName']}
488
- */
489
- getElementName(node) {
490
- if (!node) return null;
491
- if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
492
- return node.name;
493
- } else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
494
- // For components like <Foo.Bar>, return "Foo.Bar"
495
- return this.getElementName(node.object) + '.' + this.getElementName(node.property);
496
- }
497
- return null;
498
- }
499
-
500
- /**
501
- * Get token from character code - handles Ripple-specific tokens
502
- * @type {Parse.Parser['getTokenFromCode']}
503
- */
504
- getTokenFromCode(code) {
505
- if (code === 60) {
506
- // < character
507
- const inComponent = this.#path.findLast((n) => n.type === 'Component');
508
- /** @type {number | null} */
509
- let prevNonWhitespaceChar = null;
510
-
511
- // Check if this could be TypeScript generics instead of JSX
512
- // TypeScript generics appear after: identifiers, closing parens, 'new' keyword
513
- // For example: Array<T>, func<T>(), new Map<K,V>(), method<T>()
514
- // This check applies everywhere, not just inside components
515
-
516
- // Look back to see what precedes the <
517
- let lookback = this.pos - 1;
518
-
519
- // Skip whitespace backwards
520
- while (lookback >= 0) {
521
- const ch = this.input.charCodeAt(lookback);
522
- if (ch !== 32 && ch !== 9) break; // not space or tab
523
- lookback--;
524
- }
525
-
526
- // Check what character/token precedes the <
527
- if (lookback >= 0) {
528
- const prevChar = this.input.charCodeAt(lookback);
529
- prevNonWhitespaceChar = prevChar;
530
-
531
- // If preceded by identifier character (letter, digit, _, $) or closing paren,
532
- // this is likely TypeScript generics, not JSX
533
- const isIdentifierChar =
534
- (prevChar >= 65 && prevChar <= 90) || // A-Z
535
- (prevChar >= 97 && prevChar <= 122) || // a-z
536
- (prevChar >= 48 && prevChar <= 57) || // 0-9
537
- prevChar === 95 || // _
538
- prevChar === 36 || // $
539
- prevChar === 41; // )
540
-
541
- if (isIdentifierChar) {
542
- return super.getTokenFromCode(code);
543
- }
544
- }
545
-
546
- // Support parsing standalone template markup at the top-level (outside `component`)
547
- // for tooling like Prettier, e.g.:
548
- // <Something>...</Something>\n\n<Child />
549
- // <head><style>...</style></head>
550
- // We only do this when '<' is in a tag-like position.
551
- const nextChar =
552
- this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
553
- const isWhitespaceAfterLt =
554
- nextChar === 32 || nextChar === 9 || nextChar === 10 || nextChar === 13;
555
- const isTagLikeAfterLt =
556
- !isWhitespaceAfterLt &&
557
- (nextChar === 47 || // '/'
558
- (nextChar >= 65 && nextChar <= 90) || // A-Z
559
- (nextChar >= 97 && nextChar <= 122)); // a-z
560
- const prevAllowsTagStart =
561
- prevNonWhitespaceChar === null ||
562
- prevNonWhitespaceChar === 10 || // '\n'
563
- prevNonWhitespaceChar === 13 || // '\r'
564
- prevNonWhitespaceChar === 123 || // '{'
565
- prevNonWhitespaceChar === 125 || // '}'
566
- prevNonWhitespaceChar === 62; // '>'
567
-
568
- if (!inComponent && prevAllowsTagStart && isTagLikeAfterLt) {
569
- ++this.pos;
570
- return this.finishToken(tstt.jsxTagStart);
571
- }
572
-
573
- if (inComponent) {
574
- // Inside component template bodies, allow adjacent tags without requiring
575
- // a newline/indentation before the next '<'. This is important for inputs
576
- // like `<div />` and `</div><style>...</style>` which Prettier formats.
577
- if (prevNonWhitespaceChar === 123 /* '{' */ || prevNonWhitespaceChar === 62 /* '>' */) {
578
- if (!isWhitespaceAfterLt) {
579
- ++this.pos;
580
- return this.finishToken(tstt.jsxTagStart);
581
- }
582
- }
583
-
584
- // Check if we're inside a nested function (arrow function, function expression, etc.)
585
- // We need to distinguish between being inside a function vs just being in nested scopes
586
- // (like for loops, if blocks, JSX elements, etc.)
587
- const nestedFunctionContext = this.context.some((ctx) => ctx.token === 'function');
588
-
589
- // Inside nested functions, treat < as relational/generic operator
590
- // BUT: if the < is followed by /, it's a closing JSX tag, not a less-than operator
591
- const nextChar =
592
- this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
593
- const isClosingTag = nextChar === 47; // '/'
594
-
595
- if (nestedFunctionContext && !isClosingTag) {
596
- // Inside function - treat as TypeScript generic, not JSX
597
- ++this.pos;
598
- return this.finishToken(tt.relational, '<');
599
- }
600
-
601
- // Check if everything before this position on the current line is whitespace
602
- let lineStart = this.pos - 1;
603
- while (
604
- lineStart >= 0 &&
605
- this.input.charCodeAt(lineStart) !== 10 &&
606
- this.input.charCodeAt(lineStart) !== 13
607
- ) {
608
- lineStart--;
609
- }
610
- lineStart++; // Move past the newline character
611
-
612
- // Check if all characters from line start to current position are whitespace
613
- let allWhitespace = true;
614
- for (let i = lineStart; i < this.pos; i++) {
615
- const ch = this.input.charCodeAt(i);
616
- if (ch !== 32 && ch !== 9) {
617
- allWhitespace = false;
618
- break;
619
- }
620
- }
621
-
622
- // Check if the character after < is not whitespace
623
- if (allWhitespace && this.pos + 1 < this.input.length) {
624
- const nextChar = this.input.charCodeAt(this.pos + 1);
625
- if (nextChar !== 32 && nextChar !== 9 && nextChar !== 10 && nextChar !== 13) {
626
- ++this.pos;
627
- return this.finishToken(tstt.jsxTagStart);
628
- }
629
- }
630
- }
631
- }
632
-
633
- if (code === 35) {
634
- // # character
635
- if (this.pos + 1 < this.input.length) {
636
- /** @param {string} value */
637
- const startsWith = (value) =>
638
- this.input.slice(this.pos, this.pos + value.length) === value;
639
- /** @param {number} length */
640
- const char_after = (length) =>
641
- this.pos + length < this.input.length ? this.input.charCodeAt(this.pos + length) : -1;
642
- /** @param {number} ch */
643
- const is_ripple_delimiter = (ch) =>
644
- ch === 40 || // (
645
- ch === 41 || // )
646
- ch === 60 || // <
647
- ch === 46 || // .
648
- ch === 44 || // ,
649
- ch === 59 || // ;
650
- ch === 91 || // [
651
- ch === 93 || // ]
652
- ch === 123 || // {
653
- ch === 125 || // }
654
- ch === 32 || // space
655
- ch === 9 || // tab
656
- ch === 10 || // newline
657
- ch === 13 || // carriage return
658
- ch === -1; // EOF
659
-
660
- if (startsWith('#server') && is_ripple_delimiter(char_after(7))) {
661
- this.pos += 7;
662
- return this.finishToken(tt.name, '#server');
663
- }
664
-
665
- if (startsWith('#style') && is_ripple_delimiter(char_after(6))) {
666
- this.pos += 6;
667
- return this.finishToken(tt.name, '#style');
668
- }
669
- }
670
- }
671
- return super.getTokenFromCode(code);
672
- }
673
-
674
- /**
675
- * Override isLet to recognize `let &{` and `let &[` as variable declarations.
676
- * Acorn's isLet checks the char after `let` and only recognizes `{`, `[`, or identifiers.
677
- * The `&` char (38) is not in that set, so `let &{...}` would not be parsed as a declaration.
678
- * @type {Parse.Parser['isLet']}
679
- */
680
- isLet(context) {
681
- if (!this.isContextual('let')) return false;
682
- const skip = /\s*/y;
683
- skip.lastIndex = this.pos;
684
- const match = skip.exec(this.input);
685
- if (!match) return super.isLet(context);
686
- const next = this.pos + match[0].length;
687
- const nextCh = this.input.charCodeAt(next);
688
- // If next char is &, check if char after & is { or [
689
- if (nextCh === 38) {
690
- const afterAmp = this.input.charCodeAt(next + 1);
691
- if (afterAmp === 123 || afterAmp === 91) return true;
692
- }
693
- return super.isLet(context);
694
- }
695
-
696
- /**
697
- * Parse binding atom - handles lazy destructuring patterns (&{...} and &[...])
698
- * When & is directly followed by { or [, parse as a lazy destructuring pattern.
699
- * The resulting ObjectPattern/ArrayPattern node gets a `lazy: true` flag.
700
- */
701
- parseBindingAtom() {
702
- if (this.type === tt.bitwiseAND) {
703
- // Check that the char immediately after & is { or [ (no whitespace)
704
- const charAfterAmp = this.input.charCodeAt(this.end);
705
- if (charAfterAmp === 123 || charAfterAmp === 91) {
706
- // & directly followed by { or [ — lazy destructuring
707
- this.next(); // consume &, now current token is { or [
708
- const pattern = super.parseBindingAtom();
709
- /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (pattern).lazy = true;
710
- return pattern;
711
- }
712
- }
713
- return super.parseBindingAtom();
714
- }
715
-
716
- /**
717
- * Parse expression atom - handles RippleArray and RippleObject literals
718
- * @type {Parse.Parser['parseExprAtom']}
719
- */
720
- parseExprAtom(refDestructuringErrors, forNew, forInit) {
721
- const lookahead_type = this.lookahead().type;
722
- const is_next_call_token = lookahead_type === tt.parenL || lookahead_type === tt.relational;
723
-
724
- // Check if this is #server identifier for server function calls
725
- if (this.type === tt.name && this.value === '#server') {
726
- const node = this.startNode();
727
- this.next();
728
- return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
729
- }
730
-
731
- if (this.type === tt.name && this.value === '#style') {
732
- const node = this.startNode();
733
- this.next();
734
- return /** @type {AST.StyleIdentifier} */ (this.finishNode(node, 'StyleIdentifier'));
735
- }
736
-
737
- // Check if this is a component expression (e.g., in object literal values)
738
- if (this.type === tt.name && this.value === 'component') {
739
- return this.parseComponent();
740
- }
741
-
742
- return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
743
- }
744
-
745
- /**
746
- * Override to track parenthesized expressions in metadata
747
- * This allows the prettier plugin to preserve parentheses where they existed
748
- * @type {Parse.Parser['parseParenAndDistinguishExpression']}
749
- */
750
- parseParenAndDistinguishExpression(canBeArrow, forInit) {
751
- const startPos = this.start;
752
- const expr = super.parseParenAndDistinguishExpression(canBeArrow, forInit);
753
-
754
- // If the expression's start position is after the opening paren,
755
- // it means it was wrapped in parentheses. Mark it in metadata.
756
- if (expr && /** @type {AST.NodeWithLocation} */ (expr).start > startPos) {
757
- expr.metadata ??= { path: [] };
758
- expr.metadata.parenthesized = true;
759
- }
760
-
761
- return expr;
762
- }
763
-
764
- /**
765
- * Override checkLocalExport to check all scopes in the scope stack.
766
- * This is needed because server blocks create nested scopes, but exports
767
- * from within server blocks should still be valid if the identifier is
768
- * declared in the server block's scope (not just the top-level module scope).
769
- * @type {Parse.Parser['checkLocalExport']}
770
- */
771
- checkLocalExport(id) {
772
- const { name } = id;
773
- if (this.hasImport(name)) return;
774
- // Check all scopes in the scope stack, not just the top-level scope
775
- for (let i = this.scopeStack.length - 1; i >= 0; i--) {
776
- const scope = this.scopeStack[i];
777
- if (scope.lexical.indexOf(name) !== -1 || scope.var.indexOf(name) !== -1) {
778
- // Found in a scope, remove from undefinedExports if it was added
779
- delete this.undefinedExports[name];
780
- return;
781
- }
782
- }
783
- // Not found in any scope, add to undefinedExports for later error
784
- this.undefinedExports[name] = id;
785
- }
786
-
787
- /**
788
- * @type {Parse.Parser['parseServerBlock']}
789
- */
790
- parseServerBlock() {
791
- const node = /** @type {AST.ServerBlock} */ (this.startNode());
792
- this.next();
793
-
794
- const body = /** @type {AST.ServerBlockStatement} */ (this.startNode());
795
- node.body = body;
796
- body.body = [];
797
-
798
- this.expect(tt.braceL);
799
- this.enterScope(0);
800
- while (this.type !== tt.braceR) {
801
- const stmt = /** @type {AST.Statement} */ (this.parseStatement(null, true));
802
- body.body.push(stmt);
803
- }
804
- this.next();
805
- this.exitScope();
806
- this.finishNode(body, 'BlockStatement');
807
-
808
- this.awaitPos = 0;
809
- return this.finishNode(node, 'ServerBlock');
810
- }
811
-
812
- /**
813
- * Parse a component - common implementation used by statements, expressions, and export defaults
814
- * @type {Parse.Parser['parseComponent']}
815
- */
816
- parseComponent({
817
- requireName = false,
818
- isDefault = false,
819
- declareName = false,
820
- skipName = false,
821
- } = {}) {
822
- const node = /** @type {AST.Component} */ (this.startNode());
823
- node.type = 'Component';
824
- node.css = null;
825
- node.default = isDefault;
826
-
827
- // skipName is used for computed property names where 'component' and the key
828
- // have already been consumed before calling parseComponent
829
- if (!skipName) {
830
- this.next(); // consume 'component'
831
- }
832
- this.enterScope(0);
833
-
834
- if (skipName) {
835
- // For computed names, the key is parsed separately, so id is null
836
- node.id = null;
837
- } else if (requireName) {
838
- node.id = this.parseIdent();
839
- if (declareName) {
840
- this.declareName(
841
- node.id.name,
842
- BINDING_TYPES.BIND_FUNCTION,
843
- /** @type {AST.NodeWithLocation} */ (node.id).start,
844
- );
845
- }
846
- } else {
847
- node.id = this.type.label === 'name' ? this.parseIdent() : null;
848
- if (declareName && node.id) {
849
- this.declareName(
850
- node.id.name,
851
- BINDING_TYPES.BIND_FUNCTION,
852
- /** @type {AST.NodeWithLocation} */ (node.id).start,
853
- );
854
- }
855
- }
856
-
857
- this.parseFunctionParams(node);
858
- this.eat(tt.braceL);
859
- node.body = [];
860
- this.#path.push(node);
861
-
862
- this.parseTemplateBody(node.body);
863
- this.#path.pop();
864
- this.exitScope();
865
-
866
- this.next();
867
- skipWhitespace(this);
868
- this.finishNode(node, 'Component');
869
- this.awaitPos = 0;
870
-
871
- return node;
872
- }
873
-
874
- /**
875
- * @type {Parse.Parser['parseExportDefaultDeclaration']}
876
- */
877
- parseExportDefaultDeclaration() {
878
- // Check if this is "export default component"
879
- if (this.value === 'component') {
880
- return this.parseComponent({ isDefault: true });
881
- }
882
-
883
- return super.parseExportDefaultDeclaration();
884
- }
885
-
886
- /** @type {Parse.Parser['parseForStatement']} */
887
- parseForStatement(node) {
888
- this.next();
889
- let awaitAt =
890
- this.options.ecmaVersion >= 9 && this.canAwait && this.eatContextual('await')
891
- ? this.lastTokStart
892
- : -1;
893
- this.labels.push({ kind: 'loop' });
894
- this.enterScope(0);
895
- this.expect(tt.parenL);
896
-
897
- if (this.type === tt.semi) {
898
- if (awaitAt > -1) this.unexpected(awaitAt);
899
- return this.parseFor(node, null);
900
- }
901
-
902
- // @ts-ignore — acorn internal: isLet accepts 0 args at runtime
903
- let isLet = this.isLet();
904
- if (this.type === tt._var || this.type === tt._const || isLet) {
905
- let init = /** @type {AST.VariableDeclaration} */ (this.startNode()),
906
- kind = isLet ? 'let' : /** @type {AST.VariableDeclaration['kind']} */ (this.value);
907
- this.next();
908
- this.parseVar(init, true, kind);
909
- this.finishNode(init, 'VariableDeclaration');
910
- return this.parseForAfterInitWithIndex(
911
- /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
912
- init,
913
- awaitAt,
914
- );
915
- }
916
-
917
- // Handle other cases like using declarations if they exist
918
- let startsWithLet = this.isContextual('let'),
919
- isForOf = false;
920
- let usingKind =
921
- this.isUsing && this.isUsing(true)
922
- ? 'using'
923
- : this.isAwaitUsing && this.isAwaitUsing(true)
924
- ? 'await using'
925
- : null;
926
- if (usingKind) {
927
- let init = /** @type {AST.VariableDeclaration} */ (this.startNode());
928
- this.next();
929
- if (usingKind === 'await using') {
930
- if (!this.canAwait) {
931
- this.raise(this.start, 'Await using cannot appear outside of async function');
932
- }
933
- this.next();
934
- }
935
- this.parseVar(init, true, usingKind);
936
- this.finishNode(init, 'VariableDeclaration');
937
- return this.parseForAfterInitWithIndex(
938
- /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
939
- init,
940
- awaitAt,
941
- );
942
- }
943
-
944
- let containsEsc = this.containsEsc;
945
- let refDestructuringErrors = new /** @type {new () => Parse.DestructuringErrors} */ (
946
- /** @type {unknown} */ (DestructuringErrors)
947
- )();
948
- let initPos = this.start;
949
- let init_expr =
950
- awaitAt > -1
951
- ? this.parseExprSubscripts(refDestructuringErrors, 'await')
952
- : this.parseExpression(true, refDestructuringErrors);
953
-
954
- if (
955
- this.type === tt._in ||
956
- (isForOf = this.options.ecmaVersion >= 6 && this.isContextual('of'))
957
- ) {
958
- if (awaitAt > -1) {
959
- // implies `ecmaVersion >= 9`
960
- if (this.type === tt._in) this.unexpected(awaitAt);
961
- /** @type {AST.ForOfStatement} */ (node).await = true;
962
- } else if (isForOf && this.options.ecmaVersion >= 8) {
963
- if (
964
- init_expr.start === initPos &&
965
- !containsEsc &&
966
- init_expr.type === 'Identifier' &&
967
- init_expr.name === 'async'
968
- )
969
- this.unexpected();
970
- else if (this.options.ecmaVersion >= 9)
971
- /** @type {AST.ForOfStatement} */ (node).await = false;
972
- }
973
- if (startsWithLet && isForOf)
974
- this.raise(
975
- /** @type {AST.NodeWithLocation} */ (init_expr).start,
976
- "The left-hand side of a for-of loop may not start with 'let'.",
977
- );
978
- const init = this.toAssignable(init_expr, false, refDestructuringErrors);
979
- this.checkLValPattern(init);
980
- return this.parseForInWithIndex(
981
- /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
982
- init,
983
- );
984
- } else {
985
- this.checkExpressionErrors(refDestructuringErrors, true);
986
- }
987
-
988
- if (awaitAt > -1) this.unexpected(awaitAt);
989
- return this.parseFor(node, init_expr);
990
- }
991
-
992
- /** @type {Parse.Parser['parseForAfterInitWithIndex']} */
993
- parseForAfterInitWithIndex(node, init, awaitAt) {
994
- if (
995
- (this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual('of'))) &&
996
- init.declarations.length === 1
997
- ) {
998
- if (this.options.ecmaVersion >= 9) {
999
- if (this.type === tt._in) {
1000
- if (awaitAt > -1) {
1001
- this.unexpected(awaitAt);
1002
- }
1003
- } else {
1004
- /** @type {AST.ForOfStatement} */ (node).await = awaitAt > -1;
1005
- }
1006
- }
1007
- return this.parseForInWithIndex(
1008
- /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
1009
- init,
1010
- );
1011
- }
1012
- if (awaitAt > -1) {
1013
- this.unexpected(awaitAt);
1014
- }
1015
- return this.parseFor(node, init);
1016
- }
1017
-
1018
- /** @type {Parse.Parser['parseForInWithIndex']} */
1019
- parseForInWithIndex(node, init) {
1020
- const isForIn = this.type === tt._in;
1021
- this.next();
1022
-
1023
- if (
1024
- init.type === 'VariableDeclaration' &&
1025
- init.declarations[0].init != null &&
1026
- (!isForIn ||
1027
- this.options.ecmaVersion < 8 ||
1028
- this.strict ||
1029
- init.kind !== 'var' ||
1030
- init.declarations[0].id.type !== 'Identifier')
1031
- ) {
1032
- this.raise(
1033
- /** @type {AST.NodeWithLocation} */ (init).start,
1034
- `${isForIn ? 'for-in' : 'for-of'} loop variable declaration may not have an initializer`,
1035
- );
1036
- }
1037
-
1038
- node.left = init;
1039
- node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign();
1040
-
1041
- // Check for our extended syntax: "; index varName"
1042
- if (!isForIn && this.type === tt.semi) {
1043
- this.next(); // consume ';'
1044
-
1045
- if (this.isContextual('index')) {
1046
- this.next(); // consume 'index'
1047
- /** @type {AST.ForOfStatement} */ (node).index = /** @type {AST.Identifier} */ (
1048
- this.parseExpression()
1049
- );
1050
- if (
1051
- /** @type {AST.Identifier} */ (/** @type {AST.ForOfStatement} */ (node).index)
1052
- .type !== 'Identifier'
1053
- ) {
1054
- this.raise(this.start, 'Expected identifier after "index" keyword');
1055
- }
1056
- this.eat(tt.semi);
1057
- }
1058
-
1059
- if (this.isContextual('key')) {
1060
- this.next(); // consume 'key'
1061
- /** @type {AST.ForOfStatement} */ (node).key = this.parseExpression();
1062
- }
1063
-
1064
- if (this.isContextual('index')) {
1065
- this.raise(this.start, '"index" must come before "key" in for-of loop');
1066
- }
1067
- } else if (!isForIn) {
1068
- // Set index to null for standard for-of loops
1069
- /** @type {AST.ForOfStatement} */ (node).index = null;
1070
- }
1071
-
1072
- this.expect(tt.parenR);
1073
- node.body = /** @type {AST.BlockStatement} */ (this.parseStatement('for'));
1074
- this.exitScope();
1075
- this.labels.pop();
1076
- return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
1077
- }
1078
-
1079
- /**
1080
- * @type {Parse.Parser['checkUnreserved']}
1081
- */
1082
- checkUnreserved(ref) {
1083
- if (ref.name === 'component') {
1084
- // Allow 'component' when it's followed by an identifier and '(' or '<' (component method in object literal or class)
1085
- // e.g., { component something() { ... } } or class Foo { component something<T>() { ... } }
1086
- // Also allow computed names: { component ['name']() { ... } }
1087
- // Also allow string literal names: { component 'name'() { ... } }
1088
- const nextChars = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
1089
- if (!nextChars) {
1090
- this.raise(
1091
- ref.start,
1092
- '"component" is a Ripple keyword and cannot be used as an identifier',
1093
- );
1094
- }
1095
- }
1096
- return super.checkUnreserved(ref);
1097
- }
1098
-
1099
- /** @type {Parse.Parser['shouldParseExportStatement']} */
1100
- shouldParseExportStatement() {
1101
- if (super.shouldParseExportStatement()) {
1102
- return true;
1103
- }
1104
- if (this.value === 'component') {
1105
- return true;
1106
- }
1107
- return this.type.keyword === 'var';
1108
- }
1109
-
1110
- /**
1111
- * @return {ESTreeJSX.JSXExpressionContainer}
1112
- */
1113
- jsx_parseExpressionContainer() {
1114
- let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1115
- this.next();
1116
-
1117
- if (this.type === tt.name && this.value === 'html') {
1118
- node.html = true;
1119
- this.next();
1120
- if (this.type === tt.braceR) {
1121
- this.raise(
1122
- this.start,
1123
- '"html" is a Ripple keyword and must be used in the form {html some_content}',
1124
- );
1125
- }
1126
- } else if (this.type === tt.name && this.value === 'text') {
1127
- node.text = true;
1128
- this.next();
1129
- if (this.type === tt.braceR) {
1130
- this.raise(
1131
- this.start,
1132
- '"text" is a Ripple keyword and must be used in the form {text some_value}',
1133
- );
1134
- }
1135
- }
1136
-
1137
- node.expression =
1138
- this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1139
- this.expect(tt.braceR);
1140
-
1141
- return this.finishNode(node, 'JSXExpressionContainer');
1142
- }
1143
-
1144
- /**
1145
- * @type {Parse.Parser['jsx_parseEmptyExpression']}
1146
- */
1147
- jsx_parseEmptyExpression() {
1148
- // Override to properly handle the range for JSXEmptyExpression
1149
- // The range should be from after { to before }
1150
- const node = /** @type {ESTreeJSX.JSXEmptyExpression} */ (
1151
- this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc)
1152
- );
1153
- node.end = this.start;
1154
- node.loc.end = this.startLoc;
1155
- return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
1156
- }
1157
-
1158
- /**
1159
- * @type {Parse.Parser['jsx_parseTupleContainer']}
1160
- */
1161
- jsx_parseTupleContainer() {
1162
- const t = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1163
- return (
1164
- this.next(),
1165
- (t.expression =
1166
- this.type === tt.bracketR ? this.jsx_parseEmptyExpression() : this.parseExpression()),
1167
- this.expect(tt.bracketR),
1168
- this.finishNode(t, 'JSXExpressionContainer')
1169
- );
1170
- }
1171
-
1172
- /**
1173
- * @type {Parse.Parser['jsx_parseAttribute']}
1174
- */
1175
- jsx_parseAttribute() {
1176
- let node = /** @type {AST.RippleAttribute | ESTreeJSX.JSXAttribute} */ (this.startNode());
1177
-
1178
- if (this.eat(tt.braceL)) {
1179
- if (this.value === 'ref') {
1180
- this.next();
1181
- if (this.type === tt.braceR) {
1182
- this.raise(
1183
- this.start,
1184
- '"ref" is a Ripple keyword and must be used in the form {ref fn}',
1185
- );
1186
- }
1187
- /** @type {AST.RefAttribute} */ (node).argument = this.parseMaybeAssign();
1188
- this.expect(tt.braceR);
1189
- return /** @type {AST.RefAttribute} */ (this.finishNode(node, 'RefAttribute'));
1190
- } else if (this.type === tt.ellipsis) {
1191
- this.expect(tt.ellipsis);
1192
- /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
1193
- this.expect(tt.braceR);
1194
- return this.finishNode(node, 'SpreadAttribute');
1195
- } else if (this.lookahead().type === tt.ellipsis) {
1196
- this.expect(tt.ellipsis);
1197
- /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
1198
- this.expect(tt.braceR);
1199
- return this.finishNode(node, 'SpreadAttribute');
1200
- } else {
1201
- const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
1202
- id.tracked = false;
1203
- this.finishNode(id, 'Identifier');
1204
- /** @type {AST.Attribute} */ (node).name = id;
1205
- /** @type {AST.Attribute} */ (node).value = id;
1206
- /** @type {AST.Attribute} */ (node).shorthand = true; // Mark as shorthand since name and value are the same
1207
- this.next();
1208
- this.expect(tt.braceR);
1209
- return this.finishNode(node, 'Attribute');
1210
- }
1211
- }
1212
- /** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
1213
- /** @type {ESTreeJSX.JSXAttribute} */ (node).value =
1214
- /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
1215
- this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
1216
- );
1217
- return this.finishNode(node, 'JSXAttribute');
1218
- }
1219
-
1220
- /**
1221
- * @type {Parse.Parser['jsx_parseNamespacedName']}
1222
- */
1223
- jsx_parseNamespacedName() {
1224
- const base = this.jsx_parseIdentifier();
1225
- if (!this.eat(tt.colon)) return base;
1226
- const node = /** @type {ESTreeJSX.JSXNamespacedName} */ (
1227
- this.startNodeAt(
1228
- /** @type {AST.NodeWithLocation} */ (base).start,
1229
- /** @type {AST.NodeWithLocation} */ (base).loc.start,
1230
- )
1231
- );
1232
- node.namespace = base;
1233
- node.name = this.jsx_parseIdentifier();
1234
- return this.finishNode(node, 'JSXNamespacedName');
1235
- }
1236
-
1237
- /**
1238
- * @type {Parse.Parser['jsx_parseIdentifier']}
1239
- */
1240
- jsx_parseIdentifier() {
1241
- const node = /** @type {ESTreeJSX.JSXIdentifier} */ (this.startNode());
1242
-
1243
- if (this.type.label === '@') {
1244
- this.next(); // consume @
1245
-
1246
- if (this.type === tt.name || this.type === tstt.jsxName) {
1247
- node.name = /** @type {string} */ (this.value);
1248
- node.tracked = true;
1249
- this.next();
1250
- } else {
1251
- // Unexpected token after @
1252
- this.unexpected();
1253
- }
1254
- } else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
1255
- node.name = /** @type {string} */ (this.value);
1256
- node.tracked = false; // Explicitly mark as not tracked
1257
- this.next();
1258
- } else {
1259
- return super.jsx_parseIdentifier();
1260
- }
1261
-
1262
- return this.finishNode(node, 'JSXIdentifier');
1263
- }
1264
-
1265
- /**
1266
- * @type {Parse.Parser['jsx_parseElementName']}
1267
- */
1268
- jsx_parseElementName() {
1269
- if (this.type === tstt.jsxTagEnd) {
1270
- return '';
1271
- }
1272
-
1273
- let node = this.jsx_parseNamespacedName();
1274
-
1275
- if (node.type === 'JSXNamespacedName') {
1276
- return node;
1277
- }
1278
-
1279
- if (this.eat(tt.dot)) {
1280
- let memberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
1281
- this.startNodeAt(
1282
- /** @type {AST.NodeWithLocation} */ (node).start,
1283
- /** @type {AST.NodeWithLocation} */ (node).loc.start,
1284
- )
1285
- );
1286
- memberExpr.object = node;
1287
- memberExpr.property = this.jsx_parseIdentifier();
1288
- memberExpr.computed = false;
1289
- memberExpr = this.finishNode(memberExpr, 'JSXMemberExpression');
1290
- while (this.eat(tt.dot)) {
1291
- let newMemberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
1292
- this.startNodeAt(
1293
- /** @type {AST.NodeWithLocation} */ (memberExpr).start,
1294
- /** @type {AST.NodeWithLocation} */ (memberExpr).loc.start,
1295
- )
1296
- );
1297
- newMemberExpr.object = memberExpr;
1298
- newMemberExpr.property = this.jsx_parseIdentifier();
1299
- newMemberExpr.computed = false;
1300
- memberExpr = this.finishNode(newMemberExpr, 'JSXMemberExpression');
1301
- }
1302
- return memberExpr;
1303
- }
1304
- return node;
1305
- }
1306
-
1307
- /** @type {Parse.Parser['jsx_parseAttributeValue']} */
1308
- jsx_parseAttributeValue() {
1309
- switch (this.type) {
1310
- case tt.braceL:
1311
- return this.jsx_parseExpressionContainer();
1312
- case tstt.jsxTagStart:
1313
- case tt.string:
1314
- return this.parseExprAtom();
1315
- default:
1316
- this.raise(this.start, 'value should be either an expression or a quoted text');
1317
- }
1318
- }
1319
-
1320
- /**
1321
- * @type {Parse.Parser['parseTryStatement']}
1322
- */
1323
- parseTryStatement(node) {
1324
- this.next();
1325
- node.block = this.parseBlock();
1326
- node.handler = null;
1327
-
1328
- if (this.value === 'pending') {
1329
- this.next();
1330
- node.pending = this.parseBlock();
1331
- } else {
1332
- node.pending = null;
1333
- }
1334
-
1335
- if (this.type === tt._catch) {
1336
- const clause = /** @type {AST.CatchClause} */ (this.startNode());
1337
- this.next();
1338
- if (this.eat(tt.parenL)) {
1339
- // Parse first param (error) manually to support optional second param (reset).
1340
- // We can't use parseCatchClauseParam() because it eats the closing paren.
1341
- const param = this.parseBindingAtom();
1342
- const simple = param.type === 'Identifier';
1343
- this.enterScope(simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : 0);
1344
- this.checkLValPattern(
1345
- param,
1346
- simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : BINDING_TYPES.BIND_LEXICAL,
1347
- );
1348
- const type = this.tsTryParseTypeAnnotation();
1349
- if (type) {
1350
- param.typeAnnotation = type;
1351
- this.resetEndLocation(param);
1352
- }
1353
- clause.param = param;
1354
-
1355
- // Optional second parameter: reset function
1356
- if (this.eat(tt.comma)) {
1357
- const reset_param = this.parseBindingAtom();
1358
- this.checkLValSimple(reset_param, BINDING_TYPES.BIND_LEXICAL);
1359
- const reset_type = this.tsTryParseTypeAnnotation();
1360
- if (reset_type) {
1361
- reset_param.typeAnnotation = reset_type;
1362
- this.resetEndLocation(reset_param);
1363
- }
1364
- clause.resetParam = reset_param;
1365
- } else {
1366
- clause.resetParam = null;
1367
- }
1368
-
1369
- this.expect(tt.parenR);
1370
- } else {
1371
- clause.param = null;
1372
- clause.resetParam = null;
1373
- this.enterScope(0);
1374
- }
1375
- clause.body = this.parseBlock(false);
1376
- this.exitScope();
1377
- node.handler = this.finishNode(clause, 'CatchClause');
1378
- }
1379
- node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
1380
-
1381
- if (!node.handler && !node.finalizer && !node.pending) {
1382
- this.raise(
1383
- /** @type {AST.NodeWithLocation} */ (node).start,
1384
- 'Missing catch or finally clause',
1385
- );
1386
- }
1387
- return this.finishNode(node, 'TryStatement');
1388
- }
1389
-
1390
- /** @type {Parse.Parser['jsx_readToken']} */
1391
- jsx_readToken() {
1392
- const inside_tsx_compat = this.#path.findLast(
1393
- (n) => n.type === 'TsxCompat' || n.type === 'Tsx',
1394
- );
1395
- if (inside_tsx_compat) {
1396
- return super.jsx_readToken();
1397
- }
1398
- let out = '',
1399
- chunkStart = this.pos;
1400
-
1401
- while (true) {
1402
- if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
1403
- let ch = this.input.charCodeAt(this.pos);
1404
-
1405
- switch (ch) {
1406
- case 60: // '<'
1407
- case 123: // '{'
1408
- // In JSX text mode, '<' and '{' always start a tag/expression container.
1409
- // `exprAllowed` can be false here due to surrounding parser state, but
1410
- // throwing breaks valid templates (e.g. sibling tags after a close).
1411
- if (ch === 60) {
1412
- ++this.pos;
1413
- return this.finishToken(tstt.jsxTagStart);
1414
- }
1415
- return this.getTokenFromCode(ch);
1416
-
1417
- case 47: // '/'
1418
- // Check if this is a comment (// or /*)
1419
- if (this.input.charCodeAt(this.pos + 1) === 47) {
1420
- // '//'
1421
- // Line comment - handle it properly
1422
- const commentStart = this.pos;
1423
- const startLoc = this.curPosition();
1424
- this.pos += 2;
1425
-
1426
- let commentText = '';
1427
- while (this.pos < this.input.length) {
1428
- const nextCh = this.input.charCodeAt(this.pos);
1429
- if (acorn.isNewLine(nextCh)) break;
1430
- commentText += this.input[this.pos];
1431
- this.pos++;
1432
- }
1433
-
1434
- const commentEnd = this.pos;
1435
- const endLoc = this.curPosition();
1436
-
1437
- // Call onComment if it exists
1438
- if (this.options.onComment) {
1439
- const metadata = this.#createCommentMetadata();
1440
- this.options.onComment(
1441
- false,
1442
- commentText,
1443
- commentStart,
1444
- commentEnd,
1445
- startLoc,
1446
- endLoc,
1447
- metadata,
1448
- );
1449
- }
1450
-
1451
- // Continue processing from current position
1452
- break;
1453
- } else if (this.input.charCodeAt(this.pos + 1) === 42) {
1454
- // '/*'
1455
- // Block comment - handle it properly
1456
- const commentStart = this.pos;
1457
- const startLoc = this.curPosition();
1458
- this.pos += 2;
1459
-
1460
- let commentText = '';
1461
- while (this.pos < this.input.length - 1) {
1462
- if (
1463
- this.input.charCodeAt(this.pos) === 42 &&
1464
- this.input.charCodeAt(this.pos + 1) === 47
1465
- ) {
1466
- this.pos += 2;
1467
- break;
1468
- }
1469
- commentText += this.input[this.pos];
1470
- this.pos++;
1471
- }
1472
-
1473
- const commentEnd = this.pos;
1474
- const endLoc = this.curPosition();
1475
-
1476
- // Call onComment if it exists
1477
- if (this.options.onComment) {
1478
- const metadata = this.#createCommentMetadata();
1479
- this.options.onComment(
1480
- true,
1481
- commentText,
1482
- commentStart,
1483
- commentEnd,
1484
- startLoc,
1485
- endLoc,
1486
- metadata,
1487
- );
1488
- }
1489
-
1490
- // Continue processing from current position
1491
- break;
1492
- }
1493
- // If not a comment, fall through to default case
1494
- this.context.push(b_stat);
1495
- this.exprAllowed = true;
1496
- return original.readToken.call(this, ch);
1497
-
1498
- case 38: // '&'
1499
- out += this.input.slice(chunkStart, this.pos);
1500
- out += this.jsx_readEntity();
1501
- chunkStart = this.pos;
1502
- break;
1503
-
1504
- case 62: // '>'
1505
- case 125: {
1506
- // '}'
1507
- if (
1508
- ch === 125 &&
1509
- (this.#path.length === 0 ||
1510
- this.#path.at(-1)?.type === 'Component' ||
1511
- this.#path.at(-1)?.type === 'Element')
1512
- ) {
1513
- return original.readToken.call(this, ch);
1514
- }
1515
- this.raise(
1516
- this.pos,
1517
- 'Unexpected token `' +
1518
- this.input[this.pos] +
1519
- '`. Did you mean `' +
1520
- (ch === 62 ? '&gt;' : '&rbrace;') +
1521
- '` or ' +
1522
- '`{"' +
1523
- this.input[this.pos] +
1524
- '"}' +
1525
- '`?',
1526
- );
1527
- }
1528
-
1529
- default:
1530
- if (acorn.isNewLine(ch)) {
1531
- out += this.input.slice(chunkStart, this.pos);
1532
- out += this.jsx_readNewLine(true);
1533
- chunkStart = this.pos;
1534
- } else if (ch === 32 || ch === 9) {
1535
- ++this.pos;
1536
- } else {
1537
- this.context.push(b_stat);
1538
- this.exprAllowed = true;
1539
- return original.readToken.call(this, ch);
1540
- }
1541
- }
1542
- }
1543
- }
1544
-
1545
- /**
1546
- * Override jsx_parseElement to intercept expression-level JSX.
1547
- * This is called by acorn-jsx's parseExprAtom when it encounters <
1548
- * in expression position. Only <tsx> and <tsx:*> are allowed.
1549
- * @type {Parse.Parser['jsx_parseElement']}
1550
- */
1551
- jsx_parseElement() {
1552
- const inside_tsx = this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
1553
- if (inside_tsx) {
1554
- // Inside tsx/tsx:*, let acorn-jsx handle it normally
1555
- return super.jsx_parseElement();
1556
- }
1557
-
1558
- // Check if the element being parsed IS a <tsx> or <tsx:*> tag
1559
- // Current token is jsxTagStart, this.end is position after '<'
1560
- const tag_name_start = this.end;
1561
- const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
1562
- const is_tsx_tag =
1563
- this.input.startsWith('tsx', tag_name_start) &&
1564
- (tag_name_start + 3 >= this.input.length ||
1565
- char_after_tsx === 62 || // >
1566
- char_after_tsx === 47 || // / (self-closing)
1567
- char_after_tsx === 32 || // space
1568
- char_after_tsx === 9 || // tab
1569
- char_after_tsx === 10 || // newline
1570
- char_after_tsx === 13 || // carriage return
1571
- char_after_tsx === 58); // : (tsx:react)
1572
-
1573
- if (is_tsx_tag) {
1574
- // Use Ripple's parseElement to create a Tsx/TsxCompat node
1575
- this.next();
1576
- return /** @type {import('estree-jsx').JSXElement} */ (
1577
- /** @type {unknown} */ (this.parseElement())
1578
- );
1579
- }
1580
-
1581
- this.raise(
1582
- this.start,
1583
- 'JSX elements cannot be used as expressions. Wrap with `<tsx>...</tsx>` or use elements as statements within a component.',
1584
- );
1585
- }
1586
-
1587
- /**
1588
- * @type {Parse.Parser['parseElement']}
1589
- */
1590
- parseElement() {
1591
- const inside_head = this.#path.findLast(
1592
- (n) => n.type === 'Element' && n.id.type === 'Identifier' && n.id.name === 'head',
1593
- );
1594
- // Adjust the start so we capture the `<` as part of the element
1595
- const start = this.start - 1;
1596
- const position = new acorn.Position(this.curLine, start - this.lineStart);
1597
-
1598
- const element = /** @type {AST.Element | AST.Tsx | AST.TsxCompat} */ (this.startNode());
1599
- element.start = start;
1600
- /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
1601
- element.metadata = { path: [] };
1602
- element.children = [];
1603
-
1604
- const open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
1605
- this.jsx_parseOpeningElementAt(start, position)
1606
- );
1607
-
1608
- // Always attach the concrete opening element node for accurate source mapping
1609
- element.openingElement = open;
1610
-
1611
- // Check if this is a namespaced element (tsx:react)
1612
- const is_tsx_compat = open.name.type === 'JSXNamespacedName';
1613
- const is_tsx =
1614
- !is_tsx_compat && open.name.type === 'JSXIdentifier' && open.name.name === 'tsx';
1615
-
1616
- if (is_tsx_compat) {
1617
- const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
1618
- /** @type {AST.TsxCompat} */ (element).type = 'TsxCompat';
1619
- /** @type {AST.TsxCompat} */ (element).kind = namespace_node.name.name; // e.g., "react" from "tsx:react"
1620
-
1621
- if (open.selfClosing) {
1622
- const tagName = namespace_node.namespace.name + ':' + namespace_node.name.name;
1623
- this.raise(
1624
- open.start,
1625
- `TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
1626
- );
1627
- }
1628
- } else if (is_tsx) {
1629
- /** @type {AST.Tsx} */ (element).type = 'Tsx';
1630
-
1631
- if (open.selfClosing) {
1632
- this.raise(
1633
- open.start,
1634
- `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
1635
- );
1636
- }
1637
- } else {
1638
- element.type = 'Element';
1639
- }
1640
-
1641
- this.#path.push(element);
1642
-
1643
- for (const attr of open.attributes) {
1644
- if (attr.type === 'JSXAttribute') {
1645
- /** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
1646
- if (attr.name.type === 'JSXIdentifier') {
1647
- /** @type {AST.Identifier} */ (/** @type {unknown} */ (attr.name)).type =
1648
- 'Identifier';
1649
- }
1650
- if (attr.value !== null) {
1651
- if (attr.value.type === 'JSXExpressionContainer') {
1652
- const expression = attr.value.expression;
1653
- if (expression.type === 'Literal') {
1654
- expression.was_expression = true;
1655
- }
1656
- // @ts-ignore — intentional AST node conversion from JSX to Ripple
1657
- /** @type {ESTreeJSX.JSXAttribute} */ (attr).value =
1658
- /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (expression);
1659
- }
1660
- }
1661
- }
1662
- }
1663
-
1664
- if (!is_tsx_compat && !is_tsx) {
1665
- /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
1666
- convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
1667
- );
1668
- element.selfClosing = open.selfClosing;
1669
- }
1670
-
1671
- element.attributes = open.attributes;
1672
- element.metadata ??= { path: [] };
1673
- element.metadata.commentContainerId = ++this.#commentContextId;
1674
-
1675
- if (element.selfClosing) {
1676
- this.#path.pop();
1677
-
1678
- if (this.type.label === '</>/<=/>=') {
1679
- this.pos--;
1680
- this.next();
1681
- }
1682
- } else {
1683
- if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
1684
- let content = '';
1685
-
1686
- // TODO implement this where we get a string for content of the content of the script tag
1687
- // This is a temporary workaround to get the content of the script tag
1688
- const start = open.end;
1689
- const input = this.input.slice(start);
1690
- const end = input.indexOf('</script>');
1691
- content = end === -1 ? input : input.slice(0, end);
1692
-
1693
- const newLines = content.match(regex_newline_characters)?.length;
1694
- if (newLines) {
1695
- this.curLine = open.loc.end.line + newLines;
1696
- this.lineStart = start + content.lastIndexOf('\n') + 1;
1697
- }
1698
- if (end !== -1) {
1699
- const closingStart = start + content.length;
1700
- const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
1701
- const closingStartLoc = new acorn.Position(
1702
- closingLineInfo.line,
1703
- closingLineInfo.column,
1704
- );
1705
-
1706
- // Ensure `</script>` can't be tokenized as `<` followed by a regexp
1707
- // start when we manually advance to the `/`.
1708
- this.exprAllowed = false;
1709
-
1710
- // Position after '<' (so next() reads '/')
1711
- this.pos = closingStart + 1;
1712
- this.type = tstt.jsxTagStart;
1713
- this.start = closingStart;
1714
- this.startLoc = closingStartLoc;
1715
- this.next();
1716
-
1717
- // Consume '/'
1718
- this.next();
1719
-
1720
- const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
1721
- element.closingElement = closingElement;
1722
- this.exprAllowed = false;
1723
-
1724
- const contentStartLineInfo = acorn.getLineInfo(this.input, start);
1725
- const contentStartLoc = new acorn.Position(
1726
- contentStartLineInfo.line,
1727
- contentStartLineInfo.column,
1728
- );
1729
-
1730
- const contentEndLineInfo = acorn.getLineInfo(this.input, closingStart);
1731
- const contentEndLoc = new acorn.Position(
1732
- contentEndLineInfo.line,
1733
- contentEndLineInfo.column,
1734
- );
1735
-
1736
- element.children = [
1737
- /** @type {AST.ScriptContent} */ (
1738
- /** @type {unknown} */ ({
1739
- type: 'ScriptContent',
1740
- content,
1741
- start,
1742
- end: closingStart,
1743
- loc: { start: contentStartLoc, end: contentEndLoc },
1744
- })
1745
- ),
1746
- ];
1747
-
1748
- this.#path.pop();
1749
- } else {
1750
- // No closing tag
1751
- if (!this.#loose) {
1752
- this.raise(
1753
- open.end,
1754
- "Unclosed tag '<script>'. Expected '</script>' before end of component.",
1755
- );
1756
- }
1757
- /** @type {AST.Element} */ (element).unclosed = true;
1758
- this.#path.pop();
1759
- }
1760
- } else if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'style') {
1761
- // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
1762
- // So backtrack to the end of the <style> tag to make sure everything is included
1763
- const start = open.end;
1764
- const input = this.input.slice(start);
1765
- const end = input.indexOf('</style>');
1766
- const content = end === -1 ? input : input.slice(0, end);
1767
-
1768
- const component = /** @type {AST.Component} */ (
1769
- this.#path.findLast((n) => n.type === 'Component')
1770
- );
1771
- const parsed_css = parse_style(content, { loose: this.#loose });
1772
-
1773
- if (!inside_head) {
1774
- if (component.css !== null) {
1775
- throw new Error('Components can only have one style tag');
1776
- }
1777
- component.css = parsed_css;
1778
- /** @type {AST.Element} */ (element).metadata.styleScopeHash = parsed_css.hash;
1779
- }
1780
-
1781
- const newLines = content.match(regex_newline_characters)?.length;
1782
- if (newLines) {
1783
- this.curLine = open.loc.end.line + newLines;
1784
- this.lineStart = start + content.lastIndexOf('\n') + 1;
1785
- }
1786
- if (end !== -1) {
1787
- const closingStart = start + content.length;
1788
- const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
1789
- const closingStartLoc = new acorn.Position(
1790
- closingLineInfo.line,
1791
- closingLineInfo.column,
1792
- );
1793
-
1794
- // Ensure `</style>` can't be tokenized as `<` followed by a regexp
1795
- // start when we manually advance to the `/`.
1796
- this.exprAllowed = false;
1797
-
1798
- // Position after '<' (so next() reads '/')
1799
- this.pos = closingStart + 1;
1800
- this.type = tstt.jsxTagStart;
1801
- this.start = closingStart;
1802
- this.startLoc = closingStartLoc;
1803
- this.next();
1804
-
1805
- // Consume '/'
1806
- this.next();
1807
-
1808
- const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
1809
- element.closingElement = closingElement;
1810
- this.exprAllowed = false;
1811
- this.#path.pop();
1812
- } else {
1813
- if (!this.#loose) {
1814
- this.raise(
1815
- open.end,
1816
- "Unclosed tag '<style>'. Expected '</style>' before end of component.",
1817
- );
1818
- }
1819
- /** @type {AST.Element} */ (element).unclosed = true;
1820
- this.#path.pop();
1821
- }
1822
- // This node is used for Prettier - always add parsed CSS as children
1823
- // for proper formatting, regardless of whether it's inside head or not
1824
- /** @type {AST.Element} */ (element).children = [
1825
- /** @type {AST.Node} */ (/** @type {unknown} */ (parsed_css)),
1826
- ];
1827
-
1828
- // Ensure we escape JSX <tag></tag> context
1829
- const curContext = this.curContext();
1830
- const parent = this.#path.at(-1);
1831
- const insideTemplate =
1832
- parent?.type === 'Component' ||
1833
- parent?.type === 'Element' ||
1834
- parent?.type === 'Tsx' ||
1835
- parent?.type === 'TsxCompat';
1836
-
1837
- if (curContext === tstc.tc_expr && !insideTemplate) {
1838
- this.context.pop();
1839
- }
1840
-
1841
- /** @type {AST.Element} */ (element).css = content;
1842
- } else {
1843
- this.enterScope(0);
1844
- this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
1845
- this.exitScope();
1846
-
1847
- if (element.type === 'Tsx') {
1848
- this.#path.pop();
1849
-
1850
- if (!element.unclosed) {
1851
- const raise_error = () => {
1852
- this.raise(this.start, `Expected closing tag '</tsx>'`);
1853
- };
1854
-
1855
- this.next();
1856
- // we should expect to see </tsx>
1857
- if (this.value !== '/') {
1858
- raise_error();
1859
- }
1860
- this.next();
1861
- if (this.value !== 'tsx') {
1862
- raise_error();
1863
- }
1864
- this.next();
1865
- if (this.type !== tstt.jsxTagEnd) {
1866
- raise_error();
1867
- }
1868
- this.next();
1869
- }
1870
- } else if (element.type === 'TsxCompat') {
1871
- this.#path.pop();
1872
-
1873
- if (!element.unclosed) {
1874
- const raise_error = () => {
1875
- this.raise(this.start, `Expected closing tag '</tsx:${element.kind}>'`);
1876
- };
1877
-
1878
- this.next();
1879
- // we should expect to see </tsx:kind>
1880
- if (this.value !== '/') {
1881
- raise_error();
1882
- }
1883
- this.next();
1884
- if (this.value !== 'tsx') {
1885
- raise_error();
1886
- }
1887
- this.next();
1888
- if (this.type.label !== ':') {
1889
- raise_error();
1890
- }
1891
- this.next();
1892
- if (this.value !== element.kind) {
1893
- raise_error();
1894
- }
1895
- this.next();
1896
- if (this.type !== tstt.jsxTagEnd) {
1897
- raise_error();
1898
- }
1899
- this.next();
1900
- }
1901
- } else if (this.#path[this.#path.length - 1] === element) {
1902
- // Check if this element was properly closed
1903
- if (!this.#loose) {
1904
- const tagName = this.getElementName(element.id);
1905
- this.raise(
1906
- this.start,
1907
- `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
1908
- );
1909
- } else {
1910
- element.unclosed = true;
1911
- element.loc.end = {
1912
- .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
1913
- };
1914
- element.end = element.openingElement.end;
1915
- this.#path.pop();
1916
- }
1917
- }
1918
- }
1919
-
1920
- // Ensure we escape JSX <tag></tag> context
1921
- const curContext = this.curContext();
1922
- const parent = this.#path.at(-1);
1923
- const insideTemplate =
1924
- parent?.type === 'Component' ||
1925
- parent?.type === 'Element' ||
1926
- parent?.type === 'Tsx' ||
1927
- parent?.type === 'TsxCompat';
1928
-
1929
- if (curContext === tstc.tc_expr && !insideTemplate) {
1930
- this.context.pop();
1931
- }
1932
- }
1933
-
1934
- if (element.closingElement && !is_tsx_compat && !is_tsx) {
1935
- /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
1936
- element.closingElement.name,
1937
- );
1938
- }
1939
-
1940
- this.finishNode(element, element.type);
1941
- return element;
1942
- }
1943
-
1944
- /**
1945
- * @type {Parse.Parser['parseTemplateBody']}
1946
- */
1947
- parseTemplateBody(body) {
1948
- const inside_func =
1949
- this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
1950
- const inside_tsx = this.#path.findLast((n) => n.type === 'Tsx');
1951
- const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
1952
-
1953
- if (!inside_func) {
1954
- if (this.type.label === 'continue') {
1955
- throw new Error('`continue` statements are not allowed in components');
1956
- }
1957
- if (this.type.label === 'break') {
1958
- throw new Error('`break` statements are not allowed in components');
1959
- }
1960
- }
1961
-
1962
- if (inside_tsx) {
1963
- this.exprAllowed = true;
1964
-
1965
- while (true) {
1966
- if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
1967
- if (!this.#loose) {
1968
- this.raise(
1969
- this.start,
1970
- `Unclosed tag '<tsx>'. Expected '</tsx>' before end of component.`,
1971
- );
1972
- } else {
1973
- inside_tsx.unclosed = true;
1974
- /** @type {AST.NodeWithLocation} */ (inside_tsx).loc.end = {
1975
- .../** @type {AST.SourceLocation} */ (inside_tsx.openingElement.loc).end,
1976
- };
1977
- inside_tsx.end = inside_tsx.openingElement.end;
1978
- }
1979
- return;
1980
- }
1981
-
1982
- if (this.input.slice(this.pos, this.pos + 4) === '/tsx') {
1983
- const after = this.input.charCodeAt(this.pos + 4);
1984
- // Make sure it's </tsx> and not </tsx:...>
1985
- if (after === 62 /* > */) {
1986
- return;
1987
- }
1988
- }
1989
-
1990
- if (this.type === tt.braceL) {
1991
- const node = this.jsx_parseExpressionContainer();
1992
- body.push(node);
1993
- } else if (this.type === tstt.jsxTagStart) {
1994
- // Parse JSX element
1995
- const node = super.parseExpression();
1996
- body.push(node);
1997
- } else {
1998
- const start = this.start;
1999
- this.pos = start;
2000
- let text = '';
2001
-
2002
- while (this.pos < this.input.length) {
2003
- const ch = this.input.charCodeAt(this.pos);
2004
-
2005
- // Stop at opening tag, expression, or the component-closing brace
2006
- if (ch === 60 || ch === 123 || ch === 125) {
2007
- // < or { or }
2008
- break;
2009
- }
2010
-
2011
- text += this.input[this.pos];
2012
- this.pos++;
2013
- }
2014
-
2015
- if (text) {
2016
- const node = /** @type {ESTreeJSX.JSXText} */ ({
2017
- type: 'JSXText',
2018
- value: text,
2019
- raw: text,
2020
- start,
2021
- end: this.pos,
2022
- });
2023
- body.push(node);
2024
- }
2025
-
2026
- // Always call next() to ensure parser makes progress
2027
- this.next();
2028
- }
2029
- }
2030
- }
2031
- if (inside_tsx_compat) {
2032
- this.exprAllowed = true;
2033
-
2034
- while (true) {
2035
- if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
2036
- if (!this.#loose) {
2037
- this.raise(
2038
- this.start,
2039
- `Unclosed tag '<tsx:${inside_tsx_compat.kind}>'. Expected '</tsx:${inside_tsx_compat.kind}>' before end of component.`,
2040
- );
2041
- } else {
2042
- inside_tsx_compat.unclosed = true;
2043
- /** @type {AST.NodeWithLocation} */ (inside_tsx_compat).loc.end = {
2044
- .../** @type {AST.SourceLocation} */ (inside_tsx_compat.openingElement.loc).end,
2045
- };
2046
- inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
2047
- }
2048
- return;
2049
- }
2050
-
2051
- if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
2052
- return;
2053
- }
2054
-
2055
- if (this.type === tt.braceL) {
2056
- const node = this.jsx_parseExpressionContainer();
2057
- body.push(node);
2058
- } else if (this.type === tstt.jsxTagStart) {
2059
- // Parse JSX element
2060
- const node = super.parseExpression();
2061
- body.push(node);
2062
- } else {
2063
- const start = this.start;
2064
- this.pos = start;
2065
- let text = '';
2066
-
2067
- while (this.pos < this.input.length) {
2068
- const ch = this.input.charCodeAt(this.pos);
2069
-
2070
- // Stop at opening tag, expression, or the component-closing brace
2071
- if (ch === 60 || ch === 123 || ch === 125) {
2072
- // < or { or }
2073
- break;
2074
- }
2075
-
2076
- text += this.input[this.pos];
2077
- this.pos++;
2078
- }
2079
-
2080
- if (text) {
2081
- const node = /** @type {ESTreeJSX.JSXText} */ ({
2082
- type: 'JSXText',
2083
- value: text,
2084
- raw: text,
2085
- start,
2086
- end: this.pos,
2087
- });
2088
- body.push(node);
2089
- }
2090
-
2091
- this.next();
2092
- }
2093
- }
2094
- }
2095
- if (this.type === tt.braceL) {
2096
- const node = this.jsx_parseExpressionContainer();
2097
- // Keep JSXEmptyExpression as-is (for prettier to handle comments)
2098
- // but convert other expressions to Html/RippleExpression/Text nodes
2099
- if (node.expression.type !== 'JSXEmptyExpression') {
2100
- /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (
2101
- /** @type {unknown} */ (node)
2102
- ).type = node.html ? 'Html' : node.text ? 'Text' : 'RippleExpression';
2103
- delete node.html;
2104
- delete node.text;
2105
- }
2106
- body.push(node);
2107
- } else if (this.type === tt.braceR) {
2108
- // Leaving a component/template body. We may still be in TSX/JSX tokenization
2109
- // context (e.g. after parsing markup), but the closing `}` is a JS token.
2110
- // If we don't reset this here, the following `next()` can read EOF using
2111
- // `jsx_readToken()` and throw "Unterminated JSX contents".
2112
- while (this.curContext() === tstc.tc_expr) {
2113
- this.context.pop();
2114
- }
2115
- return;
2116
- } else if (this.type === tstt.jsxTagStart) {
2117
- const startPos = this.start;
2118
- const startLoc = this.startLoc;
2119
- this.next();
2120
- if (this.value === '/' || this.type === tt.slash) {
2121
- // Consume '/'
2122
- this.next();
2123
-
2124
- const closingElement =
2125
- /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
2126
- this.jsx_parseClosingElementAt(startPos, startLoc)
2127
- );
2128
- this.exprAllowed = false;
2129
-
2130
- // Validate that the closing tag matches the opening tag
2131
- const currentElement = this.#path[this.#path.length - 1];
2132
- if (
2133
- !currentElement ||
2134
- (currentElement.type !== 'Element' &&
2135
- currentElement.type !== 'Tsx' &&
2136
- currentElement.type !== 'TsxCompat')
2137
- ) {
2138
- this.raise(this.start, 'Unexpected closing tag');
2139
- }
2140
-
2141
- /** @type {string | null} */
2142
- let openingTagName;
2143
- /** @type {string | null} */
2144
- let closingTagName;
2145
-
2146
- if (currentElement.type === 'TsxCompat') {
2147
- openingTagName = 'tsx:' + currentElement.kind;
2148
- closingTagName =
2149
- closingElement.name?.type === 'JSXNamespacedName'
2150
- ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2151
- : this.getElementName(closingElement.name);
2152
- } else if (currentElement.type === 'Tsx') {
2153
- openingTagName = 'tsx';
2154
- closingTagName =
2155
- closingElement.name?.type === 'JSXNamespacedName'
2156
- ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2157
- : this.getElementName(closingElement.name);
2158
- } else {
2159
- // Regular Element node
2160
- openingTagName = this.getElementName(currentElement.id);
2161
- closingTagName =
2162
- closingElement.name?.type === 'JSXNamespacedName'
2163
- ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2164
- : this.getElementName(closingElement.name);
2165
- }
2166
-
2167
- if (openingTagName !== closingTagName) {
2168
- if (!this.#loose) {
2169
- this.raise(
2170
- closingElement.start,
2171
- `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
2172
- );
2173
- } else {
2174
- // Loop through all unclosed elements on the stack
2175
- while (this.#path.length > 0) {
2176
- const elem = this.#path[this.#path.length - 1];
2177
-
2178
- // Stop at non-Element boundaries (Component, etc.)
2179
- if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
2180
- break;
2181
- }
2182
-
2183
- const elemName =
2184
- elem.type === 'TsxCompat'
2185
- ? 'tsx:' + elem.kind
2186
- : elem.type === 'Tsx'
2187
- ? 'tsx'
2188
- : this.getElementName(elem.id);
2189
-
2190
- // Found matching opening tag
2191
- if (elemName === closingTagName) {
2192
- break;
2193
- }
2194
-
2195
- // Mark as unclosed and adjust location
2196
- elem.unclosed = true;
2197
- /** @type {AST.NodeWithLocation} */ (elem).loc.end = {
2198
- .../** @type {AST.SourceLocation} */ (elem.openingElement.loc).end,
2199
- };
2200
- elem.end = elem.openingElement.end;
2201
-
2202
- this.#path.pop(); // Remove from stack
2203
- }
2204
- }
2205
- }
2206
-
2207
- const elementToClose = this.#path[this.#path.length - 1];
2208
- if (elementToClose && elementToClose.type === 'Element') {
2209
- const elementToCloseName = this.getElementName(
2210
- /** @type {AST.Element} */ (elementToClose).id,
2211
- );
2212
- if (elementToCloseName === closingTagName) {
2213
- /** @type {AST.Element} */ (elementToClose).closingElement = closingElement;
2214
- }
2215
- }
2216
-
2217
- this.#path.pop();
2218
- skipWhitespace(this);
2219
- return;
2220
- }
2221
- const node = this.parseElement();
2222
- if (node !== null) {
2223
- body.push(node);
2224
- }
2225
- } else {
2226
- skipWhitespace(this);
2227
- const node = this.parseStatement(null);
2228
- body.push(node);
2229
-
2230
- // Ensure we're not in JSX context before recursing
2231
- // This is important when elements are parsed at statement level
2232
- if (this.curContext() === tstc.tc_expr) {
2233
- this.context.pop();
2234
- }
2235
- }
2236
-
2237
- this.parseTemplateBody(body);
2238
- }
2239
-
2240
- /**
2241
- * @type {Parse.Parser['parseStatement']}
2242
- */
2243
- parseStatement(context, topLevel, exports) {
2244
- if (
2245
- context !== 'for' &&
2246
- context !== 'if' &&
2247
- this.context.at(-1) === b_stat &&
2248
- this.type === tt.braceL &&
2249
- this.context.some((c) => c === tstc.tc_expr)
2250
- ) {
2251
- const node = this.jsx_parseExpressionContainer();
2252
- // Keep JSXEmptyExpression as-is (don't convert to RippleExpression/Text/Html)
2253
- if (node.expression.type !== 'JSXEmptyExpression') {
2254
- /** @type {AST.RippleExpression | AST.Html | AST.TextNode} */ (
2255
- /** @type {unknown} */ (node)
2256
- ).type = node.html ? 'Html' : node.text ? 'Text' : 'RippleExpression';
2257
- delete node.html;
2258
- delete node.text;
2259
- }
2260
-
2261
- return /** @type {ESTreeJSX.JSXEmptyExpression | AST.RippleExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2262
- /** @type {unknown} */ (node)
2263
- );
2264
- }
2265
-
2266
- if (this.value === '#server') {
2267
- // Peek ahead to see if this is a server block (#server { ... }) vs
2268
- // a server identifier expression (#server.fn(), #server.fn().then())
2269
- let peek_pos = this.end;
2270
- while (peek_pos < this.input.length && /\s/.test(this.input[peek_pos])) peek_pos++;
2271
- if (peek_pos < this.input.length && this.input.charCodeAt(peek_pos) === 123) {
2272
- // Next non-whitespace character is '{' — parse as server block
2273
- return this.parseServerBlock();
2274
- }
2275
- // Otherwise fall through to parse as expression statement (e.g., #server.fn().then(...))
2276
- }
2277
-
2278
- if (this.value === 'component') {
2279
- this.awaitPos = 0;
2280
- return this.parseComponent({ requireName: true, declareName: true });
2281
- }
2282
-
2283
- if (this.type === tstt.jsxTagStart) {
2284
- this.next();
2285
- if (this.value === '/') {
2286
- this.unexpected();
2287
- }
2288
- const node = this.parseElement();
2289
-
2290
- if (!node) {
2291
- this.unexpected();
2292
- }
2293
- return node;
2294
- }
2295
-
2296
- // &[ or &{ at statement level — lazy destructuring assignment
2297
- // e.g., &[data] = track(0); or &{x, y} = obj;
2298
- if (this.type === tt.bitwiseAND) {
2299
- const charAfterAmp = this.input.charCodeAt(this.end);
2300
- if (charAfterAmp === 123 || charAfterAmp === 91) {
2301
- const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
2302
- const assign_node = /** @type {AST.AssignmentExpression} */ (this.startNode());
2303
- this.next(); // consume &
2304
- // Parse the left-hand side (array or object expression)
2305
- const left = /** @type {AST.ArrayPattern | AST.ObjectPattern} */ (
2306
- /** @type {unknown} */ (this.parseExprAtom())
2307
- );
2308
- // Convert expression to destructuring pattern
2309
- this.toAssignable(left, false);
2310
- left.lazy = true;
2311
- // Expect = operator
2312
- this.expect(tt.eq);
2313
- // Parse the right-hand side
2314
- assign_node.operator = '=';
2315
- assign_node.left = left;
2316
- assign_node.right = /** @type {AST.Expression} */ (this.parseMaybeAssign());
2317
- node.expression = /** @type {AST.AssignmentExpression} */ (
2318
- this.finishNode(assign_node, 'AssignmentExpression')
2319
- );
2320
- this.semicolon();
2321
- return /** @type {AST.ExpressionStatement} */ (
2322
- this.finishNode(node, 'ExpressionStatement')
2323
- );
2324
- }
2325
- }
2326
-
2327
- return super.parseStatement(context, topLevel, exports);
2328
- }
2329
-
2330
- /**
2331
- * @type {Parse.Parser['parseBlock']}
2332
- */
2333
- parseBlock(createNewLexicalScope, node, exitStrict) {
2334
- const parent = this.#path.at(-1);
2335
-
2336
- if (parent?.type === 'Component' || parent?.type === 'Element') {
2337
- if (createNewLexicalScope === void 0) createNewLexicalScope = true;
2338
- if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
2339
-
2340
- node.body = [];
2341
- this.expect(tt.braceL);
2342
- if (createNewLexicalScope) {
2343
- this.enterScope(0);
2344
- }
2345
- this.parseTemplateBody(node.body);
2346
-
2347
- if (exitStrict) {
2348
- this.strict = false;
2349
- }
2350
- this.exprAllowed = true;
2351
-
2352
- this.next();
2353
- if (createNewLexicalScope) {
2354
- this.exitScope();
2355
- }
2356
- return this.finishNode(node, 'BlockStatement');
2357
- }
2358
-
2359
- return super.parseBlock(createNewLexicalScope, node, exitStrict);
2360
- }
2361
- }
2362
-
2363
- return /** @type {Parse.ParserConstructor} */ (RippleParser);
2364
- };
2365
- }
2366
-
2367
- /**
2368
- * Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
2369
- * to add them after the fact. They are needed in order to support `ripple-ignore` comments
2370
- * in JS code and so that `prettier-plugin` doesn't remove all comments when formatting.
2371
- * @param {string} source
2372
- * @param {AST.CommentWithLocation[]} comments
2373
- * @param {number} [index=0] - Starting index
2374
- * @returns {{onComment: Parse.Options['onComment'], add_comments: (ast: AST.Node) => void}}
2375
- */
2376
- function get_comment_handlers(source, comments, index = 0) {
2377
- /**
2378
- * @param {string} text
2379
- * @param {number} startIndex
2380
- * @returns {string | null}
2381
- */
2382
- function getNextNonWhitespaceCharacter(text, startIndex) {
2383
- for (let i = startIndex; i < text.length; i++) {
2384
- const char = text[i];
2385
- if (char !== ' ' && char !== '\t' && char !== '\n' && char !== '\r') {
2386
- return char;
2387
- }
2388
- }
2389
- return null;
2390
- }
2391
-
2392
- return {
2393
- /**
2394
- * @type {Parse.Options['onComment']}
2395
- */
2396
- onComment: (block, value, start, end, start_loc, end_loc, metadata) => {
2397
- if (block && /\n/.test(value)) {
2398
- let a = start;
2399
- while (a > 0 && source[a - 1] !== '\n') a -= 1;
2400
-
2401
- let b = a;
2402
- while (/[ \t]/.test(source[b])) b += 1;
2403
-
2404
- const indentation = source.slice(a, b);
2405
- value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
2406
- }
2407
-
2408
- comments.push({
2409
- type: block ? 'Block' : 'Line',
2410
- value,
2411
- start,
2412
- end,
2413
- loc: {
2414
- start: start_loc,
2415
- end: end_loc,
2416
- },
2417
- context: metadata ?? null,
2418
- });
2419
- },
2420
-
2421
- /**
2422
- * @param {AST.Node | AST.CSS.StyleSheet} ast
2423
- */
2424
- add_comments: (ast) => {
2425
- if (comments.length === 0) return;
2426
-
2427
- comments = comments
2428
- .filter((comment) => comment.start >= index)
2429
- .map(({ type, value, start, end, loc, context }) => ({
2430
- type,
2431
- value,
2432
- start,
2433
- end,
2434
- loc,
2435
- context,
2436
- }));
2437
-
2438
- walk(ast, null, {
2439
- _(node, { next, path }) {
2440
- const metadata = /** @type {AST.Node} */ (node)?.metadata;
2441
-
2442
- /**
2443
- * Check if a comment is inside an attribute expression
2444
- * of any ancestor Elements.
2445
- * @returns {boolean}
2446
- */
2447
- function isCommentInsideAttributeExpression() {
2448
- for (let i = path.length - 1; i >= 0; i--) {
2449
- const ancestor = path[i];
2450
- if (
2451
- ancestor &&
2452
- (ancestor.type === 'JSXAttribute' ||
2453
- ancestor.type === 'Attribute' ||
2454
- ancestor.type === 'JSXExpressionContainer')
2455
- ) {
2456
- return true;
2457
- }
2458
- }
2459
- return false;
2460
- }
2461
-
2462
- /**
2463
- * Check if a comment is inside any attribute of ancestor Elements,
2464
- * but NOT if we're currently traversing inside that attribute.
2465
- * @param {AST.CommentWithLocation} comment
2466
- * @returns {boolean}
2467
- */
2468
- function isCommentInsideUnvisitedAttribute(comment) {
2469
- for (let i = path.length - 1; i >= 0; i--) {
2470
- const ancestor = path[i];
2471
- // we would definitely reach the attribute first before getting to the element
2472
- if (ancestor.type === 'JSXAttribute' || ancestor.type === 'Attribute') {
2473
- return false;
2474
- }
2475
- if (ancestor && ancestor.type === 'Element') {
2476
- for (const attr of /** @type {(AST.Attribute & AST.NodeWithLocation)[]} */ (
2477
- ancestor.attributes
2478
- )) {
2479
- if (comment.start >= attr.start && comment.end <= attr.end) {
2480
- return true;
2481
- }
2482
- }
2483
- }
2484
- }
2485
- return false;
2486
- }
2487
-
2488
- /**
2489
- * If a comment is located between an empty Element's opening and closing tags,
2490
- * attach it to the Element as `innerComments`.
2491
- * @param {AST.CommentWithLocation} comment
2492
- * @returns {AST.Element | null}
2493
- */
2494
- function getEmptyElementInnerCommentTarget(comment) {
2495
- const element = /** @type {AST.Element | undefined} */ (
2496
- path.findLast((ancestor) => ancestor && ancestor.type === 'Element')
2497
- );
2498
- if (
2499
- !element ||
2500
- element.children.length > 0 ||
2501
- !element.closingElement ||
2502
- !(
2503
- comment.start >= /** @type {AST.NodeWithLocation} */ (element.openingElement).end &&
2504
- comment.end <= /** @type {AST.NodeWithLocation} */ (element).end
2505
- )
2506
- ) {
2507
- return null;
2508
- }
2509
-
2510
- return element;
2511
- }
2512
-
2513
- // Skip CSS nodes entirely - they use CSS-local positions (relative to
2514
- // the <style> tag content) which would incorrectly match against
2515
- // absolute source positions of JS/HTML comments. Also consume any
2516
- // CSS comments (which have absolute positions) that fall within the
2517
- // parent <style> element's content range so they don't leak to
2518
- // subsequent JS nodes.
2519
- if (node.type === 'StyleSheet') {
2520
- const styleElement = /** @type {AST.Element & AST.NodeWithLocation | undefined} */ (
2521
- path.findLast(
2522
- (ancestor) =>
2523
- ancestor &&
2524
- ancestor.type === 'Element' &&
2525
- ancestor.id &&
2526
- /** @type {AST.Identifier} */ (ancestor.id).name === 'style',
2527
- )
2528
- );
2529
- if (styleElement) {
2530
- const cssStart =
2531
- /** @type {AST.NodeWithLocation} */ (styleElement.openingElement)?.end ??
2532
- styleElement.start;
2533
- const cssEnd =
2534
- /** @type {AST.NodeWithLocation} */ (styleElement.closingElement)?.start ??
2535
- styleElement.end;
2536
- while (comments[0] && comments[0].start >= cssStart && comments[0].end <= cssEnd) {
2537
- comments.shift();
2538
- }
2539
- }
2540
- return;
2541
- }
2542
-
2543
- if (metadata && metadata.commentContainerId !== undefined) {
2544
- // For empty template elements, keep comments as `innerComments`.
2545
- // The Prettier plugin uses `innerComments` to preserve them and
2546
- // to avoid collapsing the element into self-closing syntax.
2547
- const isEmptyElement =
2548
- node.type === 'Element' && (!node.children || node.children.length === 0);
2549
- if (!isEmptyElement) {
2550
- while (
2551
- comments[0] &&
2552
- comments[0].context &&
2553
- comments[0].context.containerId === metadata.commentContainerId &&
2554
- comments[0].context.beforeMeaningfulChild
2555
- ) {
2556
- // Check that the comment is actually in this element's own content
2557
- // area, not positionally inside a child element. This handles the
2558
- // case where jsx_parseOpeningElementAt() triggers jsx_readToken()
2559
- // before the child element is pushed to the parser's #path, causing
2560
- // comments inside the child to get the parent's containerId.
2561
- const commentStart = comments[0].start;
2562
- const isInsideChildElement = /** @type {AST.NodeWithChildren} */ (
2563
- node
2564
- ).children?.some(
2565
- (child) =>
2566
- child &&
2567
- child.start !== undefined &&
2568
- child.end !== undefined &&
2569
- commentStart >= child.start &&
2570
- commentStart < child.end,
2571
- );
2572
- if (isInsideChildElement) break;
2573
-
2574
- const elementComment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2575
-
2576
- (metadata.elementLeadingComments ||= []).push(elementComment);
2577
- }
2578
- }
2579
- }
2580
-
2581
- while (
2582
- comments[0] &&
2583
- comments[0].start < /** @type {AST.NodeWithLocation} */ (node).start
2584
- ) {
2585
- // Skip comments that are inside an attribute of an ancestor Element.
2586
- // Since zimmerframe visits children before attributes, we need to leave
2587
- // these comments for when the attribute nodes are visited.
2588
- if (
2589
- isCommentInsideUnvisitedAttribute(
2590
- /** @type {AST.CommentWithLocation} */ (comments[0]),
2591
- )
2592
- ) {
2593
- break;
2594
- }
2595
-
2596
- const maybeInner = getEmptyElementInnerCommentTarget(
2597
- /** @type {AST.CommentWithLocation} */ (comments[0]),
2598
- );
2599
- if (maybeInner) {
2600
- (maybeInner.innerComments ||= []).push(
2601
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2602
- );
2603
- continue;
2604
- }
2605
-
2606
- const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2607
-
2608
- // Skip leading comments for BlockStatement that is a function body
2609
- // These comments should be dangling on the function instead
2610
- if (node.type === 'BlockStatement') {
2611
- const parent = path.at(-1);
2612
- if (
2613
- parent &&
2614
- (parent.type === 'FunctionDeclaration' ||
2615
- parent.type === 'FunctionExpression' ||
2616
- parent.type === 'ArrowFunctionExpression') &&
2617
- parent.body === node
2618
- ) {
2619
- // This is a function body - don't attach comment, let it be handled by function
2620
- (parent.comments ||= []).push(comment);
2621
- continue;
2622
- }
2623
- }
2624
-
2625
- if (isCommentInsideAttributeExpression()) {
2626
- (node.leadingComments ||= []).push(comment);
2627
- continue;
2628
- }
2629
-
2630
- const ancestorElements = /** @type {(AST.Element & AST.NodeWithLocation)[]} */ (
2631
- path.filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
2632
- ).sort((a, b) => a.loc.start.line - b.loc.start.line);
2633
-
2634
- const targetAncestor = ancestorElements.find(
2635
- (ancestor) => comment.loc.start.line < ancestor.loc.start.line,
2636
- );
2637
-
2638
- if (targetAncestor) {
2639
- targetAncestor.metadata ??= { path: [] };
2640
- (targetAncestor.metadata.elementLeadingComments ||= []).push(comment);
2641
- continue;
2642
- }
2643
-
2644
- (node.leadingComments ||= []).push(comment);
2645
- }
2646
-
2647
- next();
2648
-
2649
- if (comments[0]) {
2650
- if (node.type === 'Program' && node.body.length === 0) {
2651
- // Collect all comments in an empty program (file with only comments)
2652
- while (comments.length) {
2653
- const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2654
- (node.innerComments ||= []).push(comment);
2655
- }
2656
- if (node.innerComments && node.innerComments.length > 0) {
2657
- return;
2658
- }
2659
- }
2660
- if (node.type === 'BlockStatement' && node.body.length === 0) {
2661
- // Collect all comments that fall within this empty block
2662
- while (
2663
- comments[0] &&
2664
- comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end &&
2665
- comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
2666
- ) {
2667
- const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2668
- (node.innerComments ||= []).push(comment);
2669
- }
2670
- if (node.innerComments && node.innerComments.length > 0) {
2671
- return;
2672
- }
2673
- }
2674
- // Handle JSXEmptyExpression - these represent {/* comment */} in JSX
2675
- if (node.type === 'JSXEmptyExpression') {
2676
- // Collect all comments that fall within this JSXEmptyExpression
2677
- while (
2678
- comments[0] &&
2679
- comments[0].start >= /** @type {AST.NodeWithLocation} */ (node).start &&
2680
- comments[0].end <= /** @type {AST.NodeWithLocation} */ (node).end
2681
- ) {
2682
- const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2683
- (node.innerComments ||= []).push(comment);
2684
- }
2685
- if (node.innerComments && node.innerComments.length > 0) {
2686
- return;
2687
- }
2688
- }
2689
- // Handle empty Element nodes the same way as empty BlockStatements
2690
- if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
2691
- // Collect all comments that fall within this empty element
2692
- while (
2693
- comments[0] &&
2694
- comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end &&
2695
- comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
2696
- ) {
2697
- const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2698
- (node.innerComments ||= []).push(comment);
2699
- }
2700
- if (node.innerComments && node.innerComments.length > 0) {
2701
- return;
2702
- }
2703
- }
2704
-
2705
- const parent = /** @type {AST.Node & AST.NodeWithLocation} */ (path.at(-1));
2706
-
2707
- if (parent === undefined || node.end !== parent.end) {
2708
- const slice = source.slice(node.end, comments[0].start);
2709
-
2710
- // Check if this node is the last item in an array-like structure
2711
- let is_last_in_array = false;
2712
- /** @type {(AST.Node | null)[] | null} */
2713
- let node_array = null;
2714
- let isParam = false;
2715
- let isArgument = false;
2716
- let isSwitchCaseSibling = false;
2717
-
2718
- if (parent) {
2719
- if (
2720
- parent.type === 'BlockStatement' ||
2721
- parent.type === 'Program' ||
2722
- parent.type === 'Component' ||
2723
- parent.type === 'ClassBody'
2724
- ) {
2725
- node_array = parent.body;
2726
- } else if (parent.type === 'SwitchStatement') {
2727
- node_array = parent.cases;
2728
- isSwitchCaseSibling = true;
2729
- } else if (parent.type === 'SwitchCase') {
2730
- node_array = parent.consequent;
2731
- } else if (parent.type === 'ArrayExpression') {
2732
- node_array = parent.elements;
2733
- } else if (parent.type === 'ObjectExpression') {
2734
- node_array = parent.properties;
2735
- } else if (
2736
- parent.type === 'FunctionDeclaration' ||
2737
- parent.type === 'FunctionExpression' ||
2738
- parent.type === 'ArrowFunctionExpression'
2739
- ) {
2740
- node_array = parent.params;
2741
- isParam = true;
2742
- } else if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
2743
- node_array = parent.arguments;
2744
- isArgument = true;
2745
- }
2746
- }
2747
-
2748
- if (node_array && Array.isArray(node_array)) {
2749
- is_last_in_array = node_array.indexOf(node) === node_array.length - 1;
2750
- }
2751
-
2752
- if (is_last_in_array) {
2753
- if (isParam || isArgument) {
2754
- while (comments.length) {
2755
- const potentialComment = comments[0];
2756
- if (parent && potentialComment.start >= parent.end) {
2757
- break;
2758
- }
2759
-
2760
- const maybeInner = getEmptyElementInnerCommentTarget(potentialComment);
2761
- if (maybeInner) {
2762
- (maybeInner.innerComments ||= []).push(
2763
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2764
- );
2765
- continue;
2766
- }
2767
-
2768
- const nextChar = getNextNonWhitespaceCharacter(source, potentialComment.end);
2769
- if (nextChar === ')') {
2770
- (node.trailingComments ||= []).push(
2771
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2772
- );
2773
- continue;
2774
- }
2775
-
2776
- break;
2777
- }
2778
- } else {
2779
- // Special case: There can be multiple trailing comments after the last node in a block,
2780
- // and they can be separated by newlines
2781
- while (comments.length) {
2782
- const comment = comments[0];
2783
- if (parent && comment.start >= parent.end) break;
2784
-
2785
- const maybeInner = getEmptyElementInnerCommentTarget(comment);
2786
- if (maybeInner) {
2787
- (maybeInner.innerComments ||= []).push(
2788
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2789
- );
2790
- continue;
2791
- }
2792
-
2793
- (node.trailingComments ||= []).push(comment);
2794
- comments.shift();
2795
- }
2796
- }
2797
- } else if (/** @type {AST.NodeWithLocation} */ (node).end <= comments[0].start) {
2798
- const maybeInner = getEmptyElementInnerCommentTarget(
2799
- /** @type {AST.CommentWithLocation} */ (comments[0]),
2800
- );
2801
- if (maybeInner) {
2802
- (maybeInner.innerComments ||= []).push(
2803
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2804
- );
2805
- return;
2806
- }
2807
-
2808
- const onlySimpleWhitespace = /^[,) \t]*$/.test(slice);
2809
- const onlyWhitespace = /^\s*$/.test(slice);
2810
- const hasBlankLine = /\n\s*\n/.test(slice);
2811
- const nodeEndLine = node.loc?.end?.line ?? null;
2812
- const commentStartLine = comments[0].loc?.start?.line ?? null;
2813
- const isImmediateNextLine =
2814
- nodeEndLine !== null &&
2815
- commentStartLine !== null &&
2816
- commentStartLine === nodeEndLine + 1;
2817
-
2818
- if (isSwitchCaseSibling && !is_last_in_array) {
2819
- if (
2820
- nodeEndLine !== null &&
2821
- commentStartLine !== null &&
2822
- nodeEndLine === commentStartLine
2823
- ) {
2824
- node.trailingComments = [
2825
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2826
- ];
2827
- }
2828
- return;
2829
- }
2830
-
2831
- if (
2832
- onlySimpleWhitespace ||
2833
- (onlyWhitespace && !hasBlankLine && isImmediateNextLine)
2834
- ) {
2835
- // Check if this is a block comment that's inline with the next statement
2836
- // e.g., /** @type {SomeType} */ (a) = 5;
2837
- // These should be leading comments, not trailing
2838
- if (comments[0].type === 'Block' && !is_last_in_array && node_array) {
2839
- const currentIndex = node_array.indexOf(node);
2840
- const nextSibling = node_array[currentIndex + 1];
2841
-
2842
- if (nextSibling && nextSibling.loc) {
2843
- const commentEndLine = comments[0].loc?.end?.line;
2844
- const nextSiblingStartLine = nextSibling.loc?.start?.line;
2845
-
2846
- // If comment ends on same line as next sibling starts, it's inline with next
2847
- if (commentEndLine === nextSiblingStartLine) {
2848
- // Leave it for next sibling's leading comments
2849
- return;
2850
- }
2851
- }
2852
- }
2853
-
2854
- // For function parameters, only attach as trailing comment if it's on the same line
2855
- // Comments on next line after comma should be leading comments of next parameter
2856
- if (isParam) {
2857
- // Check if comment is on same line as the node
2858
- const nodeEndLine = source.slice(0, node.end).split('\n').length;
2859
- const commentStartLine = source.slice(0, comments[0].start).split('\n').length;
2860
- if (nodeEndLine === commentStartLine) {
2861
- node.trailingComments = [
2862
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2863
- ];
2864
- }
2865
- // Otherwise leave it for next parameter's leading comments
2866
- } else {
2867
- // Line comments on the next line should be leading comments
2868
- // for the next statement, not trailing comments for this one.
2869
- // Only attach as trailing if:
2870
- // 1. It's on the same line as this node, OR
2871
- // 2. This is the last item in the array (no next sibling to attach to)
2872
- const commentOnSameLine =
2873
- nodeEndLine !== null &&
2874
- commentStartLine !== null &&
2875
- nodeEndLine === commentStartLine;
2876
-
2877
- if (commentOnSameLine || is_last_in_array) {
2878
- node.trailingComments = [
2879
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2880
- ];
2881
- }
2882
- // Otherwise leave it for next sibling's leading comments
2883
- }
2884
- } else if (hasBlankLine && onlyWhitespace && node_array) {
2885
- // When there's a blank line between node and comment(s),
2886
- // check if there's also a blank line after the comment(s) before the next node
2887
- // If so, attach comments as trailing to preserve the grouping
2888
- // Only do this for statement-level contexts (BlockStatement, Program),
2889
- // not for Element children or other contexts
2890
- const isStatementContext =
2891
- parent.type === 'BlockStatement' || parent.type === 'Program';
2892
-
2893
- // Don't apply for Component - let Prettier handle comment attachment there
2894
- // Component bodies have different comment handling via metadata.elementLeadingComments
2895
- if (!isStatementContext) {
2896
- return;
2897
- }
2898
-
2899
- const currentIndex = node_array.indexOf(node);
2900
- const nextSibling = node_array[currentIndex + 1];
2901
-
2902
- if (nextSibling && nextSibling.loc) {
2903
- // Find where the comment block ends
2904
- let lastCommentIndex = 0;
2905
- let lastCommentEnd = comments[0].end;
2906
-
2907
- // Collect consecutive comments (without blank lines between them)
2908
- while (comments[lastCommentIndex + 1]) {
2909
- const currentComment = comments[lastCommentIndex];
2910
- const nextComment = comments[lastCommentIndex + 1];
2911
- const sliceBetween = source.slice(currentComment.end, nextComment.start);
2912
-
2913
- // If there's a blank line, stop
2914
- if (/\n\s*\n/.test(sliceBetween)) {
2915
- break;
2916
- }
2917
-
2918
- lastCommentIndex++;
2919
- lastCommentEnd = nextComment.end;
2920
- }
2921
-
2922
- // Check if there's a blank line after the last comment and before next sibling
2923
- const sliceAfterComments = source.slice(lastCommentEnd, nextSibling.start);
2924
- const hasBlankLineAfter = /\n\s*\n/.test(sliceAfterComments);
2925
-
2926
- if (hasBlankLineAfter) {
2927
- // Don't attach comments as trailing if next sibling is an Element
2928
- // and any comment falls within the Element's line range
2929
- // This means the comments are inside the Element (between opening and closing tags)
2930
- const nextIsElement = nextSibling.type === 'Element';
2931
- const commentsInsideElement =
2932
- nextIsElement &&
2933
- nextSibling.loc &&
2934
- comments.some((c) => {
2935
- if (!c.loc) return false;
2936
- // Check if comment is on a line between Element's start and end lines
2937
- return (
2938
- c.loc.start.line >= nextSibling.loc.start.line &&
2939
- c.loc.end.line <= nextSibling.loc.end.line
2940
- );
2941
- });
2942
-
2943
- if (!commentsInsideElement) {
2944
- // Attach all the comments as trailing
2945
- for (let i = 0; i <= lastCommentIndex; i++) {
2946
- (node.trailingComments ||= []).push(
2947
- /** @type {AST.CommentWithLocation} */ (comments.shift()),
2948
- );
2949
- }
2950
- }
2951
- }
2952
- }
2953
- }
2954
- }
2955
- }
2956
- }
2957
- },
2958
- });
2959
- },
2960
- };
2961
- }
2962
-
2963
- /**
2964
- * Parse Ripple source code into an AST
2965
- * @param {string} source
2966
- * @param {string} [filename]
2967
- * @param {ParseOptions} [options]
2968
- * @returns {AST.Program}
2969
- */
2970
- export function parse(source, filename, options) {
2971
- /** @type {AST.CommentWithLocation[]} */
2972
- const comments = [];
2973
- const output_comments = options?.comments;
2974
-
2975
- const { onComment, add_comments } = get_comment_handlers(source, comments);
2976
- /** @type {AST.Program} */
2977
- let ast;
2978
-
2979
- try {
2980
- ast = parser.parse(source, {
2981
- sourceType: 'module',
2982
- ecmaVersion: 13,
2983
- allowReturnOutsideFunction: true,
2984
- locations: true,
2985
- onComment,
2986
- rippleOptions: {
2987
- filename,
2988
- errors: options?.errors ?? [],
2989
- loose: options?.loose || false,
2990
- },
2991
- });
2992
- } catch (e) {
2993
- throw e;
2994
- }
2995
-
2996
- if (output_comments) {
2997
- // Copy comments to output array
2998
- // as add_comments modifies the original array (e.g. shift)
2999
- for (let i = 0; i < comments.length; i++) {
3000
- output_comments.push(comments[i]);
3001
- }
3002
- }
3003
-
3004
- add_comments(ast);
3005
-
3006
- return ast;
3007
- }