ripple 0.3.9 → 0.3.11

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +2 -2
  3. package/src/compiler/errors.js +1 -1
  4. package/src/compiler/index.d.ts +3 -1
  5. package/src/compiler/phases/1-parse/index.js +195 -23
  6. package/src/compiler/phases/2-analyze/index.js +266 -108
  7. package/src/compiler/phases/2-analyze/prune.js +13 -5
  8. package/src/compiler/phases/3-transform/client/index.js +304 -80
  9. package/src/compiler/phases/3-transform/server/index.js +108 -43
  10. package/src/compiler/types/index.d.ts +28 -3
  11. package/src/compiler/types/parse.d.ts +3 -1
  12. package/src/compiler/utils.js +275 -1
  13. package/src/runtime/element.js +39 -0
  14. package/src/runtime/index-client.js +14 -4
  15. package/src/runtime/internal/client/composite.js +10 -6
  16. package/src/runtime/internal/client/expression.js +280 -0
  17. package/src/runtime/internal/client/index.js +4 -0
  18. package/src/runtime/internal/client/portal.js +12 -6
  19. package/src/runtime/internal/server/index.js +26 -1
  20. package/src/utils/builders.js +30 -0
  21. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
  22. package/tests/client/basic/basic.components.test.ripple +85 -87
  23. package/tests/client/basic/basic.errors.test.ripple +4 -8
  24. package/tests/client/basic/basic.rendering.test.ripple +27 -10
  25. package/tests/client/capture-error.js +12 -0
  26. package/tests/client/compiler/compiler.basic.test.ripple +76 -6
  27. package/tests/client/composite/composite.props.test.ripple +1 -3
  28. package/tests/client/composite/composite.render.test.ripple +91 -13
  29. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  30. package/tests/client/return.test.ripple +101 -0
  31. package/tests/client/svg.test.ripple +4 -4
  32. package/tests/client/tsx.test.ripple +486 -0
  33. package/tests/hydration/basic.test.js +23 -0
  34. package/tests/hydration/compiled/client/basic.js +111 -75
  35. package/tests/hydration/compiled/client/composite.js +81 -46
  36. package/tests/hydration/compiled/client/events.js +18 -63
  37. package/tests/hydration/compiled/client/for.js +90 -183
  38. package/tests/hydration/compiled/client/head.js +10 -25
  39. package/tests/hydration/compiled/client/hmr.js +10 -13
  40. package/tests/hydration/compiled/client/html.js +251 -380
  41. package/tests/hydration/compiled/client/if-children.js +35 -45
  42. package/tests/hydration/compiled/client/if.js +2 -2
  43. package/tests/hydration/compiled/client/mixed-control-flow.js +24 -72
  44. package/tests/hydration/compiled/client/nested-control-flow.js +115 -391
  45. package/tests/hydration/compiled/client/portal.js +8 -20
  46. package/tests/hydration/compiled/client/reactivity.js +14 -47
  47. package/tests/hydration/compiled/client/return.js +2 -5
  48. package/tests/hydration/compiled/client/try.js +4 -4
  49. package/tests/hydration/compiled/server/basic.js +64 -31
  50. package/tests/hydration/compiled/server/composite.js +62 -29
  51. package/tests/hydration/compiled/server/hmr.js +24 -37
  52. package/tests/hydration/compiled/server/html.js +472 -611
  53. package/tests/hydration/compiled/server/if-children.js +77 -103
  54. package/tests/hydration/compiled/server/portal.js +8 -8
  55. package/tests/hydration/components/basic.ripple +15 -5
  56. package/tests/hydration/components/composite.ripple +13 -1
  57. package/tests/hydration/components/hmr.ripple +1 -3
  58. package/tests/hydration/components/html.ripple +13 -35
  59. package/tests/hydration/components/if-children.ripple +4 -8
  60. package/tests/hydration/composite.test.js +11 -0
  61. package/tests/server/basic.attributes.test.ripple +50 -0
  62. package/tests/server/basic.components.test.ripple +22 -28
  63. package/tests/server/basic.test.ripple +12 -0
  64. package/tests/server/compiler.test.ripple +25 -8
  65. package/tests/server/composite.props.test.ripple +1 -3
  66. package/tests/server/style-identifier.test.ripple +2 -4
  67. package/tests/utils/compiler-compat-config.test.js +38 -0
  68. package/tests/utils/vite-plugin-config.test.js +113 -0
  69. package/tsconfig.typecheck.json +2 -1
  70. package/types/index.d.ts +8 -11
