ripple 0.3.8 → 0.3.9

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 (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +13 -157
  4. package/src/compiler/phases/2-analyze/index.js +289 -43
  5. package/src/compiler/phases/3-transform/client/index.js +9 -157
  6. package/src/compiler/phases/3-transform/segments.js +0 -7
  7. package/src/compiler/phases/3-transform/server/index.js +15 -130
  8. package/src/compiler/types/acorn.d.ts +1 -1
  9. package/src/compiler/types/estree.d.ts +1 -1
  10. package/src/compiler/types/import.d.ts +0 -2
  11. package/src/compiler/types/index.d.ts +5 -17
  12. package/src/compiler/types/parse.d.ts +1 -9
  13. package/src/compiler/utils.js +53 -20
  14. package/src/runtime/index-client.js +2 -13
  15. package/src/runtime/index-server.js +2 -2
  16. package/src/runtime/internal/client/bindings.js +3 -1
  17. package/src/runtime/internal/client/composite.js +1 -0
  18. package/src/runtime/internal/client/events.js +1 -1
  19. package/src/runtime/internal/client/head.js +3 -4
  20. package/src/runtime/internal/client/index.js +0 -1
  21. package/src/runtime/internal/client/runtime.js +0 -52
  22. package/src/runtime/internal/server/index.js +31 -55
  23. package/tests/client/basic/basic.errors.test.ripple +28 -0
  24. package/tests/client/basic/basic.reactivity.test.ripple +10 -155
  25. package/tests/client/compiler/compiler.basic.test.ripple +31 -12
  26. package/tests/client/composite/composite.props.test.ripple +5 -7
  27. package/tests/client/composite/composite.reactivity.test.ripple +35 -36
  28. package/tests/client/dynamic-elements.test.ripple +3 -4
  29. package/tests/client/lazy-destructuring.test.ripple +69 -12
  30. package/tests/server/compiler.test.ripple +22 -0
  31. package/tests/server/composite.props.test.ripple +5 -7
  32. package/tests/server/dynamic-elements.test.ripple +3 -4
  33. package/tests/server/lazy-destructuring.test.ripple +68 -12
  34. package/tsconfig.typecheck.json +4 -0
  35. package/types/index.d.ts +0 -19
  36. package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
  37. package/tests/client/tracked-expression.test.ripple +0 -26
@@ -1,5 +1,5 @@
1
1
  import type { Props, Tracked } from 'ripple';
2
- import { RippleObject, effect, flushSync, track, trackSplit } from 'ripple';
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
- 'keeps reactivity for spread props via intermediate components and usage of trackSplit()',
232
- () => {
233
- component App() {
234
- let &[count] = track(0);
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
- component CounterWrapper(props: Props) {
239
- <div>
240
- <Counter {...props} />
241
- </div>
242
- }
236
+ component CounterWrapper(props: Props) {
237
+ <div>
238
+ <Counter {...props} />
239
+ </div>
240
+ }
243
241
 
244
- component Counter(props: Props) {
245
- let [count, up, down, rest] = trackSplit(props, ['count', 'up', 'down']);
246
- <button onClick={() => up.value()}>{'UP'}</button>
247
- <button onClick={() => down.value()}>{'DOWN'}</button>
248
- <span {...rest.value}>{`Counter: ${count.value}`}</span>
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
- render(App);
248
+ render(App);
252
249
 
253
- const buttonIncrement = container.querySelectorAll('button')[0];
254
- const buttonDecrement = container.querySelectorAll('button')[1];
255
- const span = container.querySelector('span');
250
+ const buttonIncrement = container.querySelectorAll('button')[0];
251
+ const buttonDecrement = container.querySelectorAll('button')[1];
252
+ const span = container.querySelector('span');
256
253
 
257
- expect(span.textContent).toBe('Counter: 0');
254
+ expect(span.textContent).toBe('Counter: 0');
258
255
 
259
- buttonIncrement.click();
260
- flushSync();
256
+ buttonIncrement.click();
257
+ flushSync();
261
258
 
262
- expect(span.textContent).toBe('Counter: 1');
259
+ expect(span.textContent).toBe('Counter: 1');
263
260
 
264
- buttonDecrement.click();
265
- flushSync();
261
+ buttonDecrement.click();
262
+ flushSync();
266
263
 
267
- expect(span.textContent).toBe('Counter: 0');
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(props: {
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
- let [count, up, rest] = trackSplit(props, ['count', 'up']);
306
- <div {...rest.value}>{`Counter: ${count.value} Double: ${props.double.value}`}</div>
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);
@@ -1,5 +1,5 @@
1
1
  import type { PropsWithExtras } from 'ripple';
2
- import { createRefKey, flushSync, track, trackSplit } from 'ripple';
2
+ import { createRefKey, flushSync, track } from 'ripple';
3
3
 
4
4
  describe('dynamic DOM elements', () => {
5
5
  it('renders static dynamic element', () => {
@@ -405,14 +405,13 @@ describe('dynamic DOM elements', () => {
405
405
  });
406
406
 
407
407
  it('handles spread attributes with class and CSS scoping ', () => {
408
- component DynamicButton(props: PropsWithExtras<{
408
+ component DynamicButton(&{ ...rest }: PropsWithExtras<{
409
409
  class: string;
410
410
  id: string;
411
411
  onClick: EventListener;
412
412
  }>) {
413
413
  const tag = track('button');
414
- const [rest] = trackSplit(props, []);
415
- <@tag {...rest.value}>{rest.value.class}</@tag>
414
+ <@tag {...rest}>{rest.class}</@tag>
416
415
 
417
416
  <style>
418
417
  .even {
@@ -1,4 +1,4 @@
1
- import { flushSync, track, trackSplit } from 'ripple';
1
+ import { flushSync, track } from 'ripple';
2
2
 
3
3
  describe('lazy destructuring', () => {
4
4
  it('supports tracked value getter and setter', () => {
@@ -126,6 +126,38 @@ describe('lazy destructuring', () => {
126
126
  expect(container.querySelector('pre')!.textContent).toBe('1');
127
127
  });
128
128
 
129
+ it('supports nested lazy destructuring in non-lazy component params', () => {
130
+ component Inner({ something: &[first, second] }) {
131
+ first = second.value + 1;
132
+ <pre>{`${first}-${second.value}`}</pre>
133
+ }
134
+
135
+ component Test() {
136
+ <Inner something={track(1)} />
137
+ }
138
+
139
+ render(Test);
140
+ expect(container.querySelector('pre')!.textContent).toBe('2-2');
141
+ });
142
+
143
+ it(
144
+ 'preserves lazy getter/setter behavior for nested rest destructuring in non-lazy component params',
145
+ () => {
146
+ component Inner({ values: [head, ...&{ length: rest_length, 0: first_rest }] }) {
147
+ const before = `${first_rest?.value ?? 'nil'}-${rest_length}`;
148
+ rest_length = 0;
149
+ <pre>{`${head}-${before}-${first_rest?.value ?? 'nil'}-${rest_length}`}</pre>
150
+ }
151
+
152
+ component Test() {
153
+ <Inner values={[10, track(20), track(30)]} />
154
+ }
155
+
156
+ render(Test);
157
+ expect(container.querySelector('pre')!.textContent).toBe('10-20-2-nil-0');
158
+ },
159
+ );
160
+
129
161
  it('supports lazy destructuring in function params', () => {
130
162
  component Test() {
131
163
  function getInfo(&{ x, y }: { x: number; y: number }) {
@@ -139,6 +171,42 @@ describe('lazy destructuring', () => {
139
171
  expect(container.querySelector('pre')!.textContent).toBe('10');
140
172
  });
141
173
 
174
+ it('supports nested lazy destructuring in non-lazy function params', () => {
175
+ component Test() {
176
+ const something = track(1);
177
+
178
+ function getInfo({ something: &[first, second] }) {
179
+ first = second.value + 1;
180
+ return `${first}-${second.value}`;
181
+ }
182
+
183
+ const result = getInfo({ something });
184
+ <pre>{result}</pre>
185
+ }
186
+
187
+ render(Test);
188
+ expect(container.querySelector('pre')!.textContent).toBe('2-2');
189
+ });
190
+
191
+ it(
192
+ 'preserves lazy getter/setter behavior for nested rest destructuring in non-lazy function params',
193
+ () => {
194
+ component Test() {
195
+ function summarize({ values: [head, ...&{ length: rest_length, 0: first_rest }] }) {
196
+ const before = `${first_rest?.value ?? 'nil'}-${rest_length}`;
197
+ rest_length = 0;
198
+ return `${head}-${before}-${first_rest?.value ?? 'nil'}-${rest_length}`;
199
+ }
200
+
201
+ const result = summarize({ values: [5, track(6), track(7)] });
202
+ <pre>{result}</pre>
203
+ }
204
+
205
+ render(Test);
206
+ expect(container.querySelector('pre')!.textContent).toBe('5-6-2-nil-0');
207
+ },
208
+ );
209
+
142
210
  it('supports let lazy destructuring with assignment writeback', () => {
143
211
  component Test() {
144
212
  const obj = { a: 1, b: 2 };
@@ -205,17 +273,6 @@ describe('lazy destructuring', () => {
205
273
  expect(container.querySelector('pre')!.textContent).toBe('1-99');
206
274
  });
207
275
 
208
- it('does not apply the track tuple fast-path to trackSplit lazy arrays', () => {
209
- component Test() {
210
- const source = { a: 1, b: 2, c: 3 };
211
- let [a, b, rest] = trackSplit(source, ['a', 'b']);
212
- <pre>{`${a.value}-${b.value}-${rest.value.c}`}</pre>
213
- }
214
-
215
- render(Test);
216
- expect(container.querySelector('pre')!.textContent).toBe('1-2-3');
217
- });
218
-
219
276
  it('supports rest destructuring from iterable array-like tracked values', () => {
220
277
  component Test() {
221
278
  let &[value, ...rest] = track(0);
@@ -93,6 +93,28 @@ export component Layout(props) {
93
93
  '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
94
94
  );
95
95
  });
96
+
97
+ it('throws error for calling children as a function in SSR mode', () => {
98
+ const source = `
99
+ export component Layout({ children }) {
100
+ {children()}
101
+ }`;
102
+
103
+ expect(() => compile(source, 'test.ripple', { mode: 'server' })).toThrow(
104
+ '`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
105
+ );
106
+ });
107
+
108
+ it('throws error for calling props.children as a function in SSR mode', () => {
109
+ const source = `
110
+ export component Layout(props) {
111
+ {props.children()}
112
+ }`;
113
+
114
+ expect(() => compile(source, 'test.ripple', { mode: 'server' })).toThrow(
115
+ '`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
116
+ );
117
+ });
96
118
  });
97
119
 
98
120
  describe('compiler server block tests', () => {
@@ -1,4 +1,4 @@
1
- import { track, trackSplit } from 'ripple';
1
+ import { track } from 'ripple';
2
2
  import type { Tracked, Props } from 'ripple';
3
3
 
4
4
  describe('composite > props', () => {
@@ -79,9 +79,8 @@ describe('composite > props', () => {
79
79
  });
80
80
 
81
81
  it('correctly retains prop accessors and reactivity when using rest props', async () => {
82
- component Button(props: Props) {
83
- const [children, rest] = trackSplit(props, ['children']);
84
- <button {...rest.value}>
82
+ component Button(&{ children, ...rest }: Props) {
83
+ <button {...rest}>
85
84
  <@children />
86
85
  </button>
87
86
  <style>
@@ -94,10 +93,9 @@ describe('composite > props', () => {
94
93
  </style>
95
94
  }
96
95
 
97
- component Toggle(props: { pressed: Tracked<boolean> }) {
98
- const [pressed, rest] = trackSplit(props, ['pressed']);
96
+ component Toggle(&{ pressed, ...rest }: { pressed: Tracked<boolean> }) {
99
97
  const onClick = () => (pressed.value = !pressed.value);
100
- <Button {...rest.value} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
98
+ <Button {...rest} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
101
99
  <Button class={pressed.value ? 'on' : 'off'} {onClick}>{'button 2'}</Button>
102
100
  }
103
101
 
@@ -1,5 +1,5 @@
1
1
  import type { PropsWithExtras } from 'ripple';
2
- import { createRefKey, track, trackSplit } from 'ripple';
2
+ import { createRefKey, track } from 'ripple';
3
3
 
4
4
  describe('server dynamic DOM elements', () => {
5
5
  it('renders static dynamic element', async () => {
@@ -258,13 +258,12 @@ describe('server dynamic DOM elements', () => {
258
258
  });
259
259
 
260
260
  it('handles spread attributes with class and CSS scoping', async () => {
261
- component DynamicButton(props: PropsWithExtras<{
261
+ component DynamicButton(&{ ...rest }: PropsWithExtras<{
262
262
  class: string;
263
263
  id: string;
264
264
  }>) {
265
265
  const tag = track('button');
266
- const [rest] = trackSplit(props, []);
267
- <@tag {...rest.value}>{rest.value.class}</@tag>
266
+ <@tag {...rest}>{rest.class}</@tag>
268
267
 
269
268
  <style>
270
269
  .even {
@@ -1,4 +1,5 @@
1
- import { track, trackSplit } from 'ripple';
1
+ import type { Tracked } from 'ripple';
2
+ import { track } from 'ripple';
2
3
 
3
4
  describe('lazy destructuring', () => {
4
5
  it('supports tracked value getter and setter', async () => {
@@ -30,7 +31,7 @@ describe('lazy destructuring', () => {
30
31
 
31
32
  it('supports default values in lazy object destructuring', async () => {
32
33
  component Test() {
33
- const obj = { a: 5 };
34
+ const obj: { a: number; b?: number } = { a: 5 };
34
35
  const &{ a, b = 99 } = obj;
35
36
  <pre>{`${a}-${b}`}</pre>
36
37
  }
@@ -39,6 +40,20 @@ describe('lazy destructuring', () => {
39
40
  expect(body).toBeHtml('<pre>5-99</pre>');
40
41
  });
41
42
 
43
+ it('supports nested lazy destructuring in non-lazy component params', async () => {
44
+ component Inner({ something: &[first, second] }: { something: Tracked<number> }) {
45
+ first = second.value + 1;
46
+ <pre>{`${first}-${second.value}`}</pre>
47
+ }
48
+
49
+ component Test() {
50
+ <Inner something={track(1)} />
51
+ }
52
+
53
+ const { body } = await render(Test);
54
+ expect(body).toBeHtml('<pre>2-2</pre>');
55
+ });
56
+
42
57
  it('supports let lazy destructuring with assignment writeback', async () => {
43
58
  component Test() {
44
59
  const obj = { a: 1, b: 2 };
@@ -106,26 +121,67 @@ describe('lazy destructuring', () => {
106
121
  expect(body).toBeHtml('<pre>15-105</pre>');
107
122
  });
108
123
 
109
- it('supports member access on lazy destructured objects', async () => {
124
+ it('supports nested lazy destructuring in non-lazy function params', async () => {
110
125
  component Test() {
111
- const obj = { user: { name: 'Alice', age: 30 } };
112
- const &{ user } = obj;
113
- <pre>{`${user.name}-${user.age}`}</pre>
126
+ const something = track(1);
127
+
128
+ function getInfo({ something: &[first, second] }: { something: Tracked<number> }) {
129
+ first = second.value + 1;
130
+ return `${first}-${second.value}`;
131
+ }
132
+
133
+ <pre>{getInfo({ something })}</pre>
114
134
  }
115
135
 
116
136
  const { body } = await render(Test);
117
- expect(body).toBeHtml('<pre>Alice-30</pre>');
137
+ expect(body).toBeHtml('<pre>2-2</pre>');
118
138
  });
119
139
 
120
- it('treats array destructuring of trackSplit as regular array access', async () => {
140
+ it(
141
+ 'preserves lazy getter/setter behavior for RestElement nested destructuring in non-lazy component params',
142
+ async () => {
143
+ component Inner({ values: [head, ...&{ 0: first_rest, length: rest_length }] }) {
144
+ const before = `${first_rest}-${rest_length}`;
145
+ rest_length = 0;
146
+ <pre>{`${head}-${before}-${first_rest}-${rest_length}`}</pre>
147
+ }
148
+
149
+ component Test() {
150
+ <Inner values={[10, 20, 30]} />
151
+ }
152
+
153
+ const { body } = await render(Test);
154
+ expect(body).toBeHtml('<pre>10-20-2-undefined-0</pre>');
155
+ },
156
+ );
157
+
158
+ it(
159
+ 'preserves lazy getter/setter behavior for RestElement nested destructuring in non-lazy function params',
160
+ async () => {
161
+ component Test() {
162
+ function getInfo({ values: [head, ...&{ 0: first_rest, length: rest_length }] }) {
163
+ const before = `${first_rest}-${rest_length}`;
164
+ rest_length = 0;
165
+ return `${head}-${before}-${first_rest}-${rest_length}`;
166
+ }
167
+
168
+ <pre>{getInfo({ values: [5, 6, 7] })}</pre>
169
+ }
170
+
171
+ const { body } = await render(Test);
172
+ expect(body).toBeHtml('<pre>5-6-2-undefined-0</pre>');
173
+ },
174
+ );
175
+
176
+ it('supports member access on lazy destructured objects', async () => {
121
177
  component Test() {
122
- const source = { a: 1, b: 2, c: 3 };
123
- const [a, b, rest] = trackSplit(source, ['a', 'b']);
124
- <pre>{`${a.value}-${b.value}-${rest.value.c}`}</pre>
178
+ const obj = { user: { name: 'Alice', age: 30 } };
179
+ const &{ user } = obj;
180
+ <pre>{`${user.name}-${user.age}`}</pre>
125
181
  }
126
182
 
127
183
  const { body } = await render(Test);
128
- expect(body).toBeHtml('<pre>1-2-3</pre>');
184
+ expect(body).toBeHtml('<pre>Alice-30</pre>');
129
185
  });
130
186
 
131
187
  it('supports rest in lazy array destructuring for tracked tuples (iterable)', async () => {
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["./src/runtime/"]
4
+ }
package/types/index.d.ts CHANGED
@@ -176,19 +176,6 @@ export type PropsNoChildren<T extends object = {}> = Expand<T>;
176
176
 
177
177
  type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
178
178
 
179
- type WrapTracked<V> = V extends Tracked<any> ? V : Tracked<V>;
180
-
181
- type PickKeys<T, K extends readonly (keyof T)[]> = {
182
- [I in keyof K]: WrapTracked<T[K[I] & keyof T]>;
183
- };
184
-
185
- type RestKeys<T, K extends readonly (keyof T)[]> = Expand<Omit<T, K[number]>>;
186
-
187
- type SplitResult<T extends Props, K extends readonly (keyof T)[]> = [
188
- ...PickKeys<T, K>,
189
- Tracked<RestKeys<T, K>>,
190
- ];
191
-
192
179
  export function get<V>(tracked: Tracked<V>): V;
193
180
 
194
181
  export function set<V>(tracked: Tracked<V>, value: V): void;
@@ -204,11 +191,6 @@ export function track<V>(
204
191
  // Overload for non-function values
205
192
  export function track<V>(value?: V, get?: (v: V) => V, set?: (next: V, prev: V) => V): Tracked<V>;
206
193
 
207
- export function trackSplit<V extends Props, const K extends readonly (keyof V)[]>(
208
- value: V,
209
- splitKeys: K,
210
- ): SplitResult<V, K>;
211
-
212
194
  export interface AddEventOptions extends ExtendedEventOptions {
213
195
  customName?: string;
214
196
  }
@@ -568,7 +550,6 @@ export interface RippleNamespace {
568
550
  urlSearchParams: RippleURLSearchParamsCallable;
569
551
  untrack: typeof untrack;
570
552
  track: typeof track;
571
- trackSplit: typeof trackSplit;
572
553
  style: Record<string, string>;
573
554
  server: ServerBlock;
574
555
  }
@@ -1,34 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`TrackedExpression tests > should handle the syntax correctly 1`] = `
4
- <div>
5
- <div>
6
- 0
7
- </div>
8
- <div>
9
- 4
10
- </div>
11
- <div>
12
- 1
13
- </div>
14
- <div>
15
- 2
16
- </div>
17
- <div>
18
- 2
19
- </div>
20
- <div>
21
- 3
22
- </div>
23
- <div>
24
- 4
25
- </div>
26
- <div>
27
- false
28
- </div>
29
- <div>
30
- true
31
- </div>
32
-
33
- </div>
34
- `;
@@ -1,26 +0,0 @@
1
- import { track } from 'ripple';
2
-
3
- describe('TrackedExpression tests', () => {
4
- it('should handle the syntax correctly', () => {
5
- component App() {
6
- let count = track(0);
7
-
8
- function get_count() {
9
- return count;
10
- }
11
-
12
- <div>{@(count)}</div>
13
- <div>{@(get_count())}</div>
14
- <div>{++@(count)}</div>
15
- <div>{++@(get_count())}</div>
16
- <div>{@(count)++}</div>
17
- <div>{@(get_count())++}</div>
18
- <div>{@(count)}</div>
19
- <div>{!@(count)}</div>
20
- <div>{!!@(count)}</div>
21
- }
22
-
23
- render(App);
24
- expect(container).toMatchSnapshot();
25
- });
26
- });