ripple 0.2.121 → 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.121",
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();
@@ -582,6 +594,14 @@ const visitors = {
582
594
  }
583
595
  binding.metadata.is_dynamic_component = true;
584
596
  }
597
+
598
+ if (!is_dom_element && state.elements) {
599
+ state.elements.push(node);
600
+ // Mark dynamic elements as scoped by default since we can't match CSS at compile time
601
+ if (state.component?.css) {
602
+ node.metadata.scoped = true;
603
+ }
604
+ }
585
605
  }
586
606
 
587
607
  if (is_dom_element) {
@@ -736,7 +756,12 @@ const visitors = {
736
756
  mark_control_flow_has_template(context.path);
737
757
  context.next();
738
758
  },
739
-
759
+
760
+ /**
761
+ *
762
+ * @param {any} node
763
+ * @param {any} context
764
+ */
740
765
  AwaitExpression(node, context) {
741
766
  if (is_inside_component(context)) {
742
767
  if (context.state.metadata?.await === false) {
@@ -746,18 +771,27 @@ const visitors = {
746
771
  const parent_block = get_parent_block_node(context);
747
772
 
748
773
  if (parent_block !== null && parent_block.type !== 'Component') {
749
- error(
750
- '`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.',
751
- context.state.analysis.module.filename,
752
- node,
753
- );
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
+ }
754
781
  }
755
782
 
783
+ if (parent_block) {
784
+ if (!parent_block.metadata) {
785
+ parent_block.metadata = {};
786
+ }
787
+ parent_block.metadata.has_await = true;
788
+ }
789
+
756
790
  context.next();
757
791
  },
758
792
  };
759
793
 
760
- export function analyze(ast, filename) {
794
+ export function analyze(ast, filename, options = {}) {
761
795
  const scope_root = new ScopeRoot();
762
796
 
763
797
  const { scope, scopes } = create_scopes(ast, scope_root, null);
@@ -777,7 +811,7 @@ export function analyze(ast, filename) {
777
811
  scopes,
778
812
  analysis,
779
813
  inside_head: false,
780
- inside_server_block: false,
814
+ inside_server_block: options.mode === 'server',
781
815
  },
782
816
  visitors,
783
817
  );
@@ -840,6 +840,14 @@ const visitors = {
840
840
  let property =
841
841
  attr.value === null ? b.literal(true) : visit(attr.value, { ...state, metadata });
842
842
 
843
+ if (attr.name.name === 'class' && node.metadata.scoped && state.component.css) {
844
+ if (property.type === 'Literal') {
845
+ property = b.literal(`${state.component.css.hash} ${property.value}`);
846
+ } else {
847
+ property = b.array([property, b.literal(state.component.css.hash)]);
848
+ }
849
+ }
850
+
843
851
  if (metadata.tracking || attr.name.tracked) {
844
852
  if (attr.name.name === 'children') {
845
853
  children_prop = b.thunk(property);
@@ -872,6 +880,17 @@ const visitors = {
872
880
  }
873
881
  }
874
882
 
883
+ if (node.metadata.scoped && state.component.css) {
884
+ const hasClassAttr = node.attributes.some(attr =>
885
+ attr.type === 'Attribute' && attr.name.type === 'Identifier' && attr.name.name === 'class'
886
+ );
887
+ if (!hasClassAttr) {
888
+ const name = is_spreading ? '#class' : 'class';
889
+ const value = state.component.css.hash;
890
+ props.push(b.prop('init', b.key(name), b.literal(value)));
891
+ }
892
+ }
893
+
875
894
  const children_filtered = [];
876
895
 
877
896
  for (const child of node.children) {
@@ -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)));
@@ -1,8 +1,7 @@
1
1
  /** @import { Block, Component } from '#client' */
2
2
 
3
- import { branch, destroy_block, render } from './blocks.js';
3
+ import { branch, destroy_block, render, render_spread } from './blocks.js';
4
4
  import { COMPOSITE_BLOCK } from './constants.js';
5
- import { apply_element_spread } from './render';
6
5
  import { active_block } from './runtime.js';
7
6
 
8
7
  /**
@@ -48,8 +47,7 @@ export function composite(get_component, node, props) {
48
47
  };
49
48
  }
50
49
 
51
- const spread_fn = apply_element_spread(element, () => props || {});
52
- spread_fn();
50
+ render_spread(element, () => props || {});
53
51
 
54
52
  if (typeof props?.children === 'function') {
55
53
  var child_anchor = document.createComment('');
@@ -1,4 +1,4 @@
1
- import { effect } from './blocks';
1
+ import { effect } from './blocks.js';
2
2
  /**
3
3
  * @param {Text | Comment} node
4
4
  * @param {string} content
@@ -1,4 +1,5 @@
1
1
  /** @import { Component, Derived } from '#server' */
2
+ /** @import { render } from 'ripple/server'*/
2
3
  import { DERIVED, UNINITIALIZED } from '../client/constants.js';
3
4
  import { is_tracked_object } from '../client/utils.js';
4
5
  import { escape } from '../../../utils/escaping.js';
@@ -29,6 +30,8 @@ class Output {
29
30
  body = '';
30
31
  /** @type {Set<string>} */
31
32
  css = new Set();
33
+ /** @type {Promise<any>[]} */
34
+ promises = [];
32
35
  /** @type {Output | null} */
33
36
  #parent = null;
34
37
 
@@ -56,24 +59,30 @@ class Output {
56
59
  }
57
60
  }
58
61
 
59
- /**
60
- * @param {((output: Output, props: Record<string, any>) => void | Promise<void>) & { async?: boolean }} component
61
- * @returns {Promise<{head: string, body: string, css: Set<string>}>}
62
- */
62
+ /** @type {render} */
63
63
  export async function render(component) {
64
64
  const output = new Output(null);
65
+ let head, body, css;
65
66
 
66
- if (component.async) {
67
- await component(output, {});
68
- } else {
69
- component(output, {});
70
- }
71
-
72
- 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
+ }
73
76
 
74
- 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 }
75
85
  }
76
-
77
86
  /**
78
87
  * @returns {void}
79
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
  });