ripple 0.2.124 → 0.2.125

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 an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.124",
6
+ "version": "0.2.125",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -24,7 +24,7 @@ export function parse(source) {
24
24
  */
25
25
  export function compile(source, filename, options = {}) {
26
26
  const ast = parse_module(source);
27
- const analysis = analyze(ast, filename);
27
+ const analysis = analyze(ast, filename, options);
28
28
  const result = options.mode === 'server'
29
29
  ? transform_server(filename, source, analysis)
30
30
  : transform_client(filename, source, analysis, false);
@@ -1392,6 +1392,7 @@ function RipplePlugin(config) {
1392
1392
  this.next();
1393
1393
  this.enterScope(0);
1394
1394
  node.id = this.parseIdent();
1395
+ this.declareName(node.id.name, 'var', node.id.start);
1395
1396
  this.parseFunctionParams(node);
1396
1397
  this.eat(tt.braceL);
1397
1398
  node.body = [];
@@ -111,7 +111,8 @@ const visitors = {
111
111
  if (
112
112
  is_reference(node, /** @type {Node} */ (parent)) &&
113
113
  binding &&
114
- context.state.inside_server_block
114
+ context.state.inside_server_block &&
115
+ context.state.scope.server_block
115
116
  ) {
116
117
  let current_scope = binding.scope;
117
118
 
@@ -349,13 +350,14 @@ const visitors = {
349
350
  node.metadata = {
350
351
  ...node.metadata,
351
352
  has_template: false,
353
+ has_await: false,
352
354
  };
353
355
 
354
356
  context.visit(switch_case, context.state);
355
357
 
356
- if (!node.metadata.has_template) {
358
+ if (!node.metadata.has_template && !node.metadata.has_await) {
357
359
  error(
358
- 'Component switch statements must contain a template in each of their cases. Move the switch statement into an effect if it does not render anything.',
360
+ 'Component switch statements must contain a template or an await expression in each of their cases. Move the switch statement into an effect if it does not render anything.',
359
361
  context.state.analysis.module.filename,
360
362
  node,
361
363
  );
@@ -416,15 +418,16 @@ const visitors = {
416
418
  node.metadata = {
417
419
  ...node.metadata,
418
420
  has_template: false,
421
+ has_await: false,
419
422
  };
420
423
  context.next();
421
424
 
422
- if (!node.metadata.has_template) {
425
+ if (!node.metadata.has_template && !node.metadata.has_await) {
423
426
  error(
424
- 'Component for...of loops must contain a template in their body. Move the for loop into an effect if it does not render anything.',
425
- context.state.analysis.module.filename,
426
- node,
427
- );
427
+ 'Component for...of loops must contain a template or an await expression in their body. Move the for loop into an effect if it does not render anything.',
428
+ context.state.analysis.module.filename,
429
+ node,
430
+ );
428
431
  }
429
432
  },
430
433
 
@@ -437,9 +440,14 @@ const visitors = {
437
440
 
438
441
  if (declaration && declaration.type === 'FunctionDeclaration') {
439
442
  server_block.metadata.exports.push(declaration.id.name);
443
+ } else if (declaration && declaration.type === 'Component') {
444
+ // Handle exported components in server blocks
445
+ if (server_block) {
446
+ server_block.metadata.exports.push(declaration.id.name);
447
+ }
440
448
  } else {
441
449
  // TODO
442
- throw new Error('Not implemented');
450
+ throw new Error('Not implemented: Exported declaration type not supported in server blocks.');
443
451
  }
444
452
 
445
453
  return context.next();
@@ -462,35 +470,39 @@ const visitors = {
462
470
  node.metadata = {
463
471
  ...node.metadata,
464
472
  has_template: false,
473
+ has_await: false,
465
474
  };
466
475
 
467
476
  context.visit(node.consequent, context.state);
468
477
 
469
- if (!node.metadata.has_template) {
478
+ if (!node.metadata.has_template && !node.metadata.has_await) {
470
479
  error(
471
- 'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
472
- context.state.analysis.module.filename,
473
- node,
474
- );
480
+ 'Component if statements must contain a template or an await expression in their "then" body. Move the if statement into an effect if it does not render anything.',
481
+ context.state.analysis.module.filename,
482
+ node,
483
+ );
475
484
  }
476
485
 
477
486
  if (node.alternate) {
478
- node.metadata = {
479
- ...node.metadata,
480
- has_template: false,
481
- };
487
+ node.metadata.has_template = false;
488
+ node.metadata.has_await = false;
482
489
  context.visit(node.alternate, context.state);
483
490
 
484
- if (!node.metadata.has_template) {
491
+ if (!node.metadata.has_template && !node.metadata.has_await) {
485
492
  error(
486
- 'Component if statements must contain a template in their "else" body. Move the if statement into an effect if it does not render anything.',
487
- context.state.analysis.module.filename,
488
- node,
489
- );
493
+ 'Component if statements must contain a template or an await expression in their "else" body. Move the if statement into an effect if it does not render anything.',
494
+ context.state.analysis.module.filename,
495
+ node,
496
+ );
490
497
  }
491
498
  }
492
499
  },
493
-
500
+ /**
501
+ *
502
+ * @param {any} node
503
+ * @param {any} context
504
+ * @returns
505
+ */
494
506
  TryStatement(node, context) {
495
507
  if (!is_inside_component(context)) {
496
508
  return context.next();
@@ -744,7 +756,12 @@ const visitors = {
744
756
  mark_control_flow_has_template(context.path);
745
757
  context.next();
746
758
  },
747
-
759
+
760
+ /**
761
+ *
762
+ * @param {any} node
763
+ * @param {any} context
764
+ */
748
765
  AwaitExpression(node, context) {
749
766
  if (is_inside_component(context)) {
750
767
  if (context.state.metadata?.await === false) {
@@ -754,18 +771,27 @@ const visitors = {
754
771
  const parent_block = get_parent_block_node(context);
755
772
 
756
773
  if (parent_block !== null && parent_block.type !== 'Component') {
757
- error(
758
- '`await` expressions can only currently be used at the top-level of a component body. Support for using them in control flow statements will be added in the future.',
759
- context.state.analysis.module.filename,
760
- node,
761
- );
774
+ if (context.state.inside_server_block === false) {
775
+ error(
776
+ '`await` is not allowed in client-side control-flow statements',
777
+ context.state.analysis.module.filename,
778
+ node
779
+ );
780
+ }
762
781
  }
763
782
 
783
+ if (parent_block) {
784
+ if (!parent_block.metadata) {
785
+ parent_block.metadata = {};
786
+ }
787
+ parent_block.metadata.has_await = true;
788
+ }
789
+
764
790
  context.next();
765
791
  },
766
792
  };
767
793
 
768
- export function analyze(ast, filename) {
794
+ export function analyze(ast, filename, options = {}) {
769
795
  const scope_root = new ScopeRoot();
770
796
 
771
797
  const { scope, scopes } = create_scopes(ast, scope_root, null);
@@ -785,7 +811,7 @@ export function analyze(ast, filename) {
785
811
  scopes,
786
812
  analysis,
787
813
  inside_head: false,
788
- inside_server_block: false,
814
+ inside_server_block: options.mode === 'server',
789
815
  },
790
816
  visitors,
791
817
  );
@@ -120,7 +120,18 @@ const visitors = {
120
120
  component_fn = b.async(component_fn);
121
121
  }
122
122
 
123
- return component_fn;
123
+ const declaration = b.function_declaration(node.id, component_fn.params, component_fn.body, component_fn.async);
124
+
125
+ if (metadata.await) {
126
+ const parent = context.path.at(-1);
127
+ if (parent.type === 'Program' || parent.type === 'BlockStatement') {
128
+ const body = parent.body;
129
+ const index = body.indexOf(node);
130
+ body.splice(index + 1, 0, b.stmt(b.assignment('=', b.member(node.id, b.id('async')), b.true)));
131
+ }
132
+ }
133
+
134
+ return declaration;
124
135
  },
125
136
 
126
137
  CallExpression(node, context) {
@@ -270,11 +281,10 @@ const visitors = {
270
281
  let class_attribute = null;
271
282
 
272
283
  const handle_static_attr = (name, value) => {
273
- const attr_str = ` ${name}${
274
- is_boolean_attribute(name) && value === true
284
+ const attr_str = ` ${name}${is_boolean_attribute(name) && value === true
275
285
  ? ''
276
286
  : `="${value === true ? '' : escape_html(value, true)}"`
277
- }`;
287
+ }`;
278
288
 
279
289
  if (is_spreading) {
280
290
  // For spread attributes, store just the actual value, not the full attribute string
@@ -549,15 +559,22 @@ const visitors = {
549
559
  context.state.init.push(b.if(context.visit(node.test), consequent, alternate));
550
560
  },
551
561
 
552
- Identifier(node, context) {
553
- const parent = /** @type {Node} */ (context.path.at(-1));
562
+ AssignmentExpression(node, context) {
563
+ const left = node.left;
554
564
 
555
- if (is_reference(node, parent) && node.tracked) {
556
- add_ripple_internal_import(context);
557
- return b.call('_$_.get', build_getter(node, context));
565
+ if (left.type === 'Identifier' && left.tracked) {
566
+ return b.call(
567
+ 'set',
568
+ context.visit(left, { ...context.state, metadata: { tracking: false } }),
569
+ context.visit(node.right)
570
+ );
558
571
  }
572
+
573
+ return context.next();
559
574
  },
560
575
 
576
+
577
+
561
578
  ServerIdentifier(node, context) {
562
579
  return b.id('_$_server_$_');
563
580
  },
@@ -605,22 +622,22 @@ const visitors = {
605
622
  const try_statements =
606
623
  node.handler !== null
607
624
  ? [
608
- b.try(
609
- b.block(body),
610
- b.catch_clause(
611
- node.handler.param || b.id('error'),
612
- b.block(
613
- transform_body(node.handler.body.body, {
614
- ...context,
615
- state: {
616
- ...context.state,
617
- scope: context.state.scopes.get(node.handler.body),
618
- },
619
- }),
620
- ),
625
+ b.try(
626
+ b.block(body),
627
+ b.catch_clause(
628
+ node.handler.param || b.id('error'),
629
+ b.block(
630
+ transform_body(node.handler.body.body, {
631
+ ...context,
632
+ state: {
633
+ ...context.state,
634
+ scope: context.state.scopes.get(node.handler.body),
635
+ },
636
+ }),
621
637
  ),
622
638
  ),
623
- ]
639
+ ),
640
+ ]
624
641
  : body;
625
642
 
626
643
  context.state.init.push(
@@ -647,6 +664,8 @@ const visitors = {
647
664
  },
648
665
 
649
666
  AwaitExpression(node, context) {
667
+ context.state.scope.server_block = true
668
+ context.inside_server_block = true
650
669
  if (context.state.to_ts) {
651
670
  return context.next();
652
671
  }
@@ -672,10 +691,8 @@ const visitors = {
672
691
  const parent = context.path.at(-1);
673
692
 
674
693
  if (node.tracked || (node.property.type === 'Identifier' && node.property.tracked)) {
675
- add_ripple_internal_import(context);
676
-
677
694
  return b.call(
678
- '_$_.get',
695
+ 'get',
679
696
  b.member(
680
697
  context.visit(node.object),
681
698
  node.computed ? context.visit(node.property) : node.property,
@@ -690,13 +707,15 @@ const visitors = {
690
707
 
691
708
  Text(node, { visit, state }) {
692
709
  const metadata = { await: false };
693
- const expression = visit(node.expression, { ...state, metadata });
710
+ let expression = visit(node.expression, { ...state, metadata });
711
+
712
+ if (expression.type === 'Identifier' && expression.tracked) {
713
+ expression = b.call('get', expression);
714
+ }
694
715
 
695
716
  if (expression.type === 'Literal') {
696
717
  state.init.push(
697
- b.stmt(
698
- b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value))),
699
- ),
718
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value)))),
700
719
  );
701
720
  } else {
702
721
  state.init.push(
@@ -791,13 +810,7 @@ export function transform_server(filename, source, analysis) {
791
810
  }
792
811
 
793
812
  // Add async property to component functions
794
- for (const metadata of state.component_metadata) {
795
- if (metadata.async) {
796
- program.body.push(
797
- b.stmt(b.assignment('=', b.member(b.id(metadata.id), b.id('async')), b.true)),
798
- );
799
- }
800
- }
813
+
801
814
 
802
815
  for (const import_node of state.imports) {
803
816
  program.body.unshift(b.stmt(b.id(import_node)));
@@ -30,6 +30,8 @@ class Output {
30
30
  body = '';
31
31
  /** @type {Set<string>} */
32
32
  css = new Set();
33
+ /** @type {Promise<any>[]} */
34
+ promises = [];
33
35
  /** @type {Output | null} */
34
36
  #parent = null;
35
37
 
@@ -60,18 +62,27 @@ class Output {
60
62
  /** @type {render} */
61
63
  export async function render(component) {
62
64
  const output = new Output(null);
65
+ let head, body, css;
63
66
 
64
- if (component.async) {
65
- await component(output, {});
66
- } else {
67
- component(output, {});
68
- }
69
-
70
- const { head, body, css } = output;
67
+ try {
68
+ if (component.async) {
69
+ await component(output, {});
70
+ } else {
71
+ component(output, {});
72
+ }
73
+ if (output.promises.length > 0) {
74
+ await Promise.all(output.promises);
75
+ }
71
76
 
72
- return { head, body, css };
77
+ head = output.head
78
+ body = output.body
79
+ css = output.css
80
+ }
81
+ catch (error) {
82
+ console.log(error)
83
+ }
84
+ return { head, body, css }
73
85
  }
74
-
75
86
  /**
76
87
  * @returns {void}
77
88
  */
@@ -237,14 +237,14 @@ export function export_builder(declaration, specifiers = [], attributes = [], so
237
237
  * @param {ESTree.BlockStatement} body
238
238
  * @returns {ESTree.FunctionDeclaration}
239
239
  */
240
- export function function_declaration(id, params, body) {
240
+ export function function_declaration(id, params, body, async = false) {
241
241
  return {
242
242
  type: 'FunctionDeclaration',
243
243
  id,
244
244
  params,
245
245
  body,
246
246
  generator: false,
247
- async: false,
247
+ async,
248
248
  metadata: /** @type {any} */ (null), // should not be used by codegen
249
249
  };
250
250
  }
@@ -238,86 +238,6 @@ exports[`for statements > handles updating with new objects with same key 2`] =
238
238
  </div>
239
239
  `;
240
240
 
241
- exports[`for statements > render a simple dynamic array 1`] = `
242
- <div>
243
- <!---->
244
- <div
245
- class="Item 1"
246
- >
247
- Item 1
248
- </div>
249
- <div
250
- class="Item 2"
251
- >
252
- Item 2
253
- </div>
254
- <div
255
- class="Item 3"
256
- >
257
- Item 3
258
- </div>
259
- <!---->
260
- <button>
261
- Add Item
262
- </button>
263
-
264
- </div>
265
- `;
266
-
267
- exports[`for statements > render a simple dynamic array 2`] = `
268
- <div>
269
- <!---->
270
- <div
271
- class="Item 1"
272
- >
273
- Item 1
274
- </div>
275
- <div
276
- class="Item 2"
277
- >
278
- Item 2
279
- </div>
280
- <div
281
- class="Item 3"
282
- >
283
- Item 3
284
- </div>
285
- <div
286
- class="Item 4"
287
- >
288
- Item 4
289
- </div>
290
- <!---->
291
- <button>
292
- Add Item
293
- </button>
294
-
295
- </div>
296
- `;
297
-
298
- exports[`for statements > render a simple static array 1`] = `
299
- <div>
300
- <!---->
301
- <div
302
- class="Item 1"
303
- >
304
- Item 1
305
- </div>
306
- <div
307
- class="Item 2"
308
- >
309
- Item 2
310
- </div>
311
- <div
312
- class="Item 3"
313
- >
314
- Item 3
315
- </div>
316
- <!---->
317
-
318
- </div>
319
- `;
320
-
321
241
  exports[`for statements > renders a simple dynamic array 1`] = `
322
242
  <div>
323
243
  <!---->
@@ -56,51 +56,3 @@ exports[`basic client > rendering & text > should handle lexical scopes correctl
56
56
 
57
57
  </div>
58
58
  `;
59
-
60
- exports[`basic client > text rendering > basic operations 1`] = `
61
- <div>
62
- <div>
63
- 0
64
- </div>
65
- <div>
66
- 2
67
- </div>
68
- <div>
69
- 5
70
- </div>
71
- <div>
72
- 2
73
- </div>
74
-
75
- </div>
76
- `;
77
-
78
- exports[`basic client > text rendering > renders semi-dynamic text 1`] = `
79
- <div>
80
- <div>
81
- Hello World
82
- </div>
83
-
84
- </div>
85
- `;
86
-
87
- exports[`basic client > text rendering > renders simple JS expression logic correctly 1`] = `
88
- <div>
89
- <div>
90
- {"0":"Test"}
91
- </div>
92
- <div>
93
- 1
94
- </div>
95
-
96
- </div>
97
- `;
98
-
99
- exports[`basic client > text rendering > renders static text 1`] = `
100
- <div>
101
- <div>
102
- Hello World
103
- </div>
104
-
105
- </div>
106
- `;
@@ -123,4 +123,20 @@ describe('basic client > errors', () => {
123
123
 
124
124
  expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
125
125
  });
126
+
127
+ it('should throw error for await in client-side control-flow statements', () => {
128
+ const code = `
129
+ export default component App() {
130
+ let data = 'initial';
131
+ if (true) {
132
+ await new Promise(r => setTimeout(r, 100));
133
+ data = 'loaded';
134
+ }
135
+ <div>{data}</div>
136
+ }
137
+ `;
138
+ expect(() => {
139
+ compile(code, 'test.ripple', { mode: 'client' });
140
+ }).toThrow('`await` is not allowed in client-side control-flow statements');
141
+ });
126
142
  });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from 'ripple/server';
3
+ import { track, set, get } from 'ripple';
4
+
5
+ describe('await in control flow', () => {
6
+ it('should handle await inside if statement', async () => {
7
+ component App() {
8
+ let condition = true;
9
+ let data = track('loading');
10
+
11
+ if (condition) {
12
+ await new Promise(resolve => setTimeout(() => {
13
+ @data = 'loaded';
14
+ resolve();
15
+ }, 10));
16
+ }
17
+
18
+ <div>{@data}</div>
19
+ }
20
+
21
+ const { body } = await render(App);
22
+ expect(body).toBe('<div>loaded</div>');
23
+ });
24
+
25
+ it('should handle await inside for...of loop', async () => {
26
+ component App() {
27
+ const items = [1, 2, 3];
28
+ let result = '';
29
+
30
+ for (const item of items) {
31
+ await new Promise(resolve => setTimeout(resolve, 5));
32
+ result += item;
33
+ }
34
+
35
+ <div>{result}</div>
36
+ }
37
+
38
+ const { body } = await render(App);
39
+ expect(body).toBe('<div>123</div>');
40
+ });
41
+
42
+ it('should handle await inside switch statement', async () => {
43
+ component App() {
44
+ let value = 'b';
45
+
46
+ switch (value) {
47
+ case 'a':
48
+ <div>{'Case A'}</div>
49
+ break;
50
+ case 'b':
51
+ await new Promise(resolve => setTimeout(resolve, 10));
52
+ <div>{'Case B'}</div>
53
+ break;
54
+ default:
55
+ <div>{'Default Case'}</div>
56
+ }
57
+ }
58
+
59
+ const { body } = await render(App);
60
+ expect(body).toBe('<div>Case B</div>');
61
+ });});
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from 'ripple/server';
3
+
4
+ describe('for statements in SSR', () => {
5
+ it('renders a simple static array', async () => {
6
+ component App() {
7
+ const items = ['Item 1', 'Item 2', 'Item 3'];
8
+
9
+ for (const item of items) {
10
+ <div class={item}>{item}</div>
11
+ }
12
+ }
13
+
14
+ const { body } = await render(App);
15
+ expect(body).toBe('<div class="Item 1">Item 1</div><div class="Item 2">Item 2</div><div class="Item 3">Item 3</div>');
16
+ });
17
+
18
+ it('renders nested for...of loops', async () => {
19
+ component App() {
20
+ const groups = [
21
+ {
22
+ name: 'Group 1',
23
+ items: ['Item 1.1', 'Item 1.2']
24
+ },
25
+ {
26
+ name: 'Group 2',
27
+ items: ['Item 2.1', 'Item 2.2']
28
+ }
29
+ ];
30
+
31
+ for (const group of groups) {
32
+ <h1>{group.name}</h1>
33
+ <ul>
34
+ for (const item of group.items) {
35
+ <li>{item}</li>
36
+ }
37
+ </ul>
38
+ }
39
+ }
40
+
41
+ const { body } = await render(App);
42
+ expect(body).toBe('<h1>Group 1</h1><ul><li>Item 1.1</li><li>Item 1.2</li></ul><h1>Group 2</h1><ul><li>Item 2.1</li><li>Item 2.2</li></ul>');
43
+ });
44
+ });