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 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.1",
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' || this.value === 'fragment') {
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.lookahead().type === tt.ellipsis) {
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' || this.#path.at(-1).type === 'Fragment')
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
- element.id = open.name;
317
- element.id.type = 'Identifier';
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 = node.decorator === 'fragment' ? 'RenderFragment' : 'Text';
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 type = node.id.name;
352
- const is_dom_element = type[0].toLowerCase() === type[0];
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 === 'Fragment') {
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 type = node.id.name;
332
- const is_dom_element = type[0].toLowerCase() === type[0];
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
- state.template.push(
336
- b.literal(
337
- ` ${name}${
338
- is_boolean_attribute(name) && value === true
339
- ? ''
340
- : `="${value === true ? '' : escape_html(value, true)}"`
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(`<${type}`);
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(`</${type}>`);
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(b.stmt(b.call(node.id, id, b.object(props), b.id('$.active_block'))));
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('fragment');
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' && node.id.name[0].toLowerCase() !== node.id.name[0])
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' && node.id.name[0].toLowerCase() === node.id.name[0]
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')
@@ -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
  }
@@ -10,7 +10,7 @@ export {
10
10
  set_ref
11
11
  } from './render.js';
12
12
 
13
- export { render, async } from './blocks.js';
13
+ export { render, render_spread, async } from './blocks.js';
14
14
 
15
15
  export { event, delegate } from './events.js';
16
16
 
@@ -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 type Fragment<T extends any[] = []> = (...args: T) => void;
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
- // }