ripple 0.2.156 → 0.2.158
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 +7 -1
- package/src/compiler/phases/3-transform/client/index.js +25 -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.158",
|
|
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.158"
|
|
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);
|
|
@@ -305,6 +305,13 @@ const visitors = {
|
|
|
305
305
|
};
|
|
306
306
|
},
|
|
307
307
|
|
|
308
|
+
TSNonNullExpression(node, context) {
|
|
309
|
+
if (context.state.to_ts) {
|
|
310
|
+
return context.next();
|
|
311
|
+
}
|
|
312
|
+
return context.visit(node.expression);
|
|
313
|
+
},
|
|
314
|
+
|
|
308
315
|
CallExpression(node, context) {
|
|
309
316
|
if (!context.state.to_ts) {
|
|
310
317
|
delete node.typeArguments;
|
|
@@ -2193,7 +2200,17 @@ function transform_children(children, context) {
|
|
|
2193
2200
|
} else if (state.to_ts) {
|
|
2194
2201
|
transform_ts_child(node, { visit, state });
|
|
2195
2202
|
} else {
|
|
2196
|
-
|
|
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) {
|
|
2197
2214
|
create_initial(node);
|
|
2198
2215
|
}
|
|
2199
2216
|
|
|
@@ -2240,9 +2257,6 @@ function transform_children(children, context) {
|
|
|
2240
2257
|
} else if (node.type === 'TsxCompat') {
|
|
2241
2258
|
visit(node, { ...state, flush_node, namespace: state.namespace });
|
|
2242
2259
|
} else if (node.type === 'Html') {
|
|
2243
|
-
const metadata = { tracking: false, await: false };
|
|
2244
|
-
const expression = visit(node.expression, { ...state, metadata });
|
|
2245
|
-
|
|
2246
2260
|
context.state.template.push('<!>');
|
|
2247
2261
|
|
|
2248
2262
|
const id = flush_node();
|
|
@@ -2258,9 +2272,6 @@ function transform_children(children, context) {
|
|
|
2258
2272
|
),
|
|
2259
2273
|
});
|
|
2260
2274
|
} else if (node.type === 'Text') {
|
|
2261
|
-
const metadata = { tracking: false, await: false };
|
|
2262
|
-
const expression = visit(node.expression, { ...state, metadata });
|
|
2263
|
-
|
|
2264
2275
|
if (metadata.tracking) {
|
|
2265
2276
|
state.template.push(' ');
|
|
2266
2277
|
const id = flush_node();
|
|
@@ -2274,7 +2285,13 @@ function transform_children(children, context) {
|
|
|
2274
2285
|
}
|
|
2275
2286
|
} else if (normalized.length === 1) {
|
|
2276
2287
|
if (expression.type === 'Literal') {
|
|
2277
|
-
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
|
+
}
|
|
2278
2295
|
} else {
|
|
2279
2296
|
const id = flush_node();
|
|
2280
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
|
});
|