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 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.157",
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.157"
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 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);
@@ -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
- if (parent_block !== null && parent_block.type !== 'Component') {
826
- if (context.state.inside_server_block === false) {
827
- error(
828
- '`await` is not allowed in client-side control-flow statements',
829
- context.state.analysis.module.filename,
830
- node,
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
- 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) {
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.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
+ }
2285
2295
  } else {
2286
2296
  const id = flush_node();
2287
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
  });