@ripple-ts/prettier-plugin 0.2.208 → 0.2.211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,11 +1,121 @@
1
- // @ts-nocheck
1
+ /**
2
+ * @import { Doc, AstPath, ParserOptions } from 'prettier'
3
+ */
4
+
5
+ /// <reference types="@types/estree" />
6
+ /// <reference types="@types/estree-jsx" />
7
+
8
+ /**
9
+ * Re-export estree types for use in JSDoc. The Ripple compiler augments these
10
+ * with additional node types like Component, Element, etc.
11
+ * @typedef {import('estree').Node} Node
12
+ * @typedef {import('estree').Program} Program
13
+ * @typedef {import('estree').Comment} Comment
14
+ * @typedef {import('estree').Expression} Expression
15
+ * @typedef {import('estree').Pattern} Pattern
16
+ * @typedef {import('estree').Statement} Statement
17
+ * @typedef {import('estree').Identifier} Identifier
18
+ * @typedef {import('estree').Literal} Literal
19
+ * @typedef {import('estree').FunctionDeclaration} FunctionDeclaration
20
+ * @typedef {import('estree').FunctionExpression} FunctionExpression
21
+ * @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression
22
+ * @typedef {import('estree').ImportDeclaration} ImportDeclaration
23
+ * @typedef {import('estree').ExportNamedDeclaration} ExportNamedDeclaration
24
+ * @typedef {import('estree').ObjectExpression} ObjectExpression
25
+ * @typedef {import('estree').Property} Property
26
+ * @typedef {import('estree').MethodDefinition} MethodDefinition
27
+ * @typedef {import('estree').CallExpression} CallExpression
28
+ * @typedef {import('estree').NewExpression} NewExpression
29
+ * @typedef {import('estree').BinaryExpression} BinaryExpression
30
+ * @typedef {import('estree').LogicalExpression} LogicalExpression
31
+ * @typedef {import('estree').SourceLocation} SourceLocation
32
+ * @typedef {import('estree').SpreadElement} SpreadElement
33
+ * @typedef {import('estree').ImportSpecifier} ImportSpecifier
34
+ * @typedef {import('estree').ExportSpecifier} ExportSpecifier
35
+ * @typedef {import('estree').BlockStatement} BlockStatement
36
+ * @typedef {import('estree').VariableDeclaration} VariableDeclaration
37
+ * @typedef {import('estree').VariableDeclarator} VariableDeclarator
38
+ */
39
+
40
+ /**
41
+ * JSX types from estree-jsx
42
+ * @typedef {import('estree-jsx').JSXElement} JSXElement
43
+ * @typedef {import('estree-jsx').JSXFragment} JSXFragment
44
+ * @typedef {import('estree-jsx').JSXAttribute} JSXAttribute
45
+ * @typedef {import('estree-jsx').JSXSpreadAttribute} JSXSpreadAttribute
46
+ * @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier
47
+ * @typedef {import('estree-jsx').JSXExpressionContainer} JSXExpressionContainer
48
+ * @typedef {import('estree-jsx').JSXEmptyExpression} JSXEmptyExpression
49
+ */
50
+
51
+ /**
52
+ * Ripple-specific AST node types. These are defined by the Ripple compiler
53
+ * and extend the standard ESTree AST. The node type is intentionally flexible
54
+ * to accommodate all ESTree, JSX, and Ripple-specific node types. The printer
55
+ * performs runtime type checking via node.type to determine how to format each node.
56
+ *
57
+ * We use `any` as the base type because the printer must handle:
58
+ * - Standard ESTree nodes (Program, FunctionDeclaration, etc.)
59
+ * - ESTree-JSX nodes (JSXElement, JSXAttribute, etc.)
60
+ * - TypeScript AST extensions (TSTypeAnnotation, TSParameterProperty, etc.)
61
+ * - Ripple-specific nodes (Component, Element, TrackedExpression, etc.)
62
+ * - Comment nodes which have a different structure
63
+ *
64
+ * Runtime type checking via node.type ensures safe property access.
65
+ *
66
+ * @typedef {any} RippleASTNode
67
+ */
68
+
69
+ /**
70
+ * Print function callback type.
71
+ * The actual print function accepts an optional args parameter, but for
72
+ * compatibility with Prettier's path.call/path.each types, we use `any`
73
+ * for the parameters.
74
+ * @typedef {(...args: any[]) => Doc} PrintFn
75
+ */
76
+
77
+ /**
78
+ * Prettier formatting options used by this plugin
79
+ * @typedef {Object} RippleFormatOptions
80
+ * @property {boolean} [singleQuote] - Use single quotes for strings
81
+ * @property {boolean} [jsxSingleQuote] - Use single quotes in JSX attributes
82
+ * @property {boolean} [semi] - Add semicolons at end of statements
83
+ * @property {'none' | 'es5' | 'all'} [trailingComma] - Trailing comma style
84
+ * @property {boolean} [useTabs] - Use tabs for indentation
85
+ * @property {number} [tabWidth] - Number of spaces per indentation level
86
+ * @property {boolean} [singleAttributePerLine] - Put each JSX attribute on its own line
87
+ * @property {boolean} [bracketSameLine] - Put closing bracket on same line as attributes
88
+ * @property {boolean} [bracketSpacing] - Print spaces between brackets in object literals
89
+ * @property {'always' | 'avoid'} [arrowParens] - Arrow function parentheses style
90
+ * @property {string} [originalText] - Original source text
91
+ * @property {(node: RippleASTNode) => number} [locEnd] - Function to get node end position
92
+ */
93
+
94
+ /**
95
+ * Context arguments passed through print function calls
96
+ * @typedef {Object} PrintArgs
97
+ * @property {boolean} [isInAttribute] - Node is inside an attribute value
98
+ * @property {boolean} [isInArray] - Node is inside an array
99
+ * @property {boolean} [allowInlineObject] - Allow single-line object formatting
100
+ * @property {boolean} [isConditionalTest] - Node is a conditional test expression
101
+ * @property {boolean} [isNestedConditional] - Node is a nested conditional
102
+ * @property {boolean} [suppressLeadingComments] - Skip printing leading comments
103
+ * @property {boolean} [suppressExpressionLeadingComments] - Skip expression leading comments
104
+ * @property {boolean} [isInlineContext] - Node is in an inline context
105
+ * @property {boolean} [isStatement] - Node is a statement
106
+ * @property {boolean} [isLogicalAndOr] - Node is logical AND/OR expression
107
+ * @property {boolean} [allowShorthandProperty] - Allow shorthand property syntax
108
+ * @property {boolean} [isFirstChild] - Node is first child of parent
109
+ * @property {boolean} [skipComponentLabel] - Skip component label in printing
110
+ * @property {boolean} [noBreakInside] - Don't break inside the expression
111
+ * @property {boolean} [expandLastArg] - Expand the last argument
112
+ */
113
+
2
114
  import { parse } from 'ripple/compiler';
3
- import { obfuscate_identifier } from 'ripple/compiler/internal/identifier/utils';
4
115
  import { doc } from 'prettier';
5
116
 
6
117
  const { builders, utils } = doc;
