ripple 0.2.1 → 0.2.2
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 -2
- package/src/compiler/phases/1-parse/index.js +22 -44
- package/src/compiler/phases/2-analyze/index.js +5 -3
- package/src/compiler/phases/3-transform/index.js +45 -80
- package/src/compiler/scope.js +0 -13
- package/src/runtime/internal/client/blocks.js +5 -0
- package/src/runtime/internal/client/index.js +1 -1
- package/src/runtime/internal/client/render.js +18 -0
- package/types/index.d.ts +11 -1
- package/README.md +0 -286
- package/src/ai.js +0 -292
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is a TypeScript UI framework for the web",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.2",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index.js",
|
|
9
9
|
"main": "src/runtime/index.js",
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@ai-sdk/anthropic": "^2.0.5",
|
|
52
51
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
53
52
|
"@types/estree": "^1.0.8",
|
|
54
53
|
"acorn": "^8.15.0",
|
|
@@ -4,6 +4,17 @@ import { parse_style } from './style.js';
|
|
|
4
4
|
|
|
5
5
|
const parser = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }), RipplePlugin());
|
|
6
6
|
|
|
7
|
+
function convert_from_jsx(node) {
|
|
8
|
+
if (node.type === 'JSXIdentifier') {
|
|
9
|
+
node.type = 'Identifier';
|
|
10
|
+
} else if (node.type === 'JSXMemberExpression') {
|
|
11
|
+
node.type = 'MemberExpression';
|
|
12
|
+
node.object = convert_from_jsx(node.object)
|
|
13
|
+
node.property = convert_from_jsx(node.property)
|
|
14
|
+
}
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
function RipplePlugin(config) {
|
|
8
19
|
return (Parser) => {
|
|
9
20
|
const original = acorn.Parser.prototype;
|
|
@@ -17,7 +28,7 @@ function RipplePlugin(config) {
|
|
|
17
28
|
if (super.shouldParseExportStatement()) {
|
|
18
29
|
return true;
|
|
19
30
|
}
|
|
20
|
-
if (this.value === 'component'
|
|
31
|
+
if (this.value === 'component') {
|
|
21
32
|
return true;
|
|
22
33
|
}
|
|
23
34
|
return this.type.keyword === 'var';
|
|
@@ -28,17 +39,6 @@ function RipplePlugin(config) {
|
|
|
28
39
|
let node = this.startNode();
|
|
29
40
|
this.next();
|
|
30
41
|
|
|
31
|
-
if (this.type === tok.at) {
|
|
32
|
-
this.next();
|
|
33
|
-
|
|
34
|
-
if (this.value === 'fragment') {
|
|
35
|
-
node.decorator = 'fragment';
|
|
36
|
-
this.next();
|
|
37
|
-
} else {
|
|
38
|
-
throw new Error(`Invalid syntax @` + this.value);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
42
|
node.expression =
|
|
43
43
|
this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
|
|
44
44
|
this.expect(tt.braceR);
|
|
@@ -59,7 +59,12 @@ function RipplePlugin(config) {
|
|
|
59
59
|
jsx_parseAttribute() {
|
|
60
60
|
let node = this.startNode();
|
|
61
61
|
if (this.eat(tt.braceL)) {
|
|
62
|
-
if (this.
|
|
62
|
+
if (this.type === tt.ellipsis) {
|
|
63
|
+
this.expect(tt.ellipsis);
|
|
64
|
+
node.argument = this.parseMaybeAssign();
|
|
65
|
+
this.expect(tt.braceR);
|
|
66
|
+
return this.finishNode(node, 'SpreadAttribute');
|
|
67
|
+
} else if (this.lookahead().type === tt.ellipsis) {
|
|
63
68
|
this.expect(tt.ellipsis);
|
|
64
69
|
node.argument = this.parseMaybeAssign();
|
|
65
70
|
this.expect(tt.braceR);
|
|
@@ -250,7 +255,7 @@ function RipplePlugin(config) {
|
|
|
250
255
|
// '}'
|
|
251
256
|
if (
|
|
252
257
|
ch === 125 &&
|
|
253
|
-
(this.#path.at(-1).type === 'Component'
|
|
258
|
+
(this.#path.at(-1).type === 'Component')
|
|
254
259
|
) {
|
|
255
260
|
return original.readToken.call(this, ch);
|
|
256
261
|
}
|
|
@@ -313,8 +318,8 @@ function RipplePlugin(config) {
|
|
|
313
318
|
if (open.name.type === 'JSXIdentifier') {
|
|
314
319
|
open.name.type = 'Identifier';
|
|
315
320
|
}
|
|
316
|
-
|
|
317
|
-
element.id
|
|
321
|
+
|
|
322
|
+
element.id = convert_from_jsx(open.name);
|
|
318
323
|
element.attributes = open.attributes;
|
|
319
324
|
element.selfClosing = open.selfClosing;
|
|
320
325
|
element.metadata = {};
|
|
@@ -377,10 +382,7 @@ function RipplePlugin(config) {
|
|
|
377
382
|
|
|
378
383
|
if (this.type.label === '{') {
|
|
379
384
|
const node = this.jsx_parseExpressionContainer();
|
|
380
|
-
node.type =
|
|
381
|
-
if (node.decorator === 'fragment' && node.expression.type !== 'CallExpression') {
|
|
382
|
-
throw new Error('{@fragment} must be a function call');
|
|
383
|
-
}
|
|
385
|
+
node.type = 'Text';
|
|
384
386
|
body.push(node);
|
|
385
387
|
} else if (this.type.label === '}') {
|
|
386
388
|
return;
|
|
@@ -448,29 +450,6 @@ function RipplePlugin(config) {
|
|
|
448
450
|
return node;
|
|
449
451
|
}
|
|
450
452
|
|
|
451
|
-
if (this.value === 'fragment') {
|
|
452
|
-
const node = this.startNode();
|
|
453
|
-
node.type = 'Fragment';
|
|
454
|
-
this.next();
|
|
455
|
-
this.enterScope(0);
|
|
456
|
-
node.id = this.parseIdent();
|
|
457
|
-
this.parseFunctionParams(node);
|
|
458
|
-
this.eat(tt.braceL);
|
|
459
|
-
node.body = [];
|
|
460
|
-
this.#path.push(node);
|
|
461
|
-
|
|
462
|
-
this.parseTemplateBody(node.body);
|
|
463
|
-
|
|
464
|
-
this.#path.pop();
|
|
465
|
-
this.exitScope();
|
|
466
|
-
|
|
467
|
-
this.finishNode(node, 'Fragment');
|
|
468
|
-
this.next();
|
|
469
|
-
this.awaitPos = 0;
|
|
470
|
-
|
|
471
|
-
return node;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
453
|
return super.parseStatement(context, topLevel, exports);
|
|
475
454
|
}
|
|
476
455
|
|
|
@@ -479,7 +458,6 @@ function RipplePlugin(config) {
|
|
|
479
458
|
|
|
480
459
|
if (
|
|
481
460
|
parent?.type === 'Component' ||
|
|
482
|
-
parent?.type === 'Fragment' ||
|
|
483
461
|
parent?.type === 'Element'
|
|
484
462
|
) {
|
|
485
463
|
if (createNewLexicalScope === void 0) createNewLexicalScope = true;
|
|
@@ -348,8 +348,10 @@ const visitors = {
|
|
|
348
348
|
},
|
|
349
349
|
|
|
350
350
|
Element(node, { state, visit }) {
|
|
351
|
-
const
|
|
352
|
-
|
|
351
|
+
const is_dom_element =
|
|
352
|
+
node.id.type === 'Identifier' &&
|
|
353
|
+
node.id.name[0].toLowerCase() === node.id.name[0] &&
|
|
354
|
+
node.id.name[0] !== '$';
|
|
353
355
|
const attribute_names = new Set();
|
|
354
356
|
|
|
355
357
|
if (is_dom_element) {
|
|
@@ -396,7 +398,7 @@ const visitors = {
|
|
|
396
398
|
let explicit_children = false;
|
|
397
399
|
|
|
398
400
|
for (const child of node.children) {
|
|
399
|
-
if (child.type === '
|
|
401
|
+
if (child.type === 'Component') {
|
|
400
402
|
if (child.id.name === '$children') {
|
|
401
403
|
explicit_children = true;
|
|
402
404
|
if (implicit_children) {
|
|
@@ -328,25 +328,35 @@ const visitors = {
|
|
|
328
328
|
Element(node, context) {
|
|
329
329
|
const { state, visit } = context;
|
|
330
330
|
|
|
331
|
-
const
|
|
332
|
-
|
|
331
|
+
const is_dom_element =
|
|
332
|
+
node.id.type === 'Identifier' &&
|
|
333
|
+
node.id.name[0].toLowerCase() === node.id.name[0] &&
|
|
334
|
+
node.id.name[0] !== '$';
|
|
335
|
+
const is_spreading = node.attributes.some((attr) => attr.type === 'SpreadAttribute');
|
|
336
|
+
const spread_attributes = is_spreading ? [] : null;
|
|
333
337
|
|
|
334
338
|
const handle_static_attr = (name, value) => {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}`
|
|
342
|
-
)
|
|
339
|
+
const attr_value = b.literal(
|
|
340
|
+
` ${name}${
|
|
341
|
+
is_boolean_attribute(name) && value === true
|
|
342
|
+
? ''
|
|
343
|
+
: `="${value === true ? '' : escape_html(value, true)}"`
|
|
344
|
+
}`
|
|
343
345
|
);
|
|
346
|
+
|
|
347
|
+
if (is_spreading) {
|
|
348
|
+
if (spread_attributes.length === 0) {
|
|
349
|
+
state.template.push(attr_value);
|
|
350
|
+
} else {
|
|
351
|
+
spread_attributes.push(b.prop('init', b.literal(name), attr_value));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
344
354
|
};
|
|
345
355
|
|
|
346
356
|
if (is_dom_element) {
|
|
347
357
|
let class_attribute = null;
|
|
348
358
|
|
|
349
|
-
state.template.push(`<${
|
|
359
|
+
state.template.push(`<${node.id.name}`);
|
|
350
360
|
|
|
351
361
|
for (const attr of node.attributes) {
|
|
352
362
|
if (attr.type === 'Attribute') {
|
|
@@ -493,6 +503,8 @@ const visitors = {
|
|
|
493
503
|
}
|
|
494
504
|
}
|
|
495
505
|
}
|
|
506
|
+
} else if (attr.type === 'SpreadAttribute') {
|
|
507
|
+
spread_attributes.push(b.spread(b.call('$.spread_object', visit(attr.argument, state))));
|
|
496
508
|
}
|
|
497
509
|
}
|
|
498
510
|
|
|
@@ -527,13 +539,17 @@ const visitors = {
|
|
|
527
539
|
|
|
528
540
|
state.template.push('>');
|
|
529
541
|
|
|
542
|
+
if (spread_attributes !== null && spread_attributes.length > 0) {
|
|
543
|
+
const id = state.flush_node();
|
|
544
|
+
state.init.push(
|
|
545
|
+
b.stmt(b.call('$.render_spread', id, b.thunk(b.object(spread_attributes))))
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
530
549
|
transform_children(node.children, { visit, state, root: false });
|
|
531
550
|
|
|
532
|
-
state.template.push(`</${
|
|
551
|
+
state.template.push(`</${node.id.name}>`);
|
|
533
552
|
} else {
|
|
534
|
-
if (node.id.type !== 'Identifier') {
|
|
535
|
-
throw new Error('TODO');
|
|
536
|
-
}
|
|
537
553
|
const id = state.flush_node();
|
|
538
554
|
|
|
539
555
|
state.template.push('<!>');
|
|
@@ -571,7 +587,7 @@ const visitors = {
|
|
|
571
587
|
if (node.children.length > 0) {
|
|
572
588
|
const component_scope = context.state.scopes.get(node);
|
|
573
589
|
const children = b.arrow(
|
|
574
|
-
[b.id('__anchor')],
|
|
590
|
+
[b.id('__anchor'), b.id('__props'), b.id('__block')],
|
|
575
591
|
b.block(
|
|
576
592
|
transform_body(node.children, {
|
|
577
593
|
...context,
|
|
@@ -598,7 +614,9 @@ const visitors = {
|
|
|
598
614
|
)
|
|
599
615
|
);
|
|
600
616
|
} else {
|
|
601
|
-
state.init.push(
|
|
617
|
+
state.init.push(
|
|
618
|
+
b.stmt(b.call(visit(node.id, state), id, b.object(props), b.id('$.active_block')))
|
|
619
|
+
);
|
|
602
620
|
}
|
|
603
621
|
}
|
|
604
622
|
},
|
|
@@ -1007,69 +1025,10 @@ const visitors = {
|
|
|
1007
1025
|
return b.call(b.await(b.call('$.resume_context', context.visit(node.argument))));
|
|
1008
1026
|
},
|
|
1009
1027
|
|
|
1010
|
-
JSXText(node) {
|
|
1011
|
-
const text = node.value;
|
|
1012
|
-
if (text.trim() === '') {
|
|
1013
|
-
return b.empty;
|
|
1014
|
-
}
|
|
1015
|
-
return b.literal(text);
|
|
1016
|
-
},
|
|
1017
|
-
|
|
1018
|
-
JSXExpressionContainer(node, context) {
|
|
1019
|
-
const expression = context.visit(node.expression);
|
|
1020
|
-
if (expression.type === b.empty) {
|
|
1021
|
-
return b.empty;
|
|
1022
|
-
}
|
|
1023
|
-
return expression;
|
|
1024
|
-
},
|
|
1025
|
-
|
|
1026
|
-
JSXIdentifier(node, context) {
|
|
1027
|
-
return context.visit(b.id(node.name));
|
|
1028
|
-
},
|
|
1029
|
-
|
|
1030
|
-
JSXMemberExpression(node, context) {
|
|
1031
|
-
return b.member(context.visit(node.object), context.visit(node.property));
|
|
1032
|
-
},
|
|
1033
|
-
|
|
1034
1028
|
BinaryExpression(node, context) {
|
|
1035
1029
|
return b.binary(node.operator, context.visit(node.left), context.visit(node.right));
|
|
1036
1030
|
},
|
|
1037
1031
|
|
|
1038
|
-
JSXElement(node, context) {
|
|
1039
|
-
if (
|
|
1040
|
-
!context.state.imports.has(`import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime'`)
|
|
1041
|
-
) {
|
|
1042
|
-
context.state.imports.add(`import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime'`);
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
const openingElement = node.openingElement;
|
|
1046
|
-
const name = openingElement.name;
|
|
1047
|
-
const props = b.object([]);
|
|
1048
|
-
const children = node.children
|
|
1049
|
-
.map((child) => context.visit(child, context.state))
|
|
1050
|
-
.filter((child) => child !== b.empty);
|
|
1051
|
-
|
|
1052
|
-
if (children.length > 0) {
|
|
1053
|
-
props.properties.push(b.prop('init', b.id('children'), b.array(children)));
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
for (const attr of openingElement.attributes) {
|
|
1057
|
-
if (attr.type === 'JSXAttribute') {
|
|
1058
|
-
props.properties.push(
|
|
1059
|
-
b.prop('init', b.id(attr.name.name), context.visit(attr.value, context.state))
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
return b.call(
|
|
1065
|
-
children.length > 0 ? '_jsxs' : '_jsx',
|
|
1066
|
-
name.type === 'JSXIdentifier' && name.name[0] === name.name[0].toLowerCase()
|
|
1067
|
-
? b.literal(name.name)
|
|
1068
|
-
: context.visit(name),
|
|
1069
|
-
props
|
|
1070
|
-
);
|
|
1071
|
-
},
|
|
1072
|
-
|
|
1073
1032
|
RenderFragment(node, context) {
|
|
1074
1033
|
const identifer = node.expression.callee;
|
|
1075
1034
|
|
|
@@ -1231,7 +1190,7 @@ function transform_ts_child(node, context) {
|
|
|
1231
1190
|
});
|
|
1232
1191
|
|
|
1233
1192
|
if (!node.selfClosing && !has_children_props && node.children.length > 0) {
|
|
1234
|
-
const is_dom_element = type[0].toLowerCase() === type[0];
|
|
1193
|
+
const is_dom_element = type[0].toLowerCase() === type[0] && type[0] !== '$';
|
|
1235
1194
|
|
|
1236
1195
|
const component_scope = context.state.scopes.get(node);
|
|
1237
1196
|
const thunk = b.thunk(
|
|
@@ -1246,7 +1205,7 @@ function transform_ts_child(node, context) {
|
|
|
1246
1205
|
if (is_dom_element) {
|
|
1247
1206
|
children.push(b.jsx_expression_container(b.call(thunk)));
|
|
1248
1207
|
} else {
|
|
1249
|
-
const children_name = context.state.scope.generate('
|
|
1208
|
+
const children_name = context.state.scope.generate('component');
|
|
1250
1209
|
const children_id = b.id(children_name);
|
|
1251
1210
|
const jsx_id = b.jsx_id('$children');
|
|
1252
1211
|
jsx_id.loc = node.id.loc;
|
|
@@ -1343,7 +1302,10 @@ function transform_children(children, { visit, state, root }) {
|
|
|
1343
1302
|
node.type === 'TryStatement' ||
|
|
1344
1303
|
node.type === 'ForOfStatement' ||
|
|
1345
1304
|
node.type === 'RenderFragment' ||
|
|
1346
|
-
(node.type === 'Element' &&
|
|
1305
|
+
(node.type === 'Element' &&
|
|
1306
|
+
(node.id.type !== 'Identifier' ||
|
|
1307
|
+
node.id.name[0].toLowerCase() !== node.id.name[0] ||
|
|
1308
|
+
node.id.name[0] === '$'))
|
|
1347
1309
|
) ||
|
|
1348
1310
|
normalized.filter(
|
|
1349
1311
|
(node) => node.type !== 'VariableDeclaration' && node.type !== 'EmptyStatement'
|
|
@@ -1354,7 +1316,10 @@ function transform_children(children, { visit, state, root }) {
|
|
|
1354
1316
|
|
|
1355
1317
|
const get_id = (node) => {
|
|
1356
1318
|
return b.id(
|
|
1357
|
-
node.type == 'Element' &&
|
|
1319
|
+
node.type == 'Element' &&
|
|
1320
|
+
node.id.type === 'Identifier' &&
|
|
1321
|
+
node.id.name[0].toLowerCase() === node.id.name[0] &&
|
|
1322
|
+
node.id.name[0] !== '$'
|
|
1358
1323
|
? state.scope.generate(node.id.name)
|
|
1359
1324
|
: node.type == 'Text'
|
|
1360
1325
|
? state.scope.generate('text')
|
package/src/compiler/scope.js
CHANGED
|
@@ -70,22 +70,10 @@ export function create_scopes(ast, root, parent) {
|
|
|
70
70
|
next({ scope });
|
|
71
71
|
},
|
|
72
72
|
|
|
73
|
-
Fragment(node, { state, next }) {
|
|
74
|
-
const scope = state.scope.child();
|
|
75
|
-
scopes.set(node, scope);
|
|
76
|
-
|
|
77
|
-
if (node.id) scope.declare(node.id, 'normal', 'fragment');
|
|
78
|
-
|
|
79
|
-
add_params(scope, node.params);
|
|
80
|
-
next({ scope });
|
|
81
|
-
},
|
|
82
|
-
|
|
83
73
|
Element(node, { state, next }) {
|
|
84
74
|
const scope = state.scope.child();
|
|
85
75
|
scopes.set(node, scope);
|
|
86
76
|
|
|
87
|
-
scope.declare(node, 'normal', 'element');
|
|
88
|
-
|
|
89
77
|
next({ scope });
|
|
90
78
|
},
|
|
91
79
|
|
|
@@ -305,7 +293,6 @@ export class Scope {
|
|
|
305
293
|
kind,
|
|
306
294
|
declaration_kind,
|
|
307
295
|
is_called: false,
|
|
308
|
-
prop_alias: null,
|
|
309
296
|
metadata: null
|
|
310
297
|
};
|
|
311
298
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TRY_BLOCK
|
|
12
12
|
} from './constants';
|
|
13
13
|
import { next_sibling } from './operations';
|
|
14
|
+
import { apply_element_spread } from './render';
|
|
14
15
|
import {
|
|
15
16
|
active_block,
|
|
16
17
|
active_component,
|
|
@@ -49,6 +50,10 @@ export function render(fn, flags = 0) {
|
|
|
49
50
|
return block(RENDER_BLOCK | flags, fn);
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export function render_spread(element, fn, flags = 0) {
|
|
54
|
+
return block(RENDER_BLOCK | flags, apply_element_spread(element, fn));
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
export function branch(fn, flags = 0) {
|
|
53
58
|
return block(BRANCH_BLOCK | flags, fn);
|
|
54
59
|
}
|
|
@@ -60,6 +60,18 @@ export function set_attribute(element, attribute, value) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export function set_attributes(element, attributes) {
|
|
64
|
+
for (const key in attributes) {
|
|
65
|
+
let value = attributes[key];
|
|
66
|
+
|
|
67
|
+
if (key === 'class') {
|
|
68
|
+
set_class(element, value);
|
|
69
|
+
} else {
|
|
70
|
+
set_attribute(element, key, value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
/**
|
|
64
76
|
* @template V
|
|
65
77
|
* @param {V} value
|
|
@@ -154,3 +166,9 @@ export function set_ref(dom, fn) {
|
|
|
154
166
|
};
|
|
155
167
|
});
|
|
156
168
|
}
|
|
169
|
+
|
|
170
|
+
export function apply_element_spread(element, fn) {
|
|
171
|
+
return () => {
|
|
172
|
+
set_attributes(element, fn());
|
|
173
|
+
};
|
|
174
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
export type Component<T> = (props: T) => void;
|
|
1
2
|
|
|
2
|
-
export
|
|
3
|
+
export declare function mount(
|
|
4
|
+
component: () => void,
|
|
5
|
+
options: { target: HTMLElement; props?: Record<string, any> }
|
|
6
|
+
): () => void;
|
|
7
|
+
|
|
8
|
+
export declare function untrack<T>(fn: () => T): T;
|
|
9
|
+
|
|
10
|
+
export declare function flushSync<T>(fn: () => T): T;
|
|
11
|
+
|
|
12
|
+
export declare function effect(fn: (() => void) | (() => () => void)): void;
|
package/README.md
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
# Ripple
|
|
2
|
-
|
|
3
|
-
> Currently, this project is still in early development, and should not be used in production.
|
|
4
|
-
|
|
5
|
-
Ripple is a TypeScript UI framework for the web.
|
|
6
|
-
|
|
7
|
-
I wrote Ripple as a love letter for frontend web – and this is largely a project that I built in less than a week, so it's very raw.
|
|
8
|
-
|
|
9
|
-
Personally, I ([@trueadm](https://github.com/trueadm)) have been involved in some truly amazing frontend frameworks along their journeys – from [Inferno](https://github.com/infernojs/inferno), where it all began, to [React](https://github.com/facebook/react) and the journey of React Hooks, to creating [Lexical](https://github.com/facebook/lexical), to [Svelte 5](https://github.com/sveltejs/svelte) and its new compiler and signal-based reactivity runtime. Along that journey, I collected ideas, and intriguing thoughts that may or may not pay off. Given my time between roles, I decided it was the best opportunity to try them out, and for open source to see what I was cooking.
|
|
10
|
-
|
|
11
|
-
Ripple was designed to be a JS/TS-first framework, rather than HTML-first. Ripple modules have their own `.ripple` extension and these modules
|
|
12
|
-
fully support TypeScript. By introducing a new extension, it affords Ripple to invent its own superset language, that plays really nicely with
|
|
13
|
-
TypeScript and JSX, but with a few interesting touches. In my experience, this has led to better DX not only for humans, but also for LLMs.
|
|
14
|
-
|
|
15
|
-
Right now, there will be plenty of bugs, things just won't work either and you'll find TODOs everywhere. At this stage, Ripple is more of an early alpha version of something that _might_ be, rather than something you should try and adopt. If anything, maybe some of the ideas can be shared and incubated back into other frameworks. There's also a lot of similarities with Svelte 5, and that's not by accident, that's because of my recent time working on Svelte 5.
|
|
16
|
-
|
|
17
|
-
## Features
|
|
18
|
-
|
|
19
|
-
- **Reactive State Management**: Built-in reactivity with `$` prefixed variables
|
|
20
|
-
- **Component-Based Architecture**: Clean, reusable components with props and children
|
|
21
|
-
- **JSX-like Syntax**: Familiar templating with Ripple-specific enhancements
|
|
22
|
-
- **TypeScript Support**: Full TypeScript integration with type checking
|
|
23
|
-
- **VSCode Integration**: Rich editor support with diagnostics, syntax highlighting, and IntelliSense
|
|
24
|
-
|
|
25
|
-
## Missing Features
|
|
26
|
-
|
|
27
|
-
- **SSR**: Ripple is currently an SPA only, this is because I haven't gotten around to it
|
|
28
|
-
- **Testing & Types**: The codebase is very raw with limited types (I've opted for JavaScript only to avoid build problems). There aren't any tests either – I've been using the `playground` directory to manually test things as I go
|
|
29
|
-
|
|
30
|
-
## Quick Start
|
|
31
|
-
|
|
32
|
-
### Installation
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
pnpm i --save ripple
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Key Concepts
|
|
39
|
-
|
|
40
|
-
### Components
|
|
41
|
-
|
|
42
|
-
Define reusable components with the `component` keyword. These are similar to functions in that they have `props`, but crucially,
|
|
43
|
-
they allow for a JSX-like syntax to be defined alongside standard TypeScript. That means you do not _return JSX_ like in other frameworks,
|
|
44
|
-
but you instead use it like a JavaScript statement, as shown:
|
|
45
|
-
|
|
46
|
-
```ripple
|
|
47
|
-
component Button(props: {text: string, onClick: () => void}) {
|
|
48
|
-
<button onClick={props.onClick}>
|
|
49
|
-
{props.text}
|
|
50
|
-
</button>
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Usage
|
|
54
|
-
<Button text="Click me" onClick={() => console.log("Clicked!")} />
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### Reactive Variables
|
|
58
|
-
|
|
59
|
-
Variables prefixed with `$` are automatically reactive:
|
|
60
|
-
|
|
61
|
-
```ripple
|
|
62
|
-
let $name = "World";
|
|
63
|
-
let $count = 0;
|
|
64
|
-
|
|
65
|
-
// Updates automatically trigger re-renders
|
|
66
|
-
$count++;
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Object properties prefixed with `$` are also automatically reactive:
|
|
70
|
-
|
|
71
|
-
```ripple
|
|
72
|
-
let counter = { $current: 0 };
|
|
73
|
-
|
|
74
|
-
// Updates automatically trigger re-renders
|
|
75
|
-
counter.$current++;
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Derived values are simply `$` variables that combined different parts of state:
|
|
79
|
-
|
|
80
|
-
```ripple
|
|
81
|
-
let $count = 0;
|
|
82
|
-
let $double = $count * 2;
|
|
83
|
-
let $quadruple = $double * 2;
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
That means `$count` itself might be derived if it were to reference another reactive property. For example:
|
|
87
|
-
|
|
88
|
-
```ripple
|
|
89
|
-
component Counter({ $startingCount }) {
|
|
90
|
-
let $count = $startingCount;
|
|
91
|
-
let $double = $count * 2;
|
|
92
|
-
let $quadruple = $double * 2;
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Now given `$startingCount` is reactive, it would mean that `$count` might reset each time an incoming change to `$startingCount` occurs. That might not be desirable, so Ripple provides a way to `untrack` reactivity in those cases:
|
|
97
|
-
|
|
98
|
-
```ripple
|
|
99
|
-
import { untrack } from 'ripple';
|
|
100
|
-
|
|
101
|
-
component Counter({ $startingCount }) {
|
|
102
|
-
let $count = untrack(() => $startingCount);
|
|
103
|
-
let $double = $count * 2;
|
|
104
|
-
let $quadruple = $double * 2;
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Now `$count` will only reactively create its value on initialization.
|
|
109
|
-
|
|
110
|
-
> Note: you cannot define reactive variables in module/global scope, they have to be created on access from an active component
|
|
111
|
-
|
|
112
|
-
### Effects
|
|
113
|
-
|
|
114
|
-
When dealing with reactive state, you might want to be able to create side-effects based upon changes that happen upon updates.
|
|
115
|
-
To do this, you can use `effect`:
|
|
116
|
-
|
|
117
|
-
```ripple
|
|
118
|
-
import { effect } from 'ripple';
|
|
119
|
-
|
|
120
|
-
component App() {
|
|
121
|
-
let $count = 0;
|
|
122
|
-
|
|
123
|
-
effect(() => {
|
|
124
|
-
console.log($count);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
<button onClick={() => $count++}>Increment</button>
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Template
|
|
132
|
-
|
|
133
|
-
The JSX-like syntax might take some time to get used to if you're coming from another framework. For one, templating in Ripple
|
|
134
|
-
can only occur _inside_ a `component` or `fragment` body – you can't create JSX inside functions, or assign it to variables as an expression.
|
|
135
|
-
|
|
136
|
-
Furthermore, Ripple's templating language allows for native JS control flow:
|
|
137
|
-
|
|
138
|
-
```ripple
|
|
139
|
-
component Truthy({ x }) {
|
|
140
|
-
<div>
|
|
141
|
-
if (x) {
|
|
142
|
-
<span>
|
|
143
|
-
{"x is truthy"}
|
|
144
|
-
</span>
|
|
145
|
-
} else {
|
|
146
|
-
<span>
|
|
147
|
-
// you can create variables inside the template!
|
|
148
|
-
const str = "x is falsy";
|
|
149
|
-
|
|
150
|
-
console.log(str); // and function calls too!
|
|
151
|
-
|
|
152
|
-
{str}
|
|
153
|
-
</span>
|
|
154
|
-
}
|
|
155
|
-
</div>
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
You can also use for...of blocks inside Ripple's templating language, and many other blocks too!
|
|
160
|
-
|
|
161
|
-
Note that strings inside the template need to be inside `{"string"}`, you can't do `<div>hello</div>` as Ripple
|
|
162
|
-
has no idea if `hello` is a string or maybe some JavaScript code that needs evaluating, so just ensure you wrap them
|
|
163
|
-
in curly braces. This shouldn't be an issue in the real-world anyway, as you'll likely use an i18n library that means
|
|
164
|
-
using JavaScript expressions regardless.
|
|
165
|
-
|
|
166
|
-
### Props
|
|
167
|
-
|
|
168
|
-
If you want a prop to be reactive, you should also give it a `$` prefix.
|
|
169
|
-
|
|
170
|
-
```ripple
|
|
171
|
-
component Button(props: {$text: string, onClick: () => void}) {
|
|
172
|
-
<button onClick={props.onClick}>
|
|
173
|
-
{props.$text}
|
|
174
|
-
</button>
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Usage
|
|
178
|
-
<Button $text={some_text} onClick={() => console.log("Clicked!")} />
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Fragments
|
|
182
|
-
|
|
183
|
-
As mentioned before, you can only create Ripple JSX-like templating in two places – `component` or `fragment`. You can think
|
|
184
|
-
of a `fragment` as a JavaScript function that has arguments and generates output, it does not return anything! They can be used
|
|
185
|
-
with the `fragment` keyword:
|
|
186
|
-
|
|
187
|
-
```ripple
|
|
188
|
-
fragment Logo() {
|
|
189
|
-
<svg>{...}</svg>
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
You can render the `Logo` fragment using the `{@fragment ...}` directive:
|
|
194
|
-
|
|
195
|
-
```ripple
|
|
196
|
-
component SomeComponent() {
|
|
197
|
-
<div>{@fragment Logo()}</div>
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Children
|
|
202
|
-
|
|
203
|
-
Use `$children` prop and the `{@fragment ...}` directive for component composition.
|
|
204
|
-
|
|
205
|
-
When you pass in children to a component, it gets implicitly passed as the `$children` prop, in the form of a fragment.
|
|
206
|
-
|
|
207
|
-
```ripple
|
|
208
|
-
import type {Fragment} from 'ripple';
|
|
209
|
-
|
|
210
|
-
component Card(props: {$children: Fragment}) {
|
|
211
|
-
<div class="card">
|
|
212
|
-
{@fragment props.$children()}
|
|
213
|
-
</div>
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Usage
|
|
217
|
-
<Card>
|
|
218
|
-
<p>{"Card content here"}</p>
|
|
219
|
-
</Card>
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
You could also explicitly write the same code as shown:
|
|
223
|
-
|
|
224
|
-
```ripple
|
|
225
|
-
import type {Fragment} from 'ripple';
|
|
226
|
-
|
|
227
|
-
component Card(props: {$children: Fragment}) {
|
|
228
|
-
<div class="card">
|
|
229
|
-
{@fragment props.$children()}
|
|
230
|
-
</div>
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Usage with explicit fragment
|
|
234
|
-
<Card>
|
|
235
|
-
fragment $children() {
|
|
236
|
-
<p>{"Card content here"}</p>
|
|
237
|
-
}
|
|
238
|
-
</Card>
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Styling
|
|
242
|
-
|
|
243
|
-
Ripple supports native CSS styling that is localized to the given component using the `<style>` element.
|
|
244
|
-
|
|
245
|
-
```ripple
|
|
246
|
-
component MyComponent() {
|
|
247
|
-
<div class="container">
|
|
248
|
-
<h1>{"Hello World"}</h1>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
<style>
|
|
252
|
-
.container {
|
|
253
|
-
background: blue;
|
|
254
|
-
padding: 1rem;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
h1 {
|
|
258
|
-
color: white;
|
|
259
|
-
font-size: 2rem;
|
|
260
|
-
}
|
|
261
|
-
</style>
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
> Note: the `<style>` element must be top-level within a `component` and cannot be used inside a `fragment`.
|
|
266
|
-
|
|
267
|
-
## VSCode Extension
|
|
268
|
-
|
|
269
|
-
The Ripple VSCode extension provides:
|
|
270
|
-
|
|
271
|
-
- **Syntax Highlighting** for `.ripple` files
|
|
272
|
-
- **Real-time Diagnostics** for compilation errors
|
|
273
|
-
- **TypeScript Integration** for type checking
|
|
274
|
-
- **IntelliSense** for autocompletion
|
|
275
|
-
|
|
276
|
-
Clone the repository, and manually install the extension from the `packages/ripple-vscode-plugin/` directory.
|
|
277
|
-
|
|
278
|
-
## Playground
|
|
279
|
-
|
|
280
|
-
Feel free to play around with how Ripple works. If you clone the repo, you can then:
|
|
281
|
-
|
|
282
|
-
```bash
|
|
283
|
-
pnpm i && cd playground && pnpm dev
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
The playground uses Ripple's Vite plugin, where you can play around with things inside the `playground/src` directory.
|
package/src/ai.js
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
// import { anthropic } from '@ai-sdk/anthropic';
|
|
2
|
-
// import { generateText } from 'ai';
|
|
3
|
-
|
|
4
|
-
// const default_prompt = `
|
|
5
|
-
// Ripple is a web-based JavaScript framework for building user interfaces. It's syntax and design is inspired by React and Svelte 5.
|
|
6
|
-
// It uses JSX for templating inside '.ripple' modules. These modules allow for custom syntax that is not JavaScript compliant.
|
|
7
|
-
|
|
8
|
-
// One of the core differences is that it allows for a new type of JavaScript declaration which is a 'component', which is like a 'function' but is only allowed in '.ripple' modules:
|
|
9
|
-
|
|
10
|
-
// \`\`\`js
|
|
11
|
-
// component HelloComponent(props) {
|
|
12
|
-
// const title = 'Hello ';
|
|
13
|
-
|
|
14
|
-
// <div>{title + props.name}</div>;
|
|
15
|
-
// }
|
|
16
|
-
// \`\`\`
|
|
17
|
-
|
|
18
|
-
// Another difference is that 'component' declaration bodies allow for JSX templating. Except this JSX templating isn't "expression" based, but rather "statement" based. That
|
|
19
|
-
// means that 'return' is not valid syntax in a component declaration body. Nor is creating a variable that references JSX. Instead, JSX is directly written in the body of the component declaration.
|
|
20
|
-
// This means that the ordering is important, as JSX that is written first will be rendered first. This is different from React.
|
|
21
|
-
|
|
22
|
-
// Another difference from JSX in other frameworks is that JSXText is not allowed here. That's because JSX is now statement based, and not expression based. This means that all JSX must be wrapped in a JSXExpressionContainer.
|
|
23
|
-
|
|
24
|
-
// For example, this is invalid Ripple code:
|
|
25
|
-
|
|
26
|
-
// \`\`\`js
|
|
27
|
-
// <button>=</button>
|
|
28
|
-
// \`\`\`
|
|
29
|
-
|
|
30
|
-
// The correct version is:
|
|
31
|
-
|
|
32
|
-
// \`\`\`js
|
|
33
|
-
// <button>{"="}</button>
|
|
34
|
-
// \`\`\`
|
|
35
|
-
|
|
36
|
-
// Another core difference is that Ripple defines reactive variables by their usage of a "$" prefix. If the variable declaration does not have a dollar prefix, it is not reactive.
|
|
37
|
-
|
|
38
|
-
// \`\`\`js
|
|
39
|
-
// component HelloComponent(props) {
|
|
40
|
-
// let $count = 0;
|
|
41
|
-
|
|
42
|
-
// <div>{$count}</div>;
|
|
43
|
-
// <button onClick={() => $count++}>{"Increment"}</button>;
|
|
44
|
-
// }
|
|
45
|
-
// \`\`\`
|
|
46
|
-
|
|
47
|
-
// Object properties can also be reactive if the property name starts with a "$" prefix.
|
|
48
|
-
|
|
49
|
-
// \`\`\`js
|
|
50
|
-
// component HelloComponent(props) {
|
|
51
|
-
// let state = { $count: 0 };
|
|
52
|
-
|
|
53
|
-
// <div>{state.$count}</div>;
|
|
54
|
-
// <button onClick={() => state.$count++}>{"Increment"}</button>;
|
|
55
|
-
// }
|
|
56
|
-
// \`\`\`
|
|
57
|
-
|
|
58
|
-
// Ripple doesn't allow for inline expressions with JSX for conditionals or for collections such as arrays or objects.
|
|
59
|
-
// Instead, prefer using normal JavaScript logic where you have a "if" or "for" statement that wraps the JSX.
|
|
60
|
-
|
|
61
|
-
// Here is valid Ripple code:
|
|
62
|
-
|
|
63
|
-
// \`\`\`js
|
|
64
|
-
// export component Counter() {
|
|
65
|
-
// let $count = 0;
|
|
66
|
-
|
|
67
|
-
// if ($count > 5) {
|
|
68
|
-
// <div>{$count}</div>;
|
|
69
|
-
// }
|
|
70
|
-
|
|
71
|
-
// <div>
|
|
72
|
-
// if ($count > 5) {
|
|
73
|
-
// <div>{$count}</div>;
|
|
74
|
-
// }
|
|
75
|
-
// </div>;
|
|
76
|
-
|
|
77
|
-
// for (const item of items) {
|
|
78
|
-
// <div>{item}</div>;
|
|
79
|
-
// }
|
|
80
|
-
|
|
81
|
-
// <ul>
|
|
82
|
-
// for (const item of items) {
|
|
83
|
-
// <li>{item}</li>;
|
|
84
|
-
// }
|
|
85
|
-
// </ul>;
|
|
86
|
-
// }
|
|
87
|
-
// \`\`\`
|
|
88
|
-
|
|
89
|
-
// Ripple allows for shorthand props on components, so '<Child state={state} />' can be written as '<Child {state} />'.
|
|
90
|
-
|
|
91
|
-
// Ripple also allows for a singular "<style>" JSX element at the top level of the component declaration body. This is used for styling any JSX elements within the component.
|
|
92
|
-
// The style element can contain any valid CSS, and can also contain CSS variables. CSS variables are defined with a "--" prefix. This is the preferred way of doing styling over inline styles.
|
|
93
|
-
|
|
94
|
-
// If inline styles are to be used, then they should be done using the HTML style attribute approach rather than the JSX style attribute property approach.
|
|
95
|
-
|
|
96
|
-
// In Ripple variables that are created with an identifier that starts with a "$" prefix are considered reactive. If declaration init expression also references reactive variables, or function expressions, then
|
|
97
|
-
// this type of variable is considered "computed". Computed reactive declarations will re-run when any of the reactive variables they reference change. If this is not desired then the "untrack" function call should
|
|
98
|
-
// be used to prevent reactivity.
|
|
99
|
-
|
|
100
|
-
// \`\`\`js
|
|
101
|
-
// import { untrack } from 'ripple';
|
|
102
|
-
|
|
103
|
-
// component Counter({ $initial }) {
|
|
104
|
-
// let $count = untrack(() => $initial);
|
|
105
|
-
// }
|
|
106
|
-
// \`\`\`
|
|
107
|
-
|
|
108
|
-
// An important part of Ripple's reactivity model is that passing reactivity between boundaries can only happen via two ways:
|
|
109
|
-
// - the usage of closures, where a value is referenced in a function or property getter
|
|
110
|
-
// - the usage of objects and/or arrays, where the object or array is passed as a property with a "$" prefix so its reactivity is kept
|
|
111
|
-
|
|
112
|
-
// For example if you were to create a typical Ripple hook function, then you should pass any reactive values through using objects. Otherwise, the
|
|
113
|
-
// hook will act as a computed function and re-run every time the reactive value changes – which is likely not the desired behaviour of a "hook" function.
|
|
114
|
-
|
|
115
|
-
// \`\`\`js
|
|
116
|
-
// function useCounter(initial) {
|
|
117
|
-
// let $count = initial;
|
|
118
|
-
// const $double = $count * 2;
|
|
119
|
-
|
|
120
|
-
// const increment = () => $count++;
|
|
121
|
-
|
|
122
|
-
// return { $double, increment };
|
|
123
|
-
// }
|
|
124
|
-
|
|
125
|
-
// component Counter({ $count }) {
|
|
126
|
-
// const { $double, increment } = useCounter($count);
|
|
127
|
-
|
|
128
|
-
// <button onClick={increment}>{"Increment"}</button>;
|
|
129
|
-
// <div>{$double}</div>;
|
|
130
|
-
// }
|
|
131
|
-
// \`\`\`
|
|
132
|
-
|
|
133
|
-
// If a value needs to be mutated from within a hook, then it should be referenced by the hook in its object form instead:
|
|
134
|
-
|
|
135
|
-
// \`\`\`js
|
|
136
|
-
// function useCounter(state) {
|
|
137
|
-
// const $double = state.$count * 2;
|
|
138
|
-
|
|
139
|
-
// const increment = () => state.$count++;
|
|
140
|
-
|
|
141
|
-
// return { $double, increment };
|
|
142
|
-
// }
|
|
143
|
-
|
|
144
|
-
// component Counter({ $count }) {
|
|
145
|
-
// let $count = 0;
|
|
146
|
-
|
|
147
|
-
// const { $double, increment } = useCounter({ $count });
|
|
148
|
-
|
|
149
|
-
// <button onClick={increment}>{"Increment"}</button>;
|
|
150
|
-
// <div>{$double}</div>;
|
|
151
|
-
// }
|
|
152
|
-
// \`\`\`
|
|
153
|
-
|
|
154
|
-
// It should be noted that in this example, the "$count" inside the "Counter" component will not be mutated by the "increment" function.
|
|
155
|
-
|
|
156
|
-
// If this is desired, then the call to "useCounter" needs to provide a getter and setter for the "$count" value:
|
|
157
|
-
|
|
158
|
-
// \`\`\`js
|
|
159
|
-
// function useCounter(state) {
|
|
160
|
-
// const $double = state.$count * 2;
|
|
161
|
-
|
|
162
|
-
// const increment = () => state.$count++;
|
|
163
|
-
|
|
164
|
-
// return { $double, increment };
|
|
165
|
-
// }
|
|
166
|
-
|
|
167
|
-
// component Counter({ $count }) {
|
|
168
|
-
// let $count = 0;
|
|
169
|
-
|
|
170
|
-
// const { $double, increment } = useCounter({ get $count() { return $count }, set $count(value) { $count = value } });
|
|
171
|
-
|
|
172
|
-
// <button onClick={increment}>{"Increment"}</button>;
|
|
173
|
-
// <div>{$double}</div>;
|
|
174
|
-
// }
|
|
175
|
-
// \`\`\`
|
|
176
|
-
|
|
177
|
-
// Normally, you shouldn't provide getters/setters in the object returned from a hook, especially if the usage site intends to destruct the object.
|
|
178
|
-
|
|
179
|
-
// Ripple also provides a way of handling Suspense and asynchronous data fetching. This requires two parts:
|
|
180
|
-
// - a "try" block, that has an "async" block that shows the fallback pending UI. These blocks can only be used inside Ripple components
|
|
181
|
-
// - an "await" that must happen at the top-level of the component body
|
|
182
|
-
|
|
183
|
-
// Here is an example:
|
|
184
|
-
|
|
185
|
-
// \`\`\`js
|
|
186
|
-
// export component App() {
|
|
187
|
-
// try {
|
|
188
|
-
// <Child />;
|
|
189
|
-
// } async {
|
|
190
|
-
// <div>{"Loading..."}</div>;
|
|
191
|
-
// }
|
|
192
|
-
// }
|
|
193
|
-
|
|
194
|
-
// component Child() {
|
|
195
|
-
// const $pokemons = await fetch('https://pokeapi.co/api/v2/pokemon/').then((res) => res.json());
|
|
196
|
-
|
|
197
|
-
// for (const pokemon of $pokemons.results) {
|
|
198
|
-
// <div>{pokemon.name}</div>;
|
|
199
|
-
// }
|
|
200
|
-
// }
|
|
201
|
-
// \`\`\`
|
|
202
|
-
|
|
203
|
-
// It's important that the transformed code never uses an async fetch() call inside an effect function. This is an anti-pattern, instead the "await" expression should be used
|
|
204
|
-
// directly inside the fragment or component body. Also when using "await" then loading states should be handled using the "try" and "async" blocks, so this isn't required in the
|
|
205
|
-
// output code.
|
|
206
|
-
|
|
207
|
-
// Ripple also supports "fragment" syntax, which is similar to the "component" syntax but allows for multiple arguments:
|
|
208
|
-
|
|
209
|
-
// \`\`\`js
|
|
210
|
-
// fragment foo() {
|
|
211
|
-
// <div>{"Hello World"}</div>;
|
|
212
|
-
// }
|
|
213
|
-
|
|
214
|
-
// component App() {
|
|
215
|
-
// {fragment foo()};
|
|
216
|
-
// }
|
|
217
|
-
// \`\`\`
|
|
218
|
-
|
|
219
|
-
// Fragments can be seen as reactive functions that can take arguments and using the "{@fragment fragment(...args)}" syntax, they can be rendered as if they were JSX elements.
|
|
220
|
-
|
|
221
|
-
// Ripple denotes attributes and properties on JSX elements as being reactive when they also have a "$" prefix. This means that if a property is reactive, then the element will re-render when the property changes.
|
|
222
|
-
|
|
223
|
-
// Ripple does not support both a non-reactive and reactive version of a prop – so having "$ref" and "ref" is not allowed. If a prop could be possibly reactive, then it should always have a "$" prefix to ensure maximum compatibility.
|
|
224
|
-
|
|
225
|
-
// There are also some special attributes that such as "$ref" and "$children" that always start with a "$" prefix.
|
|
226
|
-
|
|
227
|
-
// When creating an implicit children fragment from a JSX component, such as:
|
|
228
|
-
|
|
229
|
-
// \`\`\`js
|
|
230
|
-
// <ChildComponent>
|
|
231
|
-
// {"Hello World"}
|
|
232
|
-
// </ChildComponent>
|
|
233
|
-
// \`\`\`
|
|
234
|
-
|
|
235
|
-
// This can also be written as:
|
|
236
|
-
|
|
237
|
-
// \`\`\`js
|
|
238
|
-
// fragment $children() {
|
|
239
|
-
// {"Hello World"};
|
|
240
|
-
// }
|
|
241
|
-
|
|
242
|
-
// <ChildComponent {$children} />;
|
|
243
|
-
// \`\`\`
|
|
244
|
-
|
|
245
|
-
// Which is the same as the previous example.
|
|
246
|
-
|
|
247
|
-
// The "Hello world" will be passed as a "$children" prop to the "ChildComponent" and it will be of the type of "Fragment". Which means that it's not a string, or JSX element, but rather a special kind of thing.
|
|
248
|
-
|
|
249
|
-
// To render a type of "Fragment" the {@fragment thing()} syntax should be used. This will render the "thing" as if it was a JSX element. Here's an example:
|
|
250
|
-
|
|
251
|
-
// \`\`\`js
|
|
252
|
-
// component Child({ $children }) {
|
|
253
|
-
// <div>
|
|
254
|
-
// {@fragment $children()};
|
|
255
|
-
// </div>;
|
|
256
|
-
// }
|
|
257
|
-
// \`\`\`
|
|
258
|
-
|
|
259
|
-
// Ripple uses for...of blocks for templating over collections or lists. While loops, standard for loops and while loops are not permitted in Ripple components or fragments.
|
|
260
|
-
|
|
261
|
-
// For example, to render a list of items:
|
|
262
|
-
|
|
263
|
-
// \`\`\`js
|
|
264
|
-
// <ul>
|
|
265
|
-
// for (const num of [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
|
|
266
|
-
// <li>{num}</li>;
|
|
267
|
-
// }
|
|
268
|
-
// </ul>;
|
|
269
|
-
// \`\`\`
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// `;
|
|
273
|
-
|
|
274
|
-
// export async function validate_with_ai(source) {
|
|
275
|
-
// const { text } = await generateText({
|
|
276
|
-
// model: anthropic('claude-3-7-sonnet-20250219'),
|
|
277
|
-
// messages: [
|
|
278
|
-
// {
|
|
279
|
-
// role: 'user',
|
|
280
|
-
// content: default_prompt,
|
|
281
|
-
// providerOptions: {
|
|
282
|
-
// anthropic: { cacheControl: { type: 'ephemeral' } }
|
|
283
|
-
// }
|
|
284
|
-
// },
|
|
285
|
-
// {
|
|
286
|
-
// role: 'user',
|
|
287
|
-
// content: `Please validate the following Ripple code and provide feedback on any issues:\n\n${source}`
|
|
288
|
-
// }
|
|
289
|
-
// ]
|
|
290
|
-
// });
|
|
291
|
-
// return text;
|
|
292
|
-
// }
|