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 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.156",
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.156"
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 regular JSX expression (JSXElement, JSXFragment, etc.)
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
- if (initial === null && root) {
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.push(escape_html(expression.value));
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(' ');
@@ -1,7 +1,7 @@
1
1
  /** @import { CompatApi } from '#client' */
2
2
 
3
- import { ROOT_BLOCK } from "./constants";
4
- import { active_block } from "./runtime";
3
+ import { ROOT_BLOCK } from "./constants.js";
4
+ import { active_block } from "./runtime.js";
5
5
 
6
6
  /**
7
7
  * @param {string} kind
@@ -3,6 +3,7 @@ export {
3
3
  child_frag,
4
4
  next_sibling as sibling,
5
5
  document,
6
+ create_text,
6
7
  } from './operations.js';
7
8
 
8
9
  export {
@@ -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 = { ...fn() };
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
- set_attributes(element, prev, next);
279
+ /** @type {Record<string | symbol, any>} */
280
+ const current = {};
281
+ for (const key in next) {
282
+ if (key === 'children') continue;
323
283
 
324
- prev = next;
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
  });