ripple 0.2.44 → 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 +1 -1
- package/src/compiler/phases/1-parse/index.js +76 -0
- package/src/compiler/phases/2-analyze/index.js +23 -1
- package/src/compiler/phases/2-analyze/prune.js +3 -1
- package/src/compiler/phases/3-transform/index.js +34 -12
- package/src/compiler/utils.js +15 -13
- package/src/runtime/array.js +118 -25
- package/src/runtime/index.js +1 -1
- package/src/runtime/internal/client/constants.js +5 -2
- package/src/runtime/internal/client/runtime.js +1 -0
- package/tests/array.test.ripple +125 -37
- package/tests/basic.test.ripple +11 -5
package/package.json
CHANGED
|
@@ -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') {
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
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 =
|
|
512
|
-
|
|
513
|
-
|
|
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);
|
|
@@ -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 (
|
|
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
|
-
|
|
1062
|
-
|
|
1082
|
+
const update_fn = transformers?.update || transformers?.update_tracked;
|
|
1083
|
+
if (update_fn) {
|
|
1084
|
+
return update_fn(node);
|
|
1063
1085
|
}
|
|
1064
1086
|
}
|
|
1065
1087
|
|
|
@@ -1303,7 +1325,7 @@ const visitors = {
|
|
|
1303
1325
|
return b.literal(node.quasis[0].value.cooked);
|
|
1304
1326
|
}
|
|
1305
1327
|
|
|
1306
|
-
const expressions = node.expressions.map(expr => context.visit(expr));
|
|
1328
|
+
const expressions = node.expressions.map((expr) => context.visit(expr));
|
|
1307
1329
|
return b.template(node.quasis, expressions);
|
|
1308
1330
|
},
|
|
1309
1331
|
|
package/src/compiler/utils.js
CHANGED
|
@@ -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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
package/src/runtime/array.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
var tracked_elements = this.#tracked_elements;
|
|
121
|
+
this[INIT_AFTER_NEW]();
|
|
97
122
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 (!
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/runtime/index.js
CHANGED
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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
26
|
export var REF_PROP = 'ref';
|
|
26
|
-
|
|
27
|
+
/** @type {unique symbol} */
|
|
28
|
+
export const ARRAY_SET_INDEX_AT = Symbol();
|
|
29
|
+
export const MAX_ARRAY_LENGTH = 2**32 - 1;
|
package/tests/array.test.ripple
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1256
|
-
}
|
|
1257
|
-
|
|
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
|
-
|
|
1261
|
+
if (items) items.push(4);
|
|
1262
1262
|
}}>{'add item'}</button>
|
|
1263
|
-
|
|
1264
|
-
<pre>{
|
|
1263
|
+
|
|
1264
|
+
<pre>{JSON.stringify(items)}</pre>
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
-
render(
|
|
1267
|
+
render(Parent);
|
|
1268
1268
|
|
|
1269
|
-
|
|
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
|
-
|
|
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
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
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(
|
|
1304
|
+
render(Parent);
|
|
1303
1305
|
|
|
1304
|
-
|
|
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
|
|
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(
|
|
1342
|
+
render(Parent);
|
|
1337
1343
|
|
|
1338
|
-
|
|
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
|
|
package/tests/basic.test.ripple
CHANGED
|
@@ -115,6 +115,12 @@ describe('basic', () => {
|
|
|
115
115
|
|
|
116
116
|
<button onClick={() => $active = !$active}>{'Toggle'}</button>
|
|
117
117
|
<div $class={$active ? 'active' : 'inactive'}>{'Dynamic Class'}</div>
|
|
118
|
+
|
|
119
|
+
<style>
|
|
120
|
+
.active {
|
|
121
|
+
color: green;
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
render(Basic);
|
|
@@ -122,17 +128,17 @@ describe('basic', () => {
|
|
|
122
128
|
const button = container.querySelector('button');
|
|
123
129
|
const div = container.querySelector('div');
|
|
124
130
|
|
|
125
|
-
expect(div.
|
|
126
|
-
|
|
131
|
+
expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
|
|
132
|
+
expect(div.classList.contains('inactive')).toBe(true);
|
|
133
|
+
|
|
127
134
|
button.click();
|
|
128
135
|
flushSync();
|
|
129
|
-
|
|
130
|
-
expect(div.className).toBe('active');
|
|
136
|
+
expect(div.classList.contains('active')).toBe(true);
|
|
131
137
|
|
|
132
138
|
button.click();
|
|
133
139
|
flushSync();
|
|
134
140
|
|
|
135
|
-
expect(div.
|
|
141
|
+
expect(div.classList.contains('inactive')).toBe(true);
|
|
136
142
|
});
|
|
137
143
|
|
|
138
144
|
it('render dynamic id attribute', () => {
|