ripple 0.2.134 → 0.2.136

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.136",
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.136"
85
85
  }
86
86
  }
@@ -29,11 +29,34 @@ function convert_from_jsx(node) {
29
29
  return node;
30
30
  }
31
31
 
32
+ const regex_whitespace_only = /\s/;
33
+
34
+ /**
35
+ * Skip whitespace characters without skipping comments.
36
+ * This is needed because Acorn's skipSpace() also skips comments, which breaks
37
+ * parsing in certain contexts. Updates parser position and line tracking.
38
+ * @param {acorn.Parser} parser
39
+ */
40
+ function skipWhitespace(parser) {
41
+ const originalStart = parser.start;
42
+ while (parser.start < parser.input.length && regex_whitespace_only.test(parser.input[parser.start])) {
43
+ parser.start++;
44
+ }
45
+ // Update line tracking if whitespace was skipped
46
+ if (parser.start !== originalStart) {
47
+ const lineInfo = acorn.getLineInfo(parser.input, parser.start);
48
+ parser.curLine = lineInfo.line;
49
+ parser.lineStart = parser.start - lineInfo.column;
50
+ parser.startLoc = lineInfo;
51
+ }
52
+ }
53
+
32
54
  function isWhitespaceTextNode(node) {
33
55
  if (!node || node.type !== 'Text') {
34
56
  return false;
35
57
  }
36
- const value = typeof node.value === 'string' ? node.value : typeof node.raw === 'string' ? node.raw : '';
58
+ const value =
59
+ typeof node.value === 'string' ? node.value : typeof node.raw === 'string' ? node.raw : '';
37
60
  return /^\s*$/.test(value);
38
61
  }
39
62
 
@@ -64,7 +87,9 @@ function RipplePlugin(config) {
64
87
  }
65
88
 
66
89
  const children = Array.isArray(container.children) ? container.children : [];
67
- const hasMeaningfulChildren = children.some((child) => child && !isWhitespaceTextNode(child));
90
+ const hasMeaningfulChildren = children.some(
91
+ (child) => child && !isWhitespaceTextNode(child),
92
+ );
68
93
 
