@tsrx/prettier-plugin 0.3.63 → 0.3.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.js +93 -18
- package/src/index.test.js +120 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsrx/prettier-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.65",
|
|
4
4
|
"description": "Ripple plugin for Prettier",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"prettier": "^3.8.3"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@tsrx/core": "0.1.
|
|
30
|
+
"@tsrx/core": "0.1.13"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"src/"
|
package/src/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @typedef {((path: AstPath) => Doc) & ((path: AstPath, args: PrintArgs) => Doc)} PrintFn
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
/** @typedef {Partial<Pick<ParserOptions, 'singleQuote' | 'jsxSingleQuote' | 'semi' | 'trailingComma' | 'useTabs' | 'tabWidth' | 'singleAttributePerLine' | 'bracketSameLine' | 'bracketSpacing' | 'arrowParens' | 'originalText'>> & { locStart: (node: AST.NodeWithLocation) => number, locEnd: (node: AST.NodeWithLocation) => number }} RippleFormatOptions */
|
|
17
|
+
/** @typedef {Partial<Pick<ParserOptions, 'singleQuote' | 'jsxSingleQuote' | 'semi' | 'trailingComma' | 'useTabs' | 'tabWidth' | 'singleAttributePerLine' | 'bracketSameLine' | 'bracketSpacing' | 'arrowParens' | 'originalText' | 'printWidth'>> & { locStart: (node: AST.NodeWithLocation) => number, locEnd: (node: AST.NodeWithLocation) => number }} RippleFormatOptions */
|
|
18
18
|
|
|
19
19
|
/** @typedef {{ isInAttribute?: boolean, isInArray?: boolean, allowInlineObject?: boolean, isConditionalTest?: boolean, isNestedConditional?: boolean, suppressLeadingComments?: boolean, suppressExpressionLeadingComments?: boolean, isInlineContext?: boolean, isStatement?: boolean, isLogicalAndOr?: boolean, allowShorthandProperty?: boolean, isFirstChild?: boolean, skipComponentLabel?: boolean, noBreakInside?: boolean, expandLastArg?: boolean }} PrintArgs */
|
|
20
20
|
|
|
@@ -1506,6 +1506,15 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1506
1506
|
break;
|
|
1507
1507
|
}
|
|
1508
1508
|
|
|
1509
|
+
case 'TSSatisfiesExpression': {
|
|
1510
|
+
nodeContent = [
|
|
1511
|
+
path.call(print, 'expression'),
|
|
1512
|
+
' satisfies ',
|
|
1513
|
+
path.call(print, 'typeAnnotation'),
|
|
1514
|
+
];
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1509
1518
|
case 'TSNonNullExpression': {
|
|
1510
1519
|
nodeContent = [path.call(print, 'expression'), '!'];
|
|
1511
1520
|
break;
|
|
@@ -1923,8 +1932,23 @@ function printRippleNode(node, path, options, print, args) {
|
|
|
1923
1932
|
case 'LogicalExpression': {
|
|
1924
1933
|
const logicalParent = path.getParentNode();
|
|
1925
1934
|
let logicalResult;
|
|
1926
|
-
|
|
1927
|
-
|
|
1935
|
+
const rightIsNullLiteral = node.right.type === 'Literal' && node.right.value === null;
|
|
1936
|
+
const shouldKeepNullishFallbackInline =
|
|
1937
|
+
node.operator === '??' &&
|
|
1938
|
+
rightIsNullLiteral &&
|
|
1939
|
+
(node.left.type === 'CallExpression' ||
|
|
1940
|
+
node.left.type === 'ChainExpression' ||
|
|
1941
|
+
node.left.type === 'NewExpression');
|
|
1942
|
+
if (shouldKeepNullishFallbackInline) {
|
|
1943
|
+
logicalResult = group([
|
|
1944
|
+
path.call(print, 'left'),
|
|
1945
|
+
' ',
|
|
1946
|
+
node.operator,
|
|
1947
|
+
' ',
|
|
1948
|
+
path.call(print, 'right'),
|
|
1949
|
+
]);
|
|
1950
|
+
} else if (args?.isConditionalTest) {
|
|
1951
|
+
// Don't add indent if we're in a conditional test context
|
|
1928
1952
|
logicalResult = group([
|
|
1929
1953
|
path.call((childPath) => print(childPath, { isConditionalTest: true }), 'left'),
|
|
1930
1954
|
' ',
|
|
@@ -2814,36 +2838,40 @@ function printArrowFunction(node, path, options, print, args) {
|
|
|
2814
2838
|
parts.push(': ', path.call(print, 'returnType'));
|
|
2815
2839
|
}
|
|
2816
2840
|
|
|
2817
|
-
parts.push(' => ');
|
|
2818
|
-
|
|
2819
2841
|
// For block statements, print the body directly to get proper formatting
|
|
2820
2842
|
if (node.body.type === 'BlockStatement') {
|
|
2843
|
+
parts.push(' => ');
|
|
2821
2844
|
parts.push(path.call(print, 'body'));
|
|
2822
2845
|
} else {
|
|
2823
2846
|
// For expression bodies, check if we need to wrap in parens
|
|
2824
2847
|
// Wrap ObjectExpression, AssignmentExpression, and SequenceExpression in parens
|
|
2825
2848
|
// to avoid ambiguity with block statements or to clarify intent
|
|
2826
2849
|
const bodyDoc = path.call(print, 'body');
|
|
2850
|
+
const groupId = Symbol('arrow');
|
|
2851
|
+
const shouldBreakBody = shouldBreakArrowExpressionBody(node.body, options, args);
|
|
2852
|
+
/** @type {Doc | Doc[]} */
|
|
2853
|
+
let bodyContent;
|
|
2827
2854
|
if (
|
|
2828
2855
|
node.body.type === 'ObjectExpression' ||
|
|
2829
2856
|
node.body.type === 'AssignmentExpression' ||
|
|
2830
|
-
node.body.type === 'SequenceExpression'
|
|
2831
|
-
(args?.isInAttribute && isTemplateExpression(node.body))
|
|
2857
|
+
node.body.type === 'SequenceExpression'
|
|
2832
2858
|
) {
|
|
2833
|
-
|
|
2834
|
-
if (isTemplateExpression(node.body)) {
|
|
2835
|
-
parts.push(indent([hardline, bodyDoc]));
|
|
2836
|
-
parts.push(hardline);
|
|
2837
|
-
} else {
|
|
2838
|
-
parts.push(bodyDoc);
|
|
2839
|
-
}
|
|
2840
|
-
parts.push(')');
|
|
2859
|
+
bodyContent = ['(', bodyDoc, ')'];
|
|
2841
2860
|
} else {
|
|
2842
|
-
|
|
2861
|
+
bodyContent = bodyDoc;
|
|
2862
|
+
}
|
|
2863
|
+
if (shouldBreakBody) {
|
|
2864
|
+
parts.push(' =>', indent([hardline, bodyContent]));
|
|
2865
|
+
} else {
|
|
2866
|
+
parts.push(
|
|
2867
|
+
' =>',
|
|
2868
|
+
group(indent(line), { id: groupId }),
|
|
2869
|
+
indentIfBreak(bodyContent, { groupId }),
|
|
2870
|
+
);
|
|
2843
2871
|
}
|
|
2844
2872
|
}
|
|
2845
2873
|
|
|
2846
|
-
return parts;
|
|
2874
|
+
return group(parts);
|
|
2847
2875
|
}
|
|
2848
2876
|
|
|
2849
2877
|
/**
|
|
@@ -2861,6 +2889,15 @@ function isTemplateExpression(node) {
|
|
|
2861
2889
|
);
|
|
2862
2890
|
}
|
|
2863
2891
|
|
|
2892
|
+
/**
|
|
2893
|
+
* Check whether a braced attribute expression should close on its own line.
|
|
2894
|
+
* @param {AST.Node} node - The expression inside the attribute braces
|
|
2895
|
+
* @returns {boolean}
|
|
2896
|
+
*/
|
|
2897
|
+
function shouldBreakAttributeExpressionClosingBrace(node) {
|
|
2898
|
+
return node.type === 'ArrowFunctionExpression' && node.body && isTemplateExpression(node.body);
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2864
2901
|
/**
|
|
2865
2902
|
* Print an export default declaration
|
|
2866
2903
|
* @param {AST.ExportDefaultDeclaration} node - The export default node
|
|
@@ -3053,6 +3090,37 @@ function shouldHugArrowFunctions(args) {
|
|
|
3053
3090
|
return firstBlockIndex === 0;
|
|
3054
3091
|
}
|
|
3055
3092
|
|
|
3093
|
+
/**
|
|
3094
|
+
* Check whether a node's original source span exceeds the configured print width.
|
|
3095
|
+
* @param {AST.NodeWithLocation} node - The node to check
|
|
3096
|
+
* @param {RippleFormatOptions} options - Prettier options
|
|
3097
|
+
* @returns {boolean}
|
|
3098
|
+
*/
|
|
3099
|
+
function sourceSpanExceedsPrintWidth(node, options) {
|
|
3100
|
+
const printWidth = options.printWidth ?? 80;
|
|
3101
|
+
if (!options.originalText || node.start === undefined || node.end === undefined) {
|
|
3102
|
+
return false;
|
|
3103
|
+
}
|
|
3104
|
+
return options.originalText.slice(node.start, node.end).length > printWidth;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* Check if an arrow expression body should break immediately after `=>`.
|
|
3109
|
+
* @param {AST.Expression} node - The arrow body expression
|
|
3110
|
+
* @param {RippleFormatOptions} options - Prettier options
|
|
3111
|
+
* @param {PrintArgs} [args] - Additional context arguments
|
|
3112
|
+
* @returns {boolean}
|
|
3113
|
+
*/
|
|
3114
|
+
function shouldBreakArrowExpressionBody(node, options, args) {
|
|
3115
|
+
if (args?.isInAttribute && isTemplateExpression(node)) {
|
|
3116
|
+
return true;
|
|
3117
|
+
}
|
|
3118
|
+
return (
|
|
3119
|
+
(node.type === 'BinaryExpression' || node.type === 'LogicalExpression') &&
|
|
3120
|
+
sourceSpanExceedsPrintWidth(/** @type {AST.NodeWithLocation} */ (node), options)
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3056
3124
|
/**
|
|
3057
3125
|
* Print call expression arguments
|
|
3058
3126
|
* @param {AstPath<AST.CallExpression>} path - The call path
|
|
@@ -5727,7 +5795,7 @@ function printTsx(node, path, options, print) {
|
|
|
5727
5795
|
return [tagName, closingTagName];
|
|
5728
5796
|
}
|
|
5729
5797
|
|
|
5730
|
-
if (printedChildren.length > 1) {
|
|
5798
|
+
if (!is_fragment || printedChildren.length > 1) {
|
|
5731
5799
|
return group([
|
|
5732
5800
|
tagName,
|
|
5733
5801
|
indent([hardline, join(hardline, printedChildren)]),
|
|
@@ -6141,11 +6209,15 @@ function printJSXAttribute(attr, path, options, print) {
|
|
|
6141
6209
|
}
|
|
6142
6210
|
|
|
6143
6211
|
if (attr.value.type === 'JSXExpressionContainer') {
|
|
6212
|
+
const expression = attr.value.expression;
|
|
6144
6213
|
const exprDoc = path.call(
|
|
6145
6214
|
(valuePath) => print(valuePath, { isInAttribute: true }),
|
|
6146
6215
|
'value',
|
|
6147
6216
|
'expression',
|
|
6148
6217
|
);
|
|
6218
|
+
if (shouldBreakAttributeExpressionClosingBrace(expression)) {
|
|
6219
|
+
return [name, '={', exprDoc, hardline, '}'];
|
|
6220
|
+
}
|
|
6149
6221
|
return [name, '={', exprDoc, '}'];
|
|
6150
6222
|
}
|
|
6151
6223
|
|
|
@@ -6723,6 +6795,9 @@ function printAttribute(node, path, options, print) {
|
|
|
6723
6795
|
parts.push('={');
|
|
6724
6796
|
// Pass inline context for attribute values (keep objects compact)
|
|
6725
6797
|
parts.push(path.call((attrPath) => print(attrPath, { isInAttribute: true }), 'value'));
|
|
6798
|
+
if (shouldBreakAttributeExpressionClosingBrace(node.value)) {
|
|
6799
|
+
parts.push(hardline);
|
|
6800
|
+
}
|
|
6726
6801
|
parts.push('}');
|
|
6727
6802
|
}
|
|
6728
6803
|
}
|
package/src/index.test.js
CHANGED
|
@@ -200,6 +200,58 @@ describe('prettier-plugin', () => {
|
|
|
200
200
|
expect(result).toBeWithNewline(expected);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
it('should format explicit tsx arrow returns like tsrx blocks', async () => {
|
|
204
|
+
const input = `component Test(props) {
|
|
205
|
+
const func = (item) => <tsx><ItemView item={item} onSelect={props.onSelect} /></tsx>;
|
|
206
|
+
|
|
207
|
+
<List
|
|
208
|
+
items={props.items}
|
|
209
|
+
renderItem={(item) => <tsx><ItemView item={item} onSelect={props.onSelect} /></tsx>}
|
|
210
|
+
/>
|
|
211
|
+
}`;
|
|
212
|
+
const expected = `component Test(props) {
|
|
213
|
+
const func = (item) => <tsx>
|
|
214
|
+
<ItemView item={item} onSelect={props.onSelect} />
|
|
215
|
+
</tsx>;
|
|
216
|
+
|
|
217
|
+
<List
|
|
218
|
+
items={props.items}
|
|
219
|
+
renderItem={(item) =>
|
|
220
|
+
<tsx>
|
|
221
|
+
<ItemView item={item} onSelect={props.onSelect} />
|
|
222
|
+
</tsx>
|
|
223
|
+
}
|
|
224
|
+
/>
|
|
225
|
+
}`;
|
|
226
|
+
const result = await format(input);
|
|
227
|
+
expect(result).toBeWithNewline(expected);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should format template arrow returns in tsx attributes like ripple attributes', async () => {
|
|
231
|
+
const input = `component Test(props) {
|
|
232
|
+
const view = <tsx>
|
|
233
|
+
<List
|
|
234
|
+
items={props.items}
|
|
235
|
+
renderItem={(item) => <tsx><ItemView item={item} onSelect={props.onSelect} /></tsx>}
|
|
236
|
+
/>
|
|
237
|
+
</tsx>;
|
|
238
|
+
}`;
|
|
239
|
+
const expected = `component Test(props) {
|
|
240
|
+
const view = <tsx>
|
|
241
|
+
<List
|
|
242
|
+
items={props.items}
|
|
243
|
+
renderItem={(item) =>
|
|
244
|
+
<tsx>
|
|
245
|
+
<ItemView item={item} onSelect={props.onSelect} />
|
|
246
|
+
</tsx>
|
|
247
|
+
}
|
|
248
|
+
/>
|
|
249
|
+
</tsx>;
|
|
250
|
+
}`;
|
|
251
|
+
const result = await format(input);
|
|
252
|
+
expect(result).toBeWithNewline(expected);
|
|
253
|
+
});
|
|
254
|
+
|
|
203
255
|
it('should preserve comments before expressions after nested tsx and tsrx blocks', async () => {
|
|
204
256
|
const input = `component App() {
|
|
205
257
|
const content = <tsx>
|
|
@@ -255,7 +307,7 @@ describe('prettier-plugin', () => {
|
|
|
255
307
|
}`,
|
|
256
308
|
expected: `component Test() {
|
|
257
309
|
<A
|
|
258
|
-
fallback={(error) =>
|
|
310
|
+
fallback={(error) =>
|
|
259
311
|
<>
|
|
260
312
|
<B
|
|
261
313
|
id="xyz"
|
|
@@ -265,7 +317,7 @@ describe('prettier-plugin', () => {
|
|
|
265
317
|
otherProp={2}
|
|
266
318
|
/>
|
|
267
319
|
</>
|
|
268
|
-
|
|
320
|
+
}
|
|
269
321
|
/>
|
|
270
322
|
}`,
|
|
271
323
|
},
|
|
@@ -279,7 +331,7 @@ describe('prettier-plugin', () => {
|
|
|
279
331
|
}`,
|
|
280
332
|
expected: `component Test() {
|
|
281
333
|
<A
|
|
282
|
-
fallback={(error) =>
|
|
334
|
+
fallback={(error) =>
|
|
283
335
|
<tsx>
|
|
284
336
|
<B
|
|
285
337
|
id="xyz"
|
|
@@ -289,7 +341,7 @@ describe('prettier-plugin', () => {
|
|
|
289
341
|
otherProp={2}
|
|
290
342
|
/>
|
|
291
343
|
</tsx>
|
|
292
|
-
|
|
344
|
+
}
|
|
293
345
|
/>
|
|
294
346
|
}`,
|
|
295
347
|
},
|
|
@@ -303,7 +355,7 @@ describe('prettier-plugin', () => {
|
|
|
303
355
|
}`,
|
|
304
356
|
expected: `component Test() {
|
|
305
357
|
<A
|
|
306
|
-
fallback={(error) =>
|
|
358
|
+
fallback={(error) =>
|
|
307
359
|
<tsrx>
|
|
308
360
|
<B
|
|
309
361
|
id="xyz"
|
|
@@ -313,7 +365,7 @@ describe('prettier-plugin', () => {
|
|
|
313
365
|
otherProp={2}
|
|
314
366
|
/>
|
|
315
367
|
</tsrx>
|
|
316
|
-
|
|
368
|
+
}
|
|
317
369
|
/>
|
|
318
370
|
}`,
|
|
319
371
|
},
|
|
@@ -327,7 +379,7 @@ describe('prettier-plugin', () => {
|
|
|
327
379
|
}`,
|
|
328
380
|
expected: `component Test() {
|
|
329
381
|
<A
|
|
330
|
-
fallback={(error) =>
|
|
382
|
+
fallback={(error) =>
|
|
331
383
|
<tsx:react>
|
|
332
384
|
<B
|
|
333
385
|
id="xyz"
|
|
@@ -337,7 +389,7 @@ describe('prettier-plugin', () => {
|
|
|
337
389
|
otherProp={2}
|
|
338
390
|
/>
|
|
339
391
|
</tsx:react>
|
|
340
|
-
|
|
392
|
+
}
|
|
341
393
|
/>
|
|
342
394
|
}`,
|
|
343
395
|
},
|
|
@@ -1015,7 +1067,9 @@ import { Something, type Props, track } from 'ripple';`;
|
|
|
1015
1067
|
const foo = <tsx><Bar {...props} /></tsx>;`;
|
|
1016
1068
|
|
|
1017
1069
|
const expected = `const props = {};
|
|
1018
|
-
const foo = <tsx
|
|
1070
|
+
const foo = <tsx>
|
|
1071
|
+
<Bar {...props} />
|
|
1072
|
+
</tsx>;`;
|
|
1019
1073
|
|
|
1020
1074
|
const result = await format(input, { singleQuote: true });
|
|
1021
1075
|
expect(result).toBeWithNewline(expected);
|
|
@@ -1380,6 +1434,31 @@ import { effect, track } from 'ripple';`;
|
|
|
1380
1434
|
expect(result).toBeWithNewline(expected);
|
|
1381
1435
|
});
|
|
1382
1436
|
|
|
1437
|
+
it('should break arrow before a long generic optional call with nullish fallback', async () => {
|
|
1438
|
+
const input = `const test = () => menuRef.current?.querySelector<HTMLElement>(
|
|
1439
|
+
"[role=\\"menuitem\\"]:not([aria-disabled=\\"true\\"])",
|
|
1440
|
+
) ??
|
|
1441
|
+
null`;
|
|
1442
|
+
const expected = `const test = () =>
|
|
1443
|
+
menuRef.current?.querySelector<HTMLElement>(
|
|
1444
|
+
'[role="menuitem"]:not([aria-disabled="true"])',
|
|
1445
|
+
) ?? null;`;
|
|
1446
|
+
const result = await format(input, { singleQuote: true });
|
|
1447
|
+
expect(result).toBeWithNewline(expected);
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
it('keeps nullish fallback inline in a conditional test', async () => {
|
|
1451
|
+
const input = `const test = menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]:not([aria-disabled="true"])') ?? null ? a : b;`;
|
|
1452
|
+
const expected = `const test =
|
|
1453
|
+
menuRef.current?.querySelector<HTMLElement>(
|
|
1454
|
+
'[role="menuitem"]:not([aria-disabled="true"])',
|
|
1455
|
+
) ?? null
|
|
1456
|
+
? a
|
|
1457
|
+
: b;`;
|
|
1458
|
+
const result = await format(input, { singleQuote: true });
|
|
1459
|
+
expect(result).toBeWithNewline(expected);
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1383
1462
|
it('does not add spaces around inlined array elements in destructured arguments', async () => {
|
|
1384
1463
|
const expected = `for (const [key, value] of Object.entries(attributes).filter(([_key, value]) => value !== '')) {
|
|
1385
1464
|
}
|
|
@@ -2254,7 +2333,9 @@ files = [...(files ?? []), ...dt.files];`;
|
|
|
2254
2333
|
|
|
2255
2334
|
const expected = `class Foo {
|
|
2256
2335
|
bar() {
|
|
2257
|
-
return <tsx>
|
|
2336
|
+
return <tsx>
|
|
2337
|
+
{'Hello'}
|
|
2338
|
+
</tsx>;
|
|
2258
2339
|
}
|
|
2259
2340
|
}`;
|
|
2260
2341
|
|
|
@@ -2613,6 +2694,35 @@ component Child({ something }) {
|
|
|
2613
2694
|
expect(result).toBeWithNewline(expected);
|
|
2614
2695
|
});
|
|
2615
2696
|
|
|
2697
|
+
it('prints satisfies expressions in switch default cases', async () => {
|
|
2698
|
+
const input = `export component Test(props: { status: "ok" | "error" }) {
|
|
2699
|
+
switch (props.status) {
|
|
2700
|
+
case "ok":
|
|
2701
|
+
<div>"ok"</div>
|
|
2702
|
+
return
|
|
2703
|
+
case "error":
|
|
2704
|
+
<div>"error"</div>
|
|
2705
|
+
return
|
|
2706
|
+
default:
|
|
2707
|
+
props.status satisfies never
|
|
2708
|
+
}
|
|
2709
|
+
}`;
|
|
2710
|
+
const expected = `export component Test(props: { status: "ok" | "error" }) {
|
|
2711
|
+
switch (props.status) {
|
|
2712
|
+
case "ok":
|
|
2713
|
+
<div>"ok"</div>
|
|
2714
|
+
return;
|
|
2715
|
+
case "error":
|
|
2716
|
+
<div>"error"</div>
|
|
2717
|
+
return;
|
|
2718
|
+
default:
|
|
2719
|
+
props.status satisfies never;
|
|
2720
|
+
}
|
|
2721
|
+
}`;
|
|
2722
|
+
const result = await format(input);
|
|
2723
|
+
expect(result).toBeWithNewline(expected);
|
|
2724
|
+
});
|
|
2725
|
+
|
|
2616
2726
|
it('prints function with a rest parameter correctly', async () => {
|
|
2617
2727
|
const expected = `function TestRest(...args: string[]) {
|
|
2618
2728
|
console.log(args);
|