ripple 0.2.3 → 0.2.4

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/README.md CHANGED
@@ -1,4 +1,9 @@
1
- # Ripple
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="assets/ripple-dark.png">
3
+ <img src="assets/ripple-light.png" alt="Ripple - the elegant UI framework for the web" />
4
+ </picture>
5
+
6
+ # What is Ripple?
2
7
 
3
8
  > Currently, this project is still in early development, and should not be used in production.
4
9
 
@@ -14,6 +19,8 @@ TypeScript and JSX, but with a few interesting touches. In my experience, this h
14
19
 
15
20
  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
21
 
22
+ If you'd like to know more, join the [Ripple Discord](https://discord.gg/JBF2ySrh2W).
23
+
17
24
  ## Features
18
25
 
19
26
  - **Reactive State Management**: Built-in reactivity with `$` prefixed variables
@@ -29,20 +36,19 @@ Right now, there will be plenty of bugs, things just won't work either and you'l
29
36
 
30
37
  ## Quick Start
31
38
 
32
- ### Installation
39
+ ### Try Ripple
33
40
 
34
- ```bash
35
- pnpm i --save ripple
36
- ```
41
+ > We're working hard on getting an online playgroud available. Watch this space!
37
42
 
38
- You'll also need Vite and Ripple's Vite plugin to compile Ripple:
43
+ You can try Ripple now by using our basic Vite template by running these commands in your terminal:
39
44
 
40
45
  ```bash
41
- pnpm i --save-dev vite-plugin-ripple
46
+ npx degit trueadm/ripple/templates/basic my-app
47
+ cd my-app
48
+ npm i # or yarn or pnpm
49
+ npm run dev # or yarn or pnpm
42
50
  ```
43
51
 
44
- You can see a working example in the [playground demo app](https://github.com/trueadm/ripple/tree/main/playground).
45
-
46
52
  ### Mounting your app
47
53
 
48
54
  You can use the `mount` API from the `ripple` package to render your Ripple component, using the `target`
@@ -77,14 +83,31 @@ component Button(props: { text: string, onClick: () => void }) {
77
83
  }
78
84
 
79
85
  // Usage
80
- <Button text="Click me" onClick={() => console.log("Clicked!")} />
86
+ component App() {
87
+ <Button text="Click me" onClick={() => console.log("Clicked!")} />
88
+ }
89
+ ```
90
+
91
+ ![iamge for the sourcecode above](assets/readme-1.png)
92
+
93
+ Ripple's templating language also supports shorthands and object spreads too:
94
+
95
+ ```svelte
96
+ // you can do a normal prop
97
+ <div onClick={onClick}>{text}</div>
98
+
99
+ // or using the shorthand prop
100
+ <div {onClick}>{text}</div>
101
+
102
+ // and you can spread props
103
+ <div {...properties}>{text}</div>
81
104
  ```
82
105
 
83
106
  ### Reactive Variables
84
107
 
85
108
  Variables prefixed with `$` are automatically reactive:
86
109
 
87
- ```ripple
110
+ ```ts
88
111
  let $name = "World";
89
112
  let $count = 0;
90
113
 
@@ -94,7 +117,7 @@ $count++;
94
117
 
95
118
  Object properties prefixed with `$` are also automatically reactive:
96
119
 
97
- ```ripple
120
+ ```ts
98
121
  let counter = { $current: 0 };
99
122
 
100
123
  // Updates automatically trigger re-renders
@@ -103,7 +126,7 @@ counter.$current++;
103
126
 
104
127
  Derived values are simply `$` variables that combined different parts of state:
105
128
 
106
- ```ripple
129
+ ```ts
107
130
  let $count = 0;
108
131
  let $double = $count * 2;
109
132
  let $quadruple = $double * 2;
@@ -172,6 +195,8 @@ can only occur _inside_ a `component` body – you can't create JSX inside funct
172
195
  </div>
173
196
  ```
174
197
 
198
+ ![iamge for the sourcecode above](assets/readme-5.png)
199
+
175
200
  Note that strings inside the template need to be inside `{"string"}`, you can't do `<div>hello</div>` as Ripple
176
201
  has no idea if `hello` is a string or maybe some JavaScript code that needs evaluating, so just ensure you wrap them
177
202
  in curly braces. This shouldn't be an issue in the real-world anyway, as you'll likely use an i18n library that means
@@ -191,13 +216,15 @@ component Truthy({ x }) {
191
216
  </span>
192
217
  } else {
193
218
  <span>
194
- {"x is truthy"}
219
+ {"x is falsy"}
195
220
  </span>
196
221
  }
197
222
  </div>
198
223
  }
