ripple 0.2.132 → 0.2.134

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.132",
6
+ "version": "0.2.134",
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.132"
84
+ "ripple": "0.2.134"
85
85
  }
86
86
  }
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /** @import { Program } from 'estree' */
2
3
  /** @import {
3
4
  * CommentWithLocation,
@@ -28,6 +29,14 @@ function convert_from_jsx(node) {
28
29
  return node;
29
30
  }
30
31
 
32
+ function isWhitespaceTextNode(node) {
33
+ if (!node || node.type !== 'Text') {
34
+ return false;
35
+ }
36
+ const value = typeof node.value === 'string' ? node.value : typeof node.raw === 'string' ? node.raw : '';
37
+ return /^\s*$/.test(value);
38
+ }
39
+
31
40
  /**
32
41
  * Acorn parser plugin for Ripple syntax extensions
33
42
  * @param {RipplePluginConfig} [config] - Plugin configuration
@@ -42,6 +51,37 @@ function RipplePlugin(config) {
42
51
  class RippleParser extends Parser {
43
52
  /** @type {any[]} */
44
53
  #path = [];
54
+ #commentContextId = 0;
55
+
56
+ #createCommentMetadata() {
57
+ if (this.#path.length === 0) {
58
+ return null;
59
+ }
60
+
61
+ const container = this.#path[this.#path.length - 1];
62
+ if (!container || container.type !== 'Element') {
63
+ return null;
64
+ }
65
+
66
+ const children = Array.isArray(container.children) ? container.children : [];
67
+ const hasMeaningfulChildren = children.some((child) => child && !isWhitespaceTextNode(child));
68
+
69
+ if (hasMeaningfulChildren) {
70
+ return null;
71
+ }
72
+
73
+ container.metadata ??= {};
74
+ if (container.metadata.commentContainerId === undefined) {
75
+ container.metadata.commentContainerId = ++this.#commentContextId;
76
+ }
77
+
78
+ return {
79
+ containerId: container.metadata.commentContainerId,
80
+ containerType: container.type,
81
+ childIndex: children.length,
82
+ beforeMeaningfulChild: !hasMeaningfulChildren,
83
+ };
84
+ }
45
85
 
46
86
  /**
47
87
  * Helper method to get the element name from a JSX identifier or member expression
@@ -1083,6 +1123,7 @@ function RipplePlugin(config) {
1083
1123
 
1084
1124
  // Call onComment if it exists
1085
1125
  if (this.options.onComment) {
1126
+ const metadata = this.#createCommentMetadata();
1086
1127
  this.options.onComment(
1087
1128
  false,
1088
1129
  commentText,
@@ -1090,6 +1131,7 @@ function RipplePlugin(config) {
1090
1131
  commentEnd,
1091
1132
  startLoc,
1092
1133
  endLoc,
1134
+ metadata,
1093
1135
  );
1094
1136
  }
1095
1137
 
@@ -1120,6 +1162,7 @@ function RipplePlugin(config) {
1120
1162
 
1121
1163
  // Call onComment if it exists
1122
1164
  if (this.options.onComment) {
1165
+ const metadata = this.#createCommentMetadata();
1123
1166
  this.options.onComment(
1124
1167
  true,
1125
1168
  commentText,
@@ -1127,6 +1170,7 @@ function RipplePlugin(config) {
1127
1170
  commentEnd,
1128
1171
  startLoc,
1129
1172
  endLoc,
1173
+ metadata,
1130
1174
  );
1131
1175
  }
1132
1176
 
@@ -1246,7 +1290,8 @@ function RipplePlugin(config) {
1246
1290
  }
1247
1291
 
1248
1292
  element.attributes = open.attributes;
1249
- element.metadata = {};
1293
+ element.metadata ??= {};
1294
+ element.metadata.commentContainerId = ++this.#commentContextId;
1250
1295
 
1251
1296
  if (element.selfClosing) {
1252
1297
  this.#path.pop();
@@ -1643,7 +1688,7 @@ function RipplePlugin(config) {
1643
1688
  */
