ripple 0.3.5 → 0.3.7
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 +43 -0
- package/package.json +2 -2
- package/src/compiler/phases/2-analyze/index.js +230 -11
- package/src/compiler/phases/3-transform/client/index.js +36 -12
- package/src/compiler/phases/3-transform/server/index.js +4 -12
- package/src/compiler/types/index.d.ts +3 -1
- package/src/compiler/utils.js +10 -6
- package/src/runtime/internal/client/index.js +2 -0
- package/src/runtime/internal/client/runtime.js +95 -45
- package/src/runtime/internal/client/types.d.ts +10 -0
- package/src/runtime/internal/client/utils.js +12 -0
- package/src/runtime/internal/server/index.js +89 -17
- package/src/runtime/internal/server/types.d.ts +10 -0
- package/src/utils/ast.js +1 -1
- package/tests/client/compiler/compiler.basic.test.ripple +29 -0
- package/tests/client/compiler/compiler.tracked-access.test.ripple +66 -0
- package/tests/client/lazy-destructuring.test.ripple +55 -1
- package/tests/hydration/components/basic.ripple +2 -2
- package/tests/hydration/components/composite.ripple +2 -2
- package/tests/server/lazy-destructuring.test.ripple +49 -0
- package/tests/utils/vite-plugin-hmr.test.js +98 -0
- package/types/index.d.ts +14 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.3.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#832](https://github.com/Ripple-TS/ripple/pull/832)
|
|
8
|
+
[`9ca9310`](https://github.com/Ripple-TS/ripple/commit/9ca9310550a800f4435821ed84b24bdd4f243117)
|
|
9
|
+
Thanks [@trueadm](https://github.com/trueadm)! - Fix lazy array rest
|
|
10
|
+
destructuring for tracked and array-like values by routing rest extraction
|
|
11
|
+
through a shared `array_slice` helper instead of calling `.slice()` directly on
|
|
12
|
+
the source.
|
|
13
|
+
|
|
14
|
+
- [#832](https://github.com/Ripple-TS/ripple/pull/832)
|
|
15
|
+
[`9ca9310`](https://github.com/Ripple-TS/ripple/commit/9ca9310550a800f4435821ed84b24bdd4f243117)
|
|
16
|
+
Thanks [@trueadm](https://github.com/trueadm)! - Allow tracked tuple `.length`
|
|
17
|
+
member access in compiler analysis and simplify tracked direct-access validation
|
|
18
|
+
into a single combined condition.
|
|
19
|
+
|
|
20
|
+
- [#832](https://github.com/Ripple-TS/ripple/pull/832)
|
|
21
|
+
[`9ca9310`](https://github.com/Ripple-TS/ripple/commit/9ca9310550a800f4435821ed84b24bdd4f243117)
|
|
22
|
+
Thanks [@trueadm](https://github.com/trueadm)! - Fix `to_ts` output for lazy
|
|
23
|
+
array destructuring so it keeps direct destructuring syntax for `track()` and
|
|
24
|
+
`trackSplit()` instead of expanding through an intermediate `lazy` variable.
|
|
25
|
+
|
|
26
|
+
- [#832](https://github.com/Ripple-TS/ripple/pull/832)
|
|
27
|
+
[`9ca9310`](https://github.com/Ripple-TS/ripple/commit/9ca9310550a800f4435821ed84b24bdd4f243117)
|
|
28
|
+
Thanks [@trueadm](https://github.com/trueadm)! - Replace tracked `get()`/`set()`
|
|
29
|
+
APIs with a `value` getter/setter across runtime, types, analyzer tracked-access
|
|
30
|
+
rules, and lazy destructuring tests.
|
|
31
|
+
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
[[`9ca9310`](https://github.com/Ripple-TS/ripple/commit/9ca9310550a800f4435821ed84b24bdd4f243117),
|
|
34
|
+
[`9ca9310`](https://github.com/Ripple-TS/ripple/commit/9ca9310550a800f4435821ed84b24bdd4f243117),
|
|
35
|
+
[`9ca9310`](https://github.com/Ripple-TS/ripple/commit/9ca9310550a800f4435821ed84b24bdd4f243117),
|
|
36
|
+
[`9ca9310`](https://github.com/Ripple-TS/ripple/commit/9ca9310550a800f4435821ed84b24bdd4f243117)]:
|
|
37
|
+
- ripple@0.3.7
|
|
38
|
+
|
|
39
|
+
## 0.3.6
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- Updated dependencies []:
|
|
44
|
+
- ripple@0.3.6
|
|
45
|
+
|
|
3
46
|
## 0.3.5
|
|
4
47
|
|
|
5
48
|
### 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.
|
|
6
|
+
"version": "0.3.7",
|
|
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.
|
|
108
|
+
"ripple": "0.3.7"
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -76,8 +76,16 @@ function mark_control_flow_has_template(path) {
|
|
|
76
76
|
* @param {AST.Identifier} source_id - The identifier to access properties on
|
|
77
77
|
* @param {AnalysisState} state - The analysis state
|
|
78
78
|
* @param {boolean} writable - Whether assignments/updates should be supported (let vs const)
|
|
79
|
+
* @param {boolean} is_track_call - Whether the RHS is a Ripple track() call
|
|
79
80
|
*/
|
|
80
|
-
function setup_lazy_transforms(pattern, source_id, state, writable) {
|
|
81
|
+
function setup_lazy_transforms(pattern, source_id, state, writable, is_track_call) {
|
|
82
|
+
// For ArrayPattern from track() calls, use direct get/set calls as a fast path
|
|
83
|
+
// instead of going through prototype getters source[0]/source[1]
|
|
84
|
+
if (pattern.type === 'ArrayPattern' && is_track_call) {
|
|
85
|
+
setup_lazy_array_transforms(pattern, source_id, state, writable);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
const paths = extract_paths(pattern);
|
|
82
90
|
|
|
83
91
|
for (const path of paths) {
|
|
@@ -141,6 +149,185 @@ function setup_lazy_transforms(pattern, source_id, state, writable) {
|
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Set up fast-path transforms for lazy array destructuring of tracked values.
|
|
154
|
+
* For index 0 (the value): uses _$_.get/set/update directly instead of source[0] getters.
|
|
155
|
+
* For index 1 (the tracked ref): returns source directly instead of source[1].
|
|
156
|
+
* @param {AST.ArrayPattern} pattern - The array destructuring pattern
|
|
157
|
+
* @param {AST.Identifier} source_id - The identifier for the tracked value
|
|
158
|
+
* @param {AnalysisState} state - The analysis state
|
|
159
|
+
* @param {boolean} writable - Whether assignments/updates should be supported
|
|
160
|
+
*/
|
|
161
|
+
function setup_lazy_array_transforms(pattern, source_id, state, writable) {
|
|
162
|
+
for (let i = 0; i < pattern.elements.length; i++) {
|
|
163
|
+
const element = pattern.elements[i];
|
|
164
|
+
if (!element) continue;
|
|
165
|
+
|
|
166
|
+
// Rest elements — fall back to generic source.slice(i)
|
|
167
|
+
if (element.type === 'RestElement') {
|
|
168
|
+
const rest_paths = extract_paths(pattern);
|
|
169
|
+
for (const path of rest_paths) {
|
|
170
|
+
if (!path.is_rest) continue;
|
|
171
|
+
const name = /** @type {AST.Identifier} */ (path.node).name;
|
|
172
|
+
const binding = state.scope.get(name);
|
|
173
|
+
if (binding !== null) {
|
|
174
|
+
binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
|
|
175
|
+
binding.transform = {
|
|
176
|
+
read: (_) => path.expression(source_id),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const actual = element.type === 'AssignmentPattern' ? element.left : element;
|
|
184
|
+
const has_fallback = element.type === 'AssignmentPattern';
|
|
185
|
+
const fallback_value = has_fallback
|
|
186
|
+
? /** @type {AST.AssignmentPattern} */ (element).right
|
|
187
|
+
: null;
|
|
188
|
+
|
|
189
|
+
if (actual.type === 'Identifier' && i <= 1) {
|
|
190
|
+
const name = actual.name;
|
|
191
|
+
const binding = state.scope.get(name);
|
|
192
|
+
if (binding === null) continue;
|
|
193
|
+
|
|
194
|
+
binding.kind = has_fallback ? 'lazy_fallback' : 'lazy';
|
|
195
|
+
|
|
196
|
+
if (i === 0) {
|
|
197
|
+
// Fast path for index 0: use _$_.get(source) instead of source[0]
|
|
198
|
+
const read_expr = has_fallback
|
|
199
|
+
? () => b.call('_$_.fallback', b.call('_$_.get', source_id), fallback_value)
|
|
200
|
+
: () => b.call('_$_.get', source_id);
|
|
201
|
+
|
|
202
|
+
// Signal that read already produces an unwrapped value (calls _$_.get internally)
|
|
203
|
+
binding.read_unwraps = true;
|
|
204
|
+
|
|
205
|
+
binding.transform = {
|
|
206
|
+
read: (_) => read_expr(),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (writable) {
|
|
210
|
+
binding.transform.assign = (_, value) => {
|
|
211
|
+
return b.call('_$_.set', source_id, value);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (has_fallback) {
|
|
215
|
+
binding.transform.update = (node) => {
|
|
216
|
+
const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
|
|
217
|
+
const temp = b.id('_v');
|
|
218
|
+
|
|
219
|
+
if (node.prefix) {
|
|
220
|
+
// ++count: compute new value and set it, return new value
|
|
221
|
+
return b.call(
|
|
222
|
+
b.arrow(
|
|
223
|
+
[],
|
|
224
|
+
b.block([
|
|
225
|
+
b.var(temp, b.binary('+', read_expr(), delta)),
|
|
226
|
+
b.stmt(b.call('_$_.set', source_id, temp)),
|
|
227
|
+
b.return(temp),
|
|
228
|
+
]),
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
// count++: read old value, set new value, return old value
|
|
233
|
+
return b.call(
|
|
234
|
+
b.arrow(
|
|
235
|
+
[],
|
|
236
|
+
b.block([
|
|
237
|
+
b.var(temp, read_expr()),
|
|
238
|
+
b.stmt(b.call('_$_.set', source_id, b.binary('+', temp, delta))),
|
|
239
|
+
b.return(temp),
|
|
240
|
+
]),
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
} else {
|
|
246
|
+
binding.transform.update = (node) => {
|
|
247
|
+
const fn_name = node.prefix ? '_$_.update_pre' : '_$_.update';
|
|
248
|
+
const args = [source_id];
|
|
249
|
+
if (node.operator === '--') {
|
|
250
|
+
args.push(b.literal(-1));
|
|
251
|
+
}
|
|
252
|
+
return b.call(fn_name, ...args);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
// Fast path for index 1: source itself is the tracked ref
|
|
258
|
+
binding.transform = {
|
|
259
|
+
read: (_) => source_id,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
// Nested patterns or indices > 1: fall back to generic source[i] access via extract_paths
|
|
264
|
+
/** @type {(object: AST.Expression) => AST.Expression} */
|
|
265
|
+
const base_expression =
|
|
266
|
+
i === 0
|
|
267
|
+
? (object) => b.call('_$_.get', object)
|
|
268
|
+
: i === 1
|
|
269
|
+
? (object) => object
|
|
270
|
+
: (object) => b.member(object, b.literal(i), true);
|
|
271
|
+
|
|
272
|
+
const inner_paths = extract_paths(element);
|
|
273
|
+
for (const path of inner_paths) {
|
|
274
|
+
const name = /** @type {AST.Identifier} */ (path.node).name;
|
|
275
|
+
const binding = state.scope.get(name);
|
|
276
|
+
if (binding === null) continue;
|
|
277
|
+
|
|
278
|
+
binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
|
|
279
|
+
|
|
280
|
+
binding.transform = {
|
|
281
|
+
read: (_) => path.expression(base_expression(source_id)),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
if (writable) {
|
|
285
|
+
binding.transform.assign = (node, value) => {
|
|
286
|
+
return b.assignment(
|
|
287
|
+
'=',
|
|
288
|
+
/** @type {AST.MemberExpression} */ (
|
|
289
|
+
path.update_expression(base_expression(source_id))
|
|
290
|
+
),
|
|
291
|
+
value,
|
|
292
|
+
);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (path.has_default_value) {
|
|
296
|
+
binding.transform.update = (node) => {
|
|
297
|
+
const member = path.update_expression(base_expression(source_id));
|
|
298
|
+
const fallback_read = path.expression(base_expression(source_id));
|
|
299
|
+
const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
|
|
300
|
+
|
|
301
|
+
if (node.prefix) {
|
|
302
|
+
return b.assignment('=', member, b.binary('+', fallback_read, delta));
|
|
303
|
+
} else {
|
|
304
|
+
const temp = b.id('_v');
|
|
305
|
+
return b.call(
|
|
306
|
+
b.arrow(
|
|
307
|
+
[],
|
|
308
|
+
b.block([
|
|
309
|
+
b.var(temp, fallback_read),
|
|
310
|
+
b.stmt(b.assignment('=', member, b.binary('+', temp, delta))),
|
|
311
|
+
b.return(temp),
|
|
312
|
+
]),
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
} else {
|
|
318
|
+
binding.transform.update = (node) =>
|
|
319
|
+
b.update(
|
|
320
|
+
node.operator,
|
|
321
|
+
path.update_expression(base_expression(source_id)),
|
|
322
|
+
node.prefix,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
144
331
|
/**
|
|
145
332
|
* @param {AST.Function} node
|
|
146
333
|
* @param {AnalysisContext} context
|
|
@@ -158,7 +345,7 @@ function visit_function(node, context) {
|
|
|
158
345
|
|
|
159
346
|
if ((param.type === 'ObjectPattern' || param.type === 'ArrayPattern') && param.lazy) {
|
|
160
347
|
const param_id = b.id(context.state.scope.generate('param'));
|
|
161
|
-
setup_lazy_transforms(param, param_id, context.state, true);
|
|
348
|
+
setup_lazy_transforms(param, param_id, context.state, true, false);
|
|
162
349
|
// Store the generated identifier name on the pattern for the transform phase
|
|
163
350
|
param.metadata = { ...param.metadata, lazy_id: param_id.name };
|
|
164
351
|
}
|
|
@@ -391,6 +578,21 @@ const visitors = {
|
|
|
391
578
|
}
|
|
392
579
|
}
|
|
393
580
|
|
|
581
|
+
// Lazy bindings from track() calls (read_unwraps) are inherently reactive —
|
|
582
|
+
// propagate tracking even without the @ prefix so that control flow (if/for/switch)
|
|
583
|
+
// and early returns create reactive blocks
|
|
584
|
+
if (
|
|
585
|
+
!node.tracked &&
|
|
586
|
+
binding?.read_unwraps &&
|
|
587
|
+
is_reference(node, /** @type {AST.Node} */ (parent)) &&
|
|
588
|
+
binding.node !== node
|
|
589
|
+
) {
|
|
590
|
+
mark_as_tracked(context.path);
|
|
591
|
+
if (context.state.metadata?.tracking === false) {
|
|
592
|
+
context.state.metadata.tracking = true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
394
596
|
context.next();
|
|
395
597
|
},
|
|
396
598
|
|
|
@@ -485,13 +687,27 @@ const visitors = {
|
|
|
485
687
|
binding.initial?.type === 'CallExpression' &&
|
|
486
688
|
is_ripple_track_call(binding.initial.callee, context)
|
|
487
689
|
) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
690
|
+
const is_allowed_tracked_access =
|
|
691
|
+
// Allow [0] and [1] indexed access on tracked objects.
|
|
692
|
+
(node.computed &&
|
|
693
|
+
node.property.type === 'Literal' &&
|
|
694
|
+
(node.property.value === 0 || node.property.value === 1)) ||
|
|
695
|
+
// Allow .value and .length property access on tracked objects.
|
|
696
|
+
(!node.computed &&
|
|
697
|
+
node.property.type === 'Identifier' &&
|
|
698
|
+
(node.property.name === 'value' || node.property.name === 'length'));
|
|
699
|
+
|
|
700
|
+
if (is_allowed_tracked_access) {
|
|
701
|
+
// pass through
|
|
702
|
+
} else {
|
|
703
|
+
error(
|
|
704
|
+
`Accessing a tracked object directly is not allowed, use the \`@\` prefix to read the value inside a tracked object - for example \`@${node.object.name}${node.property.type === 'Identifier' ? `.${node.property.name}` : ''}\``,
|
|
705
|
+
context.state.analysis.module.filename,
|
|
706
|
+
node.object,
|
|
707
|
+
context.state.loose ? context.state.analysis.errors : undefined,
|
|
708
|
+
context.state.analysis.comments,
|
|
709
|
+
);
|
|
710
|
+
}
|
|
495
711
|
}
|
|
496
712
|
}
|
|
497
713
|
|
|
@@ -570,7 +786,10 @@ const visitors = {
|
|
|
570
786
|
) {
|
|
571
787
|
const lazy_id = b.id(state.scope.generate('lazy'));
|
|
572
788
|
const writable = node.kind !== 'const';
|
|
573
|
-
|
|
789
|
+
const init_is_track =
|
|
790
|
+
declarator.init?.type === 'CallExpression' &&
|
|
791
|
+
is_ripple_track_call(declarator.init.callee, context) === 'track';
|
|
792
|
+
setup_lazy_transforms(declarator.id, lazy_id, state, writable, !!init_is_track);
|
|
574
793
|
// Store the generated identifier name on the pattern for the transform phase
|
|
575
794
|
declarator.id.metadata = { ...declarator.id.metadata, lazy_id: lazy_id.name };
|
|
576
795
|
}
|
|
@@ -651,7 +870,7 @@ const visitors = {
|
|
|
651
870
|
|
|
652
871
|
if ((props.type === 'ObjectPattern' || props.type === 'ArrayPattern') && props.lazy) {
|
|
653
872
|
// Lazy destructuring: &{...} or &[...] — set up lazy transforms
|
|
654
|
-
setup_lazy_transforms(props, b.id('__props'), context.state, true);
|
|
873
|
+
setup_lazy_transforms(props, b.id('__props'), context.state, true, false);
|
|
655
874
|
} else if (props.type === 'AssignmentPattern') {
|
|
656
875
|
error(
|
|
657
876
|
'Props are always an object, use destructured props with default values instead',
|
|
@@ -533,7 +533,7 @@ const visitors = {
|
|
|
533
533
|
if (context.state.metadata?.tracking === false) {
|
|
534
534
|
context.state.metadata.tracking = true;
|
|
535
535
|
}
|
|
536
|
-
if (node.tracked) {
|
|
536
|
+
if (node.tracked && !binding?.read_unwraps) {
|
|
537
537
|
return b.call('_$_.get', build_getter(node, context));
|
|
538
538
|
}
|
|
539
539
|
}
|
|
@@ -634,17 +634,9 @@ const visitors = {
|
|
|
634
634
|
}
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
? callee.name === 'trackSplit'
|
|
641
|
-
? 'track_split'
|
|
642
|
-
: 'track'
|
|
643
|
-
: callee.type === 'MemberExpression' && callee.property.type === 'Identifier'
|
|
644
|
-
? callee.property.name === 'trackSplit'
|
|
645
|
-
? 'track_split'
|
|
646
|
-
: 'track'
|
|
647
|
-
: 'track';
|
|
637
|
+
const matched_track_call = !context.state.to_ts ? is_ripple_track_call(callee, context) : null;
|
|
638
|
+
if (matched_track_call) {
|
|
639
|
+
const track_method_name = matched_track_call === 'trackSplit' ? 'track_split' : 'track';
|
|
648
640
|
|
|
649
641
|
if (callee.type === 'Identifier' && callee.name === 'track') {
|
|
650
642
|
if (node.arguments.length === 0) {
|
|
@@ -943,6 +935,20 @@ const visitors = {
|
|
|
943
935
|
}
|
|
944
936
|
}
|
|
945
937
|
|
|
938
|
+
if (context.state.to_ts) {
|
|
939
|
+
for (const declarator of node.declarations) {
|
|
940
|
+
if (
|
|
941
|
+
(declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern') &&
|
|
942
|
+
declarator.id.lazy
|
|
943
|
+
) {
|
|
944
|
+
declarator.id.lazy = false;
|
|
945
|
+
if (declarator.id.metadata?.lazy_id) {
|
|
946
|
+
delete declarator.id.metadata.lazy_id;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
946
952
|
return context.next();
|
|
947
953
|
},
|
|
948
954
|
|
|
@@ -2019,6 +2025,24 @@ const visitors = {
|
|
|
2019
2025
|
|
|
2020
2026
|
const left = node.left;
|
|
2021
2027
|
|
|
2028
|
+
// Handle lazy binding assignments (e.g., value = 5 where value is from let &[value] = track(0))
|
|
2029
|
+
// Must come before the left.tracked check to use the binding's transform
|
|
2030
|
+
if (left.type === 'Identifier') {
|
|
2031
|
+
const binding = context.state.scope?.get(left.name);
|
|
2032
|
+
if (binding?.transform?.assign && binding.node !== left) {
|
|
2033
|
+
let value = /** @type {AST.Expression} */ (context.visit(node.right));
|
|
2034
|
+
|
|
2035
|
+
// For compound operators (+=, -=, *=, /=), expand to read + operation
|
|
2036
|
+
if (node.operator !== '=') {
|
|
2037
|
+
const operator = node.operator.slice(0, -1); // '+=' -> '+'
|
|
2038
|
+
const current = binding.transform.read(left);
|
|
2039
|
+
value = b.binary(/** @type {AST.BinaryOperator} */ (operator), current, value);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
return binding.transform.assign(left, value);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2022
2046
|
if (
|
|
2023
2047
|
left.type === 'MemberExpression' &&
|
|
2024
2048
|
(left.tracked || (left.property.type === 'Identifier' && left.property.tracked))
|
|
@@ -334,7 +334,7 @@ const visitors = {
|
|
|
334
334
|
(binding.kind === 'lazy' || binding.kind === 'lazy_fallback')
|
|
335
335
|
) {
|
|
336
336
|
const transformed = binding.transform.read(node);
|
|
337
|
-
if (node.tracked) {
|
|
337
|
+
if (node.tracked && !binding.read_unwraps) {
|
|
338
338
|
const is_right_side_of_assignment =
|
|
339
339
|
parent.type === 'AssignmentExpression' && parent.right === node;
|
|
340
340
|
if (
|
|
@@ -520,17 +520,9 @@ const visitors = {
|
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
? callee.name === 'trackSplit'
|
|
527
|
-
? 'track_split'
|
|
528
|
-
: 'track'
|
|
529
|
-
: callee.type === 'MemberExpression' && callee.property.type === 'Identifier'
|
|
530
|
-
? callee.property.name === 'trackSplit'
|
|
531
|
-
? 'track_split'
|
|
532
|
-
: 'track'
|
|
533
|
-
: 'track';
|
|
523
|
+
const track_call_name = is_ripple_track_call(callee, context);
|
|
524
|
+
if (track_call_name) {
|
|
525
|
+
const track_method_name = track_call_name === 'trackSplit' ? 'track_split' : 'track';
|
|
534
526
|
|
|
535
527
|
return {
|
|
536
528
|
...node,
|
|
@@ -1157,9 +1157,11 @@ export interface Binding {
|
|
|
1157
1157
|
/** Transform functions for reading, assigning, and updating this binding */
|
|
1158
1158
|
transform?: {
|
|
1159
1159
|
read: (node?: AST.Identifier) => AST.Expression;
|
|
1160
|
-
assign?: (node: AST.
|
|
1160
|
+
assign?: (node: AST.Identifier, value: AST.Expression) => AST.Expression;
|
|
1161
1161
|
update?: (node: AST.UpdateExpression) => AST.Expression;
|
|
1162
1162
|
};
|
|
1163
|
+
/** Whether the read transform already produces an unwrapped value (calls get() internally) */
|
|
1164
|
+
read_unwraps?: boolean;
|
|
1163
1165
|
}
|
|
1164
1166
|
|
|
1165
1167
|
/**
|
package/src/compiler/utils.js
CHANGED
|
@@ -289,27 +289,31 @@ export function is_component_level_function(context) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
/**
|
|
292
|
-
* Returns
|
|
292
|
+
* Returns the matched Ripple tracking call name
|
|
293
293
|
* @param {AST.Expression | AST.Super} callee
|
|
294
294
|
* @param {CommonContext} context
|
|
295
|
-
* @returns {
|
|
295
|
+
* @returns {'track' | 'trackSplit' | null}
|
|
296
296
|
*/
|
|
297
297
|
export function is_ripple_track_call(callee, context) {
|
|
298
298
|
// Super expressions cannot be Ripple track calls
|
|
299
|
-
if (callee.type === 'Super') return
|
|
299
|
+
if (callee.type === 'Super') return null;
|
|
300
300
|
|
|
301
301
|
if (callee.type === 'Identifier' && (callee.name === 'track' || callee.name === 'trackSplit')) {
|
|
302
|
-
return is_ripple_import(callee, context);
|
|
302
|
+
return is_ripple_import(callee, context) ? callee.name : null;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
if (
|
|
306
306
|
callee.type === 'MemberExpression' &&
|
|
307
307
|
callee.object.type === 'Identifier' &&
|
|
308
308
|
callee.property.type === 'Identifier' &&
|
|
309
309
|
(callee.property.name === 'track' || callee.property.name === 'trackSplit') &&
|
|
310
310
|
!callee.computed &&
|
|
311
311
|
is_ripple_import(callee, context)
|
|
312
|
-
)
|
|
312
|
+
) {
|
|
313
|
+
return callee.property.name;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null;
|
|
313
317
|
}
|
|
314
318
|
|
|
315
319
|
/**
|
|
@@ -80,6 +80,8 @@ export { switch_block as switch } from './switch.js';
|
|
|
80
80
|
|
|
81
81
|
export { template, append, text } from './template.js';
|
|
82
82
|
|
|
83
|
+
export { array_slice } from './utils.js';
|
|
84
|
+
|
|
83
85
|
export { ripple_array } from '../../array.js';
|
|
84
86
|
|
|
85
87
|
export { ripple_object } from '../../object.js';
|
|
@@ -289,6 +289,95 @@ export function run_block(block) {
|
|
|
289
289
|
|
|
290
290
|
var empty_get_set = { get: undefined, set: undefined };
|
|
291
291
|
|
|
292
|
+
class TrackedValue {
|
|
293
|
+
/**
|
|
294
|
+
* @param {any} v
|
|
295
|
+
* @param {Block} block
|
|
296
|
+
* @param {{ get?: Function; set?: Function }} a
|
|
297
|
+
*/
|
|
298
|
+
constructor(v, block, a) {
|
|
299
|
+
this.a = a;
|
|
300
|
+
this.b = block;
|
|
301
|
+
this.c = 0;
|
|
302
|
+
this.f = TRACKED;
|
|
303
|
+
this.__v = v;
|
|
304
|
+
}
|
|
305
|
+
get [0]() {
|
|
306
|
+
return get_tracked(/** @type {Tracked} */ (this));
|
|
307
|
+
}
|
|
308
|
+
set [0](v) {
|
|
309
|
+
set(/** @type {Tracked} */ (this), v);
|
|
310
|
+
}
|
|
311
|
+
get [1]() {
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
get value() {
|
|
315
|
+
return get_tracked(/** @type {Tracked} */ (this));
|
|
316
|
+
}
|
|
317
|
+
/** @param {any} v */
|
|
318
|
+
set value(v) {
|
|
319
|
+
set(/** @type {Tracked} */ (this), v);
|
|
320
|
+
}
|
|
321
|
+
/** @returns {2} */
|
|
322
|
+
get length() {
|
|
323
|
+
return 2;
|
|
324
|
+
}
|
|
325
|
+
*[Symbol.iterator]() {
|
|
326
|
+
yield get_tracked(/** @type {Tracked} */ (this));
|
|
327
|
+
yield this;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class DerivedValue {
|
|
332
|
+
/**
|
|
333
|
+
* @param {Function} fn
|
|
334
|
+
* @param {Block} block
|
|
335
|
+
* @param {{ get?: Function; set?: Function }} a
|
|
336
|
+
*/
|
|
337
|
+
constructor(fn, block, a) {
|
|
338
|
+
this.a = a;
|
|
339
|
+
this.b = block;
|
|
340
|
+
/** @type {null | Block[]} */
|
|
341
|
+
this.blocks = null;
|
|
342
|
+
this.c = 0;
|
|
343
|
+
this.co = active_component;
|
|
344
|
+
/** @type {null | Dependency} */
|
|
345
|
+
this.d = null;
|
|
346
|
+
this.f = TRACKED | DERIVED;
|
|
347
|
+
this.fn = fn;
|
|
348
|
+
this.__v = UNINITIALIZED;
|
|
349
|
+
}
|
|
350
|
+
get [0]() {
|
|
351
|
+
return get_derived(/** @type {Derived} */ (this));
|
|
352
|
+
}
|
|
353
|
+
set [0](v) {
|
|
354
|
+
set(/** @type {Derived} */ (this), v);
|
|
355
|
+
}
|
|
356
|
+
get [1]() {
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
get value() {
|
|
360
|
+
return get_derived(/** @type {Derived} */ (this));
|
|
361
|
+
}
|
|
362
|
+
/** @param {any} v */
|
|
363
|
+
set value(v) {
|
|
364
|
+
set(/** @type {Derived} */ (this), v);
|
|
365
|
+
}
|
|
366
|
+
/** @returns {2} */
|
|
367
|
+
get length() {
|
|
368
|
+
return 2;
|
|
369
|
+
}
|
|
370
|
+
*[Symbol.iterator]() {
|
|
371
|
+
yield get_derived(/** @type {Derived} */ (this));
|
|
372
|
+
yield this;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (DEV) {
|
|
377
|
+
define_property(TrackedValue.prototype, 'DO_NOT_ACCESS_THIS_OBJECT_DIRECTLY', { value: true });
|
|
378
|
+
define_property(DerivedValue.prototype, 'DO_NOT_ACCESS_THIS_OBJECT_DIRECTLY', { value: true });
|
|
379
|
+
}
|
|
380
|
+
|
|
292
381
|
/**
|
|
293
382
|
*
|
|
294
383
|
* @param {any} v
|
|
@@ -298,25 +387,9 @@ var empty_get_set = { get: undefined, set: undefined };
|
|
|
298
387
|
* @returns {Tracked}
|
|
299
388
|
*/
|
|
300
389
|
export function tracked(v, block, get, set) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
DO_NOT_ACCESS_THIS_OBJECT_DIRECTLY: true,
|
|
305
|
-
a: get || set ? { get, set } : empty_get_set,
|
|
306
|
-
b: block || active_block,
|
|
307
|
-
c: 0,
|
|
308
|
-
f: TRACKED,
|
|
309
|
-
__v: v,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
a: get || set ? { get, set } : empty_get_set,
|
|
315
|
-
b: block || active_block,
|
|
316
|
-
c: 0,
|
|
317
|
-
f: TRACKED,
|
|
318
|
-
__v: v,
|
|
319
|
-
};
|
|
390
|
+
return /** @type {Tracked} */ (
|
|
391
|
+
new TrackedValue(v, block || active_block, get || set ? { get, set } : empty_get_set)
|
|
392
|
+
);
|
|
320
393
|
}
|
|
321
394
|
|
|
322
395
|
/**
|
|
@@ -327,32 +400,9 @@ export function tracked(v, block, get, set) {
|
|
|
327
400
|
* @returns {Derived}
|
|
328
401
|
*/
|
|
329
402
|
export function derived(fn, block, get, set) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
a: get || set ? { get, set } : empty_get_set,
|
|
334
|
-
b: block || active_block,
|
|
335
|
-
blocks: null,
|
|
336
|
-
c: 0,
|
|
337
|
-
co: active_component,
|
|
338
|
-
d: null,
|
|
339
|
-
f: TRACKED | DERIVED,
|
|
340
|
-
fn,
|
|
341
|
-
__v: UNINITIALIZED,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
a: get || set ? { get, set } : empty_get_set,
|
|
347
|
-
b: block || active_block,
|
|
348
|
-
blocks: null,
|
|
349
|
-
c: 0,
|
|
350
|
-
co: active_component,
|
|
351
|
-
d: null,
|
|
352
|
-
f: TRACKED | DERIVED,
|
|
353
|
-
fn,
|
|
354
|
-
__v: UNINITIALIZED,
|
|
355
|
-
};
|
|
403
|
+
return /** @type {Derived} */ (
|
|
404
|
+
new DerivedValue(fn, block || active_block, get || set ? { get, set } : empty_get_set)
|
|
405
|
+
);
|
|
356
406
|
}
|
|
357
407
|
|
|
358
408
|
/**
|
|
@@ -24,6 +24,11 @@ export type Tracked<V = any> = {
|
|
|
24
24
|
c: number;
|
|
25
25
|
f: number;
|
|
26
26
|
__v: V;
|
|
27
|
+
readonly [0]: V;
|
|
28
|
+
[1]: Tracked<V>;
|
|
29
|
+
value: V;
|
|
30
|
+
readonly length: 2;
|
|
31
|
+
[Symbol.iterator](): Iterator<V | Tracked<V>>;
|
|
27
32
|
};
|
|
28
33
|
|
|
29
34
|
export type Derived = {
|
|
@@ -37,6 +42,11 @@ export type Derived = {
|
|
|
37
42
|
f: number;
|
|
38
43
|
fn: Function;
|
|
39
44
|
__v: any;
|
|
45
|
+
readonly [0]: any;
|
|
46
|
+
[1]: Derived;
|
|
47
|
+
value: any;
|
|
48
|
+
readonly length: 2;
|
|
49
|
+
[Symbol.iterator](): Iterator<any | Derived>;
|
|
40
50
|
};
|
|
41
51
|
|
|
42
52
|
export type Block = {
|
|
@@ -27,6 +27,18 @@ export var object_prototype = Object.prototype;
|
|
|
27
27
|
/** @type {typeof Array.prototype} */
|
|
28
28
|
export var array_prototype = Array.prototype;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Slice helper for arrays and array-like values.
|
|
32
|
+
* @param {ArrayLike<any>} array_like
|
|
33
|
+
* @param {...number} args
|
|
34
|
+
* @returns {any[]}
|
|
35
|
+
*/
|
|
36
|
+
export function array_slice(array_like, ...args) {
|
|
37
|
+
return is_array(array_like)
|
|
38
|
+
? array_like.slice(...args)
|
|
39
|
+
: array_prototype.slice.call(array_like, ...args);
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
/**
|
|
31
43
|
* Creates a text node that serves as an anchor point in the DOM.
|
|
32
44
|
* @returns {Text}
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { Readable } from 'stream';
|
|
7
7
|
import { DERIVED, UNINITIALIZED, TRACKED } from '../client/constants.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
is_ripple_object,
|
|
10
|
+
get_descriptor,
|
|
11
|
+
define_property,
|
|
12
|
+
is_array,
|
|
13
|
+
array_slice,
|
|
14
|
+
} from '../client/utils.js';
|
|
9
15
|
import { escape } from '../../../utils/escaping.js';
|
|
10
16
|
import { is_boolean_attribute } from '../../../compiler/utils.js';
|
|
11
17
|
import { clsx } from 'clsx';
|
|
@@ -20,6 +26,7 @@ export { escape };
|
|
|
20
26
|
export { register_component_css as register_css } from './css-registry.js';
|
|
21
27
|
export { hash } from '../../../utils/hashing.js';
|
|
22
28
|
export { context } from './context.js';
|
|
29
|
+
export { array_slice };
|
|
23
30
|
|
|
24
31
|
/** @type {null | Component} */
|
|
25
32
|
export let active_component = null;
|
|
@@ -607,6 +614,84 @@ export function spread_attrs(attrs, css_hash) {
|
|
|
607
614
|
|
|
608
615
|
var empty_get_set = { get: undefined, set: undefined };
|
|
609
616
|
|
|
617
|
+
class TrackedValue {
|
|
618
|
+
/**
|
|
619
|
+
* @param {any} v
|
|
620
|
+
* @param {{ get?: Function; set?: Function }} a
|
|
621
|
+
*/
|
|
622
|
+
constructor(v, a) {
|
|
623
|
+
this.a = a;
|
|
624
|
+
this.c = 0;
|
|
625
|
+
this.f = TRACKED;
|
|
626
|
+
this.v = v;
|
|
627
|
+
}
|
|
628
|
+
get [0]() {
|
|
629
|
+
return get(/** @type {Tracked} */ (this));
|
|
630
|
+
}
|
|
631
|
+
set [0](v) {
|
|
632
|
+
set(/** @type {Tracked} */ (this), v);
|
|
633
|
+
}
|
|
634
|
+
get [1]() {
|
|
635
|
+
return this;
|
|
636
|
+
}
|
|
637
|
+
get value() {
|
|
638
|
+
return get(/** @type {Tracked} */ (this));
|
|
639
|
+
}
|
|
640
|
+
/** @param {any} v */
|
|
641
|
+
set value(v) {
|
|
642
|
+
set(/** @type {Tracked} */ (this), v);
|
|
643
|
+
}
|
|
644
|
+
/** @returns {2} */
|
|
645
|
+
get length() {
|
|
646
|
+
return 2;
|
|
647
|
+
}
|
|
648
|
+
*[Symbol.iterator]() {
|
|
649
|
+
yield get(/** @type {Tracked} */ (this));
|
|
650
|
+
yield this;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
class DerivedValue {
|
|
655
|
+
/**
|
|
656
|
+
* @param {Function} fn
|
|
657
|
+
* @param {{ get?: Function; set?: Function }} a
|
|
658
|
+
*/
|
|
659
|
+
constructor(fn, a) {
|
|
660
|
+
this.a = a;
|
|
661
|
+
this.c = 0;
|
|
662
|
+
this.co = active_component;
|
|
663
|
+
/** @type {null | import('#server').Dependency} */
|
|
664
|
+
this.d = null;
|
|
665
|
+
this.f = TRACKED | DERIVED;
|
|
666
|
+
this.fn = fn;
|
|
667
|
+
this.v = UNINITIALIZED;
|
|
668
|
+
}
|
|
669
|
+
get [0]() {
|
|
670
|
+
return get(/** @type {Derived} */ (this));
|
|
671
|
+
}
|
|
672
|
+
set [0](v) {
|
|
673
|
+
set(/** @type {Derived} */ (this), v);
|
|
674
|
+
}
|
|
675
|
+
get [1]() {
|
|
676
|
+
return this;
|
|
677
|
+
}
|
|
678
|
+
get value() {
|
|
679
|
+
return get(/** @type {Derived} */ (this));
|
|
680
|
+
}
|
|
681
|
+
/** @param {any} v */
|
|
682
|
+
set value(v) {
|
|
683
|
+
set(/** @type {Derived} */ (this), v);
|
|
684
|
+
}
|
|
685
|
+
/** @returns {2} */
|
|
686
|
+
get length() {
|
|
687
|
+
return 2;
|
|
688
|
+
}
|
|
689
|
+
*[Symbol.iterator]() {
|
|
690
|
+
yield get(/** @type {Derived} */ (this));
|
|
691
|
+
yield this;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
610
695
|
/**
|
|
611
696
|
* @param {any} v
|
|
612
697
|
* @param {(value: any) => any} [get]
|
|
@@ -614,12 +699,7 @@ var empty_get_set = { get: undefined, set: undefined };
|
|
|
614
699
|
* @returns {Tracked}
|
|
615
700
|
*/
|
|
616
701
|
function tracked(v, get, set) {
|
|
617
|
-
return {
|
|
618
|
-
a: get || set ? { get, set } : empty_get_set,
|
|
619
|
-
c: 0,
|
|
620
|
-
f: TRACKED,
|
|
621
|
-
v,
|
|
622
|
-
};
|
|
702
|
+
return /** @type {Tracked} */ (new TrackedValue(v, get || set ? { get, set } : empty_get_set));
|
|
623
703
|
}
|
|
624
704
|
|
|
625
705
|
/**
|
|
@@ -636,15 +716,7 @@ export function track(v, get, set) {
|
|
|
636
716
|
}
|
|
637
717
|
|
|
638
718
|
if (typeof v === 'function') {
|
|
639
|
-
return {
|
|
640
|
-
a: get || set ? { get, set } : empty_get_set,
|
|
641
|
-
c: 0,
|
|
642
|
-
co: active_component,
|
|
643
|
-
d: null,
|
|
644
|
-
f: TRACKED | DERIVED,
|
|
645
|
-
fn: v,
|
|
646
|
-
v: UNINITIALIZED,
|
|
647
|
-
};
|
|
719
|
+
return /** @type {Derived} */ (new DerivedValue(v, get || set ? { get, set } : empty_get_set));
|
|
648
720
|
}
|
|
649
721
|
|
|
650
722
|
return tracked(v, get, set);
|
|
@@ -678,7 +750,7 @@ export function track_split(v, l) {
|
|
|
678
750
|
t = v[key];
|
|
679
751
|
} else {
|
|
680
752
|
t = tracked(undefined);
|
|
681
|
-
t = define_property(t, '
|
|
753
|
+
t = define_property(t, 'v', /** @type {PropertyDescriptor} */ (get_descriptor(v, key)));
|
|
682
754
|
}
|
|
683
755
|
} else {
|
|
684
756
|
t = tracked(undefined);
|
|
@@ -19,6 +19,11 @@ export type Derived = {
|
|
|
19
19
|
f: number;
|
|
20
20
|
fn: Function;
|
|
21
21
|
v: any;
|
|
22
|
+
readonly [0]: any;
|
|
23
|
+
[1]: Derived;
|
|
24
|
+
value: any;
|
|
25
|
+
readonly length: 2;
|
|
26
|
+
[Symbol.iterator](): Iterator<any | Derived>;
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
export type Tracked = {
|
|
@@ -26,4 +31,9 @@ export type Tracked = {
|
|
|
26
31
|
c: number;
|
|
27
32
|
f: number;
|
|
28
33
|
v: any;
|
|
34
|
+
readonly [0]: any;
|
|
35
|
+
[1]: Tracked;
|
|
36
|
+
value: any;
|
|
37
|
+
readonly length: 2;
|
|
38
|
+
[Symbol.iterator](): Iterator<any | Tracked>;
|
|
29
39
|
};
|
package/src/utils/ast.js
CHANGED
|
@@ -195,7 +195,7 @@ function _extract_paths(assignments = [], param, expression, update_expression,
|
|
|
195
195
|
if (element.type === 'RestElement') {
|
|
196
196
|
/** @type {DestructuredAssignment['expression']} */
|
|
197
197
|
const rest_expression = (object) =>
|
|
198
|
-
b.call(
|
|
198
|
+
b.call('_$_.array_slice', expression(object), b.literal(i));
|
|
199
199
|
if (element.argument.type === 'Identifier') {
|
|
200
200
|
assignments.push({
|
|
201
201
|
node: element.argument,
|
|
@@ -340,6 +340,35 @@ component App() {
|
|
|
340
340
|
expect(result).toMatch(/value:\s*value\??\.\['#v'\]/);
|
|
341
341
|
});
|
|
342
342
|
|
|
343
|
+
it('keeps lazy destructuring as plain destructuring in to_ts output', () => {
|
|
344
|
+
const track_split_source = `
|
|
345
|
+
import { trackSplit } from 'ripple';
|
|
346
|
+
component App() {
|
|
347
|
+
const source = { a: 1, b: 2, c: 3 };
|
|
348
|
+
let &[a, b, rest] = trackSplit(source, ['a', 'b']);
|
|
349
|
+
const sum = @a + @b + @rest.c;
|
|
350
|
+
}
|
|
351
|
+
`;
|
|
352
|
+
const track_split_result = compile_to_volar_mappings(track_split_source, 'test.ripple').code;
|
|
353
|
+
expect(track_split_result).toContain('let [a, b, rest] = trackSplit(source, [\'a\', \'b\']);');
|
|
354
|
+
expect(track_split_result).not.toContain('let lazy = trackSplit');
|
|
355
|
+
|
|
356
|
+
const track_source = `
|
|
357
|
+
import { track } from 'ripple';
|
|
358
|
+
component App() {
|
|
359
|
+
let &[value, ...rest] = track(0);
|
|
360
|
+
const x = value;
|
|
361
|
+
}
|
|
362
|
+
`;
|
|
363
|
+
const track_result = compile_to_volar_mappings(track_source, 'test.ripple').code;
|
|
364
|
+
expect(track_result).toContain('let [value, ...rest] = track(0);');
|
|
365
|
+
expect(track_result).toContain('const x = value;');
|
|
366
|
+
expect(track_result).not.toContain('let lazy = track(0)');
|
|
367
|
+
expect(track_result).not.toContain('.slice(');
|
|
368
|
+
expect(track_result).not.toContain('_$_.get(');
|
|
369
|
+
expect(track_result).not.toContain('lazy0');
|
|
370
|
+
});
|
|
371
|
+
|
|
343
372
|
it('preserves generic type args in interface extends for Volar mappings', () => {
|
|
344
373
|
const source = `
|
|
345
374
|
interface PolymorphicProps<T extends keyof HTMLElementTagNameMap> {
|
|
@@ -121,4 +121,70 @@ import { get, track } from 'ripple';
|
|
|
121
121
|
expect(() => compile(code, 'test.ripple')).not.toThrow();
|
|
122
122
|
},
|
|
123
123
|
);
|
|
124
|
+
|
|
125
|
+
it('should allow indexed [0] access on a tracked object', () => {
|
|
126
|
+
const code = `
|
|
127
|
+
import { track } from 'ripple';
|
|
128
|
+
export default component App() {
|
|
129
|
+
let count = track(0);
|
|
130
|
+
console.log(count[0]);
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
expect(() => compile(code, 'test.ripple')).not.toThrow();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should allow indexed [1] access on a tracked object', () => {
|
|
137
|
+
const code = `
|
|
138
|
+
import { track } from 'ripple';
|
|
139
|
+
export default component App() {
|
|
140
|
+
let count = track(0);
|
|
141
|
+
let raw = count[1];
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
expect(() => compile(code, 'test.ripple')).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should allow [0] write on a tracked object', () => {
|
|
148
|
+
const code = `
|
|
149
|
+
import { track } from 'ripple';
|
|
150
|
+
export default component App() {
|
|
151
|
+
let count = track(0);
|
|
152
|
+
count[0] = 5;
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
expect(() => compile(code, 'test.ripple')).not.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should allow .value read access on a tracked object', () => {
|
|
159
|
+
const code = `
|
|
160
|
+
import { track } from 'ripple';
|
|
161
|
+
export default component App() {
|
|
162
|
+
let count = track(0);
|
|
163
|
+
console.log(count.value);
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
expect(() => compile(code, 'test.ripple')).not.toThrow();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should allow .value assignment on a tracked object', () => {
|
|
170
|
+
const code = `
|
|
171
|
+
import { track } from 'ripple';
|
|
172
|
+
export default component App() {
|
|
173
|
+
let count = track(0);
|
|
174
|
+
count.value = 5;
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
expect(() => compile(code, 'test.ripple')).not.toThrow();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should allow .length read access on a tracked object', () => {
|
|
181
|
+
const code = `
|
|
182
|
+
import { track } from 'ripple';
|
|
183
|
+
export default component App() {
|
|
184
|
+
let count = track(0);
|
|
185
|
+
console.log(count.length);
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
expect(() => compile(code, 'test.ripple')).not.toThrow();
|
|
189
|
+
});
|
|
124
190
|
});
|
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
import { flushSync, track } from 'ripple';
|
|
1
|
+
import { flushSync, track, trackSplit } from 'ripple';
|
|
2
2
|
|
|
3
3
|
describe('lazy destructuring', () => {
|
|
4
|
+
it('supports tracked value getter and setter', () => {
|
|
5
|
+
component Test() {
|
|
6
|
+
let count = track(1);
|
|
7
|
+
let doubled = track(() => count.value * 2);
|
|
8
|
+
|
|
9
|
+
<div>{`${count.value}-${doubled.value}`}</div>
|
|
10
|
+
<button
|
|
11
|
+
onClick={() => {
|
|
12
|
+
count.value = 5;
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
{'set'}
|
|
16
|
+
</button>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
render(Test);
|
|
20
|
+
expect(container.querySelector('div')!.textContent).toBe('1-2');
|
|
21
|
+
container.querySelector('button')!.click();
|
|
22
|
+
flushSync();
|
|
23
|
+
expect(container.querySelector('div')!.textContent).toBe('5-10');
|
|
24
|
+
});
|
|
25
|
+
|
|
4
26
|
it('lazily accesses object properties with const', () => {
|
|
5
27
|
component Inner(&{ a, b }: { a: number; b: string }) {
|
|
6
28
|
<pre>{`${a}-${b}`}</pre>
|
|
@@ -183,6 +205,38 @@ describe('lazy destructuring', () => {
|
|
|
183
205
|
expect(container.querySelector('pre')!.textContent).toBe('1-99');
|
|
184
206
|
});
|
|
185
207
|
|
|
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}-${@b}-${@rest.c}`}</pre>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
render(Test);
|
|
216
|
+
expect(container.querySelector('pre')!.textContent).toBe('1-2-3');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('supports rest destructuring from iterable array-like tracked values', () => {
|
|
220
|
+
component Test() {
|
|
221
|
+
let &[value, ...rest] = track(0);
|
|
222
|
+
<pre>{`${value}-${@rest.length}-${@rest[0] === value}`}</pre>
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
render(Test);
|
|
226
|
+
expect(container.querySelector('pre')!.textContent).toBe('0-1-false');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('supports rest destructuring from length-only array-like sources', () => {
|
|
230
|
+
component Test() {
|
|
231
|
+
const source = { 0: 'x', 1: 'y', 2: 'z', length: 3 };
|
|
232
|
+
const &[first, ...rest] = source;
|
|
233
|
+
<pre>{`${first}-${rest.join(',')}`}</pre>
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
render(Test);
|
|
237
|
+
expect(container.querySelector('pre')!.textContent).toBe('x-y,z');
|
|
238
|
+
});
|
|
239
|
+
|
|
186
240
|
it('supports update expressions on lazy bindings with default values', () => {
|
|
187
241
|
component Test() {
|
|
188
242
|
const obj: { count?: number } = {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Basic static components for hydration testing
|
|
2
|
-
import type {
|
|
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:
|
|
100
|
+
component Layout({ children }: { children: Children }) {
|
|
101
101
|
<main>
|
|
102
102
|
<div class="container">
|
|
103
103
|
<children />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Children } from 'ripple';
|
|
2
2
|
|
|
3
|
-
export component Layout(&{ children }: { children?:
|
|
3
|
+
export component Layout(&{ children }: { children?: Children }) {
|
|
4
4
|
<div class="layout">
|
|
5
5
|
<children />
|
|
6
6
|
</div>
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
+
import { track, trackSplit } from 'ripple';
|
|
2
|
+
|
|
1
3
|
describe('lazy destructuring', () => {
|
|
4
|
+
it('supports tracked value getter and setter', async () => {
|
|
5
|
+
component Test() {
|
|
6
|
+
let count = track(1);
|
|
7
|
+
let derived = track(() => count.value * 2);
|
|
8
|
+
|
|
9
|
+
count.value = 3;
|
|
10
|
+
|
|
11
|
+
<pre>{`${count.value}-${derived.value}`}</pre>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { body } = await render(Test);
|
|
15
|
+
expect(body).toBeHtml('<pre>3-6</pre>');
|
|
16
|
+
});
|
|
17
|
+
|
|
2
18
|
it('lazily accesses object properties', async () => {
|
|
3
19
|
component Inner(&{ a, b }: { a: number; b: string }) {
|
|
4
20
|
<pre>{`${a}-${b}`}</pre>
|
|
@@ -100,4 +116,37 @@ describe('lazy destructuring', () => {
|
|
|
100
116
|
const { body } = await render(Test);
|
|
101
117
|
expect(body).toBeHtml('<pre>Alice-30</pre>');
|
|
102
118
|
});
|
|
119
|
+
|
|
120
|
+
it('treats lazy array destructuring of trackSplit as regular array access', async () => {
|
|
121
|
+
component Test() {
|
|
122
|
+
const source = { a: 1, b: 2, c: 3 };
|
|
123
|
+
const &[a, b, rest] = trackSplit(source, ['a', 'b']);
|
|
124
|
+
<pre>{`${@a}-${@b}-${@rest.c}`}</pre>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { body } = await render(Test);
|
|
128
|
+
expect(body).toBeHtml('<pre>1-2-3</pre>');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('supports rest in lazy array destructuring for tracked tuples (iterable)', async () => {
|
|
132
|
+
component Test() {
|
|
133
|
+
let tracked_value = track(0);
|
|
134
|
+
let &[value, ...rest] = tracked_value;
|
|
135
|
+
<pre>{`${value}-${@rest.length}-${@rest[0] === tracked_value}`}</pre>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { body } = await render(Test);
|
|
139
|
+
expect(body).toBeHtml('<pre>0-1-true</pre>');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('supports rest in lazy array destructuring for length-only array-like values', async () => {
|
|
143
|
+
component Test() {
|
|
144
|
+
const array_like = { 0: 'x', 1: 'y', 2: 'z', length: 3 };
|
|
145
|
+
const &[first, ...rest] = array_like;
|
|
146
|
+
<pre>{`${first}-${rest.join('')}`}</pre>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { body } = await render(Test);
|
|
150
|
+
expect(body).toBeHtml('<pre>x-yz</pre>');
|
|
151
|
+
});
|
|
103
152
|
});
|
|
@@ -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;
|
|
@@ -142,16 +145,21 @@ declare global {
|
|
|
142
145
|
export function createRefKey(): symbol;
|
|
143
146
|
|
|
144
147
|
// Base Tracked interface - all tracked values have a '#v' property containing the actual value
|
|
145
|
-
|
|
148
|
+
interface TrackedBase<V> {
|
|
146
149
|
'#v': V;
|
|
150
|
+
value: V;
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
// Augment Tracked to be callable when V is a Component
|
|
150
154
|
// This allows <@Something /> to work in JSX when Something is Tracked<Component>
|
|
151
|
-
|
|
155
|
+
interface TrackedCallable<V> {
|
|
152
156
|
(props: V extends Component<infer P> ? P : never): V extends Component ? void : never;
|
|
153
157
|
}
|
|
154
158
|
|
|
159
|
+
// Supports indexed access: track(0)[0] → value, track(0)[1] → Tracked<V>
|
|
160
|
+
// And destructuring `const [one, two] = track(0);`
|
|
161
|
+
export type Tracked<V> = [V, Tracked<V>] & TrackedBase<V> & TrackedCallable<V>;
|
|
162
|
+
|
|
155
163
|
// Helper type to infer component type from a function that returns a component
|
|
156
164
|
// If T is a function returning a Component, extract the Component type itself, not the return type (void)
|
|
157
165
|
export type InferComponent<T> = T extends () => infer R ? (R extends Component<any> ? R : T) : T;
|
|
@@ -159,10 +167,10 @@ export type InferComponent<T> = T extends () => infer R ? (R extends Component<a
|
|
|
159
167
|
export type Props<K extends PropertyKey = any, V = unknown> = Record<K, V>;
|
|
160
168
|
export type PropsWithExtras<T extends object> = Props & T & Record<string, unknown>;
|
|
161
169
|
export type PropsWithChildren<T extends object = {}> = Expand<
|
|
162
|
-
Omit<T, 'children'> & { children:
|
|
170
|
+
Omit<T, 'children'> & { children: Children }
|
|
163
171
|
>;
|
|
164
172
|
export type PropsWithChildrenOptional<T extends object = {}> = Expand<
|
|
165
|
-
Omit<T, 'children'> & { children?:
|
|
173
|
+
Omit<T, 'children'> & { children?: Children }
|
|
166
174
|
>;
|
|
167
175
|
export type PropsNoChildren<T extends object = {}> = Expand<T>;
|
|
168
176
|
|
|
@@ -383,10 +391,10 @@ export const MediaQuery: MediaQueryConstructor;
|
|
|
383
391
|
|
|
384
392
|
export function Portal<V = HTMLElement>({
|
|
385
393
|
target,
|
|
386
|
-
children
|
|
394
|
+
children,
|
|
387
395
|
}: {
|
|
388
396
|
target: V;
|
|
389
|
-
children?:
|
|
397
|
+
children?: Children;
|
|
390
398
|
}): void;
|
|
391
399
|
|
|
392
400
|
export type GetFunction<V> = () => V;
|