ripple 0.2.109 → 0.2.111
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/package.json +1 -1
- package/src/compiler/phases/1-parse/index.js +31 -10
- package/src/compiler/phases/2-analyze/index.js +90 -1
- package/src/compiler/phases/3-transform/client/index.js +46 -4
- package/src/compiler/phases/3-transform/segments.js +3 -0
- package/src/compiler/phases/3-transform/server/index.js +134 -8
- package/src/compiler/scope.js +15 -1
- package/src/compiler/types/index.d.ts +2 -2
- package/src/compiler/utils.js +2 -0
- package/src/runtime/internal/client/for.js +48 -20
- package/src/runtime/internal/client/index.js +2 -0
- package/src/runtime/internal/client/rpc.js +14 -0
- package/src/utils/builders.js +15 -2
- package/tests/client/__snapshots__/compiler.test.ripple.snap +13 -0
- package/tests/client/__snapshots__/for.test.ripple.snap +5 -5
- package/tests/client/compiler.test.ripple +15 -0
- package/tests/client/composite.test.ripple +31 -0
- package/tests/client/for.test.ripple +7 -7
- package/tests/client/switch.test.ripple +152 -0
- package/tests/server/__snapshots__/compiler.test.ripple.snap +37 -0
- package/tests/server/compiler.test.ripple +39 -0
- package/tests/server/switch.test.ripple +27 -0
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.2.
|
|
6
|
+
"version": "0.2.111",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -165,18 +165,19 @@ function RipplePlugin(config) {
|
|
|
165
165
|
|
|
166
166
|
// Check if this is #server
|
|
167
167
|
if (this.input.slice(this.pos, this.pos + 7) === '#server') {
|
|
168
|
-
// Check that next char after 'server' is whitespace or
|
|
168
|
+
// Check that next char after 'server' is whitespace, {, . (dot), or EOF
|
|
169
169
|
const charAfter =
|
|
170
170
|
this.pos + 7 < this.input.length ? this.input.charCodeAt(this.pos + 7) : -1;
|
|
171
171
|
if (
|
|
172
|
-
charAfter === 123 ||
|
|
173
|
-
charAfter ===
|
|
174
|
-
charAfter ===
|
|
175
|
-
charAfter ===
|
|
176
|
-
charAfter ===
|
|
177
|
-
charAfter ===
|
|
172
|
+
charAfter === 123 || // {
|
|
173
|
+
charAfter === 46 || // . (dot)
|
|
174
|
+
charAfter === 32 || // space
|
|
175
|
+
charAfter === 9 || // tab
|
|
176
|
+
charAfter === 10 || // newline
|
|
177
|
+
charAfter === 13 || // carriage return
|
|
178
|
+
charAfter === -1 // EOF
|
|
178
179
|
) {
|
|
179
|
-
// { or whitespace or EOF
|
|
180
|
+
// { or . or whitespace or EOF
|
|
180
181
|
this.pos += 7; // consume '#server'
|
|
181
182
|
return this.finishToken(tt.name, '#server');
|
|
182
183
|
}
|
|
@@ -379,6 +380,13 @@ function RipplePlugin(config) {
|
|
|
379
380
|
return this.parseTrackedExpression();
|
|
380
381
|
}
|
|
381
382
|
|
|
383
|
+
// Check if this is #server identifier for server function calls
|
|
384
|
+
if (this.type === tt.name && this.value === '#server') {
|
|
385
|
+
const node = this.startNode();
|
|
386
|
+
this.next();
|
|
387
|
+
return this.finishNode(node, 'ServerIdentifier');
|
|
388
|
+
}
|
|
389
|
+
|
|
382
390
|
// Check if this is a tuple literal starting with #[
|
|
383
391
|
if (this.type === tt.bracketL && this.value === '#[') {
|
|
384
392
|
return this.parseTrackedArrayExpression();
|
|
@@ -388,7 +396,6 @@ function RipplePlugin(config) {
|
|
|
388
396
|
|
|
389
397
|
return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
|
|
390
398
|
}
|
|
391
|
-
|
|
392
399
|
/**
|
|
393
400
|
* Parse `@(expression)` syntax for unboxing tracked values
|
|
394
401
|
* Creates a TrackedExpression node with the argument property
|
|
@@ -419,7 +426,21 @@ function RipplePlugin(config) {
|
|
|
419
426
|
parseServerBlock() {
|
|
420
427
|
const node = this.startNode();
|
|
421
428
|
this.next();
|
|
422
|
-
|
|
429
|
+
|
|
430
|
+
const body = this.startNode();
|
|
431
|
+
node.body = body;
|
|
432
|
+
body.body = [];
|
|
433
|
+
|
|
434
|
+
this.expect(tt.braceL);
|
|
435
|
+
this.enterScope(0);
|
|
436
|
+
while (this.type !== tt.braceR) {
|
|
437
|
+
var stmt = this.parseStatement(null, true);
|
|
438
|
+
body.body.push(stmt);
|
|
439
|
+
}
|
|
440
|
+
this.next();
|
|
441
|
+
this.exitScope();
|
|
442
|
+
this.finishNode(body, 'BlockStatement');
|
|
443
|
+
|
|
423
444
|
this.awaitPos = 0;
|
|
424
445
|
return this.finishNode(node, 'ServerBlock');
|
|
425
446
|
}
|
|
@@ -96,11 +96,46 @@ const visitors = {
|
|
|
96
96
|
return context.next({ ...context.state, function_depth: 0, expression: null });
|
|
97
97
|
},
|
|
98
98
|
|
|
99
|
+
ServerBlock(node, context) {
|
|
100
|
+
node.metadata = {
|
|
101
|
+
...node.metadata,
|
|
102
|
+
exports: [],
|
|
103
|
+
};
|
|
104
|
+
context.visit(node.body, { ...context.state, inside_server_block: true });
|
|
105
|
+
},
|
|
106
|
+
|
|
99
107
|
Identifier(node, context) {
|
|
100
108
|
const binding = context.state.scope.get(node.name);
|
|
101
109
|
const parent = context.path.at(-1);
|
|
102
110
|
|
|
103
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
is_reference(node, /** @type {Node} */ (parent)) &&
|
|
113
|
+
binding &&
|
|
114
|
+
context.state.inside_server_block
|
|
115
|
+
) {
|
|
116
|
+
let current_scope = binding.scope;
|
|
117
|
+
|
|
118
|
+
while (current_scope !== null) {
|
|
119
|
+
if (current_scope.server_block) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const parent_scope = current_scope.parent;
|
|
123
|
+
if (parent_scope === null) {
|
|
124
|
+
error(
|
|
125
|
+
`Cannot reference client-side variable "${node.name}" from a server block`,
|
|
126
|
+
context.state.analysis.module.filename,
|
|
127
|
+
node,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
current_scope = parent_scope;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
binding?.kind === 'prop' ||
|
|
136
|
+
binding?.kind === 'prop_fallback' ||
|
|
137
|
+
binding?.kind === 'for_pattern'
|
|
138
|
+
) {
|
|
104
139
|
mark_as_tracked(context.path);
|
|
105
140
|
if (context.state.metadata?.tracking === false) {
|
|
106
141
|
context.state.metadata.tracking = true;
|
|
@@ -312,6 +347,7 @@ const visitors = {
|
|
|
312
347
|
}
|
|
313
348
|
|
|
314
349
|
node.metadata = {
|
|
350
|
+
...node.metadata,
|
|
315
351
|
has_template: false,
|
|
316
352
|
};
|
|
317
353
|
|
|
@@ -347,7 +383,38 @@ const visitors = {
|
|
|
347
383
|
}
|
|
348
384
|
}
|
|
349
385
|
|
|
386
|
+
if (node.key) {
|
|
387
|
+
const state = context.state;
|
|
388
|
+
const pattern = node.left.declarations[0].id;
|
|
389
|
+
const paths = extract_paths(pattern);
|
|
390
|
+
const scope = state.scopes.get(node);
|
|
391
|
+
const pattern_id = b.id(scope.generate('pattern'));
|
|
392
|
+
|
|
393
|
+
node.left.declarations[0].id = pattern_id;
|
|
394
|
+
|
|
395
|
+
for (const path of paths) {
|
|
396
|
+
const name = path.node.name;
|
|
397
|
+
const binding = context.state.scope.get(name);
|
|
398
|
+
|
|
399
|
+
binding.kind = 'for_pattern';
|
|
400
|
+
if (!binding.metadata) {
|
|
401
|
+
binding.metadata = {
|
|
402
|
+
pattern: pattern_id,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (binding !== null) {
|
|
407
|
+
binding.transform = {
|
|
408
|
+
read: () => {
|
|
409
|
+
return path.expression(b.call('_$_.get', pattern_id));
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
350
416
|
node.metadata = {
|
|
417
|
+
...node.metadata,
|
|
351
418
|
has_template: false,
|
|
352
419
|
};
|
|
353
420
|
context.next();
|
|
@@ -361,6 +428,23 @@ const visitors = {
|
|
|
361
428
|
}
|
|
362
429
|
},
|
|
363
430
|
|
|
431
|
+
ExportNamedDeclaration(node, context) {
|
|
432
|
+
if (!context.state.inside_server_block) {
|
|
433
|
+
return context.next();
|
|
434
|
+
}
|
|
435
|
+
const server_block = context.path.find((n) => n.type === 'ServerBlock');
|
|
436
|
+
const declaration = node.declaration;
|
|
437
|
+
|
|
438
|
+
if (declaration && declaration.type === 'FunctionDeclaration') {
|
|
439
|
+
server_block.metadata.exports.push(declaration.id.name);
|
|
440
|
+
} else {
|
|
441
|
+
// TODO
|
|
442
|
+
throw new Error('Not implemented');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return context.next();
|
|
446
|
+
},
|
|
447
|
+
|
|
364
448
|
TSTypeReference(node, context) {
|
|
365
449
|
// bug in our acorn pasrer: it uses typeParameters instead of typeArguments
|
|
366
450
|
if (node.typeParameters) {
|
|
@@ -376,6 +460,7 @@ const visitors = {
|
|
|
376
460
|
}
|
|
377
461
|
|
|
378
462
|
node.metadata = {
|
|
463
|
+
...node.metadata,
|
|
379
464
|
has_template: false,
|
|
380
465
|
};
|
|
381
466
|
|
|
@@ -391,6 +476,7 @@ const visitors = {
|
|
|
391
476
|
|
|
392
477
|
if (node.alternate) {
|
|
393
478
|
node.metadata = {
|
|
479
|
+
...node.metadata,
|
|
394
480
|
has_template: false,
|
|
395
481
|
};
|
|
396
482
|
context.visit(node.alternate, context.state);
|
|
@@ -417,6 +503,7 @@ const visitors = {
|
|
|
417
503
|
}
|
|
418
504
|
|
|
419
505
|
node.metadata = {
|
|
506
|
+
...node.metadata,
|
|
420
507
|
has_template: false,
|
|
421
508
|
};
|
|
422
509
|
|
|
@@ -431,6 +518,7 @@ const visitors = {
|
|
|
431
518
|
}
|
|
432
519
|
|
|
433
520
|
node.metadata = {
|
|
521
|
+
...node.metadata,
|
|
434
522
|
has_template: false,
|
|
435
523
|
};
|
|
436
524
|
|
|
@@ -672,6 +760,7 @@ export function analyze(ast, filename) {
|
|
|
672
760
|
scopes,
|
|
673
761
|
analysis,
|
|
674
762
|
inside_head: false,
|
|
763
|
+
inside_server_block: false,
|
|
675
764
|
},
|
|
676
765
|
visitors,
|
|
677
766
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @import {Expression, FunctionExpression,
|
|
1
|
+
/** @import {Expression, FunctionExpression, Node, Program} from 'estree' */
|
|
2
2
|
|
|
3
3
|
import { walk } from 'zimmerframe';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -37,6 +37,7 @@ import is_reference from 'is-reference';
|
|
|
37
37
|
import { object } from '../../../../utils/ast.js';
|
|
38
38
|
import { render_stylesheets } from '../stylesheet.js';
|
|
39
39
|
import { is_event_attribute, is_passive_event } from '../../../../utils/events.js';
|
|
40
|
+
import { createHash } from 'node:crypto';
|
|
40
41
|
|
|
41
42
|
function add_ripple_internal_import(context) {
|
|
42
43
|
if (!context.state.to_ts) {
|
|
@@ -54,6 +55,7 @@ function visit_function(node, context) {
|
|
|
54
55
|
const state = context.state;
|
|
55
56
|
|
|
56
57
|
delete node.returnType;
|
|
58
|
+
delete node.typeParameters;
|
|
57
59
|
|
|
58
60
|
for (const param of node.params) {
|
|
59
61
|
delete param.typeAnnotation;
|
|
@@ -168,7 +170,8 @@ const visitors = {
|
|
|
168
170
|
(node.tracked ||
|
|
169
171
|
binding?.kind === 'prop' ||
|
|
170
172
|
binding?.kind === 'index' ||
|
|
171
|
-
binding?.kind === 'prop_fallback'
|
|
173
|
+
binding?.kind === 'prop_fallback' ||
|
|
174
|
+
binding?.kind === 'for_pattern') &&
|
|
172
175
|
binding?.node !== node
|
|
173
176
|
) {
|
|
174
177
|
if (context.state.metadata?.tracking === false) {
|
|
@@ -185,6 +188,10 @@ const visitors = {
|
|
|
185
188
|
}
|
|
186
189
|
},
|
|
187
190
|
|
|
191
|
+
ServerIdentifier(node, context) {
|
|
192
|
+
return b.id('_$_server_$_');
|
|
193
|
+
},
|
|
194
|
+
|
|
188
195
|
ImportDeclaration(node, context) {
|
|
189
196
|
if (!context.state.to_ts && node.importKind === 'type') {
|
|
190
197
|
return b.empty;
|
|
@@ -1199,6 +1206,14 @@ const visitors = {
|
|
|
1199
1206
|
return context.next();
|
|
1200
1207
|
},
|
|
1201
1208
|
|
|
1209
|
+
TSInstantiationExpression(node, context) {
|
|
1210
|
+
if (!context.state.to_ts) {
|
|
1211
|
+
// In JavaScript, just return the expression wrapped in parentheses
|
|
1212
|
+
return b.sequence([context.visit(node.expression)]);
|
|
1213
|
+
}
|
|
1214
|
+
return context.next();
|
|
1215
|
+
},
|
|
1216
|
+
|
|
1202
1217
|
ExportNamedDeclaration(node, context) {
|
|
1203
1218
|
if (!context.state.to_ts && node.exportKind === 'type') {
|
|
1204
1219
|
return b.empty;
|
|
@@ -1281,8 +1296,34 @@ const visitors = {
|
|
|
1281
1296
|
return b.block(statements);
|
|
1282
1297
|
},
|
|
1283
1298
|
|
|
1284
|
-
ServerBlock() {
|
|
1285
|
-
|
|
1299
|
+
ServerBlock(node, context) {
|
|
1300
|
+
const exports = node.metadata.exports;
|
|
1301
|
+
|
|
1302
|
+
if (exports.length === 0) {
|
|
1303
|
+
return b.empty;
|
|
1304
|
+
}
|
|
1305
|
+
const file_path = context.state.filename;
|
|
1306
|
+
|
|
1307
|
+
return b.const(
|
|
1308
|
+
'_$_server_$_',
|
|
1309
|
+
b.object(
|
|
1310
|
+
exports.map((name) => {
|
|
1311
|
+
const func_path = file_path + '#' + name;
|
|
1312
|
+
// needs to be a sha256 hash of func_path, to avoid leaking file structure
|
|
1313
|
+
const hash = createHash('sha256').update(func_path).digest('hex').slice(0, 8);
|
|
1314
|
+
|
|
1315
|
+
return b.prop(
|
|
1316
|
+
'init',
|
|
1317
|
+
b.id(name),
|
|
1318
|
+
b.function(
|
|
1319
|
+
null,
|
|
1320
|
+
[b.rest(b.id('args'))],
|
|
1321
|
+
b.block([b.return(b.call('_$_.rpc', b.literal(hash), b.id('args')))]),
|
|
1322
|
+
),
|
|
1323
|
+
);
|
|
1324
|
+
}),
|
|
1325
|
+
),
|
|
1326
|
+
);
|
|
1286
1327
|
},
|
|
1287
1328
|
|
|
1288
1329
|
Program(node, context) {
|
|
@@ -1791,6 +1832,7 @@ export function transform_client(filename, source, analysis, to_ts) {
|
|
|
1791
1832
|
scopes: analysis.scopes,
|
|
1792
1833
|
stylesheets: [],
|
|
1793
1834
|
to_ts,
|
|
1835
|
+
filename,
|
|
1794
1836
|
};
|
|
1795
1837
|
|
|
1796
1838
|
const program = /** @type {Program} */ (
|
|
@@ -605,6 +605,9 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
|
|
|
605
605
|
} else if (node.type === 'TemplateElement') {
|
|
606
606
|
// Leaf node, no children to visit
|
|
607
607
|
return;
|
|
608
|
+
} else if (node.type === 'Literal') {
|
|
609
|
+
// Leaf node - literals have no children to visit
|
|
610
|
+
return;
|
|
608
611
|
} else if (node.type === 'PrivateIdentifier') {
|
|
609
612
|
// Leaf node
|
|
610
613
|
return;
|
|
@@ -17,6 +17,7 @@ import is_reference from 'is-reference';
|
|
|
17
17
|
import { escape } from '../../../../utils/escaping.js';
|
|
18
18
|
import { is_event_attribute } from '../../../../utils/events.js';
|
|
19
19
|
import { render_stylesheets } from '../stylesheet.js';
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
20
21
|
|
|
21
22
|
function add_ripple_internal_import(context) {
|
|
22
23
|
if (!context.state.to_ts) {
|
|
@@ -31,6 +32,10 @@ function transform_children(children, context) {
|
|
|
31
32
|
const normalized = normalize_children(children, context);
|
|
32
33
|
|
|
33
34
|
for (const node of normalized) {
|
|
35
|
+
if (node.type === 'BreakStatement') {
|
|
36
|
+
state.init.push(b.break);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
34
39
|
if (
|
|
35
40
|
node.type === 'VariableDeclaration' ||
|
|
36
41
|
node.type === 'ExpressionStatement' ||
|
|
@@ -132,6 +137,39 @@ const visitors = {
|
|
|
132
137
|
return context.next();
|
|
133
138
|
},
|
|
134
139
|
|
|
140
|
+
FunctionDeclaration(node, context) {
|
|
141
|
+
if (!context.state.to_ts) {
|
|
142
|
+
delete node.returnType;
|
|
143
|
+
delete node.typeParameters;
|
|
144
|
+
for (const param of node.params) {
|
|
145
|
+
delete param.typeAnnotation;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return context.next();
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
FunctionExpression(node, context) {
|
|
152
|
+
if (!context.state.to_ts) {
|
|
153
|
+
delete node.returnType;
|
|
154
|
+
delete node.typeParameters;
|
|
155
|
+
for (const param of node.params) {
|
|
156
|
+
delete param.typeAnnotation;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return context.next();
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
ArrowFunctionExpression(node, context) {
|
|
163
|
+
if (!context.state.to_ts) {
|
|
164
|
+
delete node.returnType;
|
|
165
|
+
delete node.typeParameters;
|
|
166
|
+
for (const param of node.params) {
|
|
167
|
+
delete param.typeAnnotation;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return context.next();
|
|
171
|
+
},
|
|
172
|
+
|
|
135
173
|
TSAsExpression(node, context) {
|
|
136
174
|
if (!context.state.to_ts) {
|
|
137
175
|
return context.visit(node.expression);
|
|
@@ -139,6 +177,14 @@ const visitors = {
|
|
|
139
177
|
return context.next();
|
|
140
178
|
},
|
|
141
179
|
|
|
180
|
+
TSInstantiationExpression(node, context) {
|
|
181
|
+
if (!context.state.to_ts) {
|
|
182
|
+
// In JavaScript, just return the expression wrapped in parentheses
|
|
183
|
+
return b.sequence([context.visit(node.expression)]);
|
|
184
|
+
}
|
|
185
|
+
return context.next();
|
|
186
|
+
},
|
|
187
|
+
|
|
142
188
|
TSTypeAliasDeclaration(_, context) {
|
|
143
189
|
if (!context.state.to_ts) {
|
|
144
190
|
return b.empty;
|
|
@@ -157,8 +203,23 @@ const visitors = {
|
|
|
157
203
|
if (!context.state.to_ts && node.exportKind === 'type') {
|
|
158
204
|
return b.empty;
|
|
159
205
|
}
|
|
160
|
-
|
|
161
|
-
|
|
206
|
+
if (!context.state.inside_server_block) {
|
|
207
|
+
return context.next();
|
|
208
|
+
}
|
|
209
|
+
const declaration = node.declaration;
|
|
210
|
+
|
|
211
|
+
if (declaration && declaration.type === 'FunctionDeclaration') {
|
|
212
|
+
return b.stmt(
|
|
213
|
+
b.assignment(
|
|
214
|
+
'=',
|
|
215
|
+
b.member(b.id('_$_server_$_'), b.id(declaration.id.name)),
|
|
216
|
+
context.visit(declaration),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
// TODO
|
|
221
|
+
throw new Error('Not implemented');
|
|
222
|
+
}
|
|
162
223
|
},
|
|
163
224
|
|
|
164
225
|
VariableDeclaration(node, context) {
|
|
@@ -380,15 +441,43 @@ const visitors = {
|
|
|
380
441
|
}
|
|
381
442
|
} else {
|
|
382
443
|
// Component is imported or dynamic - check .async property at runtime
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
b.
|
|
386
|
-
|
|
444
|
+
// Use if-statement instead of ternary to avoid parser issues with await in conditionals
|
|
445
|
+
state.init.push(
|
|
446
|
+
b.if(
|
|
447
|
+
b.member(visit(node.id, state), b.id('async')),
|
|
448
|
+
b.block([b.stmt(b.await(component_call))]),
|
|
449
|
+
b.block([b.stmt(component_call)]),
|
|
450
|
+
),
|
|
387
451
|
);
|
|
388
|
-
|
|
452
|
+
|
|
453
|
+
// Mark parent component as async since we're using await
|
|
454
|
+
if (state.metadata?.await === false) {
|
|
455
|
+
state.metadata.await = true;
|
|
456
|
+
}
|
|
389
457
|
}
|
|
390
458
|
}
|
|
391
459
|
},
|
|
460
|
+
SwitchStatement(node, context) {
|
|
461
|
+
if (!is_inside_component(context)) {
|
|
462
|
+
return context.next();
|
|
463
|
+
}
|
|
464
|
+
const cases = [];
|
|
465
|
+
for (const switch_case of node.cases) {
|
|
466
|
+
const consequent_scope =
|
|
467
|
+
context.state.scopes.get(switch_case.consequent) || context.state.scope;
|
|
468
|
+
const consequent = b.block(
|
|
469
|
+
transform_body(switch_case.consequent, {
|
|
470
|
+
...context,
|
|
471
|
+
state: { ...context.state, scope: consequent_scope },
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
474
|
+
cases.push(
|
|
475
|
+
b.switch_case(switch_case.test ? context.visit(switch_case.test) : null, consequent.body),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
context.state.init.push(b.switch(context.visit(node.discriminant), cases));
|
|
479
|
+
},
|
|
480
|
+
|
|
392
481
|
ForOfStatement(node, context) {
|
|
393
482
|
if (!is_inside_component(context)) {
|
|
394
483
|
context.next();
|
|
@@ -440,6 +529,10 @@ const visitors = {
|
|
|
440
529
|
}
|
|
441
530
|
},
|
|
442
531
|
|
|
532
|
+
ServerIdentifier(node, context) {
|
|
533
|
+
return b.id('_$_server_$_');
|
|
534
|
+
},
|
|
535
|
+
|
|
443
536
|
ImportDeclaration(node, context) {
|
|
444
537
|
if (!context.state.to_ts && node.importKind === 'type') {
|
|
445
538
|
return b.empty;
|
|
@@ -599,7 +692,38 @@ const visitors = {
|
|
|
599
692
|
},
|
|
600
693
|
|
|
601
694
|
ServerBlock(node, context) {
|
|
602
|
-
|
|
695
|
+
const exports = node.metadata.exports;
|
|
696
|
+
|
|
697
|
+
if (exports.length === 0) {
|
|
698
|
+
return context.visit(node.body);
|
|
699
|
+
}
|
|
700
|
+
const file_path = context.state.filename;
|
|
701
|
+
const block = context.visit(node.body, { ...context.state, inside_server_block: true });
|
|
702
|
+
const rpc_modules = globalThis.rpc_modules;
|
|
703
|
+
|
|
704
|
+
if (rpc_modules) {
|
|
705
|
+
for (const name of exports) {
|
|
706
|
+
const func_path = file_path + '#' + name;
|
|
707
|
+
// needs to be a sha256 hash of func_path, to avoid leaking file structure
|
|
708
|
+
const hash = createHash('sha256').update(func_path).digest('hex').slice(0, 8);
|
|
709
|
+
rpc_modules.set(hash, [file_path, name]);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return b.export(
|
|
714
|
+
b.const(
|
|
715
|
+
'_$_server_$_',
|
|
716
|
+
b.call(
|
|
717
|
+
b.thunk(
|
|
718
|
+
b.block([
|
|
719
|
+
b.var('_$_server_$_', b.object([])),
|
|
720
|
+
...block.body,
|
|
721
|
+
b.return(b.id('_$_server_$_')),
|
|
722
|
+
]),
|
|
723
|
+
),
|
|
724
|
+
),
|
|
725
|
+
),
|
|
726
|
+
);
|
|
603
727
|
},
|
|
604
728
|
};
|
|
605
729
|
|
|
@@ -614,6 +738,8 @@ export function transform_server(filename, source, analysis) {
|
|
|
614
738
|
scopes: analysis.scopes,
|
|
615
739
|
stylesheets: [],
|
|
616
740
|
component_metadata,
|
|
741
|
+
inside_server_block: false,
|
|
742
|
+
filename,
|
|
617
743
|
};
|
|
618
744
|
|
|
619
745
|
const program = /** @type {ESTree.Program} */ (
|
package/src/compiler/scope.js
CHANGED
|
@@ -169,6 +169,14 @@ export function create_scopes(ast, root, parent) {
|
|
|
169
169
|
next({ scope });
|
|
170
170
|
},
|
|
171
171
|
|
|
172
|
+
ServerBlock(node, { state, next }) {
|
|
173
|
+
const scope = state.scope.child();
|
|
174
|
+
scope.server_block = true;
|
|
175
|
+
scopes.set(node, scope);
|
|
176
|
+
|
|
177
|
+
next({ scope });
|
|
178
|
+
},
|
|
179
|
+
|
|
172
180
|
FunctionExpression(node, { state, next }) {
|
|
173
181
|
const scope = state.scope.child();
|
|
174
182
|
scopes.set(node, scope);
|
|
@@ -327,6 +335,12 @@ export class Scope {
|
|
|
327
335
|
*/
|
|
328
336
|
tracing = null;
|
|
329
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Is this scope a top-level server block scope
|
|
340
|
+
* @type {boolean}
|
|
341
|
+
*/
|
|
342
|
+
server_block = false;
|
|
343
|
+
|
|
330
344
|
/**
|
|
331
345
|
*
|
|
332
346
|
* @param {ScopeRoot} root
|
|
@@ -353,7 +367,7 @@ export class Scope {
|
|
|
353
367
|
return this.parent.declare(node, kind, declaration_kind);
|
|
354
368
|
}
|
|
355
369
|
|
|
356
|
-
if (declaration_kind === 'import') {
|
|
370
|
+
if (declaration_kind === 'import' && !this.parent.server_block) {
|
|
357
371
|
return this.parent.declare(node, kind, declaration_kind, initial);
|
|
358
372
|
}
|
|
359
373
|
}
|
|
@@ -151,7 +151,7 @@ export type DeclarationKind =
|
|
|
151
151
|
/**
|
|
152
152
|
* Binding kinds
|
|
153
153
|
*/
|
|
154
|
-
export type BindingKind = 'normal' | '
|
|
154
|
+
export type BindingKind = 'normal' | 'for_pattern' | 'rest_prop' | 'prop' | 'prop_fallback';
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* A variable binding in a scope
|
|
@@ -299,4 +299,4 @@ export interface DelegatedEventResult {
|
|
|
299
299
|
hoisted: boolean;
|
|
300
300
|
/** The hoisted function */
|
|
301
301
|
function?: FunctionExpression | FunctionDeclaration | ArrowFunctionExpression;
|
|
302
|
-
}
|
|
302
|
+
}
|
package/src/compiler/utils.js
CHANGED
|
@@ -316,6 +316,8 @@ function get_hoisted_params(node, context) {
|
|
|
316
316
|
if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
|
|
317
317
|
if (binding.kind === 'prop') {
|
|
318
318
|
push_unique(b.id('__props'));
|
|
319
|
+
} else if (binding.kind === 'for_pattern') {
|
|
320
|
+
push_unique(binding.metadata.pattern);
|
|
319
321
|
} else if (binding.kind === 'prop_fallback') {
|
|
320
322
|
push_unique(b.id(binding.node.name));
|
|
321
323
|
} else if (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @import { Block } from '#client' */
|
|
1
|
+
/** @import { Block, Tracked } from '#client' */
|
|
2
2
|
|
|
3
3
|
import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
|
|
4
4
|
import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
|
|
@@ -12,31 +12,45 @@ import { array_from, is_array } from './utils.js';
|
|
|
12
12
|
* @param {Node} anchor
|
|
13
13
|
* @param {V} value
|
|
14
14
|
* @param {number} index
|
|
15
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
15
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
16
16
|
* @param {boolean} is_indexed
|
|
17
|
+
* @param {boolean} is_keyed
|
|
17
18
|
* @returns {Block}
|
|
18
19
|
*/
|
|
19
|
-
function create_item(anchor, value, index, render_fn, is_indexed) {
|
|
20
|
+
function create_item(anchor, value, index, render_fn, is_indexed, is_keyed) {
|
|
20
21
|
var b = branch(() => {
|
|
21
22
|
var tracked_index;
|
|
23
|
+
/** @type {V | Tracked} */
|
|
24
|
+
var tracked_value = value;
|
|
22
25
|
|
|
23
|
-
if (is_indexed) {
|
|
26
|
+
if (is_indexed || is_keyed) {
|
|
24
27
|
var block = /** @type {Block} */ (active_block);
|
|
25
28
|
|
|
26
29
|
if (block.s === null) {
|
|
27
|
-
|
|
30
|
+
if (is_indexed) {
|
|
31
|
+
tracked_index = tracked(index, block);
|
|
32
|
+
}
|
|
33
|
+
if (is_keyed) {
|
|
34
|
+
tracked_value = tracked(value, block);
|
|
35
|
+
}
|
|
28
36
|
|
|
29
37
|
block.s = {
|
|
30
38
|
start: null,
|
|
31
39
|
end: null,
|
|
32
40
|
i: tracked_index,
|
|
41
|
+
v: tracked_value,
|
|
33
42
|
};
|
|
34
43
|
} else {
|
|
35
|
-
|
|
44
|
+
if (is_indexed) {
|
|
45
|
+
tracked_index = block.s.i;
|
|
46
|
+
}
|
|
47
|
+
if (is_keyed) {
|
|
48
|
+
tracked_index = block.s.v;
|
|
49
|
+
}
|
|
36
50
|
}
|
|
37
|
-
render_fn(anchor,
|
|
51
|
+
render_fn(anchor, tracked_value, tracked_index);
|
|
38
52
|
} else {
|
|
39
|
-
render_fn(anchor,
|
|
53
|
+
render_fn(anchor, tracked_value);
|
|
40
54
|
}
|
|
41
55
|
});
|
|
42
56
|
return b;
|
|
@@ -87,7 +101,7 @@ function collection_to_array(collection) {
|
|
|
87
101
|
* @template V
|
|
88
102
|
* @param {Element} node
|
|
89
103
|
* @param {() => V[] | Iterable<V>} get_collection
|
|
90
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
104
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
91
105
|
* @param {number} flags
|
|
92
106
|
* @returns {void}
|
|
93
107
|
*/
|
|
@@ -116,7 +130,7 @@ export function for_block(node, get_collection, render_fn, flags) {
|
|
|
116
130
|
* @template K
|
|
117
131
|
* @param {Element} node
|
|
118
132
|
* @param {() => V[] | Iterable<V>} get_collection
|
|
119
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
133
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
120
134
|
* @param {number} flags
|
|
121
135
|
* @param {(item: V) => K} [get_key]
|
|
122
136
|
* @returns {void}
|
|
@@ -175,13 +189,22 @@ function update_index(block, index) {
|
|
|
175
189
|
set(block.s.i, index, block);
|
|
176
190
|
}
|
|
177
191
|
|
|
192
|
+
/**
|
|
193
|
+
* @param {Block} block
|
|
194
|
+
* @param {any} value
|
|
195
|
+
* @returns {void}
|
|
196
|
+
*/
|
|
197
|
+
function update_value(block, value) {
|
|
198
|
+
set(block.s.v, value, block);
|
|
199
|
+
}
|
|
200
|
+
|
|
178
201
|
/**
|
|
179
202
|
* @template V
|
|
180
203
|
* @template K
|
|
181
204
|
* @param {Element | Text} anchor
|
|
182
205
|
* @param {Block} block
|
|
183
206
|
* @param {V[]} b
|
|
184
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
207
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
185
208
|
* @param {boolean} is_controlled
|
|
186
209
|
* @param {boolean} is_indexed
|
|
187
210
|
* @param {(item: V) => K} get_key
|
|
@@ -236,7 +259,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
236
259
|
// Fast-path for create
|
|
237
260
|
if (a_length === 0) {
|
|
238
261
|
for (; j < b_length; j++) {
|
|
239
|
-
b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed);
|
|
262
|
+
b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, true);
|
|
240
263
|
}
|
|
241
264
|
state.array = b;
|
|
242
265
|
state.blocks = b_blocks;
|
|
@@ -261,6 +284,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
261
284
|
if (is_indexed) {
|
|
262
285
|
update_index(b_block, j);
|
|
263
286
|
}
|
|
287
|
+
update_value(b_block, b_val);
|
|
264
288
|
++j;
|
|
265
289
|
if (j > a_end || j > b_end) {
|
|
266
290
|
break outer;
|
|
@@ -282,6 +306,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
282
306
|
if (is_indexed) {
|
|
283
307
|
update_index(b_block, b_end);
|
|
284
308
|
}
|
|
309
|
+
update_value(b_block, b_val);
|
|
285
310
|
a_end--;
|
|
286
311
|
b_end--;
|
|
287
312
|
if (j > a_end || j > b_end) {
|
|
@@ -301,7 +326,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
301
326
|
while (j <= b_end) {
|
|
302
327
|
b_val = b[j];
|
|
303
328
|
var target = j >= a_length ? anchor : a_blocks[j].s.start;
|
|
304
|
-
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed);
|
|
329
|
+
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, true);
|
|
305
330
|
j++;
|
|
306
331
|
}
|
|
307
332
|
}
|
|
@@ -348,6 +373,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
348
373
|
if (is_indexed) {
|
|
349
374
|
update_index(b_block, j);
|
|
350
375
|
}
|
|
376
|
+
update_value(b_block, b_val);
|
|
351
377
|
++patched;
|
|
352
378
|
break;
|
|
353
379
|
}
|
|
@@ -387,9 +413,11 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
387
413
|
pos = j;
|
|
388
414
|
}
|
|
389
415
|
block = b_blocks[j] = a_blocks[i];
|
|
416
|
+
b_val = b[j];
|
|
390
417
|
if (is_indexed) {
|
|
391
418
|
update_index(block, j);
|
|
392
419
|
}
|
|
420
|
+
update_value(b_block, b_val);
|
|
393
421
|
++patched;
|
|
394
422
|
} else if (!fast_path_removal) {
|
|
395
423
|
destroy_block(a_blocks[i]);
|
|
@@ -417,7 +445,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
417
445
|
next_pos = pos + 1;
|
|
418
446
|
|
|
419
447
|
var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
|
|
420
|
-
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
|
|
448
|
+
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
|
|
421
449
|
} else if (j < 0 || i !== seq[j]) {
|
|
422
450
|
pos = i + b_start;
|
|
423
451
|
b_val = b[pos];
|
|
@@ -437,7 +465,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
437
465
|
next_pos = pos + 1;
|
|
438
466
|
|
|
439
467
|
var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
|
|
440
|
-
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
|
|
468
|
+
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
|
|
441
469
|
}
|
|
442
470
|
}
|
|
443
471
|
}
|
|
@@ -452,7 +480,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
452
480
|
* @param {Element | Text} anchor
|
|
453
481
|
* @param {Block} block
|
|
454
482
|
* @param {V[]} b
|
|
455
|
-
* @param {(anchor: Node, value: V, index?: any) => Block} render_fn
|
|
483
|
+
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
|
|
456
484
|
* @param {boolean} is_controlled
|
|
457
485
|
* @param {boolean} is_indexed
|
|
458
486
|
* @returns {void}
|
|
@@ -505,7 +533,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
505
533
|
// Fast-path for create
|
|
506
534
|
if (a_length === 0) {
|
|
507
535
|
for (; j < b_length; j++) {
|
|
508
|
-
b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed);
|
|
536
|
+
b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, false);
|
|
509
537
|
}
|
|
510
538
|
state.array = b;
|
|
511
539
|
state.blocks = b_blocks;
|
|
@@ -560,7 +588,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
560
588
|
while (j <= b_end) {
|
|
561
589
|
b_val = b[j];
|
|
562
590
|
var target = j >= a_length ? anchor : a_blocks[j].s.start;
|
|
563
|
-
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed);
|
|
591
|
+
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, false);
|
|
564
592
|
j++;
|
|
565
593
|
}
|
|
566
594
|
}
|
|
@@ -673,7 +701,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
673
701
|
next_pos = pos + 1;
|
|
674
702
|
|
|
675
703
|
var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
|
|
676
|
-
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
|
|
704
|
+
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
|
|
677
705
|
} else if (j < 0 || i !== seq[j]) {
|
|
678
706
|
pos = i + b_start;
|
|
679
707
|
b_val = b[pos];
|
|
@@ -693,7 +721,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
693
721
|
next_pos = pos + 1;
|
|
694
722
|
|
|
695
723
|
var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
|
|
696
|
-
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
|
|
724
|
+
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
|
|
697
725
|
}
|
|
698
726
|
}
|
|
699
727
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} hash
|
|
4
|
+
* @param {any[]} args
|
|
5
|
+
*/
|
|
6
|
+
export function rpc(hash, args) {
|
|
7
|
+
return fetch('/_$_ripple_rpc_$_/' + hash, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'application/json'
|
|
11
|
+
},
|
|
12
|
+
body: JSON.stringify(args)
|
|
13
|
+
}).then(res => res.json());
|
|
14
|
+
}
|
package/src/utils/builders.js
CHANGED
|
@@ -220,6 +220,17 @@ export function export_default(declaration) {
|
|
|
220
220
|
return { type: 'ExportDefaultDeclaration', declaration };
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* @param {ESTree.Declaration | null} declaration
|
|
225
|
+
* @param {ESTree.ExportSpecifier[]} [specifiers]
|
|
226
|
+
* @param {ESTree.ImportAttribute[]} [attributes]
|
|
227
|
+
* @param {ESTree.Literal | null} [source]
|
|
228
|
+
* @returns {ESTree.ExportNamedDeclaration}
|
|
229
|
+
*/
|
|
230
|
+
export function export_builder(declaration, specifiers = [], attributes = [], source = null) {
|
|
231
|
+
return { type: 'ExportNamedDeclaration', declaration, specifiers, attributes, source };
|
|
232
|
+
}
|
|
233
|
+
|
|
223
234
|
/**
|
|
224
235
|
* @param {ESTree.Identifier} id
|
|
225
236
|
* @param {ESTree.Pattern[]} params
|
|
@@ -581,16 +592,17 @@ export function method(kind, key, params, body, computed = false, is_static = fa
|
|
|
581
592
|
* @param {ESTree.Identifier | null} id
|
|
582
593
|
* @param {ESTree.Pattern[]} params
|
|
583
594
|
* @param {ESTree.BlockStatement} body
|
|
595
|
+
* @param {boolean} async
|
|
584
596
|
* @returns {ESTree.FunctionExpression}
|
|
585
597
|
*/
|
|
586
|
-
function function_builder(id, params, body) {
|
|
598
|
+
function function_builder(id, params, body, async = false) {
|
|
587
599
|
return {
|
|
588
600
|
type: 'FunctionExpression',
|
|
589
601
|
id,
|
|
590
602
|
params,
|
|
591
603
|
body,
|
|
592
604
|
generator: false,
|
|
593
|
-
async
|
|
605
|
+
async,
|
|
594
606
|
metadata: /** @type {any} */ (null), // should not be used by codegen
|
|
595
607
|
};
|
|
596
608
|
}
|
|
@@ -812,6 +824,7 @@ export {
|
|
|
812
824
|
let_builder as let,
|
|
813
825
|
const_builder as const,
|
|
814
826
|
var_builder as var,
|
|
827
|
+
export_builder as export,
|
|
815
828
|
true_instance as true,
|
|
816
829
|
false_instance as false,
|
|
817
830
|
break_statement as break,
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
+
exports[`compiler success tests > compiles TSInstantiationExpression 1`] = `
|
|
4
|
+
"import * as _$_ from 'ripple/internal/client';
|
|
5
|
+
|
|
6
|
+
function makeBox(value) {
|
|
7
|
+
return { value };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const makeStringBox = (makeBox);
|
|
11
|
+
const stringBox = makeStringBox('abc');
|
|
12
|
+
const ErrorMap = (Map);
|
|
13
|
+
const errorMap = new ErrorMap();"
|
|
14
|
+
`;
|
|
15
|
+
|
|
3
16
|
exports[`compiler success tests > compiles tracked values in effect with assignment expression 1`] = `"state.count = _$_.get(count);"`;
|
|
4
17
|
|
|
5
18
|
exports[`compiler success tests > compiles tracked values in effect with update expressions 1`] = `
|
|
@@ -198,7 +198,7 @@ exports[`for statements > correctly handles the index in a for...of loop 3`] = `
|
|
|
198
198
|
</div>
|
|
199
199
|
`;
|
|
200
200
|
|
|
201
|
-
exports[`for statements > handles
|
|
201
|
+
exports[`for statements > handles updating with new objects with same key 1`] = `
|
|
202
202
|
<div>
|
|
203
203
|
<!---->
|
|
204
204
|
<div>
|
|
@@ -218,17 +218,17 @@ exports[`for statements > handles reversing an array manually 1`] = `
|
|
|
218
218
|
</div>
|
|
219
219
|
`;
|
|
220
220
|
|
|
221
|
-
exports[`for statements > handles
|
|
221
|
+
exports[`for statements > handles updating with new objects with same key 2`] = `
|
|
222
222
|
<div>
|
|
223
223
|
<!---->
|
|
224
224
|
<div>
|
|
225
|
-
0:Item
|
|
225
|
+
0:Item 1!
|
|
226
226
|
</div>
|
|
227
227
|
<div>
|
|
228
|
-
1:Item 2
|
|
228
|
+
1:Item 2!
|
|
229
229
|
</div>
|
|
230
230
|
<div>
|
|
231
|
-
2:Item
|
|
231
|
+
2:Item 3!
|
|
232
232
|
</div>
|
|
233
233
|
<!---->
|
|
234
234
|
<button>
|
|
@@ -495,4 +495,19 @@ effect(() => {
|
|
|
495
495
|
const effectMatch = result.js.code.match(/effect\(\(\) => \{([\s\S]+?)\n\t\}\)\)/);
|
|
496
496
|
expect(effectMatch[1].trim()).toMatchSnapshot();
|
|
497
497
|
});
|
|
498
|
+
|
|
499
|
+
it('compiles TSInstantiationExpression', () => {
|
|
500
|
+
const source =
|
|
501
|
+
`function makeBox<T>(value: T) {
|
|
502
|
+
return { value };
|
|
503
|
+
}
|
|
504
|
+
const makeStringBox = makeBox<string>;
|
|
505
|
+
const stringBox = makeStringBox('abc');
|
|
506
|
+
const ErrorMap = Map<string, Error>;
|
|
507
|
+
const errorMap = new ErrorMap();`;
|
|
508
|
+
|
|
509
|
+
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
510
|
+
|
|
511
|
+
expect(result.js.code).toMatchSnapshot();
|
|
512
|
+
});
|
|
498
513
|
});
|
|
@@ -659,4 +659,35 @@ describe('composite components', () => {
|
|
|
659
659
|
|
|
660
660
|
expect(container.querySelector('#container').textContent).toBe('I am child 1');
|
|
661
661
|
});
|
|
662
|
+
|
|
663
|
+
it('mutating a tracked value prop should work as intended', () => {
|
|
664
|
+
const logs = [];
|
|
665
|
+
|
|
666
|
+
component Counter({count}) {
|
|
667
|
+
effect(() => {
|
|
668
|
+
logs.push(@count);
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
<button onClick={() => @count = @count + 1}>{'+'}</button>
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
component App() {
|
|
675
|
+
const count = track(0);
|
|
676
|
+
|
|
677
|
+
<div>
|
|
678
|
+
<Counter count={count} />
|
|
679
|
+
</div>
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
render(App);
|
|
683
|
+
flushSync();
|
|
684
|
+
|
|
685
|
+
expect(logs).toEqual([0]);
|
|
686
|
+
|
|
687
|
+
const button = container.querySelector('button');
|
|
688
|
+
button.click();
|
|
689
|
+
flushSync();
|
|
690
|
+
|
|
691
|
+
expect(logs).toEqual([0, 1]);
|
|
692
|
+
})
|
|
662
693
|
});
|
|
@@ -145,12 +145,12 @@ describe('for statements', () => {
|
|
|
145
145
|
expect(container).toMatchSnapshot();
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
it('handles
|
|
148
|
+
it('handles updating with new objects with same key', () => {
|
|
149
149
|
component App() {
|
|
150
150
|
let items = track([
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
{ id: 1, text: 'Item 1' },
|
|
152
|
+
{ id: 2, text: 'Item 2' },
|
|
153
|
+
{ id: 3, text: 'Item 3' },
|
|
154
154
|
]);
|
|
155
155
|
|
|
156
156
|
for (let item of @items; index i; key item.id) {
|
|
@@ -163,9 +163,9 @@ describe('for statements', () => {
|
|
|
163
163
|
@items[2].id = 1;
|
|
164
164
|
|
|
165
165
|
@items = [
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
{...@items[0], text: 'Item 1!'},
|
|
167
|
+
{...@items[1], text: 'Item 2!'},
|
|
168
|
+
{...@items[2], text: 'Item 3!'},
|
|
169
169
|
];
|
|
170
170
|
}}>{"Reverse"}</button>
|
|
171
171
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mount, flushSync, track } from 'ripple';
|
|
3
|
+
|
|
4
|
+
describe('switch statements', () => {
|
|
5
|
+
let container;
|
|
6
|
+
|
|
7
|
+
function render(component) {
|
|
8
|
+
mount(component, {
|
|
9
|
+
target: container
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
container = document.createElement('div');
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
document.body.removeChild(container);
|
|
20
|
+
container = null;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('renders simple switch with literal cases', () => {
|
|
24
|
+
component App() {
|
|
25
|
+
let value = track('b');
|
|
26
|
+
|
|
27
|
+
<button onClick={() => @value = 'c'}>{'Change to C'}</button>
|
|
28
|
+
<button onClick={() => @value = 'a'}>{'Change to A'}</button>
|
|
29
|
+
|
|
30
|
+
switch (@value) {
|
|
31
|
+
case 'a':
|
|
32
|
+
<div>{'Case A'}</div>
|
|
33
|
+
break;
|
|
34
|
+
case 'b':
|
|
35
|
+
<div>{'Case B'}</div>
|
|
36
|
+
break;
|
|
37
|
+
case 'c':
|
|
38
|
+
<div>{'Case C'}</div>
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
<div>{'Default Case'}</div>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
render(App);
|
|
46
|
+
expect(container.textContent).toBe('Change to CChange to ACase B');
|
|
47
|
+
|
|
48
|
+
container.querySelectorAll('button')[0].click(); // Change to C
|
|
49
|
+
flushSync();
|
|
50
|
+
expect(container.textContent).toBe('Change to CChange to ACase C');
|
|
51
|
+
|
|
52
|
+
container.querySelectorAll('button')[1].click(); // Change to A
|
|
53
|
+
flushSync();
|
|
54
|
+
expect(container.textContent).toBe('Change to CChange to ACase A');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders switch with reactive discriminant', () => {
|
|
58
|
+
component App() {
|
|
59
|
+
let count = track(1);
|
|
60
|
+
|
|
61
|
+
<button onClick={() => @count++}>{'Increment'}</button>
|
|
62
|
+
|
|
63
|
+
switch (@count) {
|
|
64
|
+
case 1:
|
|
65
|
+
<div>{'Count is 1'}</div>
|
|
66
|
+
break;
|
|
67
|
+
case 2:
|
|
68
|
+
<div>{'Count is 2'}</div>
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
<div>{'Count is other'}</div>
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
render(App);
|
|
76
|
+
expect(container.textContent).toBe('IncrementCount is 1');
|
|
77
|
+
|
|
78
|
+
container.querySelector('button').click();
|
|
79
|
+
flushSync();
|
|
80
|
+
expect(container.textContent).toBe('IncrementCount is 2');
|
|
81
|
+
|
|
82
|
+
container.querySelector('button').click();
|
|
83
|
+
flushSync();
|
|
84
|
+
expect(container.textContent).toBe('IncrementCount is other');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('renders switch with default clause only', () => {
|
|
88
|
+
component App() {
|
|
89
|
+
let value = track('x');
|
|
90
|
+
|
|
91
|
+
<button onClick={() => @value = 'y'}>{'Change Value'}</button>
|
|
92
|
+
|
|
93
|
+
switch (@value) {
|
|
94
|
+
default:
|
|
95
|
+
<div>{'Default for ' + @value}</div>
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
render(App);
|
|
100
|
+
expect(container.textContent).toBe('Change ValueDefault for x');
|
|
101
|
+
|
|
102
|
+
container.querySelector('button').click();
|
|
103
|
+
flushSync();
|
|
104
|
+
expect(container.textContent).toBe('Change ValueDefault for y');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders switch with template content and JS logic', () => {
|
|
108
|
+
component App() {
|
|
109
|
+
let status = track('active');
|
|
110
|
+
let message = track('');
|
|
111
|
+
|
|
112
|
+
<button onClick={() => @status = 'pending'}>{'Pending'}</button>
|
|
113
|
+
<button onClick={() => @status = 'completed'}>{'Completed'}</button>
|
|
114
|
+
<button onClick={() => @status = 'error'}>{'Error'}</button>
|
|
115
|
+
|
|
116
|
+
switch (@status) {
|
|
117
|
+
case 'active':
|
|
118
|
+
message = 'Currently active.';
|
|
119
|
+
<div>{'Status: ' + @message}</div>
|
|
120
|
+
break;
|
|
121
|
+
case 'pending':
|
|
122
|
+
message = 'Waiting for completion.';
|
|
123
|
+
<div>{'Status: ' + @message}</div>
|
|
124
|
+
break;
|
|
125
|
+
case 'completed':
|
|
126
|
+
message = 'Task finished!';
|
|
127
|
+
<div class="success">{'Status: ' + @message}</div>
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
message = 'An error occurred.';
|
|
131
|
+
<div class="error">{'Status: ' + @message}</div>
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render(App);
|
|
136
|
+
expect(container.textContent).toBe('PendingCompletedErrorStatus: Currently active.');
|
|
137
|
+
|
|
138
|
+
container.querySelectorAll('button')[0].click(); // Pending
|
|
139
|
+
flushSync();
|
|
140
|
+
expect(container.textContent).toBe('PendingCompletedErrorStatus: Waiting for completion.');
|
|
141
|
+
|
|
142
|
+
container.querySelectorAll('button')[1].click(); // Completed
|
|
143
|
+
flushSync();
|
|
144
|
+
expect(container.textContent).toBe('PendingCompletedErrorStatus: Task finished!');
|
|
145
|
+
expect(container.querySelector('.success')).toBeTruthy();
|
|
146
|
+
|
|
147
|
+
container.querySelectorAll('button')[2].click(); // Error
|
|
148
|
+
flushSync();
|
|
149
|
+
expect(container.textContent).toBe('PendingCompletedErrorStatus: An error occurred.');
|
|
150
|
+
expect(container.querySelector('.error')).toBeTruthy();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`compiler success tests > compiles TSInstantiationExpression 1`] = `
|
|
4
|
+
"import * as _$_ from 'ripple/internal/client';
|
|
5
|
+
|
|
6
|
+
function makeBox(value) {
|
|
7
|
+
return { value };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const makeStringBox = (makeBox);
|
|
11
|
+
const stringBox = makeStringBox('abc');
|
|
12
|
+
const ErrorMap = (Map);
|
|
13
|
+
const errorMap = new ErrorMap();"
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
exports[`compiler success tests > compiles imported component with conditional async in SSR 1`] = `
|
|
17
|
+
"import * as _$_ from 'ripple/internal/server';
|
|
18
|
+
|
|
19
|
+
import { ChildComponent } from './Child.ripple';
|
|
20
|
+
|
|
21
|
+
export async function App(__output) {
|
|
22
|
+
return _$_.async(async () => {
|
|
23
|
+
_$_.push_component();
|
|
24
|
+
__output.push('<div');
|
|
25
|
+
__output.push('>');
|
|
26
|
+
|
|
27
|
+
if (ChildComponent.async) {
|
|
28
|
+
await ChildComponent(__output, { message: "hello" });
|
|
29
|
+
} else {
|
|
30
|
+
ChildComponent(__output, { message: "hello" });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
__output.push('</div>');
|
|
34
|
+
_$_.pop_component();
|
|
35
|
+
});
|
|
36
|
+
}"
|
|
37
|
+
`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { compile } from 'ripple/compiler'
|
|
3
|
+
|
|
4
|
+
describe('compiler success tests', () => {
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
it('compiles TSInstantiationExpression', () => {
|
|
8
|
+
const source =
|
|
9
|
+
`function makeBox<T>(value: T) {
|
|
10
|
+
return { value };
|
|
11
|
+
}
|
|
12
|
+
const makeStringBox = makeBox<string>;
|
|
13
|
+
const stringBox = makeStringBox('abc');
|
|
14
|
+
const ErrorMap = Map<string, Error>;
|
|
15
|
+
const errorMap = new ErrorMap();`;
|
|
16
|
+
|
|
17
|
+
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
18
|
+
|
|
19
|
+
expect(result.js.code).toMatchSnapshot();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('compiles imported component with conditional async in SSR', () => {
|
|
23
|
+
const source =
|
|
24
|
+
`import { ChildComponent } from './Child.ripple';
|
|
25
|
+
|
|
26
|
+
export component App() {
|
|
27
|
+
<div>
|
|
28
|
+
<ChildComponent message="hello" />
|
|
29
|
+
</div>
|
|
30
|
+
}`;
|
|
31
|
+
|
|
32
|
+
const result = compile(source, 'test.ripple', { mode: 'server' });
|
|
33
|
+
|
|
34
|
+
// Should use if-statement instead of ternary to avoid parser issues
|
|
35
|
+
expect(result.js.code).toContain('if (ChildComponent.async)');
|
|
36
|
+
expect(result.js.code).toContain('await ChildComponent');
|
|
37
|
+
expect(result.js.code).toMatchSnapshot();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render } from 'ripple/server';
|
|
3
|
+
|
|
4
|
+
describe('SSR: switch statements', () => {
|
|
5
|
+
it('renders simple switch with literal cases', async () => {
|
|
6
|
+
component App() {
|
|
7
|
+
let value = 'b';
|
|
8
|
+
|
|
9
|
+
switch (value) {
|
|
10
|
+
case 'a':
|
|
11
|
+
<div>{'Case A'}</div>
|
|
12
|
+
break;
|
|
13
|
+
case 'b':
|
|
14
|
+
<div>{'Case B'}</div>
|
|
15
|
+
break;
|
|
16
|
+
case 'c':
|
|
17
|
+
<div>{'Case C'}</div>
|
|
18
|
+
break;
|
|
19
|
+
default:
|
|
20
|
+
<div>{'Default Case'}</div>
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { body } = await render(App);
|
|
25
|
+
expect(body).toBe('<div>Case B</div>');
|
|
26
|
+
});
|
|
27
|
+
});
|