ripple 0.2.134 → 0.2.135

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
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.134",
6
+ "version": "0.2.135",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -81,6 +81,6 @@
81
81
  "typescript": "^5.9.2"
82
82
  },
83
83
  "peerDependencies": {
84
- "ripple": "0.2.134"
84
+ "ripple": "0.2.135"
85
85
  }
86
86
  }
@@ -33,7 +33,8 @@ function isWhitespaceTextNode(node) {
33
33
  if (!node || node.type !== 'Text') {
34
34
  return false;
35
35
  }
36
- const value = typeof node.value === 'string' ? node.value : typeof node.raw === 'string' ? node.raw : '';
36
+ const value =
37
+ typeof node.value === 'string' ? node.value : typeof node.raw === 'string' ? node.raw : '';
37
38
  return /^\s*$/.test(value);
38
39
  }
39
40
 
@@ -64,7 +65,9 @@ function RipplePlugin(config) {
64
65
  }
65
66
 
66
67
  const children = Array.isArray(container.children) ? container.children : [];
67
- const hasMeaningfulChildren = children.some((child) => child && !isWhitespaceTextNode(child));
68
+ const hasMeaningfulChildren = children.some(
69
+ (child) => child && !isWhitespaceTextNode(child),
70
+ );
68
71
 
69
72
  if (hasMeaningfulChildren) {
70
73
  return null;
@@ -497,6 +500,25 @@ function RipplePlugin(config) {
497
500
 
498
501
  return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
499
502
  }
503
+
504
+ /**
505
+ * Override to track parenthesized expressions in metadata
506
+ * This allows the prettier plugin to preserve parentheses where they existed
507
+ */
508
+ parseParenAndDistinguishExpression(canBeArrow, forInit) {
509
+ const startPos = this.start;
510
+ const expr = super.parseParenAndDistinguishExpression(canBeArrow, forInit);
511
+
512
+ // If the expression's start position is after the opening paren,
513
+ // it means it was wrapped in parentheses. Mark it in metadata.
514
+ if (expr && expr.start > startPos) {
515
+ expr.metadata ??= {};
516
+ expr.metadata.parenthesized = true;
517
+ }
518
+
519
+ return expr;
520
+ }
521
+
500
522
  /**
501
523
  * Parse `@(expression)` syntax for unboxing tracked values
502
524
  * Creates a TrackedExpression node with the argument property
@@ -995,6 +1017,10 @@ function RipplePlugin(config) {
995
1017
  }
996
1018
 
997
1019
  jsx_parseElementName() {
1020
+ if (this.type?.label === 'jsxTagEnd') {
1021
+ return '';
1022
+ }
1023
+
998
1024
  let node = this.jsx_parseNamespacedName();
999
1025
 
1000
1026
  if (node.type === 'JSXNamespacedName') {
@@ -1004,8 +1030,39 @@ function RipplePlugin(config) {
1004
1030
  if (this.eat(tt.dot)) {
1005
1031
  let memberExpr = this.startNodeAt(node.start, node.loc && node.loc.start);
1006
1032
  memberExpr.object = node;
1007
- memberExpr.property = this.jsx_parseIdentifier();
1008
- memberExpr.computed = false;
1033
+
1034
+ // Check for .@[expression] syntax for tracked computed member access
1035
+ // After eating the dot, check if the current token is @ followed by [
1036
+ if (this.type.label === '@') {
1037
+ // Check if the next character after @ is [
1038
+ const nextChar = this.input.charCodeAt(this.pos);
1039
+
1040
+ if (nextChar === 91) { // [ character
1041
+ memberExpr.computed = true;
1042
+
1043
+ // Consume the @ token
1044
+ this.next();
1045
+
1046
+ // Now this.type should be bracketL
1047
+ // Consume the [ and parse the expression inside
1048
+ this.expect(tt.bracketL);
1049
+
1050
+ // Parse the expression inside brackets
1051
+ memberExpr.property = this.parseExpression();
1052
+ memberExpr.property.tracked = true;
1053
+
1054
+ // Expect closing bracket
1055
+ this.expect(tt.bracketR);
1056
+ } else {
1057
+ // @ not followed by [, treat as regular tracked identifier
1058
+ memberExpr.property = this.jsx_parseIdentifier();
1059
+ memberExpr.computed = false;
1060
+ }
1061
+ } else {
1062
+ // Regular dot notation
1063
+ memberExpr.property = this.jsx_parseIdentifier();
1064
+ memberExpr.computed = false;
1065
+ }
1009
1066
  while (this.eat(tt.dot)) {
1010
1067
  let newMemberExpr = this.startNodeAt(
1011
1068
  memberExpr.start,
@@ -1029,7 +1086,7 @@ function RipplePlugin(config) {
1029
1086
  var t = this.jsx_parseExpressionContainer();
1030
1087
  return (
1031
1088
  'JSXEmptyExpression' === t.expression.type &&
1032
- this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
1089
+ this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
1033
1090
  t
1034
1091
  );
1035
1092
  case tok.jsxTagStart:
@@ -1202,14 +1259,14 @@ function RipplePlugin(config) {
1202
1259
  this.raise(
1203
1260
  this.pos,
1204
1261
  'Unexpected token `' +
1205
- this.input[this.pos] +
1206
- '`. Did you mean `' +
1207
- (ch === 62 ? '>' : '}') +
1208
- '` or ' +
1209
- '`{"' +
1210
- this.input[this.pos] +
1211
- '"}' +
1212
- '`?',
1262
+ this.input[this.pos] +
1263
+ '`. Did you mean `' +
1264
+ (ch === 62 ? '>' : '}') +
1265
+ '` or ' +
1266
+ '`{"' +
1267
+ this.input[this.pos] +
1268
+ '"}' +
1269
+ '`?',
1213
1270
  );
1214
1271
  }
1215
1272
 
@@ -1528,7 +1585,18 @@ function RipplePlugin(config) {
1528
1585
  } else {
1529
1586
  const node = this.parseStatement(null);
1530
1587
  body.push(node);
1588
+
1589
+ // Ensure we're not in JSX context before recursing
1590
+ // This is important when elements are parsed at statement level
1591
+ const tokContexts = this.acornTypeScript?.tokContexts;
1592
+ if (tokContexts && this.curContext) {
1593
+ const curContext = this.curContext();
1594
+ if (curContext === tokContexts.tc_expr) {
1595
+ this.context.pop();
1596
+ }
1597
+ }
1531
1598
  }
1599
+
1532
1600
  this.parseTemplateBody(body);
1533
1601
  }
1534
1602
 
@@ -1687,6 +1755,16 @@ function RipplePlugin(config) {
1687
1755
  * @returns {{ onComment: Function, add_comments: Function }} Comment handler functions
1688
1756
  */
1689
1757
  function get_comment_handlers(source, comments, index = 0) {
1758
+ function getNextNonWhitespaceCharacter(text, startIndex) {
1759
+ for (let i = startIndex; i < text.length; i++) {
1760
+ const char = text[i];
1761
+ if (char !== ' ' && char !== '\t' && char !== '\n' && char !== '\r') {
1762
+ return char;
1763
+ }
1764
+ }
1765
+ return null;
1766
+ }
1767
+
1690
1768
  return {
1691
1769
  onComment: (block, value, start, end, start_loc, end_loc, metadata) => {
1692
1770
  if (block && /\n/.test(value)) {
@@ -1717,34 +1795,66 @@ function get_comment_handlers(source, comments, index = 0) {
1717
1795
 
1718
1796
  comments = comments
1719
1797
  .filter((comment) => comment.start >= index)
1720
- .map(({ type, value, start, end, loc, context }) => ({ type, value, start, end, loc, context }));
1798
+ .map(({ type, value, start, end, loc, context }) => ({
1799
+ type,
1800
+ value,
1801
+ start,
1802
+ end,
1803
+ loc,
1804
+ context,
1805
+ }));
1721
1806
 
1722
1807
  walk(ast, null, {
1723
1808
  _(node, { next, path }) {
1724
1809
  let comment;
1725
1810
 
1726
- const metadata = /** @type {{ commentContainerId?: number, elementLeadingComments?: CommentWithLocation[] }} */ (node?.metadata);
1811
+ const metadata =
1812
+ /** @type {{ commentContainerId?: number, elementLeadingComments?: CommentWithLocation[] }} */ (
1813
+ node?.metadata
1814
+ );
1727
1815
 
1728
- if (metadata && metadata.commentContainerId !== undefined) {
1729
- while (
1730
- comments[0] &&
1731
- comments[0].context &&
1732
- comments[0].context.containerId === metadata.commentContainerId &&
1733
- comments[0].context.beforeMeaningfulChild
1734
- ) {
1735
- const elementComment = /** @type {CommentWithLocation & { context?: any }} */ (comments.shift());
1736
- (metadata.elementLeadingComments ||= []).push(elementComment);
1737
- }
1816
+ if (metadata && metadata.commentContainerId !== undefined) {
1817
+ while (
1818
+ comments[0] &&
1819
+ comments[0].context &&
1820
+ comments[0].context.containerId === metadata.commentContainerId &&
1821
+ comments[0].context.beforeMeaningfulChild
1822
+ ) {
1823
+ const elementComment = /** @type {CommentWithLocation & { context?: any }} */ (
1824
+ comments.shift()
1825
+ );
1826
+ (metadata.elementLeadingComments ||= []).push(elementComment);
1738
1827
  }
1828
+ }
1739
1829
 
1740
1830
  while (comments[0] && comments[0].start < node.start) {
1741
1831
  comment = /** @type {CommentWithLocation} */ (comments.shift());
1832
+
1833
+ // Skip leading comments for BlockStatement that is a function body
1834
+ // These comments should be dangling on the function instead
1835
+ if (node.type === 'BlockStatement') {
1836
+ const parent = path.at(-1);
1837
+ if (
1838
+ parent &&
1839
+ (parent.type === 'FunctionDeclaration' ||
1840
+ parent.type === 'FunctionExpression' ||
1841
+ parent.type === 'ArrowFunctionExpression') &&
1842
+ parent.body === node
1843
+ ) {
1844
+ // This is a function body - don't attach comment, let it be handled by function
1845
+ (parent.comments ||= []).push(comment);
1846
+ continue;
1847
+ }
1848
+ }
1849
+
1742
1850
  if (comment.loc) {
1743
1851
  const ancestorElements = path
1744
1852
  .filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
1745
1853
  .sort((a, b) => a.loc.start.line - b.loc.start.line);
1746
1854
 
1747
- const targetAncestor = ancestorElements.find((ancestor) => comment.loc.start.line < ancestor.loc.start.line);
1855
+ const targetAncestor = ancestorElements.find(
1856
+ (ancestor) => comment.loc.start.line < ancestor.loc.start.line,
1857
+ );
1748
1858
 
1749
1859
  if (targetAncestor) {
1750
1860
  targetAncestor.metadata ??= {};
@@ -1755,72 +1865,183 @@ function get_comment_handlers(source, comments, index = 0) {
1755
1865
  (node.leadingComments ||= []).push(comment);
1756
1866
  }
1757
1867
 
1758
- next();
1868
+ next();
1759
1869
 
1760
- if (comments[0]) {
1761
- if (node.type === 'BlockStatement' && node.body.length === 0) {
1762
- // Collect all comments that fall within this empty block
1763
- while (comments[0] && comments[0].start < node.end && comments[0].end < node.end) {
1764
- comment = /** @type {CommentWithLocation} */ (comments.shift());
1765
- (node.innerComments ||= []).push(comment);
1766
- }
1767
- if (node.innerComments && node.innerComments.length > 0) {
1768
- return;
1769
- }
1770
- }
1771
- // Handle empty Element nodes the same way as empty BlockStatements
1772
- if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
1773
- if (comments[0].start < node.end && comments[0].end < node.end) {
1774
- comment = /** @type {CommentWithLocation} */ (comments.shift());
1775
- (node.innerComments ||= []).push(comment);
1776
- return;
1777
- }
1778
- }
1779
- const parent = /** @type {any} */ (path.at(-1)); if (parent === undefined || node.end !== parent.end) {
1870
+ if (comments[0]) {
1871
+ if (node.type === 'BlockStatement' && node.body.length === 0) {
1872
+ // Collect all comments that fall within this empty block
1873
+ while (comments[0] && comments[0].start < node.end && comments[0].end < node.end) {
1874
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
1875
+ (node.innerComments ||= []).push(comment);
1876
+ }
1877
+ if (node.innerComments && node.innerComments.length > 0) {
1878
+ return;
1879
+ }
1880
+ }
1881
+ // Handle empty Element nodes the same way as empty BlockStatements
1882
+ if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
1883
+ if (comments[0].start < node.end && comments[0].end < node.end) {
1884
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
1885
+ (node.innerComments ||= []).push(comment);
1886
+ return;
1887
+ }
1888
+ }
1889
+
1890
+ const parent = /** @type {any} */ (path.at(-1));
1891
+
1892
+ if (parent === undefined || node.end !== parent.end) {
1780
1893
  const slice = source.slice(node.end, comments[0].start);
1781
1894
 
1782
1895
  // Check if this node is the last item in an array-like structure
1783
1896
  let is_last_in_array = false;
1784
1897
  let array_prop = null;
1785
1898
 
1786
- if (parent?.type === 'BlockStatement' || parent?.type === 'Program' || parent?.type === 'Component') {
1899
+ if (
1900
+ parent?.type === 'BlockStatement' ||
1901
+ parent?.type === 'Program' ||
1902
+ parent?.type === 'Component' ||
1903
+ parent?.type === 'ClassBody'
1904
+ ) {
1787
1905
  array_prop = 'body';
1788
- } else if (parent?.type === 'ArrayExpression') {
1906
+ } else if (parent?.type === 'SwitchStatement') {
1907
+ array_prop = 'cases';
1908
+ } else if (parent?.type === 'SwitchCase') {
1909
+ array_prop = 'consequent';
1910
+ } else if (
1911
+ parent?.type === 'ArrayExpression' ||
1912
+ parent?.type === 'TrackedArrayExpression'
1913
+ ) {
1789
1914
  array_prop = 'elements';
1790
- } else if (parent?.type === 'ObjectExpression') {
1915
+ } else if (
1916
+ parent?.type === 'ObjectExpression' ||
1917
+ parent?.type === 'TrackedObjectExpression'
1918
+ ) {
1791
1919
  array_prop = 'properties';
1920
+ } else if (
1921
+ parent?.type === 'FunctionDeclaration' ||
1922
+ parent?.type === 'FunctionExpression' ||
1923
+ parent?.type === 'ArrowFunctionExpression'
1924
+ ) {
1925
+ array_prop = 'params';
1926
+ } else if (
1927
+ parent?.type === 'CallExpression' ||
1928
+ parent?.type === 'OptionalCallExpression' ||
1929
+ parent?.type === 'NewExpression'
1930
+ ) {
1931
+ array_prop = 'arguments';
1792
1932
  }
1793
-
1794
1933
  if (array_prop && Array.isArray(parent[array_prop])) {
1795
- is_last_in_array = parent[array_prop].indexOf(node) === parent[array_prop].length - 1;
1934
+ is_last_in_array =
1935
+ parent[array_prop].indexOf(node) === parent[array_prop].length - 1;
1796
1936
  }
1797
1937
 
1798
1938
  if (is_last_in_array) {
1799
- // Special case: There can be multiple trailing comments after the last node in a block,
1800
- // and they can be separated by newlines
1801
- let end = node.end;
1939
+ const isParam = array_prop === 'params';
1940
+ const isArgument = array_prop === 'arguments';
1941
+ if (isParam || isArgument) {
1942
+ while (comments.length) {
1943
+ const potentialComment = comments[0];
1944
+ if (parent && potentialComment.start >= parent.end) {
1945
+ break;
1946
+ }
1947
+
1948
+ const nextChar = getNextNonWhitespaceCharacter(source, potentialComment.end);
1949
+ if (nextChar === ')') {
1950
+ (node.trailingComments ||= []).push(
1951
+ /** @type {CommentWithLocation} */(comments.shift()),
1952
+ );
1953
+ continue;
1954
+ }
1955
+
1956
+ break;
1957
+ }
1958
+ } else {
1959
+ // Special case: There can be multiple trailing comments after the last node in a block,
1960
+ // and they can be separated by newlines
1961
+ let end = node.end;
1962
+
1963
+ while (comments.length) {
1964
+ const comment = comments[0];
1965
+ if (parent && comment.start >= parent.end) break;
1966
+
1967
+ (node.trailingComments ||= []).push(comment);
1968
+ comments.shift();
1969
+ end = comment.end;
1970
+ }
1971
+ }
1972
+ } else if (node.end <= comments[0].start) {
1973
+ const onlySimpleWhitespace = /^[,) \t]*$/.test(slice);
1974
+ const onlyWhitespace = /^\s*$/.test(slice);
1975
+ const hasBlankLine = /\n\s*\n/.test(slice);
1976
+ const nodeEndLine = node.loc?.end?.line ?? null;
1977
+ const commentStartLine = comments[0].loc?.start?.line ?? null;
1978
+ const isImmediateNextLine =
1979
+ nodeEndLine !== null &&
1980
+ commentStartLine !== null &&
1981
+ commentStartLine === nodeEndLine + 1;
1982
+ const isSwitchCaseSibling = array_prop === 'cases';
1983
+
1984
+ if (isSwitchCaseSibling && !is_last_in_array) {
1985
+ if (
1986
+ nodeEndLine !== null &&
1987
+ commentStartLine !== null &&
1988
+ nodeEndLine === commentStartLine
1989
+ ) {
1990
+ node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
1991
+ }
1992
+ return;
1993
+ }
1802
1994
 
1803
- while (comments.length) {
1804
- const comment = comments[0];
1805
- if (parent && comment.start >= parent.end) break;
1995
+ if (
1996
+ onlySimpleWhitespace ||
1997
+ (onlyWhitespace && !hasBlankLine && isImmediateNextLine)
1998
+ ) {
1999
+ // Check if this is a block comment that's inline with the next statement
2000
+ // e.g., /** @type {SomeType} */ (a) = 5;
2001
+ // These should be leading comments, not trailing
2002
+ if (
2003
+ comments[0].type === 'Block' &&
2004
+ !is_last_in_array &&
2005
+ array_prop &&
2006
+ parent[array_prop]
2007
+ ) {
2008
+ const currentIndex = parent[array_prop].indexOf(node);
2009
+ const nextSibling = parent[array_prop][currentIndex + 1];
2010
+
2011
+ if (nextSibling && nextSibling.loc) {
2012
+ const commentEndLine = comments[0].loc?.end?.line;
2013
+ const nextSiblingStartLine = nextSibling.loc?.start?.line;
2014
+
2015
+ // If comment ends on same line as next sibling starts, it's inline with next
2016
+ if (commentEndLine === nextSiblingStartLine) {
2017
+ // Leave it for next sibling's leading comments
2018
+ return;
2019
+ }
2020
+ }
2021
+ }
1806
2022
 
1807
- (node.trailingComments ||= []).push(comment);
1808
- comments.shift();
1809
- end = comment.end;
2023
+ // For function parameters, only attach as trailing comment if it's on the same line
2024
+ // Comments on next line after comma should be leading comments of next parameter
2025
+ const isParam = array_prop === 'params';
2026
+ if (isParam) {
2027
+ // Check if comment is on same line as the node
2028
+ const nodeEndLine = source.slice(0, node.end).split('\n').length;
2029
+ const commentStartLine = source.slice(0, comments[0].start).split('\n').length;
2030
+ if (nodeEndLine === commentStartLine) {
2031
+ node.trailingComments = [
2032
+ /** @type {CommentWithLocation} */ (comments.shift()),
2033
+ ];
2034
+ }
2035
+ // Otherwise leave it for next parameter's leading comments
2036
+ } else {
2037
+ node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
2038
+ }
1810
2039
  }
1811
- } else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
1812
- node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
1813
2040
  }
1814
2041
  }
1815
2042
  }
1816
2043
  },
1817
2044
  });
1818
-
1819
- // Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
1820
- // Adding them ensures that we can later detect the end of the expression tag correctly.
1821
- if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
1822
- (ast.trailingComments ||= []).push(...comments.splice(0));
1823
- }
1824
2045
  },
1825
2046
  };
1826
2047
  }