@esportsplus/reactivity 0.37.1 → 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 +3 -3
- package/pnpm-workspace.yaml +2 -2
- 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/bindings.ts
CHANGED
|
@@ -1,100 +1,303 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import {
|
|
2
|
+
import { references } from '@esportsplus/typescript/compiler';
|
|
3
|
+
import type { Origin } from '@esportsplus/typescript/compiler';
|
|
4
|
+
import { ENTRYPOINT, PACKAGE_NAME, TYPES } from './constants';
|
|
3
5
|
import type { Bindings } from './types';
|
|
4
6
|
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
let parts: string[] = [];
|
|
8
|
+
type Assignment = { call: ts.CallExpression; target: ts.Identifier };
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Per program snapshot: answers about declarations and types hold for the snapshot's lifetime
|
|
11
|
+
type Caches = {
|
|
12
|
+
arrays: Map<number, boolean>;
|
|
13
|
+
assignments: Map<string, Map<string, Assignment[]>>;
|
|
14
|
+
kinds: Map<string, TYPES | null>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const ARRAY_BRAND = 'once';
|
|
19
|
+
|
|
20
|
+
const REACTIVE_ARRAY = 'ReactiveArray';
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
let caches = new WeakMap<ts.Program, Caches>();
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// `x = reactive(...)` assignments in a file, keyed by the assigned name: a variable declared
|
|
27
|
+
// without a reactive initializer becomes a binding through one of these
|
|
28
|
+
function assignments(bindings: Bindings, file: ts.SourceFile): Map<string, Assignment[]> {
|
|
29
|
+
let cache = cachesOf(bindings.program).assignments,
|
|
30
|
+
found = cache.get(file.fileName);
|
|
31
|
+
|
|
32
|
+
if (found) {
|
|
33
|
+
return found;
|
|
12
34
|
}
|
|
13
35
|
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
let result = new Map<string, Assignment[]>(),
|
|
37
|
+
visit = (node: ts.Node): void => {
|
|
38
|
+
if (
|
|
39
|
+
ts.isBinaryExpression(node) &&
|
|
40
|
+
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
41
|
+
ts.isIdentifier(node.left)
|
|
42
|
+
) {
|
|
43
|
+
let call = unwrap(node.right);
|
|
44
|
+
|
|
45
|
+
if (ts.isCallExpression(call)) {
|
|
46
|
+
let list = result.get(node.left.text);
|
|
47
|
+
|
|
48
|
+
if (!list) {
|
|
49
|
+
list = [];
|
|
50
|
+
result.set(node.left.text, list);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
list.push({ call, target: node.left });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
node.forEachChild(visit);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
visit(file);
|
|
61
|
+
cache.set(file.fileName, result);
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cachesOf(program: ts.Program): Caches {
|
|
67
|
+
let found = caches.get(program);
|
|
68
|
+
|
|
69
|
+
if (!found) {
|
|
70
|
+
found = { arrays: new Map(), assignments: new Map(), kinds: new Map() };
|
|
71
|
+
caches.set(program, found);
|
|
16
72
|
}
|
|
17
73
|
|
|
18
|
-
return
|
|
74
|
+
return found;
|
|
19
75
|
}
|
|
20
76
|
|
|
21
|
-
|
|
22
|
-
|
|
77
|
+
// Every member of a union must agree: `number | undefined` is a signal, `number[] | undefined` is
|
|
78
|
+
// neither a signal nor an array at runtime
|
|
79
|
+
function kindOf(checker: ts.Checker, type: ts.Type): TYPES | null {
|
|
80
|
+
let members = (type.flags & ts.TypeFlags.Union) !== 0 ? (type as ts.UnionType).getTypes() : [type],
|
|
81
|
+
result: TYPES | null = null;
|
|
82
|
+
|
|
83
|
+
for (let i = 0, n = members.length; i < n; i++) {
|
|
84
|
+
let kind = kindOfType(checker, members[i]);
|
|
23
85
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
86
|
+
if (kind === null || (result !== null && result !== kind)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
result = kind;
|
|
27
91
|
}
|
|
28
92
|
|
|
29
|
-
return
|
|
93
|
+
return result;
|
|
30
94
|
}
|
|
31
95
|
|
|
96
|
+
// Kind of one non-union type as a reactive() argument; null when the type cannot say
|
|
97
|
+
function kindOfType(checker: ts.Checker, type: ts.Type): TYPES | null {
|
|
98
|
+
if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) {
|
|
99
|
+
let constraint = checker.getBaseConstraintOfType(type);
|
|
32
100
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
paths: new Map(),
|
|
40
|
-
symbols: new Map()
|
|
41
|
-
});
|
|
101
|
+
return constraint && constraint !== type ? kindOf(checker, constraint) : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Never | ts.TypeFlags.Unknown)) !== 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
42
107
|
|
|
43
|
-
|
|
44
|
-
|
|
108
|
+
if ((type.flags & (
|
|
109
|
+
ts.TypeFlags.BigIntLike |
|
|
110
|
+
ts.TypeFlags.BooleanLike |
|
|
111
|
+
ts.TypeFlags.EnumLike |
|
|
112
|
+
ts.TypeFlags.ESSymbolLike |
|
|
113
|
+
ts.TypeFlags.Null |
|
|
114
|
+
ts.TypeFlags.NumberLike |
|
|
115
|
+
ts.TypeFlags.StringLike |
|
|
116
|
+
ts.TypeFlags.TemplateLiteral |
|
|
117
|
+
ts.TypeFlags.Undefined |
|
|
118
|
+
ts.TypeFlags.Void
|
|
119
|
+
)) !== 0) {
|
|
120
|
+
return TYPES.Signal;
|
|
121
|
+
}
|
|
45
122
|
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
bindings.symbols.set(symbol, type);
|
|
123
|
+
if (checker.isArrayType(type) || checker.isTupleType(type)) {
|
|
124
|
+
return TYPES.Array;
|
|
49
125
|
}
|
|
50
|
-
};
|
|
51
126
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
127
|
+
if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0) {
|
|
128
|
+
return TYPES.Computed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (type.flags & (ts.TypeFlags.Intersection | ts.TypeFlags.NonPrimitive | ts.TypeFlags.Object)) !== 0 ? TYPES.Object : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
// Expressions whose type is a ReactiveArray (`Reactive<T[]>` carries the class's members), resolved
|
|
137
|
+
// in one checker round-trip. Found by type, a reactive array is recognized wherever it flows:
|
|
138
|
+
// imports, object properties, parameters, return values.
|
|
139
|
+
const arrays = (bindings: Bindings, nodes: ts.Node[]): Set<ts.Node> => {
|
|
140
|
+
let result = new Set<ts.Node>();
|
|
55
141
|
|
|
56
|
-
if (
|
|
57
|
-
return;
|
|
142
|
+
if (nodes.length === 0) {
|
|
143
|
+
return result;
|
|
58
144
|
}
|
|
59
145
|
|
|
60
|
-
let
|
|
146
|
+
let files = new Set(references.exported(bindings.checker, bindings.program, PACKAGE_NAME, REACTIVE_ARRAY).map(declaration => declaration.path));
|
|
61
147
|
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
bindings.paths.set(symbol, paths);
|
|
148
|
+
if (files.size === 0) {
|
|
149
|
+
return result;
|
|
65
150
|
}
|
|
66
151
|
|
|
67
|
-
bindings.
|
|
68
|
-
|
|
152
|
+
let cache = cachesOf(bindings.program).arrays,
|
|
153
|
+
types = bindings.checker.getTypeAtLocation(nodes);
|
|
154
|
+
|
|
155
|
+
for (let i = 0, n = nodes.length; i < n; i++) {
|
|
156
|
+
let type = types[i];
|
|
157
|
+
|
|
158
|
+
if (!type) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let branded = cache.get(type.id);
|
|
163
|
+
|
|
164
|
+
if (branded === undefined) {
|
|
165
|
+
let property = bindings.checker.getPropertyOfType(bindings.checker.getNonNullableType(type) ?? type, ARRAY_BRAND);
|
|
166
|
+
|
|
167
|
+
branded = property?.declarations?.some(declaration => files.has(declaration.path)) === true;
|
|
168
|
+
cache.set(type.id, branded);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (branded) {
|
|
172
|
+
result.add(nodes[i]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result;
|
|
69
177
|
};
|
|
70
178
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
179
|
+
// Kind of a reactive() call from its argument: literal syntax first, the argument's type otherwise.
|
|
180
|
+
// Null when no kind is provable (no/extra arguments, `any`, `unknown`, a mixed union).
|
|
181
|
+
const classify = (bindings: Bindings, call: ts.CallExpression): TYPES | null => {
|
|
182
|
+
let arg = call.arguments[0];
|
|
183
|
+
|
|
184
|
+
if (!arg || call.arguments.length !== 1 || ts.isSpreadElement(arg)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let value = unwrap(arg);
|
|
189
|
+
|
|
190
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
|
|
191
|
+
return TYPES.Computed;
|
|
75
192
|
}
|
|
76
193
|
|
|
77
|
-
|
|
194
|
+
if (ts.isArrayLiteralExpression(value)) {
|
|
195
|
+
return TYPES.Array;
|
|
196
|
+
}
|
|
78
197
|
|
|
79
|
-
if (
|
|
80
|
-
return
|
|
198
|
+
if (ts.isObjectLiteralExpression(value)) {
|
|
199
|
+
return TYPES.Object;
|
|
81
200
|
}
|
|
82
201
|
|
|
83
|
-
let
|
|
202
|
+
let type = bindings.checker.getTypeAtLocation(arg);
|
|
203
|
+
|
|
204
|
+
return type ? kindOf(bindings.checker, type) : null;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const create = (checker: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile): Bindings => ({
|
|
208
|
+
checker,
|
|
209
|
+
entries: new Set(references.exported(checker, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
|
|
210
|
+
origins: references.origins(checker, program, sourceFile),
|
|
211
|
+
program,
|
|
212
|
+
sourceFile
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Whether a callee expression denotes `reactive`: `reactive`, an alias, `ns.reactive`, `ns['reactive']`
|
|
216
|
+
const denotes = (bindings: Bindings, expression: ts.Expression): boolean => {
|
|
217
|
+
return references.denotes(bindings.checker, bindings.program, expression, bindings.entries);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Whether a value holds `reactive`: the export itself, reached through any import, re-export or
|
|
221
|
+
// namespace, or a const binding (`const r = reactive`, `const { reactive: r } = ns`) holding it
|
|
222
|
+
const holds = (bindings: Bindings, found: Origin | null): boolean => {
|
|
223
|
+
return references.holds(bindings.checker, bindings.program, found, bindings.entries);
|
|
224
|
+
};
|
|
84
225
|
|
|
85
|
-
|
|
226
|
+
const isReactiveCall = (bindings: Bindings, node: ts.Node): node is ts.CallExpression => {
|
|
227
|
+
return ts.isCallExpression(node) && denotes(bindings, node.expression);
|
|
86
228
|
};
|
|
87
229
|
|
|
88
|
-
|
|
89
|
-
|
|
230
|
+
// Reactive kind of the binding a declaration introduces, wherever it is read from: a variable
|
|
231
|
+
// initialized (or assigned) with reactive(), or a module's `export default reactive(...)`.
|
|
232
|
+
// Ambient declarations (.d.ts) never are: a published package's signals ship as Signal-typed.
|
|
233
|
+
const kind = (bindings: Bindings, found: Origin | null): TYPES | null => {
|
|
234
|
+
if (!found) {
|
|
90
235
|
return null;
|
|
91
236
|
}
|
|
92
237
|
|
|
93
|
-
let
|
|
94
|
-
|
|
238
|
+
let declaration = found.declaration;
|
|
239
|
+
|
|
240
|
+
if (
|
|
241
|
+
(declaration.kind !== ts.SyntaxKind.VariableDeclaration && declaration.kind !== ts.SyntaxKind.ExportAssignment) ||
|
|
242
|
+
declaration.path.endsWith('.d.ts')
|
|
243
|
+
) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let cache = cachesOf(bindings.program).kinds,
|
|
248
|
+
key = references.key(declaration),
|
|
249
|
+
known = cache.get(key);
|
|
250
|
+
|
|
251
|
+
if (known !== undefined) {
|
|
252
|
+
return known;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
cache.set(key, null);
|
|
256
|
+
|
|
257
|
+
let node = declaration.resolve() as ts.Node | undefined,
|
|
258
|
+
result: TYPES | null = null;
|
|
259
|
+
|
|
260
|
+
if (node && ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
261
|
+
let initializer = node.initializer && unwrap(node.initializer);
|
|
262
|
+
|
|
263
|
+
if (initializer && isReactiveCall(bindings, initializer)) {
|
|
264
|
+
result = classify(bindings, initializer);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
let candidates = assignments(bindings, node.getSourceFile()).get(node.name.text) ?? [];
|
|
268
|
+
|
|
269
|
+
for (let i = 0, n = candidates.length; i < n && result === null; i++) {
|
|
270
|
+
let { call, target } = candidates[i],
|
|
271
|
+
assigned = origin(bindings, target);
|
|
272
|
+
|
|
273
|
+
if (assigned && references.key(assigned.declaration) === key && isReactiveCall(bindings, call)) {
|
|
274
|
+
result = classify(bindings, call);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (node && ts.isExportAssignment(node)) {
|
|
280
|
+
let expression = unwrap(node.expression);
|
|
281
|
+
|
|
282
|
+
if (isReactiveCall(bindings, expression)) {
|
|
283
|
+
result = classify(bindings, expression);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
cache.set(key, result);
|
|
288
|
+
|
|
289
|
+
return result;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const origin = (bindings: Bindings, node: ts.Node): Origin | null => {
|
|
293
|
+
return bindings.origins.get(node) ?? references.origin(bindings.checker, bindings.program, node);
|
|
294
|
+
};
|
|
295
|
+
|
|
95
296
|
|
|
96
|
-
|
|
297
|
+
// The expression a value comes from, without parentheses, `!` and type assertions
|
|
298
|
+
const unwrap = (expression: ts.Expression): ts.Expression => {
|
|
299
|
+
return references.unwrap(expression) as ts.Expression;
|
|
97
300
|
};
|
|
98
301
|
|
|
99
302
|
|
|
100
|
-
export default { create,
|
|
303
|
+
export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, unwrap };
|
package/src/compiler/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { imports } from '@esportsplus/typescript/compiler';
|
|
3
2
|
import type { ImportIntent, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
|
|
4
3
|
import { ENTRYPOINT, NAMESPACE, PACKAGE_NAME } from './constants';
|
|
4
|
+
import type { Failure } from './primitives';
|
|
5
5
|
|
|
6
6
|
import array from './array';
|
|
7
7
|
import object from './object';
|
|
@@ -9,81 +9,170 @@ import primitives from './primitives';
|
|
|
9
9
|
import scope from './bindings';
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// Every use the compiler cannot lower fails the build, all of them reported at once: shipping one
|
|
13
|
+
// would leave an uncompiled reactive() or an unread signal in the output
|
|
14
|
+
function failed(sourceFile: ts.SourceFile, failures: Failure[]): Error {
|
|
15
|
+
let lines: string[] = [];
|
|
16
|
+
|
|
17
|
+
for (let i = 0, n = failures.length; i < n; i++) {
|
|
18
|
+
let { message, node } = failures[i],
|
|
19
|
+
position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
20
|
+
|
|
21
|
+
lines.push(` ${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}: ${node.getText(sourceFile).split('\n')[0]}`);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
return new Error(`${PACKAGE_NAME}: code that must be compiled cannot be:\n${lines.join('\n')}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isConstList(node: ts.Node | undefined): boolean {
|
|
28
|
+
return node !== undefined && ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.Const) !== 0;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
// A use of `reactive` the compiler lowers: a callee, or a const alias whose own uses are sites
|
|
32
|
+
function isLowered(site: ts.Node): boolean {
|
|
33
|
+
let parent = site.parent;
|
|
34
|
+
|
|
35
|
+
while (parent && ts.isParenthesizedExpression(parent)) {
|
|
36
|
+
site = parent;
|
|
37
|
+
parent = site.parent;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!parent) {
|
|
23
41
|
return false;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
|
-
|
|
44
|
+
if (ts.isCallExpression(parent)) {
|
|
45
|
+
return parent.expression === site;
|
|
46
|
+
}
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
48
|
+
if (ts.isVariableDeclaration(parent)) {
|
|
49
|
+
// `const r = reactive` only: destructuring would pull a member out of the compiled value
|
|
50
|
+
return parent.initializer === site && ts.isIdentifier(parent.name) && isConstList(parent.parent);
|
|
31
51
|
}
|
|
32
52
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
if (ts.isBindingElement(parent) && parent.propertyName === site) {
|
|
54
|
+
let declaration: ts.Node | undefined = parent;
|
|
55
|
+
|
|
56
|
+
while (declaration && !ts.isVariableDeclaration(declaration)) {
|
|
57
|
+
declaration = declaration.parent;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return declaration !== undefined && isConstList(declaration.parent);
|
|
36
61
|
}
|
|
37
62
|
|
|
38
63
|
return false;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
|
|
67
|
+
// `reactive`, or the member access naming it (`ns.reactive`, `ns['reactive']`)
|
|
68
|
+
function siteOf(identifier: ts.Node): ts.Node {
|
|
69
|
+
let parent = identifier.parent!;
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
(ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
|
|
73
|
+
(ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)
|
|
74
|
+
) {
|
|
75
|
+
return parent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return identifier;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
// No text patterns: a module can reach `reactive`, or read a reactive binding, under any name
|
|
83
|
+
// (a barrel's rename, a default re-export, a namespace), so every module is resolved against the
|
|
84
|
+
// program and compiled exactly where its values resolve to reactive declarations
|
|
42
85
|
export default {
|
|
43
|
-
patterns: ['reactive(', 'reactive<'],
|
|
44
86
|
transform: (ctx: TransformContext) => {
|
|
45
|
-
let checker = ctx
|
|
87
|
+
let { checker, program, sourceFile } = ctx,
|
|
88
|
+
bindings = scope.create(checker, program, sourceFile),
|
|
89
|
+
calls = new Set<ts.Node>(),
|
|
90
|
+
dependencies = new Set<string>(),
|
|
91
|
+
failures: Failure[] = [],
|
|
92
|
+
self = sourceFile.fileName.toLowerCase();
|
|
93
|
+
|
|
94
|
+
for (let [identifier, found] of bindings.origins) {
|
|
95
|
+
let entry = scope.holds(bindings, found);
|
|
96
|
+
|
|
97
|
+
if (!entry && scope.kind(bindings, found) === null) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (let file of [...found.through, found.declaration.path]) {
|
|
102
|
+
if (file.toLowerCase() !== self) {
|
|
103
|
+
dependencies.add(file);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
46
106
|
|
|
47
|
-
|
|
48
|
-
|
|
107
|
+
if (!entry) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let site = siteOf(identifier),
|
|
112
|
+
parent = site.parent;
|
|
113
|
+
|
|
114
|
+
while (parent && ts.isParenthesizedExpression(parent)) {
|
|
115
|
+
parent = parent.parent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (parent && ts.isCallExpression(parent) && scope.unwrap(parent.expression) === site) {
|
|
119
|
+
calls.add(parent);
|
|
120
|
+
}
|
|
121
|
+
else if (!isLowered(site)) {
|
|
122
|
+
failures.push({
|
|
123
|
+
message: `${ENTRYPOINT} is compiled away, so it can only be called (or aliased with const); it cannot be used as a value`,
|
|
124
|
+
node: site
|
|
125
|
+
});
|
|
126
|
+
}
|
|
49
127
|
}
|
|
50
128
|
|
|
51
|
-
let
|
|
52
|
-
candidates = new Set<ts.Node>(),
|
|
129
|
+
let isReactiveCall = (node: ts.Node): node is ts.CallExpression => calls.has(node),
|
|
53
130
|
intents = {
|
|
54
131
|
imports: [] as ImportIntent[],
|
|
55
132
|
prepend: [] as string[],
|
|
56
133
|
replacements: [] as ReplacementIntent[]
|
|
57
|
-
}
|
|
58
|
-
sourceFile = ctx.sourceFile;
|
|
134
|
+
};
|
|
59
135
|
|
|
60
|
-
|
|
136
|
+
// Signals and computeds first: every call is classified and every binding read is rewritten
|
|
137
|
+
let lowered = primitives(sourceFile, bindings, isReactiveCall);
|
|
61
138
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let isReactiveCall = (node: ts.Node): node is ts.CallExpression => candidates.has(node);
|
|
67
|
-
|
|
68
|
-
// Run primitives transform first (tracks bindings for signal/computed, collects every call)
|
|
69
|
-
let { calls, replacements } = primitives(sourceFile, bindings, isReactiveCall);
|
|
139
|
+
failures.push(...lowered.failures);
|
|
140
|
+
intents.replacements.push(...lowered.replacements);
|
|
70
141
|
|
|
71
|
-
intents.replacements.push(...replacements);
|
|
72
|
-
|
|
73
|
-
// Run object transform
|
|
74
142
|
let objects = object(sourceFile, bindings, isReactiveCall);
|
|
75
143
|
|
|
76
144
|
intents.prepend.push(...objects.prepend);
|
|
77
145
|
intents.replacements.push(...objects.replacements);
|
|
78
146
|
|
|
79
|
-
|
|
80
|
-
|
|
147
|
+
let arrays = array(sourceFile, bindings, isReactiveCall);
|
|
148
|
+
|
|
149
|
+
failures.push(...arrays.failures);
|
|
150
|
+
intents.replacements.push(...arrays.replacements);
|
|
151
|
+
|
|
152
|
+
// An array is reactive because of where it was declared; that module shapes this compile
|
|
153
|
+
for (let i = 0, n = arrays.reactive.length; i < n; i++) {
|
|
154
|
+
let root = arrays.reactive[i];
|
|
81
155
|
|
|
82
|
-
|
|
156
|
+
while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root) || ts.isParenthesizedExpression(root)) {
|
|
157
|
+
root = root.expression;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let found = ts.isIdentifier(root) ? scope.origin(bindings, root) : null;
|
|
161
|
+
|
|
162
|
+
if (found && found.declaration.path.toLowerCase() !== self) {
|
|
163
|
+
dependencies.add(found.declaration.path);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (failures.length > 0) {
|
|
168
|
+
throw failed(sourceFile, failures);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// An object literal the object transform cannot specialize (a spread) is built by the runtime
|
|
83
172
|
let transformed = new Set(intents.replacements.map(r => r.node));
|
|
84
173
|
|
|
85
|
-
for (let i = 0, n = calls.length; i < n; i++) {
|
|
86
|
-
let call = calls[i];
|
|
174
|
+
for (let i = 0, n = lowered.calls.length; i < n; i++) {
|
|
175
|
+
let call = lowered.calls[i];
|
|
87
176
|
|
|
88
177
|
if (transformed.has(call) || transformed.has(call.expression)) {
|
|
89
178
|
continue;
|
|
@@ -95,12 +184,19 @@ export default {
|
|
|
95
184
|
});
|
|
96
185
|
}
|
|
97
186
|
|
|
98
|
-
intents.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
187
|
+
if (intents.replacements.length === 0 && intents.prepend.length === 0) {
|
|
188
|
+
return dependencies.size > 0 ? { dependencies: [...dependencies] } : {};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// `.length` / element-write rewrites alone call methods on the array itself
|
|
192
|
+
if (calls.size > 0 || lowered.replacements.length > 0 || intents.prepend.length > 0 || arrays.namespaced) {
|
|
193
|
+
intents.imports.push({
|
|
194
|
+
namespace: NAMESPACE,
|
|
195
|
+
package: PACKAGE_NAME,
|
|
196
|
+
remove: [ENTRYPOINT]
|
|
197
|
+
});
|
|
198
|
+
}
|
|
103
199
|
|
|
104
|
-
return intents;
|
|
200
|
+
return { dependencies: [...dependencies], ...intents };
|
|
105
201
|
}
|
|
106
202
|
};
|