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 +2 -2
- package/src/compiler/phases/1-parse/index.js +38 -28
- package/src/compiler/phases/2-analyze/index.js +35 -1
- package/src/compiler/phases/3-transform/client/index.js +13 -2
- package/src/runtime/index-client.js +3 -3
- package/src/runtime/internal/client/blocks.js +10 -3
- package/src/runtime/internal/client/compat.js +37 -5
- package/src/runtime/internal/client/types.d.ts +10 -0
- package/tests/client/compiler/compiler.tracked-access.test.ripple +108 -0
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.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.
|
|
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
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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
|
-
|
|
1769
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
'
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
* @
|
|
4
|
-
|
|
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
|
-
|
|
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
|
+
});
|