ripple 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +2 -2
  3. package/src/compiler/errors.js +1 -1
  4. package/src/compiler/index.d.ts +3 -1
  5. package/src/compiler/phases/1-parse/index.js +195 -23
  6. package/src/compiler/phases/2-analyze/index.js +266 -108
  7. package/src/compiler/phases/2-analyze/prune.js +13 -5
  8. package/src/compiler/phases/3-transform/client/index.js +304 -80
  9. package/src/compiler/phases/3-transform/server/index.js +108 -43
  10. package/src/compiler/types/index.d.ts +28 -3
  11. package/src/compiler/types/parse.d.ts +3 -1
  12. package/src/compiler/utils.js +275 -1
  13. package/src/runtime/element.js +39 -0
  14. package/src/runtime/index-client.js +14 -4
  15. package/src/runtime/internal/client/composite.js +10 -6
  16. package/src/runtime/internal/client/expression.js +280 -0
  17. package/src/runtime/internal/client/index.js +4 -0
  18. package/src/runtime/internal/client/portal.js +12 -6
  19. package/src/runtime/internal/server/index.js +26 -1
  20. package/src/utils/builders.js +30 -0
  21. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
  22. package/tests/client/basic/basic.components.test.ripple +85 -87
  23. package/tests/client/basic/basic.errors.test.ripple +4 -8
  24. package/tests/client/basic/basic.rendering.test.ripple +27 -10
  25. package/tests/client/capture-error.js +12 -0
  26. package/tests/client/compiler/compiler.basic.test.ripple +76 -6
  27. package/tests/client/composite/composite.props.test.ripple +1 -3
  28. package/tests/client/composite/composite.render.test.ripple +91 -13
  29. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  30. package/tests/client/return.test.ripple +101 -0
  31. package/tests/client/svg.test.ripple +4 -4
  32. package/tests/client/tsx.test.ripple +486 -0
  33. package/tests/hydration/basic.test.js +23 -0
  34. package/tests/hydration/compiled/client/basic.js +111 -75
  35. package/tests/hydration/compiled/client/composite.js +81 -46
  36. package/tests/hydration/compiled/client/events.js +18 -63
  37. package/tests/hydration/compiled/client/for.js +90 -183
  38. package/tests/hydration/compiled/client/head.js +10 -25
  39. package/tests/hydration/compiled/client/hmr.js +10 -13
  40. package/tests/hydration/compiled/client/html.js +251 -380
  41. package/tests/hydration/compiled/client/if-children.js +35 -45
  42. package/tests/hydration/compiled/client/if.js +2 -2
  43. package/tests/hydration/compiled/client/mixed-control-flow.js +24 -72
  44. package/tests/hydration/compiled/client/nested-control-flow.js +115 -391
  45. package/tests/hydration/compiled/client/portal.js +8 -20
  46. package/tests/hydration/compiled/client/reactivity.js +14 -47
  47. package/tests/hydration/compiled/client/return.js +2 -5
  48. package/tests/hydration/compiled/client/try.js +4 -4
  49. package/tests/hydration/compiled/server/basic.js +64 -31
  50. package/tests/hydration/compiled/server/composite.js +62 -29
  51. package/tests/hydration/compiled/server/hmr.js +24 -37
  52. package/tests/hydration/compiled/server/html.js +472 -611
  53. package/tests/hydration/compiled/server/if-children.js +77 -103
  54. package/tests/hydration/compiled/server/portal.js +8 -8
  55. package/tests/hydration/components/basic.ripple +15 -5
  56. package/tests/hydration/components/composite.ripple +13 -1
  57. package/tests/hydration/components/hmr.ripple +1 -3
  58. package/tests/hydration/components/html.ripple +13 -35
  59. package/tests/hydration/components/if-children.ripple +4 -8
  60. package/tests/hydration/composite.test.js +11 -0
  61. package/tests/server/basic.attributes.test.ripple +50 -0
  62. package/tests/server/basic.components.test.ripple +22 -28
  63. package/tests/server/basic.test.ripple +12 -0
  64. package/tests/server/compiler.test.ripple +25 -8
  65. package/tests/server/composite.props.test.ripple +1 -3
  66. package/tests/server/style-identifier.test.ripple +2 -4
  67. package/tests/utils/compiler-compat-config.test.js +38 -0
  68. package/tests/utils/vite-plugin-config.test.js +113 -0
  69. package/tsconfig.typecheck.json +2 -1
  70. package/types/index.d.ts +8 -11
