ripple 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - ripple@0.3.6
9
+
10
+ ## 0.3.5
11
+
12
+ ### Patch Changes
13
+
14
+ - [#827](https://github.com/Ripple-TS/ripple/pull/827)
15
+ [`218a72c`](https://github.com/Ripple-TS/ripple/commit/218a72c3e663910636eec1d065c58afe30813c84)
16
+ Thanks [@trueadm](https://github.com/trueadm)! - fix(compiler): handle
17
+ UpdateExpression on lazy bindings with default values
18
+
19
+ Update expressions (`++`/`--`) on lazy destructured bindings with default values
20
+ now work correctly. For postfix operations (`count++`), an IIFE captures the
21
+ fallback value before incrementing. Also added `fallback` function to server
22
+ runtime.
23
+
24
+ - Updated dependencies
25
+ [[`218a72c`](https://github.com/Ripple-TS/ripple/commit/218a72c3e663910636eec1d065c58afe30813c84)]:
26
+ - ripple@0.3.5
27
+
3
28
  ## 0.3.4
4
29
 
5
30
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.4",
6
+ "version": "0.3.6",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -105,6 +105,6 @@
105
105
  "vscode-languageserver-types": "^3.17.5"
106
106
  },
107
107
  "peerDependencies": {
108
- "ripple": "0.3.4"
108
+ "ripple": "0.3.6"
109
109
  }
110
110
  }
@@ -85,7 +85,8 @@ function setup_lazy_transforms(pattern, source_id, state, writable) {
85
85
  const binding = state.scope.get(name);
86
86
 
87
87
  if (binding !== null) {
88
- binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
88
+ const has_fallback = path.has_default_value;
89
+ binding.kind = has_fallback ? 'lazy_fallback' : 'lazy';
89
90
 
90
91
  binding.transform = {
91
92
  read: (_) => {
@@ -101,8 +102,40 @@ function setup_lazy_transforms(pattern, source_id, state, writable) {
101
102
  value,
102
103
  );
103
104
  };
104
- binding.transform.update = (node) =>
105
- b.update(node.operator, path.update_expression(source_id), node.prefix);
105
+
106
+ if (has_fallback) {
107
+ // For bindings with default values, generate proper fallback-aware update
108
+ // e.g., count++ with default 0 becomes:
109
+ // (() => { var _v = _$_.fallback(obj.count, 0); obj.count = _v + 1; return _v; })() for postfix
110
+ // (obj.count = _$_.fallback(obj.count, 0) + 1) for prefix
111
+ binding.transform.update = (node) => {
112
+ const member = path.update_expression(source_id);
113
+ const fallback_read = path.expression(source_id);
114
+ const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
115
+
116
+ if (node.prefix) {
117
+ // ++count: return new value
118
+ return b.assignment('=', member, b.binary('+', fallback_read, delta));
119
+ } else {
120
+ // count++: return old value, write new value
121
+ // Use IIFE to declare temp variable
122
+ const temp = b.id('_v');
123
+ return b.call(
124
+ b.arrow(
125
+ [],
126
+ b.block([
127
+ b.var(temp, fallback_read),
128
+ b.stmt(b.assignment('=', member, b.binary('+', temp, delta))),
129
+ b.return(temp),
130
+ ]),
131
+ ),
132
+ );
133
+ }
134
+ };
135
+ } else {
136
+ binding.transform.update = (node) =>
137
+ b.update(node.operator, path.update_expression(source_id), node.prefix);
138
+ }
106
139
  }
107
140
  }
108
141
  }
@@ -2078,6 +2078,14 @@ const visitors = {
2078
2078
  }
2079
2079
  const argument = node.argument;
2080
2080
 
2081
+ // Handle lazy binding updates (e.g., a++ where a is from let &{a} = obj)
2082
+ if (argument.type === 'Identifier') {
2083
+ const binding = context.state.scope?.get(argument.name);
2084
+ if (binding?.transform?.update && binding.node !== argument) {
2085
+ return binding.transform.update(node);
2086
+ }
2087
+ }
2088
+
2081
2089
  if (
2082
2090
  argument.type === 'MemberExpression' &&
2083
2091
  (argument.tracked || (argument.property.type === 'Identifier' && argument.property.tracked))
@@ -1158,7 +1158,7 @@ export interface Binding {
1158
1158
  transform?: {
1159
1159
  read: (node?: AST.Identifier) => AST.Expression;
1160
1160
  assign?: (node: AST.Pattern, value: AST.Expression) => AST.AssignmentExpression;
1161
- update?: (node: AST.UpdateExpression) => AST.UpdateExpression;
1161
+ update?: (node: AST.UpdateExpression) => AST.Expression;
1162
1162
  };
1163
1163
  }
1164
1164
 
@@ -792,3 +792,14 @@ ripple_array.from_async = async function (arrayLike, map_fn, thisArg) {
792
792
  export function ripple_object(obj) {
793
793
  return obj;
794
794
  }
795
+
796
+ /**
797
+ * Returns the fallback value if the given value is undefined.
798
+ * @template T
799
+ * @param {T | undefined} value
800
+ * @param {T} fallback
801
+ * @returns {T}
802
+ */
803
+ export function fallback(value, fallback) {
804
+ return value === undefined ? fallback : value;
805
+ }
@@ -182,4 +182,28 @@ describe('lazy destructuring', () => {
182
182
  render(Test);
183
183
  expect(container.querySelector('pre')!.textContent).toBe('1-99');
184
184
  });
185
+
186
+ it('supports update expressions on lazy bindings with default values', () => {
187
+ component Test() {
188
+ const obj: { count?: number } = {};
189
+ let &{ count = 0 } = obj;
190
+ count++;
191
+ count++;
192
+ <pre>{obj.count}</pre>
193
+ }
194
+
195
+ render(Test);
196
+ expect(container.querySelector('pre')!.textContent).toBe('2');
197
+ });
198
+
199
+ it('supports member access on lazy destructured objects', () => {
200
+ component Test() {
201
+ const obj = { user: { name: 'Alice', age: 30 } };
202
+ const &{ user } = obj;
203
+ <pre>{`${user.name}-${user.age}`}</pre>
204
+ }
205
+
206
+ render(Test);
207
+ expect(container.querySelector('pre')!.textContent).toBe('Alice-30');
208
+ });
185
209
  });
@@ -1,5 +1,5 @@
1
1
  // Basic static components for hydration testing
2
- import type { Component } from 'ripple';
2
+ import type { Children } from 'ripple';
3
3
 
4
4
  export component StaticText() {
5
5
  <div>{'Hello World'}</div>
@@ -97,7 +97,7 @@ component Actions({ playgroundVisible = false }: { playgroundVisible: boolean })
97
97
  </div>
98
98
  }
99
99
 
100
- component Layout({ children }: { children: Component }) {
100
+ component Layout({ children }: { children: Children }) {
101
101
  <main>
102
102
  <div class="container">
103
103
  <children />
@@ -1,6 +1,6 @@
1
- import type { Component } from 'ripple';
1
+ import type { Children } from 'ripple';
2
2
 
3
- export component Layout(&{ children }: { children?: Component }) {
3
+ export component Layout(&{ children }: { children?: Children }) {
4
4
  <div class="layout">
5
5
  <children />
6
6
  </div>
@@ -0,0 +1,103 @@
1
+ describe('lazy destructuring', () => {
2
+ it('lazily accesses object properties', async () => {
3
+ component Inner(&{ a, b }: { a: number; b: string }) {
4
+ <pre>{`${a}-${b}`}</pre>
5
+ }
6
+
7
+ component Test() {
8
+ <Inner a={1} b="hello" />
9
+ }
10
+
11
+ const { body } = await render(Test);
12
+ expect(body).toBeHtml('<pre>1-hello</pre>');
13
+ });
14
+
15
+ it('supports default values in lazy object destructuring', async () => {
16
+ component Test() {
17
+ const obj = { a: 5 };
18
+ const &{ a, b = 99 } = obj;
19
+ <pre>{`${a}-${b}`}</pre>
20
+ }
21
+
22
+ const { body } = await render(Test);
23
+ expect(body).toBeHtml('<pre>5-99</pre>');
24
+ });
25
+
26
+ it('supports let lazy destructuring with assignment writeback', async () => {
27
+ component Test() {
28
+ const obj = { a: 1, b: 2 };
29
+ let &{ a, b } = obj;
30
+ a = 10;
31
+ b = 20;
32
+ <pre>{`${obj.a}-${obj.b}`}</pre>
33
+ }
34
+
35
+ const { body } = await render(Test);
36
+ expect(body).toBeHtml('<pre>10-20</pre>');
37
+ });
38
+
39
+ it('supports compound assignment operators on lazy bindings', async () => {
40
+ component Test() {
41
+ const obj = { a: 5, b: 10 };
42
+ let &{ a, b } = obj;
43
+ a += 3;
44
+ b *= 2;
45
+ <pre>{`${obj.a}-${obj.b}`}</pre>
46
+ }
47
+
48
+ const { body } = await render(Test);
49
+ expect(body).toBeHtml('<pre>8-20</pre>');
50
+ });
51
+
52
+ it('supports update expressions on lazy bindings', async () => {
53
+ component Test() {
54
+ const obj = { count: 0 };
55
+ let &{ count } = obj;
56
+ count++;
57
+ count++;
58
+ count--;
59
+ <pre>{obj.count}</pre>
60
+ }
61
+
62
+ const { body } = await render(Test);
63
+ expect(body).toBeHtml('<pre>1</pre>');
64
+ });
65
+
66
+ it('supports update expressions on lazy bindings with default values', async () => {
67
+ component Test() {
68
+ const obj: { count?: number } = {};
69
+ let &{ count = 0 } = obj;
70
+ count++;
71
+ count++;
72
+ <pre>{obj.count}</pre>
73
+ }
74
+
75
+ const { body } = await render(Test);
76
+ expect(body).toBeHtml('<pre>2</pre>');
77
+ });
78
+
79
+ it('supports function params with lazy destructuring and default values', async () => {
80
+ component Test() {
81
+ function calc(&{ x, y = 100 }: { x: number; y?: number }) {
82
+ return x + y;
83
+ }
84
+ const a = calc({ x: 5, y: 10 });
85
+ const b = calc({ x: 5 });
86
+ <pre>{`${a}-${b}`}</pre>
87
+ }
88
+
89
+ const { body } = await render(Test);
90
+ expect(body).toBeHtml('<pre>15-105</pre>');
91
+ });
92
+
93
+ it('supports member access on lazy destructured objects', async () => {
94
+ component Test() {
95
+ const obj = { user: { name: 'Alice', age: 30 } };
96
+ const &{ user } = obj;
97
+ <pre>{`${user.name}-${user.age}`}</pre>
98
+ }
99
+
100
+ const { body } = await render(Test);
101
+ expect(body).toBeHtml('<pre>Alice-30</pre>');
102
+ });
103
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ripple } from '@ripple-ts/vite-plugin';
3
+
4
+ describe('vite-plugin-ripple hotUpdate', () => {
5
+ it('invalidates SSR modules for non-self-accepting .ripple files', async () => {
6
+ const [plugin] = ripple({ excludeRippleExternalModules: true });
7
+ await plugin.configResolved?.({ root: '/workspace', command: 'serve' });
8
+
9
+ const transform_request = vi.fn().mockResolvedValue(undefined);
10
+ const get_css_module = vi.fn().mockReturnValue(undefined);
11
+ const invalidate_css_module = vi.fn();
12
+ const send_hot_update = vi.fn();
13
+ const get_ssr_modules = vi.fn().mockReturnValue(new Set([{ id: 'ssr:a' }, { id: 'ssr:b' }]));
14
+ const invalidate_ssr_module = vi.fn();
15
+
16
+ const result = await plugin.hotUpdate.handler.call(
17
+ {
18
+ environment: {
19
+ name: 'client',
20
+ transformRequest: transform_request,
21
+ moduleGraph: {
22
+ getModuleById: get_css_module,
23
+ invalidateModule: invalidate_css_module,
24
+ },
25
+ hot: {
26
+ send: send_hot_update,
27
+ },
28
+ },
29
+ },
30
+ {
31
+ file: '/workspace/src/non-component.ripple',
32
+ modules: [{ id: 'client:non-component', isSelfAccepting: false }],
33
+ server: {
34
+ environments: {
35
+ ssr: {
36
+ moduleGraph: {
37
+ getModulesByFile: get_ssr_modules,
38
+ invalidateModule: invalidate_ssr_module,
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ );
45
+
46
+ expect(transform_request).toHaveBeenCalledWith('/src/non-component.ripple');
47
+ expect(get_ssr_modules).toHaveBeenCalledWith('/workspace/src/non-component.ripple');
48
+ expect(invalidate_ssr_module).toHaveBeenCalledTimes(2);
49
+ expect(send_hot_update).toHaveBeenCalledWith({ type: 'full-reload' });
50
+ expect(result).toEqual([]);
51
+ });
52
+
53
+ it('keeps self-accepting .ripple files on Vite HMR path', async () => {
54
+ const [plugin] = ripple({ excludeRippleExternalModules: true });
55
+ await plugin.configResolved?.({ root: '/workspace', command: 'serve' });
56
+
57
+ const transform_request = vi.fn().mockResolvedValue(undefined);
58
+ const get_ssr_modules = vi.fn();
59
+ const invalidate_ssr_module = vi.fn();
60
+ const send_hot_update = vi.fn();
61
+
62
+ const result = await plugin.hotUpdate.handler.call(
63
+ {
64
+ environment: {
65
+ name: 'client',
66
+ transformRequest: transform_request,
67
+ moduleGraph: {
68
+ getModuleById: vi.fn().mockReturnValue(undefined),
69
+ invalidateModule: vi.fn(),
70
+ },
71
+ hot: {
72
+ send: send_hot_update,
73
+ },
74
+ },
75
+ },
76
+ {
77
+ file: '/workspace/src/component.ripple',
78
+ modules: [{ id: 'client:component', isSelfAccepting: true }],
79
+ server: {
80
+ environments: {
81
+ ssr: {
82
+ moduleGraph: {
83
+ getModulesByFile: get_ssr_modules,
84
+ invalidateModule: invalidate_ssr_module,
85
+ },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ );
91
+
92
+ expect(transform_request).toHaveBeenCalledWith('/src/component.ripple');
93
+ expect(get_ssr_modules).not.toHaveBeenCalled();
94
+ expect(invalidate_ssr_module).not.toHaveBeenCalled();
95
+ expect(send_hot_update).not.toHaveBeenCalled();
96
+ expect(result).toBeUndefined();
97
+ });
98
+ });
package/types/index.d.ts CHANGED
@@ -1,5 +1,8 @@
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[];
5
+
3
6
  export type CompatApi = {
4
7
  createRoot: () => void;
5
8
  createComponent: (node: any, children_fn: () => any) => void;
@@ -159,10 +162,10 @@ export type InferComponent<T> = T extends () => infer R ? (R extends Component<a
159
162
  export type Props<K extends PropertyKey = any, V = unknown> = Record<K, V>;
160
163
  export type PropsWithExtras<T extends object> = Props & T & Record<string, unknown>;
161
164
  export type PropsWithChildren<T extends object = {}> = Expand<
162
- Omit<T, 'children'> & { children: Component }
165
+ Omit<T, 'children'> & { children: Children }
163
166
  >;
164
167
  export type PropsWithChildrenOptional<T extends object = {}> = Expand<
165
- Omit<T, 'children'> & { children?: Component }
168
+ Omit<T, 'children'> & { children?: Children }
166
169
  >;
167
170
  export type PropsNoChildren<T extends object = {}> = Expand<T>;
168
171
 
@@ -383,10 +386,10 @@ export const MediaQuery: MediaQueryConstructor;
383
386
 
384
387
  export function Portal<V = HTMLElement>({
385
388
  target,
386
- children: Component,
389
+ children,
387
390
  }: {
388
391
  target: V;
389
- children?: Component;
392
+ children?: Children;
390
393
  }): void;
391
394
 
392
395
  export type GetFunction<V> = () => V;