1644
1689
  function get_comment_handlers(source, comments, index = 0) {
1645
1690
  return {
1646
- onComment: (block, value, start, end, start_loc, end_loc) => {
1691
+ onComment: (block, value, start, end, start_loc, end_loc, metadata) => {
1647
1692
  if (block && /\n/.test(value)) {
1648
1693
  let a = start;
1649
1694
  while (a > 0 && source[a - 1] !== '\n') a -= 1;
@@ -1664,6 +1709,7 @@ function get_comment_handlers(source, comments, index = 0) {
1664
1709
  start: /** @type {import('acorn').Position} */ (start_loc),
1665
1710
  end: /** @type {import('acorn').Position} */ (end_loc),
1666
1711
  },
1712
+ context: metadata ?? null,
1667
1713
  });
1668
1714
  },
1669
1715
  add_comments: (ast) => {
@@ -1671,40 +1717,85 @@ function get_comment_handlers(source, comments, index = 0) {
1671
1717
 
1672
1718
  comments = comments
1673
1719
  .filter((comment) => comment.start >= index)
1674
- .map(({ type, value, start, end, loc }) => ({ type, value, start, end, loc }));
1720
+ .map(({ type, value, start, end, loc, context }) => ({ type, value, start, end, loc, context }));
1675
1721
 
1676
1722
  walk(ast, null, {
1677
1723
  _(node, { next, path }) {
1678
1724
  let comment;
1679
1725
 
1726
+ const metadata = /** @type {{ commentContainerId?: number, elementLeadingComments?: CommentWithLocation[] }} */ (node?.metadata);
1727
+
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
+ }
1738
+ }
1739
+
1680
1740
  while (comments[0] && comments[0].start < node.start) {
1681
1741
  comment = /** @type {CommentWithLocation} */ (comments.shift());
1682
- (node.leadingComments ||= []).push(comment);
1683
- }
1742
+ if (comment.loc) {
1743
+ const ancestorElements = path
1744
+ .filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
1745
+ .sort((a, b) => a.loc.start.line - b.loc.start.line);
1684
1746
 
1685
- next();
1747
+ const targetAncestor = ancestorElements.find((ancestor) => comment.loc.start.line < ancestor.loc.start.line);
1686
1748
 
1687
- if (comments[0]) {
1688
- if (node.type === 'BlockStatement' && node.body.length === 0) {
1689
- if (comments[0].start < node.end && comments[0].end < node.end) {
1690
- comment = /** @type {CommentWithLocation} */ (comments.shift());
1691
- (node.innerComments ||= []).push(comment);
1692
- return;
1749
+ if (targetAncestor) {
1750
+ targetAncestor.metadata ??= {};
1751
+ (targetAncestor.metadata.elementLeadingComments ||= []).push(comment);
1752
+ continue;
1693
1753
  }
1694
1754
  }
1695
- const parent = /** @type {any} */ (path.at(-1));
1755
+ (node.leadingComments ||= []).push(comment);
1756
+ }
1757
+
1758
+ next();
1696
1759
 
1697
- if (parent === undefined || node.end !== parent.end) {
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) {
1698
1780
  const slice = source.slice(node.end, comments[0].start);
1699
- const is_last_in_body =
1700
- ((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
1701
- parent.body.indexOf(node) === parent.body.length - 1) ||
1702
- (parent?.type === 'ArrayExpression' &&
1703
- parent.elements.indexOf(node) === parent.elements.length - 1) ||
1704
- (parent?.type === 'ObjectExpression' &&
1705
- parent.properties.indexOf(node) === parent.properties.length - 1);
1706
-
1707
- if (is_last_in_body) {
1781
+
1782
+ // Check if this node is the last item in an array-like structure
1783
+ let is_last_in_array = false;
1784
+ let array_prop = null;
1785
+
1786
+ if (parent?.type === 'BlockStatement' || parent?.type === 'Program' || parent?.type === 'Component') {
1787
+ array_prop = 'body';
1788
+ } else if (parent?.type === 'ArrayExpression') {
1789
+ array_prop = 'elements';
1790
+ } else if (parent?.type === 'ObjectExpression') {
1791
+ array_prop = 'properties';
1792
+ }
1793
+
1794
+ if (array_prop && Array.isArray(parent[array_prop])) {
1795
+ is_last_in_array = parent[array_prop].indexOf(node) === parent[array_prop].length - 1;
1796
+ }
1797
+
1798
+ if (is_last_in_array) {
1708
1799
  // Special case: There can be multiple trailing comments after the last node in a block,
1709
1800
  // and they can be separated by newlines
1710
1801
  let end = node.end;
@@ -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);
@@ -556,6 +556,10 @@ const visitors = {
556
556
  return b.id(node.name);
557
557
  },
558
558
 
559
+ JSXExpressionContainer(node, context) {
560
+ return context.visit(node.expression);
561
+ },
562
+
559
563
  JSXElement(node, context) {
560
564
  const name = node.openingElement.name;
561
565
  const attributes = node.openingElement.attributes;
@@ -586,7 +590,7 @@ const visitors = {
586
590
  }
587
591
 
588
592
  return b.call(
589
- '_$_jsx',
593
+ '__compat.jsx',
590
594
  name.type === 'JSXIdentifier' && name.name[0].toLowerCase() === name.name[0]
591
595
  ? b.literal(name.name)
592
596
  : context.visit(name),
@@ -2064,9 +2068,16 @@ function transform_body(body, { visit, state }) {
2064
2068
  function create_tsx_with_typescript_support() {
2065
2069
  const base_tsx = tsx();
2066
2070
 
2067
- // Override the ArrowFunctionExpression handler to support TypeScript return types
2071
+ // Add custom TypeScript node handlers that aren't in tsx
2068
2072
  return {
2069
2073
  ...base_tsx,
2074
+ // Custom handler for TSParenthesizedType: (Type)
2075
+ TSParenthesizedType(node, context) {
2076
+ context.write('(');
2077
+ context.visit(node.typeAnnotation);
2078
+ context.write(')');
2079
+ },
2080
+ // Override the ArrowFunctionExpression handler to support TypeScript return types
2070
2081
  ArrowFunctionExpression(node, context) {
2071
2082
  if (node.async) context.write('async ');
2072
2083
 
@@ -1,4 +1,4 @@
1
- /** @import { Block } from '#client' */
1
+ /** @import { Block, CompatOptions } from '#client' */
2
2
 
3
3
  import { destroy_block, root } from './internal/client/blocks.js';
4
4
  import { handle_root_events } from './internal/client/events.js';
@@ -12,7 +12,7 @@ export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
12
12
 
13
13
  /**
14
14
  * @param {(anchor: Node, props: Record<string, any>, active_block: Block | null) => void} component
15
- * @param {{ props?: Record<string, any>, target: HTMLElement }} options
15
+ * @param {{ props?: Record<string, any>, target: HTMLElement, compat?: CompatOptions }} options
16
16
  * @returns {() => void}
17
17
  */
18
18
  export function mount(component, options) {
@@ -34,7 +34,7 @@ export function mount(component, options) {
34
34
 
35
35
  const _root = root(() => {
36
36
  component(anchor, props, active_block);
37
- });
37
+ }, options.compat);
38
38
 
39
39
  return () => {
40
40
  cleanup_events();
@@ -1,4 +1,4 @@
1
- /** @import { Block, Derived } from '#client' */
1
+ /** @import { Block, Derived, CompatOptions } from '#client' */
2
2
 
3
3
  import {
4
4
  BLOCK_HAS_RUN,
@@ -125,10 +125,17 @@ export function ref(element, get_fn) {
125
125
 
126
126
  /**
127
127
  * @param {() => void} fn
128
+ * @param {CompatOptions} [compat]
128
129
  * @returns {Block}
129
130
  */
130
- export function root(fn) {
131
- return block(ROOT_BLOCK, fn);
131
+ export function root(fn, compat) {
132
+ if (compat != null) {
133
+ for (var key in compat) {
134
+ var api = compat[key];
135
+ api.createRoot();
136
+ }
137
+ }
138
+ return block(ROOT_BLOCK, fn, { compat });
132
139
  }
133
140
 
134
141
  /**
@@ -1,8 +1,40 @@
1
+ /** @import { CompatApi } from '#client' */
2
+
3
+ import { ROOT_BLOCK } from "./constants";
4
+ import { active_block } from "./runtime";
5
+
1
6
  /**
2
- * @param {string} kind
3
- * @param {Node} node
4
- * @param {() => JSX.Element[]} children_fn
7
+ * @param {string} kind
8
+ * @returns {CompatApi | null}
9
+ */
10
+ function get_compat_from_root(kind) {
11
+ var current = active_block;
12
+
13
+ while (current !== null) {
14
+ if ((current.f & ROOT_BLOCK) !== 0) {
15
+ var api = current.s.compat[kind];
16
+
17
+ if (api != null) {
18
+ return api;
19
+ }
20
+ }
21
+ current = current.p;
22
+ }
23
+
24
+ return null;
25
+ }
26
+
27
+ /**
28
+ * @param {string} kind
29
+ * @param {Node} node
30
+ * @param {() => JSX.Element[]} children_fn
5
31
  */
6
32
  export function tsx_compat(kind, node, children_fn) {
7
- throw new Error("Not implemented yet");
8
- }
33
+ var compat = get_compat_from_root(kind);
34
+
35
+ if (compat == null) {
36
+ throw new Error(`No compat API found for kind "${kind}"`);
37
+ }
38
+
39
+ compat.createComponent(node, children_fn);
40
+ }
@@ -53,3 +53,13 @@ export type Block = {
53
53
  // teardown function
54
54
  t: (() => {}) | null;
55
55
  };
56
+
57
+ export type CompatApi = {
58
+ createRoot: () => void;
59
+ createComponent: (node: any, children_fn: () => any) => void;
60
+ jsx: (type: any, props: any) => any;
61
+ }
62
+
63
+ export type CompatOptions = {
64
+ [key: string]: CompatApi;
65
+ }
@@ -0,0 +1,108 @@
1
+ import { compile } from 'ripple/compiler';
2
+ import { track } from 'ripple';
3
+
4
+ describe('Compiler: Tracked Object Direct Access Checks', () => {
5
+
6
+ it('should error on direct access to __v of a tracked object', () => {
7
+ const code = `
8
+ export default component App() {
9
+ let count = track(0);
10
+ console.log(count.__v);
11
+ }
12
+ `;
13
+ expect(() => compile(code, 'test.ripple')).toThrow(/Directly accessing internal property "__v" of a tracked object is not allowed/);
14
+ });
15
+
16
+ it('should error on direct access to "a" (get/set config) of a tracked object', () => {
17
+ const code = `
18
+ export default component App() {
19
+ let myTracked = track(0);
20
+ console.log(myTracked.a);
21
+ }
22
+ `;
23
+ expect(() => compile(code, 'test.ripple')).toThrow(/Directly accessing internal property "a" of a tracked object is not allowed/);
24
+ });
25
+
26
+ it('should error on direct access to "b" (block) of a tracked object', () => {
27
+ const code = `
28
+ export default component App() {
29
+ let myTracked = track(0);
30
+ console.log(myTracked.b);
31
+ }
32
+ `;
33
+ expect(() => compile(code, 'test.ripple')).toThrow(/Directly accessing internal property "b" of a tracked object is not allowed/);
34
+ });
35
+
36
+ it('should error on direct access to "c" (clock) of a tracked object', () => {
37
+ const code = `
38
+ export default component App() {
39
+ let myTracked = track(0);
40
+ console.log(myTracked.c);
41
+ }
42
+ `;
43
+ expect(() => compile(code, 'test.ripple')).toThrow(/Directly accessing internal property "c" of a tracked object is not allowed/);
44
+ });
45
+
46
+ it('should error on direct access to "f" (flags) of a tracked object', () => {
47
+ const code = `
48
+ export default component App() {
49
+ let myTracked = track(0);
50
+ console.log(myTracked.f);
51
+ }
52
+ `;
53
+ expect(() => compile(code, 'test.ripple')).toThrow(/Directly accessing internal property "f" of a tracked object is not allowed/);
54
+ });
55
+
56
+ it('should compile successfully with correct @ syntax access', () => {
57
+ const code = `
58
+ export default component App() {
59
+ let count = track(0);
60
+ console.log(@count);
61
+ }
62
+ `;
63
+ expect(() => compile(code, 'test.ripple')).not.toThrow();
64
+ });
65
+
66
+ it('should compile successfully with correct get() function access', () => {
67
+ const code = `
68
+ import { get, track } from 'ripple';
69
+ export default component App() {
70
+ let count = track(0);
71
+ console.log(get(count));
72
+ }
73
+ `;
74
+ expect(() => compile(code, 'test.ripple')).not.toThrow();
75
+ });
76
+
77
+ it('should not error on accessing __v of a non-tracked object', () => {
78
+ const code = `
79
+ export default component App() {
80
+ let obj = { __v: 123 };
81
+ console.log(obj.__v);
82
+ }
83
+ `;
84
+ expect(() => compile(code, 'test.ripple')).not.toThrow();
85
+ });
86
+
87
+ it('should not error on accessing __v of a non-tracked object (member expression)', () => {
88
+ const code = `
89
+ export default component App() {
90
+ let data = { value: { __v: 456 } };
91
+ console.log(data.value.__v);
92
+ }
93
+ `;
94
+ expect(() => compile(code, 'test.ripple')).not.toThrow();
95
+ });
96
+
97
+ it('should not error on accessing a property named like an internal one on a non-tracked object', () => {
98
+ const code = `
99
+ export default component App() {
100
+ let config = { a: 'some_value', b: 'another_value' };
101
+ console.log(config.a);
102
+ console.log(config.b);
103
+ }
104
+ `;
105
+ expect(() => compile(code, 'test.ripple')).not.toThrow();
106
+ });
107
+
108
+ });