@@ -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,9 +87,7 @@ export component Layout(props) {
89
87
  <div>{props.children}</div>
90
88
  }`;
91
89
 
92
- expect(() => compile(source, 'test.ripple', { mode: 'server' })).toThrow(
93
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
94
- );
90
+ expect(() => compile(source, 'test.ripple', { mode: 'server' })).not.toThrow();
95
91
  });
96
92
 
97
93
  it('throws error for calling children as a function in SSR mode', () => {
@@ -101,7 +97,7 @@ export component Layout({ children }) {
101
97
  }`;
102
98
 
103
99
  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 />`.',
100
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
105
101
  );
106
102
  });
107
103
 
@@ -112,9 +108,30 @@ export component Layout(props) {
112
108
  }`;
113
109
 
114
110
  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 />`.',
111
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
116
112
  );
117
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
+ });
118
135
  });
119
136
 
120
137
  describe('compiler server block tests', () => {
@@ -80,9 +80,7 @@ describe('composite > props', () => {
80
80
 
81
81
  it('correctly retains prop accessors and reactivity when using rest props', async () => {
82
82
  component Button(&{ children, ...rest }: Props) {
83
- <button {...rest}>
84
- <@children />
85
- </button>
83
+ <button {...rest}>{children}</button>
86
84
  <style>
87
85
  .on {
88
86
  color: blue;
@@ -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,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { compile } from 'ripple/compiler';
3
+
4
+ const source = `
5
+ component App() {
6
+ <tsx:react>
7
+ <div className="react-content">{'Hello'}</div>
8
+ </tsx:react>
9
+ }
10
+ `;
11
+
12
+ describe('compiler tsx compat configuration', () => {
13
+ it('allows tsx compat when no compat config is provided', () => {
14
+ expect(() =>
15
+ compile(source, '/src/App.ripple', {
16
+ mode: 'client',
17
+ }),
18
+ ).not.toThrow();
19
+ });
20
+
21
+ it('throws when tsx compat kind is not configured', () => {
22
+ expect(() =>
23
+ compile(source, '/src/App.ripple', {
24
+ mode: 'client',
25
+ compat_kinds: [],
26
+ }),
27
+ ).toThrow('<tsx:react> requires "react" compat to be configured in ripple.config.ts.');
28
+ });
29
+
30
+ it('allows tsx compat kinds that are configured', () => {
31
+ expect(() =>
32
+ compile(source, '/src/App.ripple', {
33
+ mode: 'client',
34
+ compat_kinds: ['react'],
35
+ }),
36
+ ).not.toThrow();
37
+ });
38
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { RenderRoute, resolveRippleConfig } from '@ripple-ts/vite-plugin';
3
+
4
+ const fake_compat_factory = Object.assign(
5
+ () =>
6
+ Object.assign(
7
+ {
8
+ createComponent() {},
9
+ createRoot() {
10
+ return () => {};
11
+ },
12
+ },
13
+ {
14
+ __ripple_compat__: {
15
+ from: '@ripple-ts/compat-react',
16
+ factory: 'createReactCompat',
17
+ },
18
+ },
19
+ ),
20
+ {
21
+ __ripple_compat__: {
22
+ from: '@ripple-ts/compat-react',
23
+ factory: 'createReactCompat',
24
+ },
25
+ },
26
+ );
27
+
28
+ describe('vite-plugin-ripple config resolution', () => {
29
+ it('preserves compat descriptors and applies defaults', () => {
30
+ const config = resolveRippleConfig({
31
+ router: {
32
+ routes: [
33
+ new RenderRoute({
34
+ path: '/',
35
+ entry: '/src/App.ripple',
36
+ }),
37
+ ],
38
+ },
39
+ compat: {
40
+ react: {
41
+ from: '@ripple-ts/compat-react',
42
+ factory: 'createReactCompat',
43
+ },
44
+ },
45
+ });
46
+
47
+ expect(config.compat).toEqual({
48
+ react: {
49
+ from: '@ripple-ts/compat-react',
50
+ factory: 'createReactCompat',
51
+ },
52
+ });
53
+ expect(config.middlewares).toEqual([]);
54
+ expect(config.platform.env).toEqual({});
55
+ expect(config.server.trustProxy).toBe(false);
56
+ });
57
+
58
+ it('defaults compat to an empty object', () => {
59
+ const config = resolveRippleConfig({
60
+ router: {
61
+ routes: [],
62
+ },
63
+ });
64
+
65
+ expect(config.compat).toEqual({});
66
+ });
67
+
68
+ it('allows compat-only configs without routes', () => {
69
+ const config = resolveRippleConfig({
70
+ compat: {
71
+ react: fake_compat_factory,
72
+ },
73
+ });
74
+
75
+ expect(config.router.routes).toEqual([]);
76
+ expect(config.compat.react).toEqual({
77
+ from: '@ripple-ts/compat-react',
78
+ factory: 'createReactCompat',
79
+ });
80
+ });
81
+
82
+ it('normalizes imported compat factories to descriptors', () => {
83
+ const config = resolveRippleConfig({
84
+ router: {
85
+ routes: [],
86
+ },
87
+ compat: {
88
+ react: fake_compat_factory,
89
+ },
90
+ });
91
+
92
+ expect(config.compat.react).toEqual({
93
+ from: '@ripple-ts/compat-react',
94
+ factory: 'createReactCompat',
95
+ });
96
+ });
97
+
98
+ it('normalizes invoked compat entries to descriptors', () => {
99
+ const config = resolveRippleConfig({
100
+ router: {
101
+ routes: [],
102
+ },
103
+ compat: {
104
+ react: fake_compat_factory(),
105
+ },
106
+ });
107
+
108
+ expect(config.compat.react).toEqual({
109
+ from: '@ripple-ts/compat-react',
110
+ factory: 'createReactCompat',
111
+ });
112
+ });
113
+ });
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
- "include": ["./src/runtime/"]
3
+ "include": ["./src/"],
4
+ "exclude": ["./tests/"]
4
5
  }
package/types/index.d.ts CHANGED
@@ -1,26 +1,23 @@
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;
5
4
 
6
- export type CompatApi = {
7
- createRoot: () => void;
8
- createComponent: (node: any, children_fn: () => any) => void;
9
- jsx: (type: any, props: any) => any;
5
+ export type RippleElement = {
6
+ readonly render: Function;
7
+ readonly [RIPPLE_ELEMENT]: true;
10
8
  };
11
9
 
12
- export type CompatOptions = {
13
- [key: string]: CompatApi;
14
- };
10
+ /** Type for implicit children fragments rendered with `{children}`. */
11
+ export type Children = RippleElement;
15
12
 
16
13
  export function mount(
17
14
  component: Component,
18
- options: { target: HTMLElement; props?: Record<string, any>; compat?: CompatOptions },
15
+ options: { target: HTMLElement; props?: Record<string, any> },
19
16
  ): () => void;
20
17
 
21
18
  export function hydrate(
22
19
  component: Component,
23
- options: { target: HTMLElement; props?: Record<string, any>; compat?: CompatOptions },
20
+ options: { target: HTMLElement; props?: Record<string, any> },
24
21
  ): () => void;
25
22
 
26
23
  export function tick(): Promise<void>;