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.
Files changed (79) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +38 -172
  4. package/src/compiler/phases/2-analyze/index.js +308 -115
  5. package/src/compiler/phases/2-analyze/prune.js +13 -5
  6. package/src/compiler/phases/3-transform/client/index.js +197 -213
  7. package/src/compiler/phases/3-transform/segments.js +0 -7
  8. package/src/compiler/phases/3-transform/server/index.js +77 -170
  9. package/src/compiler/types/acorn.d.ts +1 -1
  10. package/src/compiler/types/estree.d.ts +1 -1
  11. package/src/compiler/types/import.d.ts +0 -2
  12. package/src/compiler/types/index.d.ts +14 -18
  13. package/src/compiler/types/parse.d.ts +3 -9
  14. package/src/compiler/utils.js +154 -21
  15. package/src/runtime/element.js +39 -0
  16. package/src/runtime/index-client.js +2 -13
  17. package/src/runtime/index-server.js +2 -2
  18. package/src/runtime/internal/client/bindings.js +3 -1
  19. package/src/runtime/internal/client/composite.js +11 -6
  20. package/src/runtime/internal/client/events.js +1 -1
  21. package/src/runtime/internal/client/expression.js +218 -0
  22. package/src/runtime/internal/client/head.js +3 -4
  23. package/src/runtime/internal/client/index.js +4 -1
  24. package/src/runtime/internal/client/portal.js +12 -6
  25. package/src/runtime/internal/client/runtime.js +0 -52
  26. package/src/runtime/internal/server/index.js +57 -56
  27. package/tests/client/basic/basic.components.test.ripple +85 -87
  28. package/tests/client/basic/basic.errors.test.ripple +28 -4
  29. package/tests/client/basic/basic.reactivity.test.ripple +10 -155
  30. package/tests/client/basic/basic.rendering.test.ripple +23 -8
  31. package/tests/client/capture-error.js +12 -0
  32. package/tests/client/compiler/compiler.basic.test.ripple +107 -18
  33. package/tests/client/composite/composite.props.test.ripple +5 -9
  34. package/tests/client/composite/composite.reactivity.test.ripple +35 -36
  35. package/tests/client/composite/composite.render.test.ripple +45 -13
  36. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  37. package/tests/client/dynamic-elements.test.ripple +3 -4
  38. package/tests/client/lazy-destructuring.test.ripple +69 -12
  39. package/tests/client/svg.test.ripple +4 -4
  40. package/tests/hydration/basic.test.js +23 -0
  41. package/tests/hydration/compiled/client/basic.js +118 -66
  42. package/tests/hydration/compiled/client/composite.js +90 -37
  43. package/tests/hydration/compiled/client/events.js +18 -18
  44. package/tests/hydration/compiled/client/for.js +62 -62
  45. package/tests/hydration/compiled/client/head.js +10 -10
  46. package/tests/hydration/compiled/client/hmr.js +13 -10
  47. package/tests/hydration/compiled/client/html.js +274 -236
  48. package/tests/hydration/compiled/client/if-children.js +41 -35
  49. package/tests/hydration/compiled/client/if.js +2 -2
  50. package/tests/hydration/compiled/client/mixed-control-flow.js +12 -12
  51. package/tests/hydration/compiled/client/nested-control-flow.js +46 -46
  52. package/tests/hydration/compiled/client/portal.js +8 -8
  53. package/tests/hydration/compiled/client/reactivity.js +14 -14
  54. package/tests/hydration/compiled/client/return.js +2 -2
  55. package/tests/hydration/compiled/client/try.js +4 -4
  56. package/tests/hydration/compiled/server/basic.js +64 -31
  57. package/tests/hydration/compiled/server/composite.js +62 -29
  58. package/tests/hydration/compiled/server/hmr.js +24 -37
  59. package/tests/hydration/compiled/server/html.js +472 -611
  60. package/tests/hydration/compiled/server/if-children.js +77 -103
  61. package/tests/hydration/compiled/server/portal.js +8 -8
  62. package/tests/hydration/components/basic.ripple +15 -5
  63. package/tests/hydration/components/composite.ripple +13 -1
  64. package/tests/hydration/components/hmr.ripple +1 -3
  65. package/tests/hydration/components/html.ripple +13 -35
  66. package/tests/hydration/components/if-children.ripple +4 -8
  67. package/tests/hydration/composite.test.js +11 -0
  68. package/tests/server/basic.attributes.test.ripple +50 -0
  69. package/tests/server/basic.components.test.ripple +22 -28
  70. package/tests/server/basic.test.ripple +12 -0
  71. package/tests/server/compiler.test.ripple +43 -4
  72. package/tests/server/composite.props.test.ripple +5 -9
  73. package/tests/server/dynamic-elements.test.ripple +3 -4
  74. package/tests/server/lazy-destructuring.test.ripple +68 -12
  75. package/tests/server/style-identifier.test.ripple +2 -4
  76. package/tsconfig.typecheck.json +4 -0
  77. package/types/index.d.ts +9 -21
  78. package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
  79. package/tests/client/tracked-expression.test.ripple +0 -26