199
224
  ```
200
225
 
226
+ ![iamge for the sourcecode above](assets/readme-2.png)
227
+
201
228
  ### For statements
202
229
 
203
230
  You can render collections using a `for...of` block, and you don't need to specify a `key` prop like
@@ -214,6 +241,8 @@ component ListView({ title, items }) {
214
241
  }
215
242
  ```
216
243
 
244
+ ![iamge for the sourcecode above](assets/readme-3.png)
245
+
217
246
  ### Try statements
218
247
 
219
248
  Try blocks work to building the foundation for **error boundaries**, when the runtime encounters
@@ -235,6 +264,8 @@ component ErrorBoundary() {
235
264
  }
236
265
  ```
237
266
 
267
+ ![iamge for the sourcecode above](assets/readme-4.png)
268
+
238
269
  ### Props
239
270
 
240
271
  If you want a prop to be reactive, you should also give it a `$` prefix.
@@ -252,7 +283,7 @@ component Button(props: { $text: string, onClick: () => void }) {
252
283
 
253
284
  ### Children
254
285
 
255
- Use `$children` prop and the `<$component />` directive for component composition.
286
+ Use `$children` prop and then use it in the form of `<$children />` for component composition.
256
287
 
257
288
  When you pass in children to a component, it gets implicitly passed as the `$children` prop, in the form of a component.
258
289
 
@@ -261,7 +292,7 @@ import type { Component } from 'ripple';
261
292
 
262
293
  component Card(props: { $children: Component }) {
263
294
  <div class="card">
264
- <$component />
295
+ <$children />
265
296
  </div>
266
297
  }
267
298
 
@@ -278,7 +309,7 @@ import type { Component } from 'ripple';
278
309
 
279
310
  component Card(props: { $children: Component }) {
280
311
  <div class="card">
281
- <$component />
312
+ <$children />
282
313
  </div>
283
314
  }
284
315
 
@@ -336,14 +367,14 @@ component MyComponent() {
336
367
 
337
368
  ## VSCode Extension
338
369
 
339
- The Ripple VSCode extension provides:
370
+ The [Ripple VSCode extension](https://marketplace.visualstudio.com/items?itemName=ripplejs.ripple-vscode-plugin) provides:
340
371
 
341
372
  - **Syntax Highlighting** for `.ripple` files
342
373
  - **Real-time Diagnostics** for compilation errors
343
374
  - **TypeScript Integration** for type checking
344
375
  - **IntelliSense** for autocompletion
345
376
 
346
- Clone the repository, and manually install the extension from the `packages/ripple-vscode-plugin/` directory.
377
+ You can find the extension on the VS Code Marketplace as [`Ripple for VS Code`](https://marketplace.visualstudio.com/items?itemName=ripplejs.ripple-vscode-plugin).
347
378
 
348
379
  ## Playground
349
380
 
@@ -353,4 +384,4 @@ Feel free to play around with how Ripple works. If you clone the repo, you can t
353
384
  pnpm i && cd playground && pnpm dev
354
385
  ```
355
386
 
356
- The playground uses Ripple's Vite plugin, where you can play around with things inside the `playground/src` directory.
387
+ The playground uses Ripple's Vite plugin, where you can play around with things inside the `playground/src` directory.
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.3",
6
+ "version": "0.2.4",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -9,8 +9,8 @@ function convert_from_jsx(node) {
9
9
  node.type = 'Identifier';
10
10
  } else if (node.type === 'JSXMemberExpression') {
11
11
  node.type = 'MemberExpression';
12
- node.object = convert_from_jsx(node.object)
13
- node.property = convert_from_jsx(node.property)
12
+ node.object = convert_from_jsx(node.object);
13
+ node.property = convert_from_jsx(node.property);
14
14
  }
