@esportsplus/reactivity 0.34.0 → 0.36.1
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/README.md +9 -7
- package/bench/reactive/array.bench.ts +17 -17
- package/build/compiler/array.d.ts +2 -2
- package/build/compiler/array.js +10 -52
- package/build/compiler/constants.d.ts +4 -4
- package/build/compiler/constants.js +20 -4
- package/build/compiler/index.js +25 -38
- package/build/compiler/object.d.ts +2 -2
- package/build/compiler/object.js +5 -16
- package/build/compiler/primitives.d.ts +6 -2
- package/build/compiler/primitives.js +31 -38
- package/build/compiler/types.d.ts +3 -1
- package/build/constants.d.ts +2 -2
- package/build/constants.js +2 -2
- package/build/reactive/array.d.ts +1 -1
- package/build/reactive/array.js +26 -15
- package/build/reactive/index.js +5 -14
- package/build/reactive/object.js +1 -1
- package/build/system.d.ts +1 -4
- package/build/system.js +105 -83
- package/build/types.d.ts +2 -7
- package/package.json +5 -5
- package/pnpm-workspace.yaml +2 -1
- package/src/compiler/array.ts +12 -62
- package/src/compiler/constants.ts +21 -5
- package/src/compiler/index.ts +38 -69
- package/src/compiler/object.ts +7 -24
- package/src/compiler/primitives.ts +51 -49
- package/src/compiler/types.ts +4 -1
- package/src/constants.ts +4 -4
- package/src/reactive/array.ts +33 -17
- package/src/reactive/index.ts +5 -17
- package/src/reactive/object.ts +1 -1
- package/src/system.ts +138 -101
- package/src/types.ts +2 -9
- package/test/async-computed.test.ts +2 -2
- package/test/compiler/compiler.test.ts +38 -22
- package/test/effects.test.ts +37 -1
- package/test/lib/uncaught.ts +26 -0
- package/test/reactive/array.test.ts +169 -99
- package/test/reactive/nested.test.ts +14 -14
- package/test/reactive/objects.test.ts +1 -1
- package/test/reactive/reactive.test.ts +49 -0
- package/test/system.test.ts +130 -22
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# @esportsplus/reactivity
|
|
2
2
|
|
|
3
|
-
Project doc bundle: [docs/index.md](./docs/index.md) — rejected dead-ends, skip reasons, completed-work index.
|
|
4
|
-
|
|
5
3
|
A fine-grained reactivity system with compile-time transformations. Write reactive code with natural JavaScript syntax while the compiler generates optimized signal-based code.
|
|
6
4
|
|
|
7
5
|
## Installation
|
|
@@ -223,7 +221,7 @@ let user = new ReactiveObject_1();
|
|
|
223
221
|
| `reactive(() => expr)` | Creates a computed value (compile-time only) |
|
|
224
222
|
| `reactive({...})` | Creates a reactive object with signals and computeds |
|
|
225
223
|
| `reactive([...])` | Creates a reactive array |
|
|
226
|
-
| `effect(fn,
|
|
224
|
+
| `effect(fn, apply?)` | Runs a function that re-executes when dependencies change. Returns a dispose function. Optional `apply(value, prev)` runs untracked after each run with `fn`'s return value |
|
|
227
225
|
| `root(fn)` | Creates an untracked scope. If `fn` accepts an argument, a dispose function is provided |
|
|
228
226
|
| `onCleanup(fn)` | Registers a cleanup function for the current effect/computed |
|
|
229
227
|
|
|
@@ -259,7 +257,7 @@ For advanced use cases, the underlying classes are exported:
|
|
|
259
257
|
|
|
260
258
|
| Class | Description |
|
|
261
259
|
|-------|-------------|
|
|
262
|
-
| `ReactiveArray<T>` | Array subclass with reactivity and event dispatching |
|
|
260
|
+
| `ReactiveArray<T>` | Array subclass with reactivity and event dispatching. `new ReactiveArray(items?: T[])` takes the initial items as one array |
|
|
263
261
|
| `ReactiveObject<T>` | Base class for reactive objects |
|
|
264
262
|
|
|
265
263
|
### Constants
|
|
@@ -283,7 +281,6 @@ Symbol constants for type identification:
|
|
|
283
281
|
| `SelectorSignal<T>` | Signal variant backing `signal.selector` per-key subscriptions |
|
|
284
282
|
| `Link` | Dependency graph link between nodes |
|
|
285
283
|
| `Reactive<T>` | Utility type for inferring reactive object/array types |
|
|
286
|
-
| `TransformResult` | Compiler transform output metadata |
|
|
287
284
|
|
|
288
285
|
## ReactiveArray
|
|
289
286
|
|
|
@@ -341,8 +338,13 @@ pnpm bench # vitest bench --run
|
|
|
341
338
|
`agent:test` (`tsc --noEmit && vitest run`) and `agent:bench` (`vitest bench --run`) are the
|
|
342
339
|
CI-facing aliases of the same build/test/bench commands.
|
|
343
340
|
|
|
341
|
+
## Changes in 0.35.0
|
|
342
|
+
|
|
343
|
+
- `new ReactiveArray(items?)` takes an array instead of variadic items. `new ReactiveArray(5)` previously produced a length-5 holey array, and spreading large arrays overflowed the call stack.
|
|
344
|
+
- `root.disposables` is removed. `reactive()` now always registers its disposal with the enclosing effect or root scope.
|
|
345
|
+
- `ReactiveArray` listener errors are still detached but are now rethrown on a microtask instead of swallowed.
|
|
346
|
+
- `TransformResult` is no longer exported.
|
|
347
|
+
|
|
344
348
|
## License
|
|
345
349
|
|
|
346
350
|
MIT
|
|
347
|
-
|
|
348
|
-
<!-- claude-code:readme-source-hash: 189167a17e9be8c6 -->
|
|
@@ -9,7 +9,7 @@ describe('ReactiveArray creation', () => {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
bench('create with 10 items', () => {
|
|
12
|
-
new ReactiveArray(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
|
|
12
|
+
new ReactiveArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
bench('create with 100 items', () => {
|
|
@@ -19,7 +19,7 @@ describe('ReactiveArray creation', () => {
|
|
|
19
19
|
items.push(i);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
new ReactiveArray(
|
|
22
|
+
new ReactiveArray(items);
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -56,7 +56,7 @@ describe('ReactiveArray push', () => {
|
|
|
56
56
|
|
|
57
57
|
describe('ReactiveArray pop', () => {
|
|
58
58
|
bench('pop', () => {
|
|
59
|
-
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
59
|
+
let arr = new ReactiveArray([1, 2, 3, 4, 5]);
|
|
60
60
|
|
|
61
61
|
arr.pop();
|
|
62
62
|
});
|
|
@@ -65,19 +65,19 @@ describe('ReactiveArray pop', () => {
|
|
|
65
65
|
|
|
66
66
|
describe('ReactiveArray splice', () => {
|
|
67
67
|
bench('splice remove 1', () => {
|
|
68
|
-
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
68
|
+
let arr = new ReactiveArray([1, 2, 3, 4, 5]);
|
|
69
69
|
|
|
70
70
|
arr.splice(2, 1);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
bench('splice insert 1', () => {
|
|
74
|
-
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
74
|
+
let arr = new ReactiveArray([1, 2, 3, 4, 5]);
|
|
75
75
|
|
|
76
76
|
arr.splice(2, 0, 99);
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
bench('splice replace 1', () => {
|
|
80
|
-
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
80
|
+
let arr = new ReactiveArray([1, 2, 3, 4, 5]);
|
|
81
81
|
|
|
82
82
|
arr.splice(2, 1, 99);
|
|
83
83
|
});
|
|
@@ -86,7 +86,7 @@ describe('ReactiveArray splice', () => {
|
|
|
86
86
|
|
|
87
87
|
describe('ReactiveArray sort', () => {
|
|
88
88
|
bench('sort 10 items', () => {
|
|
89
|
-
let arr = new ReactiveArray(5, 3, 8, 1, 9, 2, 7, 4, 6, 10);
|
|
89
|
+
let arr = new ReactiveArray([5, 3, 8, 1, 9, 2, 7, 4, 6, 10]);
|
|
90
90
|
|
|
91
91
|
arr.sort((a, b) => a - b);
|
|
92
92
|
});
|
|
@@ -98,7 +98,7 @@ describe('ReactiveArray sort', () => {
|
|
|
98
98
|
items.push(i);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
let arr = new ReactiveArray(
|
|
101
|
+
let arr = new ReactiveArray(items);
|
|
102
102
|
|
|
103
103
|
arr.sort((a, b) => a - b);
|
|
104
104
|
});
|
|
@@ -110,7 +110,7 @@ describe('ReactiveArray sort', () => {
|
|
|
110
110
|
items.push(i);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
let arr = new ReactiveArray(
|
|
113
|
+
let arr = new ReactiveArray(items);
|
|
114
114
|
|
|
115
115
|
arr.sort((a, b) => a - b);
|
|
116
116
|
});
|
|
@@ -119,13 +119,13 @@ describe('ReactiveArray sort', () => {
|
|
|
119
119
|
|
|
120
120
|
describe('ReactiveArray $set', () => {
|
|
121
121
|
bench('$set', () => {
|
|
122
|
-
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
122
|
+
let arr = new ReactiveArray([1, 2, 3, 4, 5]);
|
|
123
123
|
|
|
124
124
|
arr.$set(2, 99);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
bench('$set same value (no-op)', () => {
|
|
128
|
-
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
128
|
+
let arr = new ReactiveArray([1, 2, 3, 4, 5]);
|
|
129
129
|
|
|
130
130
|
arr.$set(2, 3);
|
|
131
131
|
});
|
|
@@ -169,7 +169,7 @@ describe('ReactiveArray dispose/clear', () => {
|
|
|
169
169
|
items.push(i);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
let arr = new ReactiveArray(
|
|
172
|
+
let arr = new ReactiveArray(items);
|
|
173
173
|
|
|
174
174
|
arr.dispose();
|
|
175
175
|
});
|
|
@@ -181,7 +181,7 @@ describe('ReactiveArray dispose/clear', () => {
|
|
|
181
181
|
items.push(i);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
let arr = new ReactiveArray(
|
|
184
|
+
let arr = new ReactiveArray(items);
|
|
185
185
|
|
|
186
186
|
arr.clear();
|
|
187
187
|
});
|
|
@@ -201,13 +201,13 @@ describe('ReactiveArray concat/unshift/shift/reverse', () => {
|
|
|
201
201
|
});
|
|
202
202
|
|
|
203
203
|
bench('unshift 10 items', () => {
|
|
204
|
-
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
204
|
+
let arr = new ReactiveArray([1, 2, 3, 4, 5]);
|
|
205
205
|
|
|
206
206
|
arr.unshift(10, 20, 30, 40, 50, 60, 70, 80, 90, 100);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
bench('shift', () => {
|
|
210
|
-
let arr = new ReactiveArray(1, 2, 3, 4, 5);
|
|
210
|
+
let arr = new ReactiveArray([1, 2, 3, 4, 5]);
|
|
211
211
|
|
|
212
212
|
arr.shift();
|
|
213
213
|
});
|
|
@@ -219,7 +219,7 @@ describe('ReactiveArray concat/unshift/shift/reverse', () => {
|
|
|
219
219
|
items.push(i);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
let arr = new ReactiveArray(
|
|
222
|
+
let arr = new ReactiveArray(items);
|
|
223
223
|
|
|
224
224
|
arr.reverse();
|
|
225
225
|
});
|
|
@@ -228,7 +228,7 @@ describe('ReactiveArray concat/unshift/shift/reverse', () => {
|
|
|
228
228
|
|
|
229
229
|
describe('ReactiveArray reactive length', () => {
|
|
230
230
|
bench('read $length in effect', () => {
|
|
231
|
-
let arr = new ReactiveArray(1, 2, 3);
|
|
231
|
+
let arr = new ReactiveArray([1, 2, 3]);
|
|
232
232
|
|
|
233
233
|
let stop = effect(() => {
|
|
234
234
|
arr.$length;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
3
|
-
import type { Bindings } from './types.js';
|
|
3
|
+
import type { Bindings, IsReactiveCall } from './types.js';
|
|
4
4
|
export default _default;
|
|
5
|
-
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings,
|
|
5
|
+
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): ReplacementIntent[];
|
package/build/compiler/array.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { ast
|
|
3
|
-
import {
|
|
4
|
-
function isReactiveCall(checker, node) {
|
|
5
|
-
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
|
|
6
|
-
return false;
|
|
7
|
-
}
|
|
8
|
-
let expr = node.expression;
|
|
9
|
-
if (checker) {
|
|
10
|
-
return imports.includes(checker, expr, PACKAGE_NAME, ENTRYPOINT);
|
|
11
|
-
}
|
|
12
|
-
return expr.text === ENTRYPOINT;
|
|
13
|
-
}
|
|
2
|
+
import { ast } from '@esportsplus/typescript/compiler';
|
|
3
|
+
import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants.js';
|
|
14
4
|
function getElementTypeText(typeNode, sourceFile) {
|
|
15
5
|
if (ts.isArrayTypeNode(typeNode)) {
|
|
16
6
|
return typeNode.elementType.getText(sourceFile);
|
|
@@ -24,40 +14,8 @@ function getElementTypeText(typeNode, sourceFile) {
|
|
|
24
14
|
}
|
|
25
15
|
return null;
|
|
26
16
|
}
|
|
27
|
-
function getOperator(kind) {
|
|
28
|
-
switch (kind) {
|
|
29
|
-
case ts.SyntaxKind.PlusEqualsToken: return '+';
|
|
30
|
-
case ts.SyntaxKind.MinusEqualsToken: return '-';
|
|
31
|
-
case ts.SyntaxKind.AsteriskEqualsToken: return '*';
|
|
32
|
-
case ts.SyntaxKind.SlashEqualsToken: return '/';
|
|
33
|
-
case ts.SyntaxKind.PercentEqualsToken: return '%';
|
|
34
|
-
case ts.SyntaxKind.AsteriskAsteriskEqualsToken: return '**';
|
|
35
|
-
case ts.SyntaxKind.AmpersandEqualsToken: return '&';
|
|
36
|
-
case ts.SyntaxKind.BarEqualsToken: return '|';
|
|
37
|
-
case ts.SyntaxKind.CaretEqualsToken: return '^';
|
|
38
|
-
default: return '+';
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function isAssignmentOperator(kind) {
|
|
42
|
-
return kind === ts.SyntaxKind.EqualsToken ||
|
|
43
|
-
kind === ts.SyntaxKind.PlusEqualsToken ||
|
|
44
|
-
kind === ts.SyntaxKind.MinusEqualsToken ||
|
|
45
|
-
kind === ts.SyntaxKind.AsteriskEqualsToken ||
|
|
46
|
-
kind === ts.SyntaxKind.SlashEqualsToken ||
|
|
47
|
-
kind === ts.SyntaxKind.PercentEqualsToken ||
|
|
48
|
-
kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken ||
|
|
49
|
-
kind === ts.SyntaxKind.LessThanLessThanEqualsToken ||
|
|
50
|
-
kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken ||
|
|
51
|
-
kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken ||
|
|
52
|
-
kind === ts.SyntaxKind.AmpersandEqualsToken ||
|
|
53
|
-
kind === ts.SyntaxKind.BarEqualsToken ||
|
|
54
|
-
kind === ts.SyntaxKind.CaretEqualsToken ||
|
|
55
|
-
kind === ts.SyntaxKind.BarBarEqualsToken ||
|
|
56
|
-
kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
|
|
57
|
-
kind === ts.SyntaxKind.QuestionQuestionEqualsToken;
|
|
58
|
-
}
|
|
59
17
|
function visit(ctx, node) {
|
|
60
|
-
if (isReactiveCall(
|
|
18
|
+
if (ctx.isReactiveCall(node) && node.arguments.length > 0) {
|
|
61
19
|
let arg = node.arguments[0], expression = ts.isAsExpression(arg) ? arg.expression : arg;
|
|
62
20
|
if (ts.isArrayLiteralExpression(expression)) {
|
|
63
21
|
let elementType = null;
|
|
@@ -74,7 +32,7 @@ function visit(ctx, node) {
|
|
|
74
32
|
ctx.replacements.push({
|
|
75
33
|
node,
|
|
76
34
|
generate: (sf) => expression.elements.length > 0
|
|
77
|
-
? ` new ${NAMESPACE}.ReactiveArray${typeParam}(
|
|
35
|
+
? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${expression.getText(sf)})`
|
|
78
36
|
: ` new ${NAMESPACE}.ReactiveArray${typeParam}()`
|
|
79
37
|
});
|
|
80
38
|
}
|
|
@@ -105,9 +63,9 @@ function visit(ctx, node) {
|
|
|
105
63
|
let name = ast.expression.name(node.expression);
|
|
106
64
|
if (name && ctx.bindings.get(name) === TYPES.Array) {
|
|
107
65
|
let expr = node.expression, parent = node.parent;
|
|
108
|
-
if (parent && ts.isBinaryExpression(parent) && parent.left === node &&
|
|
109
|
-
let op = parent.operatorToken.kind;
|
|
110
|
-
if (op ===
|
|
66
|
+
if (parent && ts.isBinaryExpression(parent) && parent.left === node && (parent.operatorToken.kind === ts.SyntaxKind.EqualsToken || COMPOUND_OPERATORS.has(parent.operatorToken.kind))) {
|
|
67
|
+
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
|
|
68
|
+
if (op === undefined) {
|
|
111
69
|
ctx.replacements.push({
|
|
112
70
|
node: parent,
|
|
113
71
|
generate: (sf) => `${expr.getText(sf)}.$length = ${parent.right.getText(sf)}`
|
|
@@ -116,7 +74,7 @@ function visit(ctx, node) {
|
|
|
116
74
|
else {
|
|
117
75
|
ctx.replacements.push({
|
|
118
76
|
node: parent,
|
|
119
|
-
generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${
|
|
77
|
+
generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} ${parent.right.getText(sf)}`
|
|
120
78
|
});
|
|
121
79
|
}
|
|
122
80
|
}
|
|
@@ -174,10 +132,10 @@ function visit(ctx, node) {
|
|
|
174
132
|
}
|
|
175
133
|
node.forEachChild(n => visit(ctx, n));
|
|
176
134
|
}
|
|
177
|
-
export default (sourceFile, bindings,
|
|
135
|
+
export default (sourceFile, bindings, isReactiveCall) => {
|
|
178
136
|
let ctx = {
|
|
179
137
|
bindings,
|
|
180
|
-
|
|
138
|
+
isReactiveCall,
|
|
181
139
|
replacements: [],
|
|
182
140
|
sourceFile
|
|
183
141
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { ts } from '@esportsplus/typescript';
|
|
2
|
+
declare const COMPOUND_OPERATORS: Map<ts.SyntaxKind, string>;
|
|
1
3
|
declare const ENTRYPOINT = "reactive";
|
|
2
|
-
declare const ENTRYPOINT_REGEX: RegExp;
|
|
3
4
|
declare const NAMESPACE: string;
|
|
4
5
|
declare const TYPES: {
|
|
5
6
|
readonly Array: 0;
|
|
6
7
|
readonly Computed: 1;
|
|
7
|
-
readonly
|
|
8
|
-
readonly Signal: 3;
|
|
8
|
+
readonly Signal: 2;
|
|
9
9
|
};
|
|
10
10
|
type TYPES = typeof TYPES[keyof typeof TYPES];
|
|
11
|
-
export {
|
|
11
|
+
export { COMPOUND_OPERATORS, ENTRYPOINT, NAMESPACE, TYPES };
|
|
12
12
|
export { PACKAGE_NAME } from '../constants.js';
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import { ts } from '@esportsplus/typescript';
|
|
1
2
|
import { uid } from '@esportsplus/typescript/compiler';
|
|
3
|
+
const COMPOUND_OPERATORS = new Map([
|
|
4
|
+
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, '&&'],
|
|
5
|
+
[ts.SyntaxKind.AmpersandEqualsToken, '&'],
|
|
6
|
+
[ts.SyntaxKind.AsteriskAsteriskEqualsToken, '**'],
|
|
7
|
+
[ts.SyntaxKind.AsteriskEqualsToken, '*'],
|
|
8
|
+
[ts.SyntaxKind.BarBarEqualsToken, '||'],
|
|
9
|
+
[ts.SyntaxKind.BarEqualsToken, '|'],
|
|
10
|
+
[ts.SyntaxKind.CaretEqualsToken, '^'],
|
|
11
|
+
[ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, '>>'],
|
|
12
|
+
[ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, '>>>'],
|
|
13
|
+
[ts.SyntaxKind.LessThanLessThanEqualsToken, '<<'],
|
|
14
|
+
[ts.SyntaxKind.MinusEqualsToken, '-'],
|
|
15
|
+
[ts.SyntaxKind.PercentEqualsToken, '%'],
|
|
16
|
+
[ts.SyntaxKind.PlusEqualsToken, '+'],
|
|
17
|
+
[ts.SyntaxKind.QuestionQuestionEqualsToken, '??'],
|
|
18
|
+
[ts.SyntaxKind.SlashEqualsToken, '/']
|
|
19
|
+
]);
|
|
2
20
|
const ENTRYPOINT = 'reactive';
|
|
3
|
-
const ENTRYPOINT_REGEX = /\breactive\b/;
|
|
4
21
|
const NAMESPACE = uid('reactivity');
|
|
5
22
|
const TYPES = {
|
|
6
23
|
Array: 0,
|
|
7
24
|
Computed: 1,
|
|
8
|
-
|
|
9
|
-
Signal: 3
|
|
25
|
+
Signal: 2
|
|
10
26
|
};
|
|
11
|
-
export {
|
|
27
|
+
export { COMPOUND_OPERATORS, ENTRYPOINT, NAMESPACE, TYPES };
|
|
12
28
|
export { PACKAGE_NAME } from '../constants.js';
|
package/build/compiler/index.js
CHANGED
|
@@ -4,11 +4,6 @@ import { ENTRYPOINT, NAMESPACE, PACKAGE_NAME } from './constants.js';
|
|
|
4
4
|
import array from './array.js';
|
|
5
5
|
import object from './object.js';
|
|
6
6
|
import primitives from './primitives.js';
|
|
7
|
-
function findRemainingCalls(checker, sourceFile, transformedNodes) {
|
|
8
|
-
let ctx = { checker, replacements: [], sourceFile, transformedNodes };
|
|
9
|
-
visit(ctx, sourceFile);
|
|
10
|
-
return ctx.replacements;
|
|
11
|
-
}
|
|
12
7
|
function isReactiveCallExpression(checker, node) {
|
|
13
8
|
if (!ts.isCallExpression(node)) {
|
|
14
9
|
return false;
|
|
@@ -22,51 +17,43 @@ function isReactiveCallExpression(checker, node) {
|
|
|
22
17
|
}
|
|
23
18
|
return false;
|
|
24
19
|
}
|
|
25
|
-
function hasReactiveCalls(checker, node) {
|
|
26
|
-
if (isReactiveCallExpression(checker, node)) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
let found = false;
|
|
30
|
-
node.forEachChild(child => {
|
|
31
|
-
if (!found && hasReactiveCalls(checker, child)) {
|
|
32
|
-
found = true;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
return found;
|
|
36
|
-
}
|
|
37
|
-
function visit(ctx, node) {
|
|
38
|
-
if (isReactiveCallExpression(ctx.checker, node) && !ctx.transformedNodes.has(node) && !ctx.transformedNodes.has(node.expression)) {
|
|
39
|
-
ctx.replacements.push({
|
|
40
|
-
generate: () => `${NAMESPACE}.reactive(${node.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
|
|
41
|
-
node
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
node.forEachChild(n => visit(ctx, n));
|
|
45
|
-
}
|
|
46
20
|
export default {
|
|
47
21
|
patterns: ['reactive(', 'reactive<'],
|
|
48
22
|
transform: (ctx) => {
|
|
49
|
-
|
|
23
|
+
let checker = ctx.checker;
|
|
24
|
+
if (!checker) {
|
|
50
25
|
return {};
|
|
51
26
|
}
|
|
52
27
|
let bindings = new Map(), intents = {
|
|
53
28
|
imports: [],
|
|
54
29
|
prepend: [],
|
|
55
30
|
replacements: []
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
31
|
+
}, isReactiveCall = (node) => isReactiveCallExpression(checker, node), sourceFile = ctx.sourceFile;
|
|
32
|
+
let { calls, replacements } = primitives(sourceFile, bindings, isReactiveCall);
|
|
33
|
+
if (calls.length === 0) {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
60
36
|
intents.replacements.push(...replacements);
|
|
61
|
-
|
|
62
|
-
intents.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
37
|
+
let objects = object(sourceFile, bindings, isReactiveCall);
|
|
38
|
+
intents.prepend.push(...objects.prepend);
|
|
39
|
+
intents.replacements.push(...objects.replacements);
|
|
40
|
+
intents.replacements.push(...array(sourceFile, bindings, isReactiveCall));
|
|
41
|
+
let transformed = new Set(intents.replacements.map(r => r.node));
|
|
42
|
+
for (let i = 0, n = calls.length; i < n; i++) {
|
|
43
|
+
let call = calls[i];
|
|
44
|
+
if (transformed.has(call) || transformed.has(call.expression)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
intents.replacements.push({
|
|
48
|
+
generate: () => `${NAMESPACE}.reactive(${call.arguments.map(a => a.getText(sourceFile)).join(', ')})`,
|
|
49
|
+
node: call
|
|
68
50
|
});
|
|
69
51
|
}
|
|
52
|
+
intents.imports.push({
|
|
53
|
+
namespace: NAMESPACE,
|
|
54
|
+
package: PACKAGE_NAME,
|
|
55
|
+
remove: [ENTRYPOINT]
|
|
56
|
+
});
|
|
70
57
|
return intents;
|
|
71
58
|
}
|
|
72
59
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
3
|
-
import type { Bindings } from './types.js';
|
|
3
|
+
import type { Bindings, IsReactiveCall } from './types.js';
|
|
4
4
|
type ObjectTransformResult = {
|
|
5
5
|
prepend: string[];
|
|
6
6
|
replacements: ReplacementIntent[];
|
|
7
7
|
};
|
|
8
8
|
export default _default;
|
|
9
|
-
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings,
|
|
9
|
+
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): ObjectTransformResult;
|
package/build/compiler/object.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { code,
|
|
3
|
-
import {
|
|
2
|
+
import { code, uid } from '@esportsplus/typescript/compiler';
|
|
3
|
+
import { NAMESPACE, TYPES } from './constants.js';
|
|
4
4
|
function analyzeProperty(prop, sourceFile) {
|
|
5
5
|
if (!ts.isPropertyAssignment(prop)) {
|
|
6
6
|
return null;
|
|
@@ -139,24 +139,13 @@ function isStaticValue(node) {
|
|
|
139
139
|
node.kind === ts.SyntaxKind.UndefinedKeyword ||
|
|
140
140
|
(ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand));
|
|
141
141
|
}
|
|
142
|
-
function isReactiveCall(checker, node) {
|
|
143
|
-
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
let expr = node.expression;
|
|
147
|
-
if (checker) {
|
|
148
|
-
return imports.includes(checker, expr, PACKAGE_NAME, ENTRYPOINT);
|
|
149
|
-
}
|
|
150
|
-
return expr.text === ENTRYPOINT;
|
|
151
|
-
}
|
|
152
142
|
function visit(ctx, node) {
|
|
153
|
-
if (isReactiveCall(
|
|
143
|
+
if (ctx.isReactiveCall(node)) {
|
|
154
144
|
let arg = node.arguments[0];
|
|
155
145
|
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
156
146
|
let properties = [], props = arg.properties, varname = null;
|
|
157
147
|
if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
|
|
158
148
|
varname = node.parent.name.text;
|
|
159
|
-
ctx.bindings.set(varname, TYPES.Object);
|
|
160
149
|
}
|
|
161
150
|
for (let i = 0, n = props.length; i < n; i++) {
|
|
162
151
|
let prop = props[i];
|
|
@@ -185,11 +174,11 @@ function visit(ctx, node) {
|
|
|
185
174
|
}
|
|
186
175
|
node.forEachChild(n => visit(ctx, n));
|
|
187
176
|
}
|
|
188
|
-
export default (sourceFile, bindings,
|
|
177
|
+
export default (sourceFile, bindings, isReactiveCall) => {
|
|
189
178
|
let ctx = {
|
|
190
179
|
bindings,
|
|
191
180
|
calls: [],
|
|
192
|
-
|
|
181
|
+
isReactiveCall,
|
|
193
182
|
sourceFile
|
|
194
183
|
};
|
|
195
184
|
visit(ctx, sourceFile);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
3
|
-
import type { Bindings } from './types.js';
|
|
3
|
+
import type { Bindings, IsReactiveCall } from './types.js';
|
|
4
|
+
type PrimitivesTransformResult = {
|
|
5
|
+
calls: ts.CallExpression[];
|
|
6
|
+
replacements: ReplacementIntent[];
|
|
7
|
+
};
|
|
4
8
|
export default _default;
|
|
5
|
-
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall:
|
|
9
|
+
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): PrimitivesTransformResult;
|
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { NAMESPACE, TYPES } from './constants.js';
|
|
3
|
-
const COMPOUND_OPERATORS = new Map([
|
|
4
|
-
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, '&&'],
|
|
5
|
-
[ts.SyntaxKind.AmpersandEqualsToken, '&'],
|
|
6
|
-
[ts.SyntaxKind.AsteriskAsteriskEqualsToken, '**'],
|
|
7
|
-
[ts.SyntaxKind.AsteriskEqualsToken, '*'],
|
|
8
|
-
[ts.SyntaxKind.BarBarEqualsToken, '||'],
|
|
9
|
-
[ts.SyntaxKind.BarEqualsToken, '|'],
|
|
10
|
-
[ts.SyntaxKind.CaretEqualsToken, '^'],
|
|
11
|
-
[ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, '>>'],
|
|
12
|
-
[ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, '>>>'],
|
|
13
|
-
[ts.SyntaxKind.LessThanLessThanEqualsToken, '<<'],
|
|
14
|
-
[ts.SyntaxKind.MinusEqualsToken, '-'],
|
|
15
|
-
[ts.SyntaxKind.PercentEqualsToken, '%'],
|
|
16
|
-
[ts.SyntaxKind.PlusEqualsToken, '+'],
|
|
17
|
-
[ts.SyntaxKind.QuestionQuestionEqualsToken, '??'],
|
|
18
|
-
[ts.SyntaxKind.SlashEqualsToken, '/']
|
|
19
|
-
]);
|
|
2
|
+
import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants.js';
|
|
20
3
|
function inScope(reference, binding) {
|
|
21
4
|
let current = reference;
|
|
22
5
|
while (current) {
|
|
@@ -27,9 +10,34 @@ function inScope(reference, binding) {
|
|
|
27
10
|
}
|
|
28
11
|
return false;
|
|
29
12
|
}
|
|
13
|
+
function isScope(node) {
|
|
14
|
+
return ts.isArrowFunction(node) ||
|
|
15
|
+
ts.isBlock(node) ||
|
|
16
|
+
ts.isCatchClause(node) ||
|
|
17
|
+
ts.isForInStatement(node) ||
|
|
18
|
+
ts.isForOfStatement(node) ||
|
|
19
|
+
ts.isForStatement(node) ||
|
|
20
|
+
ts.isFunctionDeclaration(node) ||
|
|
21
|
+
ts.isFunctionExpression(node) ||
|
|
22
|
+
ts.isSourceFile(node);
|
|
23
|
+
}
|
|
24
|
+
function scopeOf(node) {
|
|
25
|
+
let current = node.parent, depth = 0, scope = node.getSourceFile();
|
|
26
|
+
while (current) {
|
|
27
|
+
if (isScope(current)) {
|
|
28
|
+
if (scope === node.getSourceFile() && !ts.isSourceFile(current)) {
|
|
29
|
+
scope = current;
|
|
30
|
+
}
|
|
31
|
+
depth++;
|
|
32
|
+
}
|
|
33
|
+
current = current.parent;
|
|
34
|
+
}
|
|
35
|
+
return { depth, scope };
|
|
36
|
+
}
|
|
30
37
|
function visit(ctx, node) {
|
|
31
38
|
if (ctx.isReactiveCall(node)) {
|
|
32
39
|
let call = node;
|
|
40
|
+
ctx.calls.push(call);
|
|
33
41
|
if (call.arguments.length > 0) {
|
|
34
42
|
let arg = call.arguments[0], classification = TYPES.Signal;
|
|
35
43
|
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
@@ -64,25 +72,9 @@ function visit(ctx, node) {
|
|
|
64
72
|
varname = call.parent.left.text;
|
|
65
73
|
}
|
|
66
74
|
if (varname) {
|
|
67
|
-
let
|
|
68
|
-
while (current) {
|
|
69
|
-
if (ts.isArrowFunction(current) ||
|
|
70
|
-
ts.isBlock(current) ||
|
|
71
|
-
ts.isForInStatement(current) ||
|
|
72
|
-
ts.isForOfStatement(current) ||
|
|
73
|
-
ts.isForStatement(current) ||
|
|
74
|
-
ts.isFunctionDeclaration(current) ||
|
|
75
|
-
ts.isFunctionExpression(current) ||
|
|
76
|
-
ts.isSourceFile(current)) {
|
|
77
|
-
scope = current;
|
|
78
|
-
}
|
|
79
|
-
current = current.parent;
|
|
80
|
-
}
|
|
81
|
-
if (!scope) {
|
|
82
|
-
scope = call.getSourceFile();
|
|
83
|
-
}
|
|
75
|
+
let { depth, scope } = scopeOf(call);
|
|
84
76
|
ctx.bindings.set(varname, classification);
|
|
85
|
-
ctx.scopedBindings.push({ name: varname, scope, type: classification });
|
|
77
|
+
ctx.scopedBindings.push({ depth, name: varname, scope, type: classification });
|
|
86
78
|
}
|
|
87
79
|
ctx.replacements.push({
|
|
88
80
|
generate: () => classification === TYPES.Computed
|
|
@@ -103,7 +95,7 @@ function visit(ctx, node) {
|
|
|
103
95
|
let bindings = ctx.scopedBindings, binding, name = node.text;
|
|
104
96
|
for (let i = 0, n = bindings.length; i < n; i++) {
|
|
105
97
|
let b = bindings[i];
|
|
106
|
-
if (b.name === name && inScope(node, b)) {
|
|
98
|
+
if (b.name === name && (!binding || b.depth >= binding.depth) && inScope(node, b)) {
|
|
107
99
|
binding = b;
|
|
108
100
|
}
|
|
109
101
|
}
|
|
@@ -183,6 +175,7 @@ function visit(ctx, node) {
|
|
|
183
175
|
export default (sourceFile, bindings, isReactiveCall) => {
|
|
184
176
|
let ctx = {
|
|
185
177
|
bindings,
|
|
178
|
+
calls: [],
|
|
186
179
|
isReactiveCall,
|
|
187
180
|
replacements: [],
|
|
188
181
|
scopedBindings: [],
|
|
@@ -190,5 +183,5 @@ export default (sourceFile, bindings, isReactiveCall) => {
|
|
|
190
183
|
tmpCounter: 0
|
|
191
184
|
};
|
|
192
185
|
visit(ctx, sourceFile);
|
|
193
|
-
return ctx.replacements;
|
|
186
|
+
return { calls: ctx.calls, replacements: ctx.replacements };
|
|
194
187
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ts } from '@esportsplus/typescript';
|
|
1
2
|
import { TYPES } from './constants.js';
|
|
2
3
|
type Bindings = Map<string, TYPES>;
|
|
3
|
-
|
|
4
|
+
type IsReactiveCall = (node: ts.Node) => node is ts.CallExpression;
|
|
5
|
+
export type { Bindings, IsReactiveCall };
|
package/build/constants.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ declare const PACKAGE_NAME = "@esportsplus/reactivity";
|
|
|
3
3
|
declare const REACTIVE_ARRAY: unique symbol;
|
|
4
4
|
declare const REACTIVE_OBJECT: unique symbol;
|
|
5
5
|
declare const SIGNAL: unique symbol;
|
|
6
|
+
declare const STABILIZER_DEFERRED = 4;
|
|
6
7
|
declare const STABILIZER_IDLE = 0;
|
|
7
8
|
declare const STABILIZER_RESCHEDULE = 1;
|
|
8
9
|
declare const STABILIZER_RUNNING = 2;
|
|
9
10
|
declare const STABILIZER_SCHEDULED = 3;
|
|
10
|
-
declare const STATE_NONE = 0;
|
|
11
11
|
declare const STATE_CHECK: number;
|
|
12
12
|
declare const STATE_DIRTY: number;
|
|
13
13
|
declare const STATE_RECOMPUTING: number;
|
|
@@ -16,4 +16,4 @@ declare const STATE_COMPUTED: number;
|
|
|
16
16
|
declare const STATE_ERROR: number;
|
|
17
17
|
declare const STATE_EFFECT: number;
|
|
18
18
|
declare const STATE_NOTIFY_MASK: number;
|
|
19
|
-
export { COMPUTED, PACKAGE_NAME, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP,
|
|
19
|
+
export { COMPUTED, PACKAGE_NAME, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL, STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING };
|