@@ -10,17 +10,15 @@ import type {
10
10
  describe('basic server > components & composition', () => {
11
11
  it('renders with component composition and children', async () => {
12
12
  component Card(props: PropsWithChildren<{}>) {
13
- <div class="card">
14
- <props.children />
15
- </div>
13
+ <div class="card">{props.children}</div>
16
14
  }
17
15
 
18
16
  component Basic() {
19
- <Card>
20
- component children() {
21
- <p>{'Card content here'}</p>
22
- }
23
- </Card>
17
+ component children() {
18
+ <p>{'Card content here'}</p>
19
+ }
20
+
21
+ <Card {children} />
24
22
  }
25
23
 
26
24
  const { body } = await render(Basic);
@@ -37,16 +35,16 @@ describe('basic server > components & composition', () => {
37
35
  component Card(props: PropsWithChildrenOptional<{ test: Component }>) {
38
36
  <div class="card">
39
37
  // @ts-expect-error - ripple automatically handles falsy children
40
- <props.children />
38
+ {props.children}
41
39
  </div>
42
40
  }
43
41
 
44
42
  component Basic() {
45
- <Card>
46
- component test() {
47
- <p>{'Card content here'}</p>
48
- }
49
- </Card>
43
+ component test() {
44
+ <p>{'Card content here'}</p>
45
+ }
46
+
47
+ <Card {test} />
50
48
  }
51
49
 
52
50
  const { body } = await render(Basic);
@@ -62,9 +60,7 @@ describe('basic server > components & composition', () => {
62
60
 
63
61
  it('renders a component when children is set a component prop', async () => {
64
62
  component Card(props: PropsWithChildren<{}>) {
65
- <div class="card">
66
- <props.children />
67
- </div>
63
+ <div class="card">{props.children}</div>
68
64
  }
69
65
 
70
66
  component Basic() {
@@ -174,21 +170,19 @@ describe('basic server > components & composition', () => {
174
170
  <span>{'Hello from Span'}</span>
175
171
  },
176
172
  button: component({ children }: PropsWithChildren<{}>) {
177
- <button>
178
- <children />
179
- </button>
173
+ <button>{children}</button>
180
174
  },
181
175
  };
182
176
 
183
177
  component App() {
178
+ component children() {
179
+ <span>{'Click me!'}</span>
180
+ }
181
+
184
182
  <div>
185
183
  <h1>{'Component as Property Test'}</h1>
186
184
  <UI.span />
187
- <UI.button>
188
- component children() {
189
- <span>{'Click me!'}</span>
190
- }
191
- </UI.button>
185
+ <UI.button {children} />
192
186
  </div>
193
187
  }
194
188
 
@@ -207,13 +201,13 @@ describe('basic server > components & composition', () => {
207
201
 
208
202
  it('handles empty string children', async () => {
209
203
  component Button({ children }: PropsWithChildren<{}>) {
210
- <children />
204
+ {children}
211
205
  }
212
206
 
213
207
  component App() {
214
- let text = '';
208
+ let content = '';
215
209
  <Button>{''}</Button>
216
- <Button>{text}</Button>
210
+ <Button>{content}</Button>
217
211
  }
218
212
 
219
213
  const { body } = await render(App);
@@ -13,6 +13,18 @@ describe('basic client', () => {
13
13
  expect(body).toBeHtml('<div>Hello World</div>');
14
14
  });
15
15
 
16
+ it('renders explicit text interpolation as escaped text', async () => {
17
+ component Basic() {
18
+ let markup = '<span>Not HTML</span>';
19
+
20
+ <div>{text markup}</div>
21
+ }
22
+
23
+ const { body } = await render(Basic);
24
+
25
+ expect(body).toBeHtml('<div>&lt;span>Not HTML&lt;/span></div>');
26
+ });
27
+
16
28
  it('renders tracked state updates', async () => {
17
29
  component Counter() {
18
30
  let &[count] = track(0);
@@ -78,9 +78,7 @@ export component Layout({ children }) {
78
78
  <div>{children}</div>
79
79
  }`;
80
80
 
81
- expect(() => compile(source, 'test.ripple', { mode: 'server' })).toThrow(
82
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
83
- );
81
+ expect(() => compile(source, 'test.ripple', { mode: 'server' })).not.toThrow();
84
82
  });
85
83
 
86
84
  it('throws error for interpolating props.children as text in SSR mode', () => {
@@ -89,10 +87,51 @@ export component Layout(props) {
89
87
  <div>{props.children}</div>
90
88
  }`;
91
89
 
90
+ expect(() => compile(source, 'test.ripple', { mode: 'server' })).not.toThrow();
91
+ });
92
+
93
+ it('throws error for calling children as a function in SSR mode', () => {
94
+ const source = `
95
+ export component Layout({ children }) {
96
+ {children()}
97
+ }`;
98
+
92
99
  expect(() => compile(source, 'test.ripple', { mode: 'server' })).toThrow(
93
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
100
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
94
101
  );
95
102
  });
103
+
104
+ it('throws error for calling props.children as a function in SSR mode', () => {
105
+ const source = `
106
+ export component Layout(props) {
107
+ {props.children()}
108
+ }`;
109
+
110
+ expect(() => compile(source, 'test.ripple', { mode: 'server' })).toThrow(
111
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
112
+ );
113
+ });
114
+
115
+ it('merges explicit children prop with implicit children in SSR output', () => {
116
+ const source = `
117
+ component Card(props) {
118
+ <div>{props.children}</div>
119
+ }
120
+
121
+ export component App() {
122
+ const fallback = 'fallback';
123
+
124
+ <Card children={fallback}>
125
+ <span>{'content'}</span>
126
+ </Card>
127
+ }
128
+ `;
129
+
130
+ const result = compile(source, 'test.ripple', { mode: 'server' }).js.code;
131
+
132
+ expect((result.match(/children:/g) || []).length).toBe(1);
133
+ expect(result).toContain('children: _$_.normalize_children(fallback) ?? _$_.ripple_element(');
134
+ });
96
135
  });
97
136
 
98
137
  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,11 +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}>
85
- <@children />
86
- </button>
82
+ component Button(&{ children, ...rest }: Props) {
83
+ <button {...rest}>{children}</button>
87
84
  <style>
88
85
  .on {
89
86
  color: blue;
@@ -94,10 +91,9 @@ describe('composite > props', () => {
94
91
  </style>
95
92
  }
96
93
 
97
- component Toggle(props: { pressed: Tracked<boolean> }) {
98
- const [pressed, rest] = trackSplit(props, ['pressed']);
94
+ component Toggle(&{ pressed, ...rest }: { pressed: Tracked<boolean> }) {
99
95
  const onClick = () => (pressed.value = !pressed.value);
100
- <Button {...rest.value} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
96
+ <Button {...rest} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
101
97
  <Button class={pressed.value ? 'on' : 'off'} {onClick}>{'button 2'}</Button>
102
98
  }
103
99
 
@@ -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 () => {
@@ -190,7 +190,7 @@ describe('#style identifier (server)', () => {
190
190
  component Wrapper({ children }) {
191
191
  <div class="green">
192
192
  {'Wrapper'}
193
- <children />
193
+ {children}
194
194
  </div>
195
195
 
196
196
  <style>
@@ -235,9 +235,7 @@ describe('#style identifier (server)', () => {
235
235
 
236
236
  it('applies caller scoped hash to slotted children through dynamic components', async () => {
237
237
  component Wrapper({ children }) {
238
- <section>
239
- <children />
240
- </section>
238
+ <section>{children}</section>
241
239
  }
242
240
 
243
241
  component App() {
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["./src/runtime/"]
4
+ }
package/types/index.d.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  export type Component<T = Record<string, any>> = (props: T) => void;
2
2
 
3
- /** Type for JSX children - accepts single child, multiple children, or no children */
4
- export type Children = Component | readonly Component[];
3
+ declare const RIPPLE_ELEMENT: unique symbol;
4
+
5
+ export type RippleElement = {
6
+ readonly render: Function;
7
+ readonly [RIPPLE_ELEMENT]: true;
8
+ };
9
+
10
+ /** Type for implicit children fragments rendered with `{children}`. */
11
+ export type Children = RippleElement;
5
12
 
6
13
  export type CompatApi = {
7
14
  createRoot: () => void;
@@ -176,19 +183,6 @@ export type PropsNoChildren<T extends object = {}> = Expand<T>;
176
183
 
177
184
  type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
178
185
 
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
186
  export function get<V>(tracked: Tracked<V>): V;
193
187
 
194
188
  export function set<V>(tracked: Tracked<V>, value: V): void;
@@ -204,11 +198,6 @@ export function track<V>(
204
198
  // Overload for non-function values
205
199
  export function track<V>(value?: V, get?: (v: V) => V, set?: (next: V, prev: V) => V): Tracked<V>;
206
200
 
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
201
  export interface AddEventOptions extends ExtendedEventOptions {
213
202
  customName?: string;
214
203
  }
@@ -568,7 +557,6 @@ export interface RippleNamespace {
568
557
  urlSearchParams: RippleURLSearchParamsCallable;
569
558
  untrack: typeof untrack;
570
559
  track: typeof track;
571
- trackSplit: typeof trackSplit;
572
560
  style: Record<string, string>;
573
561
  server: ServerBlock;
574
562
  }
@@ -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
- });