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 +2 -2
- package/src/compiler/phases/1-parse/index.js +286 -55
- package/src/compiler/phases/2-analyze/index.js +35 -1
- package/src/compiler/phases/3-transform/client/index.js +224 -60
- package/src/runtime/index-client.js +3 -3
- package/src/runtime/internal/client/blocks.js +26 -4
- package/src/runtime/internal/client/compat.js +37 -5
- package/src/runtime/internal/client/types.d.ts +10 -0
- package/src/utils/builders.js +17 -0
- package/tests/client/basic/basic.components.test.ripple +30 -20
- package/tests/client/compiler/compiler.basic.test.ripple +93 -64
- package/tests/client/compiler/compiler.tracked-access.test.ripple +108 -0
- package/tests/client/dynamic-elements.test.ripple +120 -1
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.
|
|
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.
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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 }) => ({
|
|
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
|
-
|
|
1811
|
+
const metadata =
|
|
1812
|
+
/** @type {{ commentContainerId?: number, elementLeadingComments?: CommentWithLocation[] }} */ (
|
|
1813
|
+
node?.metadata
|
|
1814
|
+
);
|
|
1727
1815
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
|
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);
|