ripple 0.2.133 → 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.133",
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.133"
84
+ "ripple": "0.2.134"
85
85
  }
86
86
  }
@@ -1755,37 +1755,47 @@ function get_comment_handlers(source, comments, index = 0) {
1755
1755
  (node.leadingComments ||= []).push(comment);
1756
1756
  }
1757
1757
 
1758
- next();
1759
-
1760
- if (comments[0]) {
1761
- if (node.type === 'BlockStatement' && node.body.length === 0) {
1762
- if (comments[0].start < node.end && comments[0].end < node.end) {
1763
- comment = /** @type {CommentWithLocation} */ (comments.shift());
1764
- (node.innerComments ||= []).push(comment);
1765
- return;
1758
+ next();
1759
+
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) {
1780
+ const slice = source.slice(node.end, comments[0].start);
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';
1766
1792
  }
1767
- }
1768
- // Handle empty Element nodes the same way as empty BlockStatements
1769
- if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
1770
- if (comments[0].start < node.end && comments[0].end < node.end) {
1771
- comment = /** @type {CommentWithLocation} */ (comments.shift());
1772
- (node.innerComments ||= []).push(comment);
1773
- return;
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;
1774
1796
  }
1775
- }
1776
- const parent = /** @type {any} */ (path.at(-1));
1777
1797
 
1778
- if (parent === undefined || node.end !== parent.end) {
1779
- 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) {
1798
+ if (is_last_in_array) {
1789
1799
  // Special case: There can be multiple trailing comments after the last node in a block,
1790
1800
  // and they can be separated by newlines
1791
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
+ });