@@ -7,21 +7,20 @@ import type {
7
7
  PropsWithChildrenOptional,
8
8
  } from 'ripple';
9
9
  import { flushSync, track } from 'ripple';
10
+ import { did_error } from '../capture-error.js';
10
11
 
11
12
  describe('basic client > components & composition', () => {
12
13
  it('renders with component composition and children', () => {
13
14
  component Card(props: PropsWithChildren<{}>) {
14
- <div class="card">
15
- <props.children />
16
- </div>
15
+ <div class="card">{props.children}</div>
17
16
  }
18
17
 
19
18
  component Basic() {
20
- <Card>
21
- component children() {
22
- <p>{'Card content here'}</p>
23
- }
24
- </Card>
19
+ component children() {
20
+ <p>{'Card content here'}</p>
21
+ }
22
+
23
+ <Card {children} />
25
24
  }
26
25
 
27
26
  render(Basic);
@@ -37,17 +36,17 @@ describe('basic client > components & composition', () => {
37
36
  component Card(props: PropsWithChildrenOptional<{ test?: Component }>) {
38
37
  <div class="card">
39
38
  if (props.children) {
40
- <props.children />
39
+ {props.children}
41
40
  }
42
41
  </div>
43
42
  }
44
43
 
45
44
  component Basic() {
46
- <Card>
47
- component test() {
48
- <p>{'Card content here'}</p>
49
- }
50
- </Card>
45
+ component test() {
46
+ <p>{'Card content here'}</p>
47
+ }
48
+
49
+ <Card {test} />
51
50
  }
52
51
 
53
52
  render(Basic);
@@ -59,22 +58,23 @@ describe('basic client > components & composition', () => {
59
58
  expect(paragraph).toBeFalsy();
60
59
  });
61
60
 
62
- it('allows tracked variable and slot component with same name in nested scope', () => {
61
+ it('allows tracked variables alongside explicit component props', () => {
63
62
  component Card(props: PropsWithChildrenOptional<{ test?: Component }>) {
64
63
  <div class="card">
65
64
  if (props.children) {
66
- <props.children />
65
+ {props.children}
67
66
  }
68
67
  </div>
69
68
  }
70
69
 
71
70
  component Basic() {
72
71
  let &[test] = track(false);
73
- <Card>
74
- component test() {
75
- <p>{'Card content here'}</p>
76
- }
77
- </Card>
72
+
73
+ component TestSlot() {
74
+ <p>{'Card content here'}</p>
75
+ }
76
+
77
+ <Card test={TestSlot} />
78
78
  <div>{test ? 'yes' : 'no'}</div>
79
79
  }
80
80
 
@@ -90,9 +90,7 @@ describe('basic client > components & composition', () => {
90
90
 
91
91
  it('renders a component when children is set a component prop', () => {
92
92
  component Card(props: PropsWithChildren<{}>) {
93
- <div class="card">
94
- <props.children />
95
- </div>
93
+ <div class="card">{props.children}</div>
96
94
  }
97
95
 
98
96
  component Basic() {
@@ -211,6 +209,28 @@ describe('basic client > components & composition', () => {
211
209
  expect(countDiv.textContent).toBe('3');
212
210
  });
213
211
 
212
+ it('updates explicit text children props reactively', () => {
213
+ component TextProp(&{ children }: PropsWithChildren<{}>) {
214
+ <div class="text-prop">{children}</div>
215
+ }
216
+
217
+ component Basic() {
218
+ let &[show] = track(false);
219
+
220
+ <TextProp children={show ? 'hello' : ''} />
221
+ <button class="show-text" onClick={() => (show = true)}>{'Show'}</button>
222
+ }
223
+
224
+ render(Basic);
225
+
226
+ expect(container.querySelector('.text-prop')?.textContent).toBe('');
227
+
228
+ container.querySelector('.show-text')?.click();
229
+ flushSync();
230
+
231
+ expect(container.querySelector('.text-prop')?.textContent).toBe('hello');
232
+ });
233
+
214
234
  it('it retains this context with bracketed prop functions and keeps original chaining', () => {
215
235
  component App() {
216
236
  const SYMBOL_PROP = Symbol();
@@ -233,60 +253,40 @@ describe('basic client > components & composition', () => {
233
253
 
234
254
  const obj2 = null;
235
255
 
256
+ function trigger_nonexistent() {
257
+ hasError = did_error(() => {
258
+ // @ts-ignore
259
+ obj['nonexistent']();
260
+ });
261
+ }
262
+
263
+ function trigger_nonexistent_chaining() {
264
+ hasError = did_error(() => {
265
+ // @ts-ignore
266
+ obj['nonexistent']?.();
267
+ });
268
+ }
269
+
270
+ function trigger_object_null() {
271
+ hasError = did_error(() => {
272
+ // @ts-ignore
273
+ obj2['nonexistent']();
274
+ });
275
+ }
276
+
277
+ function trigger_object_null_chained() {
278
+ hasError = did_error(() => {
279
+ // @ts-ignore
280
+ obj2?.['nonexistent']?.();
281
+ });
282
+ }
283
+
236
284
  <button onClick={() => obj['increment']()}>{'Increment'}</button>
237
285
  <button onClick={() => obj[SYMBOL_PROP]()}>{'Increment'}</button>
238
- <button
239
- onClick={() => {
240
- hasError = false;
241
- try {
242
- // @ts-ignore
243
- obj['nonexistent']();
244
- } catch {
245
- hasError = true;
246
- }
247
- }}
248
- >
249
- {'Nonexistent'}
250
- </button>
251
- <button
252
- onClick={() => {
253
- hasError = false;
254
- try {
255
- // @ts-ignore
256
- obj['nonexistent']?.();
257
- } catch {
258
- hasError = true;
259
- }
260
- }}
261
- >
262
- {'Nonexistent chaining'}
263
- </button>
264
- <button
265
- onClick={() => {
266
- hasError = false;
267
- try {
268
- // @ts-ignore
269
- obj2['nonexistent']();
270
- } catch {
271
- hasError = true;
272
- }
273
- }}
274
- >
275
- {'Object null'}
276
- </button>
277
- <button
278
- onClick={() => {
279
- hasError = false;
280
- try {
281
- // @ts-ignore
282
- obj2?.['nonexistent']?.();
283
- } catch {
284
- hasError = true;
285
- }
286
- }}
287
- >
288
- {'Object null chained'}
289
- </button>
286
+ <button onClick={trigger_nonexistent}>{'Nonexistent'}</button>
287
+ <button onClick={trigger_nonexistent_chaining}>{'Nonexistent chaining'}</button>
288
+ <button onClick={trigger_object_null}>{'Object null'}</button>
289
+ <button onClick={trigger_object_null_chained}>{'Object null chained'}</button>
290
290
  <button onClick={() => obj.arr[obj.arr.length - 1]()}>{'BinaryExpression prop'}</button>
291
291
 
292
292
  <span>{obj.count.value}</span>
@@ -346,21 +346,19 @@ describe('basic client > components & composition', () => {
346
346
  <span>{'Hello from Span'}</span>
347
347
  },
348
348
  button: component({ children }: PropsWithChildren<{}>) {
349
- <button>
350
- <children />
351
- </button>
349
+ <button>{children}</button>
352
350
  },
353
351
  };
354
352
 
355
353
  component App() {
354
+ component children() {
355
+ <span>{'Click me!'}</span>
356
+ }
357
+
356
358
  <div>
357
359
  <h1>{'Component as Property Test'}</h1>
358
360
  <UI.span />
359
- <UI.button>
360
- component children() {
361
- <span>{'Click me!'}</span>
362
- }
363
- </UI.button>
361
+ <UI.button {children} />
364
362
  </div>
365
363
  }
366
364
 
@@ -378,13 +376,13 @@ describe('basic client > components & composition', () => {
378
376
 
379
377
  it('handles empty string children', () => {
380
378
  component Button({ children }: PropsWithChildren<{}>) {
381
- <children />
379
+ {children}
382
380
  }
383
381
 
384
382
  component App() {
385
- let text = '';
383
+ let content = '';
386
384
  <Button>{''}</Button>
387
- <Button>{text}</Button>
385
+ <Button>{content}</Button>
388
386
  }
389
387
 
390
388
  expect(() => {
@@ -70,9 +70,7 @@ describe('basic client > errors', () => {
70
70
 
71
71
  expect(() => {
72
72
  compile(code, 'test.ripple');
73
- }).toThrow(
74
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
75
- );
73
+ }).not.toThrow();
76
74
  });
77
75
 
78
76
  it('should throw error for interpolating props.children as text', () => {
@@ -84,9 +82,7 @@ describe('basic client > errors', () => {
84
82
 
85
83
  expect(() => {
86
84
  compile(code, 'test.ripple');
87
- }).toThrow(
88
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
89
- );
85
+ }).not.toThrow();
90
86
  });
91
87
 
92
88
  it('should throw error for calling children as a function', () => {
@@ -99,7 +95,7 @@ describe('basic client > errors', () => {
99
95
  expect(() => {
100
96
  compile(code, 'test.ripple');
101
97
  }).toThrow(
102
- '`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
98
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
103
99
  );
104
100
  });
105
101
 
@@ -113,7 +109,7 @@ describe('basic client > errors', () => {
113
109
  expect(() => {
114
110
  compile(code, 'test.ripple');
115
111
  }).toThrow(
116
- '`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
112
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
117
113
  );
118
114
  });
119
115
 
@@ -15,9 +15,9 @@ describe('basic client > rendering & text', () => {
15
15
 
16
16
  it('renders semi-dynamic text', () => {
17
17
  component Basic() {
18
- let text = 'Hello World';
18
+ let message = 'Hello World';
19
19
 
20
- <div>{text}</div>
20
+ <div>{message}</div>
21
21
  }
22
22
 
23
23
  render(Basic);
@@ -25,18 +25,33 @@ describe('basic client > rendering & text', () => {
25
25
  expect(container).toMatchSnapshot();
26
26
  });
27
27
 
28
+ it('renders explicit text interpolation without creating HTML', () => {
29
+ component Basic() {
30
+ let markup = '<span>Not HTML</span>';
31
+
32
+ <div>{text markup}</div>
33
+ }
34
+
35
+ render(Basic);
36
+
37
+ const div = container.querySelector('div');
38
+
39
+ expect(div.textContent).toBe('<span>Not HTML</span>');
40
+ expect(div.querySelector('span')).toBeNull();
41
+ });
42
+
28
43
  it('renders dynamic text', () => {
29
44
  component Basic() {
30
- let &[text] = track('Hello World');
45
+ let &[message] = track('Hello World');
31
46
 
32
47
  <button
33
48
  onClick={() => {
34
- text = 'Hello Ripple';
49
+ message = 'Hello Ripple';
35
50
  }}
36
51
  >
37
52
  {'Change Text'}
38
53
  </button>
39
- <div>{text}</div>
54
+ <div>{message}</div>
40
55
  }
41
56
 
42
57
  render(Basic);
@@ -70,13 +85,13 @@ describe('basic client > rendering & text', () => {
70
85
  it('renders tick template literal for nested children', () => {
71
86
  component Child({ level, children }: { level: number; children: any }) {
72
87
  if (level == 1) {
73
- <h1><children /></h1>
88
+ <h1>{children}</h1>
74
89
  }
75
90
  if (level == 2) {
76
- <h2><children /></h2>
91
+ <h2>{children}</h2>
77
92
  }
78
93
  if (level == 3) {
79
- <h3><children /></h3>
94
+ <h3>{children}</h3>
80
95
  }
81
96
  }
82
97
 
@@ -152,8 +167,10 @@ describe('basic client > rendering & text', () => {
152
167
  it('basic operations', () => {
153
168
  component App() {
154
169
  let &[count] = track(0);
155
- <div>{count++}</div>
156
- <div>{++count}</div>
170
+ const a = count++;
171
+ const b = ++count;
172
+ <div>{a}</div>
173
+ <div>{b}</div>
157
174
  <div>{5}</div>
158
175
  <div>{count}</div>
159
176
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @param {() => void} fn
3
+ * @returns {boolean}
4
+ */
5
+ export function did_error(fn) {
6
+ try {
7
+ fn();
8
+ return false;
9
+ } catch {
10
+ return true;
11
+ }
12
+ }
@@ -44,6 +44,40 @@ describe('compiler > basics', () => {
44
44
  ).toEqual(style3);
45
45
  });
46
46
 
47
+ it('parses explicit text interpolation and reserves the text keyword', () => {
48
+ const source = `export component App() {
49
+ const markup = '<span>Not HTML</span>';
50
+
51
+ <div>{markup}</div>
52
+ <div>{text markup}</div>
53
+ }`;
54
+
55
+ const ast = parse(source);
56
+ const component_node = (ast.body[0] as AST.ExportNamedDeclaration).declaration as unknown as AST.Component;
57
+ const elements = component_node.body.filter((node) => node.type === 'Element') as AST.Element[];
58
+ const expression = elements[0].children[0] as AST.Node & { expression: AST.Expression };
59
+ const explicit_text = elements[1].children[0] as AST.TextNode;
60
+
61
+ expect(elements).toHaveLength(2);
62
+ expect(expression.type).toBe('RippleExpression');
63
+ expect((expression.expression as AST.Identifier).name).toBe('markup');
64
+ expect(explicit_text.type).toBe('Text');
65
+ expect((explicit_text.expression as AST.Identifier).name).toBe('markup');
66
+
67
+ const { js } = compile(source, 'text-directive.ripple', { mode: 'client' });
68
+ expect(js.code).not.toContain('_$_.html');
69
+
70
+ const invalid_source = `export component App() {
71
+ const text = 'plain';
72
+
73
+ <div>{text}</div>
74
+ }`;
75
+
76
+ expect(() => parse(invalid_source)).toThrow(
77
+ '"text" is a Ripple keyword and must be used in the form {text some_value}',
78
+ );
79
+ });
80
+
47
81
  it('renders without crashing', () => {
48
82
  component App() {
49
83
  let foo: Record<string, number>;
@@ -433,16 +467,16 @@ export component App() {
433
467
  const code = `
434
468
  component Card(props) {
435
469
  <div class="card">
436
- <props.children />
470
+ {props.children}
437
471
  </div>
438
472
  }
439
473
 
440
474
  export component App() {
441
- <Card>
442
475
  component children() {
443
476
  <p>{'Card content here'}</p>
444
477
  }
445
- </Card>
478
+
479
+ <Card {children} />
446
480
 
447
481
  const test = 5;
448
482
 
@@ -452,7 +486,7 @@ export component App() {
452
486
  expect(() => compile(code, 'test.ripple')).not.toThrow();
453
487
  });
454
488
 
455
- it('converts nested component children into props in Volar mappings', () => {
489
+ it('rejects component declarations inside composite children', () => {
456
490
  const source = `
457
491
  export component App() {
458
492
  <ark.div class="host-class" data-value="42">
@@ -461,12 +495,48 @@ export component App() {
461
495
  }
462
496
  </ark.div>
463
497
  }
498
+ `;
499
+
500
+ expect(() => compile_to_volar_mappings(source, 'test.ripple')).toThrow(
501
+ /Component declarations cannot be used inside composite component children/,
502
+ );
503
+ });
504
+
505
+ it('preserves explicit component props in Volar mappings', () => {
506
+ const source = `
507
+ export component App() {
508
+ component asChild({ children, href, ...rest }: { href: string; [key: string]: any }) {
509
+ <a id="aschild-anchor" {href} {...rest} data-extra="yes">{'Link'}</a>
510
+ }
511
+
512
+ <ark.div class="host-class" data-value="42" {asChild} />
513
+ }
464
514
  `;
465
515
  const result = compile_to_volar_mappings(source, 'test.ripple').code;
466
516
 
467
- expect(result).toContain('<ark.div class="host-class" data-value="42" asChild={');
517
+ expect(result).toContain('<ark.div class="host-class" data-value="42" asChild={asChild}');
468
518
  expect(result).not.toContain('children={() =>');
469
- expect(result).not.toContain('function asChild');
519
+ });
520
+
521
+ it('merges explicit children prop with implicit children in client output', () => {
522
+ const source = `
523
+ component Card(props) {
524
+ <div>{props.children}</div>
525
+ }
526
+
527
+ export component App() {
528
+ const fallback = 'fallback';
529
+
530
+ <Card children={fallback}>
531
+ <span>{'content'}</span>
532
+ </Card>
533
+ }
534
+ `;
535
+
536
+ const result = compile(source, 'test.ripple', { mode: 'client' }).js.code;
537
+
538
+ expect((result.match(/children:/g) || []).length).toBe(1);
539
+ expect(result).toContain('children: _$_.normalize_children(fallback) ?? _$_.ripple_element(');
470
540
  });
471
541
 
472
542
  it('should not error on `this` MemberExpression with a UpdateExpression', () => {
@@ -107,9 +107,7 @@ describe('composite > props', () => {
107
107
 
108
108
  it('correctly retains prop accessors and reactivity when using rest props', () => {
109
109
  component Button(&{ children, ...rest }: Props) {
110
- <button {...rest}>
111
- <@children />
112
- </button>
110
+ <button {...rest}>{children}</button>
113
111
  <style>
114
112
  .on {
115
113
  color: blue;
@@ -32,20 +32,22 @@ describe('composite > render', () => {
32
32
  component Button({ A, B, children }: { A: () => void; B: () => void; children: () => void }) {
33
33
  <div>
34
34
  <A />
35
- <children />
35
+ {children}
36
36
  <B />
37
37
  </div>
38
38
  }
39
39
 
40
40
  component App() {
41
- <Button>
42
- component A() {
43
- <div>{'I am A'}</div>
44
- }
41
+ component A() {
42
+ <div>{'I am A'}</div>
43
+ }
44
+
45
+ component B() {
46
+ <div>{'I am B'}</div>
47
+ }
48
+
49
+ <Button {A} {B}>
45
50
  <div>{'other text'}</div>
46
- component B() {
47
- <div>{'I am B'}</div>
48
- }
49
51
  </Button>
50
52
  }
51
53
 
@@ -62,21 +64,51 @@ describe('composite > render', () => {
62
64
  }
63
65
 
64
66
  component Child({ children, ...rest }: { children: string; class: string }) {
65
- <button {...rest}>
66
- <children />
67
- </button>
67
+ <button {...rest}>{children}</button>
68
68
  }
69
69
 
70
70
  render(App);
71
71
  expect(container).toMatchSnapshot();
72
72
  });
73
73
 
74
+ it('renders explicit text around children', () => {
75
+ component Frame({ children }) {
76
+ <div class="frame">
77
+ {text 'before'}
78
+ {children}
79
+ {text 'after'}
80
+ </div>
81
+ }
82
+
83
+ component App() {
84
+ <Frame>
85
+ <span class="middle">{'middle'}</span>
86
+ </Frame>
87
+ }
88
+
89
+ render(App);
90
+
91
+ const frame = /** @type {HTMLDivElement} */ (container.querySelector('.frame'));
92
+ const nodes = Array.from(frame.childNodes).filter(
93
+ (node) => node.nodeType !== Node.COMMENT_NODE,
94
+ );
95
+
96
+ expect(nodes).toHaveLength(3);
97
+ expect(nodes[0].nodeType).toBe(Node.TEXT_NODE);
98
+ expect(nodes[0].textContent).toBe('before');
99
+ expect((/** @type {HTMLElement} */ (nodes[1])).outerHTML).toBe(
100
+ '<span class="middle">middle</span>',
101
+ );
102
+ expect(nodes[2].nodeType).toBe(Node.TEXT_NODE);
103
+ expect(nodes[2].textContent).toBe('after');
104
+ });
105
+
74
106
  it('preserves distinct scoped ripple hashes for wrapper and child content', () => {
75
107
  component App() {
76
108
  component Wrapper({ children }) {
77
109
  <div class="green">
78
110
  {'Wrapper'}
79
- <children />
111
+ {children}
80
112
  </div>
81
113
 
82
114
  <style>
@@ -141,7 +173,7 @@ describe('composite > render', () => {
141
173
  [key: string]: any;
142
174
  }) {
143
175
  <div {...props}>
144
- <children />
176
+ {children}
145
177
  // @ts-expect-error - intentionally testing behavior when a component is undefined
146
178
  <NonExistent />
147
179
  </div>
@@ -160,3 +192,49 @@ describe('composite > render', () => {
160
192
  expect(div.innerHTML).not.toContain('<undefined');
161
193
  });
162
194
  });
195
+
196
+ describe('scoped styles with children', () => {
197
+ it('generates correct CSS hashes for wrapper and child with empty style in App', () => {
198
+ component Wrapper(&{ children }: { children?: Component }) {
199
+ <div class="green">
200
+ {'Wrapper'}
201
+ {children}
202
+ </div>
203
+
204
+ <style>
205
+ .green {
206
+ color: green;
207
+ }
208
+ </style>
209
+ }
210
+
211
+ component Child() {
212
+ <div class="red">{'Child'}</div>
213
+
214
+ <style>
215
+ .red {
216
+ color: red;
217
+ }
218
+ </style>
219
+ }
220
+
221
+ component App() {
222
+ <Wrapper>
223
+ <Child />
224
+ </Wrapper>
225
+ <style></style>
226
+ }
227
+
228
+ render(App);
229
+
230
+ const wrapper = container.querySelector('.green');
231
+ const child = container.querySelector('.red');
232
+
233
+ const wrapper_classes = Array.from(wrapper.classList).filter((c) => c.startsWith('ripple-'));
234
+ const child_classes = Array.from(child.classList).filter((c) => c.startsWith('ripple-'));
235
+
236
+ expect(wrapper_classes).toHaveLength(1);
237
+ expect(child_classes).toHaveLength(1);
238
+ expect(wrapper_classes[0]).not.toBe(child_classes[0]);
239
+ });
240
+ });
@@ -353,7 +353,7 @@ export component Test({ children }) {
353
353
  <div>
354
354
  <p class="before">{'before'}</p>
355
355
 
356
- <children />
356
+ {children}
357
357
 
358
358
  <p class="foo">
359
359
  <span>{'foo'}</span>
@@ -379,8 +379,8 @@ export component Test({ children }) {
379
379
  const { css } = compile(source, 'test.ripple');
380
380
 
381
381
  expect(css).toMatch(/\.before\.ripple-[a-z0-9]+ \+ \.foo:where\(\.ripple-[a-z0-9]+\) {/);
382
- expect((css.match(/\.x\ /g) || []).length).toBe(5);
383
- expect((css.match(/\(unused\) :global\(\.x\) /g) || []).length).toBe(1);
382
+ expect((css.match(/\.x\ /g) || []).length).toBe(0);
383
+ expect((css.match(/\(unused\) :global\(\.x\) /g) || []).length).toBe(6);
384
384
  expect(css).toContain('(unused) :global(.x) + .bar {');
385
385
  });
386
386