ripple 0.2.43 → 0.2.45

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.43",
6
+ "version": "0.2.45",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -38,6 +38,82 @@ function RipplePlugin(config) {
38
38
  return null;
39
39
  }
40
40
 
41
+ // Override getTokenFromCode to handle @ as an identifier prefix
42
+ getTokenFromCode(code) {
43
+ if (code === 64) { // '@' character
44
+ // Look ahead to see if this is followed by a valid identifier character
45
+ if (this.pos + 1 < this.input.length) {
46
+ const nextChar = this.input.charCodeAt(this.pos + 1);
47
+ // Check if the next character can start an identifier
48
+ if ((nextChar >= 65 && nextChar <= 90) || // A-Z
49
+ (nextChar >= 97 && nextChar <= 122) || // a-z
50
+ nextChar === 95 || nextChar === 36) { // _ or $
51
+
52
+ // Check if we're in an expression context
53
+ // In JSX expressions, inside parentheses, assignments, etc.
54
+ // we want to treat @ as an identifier prefix rather than decorator
55
+ const currentType = this.type;
56
+ const inExpression = this.exprAllowed ||
57
+ currentType === tt.braceL || // Inside { }
58
+ currentType === tt.parenL || // Inside ( )
59
+ currentType === tt.eq || // After =
60
+ currentType === tt.comma || // After ,
61
+ currentType === tt.colon || // After :
62
+ currentType === tt.question || // After ?
63
+ currentType === tt.logicalOR || // After ||
64
+ currentType === tt.logicalAND; // After &&
65
+
66
+ if (inExpression) {
67
+ return this.readAtIdentifier();
68
+ }
69
+ }
70
+ }
71
+ }
72
+ return super.getTokenFromCode(code);
73
+ }
74
+
75
+ // Read an @ prefixed identifier
76
+ readAtIdentifier() {
77
+ const start = this.pos;
78
+ this.pos++; // skip '@'
79
+
80
+ // Read the identifier part manually
81
+ let word = '';
82
+ while (this.pos < this.input.length) {
83
+ const ch = this.input.charCodeAt(this.pos);
84
+ if ((ch >= 65 && ch <= 90) || // A-Z
85
+ (ch >= 97 && ch <= 122) || // a-z
86
+ (ch >= 48 && ch <= 57) || // 0-9
87
+ ch === 95 || ch === 36) { // _ or $
88
+ word += this.input[this.pos++];
89
+ } else {
90
+ break;
91
+ }
92
+ }
93
+
94
+ if (word === '') {
95
+ this.raise(start, 'Invalid @ identifier');
96
+ }
97
+
98
+ // Return the full identifier including @
99
+ return this.finishToken(tt.name, '@' + word);
100
+ }
101
+
102
+ // Override parseIdent to mark @ identifiers as tracked
103
+ parseIdent(liberal) {
104
+ const node = super.parseIdent(liberal);
105
+ if (node.name && node.name.startsWith('@')) {
106
+ node.name = node.name.slice(1); // Remove the '@' for internal use
107
+ node.tracked = true;
108
+ node.start++;
109
+ const prev_pos = this.pos;
110
+ this.pos = node.start;
111
+ node.loc.start = this.curPosition();
112
+ this.pos = prev_pos;
113
+ }
114
+ return node;
115
+ }
116
+
41
117
  parseExportDefaultDeclaration() {
42
118
  // Check if this is "export default component"
43
119
  if (this.value === 'component') {
@@ -139,15 +215,14 @@ function RipplePlugin(config) {
139
215
  }
140
216
 
141
217
  if (this.eat(tt.braceL)) {
142
- if (this.type.label === '@') {
218
+ if (this.value === 'ref') {
143
219
  this.next();
144
- if (this.value !== 'use') {
145
- this.unexpected();
220
+ if (this.type === tt.braceR) {
221
+ this.raise(this.start, '"ref" is a Ripple keyword and must be used in the form {ref fn}');
146
222
  }
147
- this.next();
148
223
  node.argument = this.parseMaybeAssign();
149
224
  this.expect(tt.braceR);
150
- return this.finishNode(node, 'UseAttribute');
225
+ return this.finishNode(node, 'RefAttribute');
151
226
  } else if (this.type === tt.ellipsis) {
152
227
  this.expect(tt.ellipsis);
153
228
  node.argument = this.parseMaybeAssign();
@@ -118,12 +118,34 @@ const visitors = {
118
118
  if (
119
119
  is_reference(node, /** @type {Node} */ (parent)) &&
120
120
  context.state.metadata?.tracking === false &&
121
- is_tracked_name(node.name) &&
121
+ is_tracked_name(node) &&
122
122
  binding?.node !== node
123
123
  ) {
124
124
  context.state.metadata.tracking = true;
125
125
  }
126
126
 
127
+ if (
128
+ is_reference(node, /** @type {Node} */ (parent)) &&
129
+ node.tracked &&
130
+ binding?.node !== node
131
+ ) {
132
+ if (context.state.metadata?.tracking === false) {
133
+ context.state.metadata.tracking = true;
134
+ }
135
+ binding.transform = {
136
+ read_tracked: (node) => b.call('$.get_tracked', node),
137
+ assign_tracked: (node, value) => b.call('$.set', node, value, b.id('__block')),
138
+ update_tracked: (node) => {
139
+ return b.call(
140
+ node.prefix ? '$.update_pre' : '$.update',
141
+ node.argument,
142
+ b.id('__block'),
143
+ node.operator === '--' && b.literal(-1),
144
+ );
145
+ },
146
+ };
147
+ }
148
+
127
149
  context.next();
128
150
  },
129
151
 
@@ -405,7 +405,9 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
405
405
  if (attribute.type === 'SpreadAttribute') return true;
406
406
 
407
407
  if (attribute.type !== 'Attribute') continue;
408
- if (attribute.name.name.toLowerCase() !== name.toLowerCase()) continue;
408
+
409
+ const lowerCaseName = name.toLowerCase();
410
+ if (![lowerCaseName, `$${lowerCaseName}`].includes(attribute.name.name.toLowerCase())) continue;
409
411
 
410
412
  if (expected_value === null) return true;
411
413
 
@@ -72,10 +72,15 @@ function build_getter(node, context) {
72
72
 
73
73
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
74
74
  const binding = state.scope.get(node.name);
75
+ const transform = binding?.transform;
75
76
 
76
77
  // don't transform the declaration itself
77
- if (node !== binding?.node && binding?.transform?.read) {
78
- return binding.transform.read(node, context.state?.metadata?.spread, context.visit);
78
+ if (node !== binding?.node) {
79
+ const read_fn = transform?.read || (node.tracked && transform?.read_tracked);
80
+
81
+ if (read_fn) {
82
+ return read_fn(node, context.state?.metadata?.spread, context.visit);
83
+ }
79
84
  }
80
85
  }
81
86
 
@@ -100,7 +105,7 @@ const visitors = {
100
105
  const binding = context.state.scope.get(node.name);
101
106
  if (
102
107
  context.state.metadata?.tracking === false &&
103
- is_tracked_name(node.name) &&
108
+ (is_tracked_name(node.name) || node.tracked) &&
104
109
  binding?.node !== node
105
110
  ) {
106
111
  context.state.metadata.tracking = true;
@@ -135,6 +140,18 @@ const visitors = {
135
140
  context.state.metadata.tracking = true;
136
141
  }
137
142
 
143
+ if (
144
+ !context.state.to_ts &&
145
+ callee.type === 'Identifier' &&
146
+ callee.name === 'tracked' &&
147
+ is_ripple_import(callee, context)
148
+ ) {
149
+ return {
150
+ ...node,
151
+ arguments: [...node.arguments.map((arg) => context.visit(arg)), b.id('__block')],
152
+ };
153
+ }
154
+
138
155
  if (
139
156
  !is_inside_component(context, true) ||
140
157
  context.state.to_ts ||
@@ -199,8 +216,8 @@ const visitors = {
199
216
  callee.optional ? b.true : undefined,
200
217
  node.optional ? b.true : undefined,
201
218
  ...node.arguments.map((arg) => context.visit(arg)),
202
- )
203
- )
219
+ ),
220
+ ),
204
221
  );
205
222
  }
206
223
  }
@@ -508,9 +525,10 @@ const visitors = {
508
525
 
509
526
  if (is_spreading) {
510
527
  // For spread attributes, store just the actual value, not the full attribute string
511
- const actual_value = is_boolean_attribute(name) && value === true
512
- ? b.literal(true)
513
- : b.literal(value === true ? '' : value);
528
+ const actual_value =
529
+ is_boolean_attribute(name) && value === true
530
+ ? b.literal(true)
531
+ : b.literal(value === true ? '' : value);
514
532
  spread_attributes.push(b.prop('init', b.literal(name), actual_value));
515
533
  } else {
516
534
  state.template.push(attr_value);
@@ -665,9 +683,9 @@ const visitors = {
665
683
  }
666
684
  } else if (attr.type === 'SpreadAttribute') {
667
685
  spread_attributes.push(b.spread(b.call('$.spread_object', visit(attr.argument, state))));
668
- } else if (attr.type === 'UseAttribute') {
686
+ } else if (attr.type === 'RefAttribute') {
669
687
  const id = state.flush_node();
670
- state.init.push(b.stmt(b.call('$.use', id, b.thunk(visit(attr.argument, state)))));
688
+ state.init.push(b.stmt(b.call('$.ref', id, b.thunk(visit(attr.argument, state)))));
671
689
  }
672
690
  }
673
691
 
@@ -770,8 +788,8 @@ const visitors = {
770
788
  ),
771
789
  ),
772
790
  );
773
- } else if (attr.type === 'UseAttribute') {
774
- props.push(b.prop('init', b.call('$.use_prop'), visit(attr.argument, state), true));
791
+ } else if (attr.type === 'RefAttribute') {
792
+ props.push(b.prop('init', b.call('$.ref_prop'), visit(attr.argument, state), true));
775
793
  } else if (attr.type === 'AccessorAttribute') {
776
794
  // # means it's an accessor to the runtime
777
795
  tracked.push(b.literal('#' + attr.name.name));
@@ -1002,7 +1020,10 @@ const visitors = {
1002
1020
 
1003
1021
  if (left.type === 'MemberExpression') {
1004
1022
  // need to capture setting length of array to throw a runtime error
1005
- if (left.property.type === 'Identifier' && (is_tracked_name(left.property.name) || left.property.name === 'length')) {
1023
+ if (
1024
+ left.property.type === 'Identifier' &&
1025
+ (is_tracked_name(left.property.name) || left.property.name === 'length')
1026
+ ) {
1006
1027
  if (left.property.name !== '$length') {
1007
1028
  return b.call(
1008
1029
  '$.set_property',
@@ -1058,8 +1079,9 @@ const visitors = {
1058
1079
  const transformers = left && binding?.transform;
1059
1080
 
1060
1081
  if (left === argument) {
1061
- if (transformers?.update) {
1062
- return transformers.update(node);
1082
+ const update_fn = transformers?.update || transformers?.update_tracked;
1083
+ if (update_fn) {
1084
+ return update_fn(node);
1063
1085
  }
1064
1086
  }
1065
1087
 
@@ -1299,7 +1321,11 @@ const visitors = {
1299
1321
  },
1300
1322
 
1301
1323
  TemplateLiteral(node, context) {
1302
- const expressions = node.expressions.map(expr => context.visit(expr));
1324
+ if (node.expressions.length === 0) {
1325
+ return b.literal(node.quasis[0].value.cooked);
1326
+ }
1327
+
1328
+ const expressions = node.expressions.map((expr) => context.visit(expr));
1303
1329
  return b.template(node.quasis, expressions);
1304
1330
  },
1305
1331
 
@@ -1409,12 +1435,12 @@ function transform_ts_child(node, context) {
1409
1435
  const children = [];
1410
1436
  let has_children_props = false;
1411
1437
 
1412
- // Filter out UseAttributes and handle them separately
1413
- const use_attributes = [];
1438
+ // Filter out RefAttributes and handle them separately
1439
+ const ref_attributes = [];
1414
1440
  const attributes = node.attributes
1415
1441
  .filter((attr) => {
1416
- if (attr.type === 'UseAttribute') {
1417
- use_attributes.push(attr);
1442
+ if (attr.type === 'RefAttribute') {
1443
+ ref_attributes.push(attr);
1418
1444
  return false;
1419
1445
  }
1420
1446
  return true;
@@ -1438,10 +1464,10 @@ function transform_ts_child(node, context) {
1438
1464
  }
1439
1465
  });
1440
1466
 
1441
- // Add UseAttribute references separately for sourcemap purposes
1442
- for (const use_attr of use_attributes) {
1467
+ // Add RefAttribute references separately for sourcemap purposes
1468
+ for (const ref_attr of ref_attributes) {
1443
1469
  const metadata = { await: false };
1444
- const argument = visit(use_attr.argument, { ...state, metadata });
1470
+ const argument = visit(ref_attr.argument, { ...state, metadata });
1445
1471
  state.init.push(b.stmt(argument));
1446
1472
  }
1447
1473
 
@@ -61,8 +61,8 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
61
61
  );
62
62
  const source_content = source.substring(source_offset, source_offset + segment_length);
63
63
 
64
- // Skip mappings for UseAttribute syntax to avoid overlapping sourcemaps
65
- if (source_content.includes('{@use ') || source_content.match(/\{\s*@use\s+/)) {
64
+ // Skip mappings for RefAttribute syntax to avoid overlapping sourcemaps
65
+ if (source_content.includes('{ref ') || source_content.match(/\{\s*ref\s+/)) {
66
66
  continue;
67
67
  }
68
68
 
@@ -19,7 +19,7 @@ const VOID_ELEMENT_NAMES = [
19
19
  'param',
20
20
  'source',
21
21
  'track',
22
- 'wbr'
22
+ 'wbr',
23
23
  ];
24
24
 
25
25
  /**
@@ -564,19 +564,21 @@ export function build_assignment(operator, left, right, context) {
564
564
  const transform = binding.transform;
565
565
 
566
566
  // reassignment
567
- if (
568
- (object === left || (left.type === 'MemberExpression' && left.computed && operator === '=')) &&
569
- transform?.assign
570
- ) {
571
- let value = /** @type {Expression} */ (
572
- context.visit(build_assignment_value(operator, left, right))
573
- );
567
+ if (object === left || (left.type === 'MemberExpression' && left.computed && operator === '=')) {
568
+ const assign_fn = transform?.assign || transform?.assign_tracked;
569
+ if (assign_fn) {
570
+ let value = /** @type {Expression} */ (
571
+ context.visit(build_assignment_value(operator, left, right))
572
+ );
574
573
 
575
- return transform.assign(
576
- object,
577
- value,
578
- left.type === 'MemberExpression' && left.computed ? context.visit(left.property) : undefined,
579
- );
574
+ return assign_fn(
575
+ object,
576
+ value,
577
+ left.type === 'MemberExpression' && left.computed
578
+ ? context.visit(left.property)
579
+ : undefined,
580
+ );
581
+ }
580
582
  }
581
583
 
582
584
  // mutation
@@ -1,8 +1,11 @@
1
- import { TRACKED_OBJECT, ARRAY_SET_INDEX_AT } from './internal/client/constants.js';
1
+ import { TRACKED_OBJECT, ARRAY_SET_INDEX_AT, MAX_ARRAY_LENGTH } from './internal/client/constants.js';
2
2
  import { get, safe_scope, set, tracked } from './internal/client/runtime.js';
3
3
  import { is_ripple_array } from './internal/client/utils.js';
4
4
  /** @import { Block, Tracked } from '#client' */
5
5
 
6
+ /** @type {unique symbol} */
7
+ const INIT_AFTER_NEW = Symbol();
8
+
6
9
  /** @type {(symbol | string | any)[]} */
7
10
  const introspect_methods = [
8
11
  'concat',
@@ -36,7 +39,7 @@ const introspect_methods = [
36
39
  'with',
37
40
  ];
38
41
 
39
- let init = false;
42
+ let is_proto_set = false;
40
43
 
41
44
  /**
42
45
  * @template T
@@ -45,6 +48,8 @@ let init = false;
45
48
  export class RippleArray extends Array {
46
49
  /** @type {Array<Tracked>} */
47
50
  #tracked_elements = [];
51
+ /** @type {Tracked} */
52
+ // @ts-expect-error
48
53
  #tracked_index;
49
54
 
50
55
  /**
@@ -55,11 +60,11 @@ export class RippleArray extends Array {
55
60
  * @returns {RippleArray<U>}
56
61
  */
57
62
  static from(arrayLike, mapFn, thisArg) {
58
- return new RippleArray(...(
59
- mapFn ?
63
+ var arr = mapFn ?
60
64
  Array.from(arrayLike, mapFn, thisArg)
61
- : Array.from(arrayLike)
62
- ));
65
+ : Array.from(arrayLike);
66
+
67
+ return get_instance_from_static(arr);
63
68
  }
64
69
 
65
70
  /**
@@ -70,11 +75,30 @@ export class RippleArray extends Array {
70
75
  * @returns {Promise<RippleArray<U>>}
71
76
  */
72
77
  static async fromAsync(arrayLike, mapFn, thisArg) {
73
- return new RippleArray(...(
74
- mapFn ?
78
+ var block = safe_scope();
79
+ // create empty array to get the right scope
80
+ var result = new RippleArray();
81
+
82
+ var arr = mapFn ?
75
83
  await Array.fromAsync(arrayLike, mapFn, thisArg)
76
84
  : await Array.fromAsync(arrayLike)
77
- ));
85
+
86
+ var first = get_first_if_length(arr);
87
+
88
+ if (first) {
89
+ result[0] = first;
90
+ } else {
91
+ result.length = arr.length;
92
+ for (let i = 0; i < arr.length; i++) {
93
+ if (i in arr) {
94
+ result[i] = arr[i];
95
+ }
96
+ }
97
+ }
98
+
99
+ result[INIT_AFTER_NEW](block);
100
+
101
+ return result
78
102
  }
79
103
 
80
104
  /**
@@ -83,7 +107,9 @@ export class RippleArray extends Array {
83
107
  * @returns {RippleArray<U>}
84
108
  */
85
109
  static of(...elements) {
86
- return new RippleArray(...elements);
110
+ var arr = Array.of(...elements);
111
+
112
+ return get_instance_from_static(arr);
87
113
  }
88
114
 
89
115
  /**
@@ -92,24 +118,33 @@ export class RippleArray extends Array {
92
118
  constructor(...elements) {
93
119
  super(...elements);
94
120
 
95
- var block = safe_scope();
96
- var tracked_elements = this.#tracked_elements;
121
+ this[INIT_AFTER_NEW]();
97
122
 
98
- for (var i = 0; i < this.length; i++) {
99
- if (!(i in this)) {
100
- continue;
123
+ if (!is_proto_set) {
124
+ is_proto_set = true;
125
+ this.#set_proto();
126
+ }
127
+ }
128
+
129
+ [INIT_AFTER_NEW](block = safe_scope()) {
130
+ if (this.length !== 0) {
131
+ var tracked_elements = this.#tracked_elements;
132
+ for (var i = 0; i < this.length; i++) {
133
+ if (!(i in this)) {
134
+ continue;
135
+ }
136
+ tracked_elements[i] = tracked(this[i], block);
101
137
  }
102
- tracked_elements[i] = tracked(this[i], block);
103
138
  }
104
- this.#tracked_index = tracked(this.length, block);
105
139
 
106
- if (!init) {
107
- init = true;
108
- this.#init();
140
+ if (!this.#tracked_index) {
141
+ this.#tracked_index = tracked(this.length, block);
142
+ } else if (this.#tracked_index.v !== this.length) {
143
+ set(this.#tracked_index, this.length, block);
109
144
  }
110
145
  }
111
146
 
112
- #init() {
147
+ #set_proto() {
113
148
  var proto = RippleArray.prototype;
114
149
  var array_proto = Array.prototype;
115
150
 
@@ -138,7 +173,7 @@ export class RippleArray extends Array {
138
173
  // the caller reruns on length changes
139
174
  this.$length;
140
175
  // the caller reruns on element changes
141
- get_all_elements(this);
176
+ establish_trackable_deps(this);
142
177
  return result;
143
178
  };
144
179
  }
@@ -564,14 +599,72 @@ export class RippleArray extends Array {
564
599
  export function get_all_elements(array) {
565
600
  /** @type {Tracked[]} */
566
601
  var tracked_elements = /** @type {Tracked[]} */ (array[TRACKED_OBJECT]);
567
- var arr = [];
602
+ // pre-allocate to support holey arrays
603
+ var result = new Array(array.length);
604
+
605
+ for (var i = 0; i < array.length; i++) {
606
+ if (tracked_elements[i] !== undefined) {
607
+ get(tracked_elements[i]);
608
+ }
609
+
610
+ if (i in array) {
611
+ result[i] = array[i];
612
+ }
613
+ }
614
+
615
+ return result;
616
+ }
617
+
618
+ /**
619
+ * @template T
620
+ * @param {RippleArray<T>} array
621
+ * @returns {void}
622
+ */
623
+ function establish_trackable_deps (array) {
624
+ var tracked_elements = array[TRACKED_OBJECT];
568
625
 
569
626
  for (var i = 0; i < tracked_elements.length; i++) {
570
627
  if (tracked_elements[i] !== undefined) {
571
628
  get(tracked_elements[i]);
572
629
  }
573
- arr.push(array[i]);
630
+ }
631
+ }
632
+
633
+ /**
634
+ * @template T
635
+ * @param {T[]} array
636
+ * @returns {RippleArray<T>}
637
+ */
638
+ function get_instance_from_static(array) {
639
+ /** @type RippleArray<T> */
640
+ var result;
641
+ /** @type {T | void} */
642
+ var first = get_first_if_length(array);
643
+
644
+ if (first) {
645
+ result = new RippleArray();
646
+ result[0] = first;
647
+ result[INIT_AFTER_NEW]();
648
+ } else {
649
+ result = new RippleArray(...array);
574
650
  }
575
651
 
576
- return arr;
652
+ return result;
653
+ }
654
+
655
+ /**
656
+ * @template T
657
+ * @param {T[]} array
658
+ * @returns {T | void}
659
+ */
660
+ function get_first_if_length (array) {
661
+ var first = array[0];
662
+
663
+ if (
664
+ array.length === 1 && (0 in array) && Number.isInteger(first)
665
+ && /** @type {number} */ (first) >= 0
666
+ && /** @type {number} */ (first) <= MAX_ARRAY_LENGTH
667
+ ) {
668
+ return first;
669
+ }
577
670
  }
@@ -36,7 +36,7 @@ export function mount(component, options) {
36
36
 
37
37
  export { create_context as createContext } from './internal/client/context.js';
38
38
 
39
- export { flush_sync as flushSync, untrack, deferred } from './internal/client/runtime.js';
39
+ export { flush_sync as flushSync, untrack, deferred, tracked } from './internal/client/runtime.js';
40
40
 
41
41
  export { RippleArray } from './array.js';
42
42
 
@@ -50,3 +50,4 @@ export { user_effect as effect } from './internal/client/blocks.js';
50
50
 
51
51
  export { Portal } from './internal/client/portal.js';
52
52
 
53
+ export { ref_prop as createRefKey } from './internal/client/runtime.js';
@@ -99,23 +99,23 @@ export function async(fn) {
99
99
  * @param {() => (element: Element) => (void | (() => void))} get_fn
100
100
  * @returns {Block}
101
101
  */
102
- export function use(element, get_fn) {
102
+ export function ref(element, get_fn) {
103
103
  /** @type {(element: Element) => (void | (() => void) | undefined)} */
104
- var use_fn;
104
+ var ref_fn;
105
105
  /** @type {Block | null} */
106
106
  var e;
107
107
 
108
108
  return block(RENDER_BLOCK, () => {
109
- if (use_fn !== (use_fn = get_fn())) {
109
+ if (ref_fn !== (ref_fn = get_fn())) {
110
110
  if (e) {
111
111
  destroy_block(e);
112
112
  e = null;
113
113
  }
114
114
 
115
- if (use_fn) {
115
+ if (ref_fn) {
116
116
  e = branch(() => {
117
117
  effect(() => {
118
- return use_fn(element);
118
+ return ref_fn(element);
119
119
  });
120
120
  });
121
121
  }
@@ -19,8 +19,11 @@ export var DESTROYED = 1 << 17;
19
19
  export var LOGIC_BLOCK = FOR_BLOCK | IF_BLOCK | TRY_BLOCK;
20
20
 
21
21
  export var UNINITIALIZED = Symbol();
22
- export var TRACKED_OBJECT = Symbol();
22
+ /** @type {unique symbol} */
23
+ export const TRACKED_OBJECT = Symbol();
23
24
  export var SPREAD_OBJECT = Symbol();
24
25
  export var COMPUTED_PROPERTY = Symbol();
25
- export var USE_PROP = '@use';
26
- export var ARRAY_SET_INDEX_AT = Symbol();
26
+ export var REF_PROP = 'ref';
27
+ /** @type {unique symbol} */
28
+ export const ARRAY_SET_INDEX_AT = Symbol();
29
+ export const MAX_ARRAY_LENGTH = 2**32 - 1;
@@ -9,7 +9,7 @@ export {
9
9
  set_selected,
10
10
  } from './render.js';
11
11
 
12
- export { render, render_spread, async, use } from './blocks.js';
12
+ export { render, render_spread, async, ref } from './blocks.js';
13
13
 
14
14
  export { event, delegate } from './events.js';
15
15
 
@@ -42,7 +42,7 @@ export {
42
42
  push_component,
43
43
  pop_component,
44
44
  untrack,
45
- use_prop,
45
+ ref_prop,
46
46
  fallback,
47
47
  exclude_from_object,
48
48
  } from './runtime.js';
@@ -1,5 +1,5 @@
1
- import { destroy_block, use } from './blocks';
2
- import { USE_PROP } from './constants';
1
+ import { destroy_block, ref } from './blocks';
2
+ import { REF_PROP } from './constants';
3
3
  import { get_descriptors, get_own_property_symbols, get_prototype_of } from './utils';
4
4
 
5
5
  export function set_text(text, value) {
@@ -174,16 +174,16 @@ export function apply_element_spread(element, fn) {
174
174
  }
175
175
 
176
176
  for (const symbol of get_own_property_symbols(next)) {
177
- var use_fn = next[symbol];
177
+ var ref_fn = next[symbol];
178
178
 
179
- if (symbol.description === USE_PROP && (!prev || use_fn !== prev[symbol])) {
179
+ if (symbol.description === REF_PROP && (!prev || ref_fn !== prev[symbol])) {
180
180
  if (effects[symbol]) {
181
181
  destroy_block(effects[symbol]);
182
182
  }
183
- effects[symbol] = use(element, () => use_fn);
183
+ effects[symbol] = ref(element, () => ref_fn);
184
184
  }
185
185
 
186
- next[symbol] = use_fn;
186
+ next[symbol] = ref_fn;
187
187
  }
188
188
 
189
189
  set_attributes(element, next);
@@ -25,7 +25,7 @@ import {
25
25
  TRACKED_OBJECT,
26
26
  TRY_BLOCK,
27
27
  UNINITIALIZED,
28
- USE_PROP,
28
+ REF_PROP,
29
29
  ARRAY_SET_INDEX_AT,
30
30
  } from './constants';
31
31
  import { capture, suspend } from './try.js';
@@ -260,6 +260,7 @@ export function run_block(block) {
260
260
  * @returns {Tracked}
261
261
  */
262
262
  export function tracked(v, b) {
263
+ // TODO: now we expose tracked, we should likely block access in DEV somehow
263
264
  return {
264
265
  b,
265
266
  c: 0,
@@ -1203,8 +1204,8 @@ export function pop_component() {
1203
1204
  active_component = component.p;
1204
1205
  }
1205
1206
 
1206
- export function use_prop() {
1207
- return Symbol(USE_PROP);
1207
+ export function ref_prop() {
1208
+ return Symbol(REF_PROP);
1208
1209
  }
1209
1210
 
1210
1211
  /**
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { mount, flushSync, effect, untrack, RippleArray } from 'ripple';
3
- import { ARRAY_SET_INDEX_AT } from '../src/runtime/internal/client/constants.js';
3
+ import { ARRAY_SET_INDEX_AT, MAX_ARRAY_LENGTH } from '../src/runtime/internal/client/constants.js';
4
4
 
5
5
  describe('RippleArray', () => {
6
6
  let container;
@@ -1244,37 +1244,34 @@ describe('RippleArray', () => {
1244
1244
  expect(container.querySelector('pre').textContent).toBe('Cannot set length on RippleArray, use $length instead');
1245
1245
  });
1246
1246
 
1247
- ('fromAsync' in Array.prototype ? describe : describe.skip)('RippleArray fromAsync', () => {
1247
+ ('fromAsync' in Array.prototype ? describe : describe.skip)('RippleArray fromAsync', async () => {
1248
1248
  it('handles static fromAsync method with reactivity', async () => {
1249
- component ArrayTest() {
1250
- let itemsPromise = RippleArray.fromAsync(Promise.resolve([1, 2, 3]));
1251
- let items = null;
1252
- let error = null;
1253
-
1249
+ component Parent() {
1254
1250
  try {
1255
- items = await itemsPromise;
1256
- } catch (e) {
1257
- error = e.message;
1251
+ <ArrayTest />
1252
+ } async {
1253
+ <div>{'Loading placeholder...'}</div>
1258
1254
  }
1255
+ }
1256
+
1257
+ component ArrayTest() {
1258
+ let items = await RippleArray.fromAsync([1, 2, 3]);
1259
1259
 
1260
1260
  <button onClick={() => {
1261
- if (items) items.push(4);
1261
+ if (items) items.push(4);
1262
1262
  }}>{'add item'}</button>
1263
- <pre>{error ? 'Error: ' + error : 'Loaded'}</pre>
1264
- <pre>{items ? JSON.stringify(items) : 'Loading...'}</pre>
1263
+
1264
+ <pre>{JSON.stringify(items)}</pre>
1265
1265
  }
1266
1266
 
1267
- render(ArrayTest);
1267
+ render(Parent);
1268
1268
 
1269
- // Wait for promise to resolve
1270
- await new Promise(resolve => setTimeout(resolve, 50));
1269
+ await new Promise(resolve => setTimeout(resolve, 0));
1271
1270
  flushSync();
1272
1271
 
1273
1272
  const addButton = container.querySelector('button');
1274
1273
 
1275
- // Check that the promise resolved correctly
1276
- expect(container.querySelectorAll('pre')[0].textContent).toBe('Loaded');
1277
- expect(container.querySelectorAll('pre')[1].textContent).toBe('[1,2,3]');
1274
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[1,2,3]');
1278
1275
 
1279
1276
  // Test adding an item to the async-created array
1280
1277
  addButton.click();
@@ -1284,33 +1281,35 @@ describe('RippleArray', () => {
1284
1281
  });
1285
1282
 
1286
1283
  it('handles static fromAsync method with mapping function', async () => {
1284
+ component Parent() {
1285
+ try {
1286
+ <ArrayTest />
1287
+ } async {
1288
+ <div>{'Loading placeholder...'}</div>
1289
+ }
1290
+ }
1291
+
1287
1292
  component ArrayTest() {
1288
- let itemsPromise = RippleArray.fromAsync(
1289
- Promise.resolve([1, 2, 3]),
1290
- x => x * 2
1293
+ let items = await RippleArray.fromAsync(
1294
+ [1, 2, 3],
1295
+ x => x * 2
1291
1296
  );
1292
- let items = null;
1293
-
1294
- items = await itemsPromise;
1295
1297
 
1296
1298
  <button onClick={() => {
1297
- if (items) items.push(8);
1299
+ if (items) items.push(8);
1298
1300
  }}>{'add item'}</button>
1299
1301
  <pre>{items ? JSON.stringify(items) : 'Loading...'}</pre>
1300
1302
  }
1301
1303
 
1302
- render(ArrayTest);
1304
+ render(Parent);
1303
1305
 
1304
- // Wait for promise to resolve
1305
- await new Promise(resolve => setTimeout(resolve, 50));
1306
+ await new Promise(resolve => setTimeout(resolve, 0));
1306
1307
  flushSync();
1307
1308
 
1308
1309
  const addButton = container.querySelector('button');
1309
1310
 
1310
- // Check that the promise resolved correctly with mapping applied
1311
1311
  expect(container.querySelector('pre').textContent).toBe('[2,4,6]');
1312
1312
 
1313
- // Test adding an item to the async-created array
1314
1313
  addButton.click();
1315
1314
  flushSync();
1316
1315
 
@@ -1318,13 +1317,20 @@ describe('RippleArray', () => {
1318
1317
  });
1319
1318
 
1320
1319
  it('handles error in fromAsync method', async () => {
1320
+ component Parent() {
1321
+ try {
1322
+ <ArrayTest />
1323
+ } async {
1324
+ <div>{'Loading placeholder...'}</div>
1325
+ }
1326
+ }
1327
+
1321
1328
  component ArrayTest() {
1322
- let itemsPromise = RippleArray.fromAsync(Promise.reject(new Error('Async error')));
1323
1329
  let items = null;
1324
1330
  let error = null;
1325
1331
 
1326
1332
  try {
1327
- items = await itemsPromise;
1333
+ items = await RippleArray.fromAsync(Promise.reject(new Error('Async error')));
1328
1334
  } catch (e) {
1329
1335
  error = e.message;
1330
1336
  }
@@ -1333,13 +1339,11 @@ describe('RippleArray', () => {
1333
1339
  <pre>{items ? JSON.stringify(items) : 'No items'}</pre>
1334
1340
  }
1335
1341
 
1336
- render(ArrayTest);
1342
+ render(Parent);
1337
1343
 
1338
- // Wait for promise to reject
1339
- await new Promise(resolve => setTimeout(resolve, 50));
1344
+ await new Promise(resolve => setTimeout(resolve, 0));
1340
1345
  flushSync();
1341
1346
 
1342
- // Check that the error was caught correctly
1343
1347
  expect(container.querySelectorAll('pre')[0].textContent).toBe('Error: Async error');
1344
1348
  expect(container.querySelectorAll('pre')[1].textContent).toBe('No items');
1345
1349
  });
@@ -1463,6 +1467,90 @@ describe('RippleArray', () => {
1463
1467
  expect(container.querySelectorAll('pre')[5].textContent).toBe('items[4]: 4');
1464
1468
  });
1465
1469
  });
1470
+
1471
+ describe('Creates RippleArray with a single element', () => {
1472
+ it('specifies int', () => {
1473
+ component ArrayTest() {
1474
+ let items = new RippleArray(3);
1475
+ <pre>{JSON.stringify(items)}</pre>
1476
+ <pre>{items.$length}</pre>
1477
+ }
1478
+
1479
+ render(ArrayTest);
1480
+
1481
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[null,null,null]');
1482
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
1483
+ });
1484
+
1485
+ it('errors on exceeding max array size', () => {
1486
+ component ArrayTest() {
1487
+ let error = null;
1488
+
1489
+ try {
1490
+ new RippleArray(MAX_ARRAY_LENGTH + 1);
1491
+ } catch (e) {
1492
+ error = e.message;
1493
+ }
1494
+
1495
+ <pre>{error}</pre>
1496
+ }
1497
+
1498
+ render(ArrayTest);
1499
+
1500
+ expect(container.querySelector('pre').textContent).toBe('Invalid array length');
1501
+ });
1502
+
1503
+ it('specifies int using static from method', () => {
1504
+ component ArrayTest() {
1505
+ let items = RippleArray.from([4]);
1506
+ <pre>{JSON.stringify(items)}</pre>
1507
+ <pre>{items.$length}</pre>
1508
+ }
1509
+
1510
+ render(ArrayTest);
1511
+
1512
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[4]');
1513
+ // expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
1514
+ });
1515
+
1516
+ it('specifies int using static of method', () => {
1517
+ component ArrayTest() {
1518
+ let items = RippleArray.of(5);
1519
+ <pre>{JSON.stringify(items)}</pre>
1520
+ <pre>{items.$length}</pre>
1521
+ }
1522
+
1523
+ render(ArrayTest);
1524
+
1525
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[5]');
1526
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
1527
+ });
1528
+
1529
+ ('fromAsync' in Array.prototype ? it : it.skip)('specifies int using static fromAsync method', async () => {
1530
+ component Parent() {
1531
+ try {
1532
+ <ArrayTest />
1533
+ } async {
1534
+ <div>{'Loading placeholder...'}</div>
1535
+ }
1536
+ }
1537
+
1538
+ component ArrayTest() {
1539
+ const items = await RippleArray.fromAsync([6]);
1540
+
1541
+ <pre>{items ? JSON.stringify(items) : 'Loading...'}</pre>
1542
+ <pre>{items ? items.$length : ''}</pre>
1543
+ }
1544
+
1545
+ render(Parent);
1546
+
1547
+ await new Promise(resolve => setTimeout(resolve, 0));
1548
+ flushSync();
1549
+
1550
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[6]');
1551
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
1552
+ });
1553
+ });
1466
1554
  });
1467
1555
 
1468
1556
 
@@ -88,12 +88,39 @@ describe('basic', () => {
88
88
  expect(container.querySelector('div').textContent).toEqual('');
89
89
  });
90
90
 
91
+ it('render tick template literal for nested children', () => {
92
+ component Child({ $level, $children }) {
93
+ if($level == 1) {
94
+ <h1><$children /></h1>
95
+ }
96
+ if($level == 2) {
97
+ <h2><$children /></h2>
98
+ }
99
+ if($level == 3) {
100
+ <h3><$children /></h3>
101
+ }
102
+ }
103
+
104
+ component App() {
105
+ <Child $level={1}>{`Heading 1`} </Child>
106
+ }
107
+
108
+ render(App);
109
+ expect(container.querySelector('h1').textContent).toEqual('Heading 1');
110
+ });
111
+
91
112
  it('render dynamic class attribute', () => {
92
113
  component Basic() {
93
114
  let $active = false;
94
115
 
95
116
  <button onClick={() => $active = !$active}>{'Toggle'}</button>
96
117
  <div $class={$active ? 'active' : 'inactive'}>{'Dynamic Class'}</div>
118
+
119
+ <style>
120
+ .active {
121
+ color: green;
122
+ }
123
+ </style>
97
124
  }
98
125
 
99
126
  render(Basic);
@@ -101,17 +128,17 @@ describe('basic', () => {
101
128
  const button = container.querySelector('button');
102
129
  const div = container.querySelector('div');
103
130
 
104
- expect(div.className).toBe('inactive');
105
-
131
+ expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
132
+ expect(div.classList.contains('inactive')).toBe(true);
133
+
106
134
  button.click();
107
135
  flushSync();
108
-
109
- expect(div.className).toBe('active');
136
+ expect(div.classList.contains('active')).toBe(true);
110
137
 
111
138
  button.click();
112
139
  flushSync();
113
140
 
114
- expect(div.className).toBe('inactive');
141
+ expect(div.classList.contains('inactive')).toBe(true);
115
142
  });
116
143
 
117
144
  it('render dynamic id attribute', () => {
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { mount, flushSync, RippleArray } from 'ripple';
3
3
 
4
- describe('@use element decorators', () => {
4
+ describe('refs', () => {
5
5
  let container;
6
6
 
7
7
  function render(component) {
@@ -24,7 +24,7 @@ describe('@use element decorators', () => {
24
24
  let div;
25
25
 
26
26
  component Component() {
27
- <div {@use (node) => { div = node; }}>{'Hello World'}</div>
27
+ <div {ref (node) => { div = node; }}>{'Hello World'}</div>
28
28
  }
29
29
  render(Component);
30
30
  flushSync();
@@ -37,11 +37,11 @@ describe('@use element decorators', () => {
37
37
  component Component() {
38
38
  let items = RippleArray.from([1, 2, 3]);
39
39
 
40
- function ref(node) {
40
+ function componentRef(node) {
41
41
  _node = node;
42
42
  }
43
43
 
44
- <Child {@use ref} {items} />
44
+ <Child {ref componentRef} {items} />
45
45
  }
46
46
 
47
47
  component Child(props) {
package/types/index.d.ts CHANGED
@@ -79,3 +79,5 @@ declare global {
79
79
  // Add other runtime functions as needed for TypeScript analysis
80
80
  };
81
81
  }
82
+
83
+ export declare function createRefKey(): symbol;