ripple 0.2.115 → 0.2.118
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 +16 -16
- package/src/compiler/index.js +20 -1
- package/src/compiler/phases/1-parse/index.js +79 -0
- package/src/compiler/phases/3-transform/client/index.js +54 -8
- package/src/compiler/phases/3-transform/segments.js +107 -60
- package/src/compiler/phases/3-transform/server/index.js +21 -11
- package/src/compiler/types/index.d.ts +16 -0
- package/src/runtime/index-client.js +19 -185
- package/src/runtime/index-server.js +24 -0
- package/src/runtime/internal/client/bindings.js +443 -0
- package/src/runtime/internal/client/index.js +4 -0
- package/src/runtime/internal/client/runtime.js +10 -0
- package/src/runtime/internal/client/utils.js +0 -8
- package/src/runtime/map.js +11 -1
- package/src/runtime/set.js +11 -1
- package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
- package/tests/client/_etc.test.ripple +5 -0
- package/tests/client/array/array.copy-within.test.ripple +120 -0
- package/tests/client/array/array.derived.test.ripple +495 -0
- package/tests/client/array/array.iteration.test.ripple +115 -0
- package/tests/client/array/array.mutations.test.ripple +385 -0
- package/tests/client/array/array.static.test.ripple +237 -0
- package/tests/client/array/array.to-methods.test.ripple +93 -0
- package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
- package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
- package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
- package/tests/client/basic/basic.attributes.test.ripple +474 -0
- package/tests/client/basic/basic.collections.test.ripple +94 -0
- package/tests/client/basic/basic.components.test.ripple +225 -0
- package/tests/client/basic/basic.errors.test.ripple +126 -0
- package/tests/client/basic/basic.events.test.ripple +222 -0
- package/tests/client/basic/basic.reactivity.test.ripple +476 -0
- package/tests/client/basic/basic.rendering.test.ripple +204 -0
- package/tests/client/basic/basic.styling.test.ripple +63 -0
- package/tests/client/basic/basic.utilities.test.ripple +25 -0
- package/tests/client/boundaries.test.ripple +2 -21
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
- package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
- package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
- package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
- package/tests/client/compiler/compiler.basic.test.ripple +203 -0
- package/tests/client/compiler/compiler.regex.test.ripple +87 -0
- package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
- package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
- package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
- package/tests/client/composite/composite.generics.test.ripple +211 -0
- package/tests/client/composite/composite.props.test.ripple +106 -0
- package/tests/client/composite/composite.reactivity.test.ripple +184 -0
- package/tests/client/composite/composite.render.test.ripple +84 -0
- package/tests/client/computed-properties.test.ripple +2 -21
- package/tests/client/context.test.ripple +5 -22
- package/tests/client/date.test.ripple +1 -20
- package/tests/client/dynamic-elements.test.ripple +16 -24
- package/tests/client/for.test.ripple +4 -23
- package/tests/client/head.test.ripple +11 -23
- package/tests/client/html.test.ripple +1 -20
- package/tests/client/input-value.test.ripple +11 -31
- package/tests/client/map.test.ripple +82 -20
- package/tests/client/media-query.test.ripple +10 -23
- package/tests/client/object.test.ripple +5 -24
- package/tests/client/portal.test.ripple +2 -19
- package/tests/client/ref.test.ripple +8 -26
- package/tests/client/set.test.ripple +84 -22
- package/tests/client/svg.test.ripple +1 -22
- package/tests/client/switch.test.ripple +6 -25
- package/tests/client/tracked-expression.test.ripple +2 -21
- package/tests/client/typescript-generics.test.ripple +0 -21
- package/tests/client/url/url.derived.test.ripple +83 -0
- package/tests/client/url/url.parsing.test.ripple +165 -0
- package/tests/client/url/url.partial-removal.test.ripple +198 -0
- package/tests/client/url/url.reactivity.test.ripple +449 -0
- package/tests/client/url/url.serialization.test.ripple +50 -0
- package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
- package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
- package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
- package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
- package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
- package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
- package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
- package/tests/client.d.ts +12 -0
- package/tests/server/if.test.ripple +66 -0
- package/tests/setup-client.js +28 -0
- package/tsconfig.json +4 -2
- package/types/index.d.ts +92 -46
- package/LICENSE +0 -21
- package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
- package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
- package/tests/client/array.test.ripple +0 -1455
- package/tests/client/basic.test.ripple +0 -1892
- package/tests/client/compiler.test.ripple +0 -541
- package/tests/client/composite.test.ripple +0 -692
- package/tests/client/url-search-params.test.ripple +0 -912
- package/tests/client/url.test.ripple +0 -954
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { flushSync, track, TrackedURLSearchParams } from 'ripple';
|
|
2
|
+
|
|
3
|
+
describe('TrackedURLSearchParams > retrieval', () => {
|
|
4
|
+
it('handles get operation with reactivity', () => {
|
|
5
|
+
component URLTest() {
|
|
6
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
7
|
+
let foo = track(() => params.get('foo'));
|
|
8
|
+
let baz = track(() => params.get('baz'));
|
|
9
|
+
|
|
10
|
+
<button onClick={() => params.set('foo', 'updated')}>{'update foo'}</button>
|
|
11
|
+
<pre>{@foo}</pre>
|
|
12
|
+
<pre>{@baz}</pre>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
render(URLTest);
|
|
16
|
+
|
|
17
|
+
const button = container.querySelector('button');
|
|
18
|
+
|
|
19
|
+
// Initial state
|
|
20
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('bar');
|
|
21
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('qux');
|
|
22
|
+
|
|
23
|
+
// Test update
|
|
24
|
+
button.click();
|
|
25
|
+
flushSync();
|
|
26
|
+
|
|
27
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('updated');
|
|
28
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('qux');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles get for nonexistent key', () => {
|
|
32
|
+
component URLTest() {
|
|
33
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
34
|
+
let nonexistent = track(() => params.get('nonexistent'));
|
|
35
|
+
|
|
36
|
+
<pre>{String(@nonexistent)}</pre>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(URLTest);
|
|
40
|
+
|
|
41
|
+
expect(container.querySelector('pre').textContent).toBe('null');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('handles getAll operation with reactivity', () => {
|
|
45
|
+
component URLTest() {
|
|
46
|
+
const params = new TrackedURLSearchParams('foo=bar&foo=baz');
|
|
47
|
+
let allFoo = track(() => params.getAll('foo'));
|
|
48
|
+
|
|
49
|
+
<button onClick={() => params.append('foo', 'qux')}>{'append foo'}</button>
|
|
50
|
+
<pre>{JSON.stringify(@allFoo)}</pre>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
render(URLTest);
|
|
54
|
+
|
|
55
|
+
const button = container.querySelector('button');
|
|
56
|
+
|
|
57
|
+
// Initial state
|
|
58
|
+
expect(container.querySelector('pre').textContent).toBe('["bar","baz"]');
|
|
59
|
+
|
|
60
|
+
// Test append
|
|
61
|
+
button.click();
|
|
62
|
+
flushSync();
|
|
63
|
+
|
|
64
|
+
expect(container.querySelector('pre').textContent).toBe('["bar","baz","qux"]');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('handles has operation with reactivity', () => {
|
|
68
|
+
component URLTest() {
|
|
69
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
70
|
+
let hasFoo = track(() => params.has('foo'));
|
|
71
|
+
let hasBaz = track(() => params.has('baz'));
|
|
72
|
+
|
|
73
|
+
<button onClick={() => params.append('baz', 'qux')}>{'add baz'}</button>
|
|
74
|
+
<button onClick={() => params.delete('foo')}>{'delete foo'}</button>
|
|
75
|
+
<pre>{@hasFoo.toString()}</pre>
|
|
76
|
+
<pre>{@hasBaz.toString()}</pre>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render(URLTest);
|
|
80
|
+
|
|
81
|
+
const addButton = container.querySelectorAll('button')[0];
|
|
82
|
+
const deleteButton = container.querySelectorAll('button')[1];
|
|
83
|
+
|
|
84
|
+
// Initial state
|
|
85
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
|
|
86
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('false');
|
|
87
|
+
|
|
88
|
+
// Test add
|
|
89
|
+
addButton.click();
|
|
90
|
+
flushSync();
|
|
91
|
+
|
|
92
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
|
|
93
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('true');
|
|
94
|
+
|
|
95
|
+
// Test delete
|
|
96
|
+
deleteButton.click();
|
|
97
|
+
flushSync();
|
|
98
|
+
|
|
99
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('false');
|
|
100
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('true');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('handles has with specific value', () => {
|
|
104
|
+
component URLTest() {
|
|
105
|
+
const params = new TrackedURLSearchParams('foo=bar&foo=baz');
|
|
106
|
+
let hasBarValue = track(() => params.has('foo', 'bar'));
|
|
107
|
+
let hasQuxValue = track(() => params.has('foo', 'qux'));
|
|
108
|
+
|
|
109
|
+
<button onClick={() => params.append('foo', 'qux')}>{'add qux'}</button>
|
|
110
|
+
<pre>{@hasBarValue.toString()}</pre>
|
|
111
|
+
<pre>{@hasQuxValue.toString()}</pre>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render(URLTest);
|
|
115
|
+
|
|
116
|
+
const button = container.querySelector('button');
|
|
117
|
+
|
|
118
|
+
// Initial state
|
|
119
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
|
|
120
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('false');
|
|
121
|
+
|
|
122
|
+
// Test add
|
|
123
|
+
button.click();
|
|
124
|
+
flushSync();
|
|
125
|
+
|
|
126
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
|
|
127
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('true');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('handles size property with reactivity', () => {
|
|
131
|
+
component URLTest() {
|
|
132
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
133
|
+
let size = track(() => params.size);
|
|
134
|
+
|
|
135
|
+
<button onClick={() => params.append('baz', 'qux')}>{'add'}</button>
|
|
136
|
+
<button onClick={() => params.delete('foo')}>{'delete'}</button>
|
|
137
|
+
<pre>{@size}</pre>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
render(URLTest);
|
|
141
|
+
|
|
142
|
+
const addButton = container.querySelectorAll('button')[0];
|
|
143
|
+
const deleteButton = container.querySelectorAll('button')[1];
|
|
144
|
+
|
|
145
|
+
// Initial state
|
|
146
|
+
expect(container.querySelector('pre').textContent).toBe('1');
|
|
147
|
+
|
|
148
|
+
// Test add
|
|
149
|
+
addButton.click();
|
|
150
|
+
flushSync();
|
|
151
|
+
|
|
152
|
+
expect(container.querySelector('pre').textContent).toBe('2');
|
|
153
|
+
|
|
154
|
+
// Test delete
|
|
155
|
+
deleteButton.click();
|
|
156
|
+
flushSync();
|
|
157
|
+
|
|
158
|
+
expect(container.querySelector('pre').textContent).toBe('1');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { flushSync, track, TrackedURLSearchParams } from 'ripple';
|
|
2
|
+
|
|
3
|
+
describe('TrackedURLSearchParams > serialization', () => {
|
|
4
|
+
it('handles toString method with reactivity', () => {
|
|
5
|
+
component URLTest() {
|
|
6
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
7
|
+
let string = track(() => params.toString());
|
|
8
|
+
|
|
9
|
+
<button onClick={() => params.append('baz', 'qux')}>{'add'}</button>
|
|
10
|
+
<pre>{@string}</pre>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
render(URLTest);
|
|
14
|
+
|
|
15
|
+
const button = container.querySelector('button');
|
|
16
|
+
|
|
17
|
+
// Initial state
|
|
18
|
+
expect(container.querySelector('pre').textContent).toBe('foo=bar');
|
|
19
|
+
|
|
20
|
+
// Test add
|
|
21
|
+
button.click();
|
|
22
|
+
flushSync();
|
|
23
|
+
|
|
24
|
+
expect(container.querySelector('pre').textContent).toBe('foo=bar&baz=qux');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('handles special characters encoding', () => {
|
|
28
|
+
component URLTest() {
|
|
29
|
+
const params = new TrackedURLSearchParams();
|
|
30
|
+
|
|
31
|
+
<button onClick={() => params.set('key', 'value with spaces')}>{'add spaces'}</button>
|
|
32
|
+
<button onClick={() => params.set('special', '!@#$%^&*()')}>{'add special'}</button>
|
|
33
|
+
<pre>{params.toString()}</pre>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(URLTest);
|
|
37
|
+
|
|
38
|
+
const spacesButton = container.querySelectorAll('button')[0];
|
|
39
|
+
const specialButton = container.querySelectorAll('button')[1];
|
|
40
|
+
|
|
41
|
+
// Test spaces
|
|
42
|
+
spacesButton.click();
|
|
43
|
+
flushSync();
|
|
44
|
+
|
|
45
|
+
expect(container.querySelector('pre').textContent).toBe('key=value+with+spaces');
|
|
46
|
+
|
|
47
|
+
// Test special characters
|
|
48
|
+
specialButton.click();
|
|
49
|
+
flushSync();
|
|
50
|
+
|
|
51
|
+
expect(container.querySelector('pre').textContent).toContain('special');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { flushSync, TrackedURL } from 'ripple';
|
|
2
|
+
|
|
3
|
+
describe('TrackedURLSearchParams > TrackedURL integration', () => {
|
|
4
|
+
it('integrates with TrackedURL', () => {
|
|
5
|
+
component URLTest() {
|
|
6
|
+
const url = new TrackedURL('https://example.com?foo=bar');
|
|
7
|
+
const params = url.searchParams;
|
|
8
|
+
|
|
9
|
+
<button onClick={() => params.append('baz', 'qux')}>{'add param'}</button>
|
|
10
|
+
<pre>{url.href}</pre>
|
|
11
|
+
<pre>{params.toString()}</pre>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
render(URLTest);
|
|
15
|
+
|
|
16
|
+
const button = container.querySelector('button');
|
|
17
|
+
|
|
18
|
+
// Initial state
|
|
19
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/?foo=bar');
|
|
20
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('foo=bar');
|
|
21
|
+
|
|
22
|
+
// Test add param - should update URL
|
|
23
|
+
button.click();
|
|
24
|
+
flushSync();
|
|
25
|
+
|
|
26
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/?foo=bar&baz=qux');
|
|
27
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('foo=bar&baz=qux');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('handles empty search string in URL', () => {
|
|
31
|
+
component URLTest() {
|
|
32
|
+
const url = new TrackedURL('https://example.com');
|
|
33
|
+
const params = url.searchParams;
|
|
34
|
+
|
|
35
|
+
<button onClick={() => params.append('foo', 'bar')}>{'add first param'}</button>
|
|
36
|
+
<pre>{url.href}</pre>
|
|
37
|
+
<pre>{params.size}</pre>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
render(URLTest);
|
|
41
|
+
|
|
42
|
+
const button = container.querySelector('button');
|
|
43
|
+
|
|
44
|
+
// Initial state - no search params
|
|
45
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/');
|
|
46
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
47
|
+
|
|
48
|
+
// Test add first param
|
|
49
|
+
button.click();
|
|
50
|
+
flushSync();
|
|
51
|
+
|
|
52
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/?foo=bar');
|
|
53
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare var container: HTMLDivElement;
|
|
2
|
+
declare var error: string | undefined;
|
|
3
|
+
declare function render(component: () => void): void;
|
|
4
|
+
|
|
5
|
+
interface HTMLElement {
|
|
6
|
+
// We don't care about checking if it returned an element or null in tests
|
|
7
|
+
// because if it returned null, those tests will fail anyway. This
|
|
8
|
+
// typing drastically simplifies testing: you don't have to check if the
|
|
9
|
+
// query returned null or an actual element, and you don't have to do
|
|
10
|
+
// optional chaining everywhere (elem?.textContent)
|
|
11
|
+
querySelector(selectors: string): HTMLElement;
|
|
12
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render } from 'ripple/server';
|
|
3
|
+
|
|
4
|
+
describe('if statements in SSR', () => {
|
|
5
|
+
it('renders if block when condition is true', async () => {
|
|
6
|
+
component App() {
|
|
7
|
+
let condition = true;
|
|
8
|
+
|
|
9
|
+
if (condition) {
|
|
10
|
+
<div>{'If block'}</div>
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { body } = await render(App);
|
|
15
|
+
expect(body).toBe('<div>If block</div>');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders else block when condition is false', async () => {
|
|
19
|
+
component App() {
|
|
20
|
+
let condition = false;
|
|
21
|
+
|
|
22
|
+
if (condition) {
|
|
23
|
+
<div>{'If block'}</div>
|
|
24
|
+
} else {
|
|
25
|
+
<div>{'Else block'}</div>
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { body } = await render(App);
|
|
30
|
+
expect(body).toBe('<div>Else block</div>');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders else if block when condition is true', async () => {
|
|
34
|
+
component App() {
|
|
35
|
+
let value = 'b';
|
|
36
|
+
|
|
37
|
+
if (value === 'a') {
|
|
38
|
+
<div>{'Case A'}</div>
|
|
39
|
+
} else if (value === 'b') {
|
|
40
|
+
<div>{'Case B'}</div>
|
|
41
|
+
} else {
|
|
42
|
+
<div>{'Default Case'}</div>
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { body } = await render(App);
|
|
47
|
+
expect(body).toBe('<div>Case B</div>');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders final else block in an if-else if-else chain', async () => {
|
|
51
|
+
component App() {
|
|
52
|
+
let value = 'c';
|
|
53
|
+
|
|
54
|
+
if (value === 'a') {
|
|
55
|
+
<div>{'Case A'}</div>
|
|
56
|
+
} else if (value === 'b') {
|
|
57
|
+
<div>{'Case B'}</div>
|
|
58
|
+
} else {
|
|
59
|
+
<div>{'Default Case'}</div>
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { body } = await render(App);
|
|
64
|
+
expect(body).toBe('<div>Default Case</div>');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mount } from 'ripple';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {() => void} component
|
|
6
|
+
*/
|
|
7
|
+
globalThis.render = function render(component) {
|
|
8
|
+
mount(component, {
|
|
9
|
+
target: /** @type {HTMLDivElement} */ (globalThis.container)
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
globalThis.container = /** @type {HTMLDivElement} */ (document.createElement('div'));
|
|
15
|
+
document.body.appendChild(globalThis.container);
|
|
16
|
+
|
|
17
|
+
globalThis.error = undefined;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Container is guaranteed to exist in all tests, so it was easier to type it without undefined.
|
|
22
|
+
// And when we unset it, we just type-cast it to HTMLDivElement to avoid TS errors, because we
|
|
23
|
+
// know it's guaranteed to exist in the next test again.
|
|
24
|
+
document.body.removeChild(/** @type {HTMLDivElement} */ (globalThis.container));
|
|
25
|
+
globalThis.container = /** @type {HTMLDivElement} */ (/** @type {unknown} */(undefined));
|
|
26
|
+
|
|
27
|
+
globalThis.error = undefined;
|
|
28
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"noErrorTruncation": true,
|
|
11
11
|
"allowSyntheticDefaultImports": true,
|
|
12
12
|
"verbatimModuleSyntax": true,
|
|
13
|
-
"types": ["node"],
|
|
13
|
+
"types": ["node", "vitest/globals"],
|
|
14
14
|
"jsx": "preserve",
|
|
15
15
|
"jsxImportSource": "ripple",
|
|
16
16
|
"strict": true,
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"include": [
|
|
24
24
|
"./*.js",
|
|
25
25
|
"./src/",
|
|
26
|
-
"./tests
|
|
26
|
+
"./tests/**/*.test.ripple",
|
|
27
|
+
"./tests/**/*.d.ts",
|
|
28
|
+
"./tests/**/*.js"
|
|
27
29
|
]
|
|
28
30
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -9,18 +9,18 @@ export declare function tick(): Promise<void>;
|
|
|
9
9
|
|
|
10
10
|
export declare function untrack<T>(fn: () => T): T;
|
|
11
11
|
|
|
12
|
-
export declare function flushSync<T>(fn
|
|
12
|
+
export declare function flushSync<T>(fn?: () => T): T;
|
|
13
13
|
|
|
14
14
|
export declare function effect(fn: (() => void) | (() => () => void)): void;
|
|
15
15
|
|
|
16
16
|
export interface TrackedArrayConstructor {
|
|
17
|
-
new <T>(...elements: T[]): TrackedArray<T>;
|
|
17
|
+
new <T>(...elements: T[]): TrackedArray<T>; // must be used with `new`
|
|
18
18
|
from<T>(arrayLike: ArrayLike<T>): TrackedArray<T>;
|
|
19
19
|
of<T>(...items: T[]): TrackedArray<T>;
|
|
20
20
|
fromAsync<T>(iterable: AsyncIterable<T>): Promise<TrackedArray<T>>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export interface TrackedArray<T> extends Array<T> {
|
|
23
|
+
export interface TrackedArray<T> extends Array<T> {}
|
|
24
24
|
|
|
25
25
|
export declare const TrackedArray: TrackedArrayConstructor;
|
|
26
26
|
|
|
@@ -74,7 +74,9 @@ declare global {
|
|
|
74
74
|
export declare function createRefKey(): symbol;
|
|
75
75
|
|
|
76
76
|
// Base Tracked interface - all tracked values have a '#v' property containing the actual value
|
|
77
|
-
export interface Tracked<V> {
|
|
77
|
+
export interface Tracked<V> {
|
|
78
|
+
'#v': V;
|
|
79
|
+
}
|
|
78
80
|
|
|
79
81
|
// Augment Tracked to be callable when V is a Component
|
|
80
82
|
// This allows <@Something /> to work in JSX when Something is Tracked<Component>
|
|
@@ -84,32 +86,41 @@ export interface Tracked<V> {
|
|
|
84
86
|
|
|
85
87
|
// Helper type to infer component type from a function that returns a component
|
|
86
88
|
// If T is a function returning a Component, extract the Component type itself, not the return type (void)
|
|
87
|
-
export type InferComponent<T> =
|
|
88
|
-
T extends () => infer R
|
|
89
|
-
? R extends Component<any>
|
|
90
|
-
? R
|
|
91
|
-
: T
|
|
92
|
-
: T;
|
|
89
|
+
export type InferComponent<T> = T extends () => infer R ? (R extends Component<any> ? R : T) : T;
|
|
93
90
|
|
|
94
91
|
export type Props<K extends PropertyKey = any, V = unknown> = Record<K, V>;
|
|
95
92
|
export type PropsWithExtras<T extends object> = Props & T & Record<string, unknown>;
|
|
96
|
-
export type PropsWithChildren<T extends object = {}> =
|
|
97
|
-
|
|
93
|
+
export type PropsWithChildren<T extends object = {}> = Expand<
|
|
94
|
+
Omit<Props, 'children'> & { children: Component } & T
|
|
95
|
+
>;
|
|
98
96
|
|
|
99
97
|
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
|
100
98
|
|
|
101
|
-
type PickKeys<T, K extends readonly (keyof T)[]> =
|
|
102
|
-
{ [I in keyof K]: Tracked<T[K[I] & keyof T]> };
|
|
99
|
+
type PickKeys<T, K extends readonly (keyof T)[]> = { [I in keyof K]: Tracked<T[K[I] & keyof T]> };
|
|
103
100
|
|
|
104
101
|
type RestKeys<T, K extends readonly (keyof T)[]> = Expand<Omit<T, K[number]>>;
|
|
105
102
|
|
|
106
|
-
type SplitResult<T extends Props, K extends readonly (keyof T)[]> =
|
|
107
|
-
|
|
103
|
+
type SplitResult<T extends Props, K extends readonly (keyof T)[]> = [
|
|
104
|
+
...PickKeys<T, K>,
|
|
105
|
+
Tracked<RestKeys<T, K>>,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export declare function get<V>(tracked: Tracked<V>): V;
|
|
109
|
+
|
|
110
|
+
export declare function set<V>(tracked: Tracked<V>, value: V): void;
|
|
108
111
|
|
|
109
112
|
// Overload for function values - infers the return type of the function
|
|
110
|
-
export declare function track<V>(
|
|
113
|
+
export declare function track<V>(
|
|
114
|
+
value: () => V,
|
|
115
|
+
get?: (v: InferComponent<V>) => InferComponent<V>,
|
|
116
|
+
set?: (next: InferComponent<V>, prev: InferComponent<V>) => InferComponent<V>,
|
|
117
|
+
): Tracked<InferComponent<V>>;
|
|
111
118
|
// Overload for non-function values
|
|
112
|
-
export declare function track<V>(
|
|
119
|
+
export declare function track<V>(
|
|
120
|
+
value?: V,
|
|
121
|
+
get?: (v: V) => V,
|
|
122
|
+
set?: (next: V, prev: V) => V,
|
|
123
|
+
): Tracked<V>;
|
|
113
124
|
|
|
114
125
|
export declare function trackSplit<V extends Props, const K extends readonly (keyof V)[]>(
|
|
115
126
|
value: V,
|
|
@@ -120,60 +131,67 @@ export function on<Type extends keyof WindowEventMap>(
|
|
|
120
131
|
window: Window,
|
|
121
132
|
type: Type,
|
|
122
133
|
handler: (this: Window, event: WindowEventMap[Type]) => any,
|
|
123
|
-
options?: AddEventListenerOptions | undefined
|
|
134
|
+
options?: AddEventListenerOptions | undefined,
|
|
124
135
|
): () => void;
|
|
125
136
|
|
|
126
137
|
export function on<Type extends keyof DocumentEventMap>(
|
|
127
138
|
document: Document,
|
|
128
139
|
type: Type,
|
|
129
140
|
handler: (this: Document, event: DocumentEventMap[Type]) => any,
|
|
130
|
-
options?: AddEventListenerOptions | undefined
|
|
141
|
+
options?: AddEventListenerOptions | undefined,
|
|
131
142
|
): () => void;
|
|
132
143
|
|
|
133
144
|
export function on<Element extends HTMLElement, Type extends keyof HTMLElementEventMap>(
|
|
134
145
|
element: Element,
|
|
135
146
|
type: Type,
|
|
136
147
|
handler: (this: Element, event: HTMLElementEventMap[Type]) => any,
|
|
137
|
-
options?: AddEventListenerOptions | undefined
|
|
148
|
+
options?: AddEventListenerOptions | undefined,
|
|
138
149
|
): () => void;
|
|
139
150
|
|
|
140
151
|
export function on<Element extends MediaQueryList, Type extends keyof MediaQueryListEventMap>(
|
|
141
152
|
element: Element,
|
|
142
153
|
type: Type,
|
|
143
154
|
handler: (this: Element, event: MediaQueryListEventMap[Type]) => any,
|
|
144
|
-
options?: AddEventListenerOptions | undefined
|
|
155
|
+
options?: AddEventListenerOptions | undefined,
|
|
145
156
|
): () => void;
|
|
146
157
|
|
|
147
158
|
export function on(
|
|
148
159
|
element: EventTarget,
|
|
149
160
|
type: string,
|
|
150
161
|
handler: EventListener,
|
|
151
|
-
options?: AddEventListenerOptions | undefined
|
|
162
|
+
options?: AddEventListenerOptions | undefined,
|
|
152
163
|
): () => void;
|
|
153
164
|
|
|
154
165
|
export type TrackedObjectShallow<T> = {
|
|
155
166
|
[K in keyof T]: T[K] | Tracked<T[K]>;
|
|
156
167
|
};
|
|
157
168
|
|
|
158
|
-
export type TrackedObjectDeep<T> =
|
|
159
|
-
|
|
169
|
+
export type TrackedObjectDeep<T> = T extends
|
|
170
|
+
| string
|
|
171
|
+
| number
|
|
172
|
+
| boolean
|
|
173
|
+
| null
|
|
174
|
+
| undefined
|
|
175
|
+
| symbol
|
|
176
|
+
| bigint
|
|
160
177
|
? T | Tracked<T>
|
|
161
178
|
: T extends TrackedArray<infer U>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
? TrackedArray<U> | Tracked<TrackedArray<U>>
|
|
180
|
+
: T extends TrackedSet<infer U>
|
|
181
|
+
? TrackedSet<U> | Tracked<TrackedSet<U>>
|
|
182
|
+
: T extends TrackedMap<infer K, infer V>
|
|
183
|
+
? TrackedMap<K, V> | Tracked<TrackedMap<K, V>>
|
|
184
|
+
: T extends Array<infer U>
|
|
185
|
+
? Array<TrackedObjectDeep<U>> | Tracked<Array<TrackedObjectDeep<U>>>
|
|
186
|
+
: T extends Set<infer U>
|
|
187
|
+
? Set<TrackedObjectDeep<U>> | Tracked<Set<TrackedObjectDeep<U>>>
|
|
188
|
+
: T extends Map<infer K, infer V>
|
|
189
|
+
?
|
|
190
|
+
| Map<TrackedObjectDeep<K>, TrackedObjectDeep<V>>
|
|
191
|
+
| Tracked<Map<TrackedObjectDeep<K>, TrackedObjectDeep<V>>>
|
|
192
|
+
: T extends object
|
|
193
|
+
? { [K in keyof T]: TrackedObjectDeep<T[K]> | Tracked<TrackedObjectDeep<T[K]>> }
|
|
194
|
+
: T | Tracked<T>;
|
|
177
195
|
|
|
178
196
|
export type TrackedObject<T extends object> = T & {};
|
|
179
197
|
|
|
@@ -203,31 +221,59 @@ export class TrackedURL extends URL {
|
|
|
203
221
|
export function createSubscriber(start: () => void | (() => void)): () => void;
|
|
204
222
|
|
|
205
223
|
interface ReactiveValue<V> extends Tracked<V> {
|
|
206
|
-
new(fn: () => Tracked<V>, start: () => void | (() => void)): Tracked<V>;
|
|
224
|
+
new (fn: () => Tracked<V>, start: () => void | (() => void)): Tracked<V>;
|
|
207
225
|
/** @private */
|
|
208
226
|
_brand: void;
|
|
209
227
|
}
|
|
210
228
|
|
|
211
229
|
export interface MediaQuery extends Tracked<boolean> {
|
|
212
|
-
new(query: string, fallback?: boolean | undefined): Tracked<boolean>;
|
|
230
|
+
new (query: string, fallback?: boolean | undefined): Tracked<boolean>;
|
|
213
231
|
/** @private */
|
|
214
232
|
_brand: void;
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
export declare const MediaQuery: {
|
|
218
|
-
new(query: string, fallback?: boolean | undefined): Tracked<boolean>;
|
|
236
|
+
new (query: string, fallback?: boolean | undefined): Tracked<boolean>;
|
|
219
237
|
};
|
|
220
238
|
|
|
221
|
-
export function Portal<V = HTMLElement>({
|
|
239
|
+
export function Portal<V = HTMLElement>({
|
|
240
|
+
target,
|
|
241
|
+
children: Component,
|
|
242
|
+
}: {
|
|
243
|
+
target: V;
|
|
244
|
+
children?: Component;
|
|
245
|
+
}): void;
|
|
222
246
|
|
|
223
247
|
/**
|
|
224
248
|
* @param {Tracked<V>} tracked
|
|
225
249
|
* @returns {(node: HTMLInputElement | HTMLSelectElement) => void}
|
|
226
250
|
*/
|
|
227
|
-
export declare function bindValue<V>(
|
|
251
|
+
export declare function bindValue<V>(
|
|
252
|
+
tracked: Tracked<V>,
|
|
253
|
+
): (node: HTMLInputElement | HTMLSelectElement) => void;
|
|
228
254
|
|
|
229
255
|
/**
|
|
230
256
|
* @param {Tracked<V>} tracked
|
|
231
257
|
* @returns {(node: HTMLInputElement) => void}
|
|
232
258
|
*/
|
|
233
|
-
export declare function bindChecked<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
|
259
|
+
export declare function bindChecked<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
|
260
|
+
|
|
261
|
+
export declare function bindClientWidth<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
262
|
+
|
|
263
|
+
export declare function bindClientHeight<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
264
|
+
|
|
265
|
+
export declare function bindContentRect<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
266
|
+
|
|
267
|
+
export declare function bindContentBoxSize<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
268
|
+
|
|
269
|
+
export declare function bindBorderBoxSize<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
270
|
+
|
|
271
|
+
export declare function bindDevicePixelContentBoxSize<V>(
|
|
272
|
+
tracked: Tracked<V>,
|
|
273
|
+
): (node: HTMLElement) => void;
|
|
274
|
+
|
|
275
|
+
export declare function bindInnerHTML<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
276
|
+
|
|
277
|
+
export declare function bindInnerText<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
278
|
+
|
|
279
|
+
export declare function bindTextContent<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Dominic Gannaway
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|