ripple 0.2.133 → 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.133",
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.133"
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 ??= {};
@@ -1759,9 +1869,12 @@ function get_comment_handlers(source, comments, index = 0) {
1759
1869
 
1760
1870
  if (comments[0]) {
1761
1871
  if (node.type === 'BlockStatement' && node.body.length === 0) {
1762
- if (comments[0].start < node.end && comments[0].end < node.end) {
1872
+ // Collect all comments that fall within this empty block
1873
+ while (comments[0] && comments[0].start < node.end && comments[0].end < node.end) {
1763
1874
  comment = /** @type {CommentWithLocation} */ (comments.shift());
1764
1875
  (node.innerComments ||= []).push(comment);
1876
+ }
1877
+ if (node.innerComments && node.innerComments.length > 0) {
1765
1878
  return;
1766
1879
  }
1767
1880
  }
@@ -1773,44 +1886,162 @@ function get_comment_handlers(source, comments, index = 0) {
1773
1886
  return;
1774
1887
  }
1775
1888
  }
1889
+
1776
1890
  const parent = /** @type {any} */ (path.at(-1));
1777
1891
 
1778
1892
  if (parent === undefined || node.end !== parent.end) {
1779
1893
  const slice = source.slice(node.end, comments[0].start);
1780
- const is_last_in_body =
1781
- ((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
1782
- parent.body.indexOf(node) === parent.body.length - 1) ||
1783
- (parent?.type === 'ArrayExpression' &&
1784
- parent.elements.indexOf(node) === parent.elements.length - 1) ||
1785
- (parent?.type === 'ObjectExpression' &&
1786
- parent.properties.indexOf(node) === parent.properties.length - 1);
1787
-
1788
- if (is_last_in_body) {
1789
- // Special case: There can be multiple trailing comments after the last node in a block,
1790
- // and they can be separated by newlines
1791
- let end = node.end;
1792
-
1793
- while (comments.length) {
1794
- const comment = comments[0];
1795
- if (parent && comment.start >= parent.end) break;
1796
-
1797
- (node.trailingComments ||= []).push(comment);
1798
- comments.shift();
1799
- end = comment.end;
1894
+
1895
+ // Check if this node is the last item in an array-like structure
1896
+ let is_last_in_array = false;
1897
+ let array_prop = null;
1898
+
1899
+ if (
1900
+ parent?.type === 'BlockStatement' ||
1901
+ parent?.type === 'Program' ||
1902
+ parent?.type === 'Component' ||
1903
+ parent?.type === 'ClassBody'
1904
+ ) {
1905
+ array_prop = 'body';
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
+ ) {
1914
+ array_prop = 'elements';
1915
+ } else if (
1916
+ parent?.type === 'ObjectExpression' ||
1917
+ parent?.type === 'TrackedObjectExpression'
1918
+ ) {
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';
1932
+ }
1933
+ if (array_prop && Array.isArray(parent[array_prop])) {
1934
+ is_last_in_array =
1935
+ parent[array_prop].indexOf(node) === parent[array_prop].length - 1;
1936
+ }
1937
+
1938
+ if (is_last_in_array) {
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
+ }
1994
+
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
+ }
2022
+
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
+ }
1800
2039
  }
1801
- } else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
1802
- node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
1803
2040
  }
1804
2041
  }
1805
2042
  }
1806
2043
  },
1807
2044
  });
1808
-
1809
- // Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
1810
- // Adding them ensures that we can later detect the end of the expression tag correctly.
1811
- if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
1812
- (ast.trailingComments ||= []).push(...comments.splice(0));
1813
- }
1814
2045
  },
1815
2046
  };
1816
2047
  }
@@ -177,7 +177,30 @@ const visitors = {
177
177
  if (node.object.type === 'Identifier' && !node.object.tracked) {
178
178
  const binding = context.state.scope.get(node.object.name);
179
179
 
180
- if (binding !== null && binding.initial?.type === 'CallExpression' && is_ripple_track_call(binding.initial.callee, context)) {
180
+ if (binding && binding.metadata?.is_tracked_object) {
181
+ const internalProperties = new Set(['__v', 'a', 'b', 'c', 'f']);
182
+
183
+ let propertyName = null;
184
+ if (node.property.type === 'Identifier' && !node.computed) {
185
+ propertyName = node.property.name;
186
+ } else if (node.property.type === 'Literal' && typeof node.property.value === 'string') {
187
+ propertyName = node.property.value;
188
+ }
189
+
190
+ if (propertyName && internalProperties.has(propertyName)) {
191
+ error(
192
+ `Directly accessing internal property "${propertyName}" of a tracked object is not allowed. Use \`get(${node.object.name})\` or \`@${node.object.name}\` instead.`,
193
+ context.state.analysis.module.filename,
194
+ node.property
195
+ );
196
+ }
197
+ }
198
+
199
+ if (
200
+ binding !== null &&
201
+ binding.initial?.type === 'CallExpression' &&
202
+ is_ripple_track_call(binding.initial.callee, context)
203
+ ) {
181
204
  error(
182
205
  `Accessing a tracked object directly is not allowed, use the \`@\` prefix to read the value inside a tracked object - for example \`@${node.object.name}${node.property.type === 'Identifier' ? `.${node.property.name}` : ''}\``,
183
206
  context.state.analysis.module.filename,
@@ -231,6 +254,17 @@ const visitors = {
231
254
  const metadata = { tracking: false, await: false };
232
255
 
233
256
  if (declarator.id.type === 'Identifier') {
257
+ const binding = state.scope.get(declarator.id.name);
258
+ if (binding && declarator.init && declarator.init.type === 'CallExpression') {
259
+ const callee = declarator.init.callee;
260
+ // Check if it's a call to `track` or `tracked`
261
+ if (
262
+ (callee.type === 'Identifier' && (callee.name === 'track' || callee.name === 'tracked')) ||
263
+ (callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && (callee.property.name === 'track' || callee.property.name === 'tracked'))
264
+ ) {
265
+ binding.metadata = { ...binding.metadata, is_tracked_object: true };
266
+ }
267
+ }
234
268
  visit(declarator, state);
235
269
  } else {
236
270
  const paths = extract_paths(declarator.id);