@ripple-ts/prettier-plugin 0.2.169 → 0.2.171

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ripple-ts/prettier-plugin",
3
- "version": "0.2.169",
3
+ "version": "0.2.171",
4
4
  "description": "Ripple plugin for Prettier",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "prettier": "^3.6.2",
28
- "ripple": "0.2.169"
28
+ "ripple": "0.2.171"
29
29
  },
30
30
  "dependencies": {},
31
31
  "files": [
package/src/index.js CHANGED
@@ -21,11 +21,6 @@ const {
21
21
  } = builders;
22
22
  const { willBreak } = utils;
23
23
 
24
- // Embed function - not needed for now
25
- export function embed(path, options) {
26
- return null;
27
- }
28
-
29
24
  export const languages = [
30
25
  {
31
26
  name: 'ripple',
@@ -39,8 +34,7 @@ export const parsers = {
39
34
  ripple: {
40
35
  astFormat: 'ripple-ast',
41
36
  parse(text, parsers, options) {
42
- const ast = parse(text);
43
- return ast;
37
+ return parse(text);
44
38
  },
45
39
 
46
40
  locStart(node) {
@@ -66,11 +60,50 @@ export const printers = {
66
60
  }
67
61
  return typeof parts === 'string' ? parts : parts;
68
62
  },
63
+ embed(path, options) {
64
+ const node = path.getValue();
65
+
66
+ // Handle StyleSheet nodes inside style tags
67
+ if (node.type === 'StyleSheet' && node.source) {
68
+ // Return async function that will be called by Prettier
69
+ return async (textToDoc) => {
70
+ try {
71
+ // Format CSS using Prettier's textToDoc
72
+ const body = await textToDoc(node.source, {
73
+ parser: 'css',
74
+ });
75
+
76
+ // Return the formatted CSS
77
+ // Note: printElement will wrap this in indent(), so we don't add indent here
78
+ return body;
79
+ } catch (error) {
80
+ // If CSS has syntax errors, return original unformatted content
81
+ console.error('Error formatting CSS:', error);
82
+ return node.source;
83
+ }
84
+ };
85
+ }
86
+
87
+ return null;
88
+ },
69
89
  getVisitorKeys(node) {
90
+ // Exclude metadata and raw text properties that shouldn't be traversed
91
+ // The css property is specifically excluded so embed() can handle it
92
+ const excludedKeys = new Set([
93
+ 'start',
94
+ 'end',
95
+ 'loc',
96
+ 'metadata',
97
+ 'css', // Handled by embed()
98
+ 'raw',
99
+ 'regex',
100
+ ]);
101
+
70
102
  const keys = Object.keys(node).filter((key) => {
71
- return key === 'start' || key === 'end' || key === 'loc' || key === 'metadata' || 'css'
72
- ? false
73
- : typeof node[key] === 'object' && node[key] !== null;
103
+ if (excludedKeys.has(key)) {
104
+ return false;
105
+ }
106
+ return typeof node[key] === 'object' && node[key] !== null;
74
107
  });
75
108
 
76
109
  return keys;
@@ -1923,61 +1956,6 @@ function printRippleNode(node, path, options, print, args) {
1923
1956
  }
1924
1957
  break;
1925
1958
 
1926
- case 'StyleSheet':
1927
- nodeContent = printStyleSheet(node, path, options, print);
1928
- break;
1929
- case 'Rule':
1930
- nodeContent = printCSSRule(node, path, options, print);
1931
- break;
1932
-
1933
- case 'Declaration':
1934
- nodeContent = printCSSDeclaration(node, path, options, print);
1935
- break;
1936
-
1937
- case 'Atrule':
1938
- nodeContent = printCSSAtrule(node, path, options, print);
1939
- break;
1940
-
1941
- case 'SelectorList':
1942
- nodeContent = printCSSSelectorList(node, path, options, print);
1943
- break;
1944
-
1945
- case 'ComplexSelector':
1946
- nodeContent = printCSSComplexSelector(node, path, options, print);
1947
- break;
1948
-
1949
- case 'RelativeSelector':
1950
- nodeContent = printCSSRelativeSelector(node, path, options, print);
1951
- break;
1952
-
1953
- case 'TypeSelector':
1954
- nodeContent = printCSSTypeSelector(node, path, options, print);
1955
- break;
1956
-
1957
- case 'IdSelector':
1958
- nodeContent = printCSSIdSelector(node, path, options, print);
1959
- break;
1960
-
1961
- case 'ClassSelector':
1962
- nodeContent = printCSSClassSelector(node, path, options, print);
1963
- break;
1964
-
1965
- case 'NestingSelector':
1966
- nodeContent = printCSSNestingSelector(node, path, options, print);
1967
- break;
1968
-
1969
- case 'PseudoClassSelector':
1970
- nodeContent = printCSSPseudoClassSelector(node, path, options, print);
1971
- break;
1972
-
1973
- case 'PseudoElementSelector':
1974
- nodeContent = printCSSPseudoElementSelector(node, path, options, print);
1975
- break;
1976
-
1977
- case 'Block':
1978
- nodeContent = printCSSBlock(node, path, options, print);
1979
- break;
1980
-
1981
1959
  case 'Attribute':
1982
1960
  nodeContent = printAttribute(node, path, options, print);
1983
1961
  break;
@@ -2066,7 +2044,6 @@ function printRippleNode(node, path, options, print, args) {
2066
2044
  }
2067
2045
 
2068
2046
  function printImportDeclaration(node, path, options, print) {
2069
- // Use Prettier's doc builders for proper cursor tracking
2070
2047
  const parts = ['import'];
2071
2048
 
2072
2049
  // Handle type imports
@@ -2095,25 +2072,47 @@ function printImportDeclaration(node, path, options, print) {
2095
2072
  }
2096
2073
  });
2097
2074
 
2098
- // Build import clause properly
2099
- const importParts = [];
2075
+ // Build import clause with proper grouping and line breaking
2076
+ const importClauseParts = [];
2077
+
2100
2078
  if (defaultImports.length > 0) {
2101
- importParts.push(defaultImports.join(', '));
2079
+ importClauseParts.push(defaultImports.join(', '));
2102
2080
  }
2103
2081
  if (namespaceImports.length > 0) {
2104
- importParts.push(namespaceImports.join(', '));
2082
+ importClauseParts.push(namespaceImports.join(', '));
2105
2083
  }
2106
2084
  if (namedImports.length > 0) {
2107
- importParts.push('{ ' + namedImports.join(', ') + ' }');
2085
+ // Use Prettier's group and conditionalGroup for named imports to handle line breaking
2086
+ const namedImportsDocs = namedImports.map((name) => name);
2087
+ const namedImportsGroup = group(
2088
+ concat([
2089
+ '{',
2090
+ indent(
2091
+ concat([
2092
+ options.bracketSpacing ? line : softline,
2093
+ join(concat([',', line]), namedImportsDocs),
2094
+ ]),
2095
+ ),
2096
+ ifBreak(shouldPrintComma(options) ? ',' : ''),
2097
+ options.bracketSpacing ? line : softline,
2098
+ '}',
2099
+ ]),
2100
+ );
2101
+ importClauseParts.push(namedImportsGroup);
2108
2102
  }
2109
2103
 
2110
- parts.push(' ' + importParts.join(', ') + ' from');
2104
+ parts.push(' ');
2105
+ if (importClauseParts.length === 1 && typeof importClauseParts[0] === 'object') {
2106
+ parts.push(importClauseParts[0]);
2107
+ } else {
2108
+ parts.push(join(', ', importClauseParts));
2109
+ }
2110
+ parts.push(' from');
2111
2111
  }
2112
2112
 
2113
- parts.push(' ' + formatStringLiteral(node.source.value, options) + semi(options));
2113
+ parts.push(' ', formatStringLiteral(node.source.value, options), semi(options));
2114
2114
 
2115
- // Return as single string for proper cursor tracking
2116
- return parts;
2115
+ return concat(parts);
2117
2116
  }
2118
2117
 
2119
2118
  function printExportNamedDeclaration(node, path, options, print) {
@@ -4319,215 +4318,6 @@ function printTSIndexedAccessType(node, path, options, print) {
4319
4318
  return concat([path.call(print, 'objectType'), '[', path.call(print, 'indexType'), ']']);
4320
4319
  }
4321
4320
 
4322
- function printStyleSheet(node, path, options, print) {
4323
- // StyleSheet contains CSS rules in the 'body' property
4324
- if (node.body && node.body.length > 0) {
4325
- const cssItems = [];
4326
-
4327
- // Process each item in the stylesheet body
4328
- for (let i = 0; i < node.body.length; i++) {
4329
- const item = path.call(print, 'body', i);
4330
- if (item) {
4331
- cssItems.push(item);
4332
- }
4333
- }
4334
-
4335
- // Structure the CSS with proper indentation and spacing
4336
- // Check for blank lines between CSS items and preserve them
4337
- const result = [];
4338
- for (let i = 0; i < cssItems.length; i++) {
4339
- result.push(cssItems[i]);
4340
- if (i < cssItems.length - 1) {
4341
- // Check if there are blank lines between current and next item
4342
- const currentItem = node.body[i];
4343
- const nextItem = node.body[i + 1];
4344
-
4345
- // Check for blank lines in the original CSS source between rules
4346
- let hasBlankLine = false;
4347
- if (
4348
- node.source &&
4349
- typeof currentItem.end === 'number' &&
4350
- typeof nextItem.start === 'number'
4351
- ) {
4352
- const textBetween = node.source.substring(currentItem.end, nextItem.start);
4353
- // Count newlines in the text between the rules
4354
- const newlineCount = (textBetween.match(/\n/g) || []).length;
4355
- // If there are 2 or more newlines, there's at least one blank line
4356
- hasBlankLine = newlineCount >= 2;
4357
- }
4358
- if (hasBlankLine) {
4359
- // If there are blank lines, add an extra hardline (to create a blank line)
4360
- result.push(hardline, hardline);
4361
- } else {
4362
- result.push(hardline);
4363
- }
4364
- }
4365
- }
4366
-
4367
- return concat(result);
4368
- }
4369
-
4370
- // If no body, return empty string
4371
- return '';
4372
- }
4373
-
4374
- function printCSSRule(node, path, options, print) {
4375
- // CSS Rule has prelude (selector) and block (declarations)
4376
- const selector = path.call(print, 'prelude');
4377
- const block = path.call(print, 'block');
4378
-
4379
- return group([selector, ' {', indent([hardline, block]), hardline, '}']);
4380
- }
4381
-
4382
- function printCSSDeclaration(node, path, options, print) {
4383
- // CSS Declaration has property and value
4384
- const parts = [node.property];
4385
-
4386
- if (node.value) {
4387
- parts.push(': ');
4388
- const value = path.call(print, 'value');
4389
- parts.push(value);
4390
- }
4391
-
4392
- parts.push(';');
4393
- return concat(parts);
4394
- }
4395
-
4396
- function printCSSAtrule(node, path, options, print) {
4397
- // CSS At-rule like @media, @keyframes, etc.
4398
- const parts = ['@', node.name];
4399
-
4400
- if (node.prelude) {
4401
- parts.push(' ');
4402
- const prelude = path.call(print, 'prelude');
4403
- parts.push(prelude);
4404
- }
4405
-
4406
- if (node.block) {
4407
- const block = path.call(print, 'block');
4408
- parts.push(' {');
4409
- parts.push(indent([hardline, block]));
4410
- parts.push(hardline, '}');
4411
- } else {
4412
- parts.push(';');
4413
- }
4414
-
4415
- return group(parts);
4416
- }
4417
-
4418
- function printCSSSelectorList(node, path, options, print) {
4419
- // SelectorList contains multiple selectors
4420
- if (node.children && node.children.length > 0) {
4421
- const selectors = [];
4422
- for (let i = 0; i < node.children.length; i++) {
4423
- const selector = path.call(print, 'children', i);
4424
- selectors.push(selector);
4425
- }
4426
- // Join selectors with comma and line break for proper CSS formatting
4427
- return join([',', hardline], selectors);
4428
- }
4429
- return '';
4430
- }
4431
-
4432
- function printCSSComplexSelector(node, path, options, print) {
4433
- // ComplexSelector contains selector components
4434
- if (node.children && node.children.length > 0) {
4435
- const selectorParts = [];
4436
- for (let i = 0; i < node.children.length; i++) {
4437
- const part = path.call(print, 'children', i);
4438
- selectorParts.push(part);
4439
- }
4440
- return concat(selectorParts);
4441
- }
4442
- return '';
4443
- }
4444
-
4445
- function printCSSRelativeSelector(node, path, options, print) {
4446
- // RelativeSelector contains selector components in the 'selectors' property
4447
- const parts = [];
4448
-
4449
- // Print combinator if it exists (e.g., +, >, ~, or space)
4450
- if (node.combinator) {
4451
- if (node.combinator.name === ' ') {
4452
- // Space combinator (descendant selector)
4453
- parts.push(' ');
4454
- } else {
4455
- // Other combinators (+, >, ~)
4456
- parts.push(' ', node.combinator.name, ' ');
4457
- }
4458
- }
4459
-
4460
- if (node.selectors && node.selectors.length > 0) {
4461
- const selectorParts = [];
4462
- for (let i = 0; i < node.selectors.length; i++) {
4463
- const part = path.call(print, 'selectors', i);
4464
- selectorParts.push(part);
4465
- }
4466
- parts.push(...selectorParts);
4467
- }
4468
-
4469
- return concat(parts);
4470
- }
4471
-
4472
- function printCSSTypeSelector(node, path, options, print) {
4473
- // TypeSelector for element names like 'div', 'body', 'p', etc.
4474
- return node.name || '';
4475
- }
4476
-
4477
- function printCSSIdSelector(node, path, options, print) {
4478
- // IdSelector for #id
4479
- return concat(['#', node.name || '']);
4480
- }
4481
-
4482
- function printCSSClassSelector(node, path, options, print) {
4483
- // ClassSelector for .class
4484
- return concat(['.', node.name || '']);
4485
- }
4486
-
4487
- function printCSSNestingSelector(node, path, options, print) {
4488
- // NestingSelector for & (parent reference in nested CSS)
4489
- return '&';
4490
- }
4491
-
4492
- function printCSSPseudoClassSelector(node, path, options, print) {
4493
- // PseudoClassSelector for :hover, :global(), etc.
4494
- const parts = [':', node.name || ''];
4495
-
4496
- // If it has args (like :global(.classname)), print them
4497
- if (node.args !== null && node.args !== undefined) {
4498
- parts.push('(', node.args, ')');
4499
- }
4500
-
4501
- return concat(parts);
4502
- }
4503
-
4504
- function printCSSPseudoElementSelector(node, path, options, print) {
4505
- // PseudoElementSelector for ::before, ::after, etc.
4506
- const parts = ['::', node.name || ''];
4507
-
4508
- // If it has args (like ::slotted(span)), print them
4509
- if (node.args !== null && node.args !== undefined) {
4510
- parts.push('(', node.args, ')');
4511
- }
4512
-
4513
- return concat(parts);
4514
- }
4515
-
4516
- function printCSSBlock(node, path, options, print) {
4517
- // CSS Block contains declarations
4518
- if (node.children && node.children.length > 0) {
4519
- const declarations = [];
4520
- for (let i = 0; i < node.children.length; i++) {
4521
- const decl = path.call(print, 'children', i);
4522
- if (decl) {
4523
- declarations.push(decl);
4524
- }
4525
- }
4526
- return join(hardline, declarations);
4527
- }
4528
- return '';
4529
- }
4530
-
4531
4321
  function shouldInlineSingleChild(parentNode, firstChild, childDoc) {
4532
4322
  if (!firstChild || childDoc == null) {
4533
4323
  return false;
package/src/index.test.js CHANGED
@@ -663,6 +663,33 @@ import { Something, type Props, track } from 'ripple';`;
663
663
  expect(result).toBeWithNewline(expected);
664
664
  });
665
665
 
666
+ it('should format long import statements correctly', async () => {
667
+ const input = `import { flushSync, track, effect, bindValue, bindChecked, bindGroup, bindClientWidth, bindClientHeight, bindOffsetWidth, bindOffsetHeight, bindContentRect, bindContentBoxSize, bindBorderBoxSize, bindDevicePixelContentBoxSize, bindInnerHTML, bindInnerText, bindTextContent, bindNode } from 'ripple';`;
668
+ const expected = `import {
669
+ flushSync,
670
+ track,
671
+ effect,
672
+ bindValue,
673
+ bindChecked,
674
+ bindGroup,
675
+ bindClientWidth,
676
+ bindClientHeight,
677
+ bindOffsetWidth,
678
+ bindOffsetHeight,
679
+ bindContentRect,
680
+ bindContentBoxSize,
681
+ bindBorderBoxSize,
682
+ bindDevicePixelContentBoxSize,
683
+ bindInnerHTML,
684
+ bindInnerText,
685
+ bindTextContent,
686
+ bindNode,
687
+ } from 'ripple';`;
688
+
689
+ const result = await format(input, { singleQuote: true, printWidth: 80 });
690
+ expect(result).toBeWithNewline(expected);
691
+ });
692
+
666
693
  it('should preserve @ symbol in JSX attributes and shorthand syntax', async () => {
667
694
  const input = `component App() {
668
695
  const count = track(0);
@@ -1135,6 +1162,59 @@ const [obj1, obj2] = arrayOfObjects;`;
1135
1162
  expect(result).toBeWithNewline(expected);
1136
1163
  });
1137
1164
 
1165
+ it('should keep css @keyframes syntax intact', async () => {
1166
+ const input = `export component App() {
1167
+ <style>
1168
+ /* Scoped keyframe - only usable within Parent */
1169
+ @keyframes slideIn {
1170
+ from { transform: translateX(-100%); }
1171
+ to { transform: translateX(0); }
1172
+ }
1173
+
1174
+ /* Global keyframe - usable in any component */
1175
+ @keyframes -global-fadeIn {
1176
+ 0% { opacity: 0; }
1177
+ 100% { opacity: 1; }
1178
+ }
1179
+
1180
+ .parent {
1181
+ animation: slideIn 1s;
1182
+ }
1183
+ </style>
1184
+ }`;
1185
+
1186
+ const expected = `export component App() {
1187
+ <style>
1188
+ /* Scoped keyframe - only usable within Parent */
1189
+ @keyframes slideIn {
1190
+ from {
1191
+ transform: translateX(-100%);
1192
+ }
1193
+ to {
1194
+ transform: translateX(0);
1195
+ }
1196
+ }
1197
+
1198
+ /* Global keyframe - usable in any component */
1199
+ @keyframes -global-fadeIn {
1200
+ 0% {
1201
+ opacity: 0;
1202
+ }
1203
+ 100% {
1204
+ opacity: 1;
1205
+ }
1206
+ }
1207
+
1208
+ .parent {
1209
+ animation: slideIn 1s;
1210
+ }
1211
+ </style>
1212
+ }`;
1213
+
1214
+ const result = await format(input, { singleQuote: true, printWidth: 100 });
1215
+ expect(result).toBeWithNewline(expected);
1216
+ });
1217
+
1138
1218
  it('should keep TrackedMap short syntax intact', async () => {
1139
1219
  const expected = `const map = new #Map([['key1', 'value1'], ['key2', 'value2']]);
1140
1220
  const set = new #Set([1, 2, 3]);`;