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.
- package/CHANGELOG.md +43 -0
- package/package.json +2 -2
- package/src/compiler/errors.js +1 -1
- package/src/compiler/index.d.ts +3 -1
- package/src/compiler/phases/1-parse/index.js +195 -23
- package/src/compiler/phases/2-analyze/index.js +266 -108
- package/src/compiler/phases/2-analyze/prune.js +13 -5
- package/src/compiler/phases/3-transform/client/index.js +304 -80
- package/src/compiler/phases/3-transform/server/index.js +108 -43
- package/src/compiler/types/index.d.ts +28 -3
- package/src/compiler/types/parse.d.ts +3 -1
- package/src/compiler/utils.js +275 -1
- package/src/runtime/element.js +39 -0
- package/src/runtime/index-client.js +14 -4
- package/src/runtime/internal/client/composite.js +10 -6
- package/src/runtime/internal/client/expression.js +280 -0
- package/src/runtime/internal/client/index.js +4 -0
- package/src/runtime/internal/client/portal.js +12 -6
- package/src/runtime/internal/server/index.js +26 -1
- package/src/utils/builders.js +30 -0
- package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
- package/tests/client/basic/basic.components.test.ripple +85 -87
- package/tests/client/basic/basic.errors.test.ripple +4 -8
- package/tests/client/basic/basic.rendering.test.ripple +27 -10
- package/tests/client/capture-error.js +12 -0
- package/tests/client/compiler/compiler.basic.test.ripple +76 -6
- package/tests/client/composite/composite.props.test.ripple +1 -3
- package/tests/client/composite/composite.render.test.ripple +91 -13
- package/tests/client/css/global-additional-cases.test.ripple +3 -3
- package/tests/client/return.test.ripple +101 -0
- package/tests/client/svg.test.ripple +4 -4
- package/tests/client/tsx.test.ripple +486 -0
- package/tests/hydration/basic.test.js +23 -0
- package/tests/hydration/compiled/client/basic.js +111 -75
- package/tests/hydration/compiled/client/composite.js +81 -46
- package/tests/hydration/compiled/client/events.js +18 -63
- package/tests/hydration/compiled/client/for.js +90 -183
- package/tests/hydration/compiled/client/head.js +10 -25
- package/tests/hydration/compiled/client/hmr.js +10 -13
- package/tests/hydration/compiled/client/html.js +251 -380
- package/tests/hydration/compiled/client/if-children.js +35 -45
- package/tests/hydration/compiled/client/if.js +2 -2
- package/tests/hydration/compiled/client/mixed-control-flow.js +24 -72
- package/tests/hydration/compiled/client/nested-control-flow.js +115 -391
- package/tests/hydration/compiled/client/portal.js +8 -20
- package/tests/hydration/compiled/client/reactivity.js +14 -47
- package/tests/hydration/compiled/client/return.js +2 -5
- package/tests/hydration/compiled/client/try.js +4 -4
- package/tests/hydration/compiled/server/basic.js +64 -31
- package/tests/hydration/compiled/server/composite.js +62 -29
- package/tests/hydration/compiled/server/hmr.js +24 -37
- package/tests/hydration/compiled/server/html.js +472 -611
- package/tests/hydration/compiled/server/if-children.js +77 -103
- package/tests/hydration/compiled/server/portal.js +8 -8
- package/tests/hydration/components/basic.ripple +15 -5
- package/tests/hydration/components/composite.ripple +13 -1
- package/tests/hydration/components/hmr.ripple +1 -3
- package/tests/hydration/components/html.ripple +13 -35
- package/tests/hydration/components/if-children.ripple +4 -8
- package/tests/hydration/composite.test.js +11 -0
- package/tests/server/basic.attributes.test.ripple +50 -0
- package/tests/server/basic.components.test.ripple +22 -28
- package/tests/server/basic.test.ripple +12 -0
- package/tests/server/compiler.test.ripple +25 -8
- package/tests/server/composite.props.test.ripple +1 -3
- package/tests/server/style-identifier.test.ripple +2 -4
- package/tests/utils/compiler-compat-config.test.js +38 -0
- package/tests/utils/vite-plugin-config.test.js +113 -0
- package/tsconfig.typecheck.json +2 -1
- 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
39
|
+
{props.children}
|
|
41
40
|
}
|
|
42
41
|
</div>
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
component Basic() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
65
|
+
{props.children}
|
|
67
66
|
}
|
|
68
67
|
</div>
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
component Basic() {
|
|
72
71
|
let &[test] = track(false);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
379
|
+
{children}
|
|
382
380
|
}
|
|
383
381
|
|
|
384
382
|
component App() {
|
|
385
|
-
let
|
|
383
|
+
let content = '';
|
|
386
384
|
<Button>{''}</Button>
|
|
387
|
-
<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.
|
|
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.
|
|
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
|
|
18
|
+
let message = 'Hello World';
|
|
19
19
|
|
|
20
|
-
<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 &[
|
|
45
|
+
let &[message] = track('Hello World');
|
|
31
46
|
|
|
32
47
|
<button
|
|
33
48
|
onClick={() => {
|
|
34
|
-
|
|
49
|
+
message = 'Hello Ripple';
|
|
35
50
|
}}
|
|
36
51
|
>
|
|
37
52
|
{'Change Text'}
|
|
38
53
|
</button>
|
|
39
|
-
<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
|
|
88
|
+
<h1>{children}</h1>
|
|
74
89
|
}
|
|
75
90
|
if (level == 2) {
|
|
76
|
-
<h2
|
|
91
|
+
<h2>{children}</h2>
|
|
77
92
|
}
|
|
78
93
|
if (level == 3) {
|
|
79
|
-
<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
|
-
|
|
156
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
{children}
|
|
36
36
|
<B />
|
|
37
37
|
</div>
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
component App() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
383
|
-
expect((css.match(/\(unused\) :global\(\.x\) /g) || []).length).toBe(
|
|
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
|
|