15
15
  return node;
16
16
  }
@@ -24,6 +24,38 @@ function RipplePlugin(config) {
24
24
  class RippleParser extends Parser {
25
25
  #path = [];
26
26
 
27
+ parseExportDefaultDeclaration() {
28
+ // Check if this is "export default component"
29
+ if (this.value === 'component') {
30
+ const node = this.startNode();
31
+ node.type = 'Component';
32
+ node.css = null;
33
+ node.default = true;
34
+ this.next();
35
+ this.enterScope(0);
36
+
37
+ node.id = this.type.label === 'name' ? this.parseIdent() : null;
38
+
39
+ this.parseFunctionParams(node);
40
+ this.eat(tt.braceL);
41
+ node.body = [];
42
+ this.#path.push(node);
43
+
44
+ this.parseTemplateBody(node.body);
45
+
46
+ this.#path.pop();
47
+ this.exitScope();
48
+
49
+ this.next();
50
+ this.finishNode(node, 'Component');
51
+ this.awaitPos = 0;
52
+
53
+ return node;
54
+ }
55
+
56
+ return super.parseExportDefaultDeclaration();
57
+ }
58
+
27
59
  shouldParseExportStatement() {
28
60
  if (super.shouldParseExportStatement()) {
29
61
  return true;
@@ -253,10 +285,7 @@ function RipplePlugin(config) {
253
285
  case 62: // '>'
254
286
  case 125: {
255
287
  // '}'
256
- if (
257
- ch === 125 &&
258
- (this.#path.at(-1).type === 'Component')
259
- ) {
288
+ if (ch === 125 && this.#path.at(-1).type === 'Component') {
260
289
  return original.readToken.call(this, ch);
261
290
  }
262
291
  this.raise(
@@ -318,7 +347,7 @@ function RipplePlugin(config) {
318
347
  if (open.name.type === 'JSXIdentifier') {
319
348
  open.name.type = 'Identifier';
320
349
  }
321
-
350
+
322
351
  element.id = convert_from_jsx(open.name);
323
352
  element.attributes = open.attributes;
324
353
  element.selfClosing = open.selfClosing;
@@ -364,6 +393,37 @@ function RipplePlugin(config) {
364
393
  return element;
365
394
  }
366
395
 
396
+ parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit) {
397
+ if (this.value === '<' && this.#path.findLast((n) => n.type === 'Component')) {
398
+ // Check if this looks like JSX by looking ahead
399
+ const ahead = this.lookahead();
400
+ if (ahead.type.label === 'name' || ahead.value === '/' || ahead.value === '>') {
401
+ // This is JSX, rewind to the end of the object expression
402
+ // and let ASI handle the semicolon insertion naturally
403
+ this.pos = base.end;
404
+ this.type = tt.braceR;
405
+ this.value = '}';
406
+ this.start = base.end - 1;
407
+ this.end = base.end;
408
+ const position = this.curPosition();
409
+ this.startLoc = position;
410
+ this.endLoc = position;
411
+ this.next();
412
+
413
+ return base;
414
+ }
415
+ }
416
+ return super.parseSubscript(
417
+ base,
418
+ startPos,
419
+ startLoc,
420
+ noCalls,
421
+ maybeAsyncArrow,
422
+ optionalChained,
423
+ forInit
424
+ );
425
+ }
426
+
367
427
  parseTemplateBody(body) {
368
428
  var inside_func =
369
429
  this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
@@ -456,10 +516,7 @@ function RipplePlugin(config) {
456
516
  parseBlock(createNewLexicalScope, node, exitStrict) {
457
517
  const parent = this.#path.at(-1);
458
518
 
459
- if (
460
- parent?.type === 'Component' ||
461
- parent?.type === 'Element'
462
- ) {
519
+ if (parent?.type === 'Component' || parent?.type === 'Element') {
463
520
  if (createNewLexicalScope === void 0) createNewLexicalScope = true;
464
521
  if (node === void 0) node = this.startNode();
465
522
 
@@ -439,20 +439,6 @@ const visitors = {
439
439
  attribute
440
440
  );
441
441
  }
442
- } else if (name === 'ref') {
443
- if (is_dom_element) {
444
- error(
445
- 'Cannot have a `ref` prop on an element, did you mean `$ref`?',
446
- state.analysis.module.filename,
447
- attribute
448
- );
449
- } else {
450
- error(
451
- 'Cannot have a `ref` prop on a component, did you mean `$ref`?',
452
- state.analysis.module.filename,
453
- attribute
454
- );
455
- }
456
442
  }
457
443
 
458
444
  if (is_tracked_name(name)) {
@@ -44,7 +44,7 @@ function visit_function(node, context) {
44
44
 
45
45
  let body = context.visit(node.body, state);
46
46
 
47
- if (metadata.tracked === true) {
47
+ if (metadata?.tracked === true) {
48
48
  return /** @type {FunctionExpression} */ ({
49
49
  ...node,
50
50
  params: node.params.map((param) => context.visit(param, state)),
@@ -66,7 +66,7 @@ function build_getter(node, context) {
66
66
 
67
67
  // don't transform the declaration itself
68
68
  if (node !== binding?.node && binding?.transform) {
69
- return binding.transform.read(node);
69
+ return binding.transform.read(node, context.state?.metadata?.spread);
70
70
  }
71
71
  }
72
72
 
@@ -409,18 +409,12 @@ const visitors = {
409
409
  continue;
410
410
  }
411
411
 
412
- if (name === '$ref') {
413
- const id = state.flush_node();
414
- const value = attr.value;
415
-
416
- state.init.push(b.stmt(b.call('$.set_ref', id, visit(value, state))));
417
- continue;
418
- }
419
-
420
412
  if (is_event_attribute(name)) {
421
- const event_name = name.slice(2).toLowerCase();
413
+ let capture = name.endsWith('Capture');
414
+ let event_name = capture
415
+ ? name.slice(2, -7).toLowerCase()
416
+ : name.slice(2).toLowerCase();
422
417
  let handler = visit(attr.value, state);
423
- let capture = false; // TODO
424
418
 
425
419
  if (attr.metadata?.delegated) {
426
420
  let delegated_assignment;
@@ -552,8 +546,9 @@ const visitors = {
552
546
 
553
547
  state.template.push('<!>');
554
548
 
549
+ const is_spreading = node.attributes.some((attr) => attr.type === 'SpreadAttribute');
555
550
  const tracked = [];
556
- let props = [];
551
+ const props = [];
557
552
  let children_prop = null;
558
553
 
559
554
  for (const attr of node.attributes) {
@@ -577,6 +572,15 @@ const visitors = {
577
572
  } else {
578
573
  props.push(b.prop('init', attr.name, visit(attr.value, state)));
579
574
  }
575
+ } else if (attr.type === 'SpreadAttribute') {
576
+ props.push(
577
+ b.spread(
578
+ b.call(
579
+ '$.spread_object',
580
+ visit(attr.argument, { ...state, metadata: { ...state.metadata, spread: true } })
581
+ )
582
+ )
583
+ );
580
584
  } else {
581
585
  throw new Error('TODO');
582
586
  }
@@ -600,7 +604,18 @@ const visitors = {
600
604
  }
601
605
  }
602
606
 
603
- if (tracked.length > 0) {
607
+ if (is_spreading) {
608
+ state.init.push(
609
+ b.stmt(
610
+ b.call(
611
+ node.id,
612
+ id,
613
+ b.call('$.tracked_spread_object', b.thunk(b.object(props))),
614
+ b.id('$.active_block')
615
+ )
616
+ )
617
+ );
618
+ } else if (tracked.length > 0) {
604
619
  state.init.push(
605
620
  b.stmt(
606
621
  b.call(
@@ -20,4 +20,5 @@ export var LOGIC_BLOCK = FOR_BLOCK | IF_BLOCK | TRY_BLOCK;
20
20
 
21
21
  export var UNINITIALIZED = Symbol();
22
22
  export var TRACKED_OBJECT = Symbol();
23
+ export var SPREAD_OBJECT = Symbol();
23
24
  export var COMPUTED_PROPERTY = Symbol();
@@ -7,7 +7,6 @@ export {
7
7
  set_value,
8
8
  set_checked,
9
9
  set_selected,
10
- set_ref
11
10
  } from './render.js';
12
11
 
13
12
  export { render, render_spread, async } from './blocks.js';
@@ -25,6 +24,7 @@ export {
25
24
  async_computed,
26
25
  tracked,
27
26
  tracked_object,
27
+ tracked_spread_object,
28
28
  computed_property,
29
29
  get_property,
30
30
  set_property,
@@ -157,16 +157,6 @@ export function set_selected(element, selected) {
157
157
  }
158
158
  }
159
159
 
160
- export function set_ref(dom, fn) {
161
- effect(() => {
162
- fn(dom);
163
-
164
- return () => {
165
- fn(null);
166
- };
167
- });
168
- }
169
-
170
160
  export function apply_element_spread(element, fn) {
171
161
  return () => {
172
162
  set_attributes(element, fn());
@@ -18,6 +18,7 @@ import {
18
18
  EFFECT_BLOCK,
19
19
  PAUSED,
20
20
  ROOT_BLOCK,
21
+ SPREAD_OBJECT,
21
22
  TRACKED,
22
23
  TRACKED_OBJECT,
23
24
  TRY_BLOCK,
@@ -217,9 +218,9 @@ export function tracked(v, b) {
217
218
  };
218
219
  }
219
220
 
220
- export function computed(fn, b) {
221
+ export function computed(fn, block) {
221
222
  return {
222
- b,
223
+ b: block,
223
224
  blocks: null,
224
225
  c: 0,
225
226
  d: null,
@@ -644,7 +645,7 @@ export function flush_sync(fn) {
644
645
  var previous_queued_root_blocks = queued_root_blocks;
645
646
 
646
647
  try {
647
- const root_blocks = [];
648
+ var root_blocks = [];
648
649
 
649
650
  scheduler_mode = FLUSH_SYNC;
650
651
  queued_root_blocks = root_blocks;
@@ -667,6 +668,17 @@ export function flush_sync(fn) {
667
668
  }
668
669
  }
669
670
 
671
+ export function tracked_spread_object(fn) {
672
+ var obj = fn();
673
+
674
+ define_property(obj, SPREAD_OBJECT, {
675
+ value: fn,
676
+ enumerable: false
677
+ });
678
+
679
+ return obj;
680
+ }
681
+
670
682
  export function tracked_object(obj, properties, block) {
671
683
  var tracked_properties = obj[TRACKED_OBJECT];
672
684
 
@@ -719,6 +731,10 @@ export function get_property(obj, property, chain = false) {
719
731
 
720
732
  if (tracked_property !== undefined) {
721
733
  value = obj[property] = get(tracked_property);
734
+ } else if (SPREAD_OBJECT in obj) {
735
+ var spread_fn = obj[SPREAD_OBJECT];
736
+ var properties = spread_fn();
737
+ return get_property(properties, property, chain);
722
738
  }
723
739
 
724
740
  return value;
@@ -851,11 +867,11 @@ export function spread_object(obj) {
851
867
  return { ...obj };
852
868
  }
853
869
  var keys = original_object_keys(obj);
854
- const values = {};
870
+ var values = {};
855
871
 
856
872
  for (var i = 0; i < keys.length; i++) {
857
873
  var key = keys[i];
858
- values[key] = get_property_computed(obj, key);
874
+ values[key] = get_property(obj, key);
859
875
  }
860
876
 
861
877
  return values;
@@ -109,13 +109,13 @@ export function capture() {
109
109
  var previous_tracking = tracking;
110
110
  var previous_block = active_block;
111
111
  var previous_reaction = active_reaction;
112
- var previous_component = active_component;
112
+ var previous_component = active_component;
113
113
 
114
114
  return () => {
115
115
  set_tracking(previous_tracking);
116
116
  set_active_block(previous_block);
117
117
  set_active_reaction(previous_reaction);
118
- set_active_component(previous_component);
118
+ set_active_component(previous_component);
119
119
 
120
120
  queue_microtask(exit);
121
121
  };
package/types/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type Component<T> = (props: T) => void;
1
+ export type Component<T = Record<string, any>> = (props: T) => void;
2
2
 
3
3
  export declare function mount(
4
4
  component: () => void,