ripple 0.2.0 → 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/src/ai.js +0 -292
- package/test-mappings.js +0 -0
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/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
|
-
// }
|
package/test-mappings.js
DELETED
|
File without changes
|