@esportsplus/reactivity 0.37.2 → 0.38.0
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/build/compiler/array.d.ts +8 -1
- package/build/compiler/array.js +109 -93
- package/build/compiler/bindings.d.ts +18 -12
- package/build/compiler/bindings.js +172 -52
- package/build/compiler/constants.d.ts +1 -0
- package/build/compiler/constants.js +1 -0
- package/build/compiler/index.d.ts +11 -3
- package/build/compiler/index.js +96 -34
- package/build/compiler/object.js +11 -14
- package/build/compiler/plugins/vite.d.ts +14 -0
- package/build/compiler/primitives.d.ts +6 -0
- package/build/compiler/primitives.js +99 -67
- package/build/compiler/types.d.ts +5 -4
- package/build/types.d.ts +1 -1
- package/package.json +2 -2
- package/pnpm-workspace.yaml +1 -1
- package/src/compiler/array.ts +162 -120
- package/src/compiler/bindings.ts +259 -56
- package/src/compiler/constants.ts +1 -0
- package/src/compiler/index.ts +142 -46
- package/src/compiler/object.ts +17 -17
- package/src/compiler/primitives.ts +139 -88
- package/src/compiler/types.ts +7 -4
- package/src/types.ts +2 -1
- package/test/compiler/compiler.test.ts +152 -48
- package/test/compiler/coverage.test.ts +164 -0
package/src/compiler/array.ts
CHANGED
|
@@ -1,21 +1,56 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
3
|
-
import { COMPOUND_OPERATORS, NAMESPACE
|
|
3
|
+
import { COMPOUND_OPERATORS, NAMESPACE } from './constants';
|
|
4
4
|
import type { Bindings, IsReactiveCall } from './types';
|
|
5
|
+
import type { Failure } from './primitives';
|
|
5
6
|
import scope from './bindings';
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
type
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
type ArrayTransformResult = {
|
|
10
|
+
failures: Failure[];
|
|
11
|
+
// Whether the output references the runtime namespace (ReactiveArray construction)
|
|
12
|
+
namespaced: boolean;
|
|
13
|
+
// The array expressions found reactive by type
|
|
14
|
+
reactive: ts.Expression[];
|
|
11
15
|
replacements: ReplacementIntent[];
|
|
12
|
-
sourceFile: ts.SourceFile;
|
|
13
16
|
};
|
|
14
17
|
|
|
18
|
+
// Sites that must go through a ReactiveArray's tracking API when their array is reactive
|
|
19
|
+
type Candidates = {
|
|
20
|
+
// `arr.length` reads and writes
|
|
21
|
+
lengths: ts.PropertyAccessExpression[];
|
|
22
|
+
// `arr = [...]`
|
|
23
|
+
reassignments: ts.BinaryExpression[];
|
|
24
|
+
// `arr[i] = v`, `arr[i] op= v`, `arr[i]++`, `delete arr[i]`
|
|
25
|
+
writes: ts.ElementAccessExpression[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
function collect(node: ts.Node, candidates: Candidates): void {
|
|
30
|
+
if (ts.isTypeNode(node)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (ts.isPropertyAccessExpression(node) && node.name.text === 'length') {
|
|
35
|
+
candidates.lengths.push(node);
|
|
36
|
+
}
|
|
37
|
+
else if (ts.isElementAccessExpression(node) && isWritten(node)) {
|
|
38
|
+
candidates.writes.push(node);
|
|
39
|
+
}
|
|
40
|
+
else if (
|
|
41
|
+
ts.isBinaryExpression(node) &&
|
|
42
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
43
|
+
ts.isIdentifier(node.left) &&
|
|
44
|
+
ts.isArrayLiteralExpression(scope.unwrap(node.right))
|
|
45
|
+
) {
|
|
46
|
+
candidates.reassignments.push(node);
|
|
47
|
+
}
|
|
15
48
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
49
|
+
node.forEachChild(child => collect(child, candidates));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Pass 1: reactive([...]) literals become ReactiveArray instances
|
|
53
|
+
function declare(ctx: { isReactiveCall: IsReactiveCall; replacements: ReplacementIntent[]; sourceFile: ts.SourceFile }, node: ts.Node): void {
|
|
19
54
|
if (ctx.isReactiveCall(node) && node.arguments.length > 0) {
|
|
20
55
|
let arg = node.arguments[0],
|
|
21
56
|
expression = ts.isAsExpression(arg) ? arg.expression : arg;
|
|
@@ -30,10 +65,6 @@ function declare(ctx: VisitContext, node: ts.Node): void {
|
|
|
30
65
|
elementType = getElementTypeText(node.parent.type, ctx.sourceFile);
|
|
31
66
|
}
|
|
32
67
|
|
|
33
|
-
if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
|
|
34
|
-
scope.declare(ctx.bindings, node.parent.name, TYPES.Array);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
68
|
let typeParam = elementType ? `<${elementType}>` : '';
|
|
38
69
|
|
|
39
70
|
ctx.replacements.push({
|
|
@@ -45,32 +76,6 @@ function declare(ctx: VisitContext, node: ts.Node): void {
|
|
|
45
76
|
}
|
|
46
77
|
}
|
|
47
78
|
|
|
48
|
-
if (
|
|
49
|
-
ts.isVariableDeclaration(node) &&
|
|
50
|
-
ts.isIdentifier(node.name) &&
|
|
51
|
-
node.initializer &&
|
|
52
|
-
(ts.isIdentifier(node.initializer) || ts.isPropertyAccessExpression(node.initializer)) &&
|
|
53
|
-
scope.isArray(ctx.bindings, node.initializer)
|
|
54
|
-
) {
|
|
55
|
-
scope.declare(ctx.bindings, node.name, TYPES.Array);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node)) && node.parameters) {
|
|
59
|
-
for (let i = 0, n = node.parameters.length; i < n; i++) {
|
|
60
|
-
let param = node.parameters[i];
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
ts.isIdentifier(param.name) &&
|
|
64
|
-
param.type &&
|
|
65
|
-
ts.isTypeReferenceNode(param.type) &&
|
|
66
|
-
ts.isIdentifier(param.type.typeName) &&
|
|
67
|
-
param.type.typeName.text === 'ReactiveArray'
|
|
68
|
-
) {
|
|
69
|
-
scope.declare(ctx.bindings, param.name, TYPES.Array);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
79
|
node.forEachChild(child => declare(ctx, child));
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -92,108 +97,145 @@ function getElementTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): s
|
|
|
92
97
|
return null;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
function isWritten(access: ts.ElementAccessExpression): boolean {
|
|
101
|
+
let parent = access.parent;
|
|
102
|
+
|
|
103
|
+
if (!parent) {
|
|
104
|
+
return false;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
if (ts.
|
|
102
|
-
let
|
|
103
|
-
parent = node.parent;
|
|
107
|
+
if (ts.isBinaryExpression(parent)) {
|
|
108
|
+
let op = parent.operatorToken.kind;
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
|
|
110
|
+
return parent.left === access && (op === ts.SyntaxKind.EqualsToken || COMPOUND_OPERATORS.has(op));
|
|
111
|
+
}
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
generate: (sf) => `${expr.getText(sf)}.$length = ${parent.right.getText(sf)}`
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
ctx.replacements.push({
|
|
117
|
-
node: parent,
|
|
118
|
-
generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} ${parent.right.getText(sf)}`
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// arr.length++ / arr.length-- / ++arr.length / --arr.length
|
|
123
|
-
else if (parent && (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent))) {
|
|
124
|
-
let op = parent.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
|
|
113
|
+
if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
|
|
114
|
+
return parent.operator === ts.SyntaxKind.PlusPlusToken || parent.operator === ts.SyntaxKind.MinusMinusToken;
|
|
115
|
+
}
|
|
125
116
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
117
|
+
return ts.isDeleteExpression(parent);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function length(access: ts.PropertyAccessExpression, replacements: ReplacementIntent[]): void {
|
|
121
|
+
let expr = access.expression,
|
|
122
|
+
parent = access.parent;
|
|
123
|
+
|
|
124
|
+
// arr.length = value OR arr.length op= value
|
|
125
|
+
if (parent && ts.isBinaryExpression(parent) && parent.left === access && (parent.operatorToken.kind === ts.SyntaxKind.EqualsToken || COMPOUND_OPERATORS.has(parent.operatorToken.kind))) {
|
|
126
|
+
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
|
|
127
|
+
|
|
128
|
+
replacements.push({
|
|
129
|
+
node: parent,
|
|
130
|
+
generate: (sf) => op === undefined
|
|
131
|
+
? `${expr.getText(sf)}.$length = ${parent.right.getText(sf)}`
|
|
132
|
+
: `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} ${parent.right.getText(sf)}`
|
|
133
|
+
});
|
|
138
134
|
}
|
|
135
|
+
// arr.length++ / arr.length-- / ++arr.length / --arr.length
|
|
136
|
+
else if (parent && (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent))) {
|
|
137
|
+
let op = parent.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
replacements.push({
|
|
140
|
+
node: parent,
|
|
141
|
+
generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} 1`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// Read-only: arr.length -> arr.$length
|
|
145
|
+
else {
|
|
146
|
+
replacements.push({
|
|
147
|
+
node: access,
|
|
148
|
+
generate: (sf) => `${expr.getText(sf)}.$length`
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// An element write must notify, so it goes through $set; operands are evaluated once
|
|
154
|
+
function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): void {
|
|
155
|
+
let parent = access.parent!,
|
|
156
|
+
array = (sf: ts.SourceFile) => access.expression.getText(sf),
|
|
157
|
+
index = (sf: ts.SourceFile) => access.argumentExpression.getText(sf);
|
|
158
|
+
|
|
159
|
+
if (ts.isDeleteExpression(parent)) {
|
|
160
|
+
ctx.failures.push({ message: 'an element of a reactive array cannot be deleted; use splice()', node: parent });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (ts.isBinaryExpression(parent)) {
|
|
165
|
+
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind),
|
|
166
|
+
right = parent.right;
|
|
147
167
|
|
|
148
168
|
ctx.replacements.push({
|
|
149
|
-
node,
|
|
150
|
-
generate: (sf) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)`
|
|
169
|
+
node: parent,
|
|
170
|
+
generate: (sf) => op === undefined
|
|
171
|
+
? `${array(sf)}.$set(${index(sf)}, ${right.getText(sf)})`
|
|
172
|
+
: `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${right.getText(sf)})))(${array(sf)}, ${index(sf)})`
|
|
154
173
|
});
|
|
174
|
+
|
|
175
|
+
return;
|
|
155
176
|
}
|
|
156
177
|
|
|
157
|
-
|
|
158
|
-
ts.
|
|
159
|
-
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
160
|
-
ts.isIdentifier(node.left) &&
|
|
161
|
-
scope.isArray(ctx.bindings, node.left)
|
|
162
|
-
) {
|
|
163
|
-
let name = node.left.text,
|
|
164
|
-
right = node.right;
|
|
178
|
+
let unary = parent as ts.PostfixUnaryExpression | ts.PrefixUnaryExpression,
|
|
179
|
+
op = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
|
|
165
180
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
ctx.replacements.push({
|
|
182
|
+
node: unary,
|
|
183
|
+
generate: (sf) => ts.isPrefixUnaryExpression(unary)
|
|
184
|
+
? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array(sf)}, ${index(sf)})`
|
|
185
|
+
: `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array(sf)}, ${index(sf)}, ${array(sf)}[${index(sf)}])`
|
|
186
|
+
});
|
|
187
|
+
}
|
|
170
188
|
|
|
171
|
-
if (ts.isArrayLiteralExpression(right)) {
|
|
172
|
-
let elements = right;
|
|
173
189
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
// Reactive arrays are recognized by type, so `.length` and element writes compile wherever the
|
|
191
|
+
// array came from: a local reactive([...]), an import, an object property, a parameter
|
|
192
|
+
export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): ArrayTransformResult => {
|
|
193
|
+
let candidates: Candidates = { lengths: [], reassignments: [], writes: [] },
|
|
194
|
+
result: ArrayTransformResult = { failures: [], namespaced: false, reactive: [], replacements: [] };
|
|
195
|
+
|
|
196
|
+
declare({ isReactiveCall, replacements: result.replacements, sourceFile }, sourceFile);
|
|
197
|
+
|
|
198
|
+
result.namespaced = result.replacements.length > 0;
|
|
199
|
+
collect(sourceFile, candidates);
|
|
200
|
+
|
|
201
|
+
let reactive = scope.arrays(bindings, [
|
|
202
|
+
...candidates.lengths.map(access => access.expression),
|
|
203
|
+
...candidates.reassignments.map(assignment => assignment.left),
|
|
204
|
+
...candidates.writes.map(access => access.expression)
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
result.reactive = [...reactive] as ts.Expression[];
|
|
208
|
+
|
|
209
|
+
for (let i = 0, n = candidates.lengths.length; i < n; i++) {
|
|
210
|
+
if (reactive.has(candidates.lengths[i].expression)) {
|
|
211
|
+
length(candidates.lengths[i], result.replacements);
|
|
180
212
|
}
|
|
181
213
|
}
|
|
182
214
|
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
for (let i = 0, n = candidates.writes.length; i < n; i++) {
|
|
216
|
+
if (reactive.has(candidates.writes[i].expression)) {
|
|
217
|
+
write(candidates.writes[i], result);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// `arr = [...]` refills the same ReactiveArray so everything holding it stays live
|
|
222
|
+
for (let i = 0, n = candidates.reassignments.length; i < n; i++) {
|
|
223
|
+
let assignment = candidates.reassignments[i];
|
|
185
224
|
|
|
225
|
+
if (!reactive.has(assignment.left) || isReactiveCall(scope.unwrap(assignment.right))) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
186
228
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
bindings,
|
|
190
|
-
isReactiveCall,
|
|
191
|
-
replacements: [],
|
|
192
|
-
sourceFile
|
|
193
|
-
};
|
|
229
|
+
let name = (assignment.left as ts.Identifier).text,
|
|
230
|
+
elements = scope.unwrap(assignment.right) as ts.ArrayLiteralExpression;
|
|
194
231
|
|
|
195
|
-
|
|
196
|
-
|
|
232
|
+
result.replacements.push({
|
|
233
|
+
node: assignment,
|
|
234
|
+
generate: (sf) => elements.elements.length > 0
|
|
235
|
+
? `${name}.splice(0, ${name}.length, ...${elements.getText(sf)})`
|
|
236
|
+
: `${name}.splice(0, ${name}.length)`
|
|
237
|
+
});
|
|
238
|
+
}
|
|
197
239
|
|
|
198
|
-
return
|
|
240
|
+
return result;
|
|
199
241
|
};
|