7
118
  const {
8
- concat,
9
119
  join,
10
120
  line,
11
121
  softline,
@@ -22,6 +132,7 @@ const {
22
132
  } = builders;
23
133
  const { willBreak } = utils;
24
134
 
135
+ /** @type {import('prettier').Plugin['languages']} */
25
136
  export const languages = [
26
137
  {
27
138
  name: 'ripple',
@@ -31,25 +142,47 @@ export const languages = [
31
142
  },
32
143
  ];
33
144
 
145
+ /** @type {import('prettier').Plugin['parsers']} */
34
146
  export const parsers = {
35
147
  ripple: {
36
148
  astFormat: 'ripple-ast',
37
- parse(text, parsers, options) {
149
+ /**
150
+ * @param {string} text
151
+ * @param {ParserOptions<RippleASTNode>} _options
152
+ * @returns {Program}
153
+ */
154
+ parse(text, _options) {
38
155
  return parse(text);
39
156
  },
40
157
 
158
+ /**
159
+ * @param {RippleASTNode} node
160
+ * @returns {number}
161
+ */
41
162
  locStart(node) {
42
163
  return node.loc.start.index;
43
164
  },
44
165
 
166
+ /**
167
+ * @param {RippleASTNode} node
168
+ * @returns {number}
169
+ */
45
170
  locEnd(node) {
46
171
  return node.loc.end.index;
47
172
  },
48
173
  },
49
174
  };
50
175
 
176
+ /** @type {import('prettier').Plugin['printers']} */
51
177
  export const printers = {
52
178
  'ripple-ast': {
179
+ /**
180
+ * @param {AstPath<RippleASTNode>} path
181
+ * @param {RippleFormatOptions} options
182
+ * @param {PrintFn} print
183
+ * @param {PrintArgs} [args]
184
+ * @returns {Doc}
185
+ */
53
186
  print(path, options, print, args) {
54
187
  const node = path.getValue();
55
188
  const parts = printRippleNode(node, path, options, print, args);
@@ -57,11 +190,15 @@ export const printers = {
57
190
  // If it returns a string, wrap it for consistency
58
191
  // If it returns an array, concatenate it
59
192
  if (Array.isArray(parts)) {
60
- return concat(parts);
193
+ return parts;
61
194
  }
62
195
  return typeof parts === 'string' ? parts : parts;
63
196
  },
64
- embed(path, options) {
197
+ /**
198
+ * @param {AstPath<RippleASTNode>} path
199
+ * @returns {((textToDoc: (text: string, options: object) => Promise<Doc>) => Promise<Doc>) | null}
200
+ */
201
+ embed(path) {
65
202
  const node = path.getValue();
66
203
 
67
204
  // Handle StyleSheet nodes inside style tags
@@ -107,6 +244,10 @@ export const printers = {
107
244
 
108
245
  return null;
109
246
  },
247
+ /**
248
+ * @param {RippleASTNode} node
249
+ * @returns {string[]}
250
+ */
110
251
  getVisitorKeys(node) {
111
252
  // Exclude metadata and raw text properties that shouldn't be traversed
112
253
  // The css property is specifically excluded so embed() can handle it
@@ -133,7 +274,12 @@ export const printers = {
133
274
  },
134
275
  };
135
276
 
136
- // Helper function to format string literals according to Prettier options
277
+ /**
278
+ * Format a string literal according to Prettier options
279
+ * @param {string} value - The string value to format
280
+ * @param {RippleFormatOptions} options - Prettier options
281
+ * @returns {string} - The formatted string literal with quotes
282
+ */
137
283
  function formatStringLiteral(value, options) {
138
284
  if (typeof value !== 'string') {
139
285
  return JSON.stringify(value);
@@ -150,20 +296,20 @@ function formatStringLiteral(value, options) {
150
296
  return quote + escapedValue + quote;
151
297
  }
152
298
 
153
- // Helper function to create indentation according to Prettier options
154
- function createIndent(options, level = 1) {
155
- if (options.useTabs) {
156
- return '\t'.repeat(level);
157
- } else {
158
- return ' '.repeat((options.tabWidth || 2) * level);
159
- }
160
- }
161
-
162
- // Helper function to add semicolons based on options.semi setting
299
+ /**
300
+ * Add semicolon based on options.semi setting
301
+ * @param {RippleFormatOptions} options - Prettier options
302
+ * @returns {string} - Semicolon or empty string
303
+ */
163
304
  function semi(options) {
164
305
  return options.semi !== false ? ';' : '';
165
306
  }
166
307
 
308
+ /**
309
+ * Check if a node was originally on a single line in source
310
+ * @param {RippleASTNode} node - The AST node to check
311
+ * @returns {boolean} - True if the node was on a single line
312
+ */
167
313
  function wasOriginallySingleLine(node) {
168
314
  if (!node || !node.loc || !node.loc.start || !node.loc.end) {
169
315
  return false;
@@ -172,16 +318,32 @@ function wasOriginallySingleLine(node) {
172
318
  return node.loc.start.line === node.loc.end.line;
173
319
  }
174
320
 
321
+ /**
322
+ * Check if an object expression was originally single line
323
+ * @param {RippleASTNode} node - The object expression node
324
+ * @returns {boolean} - True if single line
325
+ */
175
326
  function isSingleLineObjectExpression(node) {
176
327
  return wasOriginallySingleLine(node);
177
328
  }
178
329
 
179
- // Prettier-style helper functions
330
+ /**
331
+ * Check if a node has any comments (leading, trailing, or inner)
332
+ * @param {RippleASTNode} node - The AST node to check
333
+ * @returns {boolean} - True if the node has comments
334
+ */
180
335
  function hasComment(node) {
181
336
  return !!(node.leadingComments || node.trailingComments || node.innerComments);
182
337
  }
183
338
 
339
+ /**
340
+ * Get all function parameters including `this`, params, and rest.
341
+ * TypeScript/Ripple functions can have additional `this` and `rest` parameters.
342
+ * @param {RippleASTNode} node - The function node
343
+ * @returns {Array<RippleASTNode>} - Array of parameter patterns
344
+ */
184
345
  function getFunctionParameters(node) {
346
+ /** @type {Array<RippleASTNode>} */
185
347
  const parameters = [];
186
348
  if (node.this) {
187
349
  parameters.push(node.this);
@@ -195,9 +357,17 @@ function getFunctionParameters(node) {
195
357
  return parameters;
196
358
  }
197
359
 
360
+ /**
361
+ * Iterate over function parameters with path callbacks.
362
+ * TypeScript/Ripple functions can have additional `this` and `rest` parameters.
363
+ * @param {AstPath<RippleASTNode>} path - The function path
364
+ * @param {(paramPath: AstPath<RippleASTNode>, index: number) => void} iteratee - Callback for each parameter
365
+ */
198
366
  function iterateFunctionParametersPath(path, iteratee) {
199
- const { node } = path;
367
+ /** @type {RippleASTNode} */
368
+ const node = path.node;
200
369
  let index = 0;
370
+ /** @type {(paramPath: AstPath<RippleASTNode>) => void} */
201
371
  const callback = (paramPath) => iteratee(paramPath, index++);
202
372
 
203
373
  if (node.this) {
@@ -212,6 +382,7 @@ function iterateFunctionParametersPath(path, iteratee) {
212
382
  }
213
383
 
214
384
  // Operator precedence (higher number = higher precedence)
385
+ /** @type {Record<string, number>} */
215
386
  const PRECEDENCE = {
216
387
  '||': 1,
217
388
  '&&': 2,
@@ -239,11 +410,21 @@ const PRECEDENCE = {
239
410
  '**': 11,
240
411
  };
241
412
 
413
+ /**
414
+ * Get operator precedence for binary/logical expressions
415
+ * @param {string} operator - The operator string
416
+ * @returns {number} - Precedence level (higher = binds tighter)
417
+ */
242
418
  function getPrecedence(operator) {
243
419
  return PRECEDENCE[operator] || 0;
244
420
  }
245
421
 
246
- // Check if a BinaryExpression needs parentheses
422
+ /**
423
+ * Check if a BinaryExpression needs parentheses
424
+ * @param {RippleASTNode} node - The expression node
425
+ * @param {RippleASTNode} parent - The parent node
426
+ * @returns {boolean} - True if parentheses are needed
427
+ */
247
428
  function binaryExpressionNeedsParens(node, parent) {
248
429
  if (!node.metadata?.parenthesized) {
249
430
  return false;
@@ -284,6 +465,11 @@ function binaryExpressionNeedsParens(node, parent) {
284
465
  return false;
285
466
  }
286
467
 
468
+ /**
469
+ * Create a function that skips specified characters in text
470
+ * @param {string | RegExp} characters - Characters to skip
471
+ * @returns {(text: string, startIndex: number | false, options?: { backwards?: boolean }) => number | false}
472
+ */
287
473
  function createSkip(characters) {
288
474
  return (text, startIndex, options) => {
289
475
  const backwards = Boolean(options && options.backwards);
@@ -318,16 +504,32 @@ const skipSpaces = createSkip(' \t');
318
504
  const skipToLineEnd = createSkip(',; \t');
319
505
  const skipEverythingButNewLine = createSkip(/[^\n\r\u2028\u2029]/u);
320
506
 
507
+ /**
508
+ * Check if a character is a newline
509
+ * @param {string} character - Single character to check
510
+ * @returns {boolean}
511
+ */
321
512
  function isCharNewLine(character) {
322
513
  return (
323
514
  character === '\n' || character === '\r' || character === '\u2028' || character === '\u2029'
324
515
  );
325
516
  }
326
517
 
518
+ /**
519
+ * Check if a character is whitespace (space or tab)
520
+ * @param {string} character - Single character to check
521
+ * @returns {boolean}
522
+ */
327
523
  function isCharSpace(character) {
328
524
  return character === ' ' || character === '\t';
329
525
  }
330
526
 
527
+ /**
528
+ * Skip over an inline comment (/* ... * /)
529
+ * @param {string} text - Source text
530
+ * @param {number | false} startIndex - Starting position
531
+ * @returns {number | false} - Position after comment or original position
532
+ */
331
533
  function skipInlineComment(text, startIndex) {
332
534
  if (startIndex === false) {
333
535
  return false;
@@ -344,6 +546,13 @@ function skipInlineComment(text, startIndex) {
344
546
  return startIndex;
345
547
  }
346
548
 
549
+ /**
550
+ * Skip over a newline character
551
+ * @param {string} text - Source text
552
+ * @param {number | false} startIndex - Starting position
553
+ * @param {{ backwards?: boolean }} [options] - Direction options
554
+ * @returns {number | false} - Position after newline or original position
555
+ */
347
556
  function skipNewline(text, startIndex, options) {
348
557
  const backwards = Boolean(options && options.backwards);
349
558
  if (startIndex === false) {
@@ -370,6 +579,12 @@ function skipNewline(text, startIndex, options) {
370
579
  return startIndex;
371
580
  }
372
581
 
582
+ /**
583
+ * Skip over a trailing comment (// ...)
584
+ * @param {string} text - Source text
585
+ * @param {number | false} startIndex - Starting position
586
+ * @returns {number | false} - Position after comment or original position
587
+ */
373
588
  function skipTrailingComment(text, startIndex) {
374
589
  if (startIndex === false) {
375
590
  return false;
@@ -382,6 +597,11 @@ function skipTrailingComment(text, startIndex) {
382
597
  return startIndex;
383
598
  }
384
599
 
600
+ /**
601
+ * Get the end index of a node from various possible properties
602
+ * @param {RippleASTNode} node - The AST node
603
+ * @returns {number | null} - End position or null
604
+ */
385
605
  function getNodeEndIndex(node) {
386
606
  if (node?.loc?.end && typeof node.loc.end.index === 'number') {
387
607
  return node.loc.end.index;
@@ -395,6 +615,11 @@ function getNodeEndIndex(node) {
395
615
  return null;
396
616
  }
397
617
 
618
+ /**
619
+ * Check if a node is a RegExp literal
620
+ * @param {RippleASTNode} node - The AST node
621
+ * @returns {boolean}
622
+ */
398
623
  function isRegExpLiteral(node) {
399
624
  return (
400
625
  node &&
@@ -406,6 +631,12 @@ function isRegExpLiteral(node) {
406
631
  );
407
632
  }
408
633
 
634
+ /**
635
+ * Check if a comment is followed by a paren on the same line
636
+ * @param {Comment} comment - The comment node
637
+ * @param {RippleFormatOptions} options - Prettier options
638
+ * @returns {boolean}
639
+ */
409
640
  function isCommentFollowedBySameLineParen(comment, options) {
410
641
  if (!comment || !options || typeof options.originalText !== 'string') {
411
642
  return false;
@@ -432,12 +663,25 @@ function isCommentFollowedBySameLineParen(comment, options) {
432
663
  return false;
433
664
  }
434
665
 
666
+ /**
667
+ * Check if there is a newline at the given position
668
+ * @param {string} text - Source text
669
+ * @param {number} startIndex - Starting position
670
+ * @param {{ backwards?: boolean }} [options] - Direction options
671
+ * @returns {boolean}
672
+ */
435
673
  function hasNewline(text, startIndex, options) {
436
674
  const idx = skipSpaces(text, options && options.backwards ? startIndex - 1 : startIndex, options);
437
675
  const idx2 = skipNewline(text, idx, options);
438
676
  return idx !== idx2;
439
677
  }
440
678
 
679
+ /**
680
+ * Check if the next line after a node is empty
681
+ * @param {RippleASTNode} node - The AST node
682
+ * @param {RippleFormatOptions} options - Prettier options
683
+ * @returns {boolean}
684
+ */
441
685
  function isNextLineEmpty(node, options) {
442
686
  if (!node || !options || !options.originalText) {
443
687
  return false;
@@ -483,10 +727,21 @@ function isNextLineEmpty(node, options) {
483
727
  return index !== false && hasNewline(text, index);
484
728
  }
485
729
 
730
+ /**
731
+ * Check if a function has a rest parameter
732
+ * @param {RippleASTNode} node - The function node
733
+ * @returns {boolean}
734
+ */
486
735
  function hasRestParameter(node) {
487
736
  return !!node.rest;
488
737
  }
489
738
 
739
+ /**
740
+ * Determine if a trailing comma should be printed based on options
741
+ * @param {RippleFormatOptions} options - Prettier options
742
+ * @param {'es5' | 'all'} [level='all'] - Comma level to check
743
+ * @returns {boolean}
744
+ */
490
745
  function shouldPrintComma(options, level = 'all') {
491
746
  switch (options.trailingComma) {
492
747
  case 'none':
@@ -500,6 +755,13 @@ function shouldPrintComma(options, level = 'all') {
500
755
  }
501
756
  }
502
757
 
758
+ /**
759
+ * Check if a leading comment can be attached to the previous element
760
+ * @param {RippleASTNode} comment - The comment node
761
+ * @param {RippleASTNode} previousNode - Previous node
762
+ * @param {RippleASTNode} nextNode - Next node
763
+ * @returns {boolean}
764
+ */
503
765
  function canAttachLeadingCommentToPreviousElement(comment, previousNode, nextNode) {
504
766
  if (!comment || !previousNode || !nextNode) {
505
767
  return false;
@@ -525,6 +787,11 @@ function canAttachLeadingCommentToPreviousElement(comment, previousNode, nextNod
525
787
  return true;
526
788
  }
527
789
 
790
+ /**
791
+ * Build doc for inline array comments
792
+ * @param {RippleASTNode[]} comments - Array of comment nodes
793
+ * @returns {Doc | null}
794
+ */
528
795
  function buildInlineArrayCommentDoc(comments) {
529
796
  if (!Array.isArray(comments) || comments.length === 0) {
530
797
  return null;
@@ -547,11 +814,16 @@ function buildInlineArrayCommentDoc(comments) {
547
814
  }
548
815
  }
549
816
 
550
- return docs.length > 0 ? concat(docs) : null;
817
+ return docs.length > 0 ? docs : null;
551
818
  }
552
819
 
553
820
  /**
554
- * @param {AST.Property | AST.MethodDefinition} node
821
+ * Print an object or method key
822
+ * @param {Property | MethodDefinition} node - The property or method node
823
+ * @param {AstPath<RippleASTNode>} path - The AST path
824
+ * @param {RippleFormatOptions} options - Prettier options
825
+ * @param {PrintFn} print - Print callback
826
+ * @returns {Doc[]}
555
827
  */
556
828
  function printKey(node, path, options, print) {
557
829
  const parts = [];
@@ -580,6 +852,15 @@ function printKey(node, path, options, print) {
580
852
  return parts;
581
853
  }
582
854
 
855
+ /**
856
+ * Main print function for Ripple AST nodes
857
+ * @param {RippleASTNode} node - The AST node to print
858
+ * @param {AstPath<RippleASTNode>} path - The AST path
859
+ * @param {RippleFormatOptions} options - Prettier options
860
+ * @param {PrintFn} print - Print callback
861
+ * @param {PrintArgs} [args] - Additional context arguments
862
+ * @returns {Doc}
863
+ */
583
864
  function printRippleNode(node, path, options, print, args) {
584
865
  if (!node || typeof node !== 'object') {
585
866
  return String(node || '');
@@ -678,7 +959,7 @@ function printRippleNode(node, path, options, print, args) {
678
959
  const statement = path.call(print, 'body', i);
679
960
  // If statement is an array, flatten it
680
961
  if (Array.isArray(statement)) {
681
- statements.push(concat(statement));
962
+ statements.push(statement);
682
963
  } else {
683
964
  statements.push(statement);
684
965
  }
@@ -690,7 +971,7 @@ function printRippleNode(node, path, options, print, args) {
690
971
 
691
972
  // Only add spacing when explicitly needed
692
973
  if (shouldAddBlankLine(currentStmt, nextStmt)) {
693
- statements.push(concat([line, line])); // blank line
974
+ statements.push([line, line]); // blank line
694
975
  } else {
695
976
  statements.push(line); // single line break
696
977
  }
@@ -700,9 +981,9 @@ function printRippleNode(node, path, options, print, args) {
700
981
  // Prettier always adds a trailing newline to files
701
982
  // Add it unless the code is completely empty
702
983
  if (statements.length > 0) {
703
- nodeContent = concat([...statements, hardline]);
984
+ nodeContent = [...statements, hardline];
704
985
  } else {
705
- nodeContent = concat(statements);
986
+ nodeContent = statements;
706
987
  }
707
988
  break;
708
989
  }
@@ -727,6 +1008,10 @@ function printRippleNode(node, path, options, print, args) {
727
1008
  nodeContent = printFunctionDeclaration(node, path, options, print);
728
1009
  break;
729
1010
 
1011
+ case 'TSDeclareFunction':
1012
+ nodeContent = printTSDeclareFunction(node, path, options, print);
1013
+ break;
1014
+
730
1015
  case 'IfStatement':
731
1016
  nodeContent = printIfStatement(node, path, options, print);
732
1017
  break;
@@ -796,8 +1081,9 @@ function printRippleNode(node, path, options, print, args) {
796
1081
  continue;
797
1082
  }
798
1083
 
799
- const canTransferAllLeadingComments = nextElement.leadingComments.every((comment) =>
800
- canAttachLeadingCommentToPreviousElement(comment, currentElement, nextElement),
1084
+ const canTransferAllLeadingComments = nextElement.leadingComments.every(
1085
+ (/** @type {RippleASTNode} */ comment) =>
1086
+ canAttachLeadingCommentToPreviousElement(comment, currentElement, nextElement),
801
1087
  );
802
1088
 
803
1089
  if (!canTransferAllLeadingComments) {
@@ -813,14 +1099,20 @@ function printRippleNode(node, path, options, print, args) {
813
1099
 
814
1100
  // Check if all elements are objects with multiple properties
815
1101
  // In that case, each object should be on its own line
816
- const objectElements = node.elements.filter((el) => el && el.type === 'ObjectExpression');
1102
+ const objectElements = node.elements.filter(
1103
+ (/** @type {RippleASTNode} */ el) => el && el.type === 'ObjectExpression',
1104
+ );
817
1105
  const allElementsAreObjects =
818
1106
  node.elements.length > 0 &&
819
- node.elements.every((el) => el && el.type === 'ObjectExpression');
1107
+ node.elements.every(
1108
+ (/** @type {RippleASTNode} */ el) => el && el.type === 'ObjectExpression',
1109
+ );
820
1110
  const allObjectsHaveMultipleProperties =
821
1111
  allElementsAreObjects &&
822
1112
  objectElements.length > 0 &&
823
- objectElements.every((obj) => obj.properties && obj.properties.length > 1);
1113
+ objectElements.every(
1114
+ (/** @type {RippleASTNode} */ obj) => obj.properties && obj.properties.length > 1,
1115
+ );
824
1116
 
825
1117
  // For arrays of simple objects with only a few properties, try to keep compact
826
1118
  // But NOT if all objects have multiple properties
@@ -879,16 +1171,14 @@ function printRippleNode(node, path, options, print, args) {
879
1171
  );
880
1172
 
881
1173
  if (hasObjectElements && shouldInlineObjects && arrayWasSingleLine) {
882
- const separator = concat([',', line]);
1174
+ const separator = [',', line];
883
1175
  const trailing = shouldUseTrailingComma ? ifBreak(',', '') : '';
884
- nodeContent = group(
885
- concat([
886
- prefix + '[',
887
- indent(concat([softline, join(separator, elements), trailing])),
888
- softline,
889
- ']',
890
- ]),
891
- );
1176
+ nodeContent = group([
1177
+ prefix + '[',
1178
+ indent([softline, join(separator, elements), trailing]),
1179
+ softline,
1180
+ ']',
1181
+ ]);
892
1182
  break;
893
1183
  }
894
1184
 
@@ -962,7 +1252,7 @@ function printRippleNode(node, path, options, print, args) {
962
1252
  // Check if any elements contain hard breaks (like multiline ternaries)
963
1253
  // Don't check willBreak() as that includes soft breaks from groups
964
1254
  // Only check for actual multiline content that forces breaking
965
- const hasHardBreakingElements = node.elements.some((el) => {
1255
+ const hasHardBreakingElements = node.elements.some((/** @type {RippleASTNode} */ el) => {
966
1256
  if (!el) return false;
967
1257
  // Multiline ternaries are the main case that should force all elements on separate lines
968
1258
  return el.type === 'ConditionalExpression';
@@ -981,16 +1271,14 @@ function printRippleNode(node, path, options, print, args) {
981
1271
  !hasBlankLineBeforeClosing &&
982
1272
  !hasInlineComments
983
1273
  ) {
984
- const separator = concat([',', hardline]);
1274
+ const separator = [',', hardline];
985
1275
  const trailingDoc = shouldUseTrailingComma ? ',' : '';
986
- nodeContent = group(
987
- concat([
988
- prefix + '[',
989
- indent(concat([hardline, join(separator, elements), trailingDoc])),
990
- hardline,
991
- ']',
992
- ]),
993
- );
1276
+ nodeContent = group([
1277
+ prefix + '[',
1278
+ indent([hardline, join(separator, elements), trailingDoc]),
1279
+ hardline,
1280
+ ']',
1281
+ ]);
994
1282
  break;
995
1283
  }
996
1284
 
@@ -1025,20 +1313,20 @@ function printRippleNode(node, path, options, print, args) {
1025
1313
  }
1026
1314
  }
1027
1315
  }
1028
- const commentDocNoSpace = commentParts.length > 0 ? concat(commentParts) : '';
1316
+ const commentDocNoSpace = commentParts.length > 0 ? commentParts : '';
1029
1317
 
1030
1318
  // Provide conditional rendering: inline if it fits, otherwise on separate line
1031
1319
  fillParts.push(
1032
1320
  conditionalGroup([
1033
1321
  // Try inline first (with space before comment)
1034
- concat([elements[index], ',', inlineCommentDoc, hardline]),
1322
+ [elements[index], ',', inlineCommentDoc, hardline],
1035
1323
  // If doesn't fit, put comment on next line (without leading space)
1036
- concat([elements[index], ',', hardline, commentDocNoSpace, hardline]),
1324
+ [elements[index], ',', hardline, commentDocNoSpace, hardline],
1037
1325
  ]),
1038
1326
  );
1039
1327
  skipNextSeparator = true;
1040
1328
  } else {
1041
- fillParts.push(group(concat([elements[index], ','])));
1329
+ fillParts.push(group([elements[index], ',']));
1042
1330
  skipNextSeparator = false;
1043
1331
  }
1044
1332
  } else {
@@ -1048,34 +1336,30 @@ function printRippleNode(node, path, options, print, args) {
1048
1336
  }
1049
1337
 
1050
1338
  const trailingDoc = shouldUseTrailingComma ? ifBreak(',', '') : '';
1051
- nodeContent = group(
1052
- concat([
1053
- prefix + '[',
1054
- indent(concat([softline, fill(fillParts), trailingDoc])),
1055
- softline,
1056
- ']',
1057
- ]),
1058
- );
1339
+ nodeContent = group([
1340
+ prefix + '[',
1341
+ indent([softline, fill(fillParts), trailingDoc]),
1342
+ softline,
1343
+ ']',
1344
+ ]);
1059
1345
  break;
1060
1346
  }
1061
1347
 
1062
1348
  // If array has breaking elements (multiline ternaries, functions, etc.)
1063
1349
  // use join() to put each element on its own line, per Prettier spec
1064
1350
  if (hasHardBreakingElements) {
1065
- const separator = concat([',', line]);
1351
+ const separator = [',', line];
1066
1352
  const parts = [];
1067
1353
  for (let index = 0; index < elements.length; index++) {
1068
1354
  parts.push(elements[index]);
1069
1355
  }
1070
1356
  const trailingDoc = shouldUseTrailingComma ? ifBreak(',', '') : '';
1071
- nodeContent = group(
1072
- concat([
1073
- prefix + '[',
1074
- indent(concat([softline, join(separator, parts), trailingDoc])),
1075
- softline,
1076
- ']',
1077
- ]),
1078
- );
1357
+ nodeContent = group([
1358
+ prefix + '[',
1359
+ indent([softline, join(separator, parts), trailingDoc]),
1360
+ softline,
1361
+ ']',
1362
+ ]);
1079
1363
  break;
1080
1364
  }
1081
1365
 
@@ -1092,16 +1376,14 @@ function printRippleNode(node, path, options, print, args) {
1092
1376
  allowInlineObject: wasObjSingleLine,
1093
1377
  });
1094
1378
  }, 'elements');
1095
- const separator = concat([',', hardline]);
1379
+ const separator = [',', hardline];
1096
1380
  const trailingDoc = shouldUseTrailingComma ? ifBreak(',', '') : '';
1097
- nodeContent = group(
1098
- concat([
1099
- prefix + '[',
1100
- indent(concat([hardline, join(separator, inlineElements), trailingDoc])),
1101
- hardline,
1102
- ']',
1103
- ]),
1104
- );
1381
+ nodeContent = group([
1382
+ prefix + '[',
1383
+ indent([hardline, join(separator, inlineElements), trailingDoc]),
1384
+ hardline,
1385
+ ']',
1386
+ ]);
1105
1387
  break;
1106
1388
  }
1107
1389
 
@@ -1110,7 +1392,9 @@ function printRippleNode(node, path, options, print, args) {
1110
1392
  const contentParts = [];
1111
1393
 
1112
1394
  // Split elements into groups separated by blank lines
1395
+ /** @type {number[][]} */
1113
1396
  const groups = [];
1397
+ /** @type {number[]} */
1114
1398
  let currentGroup = [];
1115
1399
 
1116
1400
  for (let i = 0; i < elements.length; i++) {
@@ -1156,7 +1440,7 @@ function printRippleNode(node, path, options, print, args) {
1156
1440
  if (isLastInArray && shouldUseTrailingComma) {
1157
1441
  fillParts.push(group(elements[elemIdx]));
1158
1442
  } else {
1159
- fillParts.push(group(concat([elements[elemIdx], ','])));
1443
+ fillParts.push(group([elements[elemIdx], ',']));
1160
1444
  }
1161
1445
  }
1162
1446
 
@@ -1170,9 +1454,7 @@ function printRippleNode(node, path, options, print, args) {
1170
1454
 
1171
1455
  // Array with blank lines - format as multi-line
1172
1456
  // Use simple group that will break to fit within printWidth
1173
- nodeContent = group(
1174
- concat([prefix + '[', indent(concat([line, concat(contentParts)])), line, ']']),
1175
- );
1457
+ nodeContent = group([prefix + '[', indent([line, contentParts]), line, ']']);
1176
1458
  break;
1177
1459
  }
1178
1460
 
@@ -1202,7 +1484,7 @@ function printRippleNode(node, path, options, print, args) {
1202
1484
  let leftPart = path.call((p) => print(p, { noBreakInside: true }), 'left');
1203
1485
  // Preserve parentheses around the left side when present
1204
1486
  if (node.left.metadata?.parenthesized) {
1205
- leftPart = concat(['(', leftPart, ')']);
1487
+ leftPart = ['(', leftPart, ')'];
1206
1488
  }
1207
1489
  // For CallExpression on the right with JSDoc comments, use fluid layout strategy
1208
1490
  const rightSide = path.call(print, 'right');
@@ -1254,7 +1536,7 @@ function printRippleNode(node, path, options, print, args) {
1254
1536
  const argsDoc = printCallArguments(path, options, print);
1255
1537
  parts.push(argsDoc);
1256
1538
 
1257
- let callContent = concat(parts);
1539
+ let callContent = parts;
1258
1540
 
1259
1541
  // Preserve parentheses for type-annotated call expressions
1260
1542
  // When parenthesized with leading comments, use grouping to allow breaking
@@ -1262,11 +1544,11 @@ function printRippleNode(node, path, options, print, args) {
1262
1544
  const hasLeadingComments = node.leadingComments && node.leadingComments.length > 0;
1263
1545
  if (hasLeadingComments) {
1264
1546
  // Group with softline to allow breaking after opening paren
1265
- callContent = group(
1266
- concat(['(', indent(concat([softline, callContent])), softline, ')']),
1267
- );
1547
+ callContent = /** @type {Doc[]} */ ([
1548
+ group(['(', indent([softline, callContent]), softline, ')']),
1549
+ ]);
1268
1550
  } else {
1269
- callContent = concat(['(', callContent, ')']);
1551
+ callContent = ['(', callContent, ')'];
1270
1552
  }
1271
1553
  }
1272
1554
  nodeContent = callContent;
@@ -1275,13 +1557,13 @@ function printRippleNode(node, path, options, print, args) {
1275
1557
 
1276
1558
  case 'AwaitExpression': {
1277
1559
  const parts = ['await ', path.call(print, 'argument')];
1278
- nodeContent = concat(parts);
1560
+ nodeContent = parts;
1279
1561
  break;
1280
1562
  }
1281
1563
 
1282
1564
  case 'TrackedExpression': {
1283
1565
  const parts = ['@(', path.call(print, 'argument'), ')'];
1284
- nodeContent = concat(parts);
1566
+ nodeContent = parts;
1285
1567
  break;
1286
1568
  }
1287
1569
 
@@ -1292,7 +1574,7 @@ function printRippleNode(node, path, options, print, args) {
1292
1574
  nodeContent = '#Map';
1293
1575
  } else {
1294
1576
  const args = path.map(print, 'arguments');
1295
- nodeContent = concat(['#Map(', join(concat([',', line]), args), ')']);
1577
+ nodeContent = ['#Map(', join([',', line], args), ')'];
1296
1578
  }
1297
1579
  break;
1298
1580
  }
@@ -1304,7 +1586,7 @@ function printRippleNode(node, path, options, print, args) {
1304
1586
  nodeContent = '#Set';
1305
1587
  } else {
1306
1588
  const args = path.map(print, 'arguments');
1307
- nodeContent = concat(['#Set(', join(concat([',', line]), args), ')']);
1589
+ nodeContent = ['#Set(', join([',', line], args), ')'];
1308
1590
  }
1309
1591
  break;
1310
1592
  }
@@ -1328,27 +1610,23 @@ function printRippleNode(node, path, options, print, args) {
1328
1610
  break;
1329
1611
 
1330
1612
  case 'TSAsExpression': {
1331
- nodeContent = concat([
1332
- path.call(print, 'expression'),
1333
- ' as ',
1334
- path.call(print, 'typeAnnotation'),
1335
- ]);
1613
+ nodeContent = [path.call(print, 'expression'), ' as ', path.call(print, 'typeAnnotation')];
1336
1614
  break;
1337
1615
  }
1338
1616
 
1339
1617
  case 'TSNonNullExpression': {
1340
- nodeContent = concat([path.call(print, 'expression'), '!']);
1618
+ nodeContent = [path.call(print, 'expression'), '!'];
1341
1619
  break;
1342
1620
  }
1343
1621
 
1344
1622
  case 'TSInstantiationExpression': {
1345
1623
  // Explicit type instantiation: foo<Type>, identity<string>
1346
- nodeContent = concat([path.call(print, 'expression'), path.call(print, 'typeArguments')]);
1624
+ nodeContent = [path.call(print, 'expression'), path.call(print, 'typeArguments')];
1347
1625
  break;
1348
1626
  }
1349
1627
 
1350
1628
  case 'JSXExpressionContainer': {
1351
- nodeContent = concat(['{', path.call(print, 'expression'), '}']);
1629
+ nodeContent = ['{', path.call(print, 'expression'), '}'];
1352
1630
  break;
1353
1631
  }
1354
1632
 
@@ -1464,7 +1742,7 @@ function printRippleNode(node, path, options, print, args) {
1464
1742
  break;
1465
1743
 
1466
1744
  case 'DebuggerStatement':
1467
- nodeContent = printDebuggerStatement(node, path, options, print);
1745
+ nodeContent = printDebuggerStatement(node, path, options);
1468
1746
  break;
1469
1747
 
1470
1748
  case 'SequenceExpression':
@@ -1478,15 +1756,15 @@ function printRippleNode(node, path, options, print, args) {
1478
1756
  const needsParens =
1479
1757
  node.argument.type === 'LogicalExpression' && node.argument.operator === '??';
1480
1758
  if (needsParens) {
1481
- nodeContent = concat(['...(', argumentDoc, ')']);
1759
+ nodeContent = ['...(', argumentDoc, ')'];
1482
1760
  } else {
1483
- nodeContent = concat(['...', argumentDoc]);
1761
+ nodeContent = ['...', argumentDoc];
1484
1762
  }
1485
1763
  break;
1486
1764
  }
1487
1765
  case 'RestElement': {
1488
1766
  const parts = ['...', path.call(print, 'argument')];
1489
- nodeContent = concat(parts);
1767
+ nodeContent = parts;
1490
1768
  break;
1491
1769
  }
1492
1770
  case 'VariableDeclaration':
@@ -1499,19 +1777,19 @@ function printRippleNode(node, path, options, print, args) {
1499
1777
  node.expression.type === 'ObjectExpression' ||
1500
1778
  node.expression.type === 'TrackedObjectExpression';
1501
1779
  if (needsParens) {
1502
- nodeContent = concat(['(', path.call(print, 'expression'), ')', semi(options)]);
1780
+ nodeContent = ['(', path.call(print, 'expression'), ')', semi(options)];
1503
1781
  } else {
1504
- nodeContent = concat([path.call(print, 'expression'), semi(options)]);
1782
+ nodeContent = [path.call(print, 'expression'), semi(options)];
1505
1783
  }
1506
1784
  break;
1507
1785
  }
1508
1786
  case 'RefAttribute':
1509
- nodeContent = concat(['{ref ', path.call(print, 'argument'), '}']);
1787
+ nodeContent = ['{ref ', path.call(print, 'argument'), '}'];
1510
1788
  break;
1511
1789
 
1512
1790
  case 'SpreadAttribute': {
1513
1791
  const parts = ['{...', path.call(print, 'argument'), '}'];
1514
- nodeContent = concat(parts);
1792
+ nodeContent = parts;
1515
1793
  break;
1516
1794
  }
1517
1795
 
@@ -1521,12 +1799,12 @@ function printRippleNode(node, path, options, print, args) {
1521
1799
  let identifierContent;
1522
1800
  if (node.typeAnnotation) {
1523
1801
  const optionalMarker = node.optional ? '?' : '';
1524
- identifierContent = concat([
1802
+ identifierContent = [
1525
1803
  trackedPrefix + node.name,
1526
1804
  optionalMarker,
1527
1805
  ': ',
1528
1806
  path.call(print, 'typeAnnotation'),
1529
- ]);
1807
+ ];
1530
1808
  } else {
1531
1809
  identifierContent = trackedPrefix + node.name;
1532
1810
  }
@@ -1540,7 +1818,7 @@ function printRippleNode(node, path, options, print, args) {
1540
1818
  (parent.type === 'AssignmentExpression' && parent.left === node));
1541
1819
  const shouldAddParens = node.metadata?.parenthesized && !parentHandlesParens;
1542
1820
  if (shouldAddParens) {
1543
- nodeContent = concat(['(', identifierContent, ')']);
1821
+ nodeContent = ['(', identifierContent, ')'];
1544
1822
  } else {
1545
1823
  nodeContent = identifierContent;
1546
1824
  }
@@ -1583,6 +1861,7 @@ function printRippleNode(node, path, options, print, args) {
1583
1861
  const hasBlankLineBefore =
1584
1862
  prevComment && getBlankLinesBetweenNodes(prevComment, comment) > 0;
1585
1863
 
1864
+ /** @type {Doc | undefined} */
1586
1865
  let commentDoc;
1587
1866
  if (comment.type === 'Line') {
1588
1867
  commentDoc = '//' + comment.value;
@@ -1590,7 +1869,9 @@ function printRippleNode(node, path, options, print, args) {
1590
1869
  commentDoc = '/*' + comment.value + '*/';
1591
1870
  }
1592
1871
 
1593
- commentDocs.push({ doc: commentDoc, hasBlankLineBefore });
1872
+ if (commentDoc !== undefined) {
1873
+ commentDocs.push({ doc: commentDoc, hasBlankLineBefore });
1874
+ }
1594
1875
  }
1595
1876
 
1596
1877
  // Build the content with proper spacing
@@ -1611,7 +1892,7 @@ function printRippleNode(node, path, options, print, args) {
1611
1892
  contentParts.push(doc);
1612
1893
  }
1613
1894
 
1614
- nodeContent = group(['{', indent([hardline, concat(contentParts)]), hardline, '}']);
1895
+ nodeContent = group(['{', indent([hardline, contentParts]), hardline, '}']);
1615
1896
  break;
1616
1897
  } else {
1617
1898
  // Fallback to simple join
@@ -1629,6 +1910,7 @@ function printRippleNode(node, path, options, print, args) {
1629
1910
  }
1630
1911
 
1631
1912
  // Process statements and handle spacing using shouldAddBlankLine
1913
+ /** @type {Doc[]} */
1632
1914
  const statements = [];
1633
1915
  for (let i = 0; i < node.body.length; i++) {
1634
1916
  const statement = path.call(print, 'body', i);
@@ -1648,24 +1930,25 @@ function printRippleNode(node, path, options, print, args) {
1648
1930
  }
1649
1931
 
1650
1932
  // Use proper block statement pattern
1651
- nodeContent = group(['{', indent([hardline, concat(statements)]), hardline, '}']);
1933
+ nodeContent = group(['{', indent([hardline, statements]), hardline, '}']);
1652
1934
  break;
1653
1935
  }
1654
1936
 
1655
1937
  case 'ServerBlock': {
1656
1938
  const blockContent = path.call(print, 'body');
1657
- nodeContent = concat(['#server ', blockContent]);
1939
+ nodeContent = ['#server ', blockContent];
1658
1940
  break;
1659
1941
  }
1660
1942
 
1661
1943
  case 'ReturnStatement': {
1944
+ /** @type {Doc[]} */
1662
1945
  const parts = ['return'];
1663
1946
  if (node.argument) {
1664
1947
  parts.push(' ');
1665
1948
  parts.push(path.call(print, 'argument'));
1666
1949
  }
1667
1950
  parts.push(semi(options));
1668
- nodeContent = concat(parts);
1951
+ nodeContent = parts;
1669
1952
  break;
1670
1953
  }
1671
1954
 
@@ -1681,41 +1964,32 @@ function printRippleNode(node, path, options, print, args) {
1681
1964
  let result;
1682
1965
  // Don't add indent if we're in a conditional test context
1683
1966
  if (args?.isConditionalTest) {
1684
- result = group(
1685
- concat([
1686
- path.call((childPath) => print(childPath, { isConditionalTest: true }), 'left'),
1687
- ' ',
1688
- node.operator,
1689
- concat([
1690
- line,
1691
- path.call((childPath) => print(childPath, { isConditionalTest: true }), 'right'),
1692
- ]),
1693
- ]),
1694
- );
1967
+ result = group([
1968
+ path.call((childPath) => print(childPath, { isConditionalTest: true }), 'left'),
1969
+ ' ',
1970
+ node.operator,
1971
+ [line, path.call((childPath) => print(childPath, { isConditionalTest: true }), 'right')],
1972
+ ]);
1695
1973
  } else if (shouldNotIndent) {
1696
1974
  // In assignment context, don't add indent - parent will handle it
1697
- result = group(
1698
- concat([
1699
- path.call(print, 'left'),
1700
- ' ',
1701
- node.operator,
1702
- concat([line, path.call(print, 'right')]),
1703
- ]),
1704
- );
1975
+ result = group([
1976
+ path.call(print, 'left'),
1977
+ ' ',
1978
+ node.operator,
1979
+ [line, path.call(print, 'right')],
1980
+ ]);
1705
1981
  } else {
1706
- result = group(
1707
- concat([
1708
- path.call(print, 'left'),
1709
- ' ',
1710
- node.operator,
1711
- indent(concat([line, path.call(print, 'right')])),
1712
- ]),
1713
- );
1982
+ result = group([
1983
+ path.call(print, 'left'),
1984
+ ' ',
1985
+ node.operator,
1986
+ indent([line, path.call(print, 'right')]),
1987
+ ]);
1714
1988
  }
1715
1989
 
1716
1990
  // Wrap in parentheses only if semantically necessary
1717
1991
  if (binaryExpressionNeedsParens(node, parent)) {
1718
- result = concat(['(', result, ')']);
1992
+ result = ['(', result, ')'];
1719
1993
  }
1720
1994
 
1721
1995
  nodeContent = result;
@@ -1724,26 +1998,19 @@ function printRippleNode(node, path, options, print, args) {
1724
1998
  case 'LogicalExpression':
1725
1999
  // Don't add indent if we're in a conditional test context
1726
2000
  if (args?.isConditionalTest) {
1727
- nodeContent = group(
1728
- concat([
1729
- path.call((childPath) => print(childPath, { isConditionalTest: true }), 'left'),
1730
- ' ',
1731
- node.operator,
1732
- concat([
1733
- line,
1734
- path.call((childPath) => print(childPath, { isConditionalTest: true }), 'right'),
1735
- ]),
1736
- ]),
1737
- );
2001
+ nodeContent = group([
2002
+ path.call((childPath) => print(childPath, { isConditionalTest: true }), 'left'),
2003
+ ' ',
2004
+ node.operator,
2005
+ [line, path.call((childPath) => print(childPath, { isConditionalTest: true }), 'right')],
2006
+ ]);
1738
2007
  } else {
1739
- nodeContent = group(
1740
- concat([
1741
- path.call(print, 'left'),
1742
- ' ',
1743
- node.operator,
1744
- indent(concat([line, path.call(print, 'right')])),
1745
- ]),
1746
- );
2008
+ nodeContent = group([
2009
+ path.call(print, 'left'),
2010
+ ' ',
2011
+ node.operator,
2012
+ indent([line, path.call(print, 'right')]),
2013
+ ]);
1747
2014
  }
1748
2015
  break;
1749
2016
 
@@ -1783,7 +2050,7 @@ function printRippleNode(node, path, options, print, args) {
1783
2050
  const alternateBreaks = willBreak(alternateDoc);
1784
2051
 
1785
2052
  // Helper to determine if a node type already handles its own indentation
1786
- const hasOwnIndentation = (nodeType) => {
2053
+ const hasOwnIndentation = (/** @type {string} */ nodeType) => {
1787
2054
  return nodeType === 'BinaryExpression' || nodeType === 'LogicalExpression';
1788
2055
  };
1789
2056
 
@@ -1804,13 +2071,11 @@ function printRippleNode(node, path, options, print, args) {
1804
2071
  !hasOwnIndentation(node.alternate.type) &&
1805
2072
  node.alternate.type !== 'ConditionalExpression';
1806
2073
 
1807
- result = concat([
2074
+ result = [
1808
2075
  testDoc,
1809
- indent(
1810
- concat([line, '? ', shouldIndentConsequent ? indent(consequentDoc) : consequentDoc]),
1811
- ),
1812
- indent(concat([line, ': ', shouldIndentAlternate ? indent(alternateDoc) : alternateDoc])),
1813
- ]);
2076
+ indent([line, '? ', shouldIndentConsequent ? indent(consequentDoc) : consequentDoc]),
2077
+ indent([line, ': ', shouldIndentAlternate ? indent(alternateDoc) : alternateDoc]),
2078
+ ];
1814
2079
  } else {
1815
2080
  // Otherwise try inline first, then multiline if it doesn't fit
1816
2081
  const shouldIndentConsequent =
@@ -1822,23 +2087,19 @@ function printRippleNode(node, path, options, print, args) {
1822
2087
 
1823
2088
  result = conditionalGroup([
1824
2089
  // Try inline first
1825
- concat([testDoc, ' ? ', consequentDoc, ' : ', alternateDoc]),
2090
+ [testDoc, ' ? ', consequentDoc, ' : ', alternateDoc],
1826
2091
  // If inline doesn't fit, use multiline
1827
- concat([
2092
+ [
1828
2093
  testDoc,
1829
- indent(
1830
- concat([line, '? ', shouldIndentConsequent ? indent(consequentDoc) : consequentDoc]),
1831
- ),
1832
- indent(
1833
- concat([line, ': ', shouldIndentAlternate ? indent(alternateDoc) : alternateDoc]),
1834
- ),
1835
- ]),
2094
+ indent([line, '? ', shouldIndentConsequent ? indent(consequentDoc) : consequentDoc]),
2095
+ indent([line, ': ', shouldIndentAlternate ? indent(alternateDoc) : alternateDoc]),
2096
+ ],
1836
2097
  ]);
1837
2098
  }
1838
2099
 
1839
2100
  // Wrap in parentheses if metadata indicates they were present
1840
2101
  if (node.metadata?.parenthesized) {
1841
- result = concat(['(', result, ')']);
2102
+ result = ['(', result, ')'];
1842
2103
  }
1843
2104
 
1844
2105
  nodeContent = result;
@@ -1847,15 +2108,15 @@ function printRippleNode(node, path, options, print, args) {
1847
2108
 
1848
2109
  case 'UpdateExpression':
1849
2110
  if (node.prefix) {
1850
- nodeContent = concat([node.operator, path.call(print, 'argument')]);
2111
+ nodeContent = [node.operator, path.call(print, 'argument')];
1851
2112
  } else {
1852
- nodeContent = concat([path.call(print, 'argument'), node.operator]);
2113
+ nodeContent = [path.call(print, 'argument'), node.operator];
1853
2114
  }
1854
2115
  break;
1855
2116
 
1856
2117
  case 'TSArrayType': {
1857
2118
  const parts = [path.call(print, 'elementType'), '[]'];
1858
- nodeContent = concat(parts);
2119
+ nodeContent = parts;
1859
2120
  break;
1860
2121
  }
1861
2122
 
@@ -1926,13 +2187,13 @@ function printRippleNode(node, path, options, print, args) {
1926
2187
  case 'TSTypeOperator': {
1927
2188
  const operator = node.operator;
1928
2189
  const type = path.call(print, 'typeAnnotation');
1929
- nodeContent = concat([operator, ' ', type]);
2190
+ nodeContent = [operator, ' ', type];
1930
2191
  break;
1931
2192
  }
1932
2193
 
1933
2194
  case 'TSTypeQuery': {
1934
2195
  const expr = path.call(print, 'exprName');
1935
- nodeContent = concat(['typeof ', expr]);
2196
+ nodeContent = ['typeof ', expr];
1936
2197
  break;
1937
2198
  }
1938
2199
 
@@ -1958,7 +2219,7 @@ function printRippleNode(node, path, options, print, args) {
1958
2219
  parts.push(path.call(print, 'typeAnnotation'));
1959
2220
  }
1960
2221
 
1961
- nodeContent = concat(parts);
2222
+ nodeContent = parts;
1962
2223
  break;
1963
2224
  }
1964
2225
 
@@ -1991,7 +2252,7 @@ function printRippleNode(node, path, options, print, args) {
1991
2252
  break;
1992
2253
 
1993
2254
  case 'TSParenthesizedType': {
1994
- nodeContent = concat(['(', path.call(print, 'typeAnnotation'), ')']);
2255
+ nodeContent = ['(', path.call(print, 'typeAnnotation'), ')'];
1995
2256
  break;
1996
2257
  }
1997
2258
 
@@ -2003,7 +2264,7 @@ function printRippleNode(node, path, options, print, args) {
2003
2264
  parts.push(path.call(print, 'typeParameters'));
2004
2265
  }
2005
2266
 
2006
- nodeContent = concat(parts);
2267
+ nodeContent = parts;
2007
2268
  break;
2008
2269
  }
2009
2270
 
@@ -2031,7 +2292,7 @@ function printRippleNode(node, path, options, print, args) {
2031
2292
  // JSXEmptyExpression represents the empty expression in {/* comment */}
2032
2293
  // The comments are attached as innerComments by the parser
2033
2294
  if (innerCommentParts.length > 0) {
2034
- nodeContent = concat(innerCommentParts);
2295
+ nodeContent = innerCommentParts;
2035
2296
  } else {
2036
2297
  nodeContent = '';
2037
2298
  }
@@ -2045,7 +2306,7 @@ function printRippleNode(node, path, options, print, args) {
2045
2306
  const expressionDoc = suppressExpressionLeadingComments
2046
2307
  ? path.call((exprPath) => print(exprPath, { suppressLeadingComments: true }), 'expression')
2047
2308
  : path.call(print, 'expression');
2048
- nodeContent = concat(['{', expressionDoc, '}']);
2309
+ nodeContent = ['{', expressionDoc, '}'];
2049
2310
  break;
2050
2311
  }
2051
2312
 
@@ -2053,7 +2314,7 @@ function printRippleNode(node, path, options, print, args) {
2053
2314
  const expressionDoc = suppressExpressionLeadingComments
2054
2315
  ? path.call((exprPath) => print(exprPath, { suppressLeadingComments: true }), 'expression')
2055
2316
  : path.call(print, 'expression');
2056
- nodeContent = concat(['{html ', expressionDoc, '}']);
2317
+ nodeContent = ['{html ', expressionDoc, '}'];
2057
2318
  break;
2058
2319
  }
2059
2320
 
@@ -2111,20 +2372,29 @@ function printRippleNode(node, path, options, print, args) {
2111
2372
  if (trailingParts.length > 0) {
2112
2373
  parts.push(nodeContent);
2113
2374
  parts.push(...trailingParts);
2114
- return concat(parts);
2375
+ return parts;
2115
2376
  }
2116
2377
  } // Return with or without leading comments
2117
2378
  if (parts.length > 0) {
2118
2379
  // Don't add blank line between leading comments and node
2119
2380
  // because they're meant to be attached together
2120
2381
  parts.push(nodeContent);
2121
- return concat(parts);
2382
+ return parts;
2122
2383
  }
2123
2384
 
2124
2385
  return nodeContent;
2125
2386
  }
2126
2387
 
2127
- function printImportDeclaration(node, path, options, print) {
2388
+ /**
2389
+ * Print an import declaration
2390
+ * @param {RippleASTNode} node - The import declaration node
2391
+ * @param {AstPath<RippleASTNode>} path - The AST path
2392
+ * @param {RippleFormatOptions} options - Prettier options
2393
+ * @param {PrintFn} _print - Print callback (unused)
2394
+ * @returns {Doc}
2395
+ */
2396
+ function printImportDeclaration(node, path, options, _print) {
2397
+ /** @type {Doc[]} */
2128
2398
  const parts = ['import'];
2129
2399
 
2130
2400
  // Handle type imports
@@ -2133,27 +2403,33 @@ function printImportDeclaration(node, path, options, print) {
2133
2403
  }
2134
2404
 
2135
2405
  if (node.specifiers && node.specifiers.length > 0) {
2406
+ /** @type {string[]} */
2136
2407
  const defaultImports = [];
2408
+ /** @type {string[]} */
2137
2409
  const namedImports = [];
2410
+ /** @type {string[]} */
2138
2411
  const namespaceImports = [];
2139
2412
 
2140
- node.specifiers.forEach((spec) => {
2413
+ node.specifiers.forEach((/** @type {RippleASTNode} */ spec) => {
2141
2414
  if (spec.type === 'ImportDefaultSpecifier') {
2142
- defaultImports.push(spec.local.name);
2415
+ defaultImports.push(/** @type {string} */ (spec.local.name));
2143
2416
  } else if (spec.type === 'ImportSpecifier') {
2144
2417
  // Handle inline type imports: import { type Component } from 'ripple'
2145
2418
  const typePrefix = spec.importKind === 'type' ? 'type ' : '';
2419
+ const importedName = /** @type {string} */ (spec.imported.name);
2420
+ const localName = /** @type {string} */ (spec.local.name);
2146
2421
  const importName =
2147
- spec.imported.name === spec.local.name
2148
- ? typePrefix + spec.local.name
2149
- : typePrefix + spec.imported.name + ' as ' + spec.local.name;
2422
+ importedName === localName
2423
+ ? typePrefix + localName
2424
+ : typePrefix + importedName + ' as ' + localName;
2150
2425
  namedImports.push(importName);
2151
2426
  } else if (spec.type === 'ImportNamespaceSpecifier') {
2152
- namespaceImports.push('* as ' + spec.local.name);
2427
+ namespaceImports.push('* as ' + /** @type {string} */ (spec.local.name));
2153
2428
  }
2154
2429
  });
2155
2430
 
2156
2431
  // Build import clause with proper grouping and line breaking
2432
+ /** @type {Doc[]} */
2157
2433
  const importClauseParts = [];
2158
2434
 
2159
2435
  if (defaultImports.length > 0) {
@@ -2165,20 +2441,13 @@ function printImportDeclaration(node, path, options, print) {
2165
2441
  if (namedImports.length > 0) {
2166
2442
  // Use Prettier's group and conditionalGroup for named imports to handle line breaking
2167
2443
  const namedImportsDocs = namedImports.map((name) => name);
2168
- const namedImportsGroup = group(
2169
- concat([
2170
- '{',
2171
- indent(
2172
- concat([
2173
- options.bracketSpacing ? line : softline,
2174
- join(concat([',', line]), namedImportsDocs),
2175
- ]),
2176
- ),
2177
- ifBreak(shouldPrintComma(options) ? ',' : ''),
2178
- options.bracketSpacing ? line : softline,
2179
- '}',
2180
- ]),
2181
- );
2444
+ const namedImportsGroup = group([
2445
+ '{',
2446
+ indent([options.bracketSpacing ? line : softline, join([',', line], namedImportsDocs)]),
2447
+ ifBreak(shouldPrintComma(options) ? ',' : ''),
2448
+ options.bracketSpacing ? line : softline,
2449
+ '}',
2450
+ ]);
2182
2451
  importClauseParts.push(namedImportsGroup);
2183
2452
  }
2184
2453
 
@@ -2186,16 +2455,28 @@ function printImportDeclaration(node, path, options, print) {
2186
2455
  if (importClauseParts.length === 1 && typeof importClauseParts[0] === 'object') {
2187
2456
  parts.push(importClauseParts[0]);
2188
2457
  } else {
2189
- parts.push(join(', ', importClauseParts));
2458
+ parts.push(/** @type {Doc} */ (join(', ', /** @type {string[]} */ (importClauseParts))));
2190
2459
  }
2191
2460
  parts.push(' from');
2192
2461
  }
2193
2462
 
2194
- parts.push(' ', formatStringLiteral(node.source.value, options), semi(options));
2463
+ parts.push(
2464
+ ' ',
2465
+ formatStringLiteral(/** @type {string} */ (node.source.value), options),
2466
+ semi(options),
2467
+ );
2195
2468
 
2196
- return concat(parts);
2469
+ return parts;
2197
2470
  }
2198
2471
 
2472
+ /**
2473
+ * Print an export named declaration
2474
+ * @param {RippleASTNode} node - The export declaration node
2475
+ * @param {AstPath<RippleASTNode>} path - The AST path
2476
+ * @param {RippleFormatOptions} options - Prettier options
2477
+ * @param {PrintFn} print - Print callback
2478
+ * @returns {Doc}
2479
+ */
2199
2480
  function printExportNamedDeclaration(node, path, options, print) {
2200
2481
  if (node.declaration) {
2201
2482
  const parts = [];
@@ -2203,11 +2484,13 @@ function printExportNamedDeclaration(node, path, options, print) {
2203
2484
  parts.push(path.call(print, 'declaration'));
2204
2485
  return parts;
2205
2486
  } else if (node.specifiers && node.specifiers.length > 0) {
2206
- const specifiers = node.specifiers.map((spec) => {
2207
- if (spec.exported.name === spec.local.name) {
2208
- return spec.local.name;
2487
+ const specifiers = node.specifiers.map((/** @type {RippleASTNode} */ spec) => {
2488
+ const exportedName = /** @type {string} */ (spec.exported.name);
2489
+ const localName = /** @type {string} */ (spec.local.name);
2490
+ if (exportedName === localName) {
2491
+ return localName;
2209
2492
  } else {
2210
- return spec.local.name + ' as ' + spec.exported.name;
2493
+ return localName + ' as ' + exportedName;
2211
2494
  }
2212
2495
  });
2213
2496
 
@@ -2220,7 +2503,7 @@ function printExportNamedDeclaration(node, path, options, print) {
2220
2503
 
2221
2504
  if (node.source) {
2222
2505
  parts.push(' from ');
2223
- parts.push(formatStringLiteral(node.source.value, options));
2506
+ parts.push(formatStringLiteral(/** @type {string} */ (node.source.value), options));
2224
2507
  }
2225
2508
  parts.push(semi(options));
2226
2509
 
@@ -2230,6 +2513,16 @@ function printExportNamedDeclaration(node, path, options, print) {
2230
2513
  return 'export';
2231
2514
  }
2232
2515
 
2516
+ /**
2517
+ * Print a Ripple component declaration
2518
+ * @param {RippleASTNode} node - The component node
2519
+ * @param {AstPath<RippleASTNode>} path - The AST path
2520
+ * @param {RippleFormatOptions} options - Prettier options
2521
+ * @param {PrintFn} print - Print callback
2522
+ * @param {Doc[]} [innerCommentParts=[]] - Inner comment docs
2523
+ * @param {{ skipComponentLabel?: boolean }} [args] - Additional args
2524
+ * @returns {Doc}
2525
+ */
2233
2526
  function printComponent(
2234
2527
  node,
2235
2528
  path,
@@ -2258,6 +2551,7 @@ function printComponent(
2258
2551
  // Print parameters using shared function
2259
2552
  const paramsPart = printFunctionParameters(path, options, print);
2260
2553
  signatureParts.push(group(paramsPart)); // Build body content using the same pattern as BlockStatement
2554
+ /** @type {Doc[]} */
2261
2555
  const statements = [];
2262
2556
 
2263
2557
  for (let i = 0; i < node.body.length; i++) {
@@ -2281,7 +2575,7 @@ function printComponent(
2281
2575
  // Process statements to add them to contentParts
2282
2576
  const contentParts = [];
2283
2577
  if (statements.length > 0) {
2284
- contentParts.push(concat(statements));
2578
+ contentParts.push(statements);
2285
2579
  }
2286
2580
 
2287
2581
  // Build script content using Prettier document builders
@@ -2306,7 +2600,8 @@ function printComponent(
2306
2600
  }
2307
2601
 
2308
2602
  // Use Prettier's standard block statement pattern
2309
- const parts = [concat(signatureParts), ' {'];
2603
+ /** @type {Doc[]} */
2604
+ const parts = [signatureParts, ' {'];
2310
2605
 
2311
2606
  if (statements.length > 0 || scriptContent) {
2312
2607
  // Build all content that goes inside the component body
@@ -2319,7 +2614,7 @@ function printComponent(
2319
2614
  if (statements.length > 0) {
2320
2615
  // The statements array contains statements separated by line breaks
2321
2616
  // We need to use join to properly handle the line breaks
2322
- contentParts.push(concat(statements));
2617
+ contentParts.push(statements);
2323
2618
  }
2324
2619
 
2325
2620
  // Add script content
@@ -2333,7 +2628,7 @@ function printComponent(
2333
2628
  }
2334
2629
 
2335
2630
  // Join content parts
2336
- const joinedContent = contentParts.length > 0 ? concat(contentParts) : '';
2631
+ const joinedContent = contentParts.length > 0 ? contentParts : '';
2337
2632
 
2338
2633
  // Apply component-level indentation
2339
2634
  const indentedContent = joinedContent ? indent([hardline, joinedContent]) : indent([hardline]);
@@ -2366,6 +2661,7 @@ function printComponent(
2366
2661
  const hasBlankLineBefore =
2367
2662
  prevComment && getBlankLinesBetweenNodes(prevComment, comment) > 0;
2368
2663
 
2664
+ /** @type {Doc | undefined} */
2369
2665
  let commentDoc;
2370
2666
  if (comment.type === 'Line') {
2371
2667
  commentDoc = '//' + comment.value;
@@ -2373,7 +2669,9 @@ function printComponent(
2373
2669
  commentDoc = '/*' + comment.value + '*/';
2374
2670
  }
2375
2671
 
2376
- commentDocs.push({ doc: commentDoc, hasBlankLineBefore });
2672
+ if (commentDoc !== undefined) {
2673
+ commentDocs.push({ doc: commentDoc, hasBlankLineBefore });
2674
+ }
2377
2675
  }
2378
2676
  }
2379
2677
 
@@ -2396,18 +2694,22 @@ function printComponent(
2396
2694
  contentParts.push(doc);
2397
2695
  }
2398
2696
 
2399
- return concat([
2400
- concat(signatureParts),
2401
- ' ',
2402
- group(['{', indent([hardline, concat(contentParts)]), hardline, '}']),
2403
- ]);
2697
+ return [signatureParts, ' ', group(['{', indent([hardline, contentParts]), hardline, '}'])];
2404
2698
  }
2405
2699
 
2406
2700
  parts[1] = ' {}';
2407
2701
  }
2408
- return concat(parts);
2702
+ return parts;
2409
2703
  }
2410
2704
 
2705
+ /**
2706
+ * Print a variable declaration
2707
+ * @param {RippleASTNode} node - The variable declaration node
2708
+ * @param {AstPath<RippleASTNode>} path - The AST path
2709
+ * @param {RippleFormatOptions} options - Prettier options
2710
+ * @param {PrintFn} print - Print callback
2711
+ * @returns {Doc}
2712
+ */
2411
2713
  function printVariableDeclaration(node, path, options, print) {
2412
2714
  const kind = node.kind || 'let';
2413
2715
 
@@ -2424,12 +2726,20 @@ function printVariableDeclaration(node, path, options, print) {
2424
2726
  const declarationParts = join(', ', declarations);
2425
2727
 
2426
2728
  if (!isForLoopInit) {
2427
- return concat([kind, ' ', declarationParts, semi(options)]);
2729
+ return [kind, ' ', declarationParts, semi(options)];
2428
2730
  }
2429
2731
 
2430
- return concat([kind, ' ', declarationParts]);
2732
+ return [kind, ' ', declarationParts];
2431
2733
  }
2432
2734
 
2735
+ /**
2736
+ * Print a function expression
2737
+ * @param {RippleASTNode} node - The function expression node
2738
+ * @param {AstPath<RippleASTNode>} path - The AST path
2739
+ * @param {RippleFormatOptions} options - Prettier options
2740
+ * @param {PrintFn} print - Print callback
2741
+ * @returns {Doc}
2742
+ */
2433
2743
  function printFunctionExpression(node, path, options, print) {
2434
2744
  const parts = [];
2435
2745
 
@@ -2478,9 +2788,17 @@ function printFunctionExpression(node, path, options, print) {
2478
2788
  parts.push(' ');
2479
2789
  parts.push(path.call(print, 'body'));
2480
2790
 
2481
- return concat(parts);
2791
+ return parts;
2482
2792
  }
2483
2793
 
2794
+ /**
2795
+ * Print an arrow function expression
2796
+ * @param {RippleASTNode} node - The arrow function node
2797
+ * @param {AstPath<RippleASTNode>} path - The AST path
2798
+ * @param {RippleFormatOptions} options - Prettier options
2799
+ * @param {PrintFn} print - Print callback
2800
+ * @returns {Doc}
2801
+ */
2484
2802
  function printArrowFunction(node, path, options, print) {
2485
2803
  const parts = [];
2486
2804
 
@@ -2541,9 +2859,17 @@ function printArrowFunction(node, path, options, print) {
2541
2859
  }
2542
2860
  }
2543
2861
 
2544
- return concat(parts);
2862
+ return parts;
2545
2863
  }
2546
2864
 
2865
+ /**
2866
+ * Print an export default declaration
2867
+ * @param {RippleASTNode} node - The export default node
2868
+ * @param {AstPath<RippleASTNode>} path - The AST path
2869
+ * @param {RippleFormatOptions} options - Prettier options
2870
+ * @param {PrintFn} print - Print callback
2871
+ * @returns {Doc[]}
2872
+ */
2547
2873
  function printExportDefaultDeclaration(node, path, options, print) {
2548
2874
  const parts = [];
2549
2875
  parts.push('export default ');
@@ -2551,6 +2877,11 @@ function printExportDefaultDeclaration(node, path, options, print) {
2551
2877
  return parts;
2552
2878
  }
2553
2879
 
2880
+ /**
2881
+ * Check if the only function parameter should be hugged (no extra parens)
2882
+ * @param {RippleASTNode} node - The function node
2883
+ * @returns {boolean}
2884
+ */
2554
2885
  function shouldHugTheOnlyFunctionParameter(node) {
2555
2886
  if (!node) {
2556
2887
  return false;
@@ -2571,6 +2902,13 @@ function shouldHugTheOnlyFunctionParameter(node) {
2571
2902
  );
2572
2903
  }
2573
2904
 
2905
+ /**
2906
+ * Print function parameters with proper formatting
2907
+ * @param {AstPath<RippleASTNode>} path - The function path
2908
+ * @param {RippleFormatOptions} options - Prettier options
2909
+ * @param {PrintFn} print - Print callback
2910
+ * @returns {Doc[]}
2911
+ */
2574
2912
  function printFunctionParameters(path, options, print) {
2575
2913
  const functionNode = path.node;
2576
2914
  const parameters = getFunctionParameters(functionNode);
@@ -2580,6 +2918,7 @@ function printFunctionParameters(path, options, print) {
2580
2918
  }
2581
2919
 
2582
2920
  const shouldHugParameters = shouldHugTheOnlyFunctionParameter(functionNode);
2921
+ /** @type {Doc[]} */
2583
2922
  const printed = [];
2584
2923
 
2585
2924
  iterateFunctionParametersPath(path, (parameterPath, index) => {
@@ -2620,10 +2959,20 @@ function printFunctionParameters(path, options, print) {
2620
2959
  ];
2621
2960
  }
2622
2961
 
2962
+ /**
2963
+ * Check if a node is spread-like (SpreadElement or RestElement)
2964
+ * @param {RippleASTNode} node - The AST node
2965
+ * @returns {boolean}
2966
+ */
2623
2967
  function isSpreadLike(node) {
2624
2968
  return node && (node.type === 'SpreadElement' || node.type === 'RestElement');
2625
2969
  }
2626
2970
 
2971
+ /**
2972
+ * Check if a node is a block-like function (function expression or arrow with block body)
2973
+ * @param {RippleASTNode} node - The AST node
2974
+ * @returns {boolean}
2975
+ */
2627
2976
  function isBlockLikeFunction(node) {
2628
2977
  if (!node) {
2629
2978
  return false;
@@ -2637,6 +2986,12 @@ function isBlockLikeFunction(node) {
2637
2986
  return false;
2638
2987
  }
2639
2988
 
2989
+ /**
2990
+ * Determine if the last argument should be hugged (no line break before it)
2991
+ * @param {RippleASTNode[]} args - Array of arguments
2992
+ * @param {boolean[]} argumentBreakFlags - Flags indicating which args break
2993
+ * @returns {boolean}
2994
+ */
2640
2995
  function shouldHugLastArgument(args, argumentBreakFlags) {
2641
2996
  if (!args || args.length === 0) {
2642
2997
  return false;
@@ -2673,7 +3028,11 @@ function shouldHugLastArgument(args, argumentBreakFlags) {
2673
3028
  return true;
2674
3029
  }
2675
3030
 
2676
- // Check if arguments contain arrow functions with block bodies that should be hugged
3031
+ /**
3032
+ * Check if arguments contain arrow functions with block bodies that should be hugged
3033
+ * @param {RippleASTNode[]} args - Array of arguments
3034
+ * @returns {boolean}
3035
+ */
2677
3036
  function shouldHugArrowFunctions(args) {
2678
3037
  if (!args || args.length === 0) {
2679
3038
  return false;
@@ -2697,6 +3056,13 @@ function shouldHugArrowFunctions(args) {
2697
3056
  return firstBlockIndex === 0;
2698
3057
  }
2699
3058
 
3059
+ /**
3060
+ * Print call expression arguments
3061
+ * @param {AstPath<RippleASTNode>} path - The call path
3062
+ * @param {RippleFormatOptions} options - Prettier options
3063
+ * @param {PrintFn} print - Print callback
3064
+ * @returns {Doc}
3065
+ */
2700
3066
  function printCallArguments(path, options, print) {
2701
3067
  const { node } = path;
2702
3068
  const args = node.arguments || [];
@@ -2715,8 +3081,11 @@ function printCallArguments(path, options, print) {
2715
3081
  finalArg.type === 'TrackedArrayExpression') &&
2716
3082
  !hasComment(finalArg);
2717
3083
 
3084
+ /** @type {Doc[]} */
2718
3085
  const printedArguments = [];
3086
+ /** @type {Doc[]} */
2719
3087
  const argumentDocs = [];
3088
+ /** @type {boolean[]} */
2720
3089
  const argumentBreakFlags = [];
2721
3090
  let anyArgumentHasEmptyLine = false;
2722
3091
 
@@ -2737,9 +3106,9 @@ function printCallArguments(path, options, print) {
2737
3106
  if (!isLast) {
2738
3107
  if (isNextLineEmpty(argumentNode, options)) {
2739
3108
  anyArgumentHasEmptyLine = true;
2740
- printedArguments.push(concat([argumentDoc, ',', hardline, hardline]));
3109
+ printedArguments.push([argumentDoc, ',', hardline, hardline]);
2741
3110
  } else {
2742
- printedArguments.push(concat([argumentDoc, ',', line]));
3111
+ printedArguments.push([argumentDoc, ',', line]);
2743
3112
  }
2744
3113
  } else {
2745
3114
  printedArguments.push(argumentDoc);
@@ -2756,7 +3125,7 @@ function printCallArguments(path, options, print) {
2756
3125
  if (isSingleArrayArgument) {
2757
3126
  // Don't use group() - just concat to allow array to control its own breaking
2758
3127
  // For single argument, no trailing comma needed
2759
- return concat(['(', argumentDocs[0], ')']);
3128
+ return ['(', argumentDocs[0], ')'];
2760
3129
  } // Check if we should hug arrow functions (keep params inline even when body breaks)
2761
3130
  const shouldHugArrows = shouldHugArrowFunctions(args);
2762
3131
  let huggedArrowDoc = null;
@@ -2765,6 +3134,7 @@ function printCallArguments(path, options, print) {
2765
3134
  // but allow the block body to break naturally
2766
3135
  if (shouldHugArrows && !anyArgumentHasEmptyLine) {
2767
3136
  // Build a version that keeps arguments inline with opening paren
3137
+ /** @type {Doc[]} */
2768
3138
  const huggedParts = ['('];
2769
3139
 
2770
3140
  for (let index = 0; index < args.length; index++) {
@@ -2775,7 +3145,7 @@ function printCallArguments(path, options, print) {
2775
3145
  }
2776
3146
 
2777
3147
  huggedParts.push(')');
2778
- huggedArrowDoc = concat(huggedParts);
3148
+ huggedArrowDoc = huggedParts;
2779
3149
  }
2780
3150
 
2781
3151
  // Build standard breaking version with indentation
@@ -2826,6 +3196,7 @@ function printCallArguments(path, options, print) {
2826
3196
  );
2827
3197
 
2828
3198
  // Build the inline version: head args inline + expanded last arg
3199
+ /** @type {Doc[]} */
2829
3200
  const inlinePartsWithExpanded = ['('];
2830
3201
  for (let index = 0; index < headArgs.length; index++) {
2831
3202
  if (index > 0) {
@@ -2841,9 +3212,9 @@ function printCallArguments(path, options, print) {
2841
3212
 
2842
3213
  return conditionalGroup([
2843
3214
  // Try with normal formatting first
2844
- concat(['(', ...argumentDocs.flatMap((doc, i) => (i > 0 ? [', ', doc] : [doc])), ')']),
3215
+ ['(', ...argumentDocs.flatMap((doc, i) => (i > 0 ? [', ', doc] : [doc])), ')'],
2845
3216
  // Then try with expanded last arg
2846
- concat(inlinePartsWithExpanded),
3217
+ inlinePartsWithExpanded,
2847
3218
  // Finally fall back to all args broken out
2848
3219
  groupedContents,
2849
3220
  ]);
@@ -2858,6 +3229,7 @@ function printCallArguments(path, options, print) {
2858
3229
  !hasComment(lastArg);
2859
3230
 
2860
3231
  if (canInlineLastArg) {
3232
+ /** @type {Doc[]} */
2861
3233
  const inlineParts = ['('];
2862
3234
  for (let index = 0; index < argumentDocs.length; index++) {
2863
3235
  if (index > 0) {
@@ -2867,11 +3239,12 @@ function printCallArguments(path, options, print) {
2867
3239
  }
2868
3240
  inlineParts.push(')');
2869
3241
 
2870
- return conditionalGroup([concat(inlineParts), groupedContents]);
3242
+ return conditionalGroup([inlineParts, groupedContents]);
2871
3243
  }
2872
3244
 
2873
3245
  if (!anyArgumentHasEmptyLine && shouldHugLastArgument(args, argumentBreakFlags)) {
2874
3246
  const lastIndex = args.length - 1;
3247
+ /** @type {Doc[]} */
2875
3248
  const inlineParts = ['('];
2876
3249
 
2877
3250
  for (let index = 0; index < lastIndex; index++) {
@@ -2894,6 +3267,74 @@ function printCallArguments(path, options, print) {
2894
3267
  return groupedContents;
2895
3268
  }
2896
3269
 
3270
+ /**
3271
+ * Print TSDeclareFunction (TypeScript function overload declaration)
3272
+ * These are function signatures without bodies, ending with semicolon
3273
+ * @param {RippleASTNode} node - The TS function declaration node
3274
+ * @param {AstPath<RippleASTNode>} path - The AST path
3275
+ * @param {RippleFormatOptions} options - Prettier options
3276
+ * @param {PrintFn} print - Print callback
3277
+ * @returns {Doc[]}
3278
+ */
3279
+ function printTSDeclareFunction(node, path, options, print) {
3280
+ const parts = [];
3281
+
3282
+ // Handle declare modifier for ambient declarations
3283
+ if (node.declare) {
3284
+ parts.push('declare ');
3285
+ }
3286
+
3287
+ // Handle async functions
3288
+ if (node.async) {
3289
+ parts.push('async ');
3290
+ }
3291
+
3292
+ parts.push('function');
3293
+
3294
+ // Handle generator functions
3295
+ if (node.generator) {
3296
+ parts.push('*');
3297
+ }
3298
+
3299
+ // Handle function name (may be null for anonymous default exports)
3300
+ if (node.id) {
3301
+ parts.push(' ');
3302
+ parts.push(node.id.name);
3303
+ }
3304
+
3305
+ // Add TypeScript generics if present
3306
+ if (node.typeParameters) {
3307
+ const typeParams = path.call(print, 'typeParameters');
3308
+ if (Array.isArray(typeParams)) {
3309
+ parts.push(...typeParams);
3310
+ } else {
3311
+ parts.push(typeParams);
3312
+ }
3313
+ }
3314
+
3315
+ // Print parameters using shared function
3316
+ const paramsPart = printFunctionParameters(path, options, print);
3317
+ parts.push(group(paramsPart));
3318
+
3319
+ // Handle return type annotation
3320
+ if (node.returnType) {
3321
+ parts.push(': ', path.call(print, 'returnType'));
3322
+ }
3323
+
3324
+ // TSDeclareFunction ends with semicolon, no body
3325
+ parts.push(';');
3326
+
3327
+ return parts;
3328
+ }
3329
+
3330
+ /**
3331
+ * Print a function declaration
3332
+ * @param {RippleASTNode} node - The function declaration node
3333
+ * @param {AstPath<RippleASTNode>} path - The AST path
3334
+ * @param {RippleFormatOptions} options - Prettier options
3335
+ * @param {PrintFn} print - Print callback
3336
+ * @returns {Doc[]}
3337
+ */
2897
3338
  function printFunctionDeclaration(node, path, options, print) {
2898
3339
  const parts = [];
2899
3340
 
@@ -2937,22 +3378,115 @@ function printFunctionDeclaration(node, path, options, print) {
2937
3378
  return parts;
2938
3379
  }
2939
3380
 
3381
+ /**
3382
+ * Extract and print leading comments from a node before a control flow statement keyword
3383
+ * @param {Node} node - The node that may have leading comments
3384
+ * @returns {Doc[]} - Array of doc parts for the comments
3385
+ */
3386
+ function extractAndPrintLeadingComments(node) {
3387
+ const leadingComments = node && node.leadingComments;
3388
+ const parts = [];
3389
+
3390
+ if (leadingComments && leadingComments.length > 0) {
3391
+ for (let i = 0; i < leadingComments.length; i++) {
3392
+ const comment = leadingComments[i];
3393
+ const nextComment = leadingComments[i + 1];
3394
+
3395
+ if (comment.type === 'Line') {
3396
+ parts.push('//' + comment.value);
3397
+ parts.push(hardline);
3398
+
3399
+ // Check if there should be blank lines between comments
3400
+ if (nextComment) {
3401
+ const blankLinesBetween = getBlankLinesBetweenNodes(comment, nextComment);
3402
+ if (blankLinesBetween > 0) {
3403
+ parts.push(hardline);
3404
+ }
3405
+ }
3406
+ } else if (comment.type === 'Block') {
3407
+ parts.push('/*' + comment.value + '*/');
3408
+ parts.push(hardline);
3409
+
3410
+ // Check if there should be blank lines between comments
3411
+ if (nextComment) {
3412
+ const blankLinesBetween = getBlankLinesBetweenNodes(comment, nextComment);
3413
+ if (blankLinesBetween > 0) {
3414
+ parts.push(hardline);
3415
+ }
3416
+ }
3417
+ }
3418
+ }
3419
+ }
3420
+
3421
+ return parts;
3422
+ }
3423
+
3424
+ /**
3425
+ * Print an if statement
3426
+ * @param {RippleASTNode} node - The if statement node
3427
+ * @param {AstPath<RippleASTNode>} path - The AST path
3428
+ * @param {RippleFormatOptions} options - Prettier options
3429
+ * @param {PrintFn} print - Print callback
3430
+ * @returns {Doc}
3431
+ */
2940
3432
  function printIfStatement(node, path, options, print) {
2941
- const test = path.call(print, 'test');
3433
+ // Extract leading comments from test node to print them before 'if' keyword
3434
+ const testNode = node.test;
3435
+
3436
+ // Print test without its leading comments (they'll be printed before 'if')
3437
+ const test = path.call((testPath) => print(testPath, { suppressLeadingComments: true }), 'test');
2942
3438
  const consequent = path.call(print, 'consequent');
2943
3439
 
2944
3440
  // Use group to allow breaking the test when it doesn't fit
2945
- const testDoc = group(concat(['if (', indent(concat([softline, test])), softline, ')']));
3441
+ const testDoc = group(['if (', indent([softline, test]), softline, ')']);
3442
+
3443
+ // Check if consequent is a block statement or another if statement
3444
+ const consequentIsBlock = node.consequent.type === 'BlockStatement';
3445
+ const consequentIsIf = node.consequent.type === 'IfStatement';
2946
3446
 
2947
- const parts = [testDoc, ' ', consequent];
3447
+ const parts = [];
3448
+
3449
+ // Print leading comments from test node before 'if' keyword
3450
+ parts.push(...extractAndPrintLeadingComments(testNode));
3451
+
3452
+ parts.push(testDoc);
3453
+
3454
+ // Handle the consequent
3455
+ if (consequentIsBlock) {
3456
+ // For block statements, add a space before the block
3457
+ parts.push(' ', consequent);
3458
+ } else if (consequentIsIf) {
3459
+ // For nested if statements, add a line break and indent
3460
+ parts.push(indent([hardline, consequent]));
3461
+ } else {
3462
+ // For other non-block statements, add a space
3463
+ parts.push(' ', consequent);
3464
+ }
2948
3465
 
3466
+ // Handle the alternate
2949
3467
  if (node.alternate) {
2950
- parts.push(' else ', path.call(print, 'alternate'));
3468
+ // If consequent is not a block, add a hardline before else
3469
+ if (!consequentIsBlock) {
3470
+ parts.push(hardline);
3471
+ } else {
3472
+ parts.push(' ');
3473
+ }
3474
+
3475
+ parts.push('else ');
3476
+ parts.push(path.call(print, 'alternate'));
2951
3477
  }
2952
3478
 
2953
- return concat(parts);
3479
+ return parts;
2954
3480
  }
2955
3481
 
3482
+ /**
3483
+ * Print a for-in statement
3484
+ * @param {RippleASTNode} node - The for-in statement node
3485
+ * @param {AstPath<RippleASTNode>} path - The AST path
3486
+ * @param {RippleFormatOptions} options - Prettier options
3487
+ * @param {PrintFn} print - Print callback
3488
+ * @returns {Doc[]}
3489
+ */
2956
3490
  function printForInStatement(node, path, options, print) {
2957
3491
  const parts = [];
2958
3492
  parts.push('for (');
@@ -2966,6 +3500,14 @@ function printForInStatement(node, path, options, print) {
2966
3500
  return parts;
2967
3501
  }
2968
3502
 
3503
+ /**
3504
+ * Print a for-of statement (with Ripple index/key extensions)
3505
+ * @param {RippleASTNode} node - The for-of statement node
3506
+ * @param {AstPath<RippleASTNode>} path - The AST path
3507
+ * @param {RippleFormatOptions} options - Prettier options
3508
+ * @param {PrintFn} print - Print callback
3509
+ * @returns {Doc[]}
3510
+ */
2969
3511
  function printForOfStatement(node, path, options, print) {
2970
3512
  const parts = [];
2971
3513
  parts.push('for (');
@@ -2990,6 +3532,14 @@ function printForOfStatement(node, path, options, print) {
2990
3532
  return parts;
2991
3533
  }
2992
3534
 
3535
+ /**
3536
+ * Print a for statement
3537
+ * @param {RippleASTNode} node - The for statement node
3538
+ * @param {AstPath<RippleASTNode>} path - The AST path
3539
+ * @param {RippleFormatOptions} options - Prettier options
3540
+ * @param {PrintFn} print - Print callback
3541
+ * @returns {Doc[]}
3542
+ */
2993
3543
  function printForStatement(node, path, options, print) {
2994
3544
  const parts = [];
2995
3545
  parts.push('for (');
@@ -3019,28 +3569,78 @@ function printForStatement(node, path, options, print) {
3019
3569
  return parts;
3020
3570
  }
3021
3571
 
3022
- // Updated for-loop formatting
3572
+ /**
3573
+ * Print a while statement
3574
+ * @param {RippleASTNode} node - The while statement node
3575
+ * @param {AstPath<RippleASTNode>} path - The AST path
3576
+ * @param {RippleFormatOptions} options - Prettier options
3577
+ * @param {PrintFn} print - Print callback
3578
+ * @returns {Doc[]}
3579
+ */
3023
3580
  function printWhileStatement(node, path, options, print) {
3581
+ // Extract leading comments from test node to print them before 'while' keyword
3582
+ const testNode = node.test;
3583
+
3584
+ // Print test without its leading comments (they'll be printed before 'while')
3585
+ const test = path.call((testPath) => print(testPath, { suppressLeadingComments: true }), 'test');
3586
+
3024
3587
  const parts = [];
3588
+
3589
+ // Print leading comments from test node before 'while' keyword
3590
+ parts.push(...extractAndPrintLeadingComments(testNode));
3591
+
3025
3592
  parts.push('while (');
3026
- parts.push(path.call(print, 'test'));
3593
+ parts.push(test);
3027
3594
  parts.push(') ');
3028
3595
  parts.push(path.call(print, 'body'));
3029
3596
 
3030
3597
  return parts;
3031
3598
  }
3032
3599
 
3600
+ /**
3601
+ * Print a do-while statement
3602
+ * @param {RippleASTNode} node - The do-while statement node
3603
+ * @param {AstPath<RippleASTNode>} path - The AST path
3604
+ * @param {RippleFormatOptions} options - Prettier options
3605
+ * @param {PrintFn} print - Print callback
3606
+ * @returns {Doc[]}
3607
+ */
3033
3608
  function printDoWhileStatement(node, path, options, print) {
3609
+ // Extract leading comments from test node to print them before 'while' keyword
3610
+ const testNode = node.test;
3611
+
3612
+ // Print test without its leading comments (they'll be printed before 'while')
3613
+ const test = path.call((testPath) => print(testPath, { suppressLeadingComments: true }), 'test');
3614
+
3034
3615
  const parts = [];
3035
3616
  parts.push('do ');
3036
3617
  parts.push(path.call(print, 'body'));
3037
- parts.push(' while (');
3038
- parts.push(path.call(print, 'test'));
3618
+
3619
+ // Print leading comments from test node before 'while' keyword
3620
+ const commentParts = extractAndPrintLeadingComments(testNode);
3621
+ if (commentParts.length > 0) {
3622
+ parts.push(' ');
3623
+ parts.push(...commentParts);
3624
+ } else {
3625
+ parts.push(' ');
3626
+ }
3627
+
3628
+ parts.push('while (');
3629
+ parts.push(test);
3039
3630
  parts.push(')');
3040
3631
 
3041
3632
  return parts;
3042
3633
  }
3043
3634
 
3635
+ /**
3636
+ * Print an object expression (or TrackedObjectExpression)
3637
+ * @param {RippleASTNode} node - The object expression node
3638
+ * @param {AstPath<RippleASTNode>} path - The AST path
3639
+ * @param {RippleFormatOptions} options - Prettier options
3640
+ * @param {PrintFn} print - Print callback
3641
+ * @param {PrintArgs} [args] - Additional context arguments
3642
+ * @returns {Doc}
3643
+ */
3044
3644
  function printObjectExpression(node, path, options, print, args) {
3045
3645
  const skip_offset = node.type === 'TrackedObjectExpression' ? 2 : 1;
3046
3646
  const open_brace = node.type === 'TrackedObjectExpression' ? '#{' : '{';
@@ -3074,18 +3674,14 @@ function printObjectExpression(node, path, options, print, args) {
3074
3674
 
3075
3675
  // Check for blank line after opening brace (before first property)
3076
3676
  if (firstProp && node.loc && node.loc.start) {
3077
- hasAnyBlankLines = getBlankLinesBetweenPositions(
3078
- node.loc.start.offset(skip_offset),
3079
- firstProp.loc.start,
3080
- );
3677
+ hasAnyBlankLines =
3678
+ getBlankLinesBetweenPositions(node.loc.start.offset(skip_offset), firstProp.loc.start) > 0;
3081
3679
  }
3082
3680
 
3083
3681
  // Check for blank line before closing brace (after last property)
3084
3682
  if (!hasAnyBlankLines && lastProp && node.loc && node.loc.end) {
3085
- hasAnyBlankLines = getBlankLinesBetweenPositions(
3086
- lastProp.loc.end,
3087
- node.loc.end.offset(-1), // -1 to skip the '}'
3088
- );
3683
+ hasAnyBlankLines =
3684
+ getBlankLinesBetweenPositions(lastProp.loc.end, node.loc.end.offset(-1)) > 0; // -1 to skip the '}'
3089
3685
  }
3090
3686
  }
3091
3687
 
@@ -3107,20 +3703,18 @@ function printObjectExpression(node, path, options, print, args) {
3107
3703
  if (isInArray) {
3108
3704
  if (isVerySimple) {
3109
3705
  // 1-property objects: force inline with spaces
3110
- return concat([open_brace, ' ', properties[0], ' ', '}']);
3706
+ return [open_brace, ' ', properties[0], ' ', '}'];
3111
3707
  }
3112
3708
  }
3113
3709
  }
3114
3710
 
3115
3711
  if (args && args.allowInlineObject) {
3116
- const separator = concat([',', line]);
3712
+ const separator = [',', line];
3117
3713
  const propertyDoc = join(separator, properties);
3118
3714
  const spacing = options.bracketSpacing === false ? softline : line;
3119
3715
  const trailingDoc = shouldUseTrailingComma ? ifBreak(',', '') : '';
3120
3716
 
3121
- return group(
3122
- concat([open_brace, indent(concat([spacing, propertyDoc, trailingDoc])), spacing, '}']),
3123
- );
3717
+ return group([open_brace, indent([spacing, propertyDoc, trailingDoc]), spacing, '}']);
3124
3718
  }
3125
3719
 
3126
3720
  // For objects that were originally inline (single-line) and don't have blank lines,
@@ -3128,16 +3722,15 @@ function printObjectExpression(node, path, options, print, args) {
3128
3722
  // This handles cases like `const T0: t17 = { x: 1 };` staying inline when it fits
3129
3723
  // The group() will automatically break to multi-line if it doesn't fit
3130
3724
  if (!hasAnyBlankLines && !isOriginallyMultiLine && !isInArray) {
3131
- const separator = concat([',', line]);
3725
+ const separator = [',', line];
3132
3726
  const propertyDoc = join(separator, properties);
3133
3727
  const spacing = options.bracketSpacing === false ? softline : line;
3134
3728
  const trailingDoc = shouldUseTrailingComma ? ifBreak(',', '') : '';
3135
3729
 
3136
- return group(
3137
- concat([open_brace, indent(concat([spacing, propertyDoc, trailingDoc])), spacing, '}']),
3138
- );
3730
+ return group([open_brace, indent([spacing, propertyDoc, trailingDoc]), spacing, '}']);
3139
3731
  }
3140
3732
 
3733
+ /** @type {Doc[]} */
3141
3734
  let content = [hardline];
3142
3735
  if (properties.length > 0) {
3143
3736
  // Build properties with blank line preservation
@@ -3174,7 +3767,7 @@ function printObjectExpression(node, path, options, print, args) {
3174
3767
  propertyParts.push(properties[i]);
3175
3768
  }
3176
3769
 
3177
- content.push(concat(propertyParts));
3770
+ content.push(...propertyParts);
3178
3771
  if (shouldUseTrailingComma) {
3179
3772
  content.push(',');
3180
3773
  }
@@ -3184,6 +3777,14 @@ function printObjectExpression(node, path, options, print, args) {
3184
3777
  return group([open_brace, indent(content.slice(0, -1)), content[content.length - 1], '}']);
3185
3778
  }
3186
3779
 
3780
+ /**
3781
+ * Print a class declaration
3782
+ * @param {RippleASTNode} node - The class node
3783
+ * @param {AstPath<RippleASTNode>} path - The AST path
3784
+ * @param {RippleFormatOptions} options - Prettier options
3785
+ * @param {PrintFn} print - Print callback
3786
+ * @returns {Doc[]}
3787
+ */
3187
3788
  function printClassDeclaration(node, path, options, print) {
3188
3789
  const parts = [];
3189
3790
  parts.push('class');
@@ -3215,10 +3816,31 @@ function printClassDeclaration(node, path, options, print) {
3215
3816
  return parts;
3216
3817
  }
3217
3818
 
3819
+ /**
3820
+ * Print a try statement (with Ripple pending block extension)
3821
+ * @param {RippleASTNode} node - The try statement node
3822
+ * @param {AstPath<RippleASTNode>} path - The AST path
3823
+ * @param {RippleFormatOptions} options - Prettier options
3824
+ * @param {PrintFn} print - Print callback
3825
+ * @returns {Doc[]}
3826
+ */
3218
3827
  function printTryStatement(node, path, options, print) {
3828
+ // Extract leading comments from block node to print them before 'try' keyword
3829
+ const blockNode = node.block;
3830
+
3831
+ // Print block without its leading comments (they'll be printed before 'try')
3832
+ const block = path.call(
3833
+ (blockPath) => print(blockPath, { suppressLeadingComments: true }),
3834
+ 'block',
3835
+ );
3836
+
3219
3837
  const parts = [];
3838
+
3839
+ // Print leading comments from block node before 'try' keyword
3840
+ parts.push(...extractAndPrintLeadingComments(blockNode));
3841
+
3220
3842
  parts.push('try ');
3221
- parts.push(path.call(print, 'block'));
3843
+ parts.push(block);
3222
3844
 
3223
3845
  if (node.pending) {
3224
3846
  parts.push(' pending ');
@@ -3244,6 +3866,14 @@ function printTryStatement(node, path, options, print) {
3244
3866
  return parts;
3245
3867
  }
3246
3868
 
3869
+ /**
3870
+ * Print a class body
3871
+ * @param {RippleASTNode} node - The class body node
3872
+ * @param {AstPath<RippleASTNode>} path - The AST path
3873
+ * @param {RippleFormatOptions} options - Prettier options
3874
+ * @param {PrintFn} print - Print callback
3875
+ * @returns {Doc}
3876
+ */
3247
3877
  function printClassBody(node, path, options, print) {
3248
3878
  if (!node.body || node.body.length === 0) {
3249
3879
  return '{}';
@@ -3266,9 +3896,17 @@ function printClassBody(node, path, options, print) {
3266
3896
  contentParts.push(members[i]);
3267
3897
  }
3268
3898
 
3269
- return group(['{', indent(concat(contentParts)), line, '}']);
3899
+ return group(['{', indent(contentParts), line, '}']);
3270
3900
  }
3271
3901
 
3902
+ /**
3903
+ * Print a class property definition
3904
+ * @param {RippleASTNode} node - The property definition node
3905
+ * @param {AstPath<RippleASTNode>} path - The AST path
3906
+ * @param {RippleFormatOptions} options - Prettier options
3907
+ * @param {PrintFn} print - Print callback
3908
+ * @returns {Doc}
3909
+ */
3272
3910
  function printPropertyDefinition(node, path, options, print) {
3273
3911
  const parts = [];
3274
3912
 
@@ -3310,9 +3948,17 @@ function printPropertyDefinition(node, path, options, print) {
3310
3948
 
3311
3949
  parts.push(semi(options));
3312
3950
 
3313
- return concat(parts);
3951
+ return parts;
3314
3952
  }
3315
3953
 
3954
+ /**
3955
+ * Print a method definition
3956
+ * @param {RippleASTNode} node - The method definition node
3957
+ * @param {AstPath<RippleASTNode>} path - The AST path
3958
+ * @param {RippleFormatOptions} options - Prettier options
3959
+ * @param {PrintFn} print - Print callback
3960
+ * @returns {Doc}
3961
+ */
3316
3962
  function printMethodDefinition(node, path, options, print) {
3317
3963
  const parts = [];
3318
3964
  const is_component = node.value?.type === 'Component';
@@ -3350,7 +3996,7 @@ function printMethodDefinition(node, path, options, print) {
3350
3996
  if (node.value.id) {
3351
3997
  // takes care of component methods
3352
3998
  parts.push(path.call(print, 'value'));
3353
- return concat(parts);
3999
+ return parts;
3354
4000
  }
3355
4001
 
3356
4002
  parts.push('component ');
@@ -3370,10 +4016,12 @@ function printMethodDefinition(node, path, options, print) {
3370
4016
  }
3371
4017
 
3372
4018
  if (is_component) {
3373
- parts.push(
3374
- ...path.call((childPath) => print(childPath, { skipComponentLabel: true }), 'value'),
4019
+ const componentDoc = path.call(
4020
+ (childPath) => print(childPath, { skipComponentLabel: true }),
4021
+ 'value',
3375
4022
  );
3376
- return concat(parts);
4023
+ parts.push(componentDoc);
4024
+ return parts;
3377
4025
  }
3378
4026
 
3379
4027
  // Parameters - use proper path.map for TypeScript support
@@ -3400,14 +4048,22 @@ function printMethodDefinition(node, path, options, print) {
3400
4048
  parts.push('{}');
3401
4049
  }
3402
4050
 
3403
- return concat(parts);
4051
+ return parts;
3404
4052
  }
3405
4053
 
4054
+ /**
4055
+ * Print a member expression (object.property or object[property])
4056
+ * @param {RippleASTNode} node - The member expression node
4057
+ * @param {AstPath<RippleASTNode>} path - The AST path
4058
+ * @param {RippleFormatOptions} options - Prettier options
4059
+ * @param {PrintFn} print - Print callback
4060
+ * @returns {Doc}
4061
+ */
3406
4062
  function printMemberExpression(node, path, options, print) {
3407
4063
  let objectPart = path.call(print, 'object');
3408
4064
  // Preserve parentheses around the object when present
3409
4065
  if (node.object.metadata?.parenthesized) {
3410
- objectPart = concat(['(', objectPart, ')']);
4066
+ objectPart = ['(', objectPart, ')'];
3411
4067
  }
3412
4068
  const propertyPart = path.call(print, 'property');
3413
4069
 
@@ -3420,10 +4076,10 @@ function printMemberExpression(node, path, options, print) {
3420
4076
  : trackedPrefix
3421
4077
  ? '.' + trackedPrefix + '['
3422
4078
  : '[';
3423
- result = concat([objectPart, openBracket, propertyPart, ']']);
4079
+ result = [objectPart, openBracket, propertyPart, ']'];
3424
4080
  } else {
3425
4081
  const separator = node.optional ? '?.' : '.';
3426
- result = concat([objectPart, separator, propertyPart]);
4082
+ result = [objectPart, separator, propertyPart];
3427
4083
  }
3428
4084
 
3429
4085
  // Preserve parentheses around the entire member expression when present
@@ -3431,15 +4087,23 @@ function printMemberExpression(node, path, options, print) {
3431
4087
  // Check if there are leading comments - if so, use group with softlines to allow breaking
3432
4088
  const hasLeadingComments = node.leadingComments && node.leadingComments.length > 0;
3433
4089
  if (hasLeadingComments) {
3434
- result = group(concat(['(', indent(concat([softline, result])), softline, ')']));
4090
+ result = group(['(', indent([softline, result]), softline, ')']);
3435
4091
  } else {
3436
- result = concat(['(', result, ')']);
4092
+ result = ['(', result, ')'];
3437
4093
  }
3438
4094
  }
3439
4095
 
3440
4096
  return result;
3441
4097
  }
3442
4098
 
4099
+ /**
4100
+ * Print a unary expression
4101
+ * @param {RippleASTNode} node - The unary expression node
4102
+ * @param {AstPath<RippleASTNode>} path - The AST path
4103
+ * @param {RippleFormatOptions} options - Prettier options
4104
+ * @param {PrintFn} print - Print callback
4105
+ * @returns {Doc}
4106
+ */
3443
4107
  function printUnaryExpression(node, path, options, print) {
3444
4108
  const parts = [];
3445
4109
 
@@ -3468,9 +4132,17 @@ function printUnaryExpression(node, path, options, print) {
3468
4132
  parts.push(node.operator);
3469
4133
  }
3470
4134
 
3471
- return concat(parts);
4135
+ return parts;
3472
4136
  }
3473
4137
 
4138
+ /**
4139
+ * Print a yield expression
4140
+ * @param {RippleASTNode} node - The yield expression node
4141
+ * @param {AstPath<RippleASTNode>} path - The AST path
4142
+ * @param {RippleFormatOptions} options - Prettier options
4143
+ * @param {PrintFn} print - Print callback
4144
+ * @returns {Doc[]}
4145
+ */
3474
4146
  function printYieldExpression(node, path, options, print) {
3475
4147
  const parts = [];
3476
4148
  parts.push('yield');
@@ -3487,6 +4159,14 @@ function printYieldExpression(node, path, options, print) {
3487
4159
  return parts;
3488
4160
  }
3489
4161
 
4162
+ /**
4163
+ * Print a new expression
4164
+ * @param {RippleASTNode} node - The new expression node
4165
+ * @param {AstPath<RippleASTNode>} path - The AST path
4166
+ * @param {RippleFormatOptions} options - Prettier options
4167
+ * @param {PrintFn} print - Print callback
4168
+ * @returns {Doc[]}
4169
+ */
3490
4170
  function printNewExpression(node, path, options, print) {
3491
4171
  const parts = [];
3492
4172
  parts.push('new ');
@@ -3514,6 +4194,14 @@ function printNewExpression(node, path, options, print) {
3514
4194
  return parts;
3515
4195
  }
3516
4196
 
4197
+ /**
4198
+ * Print a template literal
4199
+ * @param {RippleASTNode} node - The template literal node
4200
+ * @param {AstPath<RippleASTNode>} path - The AST path
4201
+ * @param {RippleFormatOptions} options - Prettier options
4202
+ * @param {PrintFn} print - Print callback
4203
+ * @returns {Doc[]}
4204
+ */
3517
4205
  function printTemplateLiteral(node, path, options, print) {
3518
4206
  const parts = [];
3519
4207
  parts.push('`');
@@ -3533,7 +4221,7 @@ function printTemplateLiteral(node, path, options, print) {
3533
4221
 
3534
4222
  if (needsBreaking) {
3535
4223
  // For expressions that break, use group with indent to format nicely
3536
- parts.push(group(concat(['${', indent(concat([softline, expressionDoc])), softline, '}'])));
4224
+ parts.push(group(['${', indent([softline, expressionDoc]), softline, '}']));
3537
4225
  } else {
3538
4226
  // For simple expressions, keep them inline
3539
4227
  parts.push('${');
@@ -3551,6 +4239,14 @@ function printTemplateLiteral(node, path, options, print) {
3551
4239
  return parts;
3552
4240
  }
3553
4241
 
4242
+ /**
4243
+ * Print a tagged template expression
4244
+ * @param {RippleASTNode} node - The tagged template node
4245
+ * @param {AstPath<RippleASTNode>} path - The AST path
4246
+ * @param {RippleFormatOptions} options - Prettier options
4247
+ * @param {PrintFn} print - Print callback
4248
+ * @returns {Doc[]}
4249
+ */
3554
4250
  function printTaggedTemplateExpression(node, path, options, print) {
3555
4251
  const parts = [];
3556
4252
  parts.push(path.call(print, 'tag'));
@@ -3558,6 +4254,14 @@ function printTaggedTemplateExpression(node, path, options, print) {
3558
4254
  return parts;
3559
4255
  }
3560
4256
 
4257
+ /**
4258
+ * Print a throw statement
4259
+ * @param {RippleASTNode} node - The throw statement node
4260
+ * @param {AstPath<RippleASTNode>} path - The AST path
4261
+ * @param {RippleFormatOptions} options - Prettier options
4262
+ * @param {PrintFn} print - Print callback
4263
+ * @returns {Doc[]}
4264
+ */
3561
4265
  function printThrowStatement(node, path, options, print) {
3562
4266
  const parts = [];
3563
4267
  parts.push('throw ');
@@ -3566,6 +4270,14 @@ function printThrowStatement(node, path, options, print) {
3566
4270
  return parts;
3567
4271
  }
3568
4272
 
4273
+ /**
4274
+ * Print a TypeScript interface declaration
4275
+ * @param {RippleASTNode} node - The interface declaration node
4276
+ * @param {AstPath<RippleASTNode>} path - The AST path
4277
+ * @param {RippleFormatOptions} options - Prettier options
4278
+ * @param {PrintFn} print - Print callback
4279
+ * @returns {Doc}
4280
+ */
3569
4281
  function printTSInterfaceDeclaration(node, path, options, print) {
3570
4282
  const parts = [];
3571
4283
  parts.push('interface ');
@@ -3585,9 +4297,17 @@ function printTSInterfaceDeclaration(node, path, options, print) {
3585
4297
  parts.push(' ');
3586
4298
  parts.push(path.call(print, 'body'));
3587
4299
 
3588
- return concat(parts);
4300
+ return parts;
3589
4301
  }
3590
4302
 
4303
+ /**
4304
+ * Print a TypeScript interface body
4305
+ * @param {RippleASTNode} node - The interface body node
4306
+ * @param {AstPath<RippleASTNode>} path - The AST path
4307
+ * @param {RippleFormatOptions} options - Prettier options
4308
+ * @param {PrintFn} print - Print callback
4309
+ * @returns {Doc}
4310
+ */
3591
4311
  function printTSInterfaceBody(node, path, options, print) {
3592
4312
  if (!node.body || node.body.length === 0) {
3593
4313
  return '{}';
@@ -3596,11 +4316,19 @@ function printTSInterfaceBody(node, path, options, print) {
3596
4316
  const members = path.map(print, 'body');
3597
4317
 
3598
4318
  // Add semicolons to all members
3599
- const membersWithSemicolons = members.map((member) => concat([member, semi(options)]));
4319
+ const membersWithSemicolons = members.map((member) => [member, semi(options)]);
3600
4320
 
3601
4321
  return group(['{', indent([hardline, join(hardline, membersWithSemicolons)]), hardline, '}']);
3602
4322
  }
3603
4323
 
4324
+ /**
4325
+ * Print a TypeScript type alias declaration
4326
+ * @param {RippleASTNode} node - The type alias node
4327
+ * @param {AstPath<RippleASTNode>} path - The AST path
4328
+ * @param {RippleFormatOptions} options - Prettier options
4329
+ * @param {PrintFn} print - Print callback
4330
+ * @returns {Doc[]}
4331
+ */
3604
4332
  function printTSTypeAliasDeclaration(node, path, options, print) {
3605
4333
  const parts = [];
3606
4334
  parts.push('type ');
@@ -3617,6 +4345,14 @@ function printTSTypeAliasDeclaration(node, path, options, print) {
3617
4345
  return parts;
3618
4346
  }
3619
4347
 
4348
+ /**
4349
+ * Print a TypeScript enum declaration
4350
+ * @param {RippleASTNode} node - The enum declaration node
4351
+ * @param {AstPath<RippleASTNode>} path - The AST path
4352
+ * @param {RippleFormatOptions} options - Prettier options
4353
+ * @param {PrintFn} print - Print callback
4354
+ * @returns {Doc[]}
4355
+ */
3620
4356
  function printTSEnumDeclaration(node, path, options, print) {
3621
4357
  const parts = [];
3622
4358
 
@@ -3647,7 +4383,7 @@ function printTSEnumDeclaration(node, path, options, print) {
3647
4383
  parts.push(
3648
4384
  group([
3649
4385
  '{',
3650
- indent([hardline, concat(membersWithCommas)]),
4386
+ indent([hardline, membersWithCommas]),
3651
4387
  options.trailingComma !== 'none' ? ',' : '',
3652
4388
  hardline,
3653
4389
  '}',
@@ -3655,9 +4391,17 @@ function printTSEnumDeclaration(node, path, options, print) {
3655
4391
  );
3656
4392
  }
3657
4393
 
3658
- return concat(parts);
4394
+ return parts;
3659
4395
  }
3660
4396
 
4397
+ /**
4398
+ * Print a TypeScript enum member
4399
+ * @param {RippleASTNode} node - The enum member node
4400
+ * @param {AstPath<RippleASTNode>} path - The AST path
4401
+ * @param {RippleFormatOptions} options - Prettier options
4402
+ * @param {PrintFn} print - Print callback
4403
+ * @returns {Doc}
4404
+ */
3661
4405
  function printTSEnumMember(node, path, options, print) {
3662
4406
  const parts = [];
3663
4407
 
@@ -3675,9 +4419,17 @@ function printTSEnumMember(node, path, options, print) {
3675
4419
  parts.push(path.call(print, 'initializer'));
3676
4420
  }
3677
4421
 
3678
- return concat(parts);
4422
+ return parts;
3679
4423
  }
3680
4424
 
4425
+ /**
4426
+ * Print TypeScript type parameter declaration (<T, U extends V>)
4427
+ * @param {RippleASTNode} node - The type parameter declaration node
4428
+ * @param {AstPath<RippleASTNode>} path - The AST path
4429
+ * @param {RippleFormatOptions} options - Prettier options
4430
+ * @param {PrintFn} print - Print callback
4431
+ * @returns {Doc}
4432
+ */
3681
4433
  function printTSTypeParameterDeclaration(node, path, options, print) {
3682
4434
  if (!node.params || node.params.length === 0) {
3683
4435
  return '';
@@ -3697,6 +4449,14 @@ function printTSTypeParameterDeclaration(node, path, options, print) {
3697
4449
  return parts;
3698
4450
  }
3699
4451
 
4452
+ /**
4453
+ * Print a single TypeScript type parameter
4454
+ * @param {RippleASTNode} node - The type parameter node
4455
+ * @param {AstPath<RippleASTNode>} path - The AST path
4456
+ * @param {RippleFormatOptions} options - Prettier options
4457
+ * @param {PrintFn} print - Print callback
4458
+ * @returns {Doc[]}
4459
+ */
3700
4460
  function printTSTypeParameter(node, path, options, print) {
3701
4461
  const parts = [];
3702
4462
  parts.push(node.name);
@@ -3714,6 +4474,14 @@ function printTSTypeParameter(node, path, options, print) {
3714
4474
  return parts;
3715
4475
  }
3716
4476
 
4477
+ /**
4478
+ * Print TypeScript type parameter instantiation (<string, number>)
4479
+ * @param {RippleASTNode} node - The type parameter instantiation node
4480
+ * @param {AstPath<RippleASTNode>} path - The AST path
4481
+ * @param {RippleFormatOptions} options - Prettier options
4482
+ * @param {PrintFn} print - Print callback
4483
+ * @returns {Doc}
4484
+ */
3717
4485
  function printTSTypeParameterInstantiation(node, path, options, print) {
3718
4486
  if (!node.params || node.params.length === 0) {
3719
4487
  return '';
@@ -3725,6 +4493,7 @@ function printTSTypeParameterInstantiation(node, path, options, print) {
3725
4493
  const hasBreakingParam = paramList.some((param) => willBreak(param));
3726
4494
 
3727
4495
  // Build inline version: <T, U>
4496
+ /** @type {Doc[]} */
3728
4497
  const inlineParts = ['<'];
3729
4498
  for (let i = 0; i < paramList.length; i++) {
3730
4499
  if (i > 0) inlineParts.push(', ');
@@ -3740,7 +4509,7 @@ function printTSTypeParameterInstantiation(node, path, options, print) {
3740
4509
  if (i > 0) breakingParts.push(',', hardline);
3741
4510
  breakingParts.push(paramList[i]);
3742
4511
  }
3743
- return group(concat(['<', indent(concat([hardline, ...breakingParts])), hardline, '>']));
4512
+ return group(['<', indent([hardline, ...breakingParts]), hardline, '>']);
3744
4513
  }
3745
4514
 
3746
4515
  // Otherwise use group to allow natural breaking
@@ -3750,31 +4519,63 @@ function printTSTypeParameterInstantiation(node, path, options, print) {
3750
4519
  parts.push(paramList[i]);
3751
4520
  }
3752
4521
 
3753
- return group(concat(['<', indent(concat([softline, ...parts])), softline, '>']));
4522
+ return group(['<', indent([softline, ...parts]), softline, '>']);
3754
4523
  }
3755
4524
 
4525
+ /**
4526
+ * Print a switch statement
4527
+ * @param {RippleASTNode} node - The switch statement node
4528
+ * @param {AstPath<RippleASTNode>} path - The AST path
4529
+ * @param {RippleFormatOptions} options - Prettier options
4530
+ * @param {PrintFn} print - Print callback
4531
+ * @returns {Doc}
4532
+ */
3756
4533
  function printSwitchStatement(node, path, options, print) {
3757
- const discriminantDoc = group(
3758
- concat(['switch (', indent([softline, path.call(print, 'discriminant')]), softline, ')']),
4534
+ // Extract leading comments from discriminant node to print them before 'switch' keyword
4535
+ const discriminantNode = node.discriminant;
4536
+
4537
+ // Print discriminant without its leading comments (they'll be printed before 'switch')
4538
+ const discriminant = path.call(
4539
+ (discriminantPath) => print(discriminantPath, { suppressLeadingComments: true }),
4540
+ 'discriminant',
3759
4541
  );
3760
4542
 
4543
+ const parts = [];
4544
+
4545
+ // Print leading comments from discriminant node before 'switch' keyword
4546
+ parts.push(...extractAndPrintLeadingComments(discriminantNode));
4547
+
4548
+ const discriminantDoc = group(['switch (', indent([softline, discriminant]), softline, ')']);
4549
+
4550
+ parts.push(discriminantDoc);
4551
+
3761
4552
  const cases = [];
3762
4553
  for (let i = 0; i < node.cases.length; i++) {
3763
4554
  const caseDoc = [path.call(print, 'cases', i)];
3764
4555
  if (i < node.cases.length - 1 && isNextLineEmpty(node.cases[i], options)) {
3765
4556
  caseDoc.push(hardline);
3766
4557
  }
3767
- cases.push(concat(caseDoc));
4558
+ cases.push(caseDoc);
3768
4559
  }
3769
4560
 
3770
4561
  const bodyDoc =
3771
- cases.length > 0 ? concat([indent([hardline, join(hardline, cases)]), hardline]) : hardline;
4562
+ cases.length > 0 ? [indent([hardline, join(hardline, cases)]), hardline] : hardline;
3772
4563
 
3773
- return concat([discriminantDoc, ' {', bodyDoc, '}']);
4564
+ parts.push(' {', bodyDoc, '}');
4565
+
4566
+ return parts;
3774
4567
  }
3775
4568
 
4569
+ /**
4570
+ * Print a switch case
4571
+ * @param {RippleASTNode} node - The switch case node
4572
+ * @param {AstPath<RippleASTNode>} path - The AST path
4573
+ * @param {RippleFormatOptions} options - Prettier options
4574
+ * @param {PrintFn} print - Print callback
4575
+ * @returns {Doc}
4576
+ */
3776
4577
  function printSwitchCase(node, path, options, print) {
3777
- const header = node.test ? concat(['case ', path.call(print, 'test'), ':']) : 'default:';
4578
+ const header = node.test ? ['case ', path.call(print, 'test'), ':'] : 'default:';
3778
4579
 
3779
4580
  const consequents = node.consequent || [];
3780
4581
  const printedConsequents = [];
@@ -3794,7 +4595,7 @@ function printSwitchCase(node, path, options, print) {
3794
4595
  const singleBlock =
3795
4596
  printedConsequents.length === 1 && referencedConsequents[0].type === 'BlockStatement';
3796
4597
  if (singleBlock) {
3797
- bodyDoc = concat([' ', printedConsequents[0]]);
4598
+ bodyDoc = [' ', printedConsequents[0]];
3798
4599
  } else {
3799
4600
  bodyDoc = indent([hardline, join(hardline, printedConsequents)]);
3800
4601
  }
@@ -3816,17 +4617,16 @@ function printSwitchCase(node, path, options, print) {
3816
4617
  commentDocs.push(hardline);
3817
4618
  }
3818
4619
  const commentDoc =
3819
- comment.type === 'Line'
3820
- ? concat(['//', comment.value])
3821
- : concat(['/*', comment.value, '*/']);
4620
+ comment.type === 'Line' ? ['//', comment.value] : ['/*', comment.value, '*/'];
3822
4621
  commentDocs.push(commentDoc);
3823
4622
  previousNode = comment;
3824
4623
  }
3825
4624
 
3826
- trailingDoc = concat(commentDocs);
4625
+ trailingDoc = commentDocs;
3827
4626
  delete node.trailingComments;
3828
4627
  }
3829
4628
 
4629
+ /** @type {Doc[]} */
3830
4630
  const parts = [header];
3831
4631
  if (bodyDoc) {
3832
4632
  parts.push(bodyDoc);
@@ -3835,9 +4635,17 @@ function printSwitchCase(node, path, options, print) {
3835
4635
  parts.push(trailingDoc);
3836
4636
  }
3837
4637
 
3838
- return concat(parts);
4638
+ return parts;
3839
4639
  }
3840
4640
 
4641
+ /**
4642
+ * Print a break statement
4643
+ * @param {RippleASTNode} node - The break statement node
4644
+ * @param {AstPath<RippleASTNode>} path - The AST path
4645
+ * @param {RippleFormatOptions} options - Prettier options
4646
+ * @param {PrintFn} print - Print callback
4647
+ * @returns {Doc[]}
4648
+ */
3841
4649
  function printBreakStatement(node, path, options, print) {
3842
4650
  const parts = [];
3843
4651
  parts.push('break');
@@ -3849,6 +4657,14 @@ function printBreakStatement(node, path, options, print) {
3849
4657
  return parts;
3850
4658
  }
3851
4659
 
4660
+ /**
4661
+ * Print a continue statement
4662
+ * @param {RippleASTNode} node - The continue statement node
4663
+ * @param {AstPath<RippleASTNode>} path - The AST path
4664
+ * @param {RippleFormatOptions} options - Prettier options
4665
+ * @param {PrintFn} print - Print callback
4666
+ * @returns {Doc[]}
4667
+ */
3852
4668
  function printContinueStatement(node, path, options, print) {
3853
4669
  const parts = [];
3854
4670
  parts.push('continue');
@@ -3860,10 +4676,25 @@ function printContinueStatement(node, path, options, print) {
3860
4676
  return parts;
3861
4677
  }
3862
4678
 
3863
- function printDebuggerStatement(node, path, options, print) {
4679
+ /**
4680
+ * Print a debugger statement
4681
+ * @param {RippleASTNode} node - The debugger statement node
4682
+ * @param {AstPath<RippleASTNode>} path - The AST path
4683
+ * @param {RippleFormatOptions} options - Prettier options
4684
+ * @returns {string}
4685
+ */
4686
+ function printDebuggerStatement(node, path, options) {
3864
4687
  return 'debugger' + semi(options);
3865
4688
  }
3866
4689
 
4690
+ /**
4691
+ * Print a sequence expression
4692
+ * @param {RippleASTNode} node - The sequence expression node
4693
+ * @param {AstPath<RippleASTNode>} path - The AST path
4694
+ * @param {RippleFormatOptions} options - Prettier options
4695
+ * @param {PrintFn} print - Print callback
4696
+ * @returns {Doc[]}
4697
+ */
3867
4698
  function printSequenceExpression(node, path, options, print) {
3868
4699
  const parts = [];
3869
4700
  parts.push('(');
@@ -3876,6 +4707,12 @@ function printSequenceExpression(node, path, options, print) {
3876
4707
  return parts;
3877
4708
  }
3878
4709
 
4710
+ /**
4711
+ * Get number of blank lines between two positions
4712
+ * @param {{ line: number }} current_pos - Current position
4713
+ * @param {{ line: number }} next_pos - Next position
4714
+ * @returns {number}
4715
+ */
3879
4716
  function getBlankLinesBetweenPositions(current_pos, next_pos) {
3880
4717
  const line_gap = next_pos.line - current_pos.line;
3881
4718
 
@@ -3885,6 +4722,12 @@ function getBlankLinesBetweenPositions(current_pos, next_pos) {
3885
4722
  return Math.max(0, line_gap - 1);
3886
4723
  }
3887
4724
 
4725
+ /**
4726
+ * Get number of blank lines between two nodes
4727
+ * @param {RippleASTNode} currentNode - Current node
4728
+ * @param {RippleASTNode} nextNode - Next node
4729
+ * @returns {number}
4730
+ */
3888
4731
  function getBlankLinesBetweenNodes(currentNode, nextNode) {
3889
4732
  // Return the number of blank lines between two nodes based on their location
3890
4733
  if (
@@ -3900,6 +4743,12 @@ function getBlankLinesBetweenNodes(currentNode, nextNode) {
3900
4743
  return 0;
3901
4744
  }
3902
4745
 
4746
+ /**
4747
+ * Determine if a blank line should be added between nodes
4748
+ * @param {RippleASTNode} currentNode - Current node
4749
+ * @param {RippleASTNode} nextNode - Next node
4750
+ * @returns {boolean}
4751
+ */
3903
4752
  function shouldAddBlankLine(currentNode, nextNode) {
3904
4753
  // Simplified blank line logic:
3905
4754
  // 1. Check if there was originally 1+ blank lines between nodes
@@ -3934,11 +4783,19 @@ function shouldAddBlankLine(currentNode, nextNode) {
3934
4783
  return originalBlankLines > 0;
3935
4784
  }
3936
4785
 
4786
+ /**
4787
+ * Print an object pattern (destructuring)
4788
+ * @param {RippleASTNode} node - The object pattern node
4789
+ * @param {AstPath<RippleASTNode>} path - The AST path
4790
+ * @param {RippleFormatOptions} options - Prettier options
4791
+ * @param {PrintFn} print - Print callback
4792
+ * @returns {Doc}
4793
+ */
3937
4794
  function printObjectPattern(node, path, options, print) {
3938
4795
  const propList = path.map(print, 'properties');
3939
4796
  if (propList.length === 0) {
3940
4797
  if (node.typeAnnotation) {
3941
- return concat(['{}', ': ', path.call(print, 'typeAnnotation')]);
4798
+ return ['{}', ': ', path.call(print, 'typeAnnotation')];
3942
4799
  }
3943
4800
  return '{}';
3944
4801
  }
@@ -3966,49 +4823,53 @@ function printObjectPattern(node, path, options, print) {
3966
4823
 
3967
4824
  // Use softline for proper spacing - will become space when inline, line when breaking
3968
4825
  // Format type members with semicolons between AND after the last member
3969
- const typeMemberDocs = join(concat([';', line]), typeMembers);
4826
+ const typeMemberDocs = join([';', line], typeMembers);
3970
4827
 
3971
4828
  // Don't wrap in group - let the outer params group control breaking
3972
- const objectDoc = concat([
4829
+ const objectDoc = [
3973
4830
  '{',
3974
- indent(concat([line, join(concat([',', line]), propList), trailingCommaDoc])),
4831
+ indent([line, join([',', line], propList), trailingCommaDoc]),
3975
4832
  line,
3976
4833
  '}',
3977
- ]);
4834
+ ];
3978
4835
  const typeDoc =
3979
4836
  typeMembers.length === 0
3980
4837
  ? '{}'
3981
- : concat(['{', indent(concat([line, typeMemberDocs, ifBreak(';', '')])), line, '}']);
4838
+ : ['{', indent([line, typeMemberDocs, ifBreak(';', '')]), line, '}'];
3982
4839
 
3983
4840
  // Return combined
3984
- return concat([objectDoc, ': ', typeDoc]);
4841
+ return [objectDoc, ': ', typeDoc];
3985
4842
  }
3986
4843
 
3987
4844
  // For other type annotations, just concatenate
3988
- const objectContent = group(
3989
- concat([
3990
- '{',
3991
- indent(concat([line, join(concat([',', line]), propList), trailingCommaDoc])),
3992
- line,
3993
- '}',
3994
- ]),
3995
- );
3996
- return concat([objectContent, ': ', path.call(print, 'typeAnnotation')]);
3997
- }
3998
-
3999
- // No type annotation - just format the object pattern
4000
- const objectContent = group(
4001
- concat([
4845
+ const objectContent = group([
4002
4846
  '{',
4003
- indent(concat([line, join(concat([',', line]), propList), trailingCommaDoc])),
4847
+ indent([line, join([',', line], propList), trailingCommaDoc]),
4004
4848
  line,
4005
4849
  '}',
4006
- ]),
4007
- );
4850
+ ]);
4851
+ return [objectContent, ': ', path.call(print, 'typeAnnotation')];
4852
+ }
4853
+
4854
+ // No type annotation - just format the object pattern
4855
+ const objectContent = group([
4856
+ '{',
4857
+ indent([line, join([',', line], propList), trailingCommaDoc]),
4858
+ line,
4859
+ '}',
4860
+ ]);
4008
4861
 
4009
4862
  return objectContent;
4010
4863
  }
4011
4864
 
4865
+ /**
4866
+ * Print an array pattern (destructuring)
4867
+ * @param {RippleASTNode} node - The array pattern node
4868
+ * @param {AstPath<RippleASTNode>} path - The AST path
4869
+ * @param {RippleFormatOptions} options - Prettier options
4870
+ * @param {PrintFn} print - Print callback
4871
+ * @returns {Doc}
4872
+ */
4012
4873
  function printArrayPattern(node, path, options, print) {
4013
4874
  const parts = [];
4014
4875
  parts.push('[');
@@ -4024,9 +4885,17 @@ function printArrayPattern(node, path, options, print) {
4024
4885
  parts.push(path.call(print, 'typeAnnotation'));
4025
4886
  }
4026
4887
 
4027
- return concat(parts);
4888
+ return parts;
4028
4889
  }
4029
4890
 
4891
+ /**
4892
+ * Print a property (object property or method)
4893
+ * @param {RippleASTNode} node - The property node
4894
+ * @param {AstPath<RippleASTNode>} path - The AST path
4895
+ * @param {RippleFormatOptions} options - Prettier options
4896
+ * @param {PrintFn} print - Print callback
4897
+ * @returns {Doc}
4898
+ */
4030
4899
  function printProperty(node, path, options, print) {
4031
4900
  if (node.shorthand) {
4032
4901
  // For shorthand properties, if value is AssignmentPattern, print the value (which includes the default)
@@ -4062,7 +4931,7 @@ function printProperty(node, path, options, print) {
4062
4931
  }
4063
4932
 
4064
4933
  methodParts.push(' ', path.call(print, 'value', 'body'));
4065
- return concat(methodParts);
4934
+ return methodParts;
4066
4935
  }
4067
4936
 
4068
4937
  // Handle method shorthand: increment() {} instead of increment: function() {}
@@ -4085,11 +4954,16 @@ function printProperty(node, path, options, print) {
4085
4954
 
4086
4955
  methodParts.push(...printKey(node, path, options, print));
4087
4956
 
4957
+ // Handle type parameters (generics)
4958
+ if (funcValue.typeParameters) {
4959
+ methodParts.push(path.call(print, 'value', 'typeParameters'));
4960
+ }
4961
+
4088
4962
  if (is_component) {
4089
4963
  methodParts.push(
4090
4964
  path.call((childPath) => print(childPath, { skipComponentLabel: true }), 'value'),
4091
4965
  );
4092
- return concat(methodParts);
4966
+ return methodParts;
4093
4967
  }
4094
4968
 
4095
4969
  // Print parameters by calling into the value path
@@ -4105,7 +4979,7 @@ function printProperty(node, path, options, print) {
4105
4979
  }
4106
4980
 
4107
4981
  methodParts.push(' ', path.call(print, 'value', 'body'));
4108
- return concat(methodParts);
4982
+ return methodParts;
4109
4983
  }
4110
4984
 
4111
4985
  const parts = [];
@@ -4113,9 +4987,17 @@ function printProperty(node, path, options, print) {
4113
4987
 
4114
4988
  parts.push(': ');
4115
4989
  parts.push(path.call(print, 'value'));
4116
- return concat(parts);
4990
+ return parts;
4117
4991
  }
4118
4992
 
4993
+ /**
4994
+ * Print a variable declarator
4995
+ * @param {RippleASTNode} node - The variable declarator node
4996
+ * @param {AstPath<RippleASTNode>} path - The AST path
4997
+ * @param {RippleFormatOptions} options - Prettier options
4998
+ * @param {PrintFn} print - Print callback
4999
+ * @returns {Doc}
5000
+ */
4119
5001
  function printVariableDeclarator(node, path, options, print) {
4120
5002
  if (node.init) {
4121
5003
  const id = path.call(print, 'id');
@@ -4143,7 +5025,7 @@ function printVariableDeclarator(node, path, options, print) {
4143
5025
  node.init.alternate.type === 'ConditionalExpression';
4144
5026
 
4145
5027
  if (ternaryWillBreak || hasComplexBranch || hasComplexTest || hasNestedTernary) {
4146
- return concat([id, ' =', indent(concat([line, init]))]);
5028
+ return [id, ' =', indent([line, init])];
4147
5029
  }
4148
5030
  }
4149
5031
 
@@ -4187,9 +5069,9 @@ function printVariableDeclarator(node, path, options, print) {
4187
5069
  // Prettier picks the broken version if inline doesn't fit
4188
5070
  return conditionalGroup([
4189
5071
  // Try inline first
4190
- concat([id, ' = ', init]),
5072
+ [id, ' = ', init],
4191
5073
  // Fall back to broken with extra indent
4192
- concat([id, ' =', indent(concat([line, init]))]),
5074
+ [id, ' =', indent([line, init])],
4193
5075
  ]);
4194
5076
  }
4195
5077
  }
@@ -4201,7 +5083,7 @@ function printVariableDeclarator(node, path, options, print) {
4201
5083
  if (isBinaryish) {
4202
5084
  // Use Prettier's break-after-operator strategy: break after = and let the expression break naturally
4203
5085
  const init = path.call(print, 'init');
4204
- return group([group(id), ' =', group(indent(concat([line, init])))]);
5086
+ return group([group(id), ' =', group(indent([line, init]))]);
4205
5087
  }
4206
5088
  // For CallExpression inits, use fluid layout strategy to break after = if needed
4207
5089
  const isCallExpression = node.init.type === 'CallExpression';
@@ -4222,17 +5104,33 @@ function printVariableDeclarator(node, path, options, print) {
4222
5104
 
4223
5105
  // Default: simple inline format with space
4224
5106
  // Use group to allow breaking if needed - but keep inline when it fits
4225
- return group(concat([id, ' = ', init]));
5107
+ return group([id, ' = ', init]);
4226
5108
  }
4227
5109
 
4228
5110
  return path.call(print, 'id');
4229
5111
  }
4230
5112
 
5113
+ /**
5114
+ * Print an assignment pattern (default parameter)
5115
+ * @param {RippleASTNode} node - The assignment pattern node
5116
+ * @param {AstPath<RippleASTNode>} path - The AST path
5117
+ * @param {RippleFormatOptions} options - Prettier options
5118
+ * @param {PrintFn} print - Print callback
5119
+ * @returns {Doc}
5120
+ */
4231
5121
  function printAssignmentPattern(node, path, options, print) {
4232
5122
  // Handle default parameters like: count: number = 0
4233
- return concat([path.call(print, 'left'), ' = ', path.call(print, 'right')]);
5123
+ return [path.call(print, 'left'), ' = ', path.call(print, 'right')];
4234
5124
  }
4235
5125
 
5126
+ /**
5127
+ * Print a TypeScript type literal
5128
+ * @param {RippleASTNode} node - The type literal node
5129
+ * @param {AstPath<RippleASTNode>} path - The AST path
5130
+ * @param {RippleFormatOptions} options - Prettier options
5131
+ * @param {PrintFn} print - Print callback
5132
+ * @returns {Doc}
5133
+ */
4236
5134
  function printTSTypeLiteral(node, path, options, print) {
4237
5135
  if (!node.members || node.members.length === 0) {
4238
5136
  return '{}';
@@ -4240,23 +5138,32 @@ function printTSTypeLiteral(node, path, options, print) {
4240
5138
 
4241
5139
  const members = path.map(print, 'members');
4242
5140
  const inlineMembers = members.map((member, index) =>
4243
- index < members.length - 1 ? concat([member, ';']) : member,
5141
+ index < members.length - 1 ? [member, ';'] : member,
4244
5142
  );
4245
- const multilineMembers = members.map((member) => concat([member, ';']));
5143
+ const multilineMembers = members.map((member) => [member, ';']);
4246
5144
 
4247
- const inlineDoc = group(
4248
- concat(['{', indent(concat([line, join(line, inlineMembers)])), line, '}']),
4249
- );
5145
+ const inlineDoc = group(['{', indent([line, join(line, inlineMembers)]), line, '}']);
4250
5146
 
4251
- const multilineDoc = group(
4252
- concat(['{', indent(concat([hardline, join(hardline, multilineMembers)])), hardline, '}']),
4253
- );
5147
+ const multilineDoc = group([
5148
+ '{',
5149
+ indent([hardline, join(hardline, multilineMembers)]),
5150
+ hardline,
5151
+ '}',
5152
+ ]);
4254
5153
 
4255
5154
  return conditionalGroup(
4256
5155
  wasOriginallySingleLine(node) ? [inlineDoc, multilineDoc] : [multilineDoc, inlineDoc],
4257
5156
  );
4258
5157
  }
4259
5158
 
5159
+ /**
5160
+ * Print a TypeScript property signature in an interface
5161
+ * @param {RippleASTNode} node - The property signature node
5162
+ * @param {AstPath<RippleASTNode>} path - The AST path
5163
+ * @param {RippleFormatOptions} options - Prettier options
5164
+ * @param {PrintFn} print - Print callback
5165
+ * @returns {Doc}
5166
+ */
4260
5167
  function printTSPropertySignature(node, path, options, print) {
4261
5168
  const parts = [];
4262
5169
  parts.push(path.call(print, 'key'));
@@ -4270,9 +5177,17 @@ function printTSPropertySignature(node, path, options, print) {
4270
5177
  parts.push(path.call(print, 'typeAnnotation'));
4271
5178
  }
4272
5179
 
4273
- return concat(parts);
5180
+ return parts;
4274
5181
  }
4275
5182
 
5183
+ /**
5184
+ * Print a TypeScript method signature in an interface
5185
+ * @param {RippleASTNode} node - The method signature node
5186
+ * @param {AstPath<RippleASTNode>} path - The AST path
5187
+ * @param {RippleFormatOptions} options - Prettier options
5188
+ * @param {PrintFn} print - Print callback
5189
+ * @returns {Doc}
5190
+ */
4276
5191
  function printTSMethodSignature(node, path, options, print) {
4277
5192
  const parts = [];
4278
5193
 
@@ -4311,9 +5226,17 @@ function printTSMethodSignature(node, path, options, print) {
4311
5226
  parts.push(path.call(print, 'typeAnnotation'));
4312
5227
  }
4313
5228
 
4314
- return concat(parts);
5229
+ return parts;
4315
5230
  }
4316
5231
 
5232
+ /**
5233
+ * Print a TypeScript type reference (e.g., Array<string>)
5234
+ * @param {RippleASTNode} node - The type reference node
5235
+ * @param {AstPath<RippleASTNode>} path - The AST path
5236
+ * @param {RippleFormatOptions} options - Prettier options
5237
+ * @param {PrintFn} print - Print callback
5238
+ * @returns {Doc}
5239
+ */
4317
5240
  function printTSTypeReference(node, path, options, print) {
4318
5241
  const parts = [path.call(print, 'typeName')];
4319
5242
 
@@ -4336,10 +5259,19 @@ function printTSTypeReference(node, path, options, print) {
4336
5259
  parts.push('>');
4337
5260
  }
4338
5261
 
4339
- return concat(parts);
5262
+ return parts;
4340
5263
  }
4341
5264
 
5265
+ /**
5266
+ * Print a TypeScript tuple type
5267
+ * @param {RippleASTNode} node - The tuple type node
5268
+ * @param {AstPath<RippleASTNode>} path - The AST path
5269
+ * @param {RippleFormatOptions} options - Prettier options
5270
+ * @param {PrintFn} print - Print callback
5271
+ * @returns {Doc}
5272
+ */
4342
5273
  function printTSTupleType(node, path, options, print) {
5274
+ /** @type {Doc[]} */
4343
5275
  const parts = ['['];
4344
5276
  const elements = node.elementTypes ? path.map(print, 'elementTypes') : [];
4345
5277
  for (let i = 0; i < elements.length; i++) {
@@ -4347,9 +5279,17 @@ function printTSTupleType(node, path, options, print) {
4347
5279
  parts.push(elements[i]);
4348
5280
  }
4349
5281
  parts.push(']');
4350
- return concat(parts);
5282
+ return parts;
4351
5283
  }
4352
5284
 
5285
+ /**
5286
+ * Print a TypeScript index signature
5287
+ * @param {RippleASTNode} node - The index signature node
5288
+ * @param {AstPath<RippleASTNode>} path - The AST path
5289
+ * @param {RippleFormatOptions} options - Prettier options
5290
+ * @param {PrintFn} print - Print callback
5291
+ * @returns {Doc}
5292
+ */
4353
5293
  function printTSIndexSignature(node, path, options, print) {
4354
5294
  const parts = [];
4355
5295
  if (node.readonly === true || node.readonly === 'plus' || node.readonly === '+') {
@@ -4371,9 +5311,17 @@ function printTSIndexSignature(node, path, options, print) {
4371
5311
  parts.push(path.call(print, 'typeAnnotation'));
4372
5312
  }
4373
5313
 
4374
- return concat(parts);
5314
+ return parts;
4375
5315
  }
4376
5316
 
5317
+ /**
5318
+ * Print a TypeScript constructor type
5319
+ * @param {RippleASTNode} node - The constructor type node
5320
+ * @param {AstPath<RippleASTNode>} path - The AST path
5321
+ * @param {RippleFormatOptions} options - Prettier options
5322
+ * @param {PrintFn} print - Print callback
5323
+ * @returns {Doc}
5324
+ */
4377
5325
  function printTSConstructorType(node, path, options, print) {
4378
5326
  const parts = [];
4379
5327
  parts.push('new ');
@@ -4394,9 +5342,17 @@ function printTSConstructorType(node, path, options, print) {
4394
5342
  } else if (node.typeAnnotation) {
4395
5343
  parts.push(path.call(print, 'typeAnnotation'));
4396
5344
  }
4397
- return concat(parts);
5345
+ return parts;
4398
5346
  }
4399
5347
 
5348
+ /**
5349
+ * Print a TypeScript conditional type
5350
+ * @param {RippleASTNode} node - The conditional type node
5351
+ * @param {AstPath<RippleASTNode>} path - The AST path
5352
+ * @param {RippleFormatOptions} options - Prettier options
5353
+ * @param {PrintFn} print - Print callback
5354
+ * @returns {Doc}
5355
+ */
4400
5356
  function printTSConditionalType(node, path, options, print) {
4401
5357
  const parts = [];
4402
5358
  parts.push(path.call(print, 'checkType'));
@@ -4406,9 +5362,17 @@ function printTSConditionalType(node, path, options, print) {
4406
5362
  parts.push(path.call(print, 'trueType'));
4407
5363
  parts.push(' : ');
4408
5364
  parts.push(path.call(print, 'falseType'));
4409
- return concat(parts);
5365
+ return parts;
4410
5366
  }
4411
5367
 
5368
+ /**
5369
+ * Print a TypeScript mapped type
5370
+ * @param {RippleASTNode} node - The mapped type node
5371
+ * @param {AstPath<RippleASTNode>} path - The AST path
5372
+ * @param {RippleFormatOptions} options - Prettier options
5373
+ * @param {PrintFn} print - Print callback
5374
+ * @returns {Doc}
5375
+ */
4412
5376
  function printTSMappedType(node, path, options, print) {
4413
5377
  const readonlyMod =
4414
5378
  node.readonly === true || node.readonly === 'plus' || node.readonly === '+'
@@ -4448,17 +5412,37 @@ function printTSMappedType(node, path, options, print) {
4448
5412
  innerParts.push(path.call(print, 'typeAnnotation'));
4449
5413
  }
4450
5414
 
4451
- return group(['{ ', readonlyMod, concat(innerParts), ' }']);
5415
+ return group(['{ ', readonlyMod, innerParts, ' }']);
4452
5416
  }
4453
5417
 
5418
+ /**
5419
+ * @param {RippleASTNode} node
5420
+ * @param {AstPath<RippleASTNode>} path
5421
+ * @param {RippleFormatOptions} options
5422
+ * @param {PrintFn} print
5423
+ * @returns {Doc}
5424
+ */
4454
5425
  function printTSQualifiedName(node, path, options, print) {
4455
- return concat([path.call(print, 'left'), '.', path.call(print, 'right')]);
5426
+ return [path.call(print, 'left'), '.', path.call(print, 'right')];
4456
5427
  }
4457
5428
 
5429
+ /**
5430
+ * @param {RippleASTNode} node
5431
+ * @param {AstPath<RippleASTNode>} path
5432
+ * @param {RippleFormatOptions} options
5433
+ * @param {PrintFn} print
5434
+ * @returns {Doc}
5435
+ */
4458
5436
  function printTSIndexedAccessType(node, path, options, print) {
4459
- return concat([path.call(print, 'objectType'), '[', path.call(print, 'indexType'), ']']);
5437
+ return [path.call(print, 'objectType'), '[', path.call(print, 'indexType'), ']'];
4460
5438
  }
4461
5439
 
5440
+ /**
5441
+ * @param {RippleASTNode} parentNode
5442
+ * @param {RippleASTNode} firstChild
5443
+ * @param {Doc} childDoc
5444
+ * @returns {boolean}
5445
+ */
4462
5446
  function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
4463
5447
  if (!firstChild || childDoc == null) {
4464
5448
  return false;
@@ -4493,6 +5477,11 @@ function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
4493
5477
  return false;
4494
5478
  }
4495
5479
 
5480
+ /**
5481
+ * Get leading comments from element metadata
5482
+ * @param {RippleASTNode} node - The element node
5483
+ * @returns {Comment[]}
5484
+ */
4496
5485
  function getElementLeadingComments(node) {
4497
5486
  const fromMetadata = node?.metadata?.elementLeadingComments;
4498
5487
  if (Array.isArray(fromMetadata)) {
@@ -4501,6 +5490,11 @@ function getElementLeadingComments(node) {
4501
5490
  return [];
4502
5491
  }
4503
5492
 
5493
+ /**
5494
+ * Create doc parts for element-level comments
5495
+ * @param {Comment[]} comments - Array of comments
5496
+ * @returns {Doc[]}
5497
+ */
4504
5498
  function createElementLevelCommentParts(comments) {
4505
5499
  if (!comments || comments.length === 0) {
4506
5500
  return [];
@@ -4531,6 +5525,11 @@ function createElementLevelCommentParts(comments) {
4531
5525
  return parts;
4532
5526
  }
4533
5527
 
5528
+ /**
5529
+ * Create element-level comment parts with trailing hardline trimmed
5530
+ * @param {Comment[]} comments - Array of comments
5531
+ * @returns {Doc[]}
5532
+ */
4534
5533
  function createElementLevelCommentPartsTrimmed(comments) {
4535
5534
  const parts = createElementLevelCommentParts(comments);
4536
5535
  if (parts.length > 0 && parts[parts.length - 1] === hardline) {
@@ -4539,6 +5538,14 @@ function createElementLevelCommentPartsTrimmed(comments) {
4539
5538
  return parts;
4540
5539
  }
4541
5540
 
5541
+ /**
5542
+ * Print a TSX compatibility node
5543
+ * @param {RippleASTNode} node - The TSX compat node
5544
+ * @param {AstPath<RippleASTNode>} path - The AST path
5545
+ * @param {RippleFormatOptions} options - Prettier options
5546
+ * @param {PrintFn} print - Print callback
5547
+ * @returns {Doc}
5548
+ */
4542
5549
  function printTsxCompat(node, path, options, print) {
4543
5550
  const tagName = `<tsx:${node.kind}>`;
4544
5551
  const closingTagName = `</tsx:${node.kind}>`;
@@ -4546,7 +5553,7 @@ function printTsxCompat(node, path, options, print) {
4546
5553
  const hasChildren = Array.isArray(node.children) && node.children.length > 0;
4547
5554
 
4548
5555
  if (!hasChildren) {
4549
- return concat([tagName, closingTagName]);
5556
+ return [tagName, closingTagName];
4550
5557
  }
4551
5558
 
4552
5559
  // Print JSXElement children - they remain as JSX
@@ -4606,7 +5613,7 @@ function printTsxCompat(node, path, options, print) {
4606
5613
  // Format the TsxCompat element
4607
5614
  const elementOutput = group([
4608
5615
  tagName,
4609
- indent(concat([hardline, ...finalChildren])),
5616
+ indent([hardline, ...finalChildren]),
4610
5617
  hardline,
4611
5618
  closingTagName,
4612
5619
  ]);
@@ -4614,6 +5621,14 @@ function printTsxCompat(node, path, options, print) {
4614
5621
  return elementOutput;
4615
5622
  }
4616
5623
 
5624
+ /**
5625
+ * Print a JSX element
5626
+ * @param {RippleASTNode} node - The JSX element node
5627
+ * @param {AstPath<RippleASTNode>} path - The AST path
5628
+ * @param {RippleFormatOptions} options - Prettier options
5629
+ * @param {PrintFn} print - Print callback
5630
+ * @returns {Doc}
5631
+ */
4617
5632
  function printJSXElement(node, path, options, print) {
4618
5633
  // Get the tag name from the opening element
4619
5634
  const openingElement = node.openingElement;
@@ -4634,34 +5649,34 @@ function printJSXElement(node, path, options, print) {
4634
5649
  const hasChildren = node.children && node.children.length > 0;
4635
5650
 
4636
5651
  // Format attributes
5652
+ /** @type {Doc} */
4637
5653
  let attributesDoc = '';
4638
5654
  if (hasAttributes) {
4639
- const attrs = openingElement.attributes.map((attr, i) => {
4640
- if (attr.type === 'JSXAttribute') {
4641
- return path.call(
4642
- (attrPath) => printJSXAttribute(attrPath.getValue(), attrPath, options, print),
4643
- 'openingElement',
4644
- 'attributes',
4645
- i,
4646
- );
4647
- } else if (attr.type === 'JSXSpreadAttribute') {
4648
- return concat([
4649
- '{...',
4650
- path.call(print, 'openingElement', 'attributes', i, 'argument'),
4651
- '}',
4652
- ]);
4653
- }
4654
- return '';
4655
- });
4656
- attributesDoc = concat([' ', join(' ', attrs)]);
5655
+ /** @type {Doc[]} */
5656
+ const attrs = openingElement.attributes.map(
5657
+ (/** @type {RippleASTNode} */ attr, /** @type {number} */ i) => {
5658
+ if (attr.type === 'JSXAttribute') {
5659
+ return path.call(
5660
+ (attrPath) => printJSXAttribute(attrPath.getValue(), attrPath, options, print),
5661
+ 'openingElement',
5662
+ 'attributes',
5663
+ i,
5664
+ );
5665
+ } else if (attr.type === 'JSXSpreadAttribute') {
5666
+ return ['{...', path.call(print, 'openingElement', 'attributes', i, 'argument'), '}'];
5667
+ }
5668
+ return '';
5669
+ },
5670
+ );
5671
+ attributesDoc = [' ', join(' ', attrs)];
4657
5672
  }
4658
5673
 
4659
5674
  if (isSelfClosing) {
4660
- return concat(['<', tagName, attributesDoc, ' />']);
5675
+ return ['<', tagName, attributesDoc, ' />'];
4661
5676
  }
4662
5677
 
4663
5678
  if (!hasChildren) {
4664
- return concat(['<', tagName, attributesDoc, '></', tagName, '>']);
5679
+ return ['<', tagName, attributesDoc, '></', tagName, '>'];
4665
5680
  }
4666
5681
 
4667
5682
  // Format children - filter out empty text nodes and merge adjacent text nodes
@@ -4690,7 +5705,7 @@ function printJSXElement(node, path, options, print) {
4690
5705
 
4691
5706
  if (child.type === 'JSXExpressionContainer') {
4692
5707
  // Handle JSX expression containers
4693
- childrenDocs.push(concat(['{', path.call(print, 'children', i, 'expression'), '}']));
5708
+ childrenDocs.push(['{', path.call(print, 'children', i, 'expression'), '}']);
4694
5709
  } else {
4695
5710
  // Handle nested JSX elements
4696
5711
  childrenDocs.push(path.call(print, 'children', i));
@@ -4705,7 +5720,7 @@ function printJSXElement(node, path, options, print) {
4705
5720
 
4706
5721
  // Check if content can be inlined (single text node or single expression)
4707
5722
  if (childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
4708
- return concat(['<', tagName, attributesDoc, '>', childrenDocs[0], '</', tagName, '>']);
5723
+ return ['<', tagName, attributesDoc, '>', childrenDocs[0], '</', tagName, '>'];
4709
5724
  }
4710
5725
 
4711
5726
  // Multiple children or complex children - format with line breaks
@@ -4723,7 +5738,7 @@ function printJSXElement(node, path, options, print) {
4723
5738
  tagName,
4724
5739
  attributesDoc,
4725
5740
  '>',
4726
- indent(concat([hardline, ...formattedChildren])),
5741
+ indent([hardline, ...formattedChildren]),
4727
5742
  hardline,
4728
5743
  '</',
4729
5744
  tagName,
@@ -4731,6 +5746,14 @@ function printJSXElement(node, path, options, print) {
4731
5746
  ]);
4732
5747
  }
4733
5748
 
5749
+ /**
5750
+ * Print a JSX fragment (<>...</>)
5751
+ * @param {RippleASTNode} node - The JSX fragment node
5752
+ * @param {AstPath<RippleASTNode>} path - The AST path
5753
+ * @param {RippleFormatOptions} options - Prettier options
5754
+ * @param {PrintFn} print - Print callback
5755
+ * @returns {Doc}
5756
+ */
4734
5757
  function printJSXFragment(node, path, options, print) {
4735
5758
  const hasChildren = node.children && node.children.length > 0;
4736
5759
 
@@ -4751,7 +5774,7 @@ function printJSXFragment(node, path, options, print) {
4751
5774
  }
4752
5775
  } else if (child.type === 'JSXExpressionContainer') {
4753
5776
  // Handle JSX expression containers
4754
- childrenDocs.push(concat(['{', path.call(print, 'children', i, 'expression'), '}']));
5777
+ childrenDocs.push(['{', path.call(print, 'children', i, 'expression'), '}']);
4755
5778
  } else {
4756
5779
  // Handle nested JSX elements and fragments
4757
5780
  childrenDocs.push(path.call(print, 'children', i));
@@ -4760,7 +5783,7 @@ function printJSXFragment(node, path, options, print) {
4760
5783
 
4761
5784
  // Check if content can be inlined (single text node or single expression)
4762
5785
  if (childrenDocs.length === 1 && typeof childrenDocs[0] === 'string') {
4763
- return concat(['<>', childrenDocs[0], '</>']);
5786
+ return ['<>', childrenDocs[0], '</>'];
4764
5787
  }
4765
5788
 
4766
5789
  // Multiple children or complex children - format with line breaks
@@ -4773,9 +5796,17 @@ function printJSXFragment(node, path, options, print) {
4773
5796
  }
4774
5797
 
4775
5798
  // Build the final fragment
4776
- return group(['<>', indent(concat([hardline, ...formattedChildren])), hardline, '</>']);
5799
+ return group(['<>', indent([hardline, ...formattedChildren]), hardline, '</>']);
4777
5800
  }
4778
5801
 
5802
+ /**
5803
+ * Print a JSX attribute
5804
+ * @param {RippleASTNode} attr - The JSX attribute node
5805
+ * @param {AstPath<RippleASTNode>} path - The AST path
5806
+ * @param {RippleFormatOptions} options - Prettier options
5807
+ * @param {PrintFn} print - Print callback
5808
+ * @returns {Doc}
5809
+ */
4779
5810
  function printJSXAttribute(attr, path, options, print) {
4780
5811
  const name = attr.name.name;
4781
5812
 
@@ -4785,17 +5816,22 @@ function printJSXAttribute(attr, path, options, print) {
4785
5816
 
4786
5817
  if (attr.value.type === 'Literal' || attr.value.type === 'StringLiteral') {
4787
5818
  const quote = options.jsxSingleQuote ? "'" : '"';
4788
- return concat([name, '=', quote, attr.value.value, quote]);
5819
+ return [name, '=', quote, attr.value.value, quote];
4789
5820
  }
4790
5821
 
4791
5822
  if (attr.value.type === 'JSXExpressionContainer') {
4792
5823
  const exprDoc = path.call(print, 'value', 'expression');
4793
- return concat([name, '={', exprDoc, '}']);
5824
+ return [name, '={', exprDoc, '}'];
4794
5825
  }
4795
5826
 
4796
5827
  return name;
4797
5828
  }
4798
5829
 
5830
+ /**
5831
+ * Print a JSX member expression (e.g., React.Fragment)
5832
+ * @param {RippleASTNode} node - The JSX member expression or identifier
5833
+ * @returns {string}
5834
+ */
4799
5835
  function printJSXMemberExpression(node) {
4800
5836
  if (node.type === 'JSXIdentifier') {
4801
5837
  return node.name;
@@ -4806,6 +5842,13 @@ function printJSXMemberExpression(node) {
4806
5842
  return 'Unknown';
4807
5843
  }
4808
5844
 
5845
+ /**
5846
+ * Print a member expression as simple string (for element tag names)
5847
+ * @param {RippleASTNode} node - The node to print
5848
+ * @param {RippleFormatOptions} options - Prettier options
5849
+ * @param {boolean} [computed=false] - Whether the property is computed
5850
+ * @returns {string}
5851
+ */
4809
5852
  function printMemberExpressionSimple(node, options, computed = false) {
4810
5853
  if (node.type === 'Identifier') {
4811
5854
  // When computed is true, it means we're inside brackets and tracked is already handled by .@[ or [
@@ -4832,6 +5875,14 @@ function printMemberExpressionSimple(node, options, computed = false) {
4832
5875
  return '';
4833
5876
  }
4834
5877
 
5878
+ /**
5879
+ * Print a Ripple Element node
5880
+ * @param {RippleASTNode} node - The element node
5881
+ * @param {AstPath<RippleASTNode>} path - The AST path
5882
+ * @param {RippleFormatOptions} options - Prettier options
5883
+ * @param {PrintFn} print - Print callback
5884
+ * @returns {Doc}
5885
+ */
4835
5886
  function printElement(node, path, options, print) {
4836
5887
  const tagName = printMemberExpressionSimple(node.id, options);
4837
5888
  const elementLeadingComments = getElementLeadingComments(node);
@@ -4839,10 +5890,11 @@ function printElement(node, path, options, print) {
4839
5890
  // `metadata.elementLeadingComments` may include comments that actually appear *inside* the element
4840
5891
  // body (after the opening tag). Those must not be hoisted before the element.
4841
5892
  const outerElementLeadingComments = elementLeadingComments.filter(
4842
- (comment) => typeof comment.start !== 'number' || comment.start < node.start,
5893
+ (/** @type {RippleASTNode} */ comment) =>
5894
+ typeof comment.start !== 'number' || comment.start < node.start,
4843
5895
  );
4844
5896
  const innerElementBodyComments = elementLeadingComments.filter(
4845
- (comment) =>
5897
+ (/** @type {RippleASTNode} */ comment) =>
4846
5898
  typeof comment.start === 'number' &&
4847
5899
  comment.start >= node.openingElement.end &&
4848
5900
  comment.start < node.end,
@@ -4861,9 +5913,7 @@ function printElement(node, path, options, print) {
4861
5913
 
4862
5914
  if (isSelfClosing && !hasInnerComments && !hasAttributes) {
4863
5915
  const elementDoc = group(['<', tagName, ' />']);
4864
- return metadataCommentParts.length > 0
4865
- ? concat([...metadataCommentParts, elementDoc])
4866
- : elementDoc;
5916
+ return metadataCommentParts.length > 0 ? [...metadataCommentParts, elementDoc] : elementDoc;
4867
5917
  }
4868
5918
 
4869
5919
  // Determine the line break type for attributes
@@ -4877,13 +5927,11 @@ function printElement(node, path, options, print) {
4877
5927
  '<',
4878
5928
  tagName,
4879
5929
  hasAttributes
4880
- ? indent(
4881
- concat([
4882
- ...path.map((attrPath) => {
4883
- return concat([attrLineBreak, print(attrPath)]);
4884
- }, 'attributes'),
4885
- ]),
4886
- )
5930
+ ? indent([
5931
+ ...path.map((attrPath) => {
5932
+ return [attrLineBreak, print(attrPath)];
5933
+ }, 'attributes'),
5934
+ ])
4887
5935
  : '',
4888
5936
  // Add line break opportunity before > or />
4889
5937
  // Use line for self-closing (keeps space), softline for non-self-closing when attributes present
@@ -4900,9 +5948,7 @@ function printElement(node, path, options, print) {
4900
5948
 
4901
5949
  if (!hasChildren) {
4902
5950
  if (!hasInnerComments) {
4903
- return metadataCommentParts.length > 0
4904
- ? concat([...metadataCommentParts, openingTag])
4905
- : openingTag;
5951
+ return metadataCommentParts.length > 0 ? [...metadataCommentParts, openingTag] : openingTag;
4906
5952
  }
4907
5953
 
4908
5954
  const innerParts = [];
@@ -4920,15 +5966,15 @@ function printElement(node, path, options, print) {
4920
5966
  innerParts.pop();
4921
5967
  }
4922
5968
 
4923
- const closingTag = concat(['</', tagName, '>']);
5969
+ const closingTag = ['</', tagName, '>'];
4924
5970
  const elementOutput = group([
4925
5971
  openingTag,
4926
- indent(concat([hardline, ...innerParts])),
5972
+ indent([hardline, ...innerParts]),
4927
5973
  hardline,
4928
5974
  closingTag,
4929
5975
  ]);
4930
5976
  return metadataCommentParts.length > 0
4931
- ? concat([...metadataCommentParts, elementOutput])
5977
+ ? [...metadataCommentParts, elementOutput]
4932
5978
  : elementOutput;
4933
5979
  }
4934
5980
 
@@ -4937,7 +5983,12 @@ function printElement(node, path, options, print) {
4937
5983
  const finalChildren = [];
4938
5984
  const sortedInnerElementBodyComments =
4939
5985
  innerElementBodyComments.length > 0
4940
- ? innerElementBodyComments.slice().sort((a, b) => (a.start ?? 0) - (b.start ?? 0))
5986
+ ? innerElementBodyComments
5987
+ .slice()
5988
+ .sort(
5989
+ (/** @type {RippleASTNode} */ a, /** @type {RippleASTNode} */ b) =>
5990
+ (a.start ?? 0) - (b.start ?? 0),
5991
+ )
4941
5992
  : [];
4942
5993
  let innerElementBodyCommentIndex = 0;
4943
5994
 
@@ -4951,6 +6002,7 @@ function printElement(node, path, options, print) {
4951
6002
  if (currentChildStart != null) {
4952
6003
  const commentsBefore = [];
4953
6004
  while (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6005
+ /** @type {RippleASTNode} */
4954
6006
  const comment = sortedInnerElementBodyComments[innerElementBodyCommentIndex];
4955
6007
  if (typeof comment.start !== 'number' || comment.start >= currentChildStart) {
4956
6008
  break;
@@ -4985,7 +6037,7 @@ function printElement(node, path, options, print) {
4985
6037
  }
4986
6038
  }
4987
6039
 
4988
- const childPrintArgs = {};
6040
+ const childPrintArgs = /** @type {PrintArgs} */ ({});
4989
6041
  if (hasTextLeadingComments) {
4990
6042
  childPrintArgs.suppressLeadingComments = true;
4991
6043
  }
@@ -5000,7 +6052,7 @@ function printElement(node, path, options, print) {
5000
6052
 
5001
6053
  const childDoc =
5002
6054
  rawExpressionLeadingComments && rawExpressionLeadingComments.length > 0
5003
- ? concat([...createElementLevelCommentParts(rawExpressionLeadingComments), printedChild])
6055
+ ? [...createElementLevelCommentParts(rawExpressionLeadingComments), printedChild]
5004
6056
  : printedChild;
5005
6057
  finalChildren.push(childDoc);
5006
6058
 
@@ -5013,6 +6065,7 @@ function printElement(node, path, options, print) {
5013
6065
  if (typeof currentChildEnd === 'number') {
5014
6066
  const commentsBetween = [];
5015
6067
  while (innerElementBodyCommentIndex < sortedInnerElementBodyComments.length) {
6068
+ /** @type {RippleASTNode} */
5016
6069
  const comment = sortedInnerElementBodyComments[innerElementBodyCommentIndex];
5017
6070
  if (typeof comment.start !== 'number') {
5018
6071
  innerElementBodyCommentIndex++;
@@ -5057,7 +6110,11 @@ function printElement(node, path, options, print) {
5057
6110
  if (insertedBodyCommentsBetween) {
5058
6111
  continue;
5059
6112
  }
5060
- const whitespaceLinesCount = getBlankLinesBetweenNodes(currentChild, nextChild);
6113
+ const whitespaceTarget =
6114
+ nextChild.leadingComments && nextChild.leadingComments.length > 0
6115
+ ? nextChild.leadingComments[0]
6116
+ : nextChild;
6117
+ const whitespaceLinesCount = getBlankLinesBetweenNodes(currentChild, whitespaceTarget);
5061
6118
  const isTextOrHtmlChild =
5062
6119
  currentChild.type === 'Text' ||
5063
6120
  currentChild.type === 'Html' ||
@@ -5076,6 +6133,32 @@ function printElement(node, path, options, print) {
5076
6133
  }
5077
6134
  }
5078
6135
 
6136
+ // Collect comments attached to the closing element (comments that appear after the last child
6137
+ // but before the closing tag, e.g. `</div>`). The parser attaches these as leadingComments
6138
+ // on either the closingElement node or the closingElement.name node depending on context.
6139
+ const closingElementComments = [
6140
+ ...(node.closingElement && Array.isArray(node.closingElement.leadingComments)
6141
+ ? node.closingElement.leadingComments
6142
+ : []),
6143
+ ...(node.closingElement &&
6144
+ node.closingElement.name &&
6145
+ Array.isArray(node.closingElement.name.leadingComments)
6146
+ ? node.closingElement.name.leadingComments
6147
+ : []),
6148
+ ];
6149
+
6150
+ if (closingElementComments.length > 0) {
6151
+ const lastChild = node.children[node.children.length - 1];
6152
+ if (lastChild) {
6153
+ const blankLinesBefore = getBlankLinesBetweenNodes(lastChild, closingElementComments[0]);
6154
+ finalChildren.push(hardline);
6155
+ if (blankLinesBefore > 0) {
6156
+ finalChildren.push(hardline);
6157
+ }
6158
+ }
6159
+ finalChildren.push(...createElementLevelCommentPartsTrimmed(closingElementComments));
6160
+ }
6161
+
5079
6162
  const fallbackCommentParts =
5080
6163
  fallbackElementComments.length > 0
5081
6164
  ? createElementLevelCommentParts(fallbackElementComments)
@@ -5085,12 +6168,14 @@ function printElement(node, path, options, print) {
5085
6168
  ? [...metadataCommentParts, ...fallbackCommentParts]
5086
6169
  : fallbackCommentParts;
5087
6170
 
5088
- const closingTag = concat(['</', tagName, '>']);
6171
+ const closingTag = ['</', tagName, '>'];
5089
6172
  let elementOutput;
5090
6173
 
5091
6174
  const hasComponentChild =
5092
6175
  node.children &&
5093
- node.children.some((child) => child.type === 'Component' && !child.selfClosing);
6176
+ node.children.some(
6177
+ (/** @type {RippleASTNode} */ child) => child.type === 'Component' && !child.selfClosing,
6178
+ );
5094
6179
 
5095
6180
  if (finalChildren.length === 1 && !hasComponentChild) {
5096
6181
  const child = finalChildren[0];
@@ -5111,42 +6196,28 @@ function printElement(node, path, options, print) {
5111
6196
  shouldInlineSingleChild(node, firstChild, child)
5112
6197
  ) {
5113
6198
  if (isElementChild && hasAttributes) {
5114
- elementOutput = concat([
5115
- openingTag,
5116
- indent(concat([hardline, child])),
5117
- hardline,
5118
- closingTag,
5119
- ]);
6199
+ elementOutput = [openingTag, indent([hardline, child]), hardline, closingTag];
5120
6200
  } else {
5121
- elementOutput = group([
5122
- openingTag,
5123
- indent(concat([softline, child])),
5124
- softline,
5125
- closingTag,
5126
- ]);
6201
+ elementOutput = group([openingTag, indent([softline, child]), softline, closingTag]);
5127
6202
  }
5128
6203
  } else {
5129
- elementOutput = concat([
5130
- openingTag,
5131
- indent(concat([hardline, ...finalChildren])),
5132
- hardline,
5133
- closingTag,
5134
- ]);
6204
+ elementOutput = [openingTag, indent([hardline, ...finalChildren]), hardline, closingTag];
5135
6205
  }
5136
6206
  } else {
5137
- elementOutput = group([
5138
- openingTag,
5139
- indent(concat([hardline, ...finalChildren])),
5140
- hardline,
5141
- closingTag,
5142
- ]);
6207
+ elementOutput = group([openingTag, indent([hardline, ...finalChildren]), hardline, closingTag]);
5143
6208
  }
5144
6209
 
5145
- return leadingCommentParts.length > 0
5146
- ? concat([...leadingCommentParts, elementOutput])
5147
- : elementOutput;
6210
+ return leadingCommentParts.length > 0 ? [...leadingCommentParts, elementOutput] : elementOutput;
5148
6211
  }
5149
6212
 
6213
+ /**
6214
+ * Print a Ripple attribute node
6215
+ * @param {RippleASTNode} node - The attribute node
6216
+ * @param {AstPath<RippleASTNode>} path - The AST path
6217
+ * @param {RippleFormatOptions} options - Prettier options
6218
+ * @param {PrintFn} print - Print callback
6219
+ * @returns {Doc[]}
6220
+ */
5150
6221
  function printAttribute(node, path, options, print) {
5151
6222
  const parts = [];
5152
6223