ripple 0.2.157 → 0.2.159
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 +32 -2
- package/src/compiler/phases/2-analyze/index.js +17 -10
- package/src/compiler/phases/3-transform/client/index.js +18 -8
- package/src/runtime/internal/client/compat.js +2 -2
- package/src/runtime/internal/client/index.js +1 -0
- package/src/runtime/internal/client/render.js +20 -46
- package/tests/client/basic/basic.components.test.ripple +16 -0
- package/tests/client/composite/composite.reactivity.test.ripple +60 -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.159",
|
|
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.159"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -1591,10 +1591,40 @@ function RipplePlugin(config) {
|
|
|
1591
1591
|
if (this.type === tt.braceL) {
|
|
1592
1592
|
const node = this.jsx_parseExpressionContainer();
|
|
1593
1593
|
body.push(node);
|
|
1594
|
-
} else {
|
|
1595
|
-
// Parse
|
|
1594
|
+
} else if (this.type === tstt.jsxTagStart) {
|
|
1595
|
+
// Parse JSX element
|
|
1596
1596
|
const node = super.parseExpression();
|
|
1597
1597
|
body.push(node);
|
|
1598
|
+
} else {
|
|
1599
|
+
const start = this.start;
|
|
1600
|
+
this.pos = start;
|
|
1601
|
+
let text = '';
|
|
1602
|
+
|
|
1603
|
+
while (this.pos < this.input.length) {
|
|
1604
|
+
const ch = this.input.charCodeAt(this.pos);
|
|
1605
|
+
|
|
1606
|
+
// Stop at opening tag, closing tag, or expression
|
|
1607
|
+
if (ch === 60 || ch === 123) {
|
|
1608
|
+
// < or {
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
text += this.input[this.pos];
|
|
1613
|
+
this.pos++;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (text) {
|
|
1617
|
+
const node = {
|
|
1618
|
+
type: 'JSXText',
|
|
1619
|
+
value: text,
|
|
1620
|
+
raw: text,
|
|
1621
|
+
start,
|
|
1622
|
+
end: this.pos,
|
|
1623
|
+
};
|
|
1624
|
+
body.push(node);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
this.next();
|
|
1598
1628
|
}
|
|
1599
1629
|
}
|
|
1600
1630
|
}
|
|
@@ -36,7 +36,8 @@ function mark_control_flow_has_template(path) {
|
|
|
36
36
|
node.type === 'ForOfStatement' ||
|
|
37
37
|
node.type === 'TryStatement' ||
|
|
38
38
|
node.type === 'IfStatement' ||
|
|
39
|
-
node.type === 'SwitchStatement'
|
|
39
|
+
node.type === 'SwitchStatement' ||
|
|
40
|
+
node.type === 'TsxCompat'
|
|
40
41
|
) {
|
|
41
42
|
node.metadata.has_template = true;
|
|
42
43
|
}
|
|
@@ -629,6 +630,11 @@ const visitors = {
|
|
|
629
630
|
);
|
|
630
631
|
},
|
|
631
632
|
|
|
633
|
+
TsxCompat(_, context) {
|
|
634
|
+
mark_control_flow_has_template(context.path);
|
|
635
|
+
return context.next();
|
|
636
|
+
},
|
|
637
|
+
|
|
632
638
|
Element(node, context) {
|
|
633
639
|
const { state, visit, path } = context;
|
|
634
640
|
const is_dom_element = is_element_dom_element(node);
|
|
@@ -815,20 +821,21 @@ const visitors = {
|
|
|
815
821
|
* @param {any} context
|
|
816
822
|
*/
|
|
817
823
|
AwaitExpression(node, context) {
|
|
824
|
+
const parent_block = get_parent_block_node(context);
|
|
825
|
+
|
|
818
826
|
if (is_inside_component(context)) {
|
|
819
827
|
if (context.state.metadata?.await === false) {
|
|
820
828
|
context.state.metadata.await = true;
|
|
821
829
|
}
|
|
822
|
-
}
|
|
823
|
-
const parent_block = get_parent_block_node(context);
|
|
824
830
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
831
|
+
if (parent_block !== null && parent_block.type !== 'Component') {
|
|
832
|
+
if (context.state.inside_server_block === false) {
|
|
833
|
+
error(
|
|
834
|
+
'`await` is not allowed in client-side control-flow statements',
|
|
835
|
+
context.state.analysis.module.filename,
|
|
836
|
+
node,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
832
839
|
}
|
|
833
840
|
}
|
|
834
841
|
|
|
@@ -2200,7 +2200,17 @@ function transform_children(children, context) {
|
|
|
2200
2200
|
} else if (state.to_ts) {
|
|
2201
2201
|
transform_ts_child(node, { visit, state });
|
|
2202
2202
|
} else {
|
|
2203
|
-
|
|
2203
|
+
let metadata;
|
|
2204
|
+
let expression;
|
|
2205
|
+
let isCreateTextOnly = false;
|
|
2206
|
+
if (node.type === 'Text' || node.type === 'Html') {
|
|
2207
|
+
metadata = { tracking: false, await: false };
|
|
2208
|
+
expression = visit(node.expression, { ...state, metadata });
|
|
2209
|
+
isCreateTextOnly =
|
|
2210
|
+
node.type === 'Text' && normalized.length === 1 && expression.type === 'Literal';
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
if (initial === null && root && !isCreateTextOnly) {
|
|
2204
2214
|
create_initial(node);
|
|
2205
2215
|
}
|
|
2206
2216
|
|
|
@@ -2247,9 +2257,6 @@ function transform_children(children, context) {
|
|
|
2247
2257
|
} else if (node.type === 'TsxCompat') {
|
|
2248
2258
|
visit(node, { ...state, flush_node, namespace: state.namespace });
|
|
2249
2259
|
} else if (node.type === 'Html') {
|
|
2250
|
-
const metadata = { tracking: false, await: false };
|
|
2251
|
-
const expression = visit(node.expression, { ...state, metadata });
|
|
2252
|
-
|
|
2253
2260
|
context.state.template.push('<!>');
|
|
2254
2261
|
|
|
2255
2262
|
const id = flush_node();
|
|
@@ -2265,9 +2272,6 @@ function transform_children(children, context) {
|
|
|
2265
2272
|
),
|
|
2266
2273
|
});
|
|
2267
2274
|
} else if (node.type === 'Text') {
|
|
2268
|
-
const metadata = { tracking: false, await: false };
|
|
2269
|
-
const expression = visit(node.expression, { ...state, metadata });
|
|
2270
|
-
|
|
2271
2275
|
if (metadata.tracking) {
|
|
2272
2276
|
state.template.push(' ');
|
|
2273
2277
|
const id = flush_node();
|
|
@@ -2281,7 +2285,13 @@ function transform_children(children, context) {
|
|
|
2281
2285
|
}
|
|
2282
2286
|
} else if (normalized.length === 1) {
|
|
2283
2287
|
if (expression.type === 'Literal') {
|
|
2284
|
-
state.template.
|
|
2288
|
+
if (state.template.length > 0) {
|
|
2289
|
+
state.template.push(escape_html(expression.value));
|
|
2290
|
+
} else {
|
|
2291
|
+
const id = flush_node();
|
|
2292
|
+
state.init.push(b.var(id, b.call('_$_.create_text', expression)));
|
|
2293
|
+
state.final.push(b.stmt(b.call('_$_.append', b.id('__anchor'), id)));
|
|
2294
|
+
}
|
|
2285
2295
|
} else {
|
|
2286
2296
|
const id = flush_node();
|
|
2287
2297
|
state.template.push(' ');
|
|
@@ -122,49 +122,6 @@ export function apply_styles(element, newStyles) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
/**
|
|
126
|
-
* @param {Element} element
|
|
127
|
-
* @param {Record<string, any>} prev
|
|
128
|
-
* @param {Record<string, any>} next
|
|
129
|
-
* @returns {void}
|
|
130
|
-
*/
|
|
131
|
-
export function set_attributes(element, prev, next) {
|
|
132
|
-
let found_enumerable_keys = false;
|
|
133
|
-
|
|
134
|
-
for (const key in next) {
|
|
135
|
-
if (key === 'children') continue;
|
|
136
|
-
found_enumerable_keys = true;
|
|
137
|
-
|
|
138
|
-
let value = next[key];
|
|
139
|
-
if (prev[key] === value && key !== '#class') {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (is_tracked_object(value)) {
|
|
143
|
-
value = get(value);
|
|
144
|
-
}
|
|
145
|
-
set_attribute_helper(element, key, value);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Only if no enumerable keys but attributes object exists
|
|
149
|
-
// This handles spread_props Proxy objects from dynamic elements with {...spread}
|
|
150
|
-
if (!found_enumerable_keys && next) {
|
|
151
|
-
const allKeys = Reflect.ownKeys(next);
|
|
152
|
-
for (const key of allKeys) {
|
|
153
|
-
if (key === 'children') continue;
|
|
154
|
-
if (typeof key === 'symbol') continue; // Skip symbols - handled by apply_element_spread
|
|
155
|
-
|
|
156
|
-
let value = next[key];
|
|
157
|
-
if (prev[key] === value && key !== '#class') {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (is_tracked_object(value)) {
|
|
161
|
-
value = get(value);
|
|
162
|
-
}
|
|
163
|
-
set_attribute_helper(element, key, value);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
125
|
/**
|
|
169
126
|
* Helper function to set a single attribute
|
|
170
127
|
* @param {Element} element
|
|
@@ -298,7 +255,7 @@ export function apply_element_spread(element, fn) {
|
|
|
298
255
|
var effects = {};
|
|
299
256
|
|
|
300
257
|
return () => {
|
|
301
|
-
var next =
|
|
258
|
+
var next = fn();
|
|
302
259
|
|
|
303
260
|
for (let symbol of get_own_property_symbols(effects)) {
|
|
304
261
|
if (!next[symbol]) {
|
|
@@ -319,8 +276,25 @@ export function apply_element_spread(element, fn) {
|
|
|
319
276
|
next[symbol] = ref_fn;
|
|
320
277
|
}
|
|
321
278
|
|
|
322
|
-
|
|
279
|
+
/** @type {Record<string | symbol, any>} */
|
|
280
|
+
const current = {};
|
|
281
|
+
for (const key in next) {
|
|
282
|
+
if (key === 'children') continue;
|
|
323
283
|
|
|
324
|
-
|
|
284
|
+
let value = next[key];
|
|
285
|
+
if (is_tracked_object(value)) {
|
|
286
|
+
value = get(value);
|
|
287
|
+
}
|
|
288
|
+
current[key] = value;
|
|
289
|
+
|
|
290
|
+
if (!(key in prev) || prev[key] !== value) {
|
|
291
|
+
prev[key] = value;
|
|
292
|
+
} else if (key !== '#class') {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
set_attribute_helper(element, key, value);
|
|
297
|
+
}
|
|
298
|
+
prev = current;
|
|
325
299
|
};
|
|
326
300
|
}
|
|
@@ -268,4 +268,20 @@ describe('basic client > components & composition', () => {
|
|
|
268
268
|
expect(span.textContent).toBe('Hello from Span');
|
|
269
269
|
expect(buttonSpan.textContent).toBe('Click me!');
|
|
270
270
|
});
|
|
271
|
+
|
|
272
|
+
it('handles empty string children', () => {
|
|
273
|
+
component Button({ children }) {
|
|
274
|
+
<children />
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
component App() {
|
|
278
|
+
let text = '';
|
|
279
|
+
<Button>{''}</Button>
|
|
280
|
+
<Button>{text}</Button>
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
expect(() => {
|
|
284
|
+
render(App);
|
|
285
|
+
}).not.toThrow();
|
|
286
|
+
});
|
|
271
287
|
});
|
|
@@ -266,4 +266,64 @@ describe('composite > reactivity', () => {
|
|
|
266
266
|
expect(span.textContent).toBe('Counter: 0');
|
|
267
267
|
},
|
|
268
268
|
);
|
|
269
|
+
|
|
270
|
+
it('keeps reactivity on elements for element spreads and adds / removes dynamic props', () => {
|
|
271
|
+
component App() {
|
|
272
|
+
const count = track(0);
|
|
273
|
+
<CounterWrapper {@count} up={() => @count++} />
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
component CounterWrapper(props) {
|
|
277
|
+
const more = #{
|
|
278
|
+
double: track(() => props.@count * 2),
|
|
279
|
+
another: track(0),
|
|
280
|
+
onemore: 100,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
effect(() => {
|
|
284
|
+
props.count;
|
|
285
|
+
if (props.count === 1) {
|
|
286
|
+
delete more.another;
|
|
287
|
+
} else if (props.count === 2) {
|
|
288
|
+
more.another = 0;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
<div>
|
|
293
|
+
<Counter {...props} double={more.double} another={more.another} onemore={more.onemore} />
|
|
294
|
+
</div>
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
component Counter(props) {
|
|
298
|
+
const [count, up, rest] = trackSplit(props, ['count', 'up']);
|
|
299
|
+
<div {...@rest}>{`Counter: ${@count} Double: ${props.@double}`}</div>
|
|
300
|
+
<button onClick={() => @up()}>{'UP'}</button>
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
render(App);
|
|
304
|
+
|
|
305
|
+
const buttonIncrement = container.querySelectorAll('button')[0];
|
|
306
|
+
const div = container.querySelectorAll('div')[1];
|
|
307
|
+
|
|
308
|
+
expect(div.getAttribute('double')).toBe('0');
|
|
309
|
+
expect(div.getAttribute('another')).toBe('0');
|
|
310
|
+
expect(div.getAttribute('onemore')).toBe('100');
|
|
311
|
+
expect(div.textContent).toBe('Counter: 0 Double: 0');
|
|
312
|
+
|
|
313
|
+
buttonIncrement.click();
|
|
314
|
+
flushSync();
|
|
315
|
+
|
|
316
|
+
expect(div.getAttribute('double')).toBe('2');
|
|
317
|
+
expect(div.hasAttribute('another')).toBe(false);
|
|
318
|
+
expect(div.getAttribute('onemore')).toBe('100');
|
|
319
|
+
expect(div.textContent).toBe('Counter: 1 Double: 2');
|
|
320
|
+
|
|
321
|
+
buttonIncrement.click();
|
|
322
|
+
flushSync();
|
|
323
|
+
|
|
324
|
+
expect(div.getAttribute('double')).toBe('4');
|
|
325
|
+
expect(div.getAttribute('another')).toBe('0');
|
|
326
|
+
expect(div.getAttribute('onemore')).toBe('100');
|
|
327
|
+
expect(div.textContent).toBe('Counter: 2 Double: 4');
|
|
328
|
+
});
|
|
269
329
|
});
|