ripple 0.3.8 → 0.3.10
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 +19 -0
- package/package.json +2 -2
- package/src/compiler/phases/1-parse/index.js +38 -172
- package/src/compiler/phases/2-analyze/index.js +308 -115
- package/src/compiler/phases/2-analyze/prune.js +13 -5
- package/src/compiler/phases/3-transform/client/index.js +197 -213
- package/src/compiler/phases/3-transform/segments.js +0 -7
- package/src/compiler/phases/3-transform/server/index.js +77 -170
- package/src/compiler/types/acorn.d.ts +1 -1
- package/src/compiler/types/estree.d.ts +1 -1
- package/src/compiler/types/import.d.ts +0 -2
- package/src/compiler/types/index.d.ts +14 -18
- package/src/compiler/types/parse.d.ts +3 -9
- package/src/compiler/utils.js +154 -21
- package/src/runtime/element.js +39 -0
- package/src/runtime/index-client.js +2 -13
- package/src/runtime/index-server.js +2 -2
- package/src/runtime/internal/client/bindings.js +3 -1
- package/src/runtime/internal/client/composite.js +11 -6
- package/src/runtime/internal/client/events.js +1 -1
- package/src/runtime/internal/client/expression.js +218 -0
- package/src/runtime/internal/client/head.js +3 -4
- package/src/runtime/internal/client/index.js +4 -1
- package/src/runtime/internal/client/portal.js +12 -6
- package/src/runtime/internal/client/runtime.js +0 -52
- package/src/runtime/internal/server/index.js +57 -56
- package/tests/client/basic/basic.components.test.ripple +85 -87
- package/tests/client/basic/basic.errors.test.ripple +28 -4
- package/tests/client/basic/basic.reactivity.test.ripple +10 -155
- package/tests/client/basic/basic.rendering.test.ripple +23 -8
- package/tests/client/capture-error.js +12 -0
- package/tests/client/compiler/compiler.basic.test.ripple +107 -18
- package/tests/client/composite/composite.props.test.ripple +5 -9
- package/tests/client/composite/composite.reactivity.test.ripple +35 -36
- package/tests/client/composite/composite.render.test.ripple +45 -13
- package/tests/client/css/global-additional-cases.test.ripple +3 -3
- package/tests/client/dynamic-elements.test.ripple +3 -4
- package/tests/client/lazy-destructuring.test.ripple +69 -12
- package/tests/client/svg.test.ripple +4 -4
- package/tests/hydration/basic.test.js +23 -0
- package/tests/hydration/compiled/client/basic.js +118 -66
- package/tests/hydration/compiled/client/composite.js +90 -37
- package/tests/hydration/compiled/client/events.js +18 -18
- package/tests/hydration/compiled/client/for.js +62 -62
- package/tests/hydration/compiled/client/head.js +10 -10
- package/tests/hydration/compiled/client/hmr.js +13 -10
- package/tests/hydration/compiled/client/html.js +274 -236
- package/tests/hydration/compiled/client/if-children.js +41 -35
- package/tests/hydration/compiled/client/if.js +2 -2
- package/tests/hydration/compiled/client/mixed-control-flow.js +12 -12
- package/tests/hydration/compiled/client/nested-control-flow.js +46 -46
- package/tests/hydration/compiled/client/portal.js +8 -8
- package/tests/hydration/compiled/client/reactivity.js +14 -14
- package/tests/hydration/compiled/client/return.js +2 -2
- 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 +43 -4
- package/tests/server/composite.props.test.ripple +5 -9
- package/tests/server/dynamic-elements.test.ripple +3 -4
- package/tests/server/lazy-destructuring.test.ripple +68 -12
- package/tests/server/style-identifier.test.ripple +2 -4
- package/tsconfig.typecheck.json +4 -0
- package/types/index.d.ts +9 -21
- package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
- package/tests/client/tracked-expression.test.ripple +0 -26
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PropsWithChildren, Tracked } from 'ripple';
|
|
2
|
-
import { effect, flushSync, track,
|
|
2
|
+
import { effect, flushSync, track, untrack } from 'ripple';
|
|
3
3
|
|
|
4
4
|
describe('basic client > reactivity', () => {
|
|
5
5
|
it('renders multiple reactive lexical blocks', () => {
|
|
@@ -384,162 +384,17 @@ describe('basic client > reactivity', () => {
|
|
|
384
384
|
expect(state.finalValue).toBe(5);
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
const [a, b, rest] = trackSplit(null, ['a', 'b']);
|
|
394
|
-
} catch (e) {
|
|
395
|
-
message = (e as Error).message;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
<pre>{message}</pre>
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
render(App);
|
|
402
|
-
|
|
403
|
-
const pre = container.querySelectorAll('pre')[0];
|
|
404
|
-
expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('errors on invalid value as array for track with trackSplit', () => {
|
|
408
|
-
component App() {
|
|
409
|
-
let &[message] = track('');
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
const [a, b, rest] = trackSplit([1, 2, 3], ['a', 'b']);
|
|
413
|
-
} catch (e) {
|
|
414
|
-
message = (e as Error).message;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
<pre>{message}</pre>
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
render(App);
|
|
421
|
-
|
|
422
|
-
const pre = container.querySelectorAll('pre')[0];
|
|
423
|
-
expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('errors on invalid value as tracked for track with trackSplit', () => {
|
|
427
|
-
component App() {
|
|
428
|
-
const t = track({ a: 1, b: 2, c: 3 });
|
|
429
|
-
let &[message] = track('');
|
|
430
|
-
|
|
431
|
-
try {
|
|
432
|
-
const [a, b, rest] = trackSplit(t, ['a', 'b']);
|
|
433
|
-
} catch (e) {
|
|
434
|
-
message = (e as Error).message;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
<pre>{message}</pre>
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
render(App);
|
|
441
|
-
|
|
442
|
-
const pre = container.querySelectorAll('pre')[0];
|
|
443
|
-
expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('returns undefined for non-existent props in track with trackSplit', () => {
|
|
447
|
-
component App() {
|
|
448
|
-
const [a, b, rest] = trackSplit({ a: 1, c: 1 }, ['a', 'b']);
|
|
449
|
-
|
|
450
|
-
<pre>{a.value}</pre>
|
|
451
|
-
<pre>{String(b.value)}</pre>
|
|
452
|
-
<pre>{rest.value.c}</pre>
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
render(App);
|
|
456
|
-
|
|
457
|
-
const preA = container.querySelectorAll('pre')[0];
|
|
458
|
-
const preB = container.querySelectorAll('pre')[1];
|
|
459
|
-
const preC = container.querySelectorAll('pre')[2];
|
|
460
|
-
|
|
461
|
-
expect(preA.textContent).toBe('1');
|
|
462
|
-
expect(preB.textContent).toBe('undefined');
|
|
463
|
-
expect(preC.textContent).toBe('1');
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it('returns the same tracked object if plain track is called with a tracked object', () => {
|
|
467
|
-
component App() {
|
|
468
|
-
const t = track({ a: 1, b: 2, c: 3 });
|
|
469
|
-
const doublet = track(t);
|
|
470
|
-
|
|
471
|
-
<pre>{t === doublet}</pre>
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
render(App);
|
|
475
|
-
|
|
476
|
-
const pre = container.querySelectorAll('pre')[0];
|
|
477
|
-
expect(pre.textContent).toBe('true');
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('can retain reactivity for destructure rest via track trackSplit', () => {
|
|
481
|
-
let logs: string[] = [];
|
|
482
|
-
|
|
483
|
-
component App() {
|
|
484
|
-
let &[count] = track(0);
|
|
485
|
-
let &[name] = track('Click Me');
|
|
486
|
-
|
|
487
|
-
function buttonRef(el: HTMLButtonElement) {
|
|
488
|
-
logs.push('ref called');
|
|
489
|
-
return () => {
|
|
490
|
-
logs.push('cleanup ref');
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
<Child
|
|
495
|
-
class="my-button"
|
|
496
|
-
onClick={() => name === 'Click Me' ? name = 'Clicked' : name = 'Click Me'}
|
|
497
|
-
{count}
|
|
498
|
-
{ref buttonRef}
|
|
499
|
-
>
|
|
500
|
-
{name}
|
|
501
|
-
</Child>
|
|
502
|
-
|
|
503
|
-
<button onClick={() => count++}>{'Increment Count'}</button>
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
component Child(props: PropsWithChildren<{
|
|
507
|
-
count: Tracked<number>;
|
|
508
|
-
class: string;
|
|
509
|
-
onClick: () => void;
|
|
510
|
-
}>) {
|
|
511
|
-
const [children, count, rest] = trackSplit(props, ['children', 'count']);
|
|
512
|
-
|
|
513
|
-
if (count.value < 2) {
|
|
514
|
-
<button {...rest.value}>
|
|
515
|
-
<@children />
|
|
516
|
-
</button>
|
|
517
|
-
}
|
|
518
|
-
<pre>{count.value}</pre>
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
render(App);
|
|
522
|
-
flushSync();
|
|
523
|
-
|
|
524
|
-
const buttonClickMe = container.querySelectorAll('button')[0];
|
|
525
|
-
const buttonIncrement = container.querySelectorAll('button')[1];
|
|
526
|
-
const countPre = container.querySelector('pre');
|
|
527
|
-
|
|
528
|
-
expect(buttonClickMe.textContent).toBe('Click Me');
|
|
529
|
-
expect(countPre.textContent).toBe('0');
|
|
530
|
-
expect(logs).toEqual(['ref called']);
|
|
531
|
-
|
|
532
|
-
buttonClickMe.click();
|
|
533
|
-
buttonIncrement.click();
|
|
534
|
-
flushSync();
|
|
387
|
+
it('returns the same tracked object if plain track is called with a tracked object', () => {
|
|
388
|
+
component App() {
|
|
389
|
+
const t = track({ a: 1, b: 2, c: 3 });
|
|
390
|
+
const doublet = track(t);
|
|
535
391
|
|
|
536
|
-
|
|
537
|
-
|
|
392
|
+
<pre>{t === doublet}</pre>
|
|
393
|
+
}
|
|
538
394
|
|
|
539
|
-
|
|
540
|
-
flushSync();
|
|
395
|
+
render(App);
|
|
541
396
|
|
|
542
|
-
|
|
543
|
-
|
|
397
|
+
const pre = container.querySelectorAll('pre')[0];
|
|
398
|
+
expect(pre.textContent).toBe('true');
|
|
544
399
|
});
|
|
545
400
|
});
|
|
@@ -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
|
|
|
@@ -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>;
|
|
@@ -341,18 +375,6 @@ component App() {
|
|
|
341
375
|
});
|
|
342
376
|
|
|
343
377
|
it('keeps lazy destructuring as plain destructuring in to_ts output', () => {
|
|
344
|
-
const track_split_source = `
|
|
345
|
-
import { trackSplit } from 'ripple';
|
|
346
|
-
component App() {
|
|
347
|
-
const source = { a: 1, b: 2, c: 3 };
|
|
348
|
-
let &[a, b, rest] = trackSplit(source, ['a', 'b']);
|
|
349
|
-
const sum = a + b + rest.c;
|
|
350
|
-
}
|
|
351
|
-
`;
|
|
352
|
-
const track_split_result = compile_to_volar_mappings(track_split_source, 'test.ripple').code;
|
|
353
|
-
expect(track_split_result).toContain('let [a, b, rest] = trackSplit(source, [\'a\', \'b\']);');
|
|
354
|
-
expect(track_split_result).not.toContain('let lazy = trackSplit');
|
|
355
|
-
|
|
356
378
|
const track_source = `
|
|
357
379
|
import { track } from 'ripple';
|
|
358
380
|
component App() {
|
|
@@ -369,6 +391,37 @@ component App() {
|
|
|
369
391
|
expect(track_result).not.toContain('lazy0');
|
|
370
392
|
});
|
|
371
393
|
|
|
394
|
+
it('uses tracked fast path for nested lazy params typed as Tracked', () => {
|
|
395
|
+
const source = `
|
|
396
|
+
import type { Tracked } from 'ripple';
|
|
397
|
+
function use_nested({ value: &[count, tracked] }: { value: Tracked<number> }) {
|
|
398
|
+
count++;
|
|
399
|
+
return tracked;
|
|
400
|
+
}
|
|
401
|
+
`;
|
|
402
|
+
const { js } = compile(source, 'tracked-nested-lazy.ripple', { mode: 'client' });
|
|
403
|
+
|
|
404
|
+
// Nested lazy array should still use tracked tuple fast path from outer annotation.
|
|
405
|
+
expect(js.code).toContain('_$_.update(');
|
|
406
|
+
expect(js.code).not.toContain('[0]');
|
|
407
|
+
expect(js.code).not.toContain('[1]');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('uses tracked fast path for nested lazy params at tuple rest positions', () => {
|
|
411
|
+
const source = `
|
|
412
|
+
import type { Tracked } from 'ripple';
|
|
413
|
+
function use_tuple_rest({ value: [head, &[count, tracked]] }: { value: [number, ...Tracked<number>[]] }) {
|
|
414
|
+
count++;
|
|
415
|
+
return tracked;
|
|
416
|
+
}
|
|
417
|
+
`;
|
|
418
|
+
const { js } = compile(source, 'tracked-nested-lazy-tuple-rest.ripple', { mode: 'client' });
|
|
419
|
+
|
|
420
|
+
// Tuple rest element access should resolve to Tracked<number>, not Tracked<number>[].
|
|
421
|
+
expect(js.code).toContain('_$_.update(');
|
|
422
|
+
expect(js.code).not.toContain('[1]');
|
|
423
|
+
});
|
|
424
|
+
|
|
372
425
|
it('preserves generic type args in interface extends for Volar mappings', () => {
|
|
373
426
|
const source = `
|
|
374
427
|
interface PolymorphicProps<T extends keyof HTMLElementTagNameMap> {
|
|
@@ -414,16 +467,16 @@ export component App() {
|
|
|
414
467
|
const code = `
|
|
415
468
|
component Card(props) {
|
|
416
469
|
<div class="card">
|
|
417
|
-
|
|
470
|
+
{props.children}
|
|
418
471
|
</div>
|
|
419
472
|
}
|
|
420
473
|
|
|
421
474
|
export component App() {
|
|
422
|
-
<Card>
|
|
423
475
|
component children() {
|
|
424
476
|
<p>{'Card content here'}</p>
|
|
425
477
|
}
|
|
426
|
-
|
|
478
|
+
|
|
479
|
+
<Card {children} />
|
|
427
480
|
|
|
428
481
|
const test = 5;
|
|
429
482
|
|
|
@@ -433,7 +486,7 @@ export component App() {
|
|
|
433
486
|
expect(() => compile(code, 'test.ripple')).not.toThrow();
|
|
434
487
|
});
|
|
435
488
|
|
|
436
|
-
it('
|
|
489
|
+
it('rejects component declarations inside composite children', () => {
|
|
437
490
|
const source = `
|
|
438
491
|
export component App() {
|
|
439
492
|
<ark.div class="host-class" data-value="42">
|
|
@@ -442,12 +495,48 @@ export component App() {
|
|
|
442
495
|
}
|
|
443
496
|
</ark.div>
|
|
444
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
|
+
}
|
|
445
514
|
`;
|
|
446
515
|
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
447
516
|
|
|
448
|
-
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}');
|
|
449
518
|
expect(result).not.toContain('children={() =>');
|
|
450
|
-
|
|
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(');
|
|
451
540
|
});
|
|
452
541
|
|
|
453
542
|
it('should not error on `this` MemberExpression with a UpdateExpression', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Tracked, Props } from 'ripple';
|
|
2
|
-
import { effect, flushSync, track
|
|
2
|
+
import { effect, flushSync, track } from 'ripple';
|
|
3
3
|
|
|
4
4
|
describe('composite > props', () => {
|
|
5
5
|
it('correctly handles default prop values', () => {
|
|
@@ -106,11 +106,8 @@ describe('composite > props', () => {
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
it('correctly retains prop accessors and reactivity when using rest props', () => {
|
|
109
|
-
component Button(
|
|
110
|
-
|
|
111
|
-
<button {...rest.value}>
|
|
112
|
-
<@children />
|
|
113
|
-
</button>
|
|
109
|
+
component Button(&{ children, ...rest }: Props) {
|
|
110
|
+
<button {...rest}>{children}</button>
|
|
114
111
|
<style>
|
|
115
112
|
.on {
|
|
116
113
|
color: blue;
|
|
@@ -121,10 +118,9 @@ describe('composite > props', () => {
|
|
|
121
118
|
</style>
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
component Toggle(
|
|
125
|
-
const [pressed, rest] = trackSplit(props, ['pressed']);
|
|
121
|
+
component Toggle(&{ pressed, ...rest }: { pressed: Tracked<boolean> }) {
|
|
126
122
|
const onClick = () => (pressed.value = !pressed.value);
|
|
127
|
-
<Button {...rest
|
|
123
|
+
<Button {...rest} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
|
|
128
124
|
<Button class={pressed.value ? 'on' : 'off'} {onClick}>{'button 2'}</Button>
|
|
129
125
|
}
|
|
130
126
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Props, Tracked } from 'ripple';
|
|
2
|
-
import { RippleObject, effect, flushSync, track
|
|
2
|
+
import { RippleObject, effect, flushSync, track } from 'ripple';
|
|
3
3
|
|
|
4
4
|
describe('composite > reactivity', () => {
|
|
5
5
|
it('renders composite components with object state', () => {
|
|
@@ -227,46 +227,42 @@ describe('composite > reactivity', () => {
|
|
|
227
227
|
expect(logs).toEqual(['Child effect: 1, 2, 3', 'Child effect: 2, 3, 4']);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
-
it(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<CounterWrapper {count} up={() => count++} down={() => count--} />
|
|
236
|
-
}
|
|
230
|
+
it('keeps reactivity for spread props via intermediate components and lazy destructuring', () => {
|
|
231
|
+
component App() {
|
|
232
|
+
let &[count] = track(0);
|
|
233
|
+
<CounterWrapper {count} up={() => count++} down={() => count--} />
|
|
234
|
+
}
|
|
237
235
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
236
|
+
component CounterWrapper(props: Props) {
|
|
237
|
+
<div>
|
|
238
|
+
<Counter {...props} />
|
|
239
|
+
</div>
|
|
240
|
+
}
|
|
243
241
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
242
|
+
component Counter(&{ count, up, down, ...rest }: Props) {
|
|
243
|
+
<button onClick={() => up()}>{'UP'}</button>
|
|
244
|
+
<button onClick={() => down()}>{'DOWN'}</button>
|
|
245
|
+
<span {...rest}>{`Counter: ${count}`}</span>
|
|
246
|
+
}
|
|
250
247
|
|
|
251
|
-
|
|
248
|
+
render(App);
|
|
252
249
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
250
|
+
const buttonIncrement = container.querySelectorAll('button')[0];
|
|
251
|
+
const buttonDecrement = container.querySelectorAll('button')[1];
|
|
252
|
+
const span = container.querySelector('span');
|
|
256
253
|
|
|
257
|
-
|
|
254
|
+
expect(span.textContent).toBe('Counter: 0');
|
|
258
255
|
|
|
259
|
-
|
|
260
|
-
|
|
256
|
+
buttonIncrement.click();
|
|
257
|
+
flushSync();
|
|
261
258
|
|
|
262
|
-
|
|
259
|
+
expect(span.textContent).toBe('Counter: 1');
|
|
263
260
|
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
buttonDecrement.click();
|
|
262
|
+
flushSync();
|
|
266
263
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
);
|
|
264
|
+
expect(span.textContent).toBe('Counter: 0');
|
|
265
|
+
});
|
|
270
266
|
|
|
271
267
|
it('keeps reactivity on elements for element spreads and adds / removes dynamic props', () => {
|
|
272
268
|
component App() {
|
|
@@ -295,16 +291,19 @@ describe('composite > reactivity', () => {
|
|
|
295
291
|
</div>
|
|
296
292
|
}
|
|
297
293
|
|
|
298
|
-
component Counter(
|
|
294
|
+
component Counter(&{
|
|
295
|
+
count,
|
|
296
|
+
up,
|
|
297
|
+
...rest
|
|
298
|
+
}: {
|
|
299
299
|
count: number;
|
|
300
300
|
up: () => void;
|
|
301
301
|
double: Tracked<number>;
|
|
302
302
|
another?: number;
|
|
303
303
|
extra: number;
|
|
304
304
|
}) {
|
|
305
|
-
|
|
306
|
-
<
|
|
307
|
-
<button onClick={() => up.value()}>{'UP'}</button>
|
|
305
|
+
<div {...rest}>{`Counter: ${count} Double: ${rest.double.value}`}</div>
|
|
306
|
+
<button onClick={() => up()}>{'UP'}</button>
|
|
308
307
|
}
|
|
309
308
|
|
|
310
309
|
render(App);
|
|
@@ -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>
|
|
@@ -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
|
|