69
94
  if (hasMeaningfulChildren) {
70
95
  return null;
@@ -497,6 +522,25 @@ function RipplePlugin(config) {
497
522
 
498
523
  return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
499
524
  }
525
+
526
+ /**
527
+ * Override to track parenthesized expressions in metadata
528
+ * This allows the prettier plugin to preserve parentheses where they existed
529
+ */
530
+ parseParenAndDistinguishExpression(canBeArrow, forInit) {
531
+ const startPos = this.start;
532
+ const expr = super.parseParenAndDistinguishExpression(canBeArrow, forInit);
533
+
534
+ // If the expression's start position is after the opening paren,
535
+ // it means it was wrapped in parentheses. Mark it in metadata.
536
+ if (expr && expr.start > startPos) {
537
+ expr.metadata ??= {};
538
+ expr.metadata.parenthesized = true;
539
+ }
540
+
541
+ return expr;
542
+ }
543
+
500
544
  /**
501
545
  * Parse `@(expression)` syntax for unboxing tracked values
502
546
  * Creates a TrackedExpression node with the argument property
@@ -688,6 +732,7 @@ function RipplePlugin(config) {
688
732
  this.exitScope();
689
733
 
690
734
  this.next();
735
+ skipWhitespace(this);
691
736
  this.finishNode(node, 'Component');
692
737
  this.awaitPos = 0;
693
738
 
@@ -995,6 +1040,10 @@ function RipplePlugin(config) {
995
1040
  }
996
1041
 
997
1042
  jsx_parseElementName() {
1043
+ if (this.type?.label === 'jsxTagEnd') {
1044
+ return '';
1045
+ }
1046
+
998
1047
  let node = this.jsx_parseNamespacedName();
999
1048
 
1000
1049
  if (node.type === 'JSXNamespacedName') {
@@ -1004,8 +1053,39 @@ function RipplePlugin(config) {
1004
1053
  if (this.eat(tt.dot)) {
1005
1054
  let memberExpr = this.startNodeAt(node.start, node.loc && node.loc.start);
1006
1055
  memberExpr.object = node;
1007
- memberExpr.property = this.jsx_parseIdentifier();
1008
- memberExpr.computed = false;
1056
+
1057
+ // Check for .@[expression] syntax for tracked computed member access
1058
+ // After eating the dot, check if the current token is @ followed by [
1059
+ if (this.type.label === '@') {
1060
+ // Check if the next character after @ is [
1061
+ const nextChar = this.input.charCodeAt(this.pos);
1062
+
1063
+ if (nextChar === 91) { // [ character
1064
+ memberExpr.computed = true;
1065
+
1066
+ // Consume the @ token
1067
+ this.next();
1068
+
1069
+ // Now this.type should be bracketL
1070
+ // Consume the [ and parse the expression inside
1071
+ this.expect(tt.bracketL);
1072
+
1073
+ // Parse the expression inside brackets
1074
+ memberExpr.property = this.parseExpression();
1075
+ memberExpr.property.tracked = true;
1076
+
1077
+ // Expect closing bracket
1078
+ this.expect(tt.bracketR);
1079
+ } else {
1080
+ // @ not followed by [, treat as regular tracked identifier
1081
+ memberExpr.property = this.jsx_parseIdentifier();
1082
+ memberExpr.computed = false;
1083
+ }
1084
+ } else {
1085
+ // Regular dot notation
1086
+ memberExpr.property = this.jsx_parseIdentifier();
1087
+ memberExpr.computed = false;
1088
+ }
1009
1089
  while (this.eat(tt.dot)) {
1010
1090
  let newMemberExpr = this.startNodeAt(
1011
1091
  memberExpr.start,
@@ -1029,7 +1109,7 @@ function RipplePlugin(config) {
1029
1109
  var t = this.jsx_parseExpressionContainer();
1030
1110
  return (
1031
1111
  'JSXEmptyExpression' === t.expression.type &&
1032
- this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
1112
+ this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
1033
1113
  t
1034
1114
  );
1035
1115
  case tok.jsxTagStart:
@@ -1202,14 +1282,14 @@ function RipplePlugin(config) {
1202
1282
  this.raise(
1203
1283
  this.pos,
1204
1284
  'Unexpected token `' +
1205
- this.input[this.pos] +
1206
- '`. Did you mean `' +
1207
- (ch === 62 ? '&gt;' : '&rbrace;') +
1208
- '` or ' +
1209
- '`{"' +
1210
- this.input[this.pos] +
1211
- '"}' +
1212
- '`?',
1285
+ this.input[this.pos] +
1286
+ '`. Did you mean `' +
1287
+ (ch === 62 ? '&gt;' : '&rbrace;') +
1288
+ '` or ' +
1289
+ '`{"' +
1290
+ this.input[this.pos] +
1291
+ '"}' +
1292
+ '`?',
1213
1293
  );
1214
1294
  }
1215
1295
 
@@ -1519,6 +1599,7 @@ function RipplePlugin(config) {
1519
1599
 
1520
1600
  this.#path.pop();
1521
1601
  this.next();
1602
+ skipWhitespace(this);
1522
1603
  return;
1523
1604
  }
1524
1605
  const node = this.parseElement();
@@ -1528,7 +1609,14 @@ function RipplePlugin(config) {
1528
1609
  } else {
1529
1610
  const node = this.parseStatement(null);
1530
1611
  body.push(node);
1612
+
1613
+ // Ensure we're not in JSX context before recursing
1614
+ // This is important when elements are parsed at statement level
1615
+ if (this.curContext() === this.acornTypeScript.tokContexts.tc_expr) {
1616
+ this.context.pop();
1617
+ }
1531
1618
  }
1619
+
1532
1620
  this.parseTemplateBody(body);
1533
1621
  }
1534
1622
 
@@ -1575,6 +1663,7 @@ function RipplePlugin(config) {
1575
1663
  this.exitScope();
1576
1664
 
1577
1665
  this.next();
1666
+ skipWhitespace(this);
1578
1667
  this.finishNode(node, 'Component');
1579
1668
  this.awaitPos = 0;
1580
1669
 
@@ -1687,6 +1776,16 @@ function RipplePlugin(config) {
1687
1776
  * @returns {{ onComment: Function, add_comments: Function }} Comment handler functions
1688
1777
  */
1689
1778
  function get_comment_handlers(source, comments, index = 0) {
1779
+ function getNextNonWhitespaceCharacter(text, startIndex) {
1780
+ for (let i = startIndex; i < text.length; i++) {
1781
+ const char = text[i];
1782
+ if (char !== ' ' && char !== '\t' && char !== '\n' && char !== '\r') {
1783
+ return char;
1784
+ }
1785
+ }
1786
+ return null;
1787
+ }
1788
+
1690
1789
  return {
1691
1790
  onComment: (block, value, start, end, start_loc, end_loc, metadata) => {
1692
1791
  if (block && /\n/.test(value)) {
@@ -1717,34 +1816,66 @@ function get_comment_handlers(source, comments, index = 0) {
1717
1816
 
1718
1817
  comments = comments
1719
1818
  .filter((comment) => comment.start >= index)
1720
- .map(({ type, value, start, end, loc, context }) => ({ type, value, start, end, loc, context }));
1819
+ .map(({ type, value, start, end, loc, context }) => ({
1820
+ type,
1821
+ value,
1822
+ start,
1823
+ end,
1824
+ loc,
1825
+ context,
1826
+ }));
1721
1827
 
1722
1828
  walk(ast, null, {
1723
1829
  _(node, { next, path }) {
1724
1830
  let comment;
1725
1831
 
1726
- const metadata = /** @type {{ commentContainerId?: number, elementLeadingComments?: CommentWithLocation[] }} */ (node?.metadata);
1832
+ const metadata =
1833
+ /** @type {{ commentContainerId?: number, elementLeadingComments?: CommentWithLocation[] }} */ (
1834
+ node?.metadata
1835
+ );
1727
1836
 
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
- }
1837
+ if (metadata && metadata.commentContainerId !== undefined) {
1838
+ while (
1839
+ comments[0] &&
1840
+ comments[0].context &&
1841
+ comments[0].context.containerId === metadata.commentContainerId &&
1842
+ comments[0].context.beforeMeaningfulChild
1843
+ ) {
1844
+ const elementComment = /** @type {CommentWithLocation & { context?: any }} */ (
1845
+ comments.shift()
1846
+ );
1847
+ (metadata.elementLeadingComments ||= []).push(elementComment);
1738
1848
  }
1849
+ }
1739
1850
 
1740
1851
  while (comments[0] && comments[0].start < node.start) {
1741
1852
  comment = /** @type {CommentWithLocation} */ (comments.shift());
1853
+
1854
+ // Skip leading comments for BlockStatement that is a function body
1855
+ // These comments should be dangling on the function instead
1856
+ if (node.type === 'BlockStatement') {
1857
+ const parent = path.at(-1);
1858
+ if (
1859
+ parent &&
1860
+ (parent.type === 'FunctionDeclaration' ||
1861
+ parent.type === 'FunctionExpression' ||
1862
+ parent.type === 'ArrowFunctionExpression') &&
1863
+ parent.body === node
1864
+ ) {
1865
+ // This is a function body - don't attach comment, let it be handled by function
1866
+ (parent.comments ||= []).push(comment);
1867
+ continue;
1868
+ }
1869
+ }
1870
+
1742
1871
  if (comment.loc) {
1743
1872
  const ancestorElements = path
1744
1873
  .filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
1745
1874
  .sort((a, b) => a.loc.start.line - b.loc.start.line);
1746
1875
 
1747
- const targetAncestor = ancestorElements.find((ancestor) => comment.loc.start.line < ancestor.loc.start.line);
1876
+ const targetAncestor = ancestorElements.find(
1877
+ (ancestor) => comment.loc.start.line < ancestor.loc.start.line,
1878
+ );
1748
1879
 
1749
1880
  if (targetAncestor) {
1750
1881
  targetAncestor.metadata ??= {};
@@ -1755,72 +1886,183 @@ function get_comment_handlers(source, comments, index = 0) {
1755
1886
  (node.leadingComments ||= []).push(comment);
1756
1887
  }
1757
1888
 
1758
- next();
1889
+ next();
1759
1890
 
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) {
1891
+ if (comments[0]) {
1892
+ if (node.type === 'BlockStatement' && node.body.length === 0) {
1893
+ // Collect all comments that fall within this empty block
1894
+ while (comments[0] && comments[0].start < node.end && comments[0].end < node.end) {
1895
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
1896
+ (node.innerComments ||= []).push(comment);
1897
+ }
1898
+ if (node.innerComments && node.innerComments.length > 0) {
1899
+ return;
1900
+ }
1901
+ }
1902
+ // Handle empty Element nodes the same way as empty BlockStatements
1903
+ if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
1904
+ if (comments[0].start < node.end && comments[0].end < node.end) {
1905
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
1906
+ (node.innerComments ||= []).push(comment);
1907
+ return;
1908
+ }
1909
+ }
1910
+
1911
+ const parent = /** @type {any} */ (path.at(-1));
1912
+
1913
+ if (parent === undefined || node.end !== parent.end) {
1780
1914
  const slice = source.slice(node.end, comments[0].start);
1781
1915
 
1782
1916
  // Check if this node is the last item in an array-like structure
1783
1917
  let is_last_in_array = false;
1784
1918
  let array_prop = null;
1785
1919
 
1786
- if (parent?.type === 'BlockStatement' || parent?.type === 'Program' || parent?.type === 'Component') {
1920
+ if (
1921
+ parent?.type === 'BlockStatement' ||
1922
+ parent?.type === 'Program' ||
1923
+ parent?.type === 'Component' ||
1924
+ parent?.type === 'ClassBody'
1925
+ ) {
1787
1926
  array_prop = 'body';
1788
- } else if (parent?.type === 'ArrayExpression') {
1927
+ } else if (parent?.type === 'SwitchStatement') {
1928
+ array_prop = 'cases';
1929
+ } else if (parent?.type === 'SwitchCase') {
1930
+ array_prop = 'consequent';
1931
+ } else if (
1932
+ parent?.type === 'ArrayExpression' ||
1933
+ parent?.type === 'TrackedArrayExpression'
1934
+ ) {
1789
1935
  array_prop = 'elements';
1790
- } else if (parent?.type === 'ObjectExpression') {
1936
+ } else if (
1937
+ parent?.type === 'ObjectExpression' ||
1938
+ parent?.type === 'TrackedObjectExpression'
1939
+ ) {
1791
1940
  array_prop = 'properties';
1941
+ } else if (
1942
+ parent?.type === 'FunctionDeclaration' ||
1943
+ parent?.type === 'FunctionExpression' ||
1944
+ parent?.type === 'ArrowFunctionExpression'
1945
+ ) {
1946
+ array_prop = 'params';
1947
+ } else if (
1948
+ parent?.type === 'CallExpression' ||
1949
+ parent?.type === 'OptionalCallExpression' ||
1950
+ parent?.type === 'NewExpression'
1951
+ ) {
1952
+ array_prop = 'arguments';
1792
1953
  }
1793
-
1794
1954
  if (array_prop && Array.isArray(parent[array_prop])) {
1795
- is_last_in_array = parent[array_prop].indexOf(node) === parent[array_prop].length - 1;
1955
+ is_last_in_array =
1956
+ parent[array_prop].indexOf(node) === parent[array_prop].length - 1;
1796
1957
  }
1797
1958
 
1798
1959
  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;
1960
+ const isParam = array_prop === 'params';
1961
+ const isArgument = array_prop === 'arguments';
1962
+ if (isParam || isArgument) {
1963
+ while (comments.length) {
1964
+ const potentialComment = comments[0];
1965
+ if (parent && potentialComment.start >= parent.end) {
1966
+ break;
1967
+ }
1968
+
1969
+ const nextChar = getNextNonWhitespaceCharacter(source, potentialComment.end);
1970
+ if (nextChar === ')') {
1971
+ (node.trailingComments ||= []).push(
1972
+ /** @type {CommentWithLocation} */(comments.shift()),
1973
+ );
1974
+ continue;
1975
+ }
1976
+
1977
+ break;
1978
+ }
1979
+ } else {
1980
+ // Special case: There can be multiple trailing comments after the last node in a block,
1981
+ // and they can be separated by newlines
1982
+ let end = node.end;
1802
1983
 
1803
- while (comments.length) {
1804
- const comment = comments[0];
1805
- if (parent && comment.start >= parent.end) break;
1984
+ while (comments.length) {
1985
+ const comment = comments[0];
1986
+ if (parent && comment.start >= parent.end) break;
1806
1987
 
1807
- (node.trailingComments ||= []).push(comment);
1808
- comments.shift();
1809
- end = comment.end;
1988
+ (node.trailingComments ||= []).push(comment);
1989
+ comments.shift();
1990
+ end = comment.end;
1991
+ }
1992
+ }
1993
+ } else if (node.end <= comments[0].start) {
1994
+ const onlySimpleWhitespace = /^[,) \t]*$/.test(slice);
1995
+ const onlyWhitespace = /^\s*$/.test(slice);
1996
+ const hasBlankLine = /\n\s*\n/.test(slice);
1997
+ const nodeEndLine = node.loc?.end?.line ?? null;
1998
+ const commentStartLine = comments[0].loc?.start?.line ?? null;
1999
+ const isImmediateNextLine =
2000
+ nodeEndLine !== null &&
2001
+ commentStartLine !== null &&
2002
+ commentStartLine === nodeEndLine + 1;
2003
+ const isSwitchCaseSibling = array_prop === 'cases';
2004
+
2005
+ if (isSwitchCaseSibling && !is_last_in_array) {
2006
+ if (
2007
+ nodeEndLine !== null &&
2008
+ commentStartLine !== null &&
2009
+ nodeEndLine === commentStartLine
2010
+ ) {
2011
+ node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
2012
+ }
2013
+ return;
2014
+ }
2015
+
2016
+ if (
2017
+ onlySimpleWhitespace ||
2018
+ (onlyWhitespace && !hasBlankLine && isImmediateNextLine)
2019
+ ) {
2020
+ // Check if this is a block comment that's inline with the next statement
2021
+ // e.g., /** @type {SomeType} */ (a) = 5;
2022
+ // These should be leading comments, not trailing
2023
+ if (
2024
+ comments[0].type === 'Block' &&
2025
+ !is_last_in_array &&
2026
+ array_prop &&
2027
+ parent[array_prop]
2028
+ ) {
2029
+ const currentIndex = parent[array_prop].indexOf(node);
2030
+ const nextSibling = parent[array_prop][currentIndex + 1];
2031
+
2032
+ if (nextSibling && nextSibling.loc) {
2033
+ const commentEndLine = comments[0].loc?.end?.line;
2034
+ const nextSiblingStartLine = nextSibling.loc?.start?.line;
2035
+
2036
+ // If comment ends on same line as next sibling starts, it's inline with next
2037
+ if (commentEndLine === nextSiblingStartLine) {
2038
+ // Leave it for next sibling's leading comments
2039
+ return;
2040
+ }
2041
+ }
2042
+ }
2043
+
2044
+ // For function parameters, only attach as trailing comment if it's on the same line
2045
+ // Comments on next line after comma should be leading comments of next parameter
2046
+ const isParam = array_prop === 'params';
2047
+ if (isParam) {
2048
+ // Check if comment is on same line as the node
2049
+ const nodeEndLine = source.slice(0, node.end).split('\n').length;
2050
+ const commentStartLine = source.slice(0, comments[0].start).split('\n').length;
2051
+ if (nodeEndLine === commentStartLine) {
2052
+ node.trailingComments = [
2053
+ /** @type {CommentWithLocation} */ (comments.shift()),
2054
+ ];
2055
+ }
2056
+ // Otherwise leave it for next parameter's leading comments
2057
+ } else {
2058
+ node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
2059
+ }
1810
2060
  }
1811
- } else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
1812
- node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
1813
2061
  }
1814
2062
  }
1815
2063
  }
1816
2064
  },
1817
2065
  });
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
2066
  },
1825
2067
  };
1826
2068
  }