@esportsplus/reactivity 0.28.2 → 0.29.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/build/compiler/array.d.ts +1 -1
- package/build/compiler/array.js +14 -15
- package/build/compiler/index.js +21 -44
- package/build/compiler/object.d.ts +1 -1
- package/build/compiler/object.js +4 -8
- package/build/compiler/primitives.d.ts +5 -0
- package/build/compiler/primitives.js +180 -0
- package/build/reactive/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/compiler/array.ts +22 -31
- package/src/compiler/index.ts +41 -61
- package/src/compiler/object.ts +4 -11
- package/src/compiler/primitives.ts +254 -0
- package/src/reactive/index.ts +3 -2
- package/test/primitives.ts +73 -157
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { ts } from '@esportsplus/typescript';
|
|
3
3
|
import type { Bindings } from '../types.js';
|
|
4
|
-
declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings
|
|
4
|
+
declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings) => ReplacementIntent[];
|
|
5
5
|
export default _default;
|
package/build/compiler/array.js
CHANGED
|
@@ -14,11 +14,11 @@ function getElementTypeText(typeNode, sourceFile) {
|
|
|
14
14
|
}
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
|
-
function isReactiveCall(node) {
|
|
18
|
-
return ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
|
|
19
|
-
}
|
|
20
17
|
function visit(ctx, node) {
|
|
21
|
-
if (ts.isCallExpression(node) &&
|
|
18
|
+
if (ts.isCallExpression(node) &&
|
|
19
|
+
ts.isIdentifier(node.expression) &&
|
|
20
|
+
node.expression.text === 'reactive' &&
|
|
21
|
+
node.arguments.length > 0) {
|
|
22
22
|
let arg = node.arguments[0], expression = ts.isAsExpression(arg) ? arg.expression : arg;
|
|
23
23
|
if (ts.isArrayLiteralExpression(expression)) {
|
|
24
24
|
let elementType = null;
|
|
@@ -45,7 +45,7 @@ function visit(ctx, node) {
|
|
|
45
45
|
ctx.bindings.set(node.name.text, COMPILER_TYPES.Array);
|
|
46
46
|
}
|
|
47
47
|
if (ts.isPropertyAccessExpression(node.initializer)) {
|
|
48
|
-
let path = ast.
|
|
48
|
+
let path = ast.getPropertyPath(node.initializer);
|
|
49
49
|
if (path && ctx.bindings.get(path) === COMPILER_TYPES.Array) {
|
|
50
50
|
ctx.bindings.set(node.name.text, COMPILER_TYPES.Array);
|
|
51
51
|
}
|
|
@@ -62,12 +62,12 @@ function visit(ctx, node) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
let parent = node.parent;
|
|
66
65
|
if (ts.isPropertyAccessExpression(node) &&
|
|
67
66
|
node.name.text === 'length' &&
|
|
68
|
-
(
|
|
69
|
-
ts.
|
|
70
|
-
|
|
67
|
+
(!node.parent ||
|
|
68
|
+
(!(ts.isBinaryExpression(node.parent) && node.parent.left === node) &&
|
|
69
|
+
!ts.isPostfixUnaryExpression(node.parent) &&
|
|
70
|
+
!ts.isPrefixUnaryExpression(node.parent)))) {
|
|
71
71
|
let name = ast.getExpressionName(node.expression);
|
|
72
72
|
if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
|
|
73
73
|
ctx.replacements.push({
|
|
@@ -83,19 +83,18 @@ function visit(ctx, node) {
|
|
|
83
83
|
if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
|
|
84
84
|
ctx.replacements.push({
|
|
85
85
|
node,
|
|
86
|
-
generate: (sf) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
generate: (sf) => `${element.expression.getText(sf)}.$set(
|
|
87
|
+
${element.argumentExpression.getText(sf)},
|
|
88
|
+
${node.right.getText(sf)}
|
|
89
|
+
)`
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
ts.forEachChild(node, n => visit(ctx, n));
|
|
94
94
|
}
|
|
95
|
-
export default (sourceFile, bindings
|
|
95
|
+
export default (sourceFile, bindings) => {
|
|
96
96
|
let ctx = {
|
|
97
97
|
bindings,
|
|
98
|
-
checker,
|
|
99
98
|
replacements: [],
|
|
100
99
|
sourceFile
|
|
101
100
|
};
|
package/build/compiler/index.js
CHANGED
|
@@ -3,30 +3,11 @@ import { imports } from '@esportsplus/typescript/compiler';
|
|
|
3
3
|
import { COMPILER_ENTRYPOINT, COMPILER_NAMESPACE, PACKAGE } from '../constants.js';
|
|
4
4
|
import array from './array.js';
|
|
5
5
|
import object from './object.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (!symbol) {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
15
|
-
symbol = checker.getAliasedSymbol(symbol);
|
|
16
|
-
}
|
|
17
|
-
let declarations = symbol.getDeclarations();
|
|
18
|
-
if (!declarations || declarations.length === 0) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
for (let i = 0, n = declarations.length; i < n; i++) {
|
|
22
|
-
let decl = declarations[i], sourceFile = decl.getSourceFile();
|
|
23
|
-
if (sourceFile.fileName.includes(PACKAGE) || sourceFile.fileName.includes('reactivity')) {
|
|
24
|
-
if (symbol.name === COMPILER_ENTRYPOINT) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
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;
|
|
30
11
|
}
|
|
31
12
|
function isReactiveCallExpression(checker, node) {
|
|
32
13
|
if (!ts.isCallExpression(node)) {
|
|
@@ -38,38 +19,34 @@ function isReactiveCallExpression(checker, node) {
|
|
|
38
19
|
return true;
|
|
39
20
|
}
|
|
40
21
|
if (checker) {
|
|
41
|
-
return
|
|
22
|
+
return imports.inPackage(checker, expr, PACKAGE, COMPILER_ENTRYPOINT);
|
|
42
23
|
}
|
|
43
24
|
}
|
|
44
25
|
if (ts.isPropertyAccessExpression(expr) && expr.name.text === COMPILER_ENTRYPOINT && checker) {
|
|
45
|
-
return
|
|
26
|
+
return imports.inPackage(checker, expr, PACKAGE);
|
|
46
27
|
}
|
|
47
28
|
return false;
|
|
48
29
|
}
|
|
30
|
+
function visit(ctx, node) {
|
|
31
|
+
if (isReactiveCallExpression(ctx.checker, node) && !ctx.transformedNodes.has(node) && !ctx.transformedNodes.has(node.expression)) {
|
|
32
|
+
ctx.replacements.push({
|
|
33
|
+
generate: () => `${COMPILER_NAMESPACE}.reactive(${node.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
|
|
34
|
+
node
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
38
|
+
}
|
|
49
39
|
const plugin = {
|
|
50
40
|
patterns: ['reactive(', 'reactive<'],
|
|
51
41
|
transform: (ctx) => {
|
|
52
|
-
if (!
|
|
42
|
+
if (!imports.find(ctx.sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT))) {
|
|
53
43
|
return {};
|
|
54
44
|
}
|
|
55
|
-
let bindings = new Map(), importsIntent = [], prepend = [], replacements = [];
|
|
56
|
-
|
|
45
|
+
let bindings = new Map(), importsIntent = [], isReactive = (node) => isReactiveCallExpression(ctx.checker, node), prepend = [], replacements = [];
|
|
46
|
+
replacements.push(...primitives(ctx.sourceFile, bindings, isReactive));
|
|
47
|
+
let objectResult = object(ctx.sourceFile, bindings);
|
|
57
48
|
prepend.push(...objectResult.prepend);
|
|
58
|
-
replacements.push(...objectResult.replacements);
|
|
59
|
-
let arrayResult = array(ctx.sourceFile, bindings, ctx.checker);
|
|
60
|
-
replacements.push(...arrayResult);
|
|
61
|
-
let transformedNodes = new Set(replacements.map(r => r.node));
|
|
62
|
-
function findRemainingReactiveCalls(node) {
|
|
63
|
-
if (isReactiveCallExpression(ctx.checker, node) && !transformedNodes.has(node)) {
|
|
64
|
-
let call = node;
|
|
65
|
-
replacements.push({
|
|
66
|
-
generate: () => `${COMPILER_NAMESPACE}.reactive(${call.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
|
|
67
|
-
node: call
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
ts.forEachChild(node, findRemainingReactiveCalls);
|
|
71
|
-
}
|
|
72
|
-
findRemainingReactiveCalls(ctx.sourceFile);
|
|
49
|
+
replacements.push(...objectResult.replacements, ...array(ctx.sourceFile, bindings), ...findRemainingCalls(ctx.checker, ctx.sourceFile, new Set(replacements.map(r => r.node))));
|
|
73
50
|
if (replacements.length > 0 || prepend.length > 0) {
|
|
74
51
|
importsIntent.push({
|
|
75
52
|
namespace: COMPILER_NAMESPACE,
|
|
@@ -5,5 +5,5 @@ type ObjectTransformResult = {
|
|
|
5
5
|
prepend: string[];
|
|
6
6
|
replacements: ReplacementIntent[];
|
|
7
7
|
};
|
|
8
|
-
declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings
|
|
8
|
+
declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings) => ObjectTransformResult;
|
|
9
9
|
export default _default;
|
package/build/compiler/object.js
CHANGED
|
@@ -110,11 +110,8 @@ function isStaticValue(node) {
|
|
|
110
110
|
}
|
|
111
111
|
return false;
|
|
112
112
|
}
|
|
113
|
-
function isReactiveCall(node) {
|
|
114
|
-
return ts.isIdentifier(node.expression) && node.expression.text === "reactive";
|
|
115
|
-
}
|
|
116
113
|
function visit(ctx, node) {
|
|
117
|
-
if (ts.isCallExpression(node) &&
|
|
114
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive') {
|
|
118
115
|
let arg = node.arguments[0];
|
|
119
116
|
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
120
117
|
let properties = [], props = arg.properties, varName = null;
|
|
@@ -148,11 +145,10 @@ function visit(ctx, node) {
|
|
|
148
145
|
}
|
|
149
146
|
ts.forEachChild(node, n => visit(ctx, n));
|
|
150
147
|
}
|
|
151
|
-
export default (sourceFile, bindings
|
|
148
|
+
export default (sourceFile, bindings) => {
|
|
152
149
|
let ctx = {
|
|
153
150
|
bindings,
|
|
154
151
|
calls: [],
|
|
155
|
-
checker,
|
|
156
152
|
sourceFile
|
|
157
153
|
};
|
|
158
154
|
visit(ctx, sourceFile);
|
|
@@ -164,11 +160,11 @@ export default (sourceFile, bindings, checker) => {
|
|
|
164
160
|
let call = ctx.calls[i];
|
|
165
161
|
prepend.push(buildClassCode(call.className, call.properties));
|
|
166
162
|
replacements.push({
|
|
167
|
-
node: call.node,
|
|
168
163
|
generate: () => ` new ${call.className}(${call.properties
|
|
169
164
|
.filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
|
|
170
165
|
.map(p => p.valueText)
|
|
171
|
-
.join(', ')})
|
|
166
|
+
.join(', ')})`,
|
|
167
|
+
node: call.node,
|
|
172
168
|
});
|
|
173
169
|
}
|
|
174
170
|
return { prepend, replacements };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import type { Bindings } from '../types.js';
|
|
4
|
+
declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (node: ts.Node) => boolean) => ReplacementIntent[];
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { ts } from '@esportsplus/typescript';
|
|
2
|
+
import { COMPILER_NAMESPACE, COMPILER_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
|
+
]);
|
|
20
|
+
function isInScope(reference, binding) {
|
|
21
|
+
let current = reference;
|
|
22
|
+
while (current) {
|
|
23
|
+
if (current === binding.scope) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
current = current.parent;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function visit(ctx, node) {
|
|
31
|
+
if (ctx.isReactiveCall(node)) {
|
|
32
|
+
let call = node;
|
|
33
|
+
if (call.arguments.length > 0) {
|
|
34
|
+
let arg = call.arguments[0], classification = COMPILER_TYPES.Signal;
|
|
35
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
36
|
+
classification = COMPILER_TYPES.Computed;
|
|
37
|
+
}
|
|
38
|
+
else if (ts.isArrayLiteralExpression(arg) || ts.isObjectLiteralExpression(arg)) {
|
|
39
|
+
classification = null;
|
|
40
|
+
}
|
|
41
|
+
if (classification) {
|
|
42
|
+
let varName = null;
|
|
43
|
+
if (call.parent && ts.isVariableDeclaration(call.parent) && ts.isIdentifier(call.parent.name)) {
|
|
44
|
+
varName = call.parent.name.text;
|
|
45
|
+
}
|
|
46
|
+
else if (call.parent &&
|
|
47
|
+
ts.isBinaryExpression(call.parent) &&
|
|
48
|
+
call.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
49
|
+
ts.isIdentifier(call.parent.left)) {
|
|
50
|
+
varName = call.parent.left.text;
|
|
51
|
+
}
|
|
52
|
+
if (varName) {
|
|
53
|
+
let current = call.parent, scope;
|
|
54
|
+
while (current) {
|
|
55
|
+
if (ts.isArrowFunction(current) ||
|
|
56
|
+
ts.isBlock(current) ||
|
|
57
|
+
ts.isForInStatement(current) ||
|
|
58
|
+
ts.isForOfStatement(current) ||
|
|
59
|
+
ts.isForStatement(current) ||
|
|
60
|
+
ts.isFunctionDeclaration(current) ||
|
|
61
|
+
ts.isFunctionExpression(current) ||
|
|
62
|
+
ts.isSourceFile(current)) {
|
|
63
|
+
scope = current;
|
|
64
|
+
}
|
|
65
|
+
current = current.parent;
|
|
66
|
+
}
|
|
67
|
+
if (!scope) {
|
|
68
|
+
scope = call.getSourceFile();
|
|
69
|
+
}
|
|
70
|
+
ctx.scopedBindings.push({ name: varName, scope, type: classification });
|
|
71
|
+
ctx.bindings.set(varName, classification);
|
|
72
|
+
}
|
|
73
|
+
ctx.replacements.push({
|
|
74
|
+
generate: () => classification === COMPILER_TYPES.Computed
|
|
75
|
+
? `${COMPILER_NAMESPACE}.computed`
|
|
76
|
+
: `${COMPILER_NAMESPACE}.signal`,
|
|
77
|
+
node: call.expression
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (ts.isIdentifier(node) &&
|
|
83
|
+
node.parent &&
|
|
84
|
+
!(ts.isVariableDeclaration(node.parent) && node.parent.name === node)) {
|
|
85
|
+
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
|
|
86
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let bindings = ctx.scopedBindings, binding, name = node.text;
|
|
90
|
+
for (let i = 0, n = bindings.length; i < n; i++) {
|
|
91
|
+
let b = bindings[i];
|
|
92
|
+
if (b.name === name && isInScope(node, b)) {
|
|
93
|
+
binding = b;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (binding && node.parent) {
|
|
97
|
+
let parent = node.parent;
|
|
98
|
+
if (!(ts.isBinaryExpression(parent) &&
|
|
99
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
100
|
+
ctx.isReactiveCall(parent.right)) &&
|
|
101
|
+
!(ts.isTypeOfExpression(parent) && parent.expression === node)) {
|
|
102
|
+
let writeCtx;
|
|
103
|
+
if (ts.isBinaryExpression(parent) && parent.left === node) {
|
|
104
|
+
let op = parent.operatorToken.kind;
|
|
105
|
+
if (op === ts.SyntaxKind.EqualsToken) {
|
|
106
|
+
writeCtx = 'simple';
|
|
107
|
+
}
|
|
108
|
+
else if (COMPOUND_OPERATORS.has(op)) {
|
|
109
|
+
writeCtx = 'compound';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
|
|
113
|
+
let op = parent.operator;
|
|
114
|
+
if (op === ts.SyntaxKind.MinusMinusToken || op === ts.SyntaxKind.PlusPlusToken) {
|
|
115
|
+
writeCtx = 'increment';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (writeCtx) {
|
|
119
|
+
if (binding.type !== COMPILER_TYPES.Computed) {
|
|
120
|
+
if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
|
|
121
|
+
let right = parent.right;
|
|
122
|
+
ctx.replacements.push({
|
|
123
|
+
generate: (sf) => `${COMPILER_NAMESPACE}.write(${name}, ${right.getText(sf)})`,
|
|
124
|
+
node: parent
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
|
|
128
|
+
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind) ?? '+', right = parent.right;
|
|
129
|
+
ctx.replacements.push({
|
|
130
|
+
generate: (sf) => `${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${op} ${right.getText(sf)})`,
|
|
131
|
+
node: parent
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (writeCtx === 'increment') {
|
|
135
|
+
let delta = parent.operator === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1', isPrefix = ts.isPrefixUnaryExpression(parent);
|
|
136
|
+
if (ts.isExpressionStatement(parent.parent)) {
|
|
137
|
+
ctx.replacements.push({
|
|
138
|
+
generate: () => `${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${delta})`,
|
|
139
|
+
node: parent
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else if (isPrefix) {
|
|
143
|
+
ctx.replacements.push({
|
|
144
|
+
generate: () => `(${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${delta}), ${name}.value)`,
|
|
145
|
+
node: parent
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
let tmp = `_t${ctx.tmpCounter++}`;
|
|
150
|
+
ctx.replacements.push({
|
|
151
|
+
generate: () => `((${tmp}) => (${COMPILER_NAMESPACE}.write(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
|
|
152
|
+
node: parent
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
ctx.replacements.push({
|
|
160
|
+
generate: () => `${COMPILER_NAMESPACE}.read(${name})`,
|
|
161
|
+
node
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
168
|
+
}
|
|
169
|
+
export default (sourceFile, bindings, isReactiveCall) => {
|
|
170
|
+
let ctx = {
|
|
171
|
+
bindings,
|
|
172
|
+
isReactiveCall,
|
|
173
|
+
replacements: [],
|
|
174
|
+
scopedBindings: [],
|
|
175
|
+
sourceFile,
|
|
176
|
+
tmpCounter: 0
|
|
177
|
+
};
|
|
178
|
+
visit(ctx, sourceFile);
|
|
179
|
+
return ctx.replacements;
|
|
180
|
+
};
|
|
@@ -8,5 +8,6 @@ type Guard<T> = T extends Record<PropertyKey, unknown> ? T extends {
|
|
|
8
8
|
} : T : never;
|
|
9
9
|
declare function reactive<T extends unknown[]>(input: T): Reactive<T>;
|
|
10
10
|
declare function reactive<T extends Record<PropertyKey, unknown>>(input: Guard<T>): Reactive<T>;
|
|
11
|
+
declare function reactive<T>(input: T): Reactive<T>;
|
|
11
12
|
export default reactive;
|
|
12
13
|
export { reactive, ReactiveArray, ReactiveObject };
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"@esportsplus/utilities": "^0.27.2"
|
|
5
5
|
},
|
|
6
6
|
"devDependencies": {
|
|
7
|
-
"@esportsplus/typescript": "^0.
|
|
7
|
+
"@esportsplus/typescript": "^0.26.0",
|
|
8
8
|
"@types/node": "^25.0.3",
|
|
9
9
|
"vite": "^7.3.1"
|
|
10
10
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"type": "module",
|
|
33
33
|
"types": "build/index.d.ts",
|
|
34
|
-
"version": "0.
|
|
34
|
+
"version": "0.29.1",
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc",
|
|
37
37
|
"build:test": "pnpm build && vite build --config test/vite.config.ts",
|
package/src/compiler/array.ts
CHANGED
|
@@ -5,14 +5,6 @@ import { COMPILER_NAMESPACE, COMPILER_TYPES } from '~/constants';
|
|
|
5
5
|
import type { Bindings } from '~/types';
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
interface VisitContext {
|
|
9
|
-
bindings: Bindings;
|
|
10
|
-
checker?: ts.TypeChecker;
|
|
11
|
-
replacements: ReplacementIntent[];
|
|
12
|
-
sourceFile: ts.SourceFile;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
8
|
function getElementTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): string | null {
|
|
17
9
|
if (ts.isArrayTypeNode(typeNode)) {
|
|
18
10
|
return typeNode.elementType.getText(sourceFile);
|
|
@@ -31,12 +23,13 @@ function getElementTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): s
|
|
|
31
23
|
return null;
|
|
32
24
|
}
|
|
33
25
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
function visit(ctx: { bindings: Bindings, replacements: ReplacementIntent[], sourceFile: ts.SourceFile }, node: ts.Node): void {
|
|
27
|
+
if (
|
|
28
|
+
ts.isCallExpression(node) &&
|
|
29
|
+
ts.isIdentifier(node.expression) &&
|
|
30
|
+
node.expression.text === 'reactive' &&
|
|
31
|
+
node.arguments.length > 0
|
|
32
|
+
) {
|
|
40
33
|
let arg = node.arguments[0],
|
|
41
34
|
expression = ts.isAsExpression(arg) ? arg.expression : arg;
|
|
42
35
|
|
|
@@ -71,7 +64,7 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
if (ts.isPropertyAccessExpression(node.initializer)) {
|
|
74
|
-
let path = ast.
|
|
67
|
+
let path = ast.getPropertyPath(node.initializer);
|
|
75
68
|
|
|
76
69
|
if (path && ctx.bindings.get(path) === COMPILER_TYPES.Array) {
|
|
77
70
|
ctx.bindings.set(node.name.text, COMPILER_TYPES.Array);
|
|
@@ -94,16 +87,17 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
94
87
|
}
|
|
95
88
|
}
|
|
96
89
|
|
|
97
|
-
let parent = node.parent;
|
|
98
|
-
|
|
99
90
|
if (
|
|
100
91
|
ts.isPropertyAccessExpression(node) &&
|
|
101
92
|
node.name.text === 'length' &&
|
|
102
|
-
(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
93
|
+
(
|
|
94
|
+
!node.parent ||
|
|
95
|
+
(
|
|
96
|
+
!(ts.isBinaryExpression(node.parent) && node.parent.left === node) &&
|
|
97
|
+
!ts.isPostfixUnaryExpression(node.parent) &&
|
|
98
|
+
!ts.isPrefixUnaryExpression(node.parent)
|
|
99
|
+
)
|
|
100
|
+
)
|
|
107
101
|
) {
|
|
108
102
|
let name = ast.getExpressionName(node.expression);
|
|
109
103
|
|
|
@@ -126,12 +120,10 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
126
120
|
if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
|
|
127
121
|
ctx.replacements.push({
|
|
128
122
|
node,
|
|
129
|
-
generate: (sf) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return `${element.expression.getText(sf)}.$set(${index}, ${value})`;
|
|
134
|
-
}
|
|
123
|
+
generate: (sf) => `${element.expression.getText(sf)}.$set(
|
|
124
|
+
${element.argumentExpression.getText(sf)},
|
|
125
|
+
${node.right.getText(sf)}
|
|
126
|
+
)`
|
|
135
127
|
});
|
|
136
128
|
}
|
|
137
129
|
}
|
|
@@ -140,10 +132,9 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
140
132
|
}
|
|
141
133
|
|
|
142
134
|
|
|
143
|
-
export default (sourceFile: ts.SourceFile, bindings: Bindings
|
|
144
|
-
let ctx
|
|
135
|
+
export default (sourceFile: ts.SourceFile, bindings: Bindings): ReplacementIntent[] => {
|
|
136
|
+
let ctx = {
|
|
145
137
|
bindings,
|
|
146
|
-
checker,
|
|
147
138
|
replacements: [],
|
|
148
139
|
sourceFile
|
|
149
140
|
};
|
package/src/compiler/index.ts
CHANGED
|
@@ -5,44 +5,26 @@ import { COMPILER_ENTRYPOINT, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
|
|
|
5
5
|
import type { Bindings } from '~/types';
|
|
6
6
|
import array from './array';
|
|
7
7
|
import object from './object';
|
|
8
|
+
import primitives from './primitives';
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!symbol) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Follow aliases to original symbol
|
|
22
|
-
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
23
|
-
symbol = checker.getAliasedSymbol(symbol);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let declarations = symbol.getDeclarations();
|
|
27
|
-
|
|
28
|
-
if (!declarations || declarations.length === 0) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
11
|
+
type FindRemainingContext = {
|
|
12
|
+
checker: ts.TypeChecker | undefined;
|
|
13
|
+
replacements: ReplacementIntent[];
|
|
14
|
+
sourceFile: ts.SourceFile;
|
|
15
|
+
transformedNodes: Set<ts.Node>;
|
|
16
|
+
};
|
|
31
17
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
function findRemainingCalls(
|
|
19
|
+
checker: ts.TypeChecker | undefined,
|
|
20
|
+
sourceFile: ts.SourceFile,
|
|
21
|
+
transformedNodes: Set<ts.Node>
|
|
22
|
+
): ReplacementIntent[] {
|
|
23
|
+
let ctx: FindRemainingContext = { checker, replacements: [], sourceFile, transformedNodes };
|
|
35
24
|
|
|
36
|
-
|
|
37
|
-
if (sourceFile.fileName.includes(PACKAGE) || sourceFile.fileName.includes('reactivity')) {
|
|
38
|
-
// Verify it's the reactive export
|
|
39
|
-
if (symbol.name === COMPILER_ENTRYPOINT) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
25
|
+
visit(ctx, sourceFile);
|
|
44
26
|
|
|
45
|
-
return
|
|
27
|
+
return ctx.replacements;
|
|
46
28
|
}
|
|
47
29
|
|
|
48
30
|
function isReactiveCallExpression(checker: ts.TypeChecker | undefined, node: ts.Node): node is ts.CallExpression {
|
|
@@ -61,60 +43,58 @@ function isReactiveCallExpression(checker: ts.TypeChecker | undefined, node: ts.
|
|
|
61
43
|
|
|
62
44
|
// Use checker to resolve aliases
|
|
63
45
|
if (checker) {
|
|
64
|
-
return
|
|
46
|
+
return imports.inPackage(checker, expr, PACKAGE, COMPILER_ENTRYPOINT);
|
|
65
47
|
}
|
|
66
48
|
}
|
|
67
49
|
|
|
68
50
|
// Property access: ns.reactive(...)
|
|
69
51
|
if (ts.isPropertyAccessExpression(expr) && expr.name.text === COMPILER_ENTRYPOINT && checker) {
|
|
70
|
-
return
|
|
52
|
+
return imports.inPackage(checker, expr, PACKAGE);
|
|
71
53
|
}
|
|
72
54
|
|
|
73
55
|
return false;
|
|
74
56
|
}
|
|
75
57
|
|
|
58
|
+
function visit(ctx: FindRemainingContext, node: ts.Node): void {
|
|
59
|
+
// Check if call or its expression has already been transformed
|
|
60
|
+
if (isReactiveCallExpression(ctx.checker, node) && !ctx.transformedNodes.has(node) && !ctx.transformedNodes.has(node.expression)) {
|
|
61
|
+
ctx.replacements.push({
|
|
62
|
+
generate: () => `${COMPILER_NAMESPACE}.reactive(${node.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
|
|
63
|
+
node
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
68
|
+
}
|
|
69
|
+
|
|
76
70
|
|
|
77
71
|
const plugin: Plugin = {
|
|
78
72
|
patterns: ['reactive(', 'reactive<'],
|
|
79
73
|
|
|
80
74
|
transform: (ctx: TransformContext) => {
|
|
81
|
-
if (!
|
|
75
|
+
if (!imports.find(ctx.sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT))) {
|
|
82
76
|
return {};
|
|
83
77
|
}
|
|
84
78
|
|
|
85
79
|
let bindings: Bindings = new Map(),
|
|
86
80
|
importsIntent: ImportIntent[] = [],
|
|
81
|
+
isReactive = (node: ts.Node) => isReactiveCallExpression(ctx.checker, node),
|
|
87
82
|
prepend: string[] = [],
|
|
88
83
|
replacements: ReplacementIntent[] = [];
|
|
89
84
|
|
|
85
|
+
// Run primitives transform first (tracks bindings for signal/computed)
|
|
86
|
+
replacements.push(...primitives(ctx.sourceFile, bindings, isReactive));
|
|
87
|
+
|
|
90
88
|
// Run object transform
|
|
91
|
-
let objectResult = object(ctx.sourceFile, bindings
|
|
89
|
+
let objectResult = object(ctx.sourceFile, bindings);
|
|
92
90
|
|
|
93
91
|
prepend.push(...objectResult.prepend);
|
|
94
|
-
replacements.push(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Find remaining reactive() calls that weren't transformed and replace with namespace version
|
|
102
|
-
let transformedNodes = new Set(replacements.map(r => r.node));
|
|
103
|
-
|
|
104
|
-
function findRemainingReactiveCalls(node: ts.Node): void {
|
|
105
|
-
if (isReactiveCallExpression(ctx.checker, node) && !transformedNodes.has(node)) {
|
|
106
|
-
let call = node;
|
|
107
|
-
|
|
108
|
-
replacements.push({
|
|
109
|
-
generate: () => `${COMPILER_NAMESPACE}.reactive(${call.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
|
|
110
|
-
node: call
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
ts.forEachChild(node, findRemainingReactiveCalls);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
findRemainingReactiveCalls(ctx.sourceFile);
|
|
92
|
+
replacements.push(
|
|
93
|
+
...objectResult.replacements,
|
|
94
|
+
...array(ctx.sourceFile, bindings),
|
|
95
|
+
// Find remaining reactive() calls that weren't transformed and replace with namespace version
|
|
96
|
+
...findRemainingCalls(ctx.checker, ctx.sourceFile, new Set(replacements.map(r => r.node)))
|
|
97
|
+
);
|
|
118
98
|
|
|
119
99
|
// Build import intent
|
|
120
100
|
if (replacements.length > 0 || prepend.length > 0) {
|
package/src/compiler/object.ts
CHANGED
|
@@ -22,7 +22,6 @@ interface ReactiveObjectCall {
|
|
|
22
22
|
interface VisitContext {
|
|
23
23
|
bindings: Bindings;
|
|
24
24
|
calls: ReactiveObjectCall[];
|
|
25
|
-
checker?: ts.TypeChecker;
|
|
26
25
|
sourceFile: ts.SourceFile;
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -166,12 +165,8 @@ function isStaticValue(node: ts.Node): boolean {
|
|
|
166
165
|
return false;
|
|
167
166
|
}
|
|
168
167
|
|
|
169
|
-
function isReactiveCall(node: ts.CallExpression): boolean {
|
|
170
|
-
return ts.isIdentifier(node.expression) && node.expression.text === "reactive";
|
|
171
|
-
}
|
|
172
|
-
|
|
173
168
|
function visit(ctx: VisitContext, node: ts.Node): void {
|
|
174
|
-
if (ts.isCallExpression(node) &&
|
|
169
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive') {
|
|
175
170
|
let arg = node.arguments[0];
|
|
176
171
|
|
|
177
172
|
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
@@ -225,11 +220,10 @@ type ObjectTransformResult = {
|
|
|
225
220
|
};
|
|
226
221
|
|
|
227
222
|
|
|
228
|
-
export default (sourceFile: ts.SourceFile, bindings: Bindings
|
|
223
|
+
export default (sourceFile: ts.SourceFile, bindings: Bindings): ObjectTransformResult => {
|
|
229
224
|
let ctx: VisitContext = {
|
|
230
225
|
bindings,
|
|
231
226
|
calls: [],
|
|
232
|
-
checker,
|
|
233
227
|
sourceFile
|
|
234
228
|
};
|
|
235
229
|
|
|
@@ -246,15 +240,14 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Type
|
|
|
246
240
|
let call = ctx.calls[i];
|
|
247
241
|
|
|
248
242
|
prepend.push(buildClassCode(call.className, call.properties));
|
|
249
|
-
|
|
250
243
|
replacements.push({
|
|
251
|
-
node: call.node,
|
|
252
244
|
generate: () => ` new ${call.className}(${
|
|
253
245
|
call.properties
|
|
254
246
|
.filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
|
|
255
247
|
.map(p => p.valueText)
|
|
256
248
|
.join(', ')
|
|
257
|
-
})
|
|
249
|
+
})`,
|
|
250
|
+
node: call.node,
|
|
258
251
|
});
|
|
259
252
|
}
|
|
260
253
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import { COMPILER_NAMESPACE, COMPILER_TYPES } from '~/constants';
|
|
4
|
+
import type { Bindings } from '~/types';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
interface ScopeBinding {
|
|
8
|
+
name: string;
|
|
9
|
+
scope: ts.Node;
|
|
10
|
+
type: COMPILER_TYPES;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TransformContext {
|
|
14
|
+
bindings: Bindings;
|
|
15
|
+
isReactiveCall: (node: ts.Node) => boolean;
|
|
16
|
+
replacements: ReplacementIntent[];
|
|
17
|
+
scopedBindings: ScopeBinding[];
|
|
18
|
+
sourceFile: ts.SourceFile;
|
|
19
|
+
tmpCounter: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const COMPOUND_OPERATORS = new Map<ts.SyntaxKind, string>([
|
|
24
|
+
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, '&&'],
|
|
25
|
+
[ts.SyntaxKind.AmpersandEqualsToken, '&'],
|
|
26
|
+
[ts.SyntaxKind.AsteriskAsteriskEqualsToken, '**'],
|
|
27
|
+
[ts.SyntaxKind.AsteriskEqualsToken, '*'],
|
|
28
|
+
[ts.SyntaxKind.BarBarEqualsToken, '||'],
|
|
29
|
+
[ts.SyntaxKind.BarEqualsToken, '|'],
|
|
30
|
+
[ts.SyntaxKind.CaretEqualsToken, '^'],
|
|
31
|
+
[ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, '>>'],
|
|
32
|
+
[ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, '>>>'],
|
|
33
|
+
[ts.SyntaxKind.LessThanLessThanEqualsToken, '<<'],
|
|
34
|
+
[ts.SyntaxKind.MinusEqualsToken, '-'],
|
|
35
|
+
[ts.SyntaxKind.PercentEqualsToken, '%'],
|
|
36
|
+
[ts.SyntaxKind.PlusEqualsToken, '+'],
|
|
37
|
+
[ts.SyntaxKind.QuestionQuestionEqualsToken, '??'],
|
|
38
|
+
[ts.SyntaxKind.SlashEqualsToken, '/']
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
function isInScope(reference: ts.Node, binding: ScopeBinding): boolean {
|
|
43
|
+
let current: ts.Node | undefined = reference;
|
|
44
|
+
|
|
45
|
+
while (current) {
|
|
46
|
+
if (current === binding.scope) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
current = current.parent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function visit(ctx: TransformContext, node: ts.Node): void {
|
|
57
|
+
if (ctx.isReactiveCall(node)) {
|
|
58
|
+
let call = node as ts.CallExpression;
|
|
59
|
+
|
|
60
|
+
if (call.arguments.length > 0) {
|
|
61
|
+
let arg = call.arguments[0],
|
|
62
|
+
classification: COMPILER_TYPES | null = COMPILER_TYPES.Signal;
|
|
63
|
+
|
|
64
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
65
|
+
classification = COMPILER_TYPES.Computed;
|
|
66
|
+
}
|
|
67
|
+
else if (ts.isArrayLiteralExpression(arg) || ts.isObjectLiteralExpression(arg)) {
|
|
68
|
+
classification = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (classification) {
|
|
72
|
+
let varName: string | null = null;
|
|
73
|
+
|
|
74
|
+
if (call.parent && ts.isVariableDeclaration(call.parent) && ts.isIdentifier(call.parent.name)) {
|
|
75
|
+
varName = call.parent.name.text;
|
|
76
|
+
}
|
|
77
|
+
else if (
|
|
78
|
+
call.parent &&
|
|
79
|
+
ts.isBinaryExpression(call.parent) &&
|
|
80
|
+
call.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
81
|
+
ts.isIdentifier(call.parent.left)
|
|
82
|
+
) {
|
|
83
|
+
varName = call.parent.left.text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (varName) {
|
|
87
|
+
let current = call.parent,
|
|
88
|
+
scope;
|
|
89
|
+
|
|
90
|
+
while (current) {
|
|
91
|
+
if (
|
|
92
|
+
ts.isArrowFunction(current) ||
|
|
93
|
+
ts.isBlock(current) ||
|
|
94
|
+
ts.isForInStatement(current) ||
|
|
95
|
+
ts.isForOfStatement(current) ||
|
|
96
|
+
ts.isForStatement(current) ||
|
|
97
|
+
ts.isFunctionDeclaration(current) ||
|
|
98
|
+
ts.isFunctionExpression(current) ||
|
|
99
|
+
ts.isSourceFile(current)
|
|
100
|
+
) {
|
|
101
|
+
scope = current;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
current = current.parent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!scope) {
|
|
108
|
+
scope = call.getSourceFile();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ctx.scopedBindings.push({ name: varName, scope, type: classification });
|
|
112
|
+
ctx.bindings.set(varName, classification);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Replace just the 'reactive' identifier with the appropriate namespace function
|
|
116
|
+
ctx.replacements.push({
|
|
117
|
+
generate: () => classification === COMPILER_TYPES.Computed
|
|
118
|
+
? `${COMPILER_NAMESPACE}.computed`
|
|
119
|
+
: `${COMPILER_NAMESPACE}.signal`,
|
|
120
|
+
node: call.expression
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Continue visiting children - inner identifiers will get their own ReplacementIntents
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
ts.isIdentifier(node) &&
|
|
130
|
+
node.parent &&
|
|
131
|
+
!(ts.isVariableDeclaration(node.parent) && node.parent.name === node)
|
|
132
|
+
) {
|
|
133
|
+
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
|
|
134
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let bindings = ctx.scopedBindings,
|
|
139
|
+
binding,
|
|
140
|
+
name = node.text;
|
|
141
|
+
|
|
142
|
+
for (let i = 0, n = bindings.length; i < n; i++) {
|
|
143
|
+
let b = bindings[i];
|
|
144
|
+
|
|
145
|
+
if (b.name === name && isInScope(node, b)) {
|
|
146
|
+
binding = b;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (binding && node.parent) {
|
|
151
|
+
let parent = node.parent;
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
!(
|
|
155
|
+
ts.isBinaryExpression(parent) &&
|
|
156
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
157
|
+
ctx.isReactiveCall(parent.right)
|
|
158
|
+
) &&
|
|
159
|
+
!(ts.isTypeOfExpression(parent) && parent.expression === node)
|
|
160
|
+
) {
|
|
161
|
+
let writeCtx;
|
|
162
|
+
|
|
163
|
+
if (ts.isBinaryExpression(parent) && parent.left === node) {
|
|
164
|
+
let op = parent.operatorToken.kind;
|
|
165
|
+
|
|
166
|
+
if (op === ts.SyntaxKind.EqualsToken) {
|
|
167
|
+
writeCtx = 'simple';
|
|
168
|
+
}
|
|
169
|
+
else if (COMPOUND_OPERATORS.has(op)) {
|
|
170
|
+
writeCtx = 'compound';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
|
|
174
|
+
let op = parent.operator;
|
|
175
|
+
|
|
176
|
+
if (op === ts.SyntaxKind.MinusMinusToken || op === ts.SyntaxKind.PlusPlusToken) {
|
|
177
|
+
writeCtx = 'increment';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (writeCtx) {
|
|
182
|
+
if (binding.type !== COMPILER_TYPES.Computed) {
|
|
183
|
+
if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
|
|
184
|
+
let right = parent.right;
|
|
185
|
+
|
|
186
|
+
ctx.replacements.push({
|
|
187
|
+
generate: (sf) => `${COMPILER_NAMESPACE}.write(${name}, ${right.getText(sf)})`,
|
|
188
|
+
node: parent
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
|
|
192
|
+
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind) ?? '+',
|
|
193
|
+
right = parent.right;
|
|
194
|
+
|
|
195
|
+
ctx.replacements.push({
|
|
196
|
+
generate: (sf) => `${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${op} ${right.getText(sf)})`,
|
|
197
|
+
node: parent
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else if (writeCtx === 'increment') {
|
|
201
|
+
let delta = (parent as ts.PostfixUnaryExpression | ts.PrefixUnaryExpression).operator === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1',
|
|
202
|
+
isPrefix = ts.isPrefixUnaryExpression(parent);
|
|
203
|
+
|
|
204
|
+
if (ts.isExpressionStatement(parent.parent)) {
|
|
205
|
+
ctx.replacements.push({
|
|
206
|
+
generate: () => `${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${delta})`,
|
|
207
|
+
node: parent
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else if (isPrefix) {
|
|
211
|
+
ctx.replacements.push({
|
|
212
|
+
generate: () => `(${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${delta}), ${name}.value)`,
|
|
213
|
+
node: parent
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
let tmp = `_t${ctx.tmpCounter++}`;
|
|
218
|
+
|
|
219
|
+
ctx.replacements.push({
|
|
220
|
+
generate: () => `((${tmp}) => (${COMPILER_NAMESPACE}.write(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
|
|
221
|
+
node: parent
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
ctx.replacements.push({
|
|
229
|
+
generate: () => `${COMPILER_NAMESPACE}.read(${name})`,
|
|
230
|
+
node
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (node: ts.Node) => boolean) => {
|
|
242
|
+
let ctx: TransformContext = {
|
|
243
|
+
bindings,
|
|
244
|
+
isReactiveCall,
|
|
245
|
+
replacements: [],
|
|
246
|
+
scopedBindings: [],
|
|
247
|
+
sourceFile,
|
|
248
|
+
tmpCounter: 0
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
visit(ctx, sourceFile);
|
|
252
|
+
|
|
253
|
+
return ctx.replacements;
|
|
254
|
+
};
|
package/src/reactive/index.ts
CHANGED
|
@@ -16,7 +16,8 @@ type Guard<T> =
|
|
|
16
16
|
|
|
17
17
|
function reactive<T extends unknown[]>(input: T): Reactive<T>;
|
|
18
18
|
function reactive<T extends Record<PropertyKey, unknown>>(input: Guard<T>): Reactive<T>;
|
|
19
|
-
function reactive<T
|
|
19
|
+
function reactive<T>(input: T): Reactive<T>;
|
|
20
|
+
function reactive<T>(input: T): Reactive<T> {
|
|
20
21
|
let dispose = false,
|
|
21
22
|
value = root(() => {
|
|
22
23
|
let response: Reactive<T> | undefined;
|
|
@@ -40,7 +41,7 @@ function reactive<T extends unknown[] | Record<PropertyKey, unknown>>(input: T):
|
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
if (dispose) {
|
|
43
|
-
onCleanup(() => value.dispose());
|
|
44
|
+
onCleanup(() => (value as any as { dispose: VoidFunction }).dispose());
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
return value;
|
package/test/primitives.ts
CHANGED
|
@@ -1,171 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
import { effect, reactive } from '@esportsplus/reactivity';
|
|
1
|
+
import { reactive } from '@esportsplus/reactivity';
|
|
3
2
|
|
|
4
3
|
|
|
5
|
-
//
|
|
6
|
-
// Standalone Signal Primitives
|
|
7
|
-
// =============================================================================
|
|
8
|
-
|
|
9
|
-
console.log('=== Standalone Signal Primitives ===');
|
|
10
|
-
|
|
4
|
+
// Signal creation
|
|
11
5
|
let count = reactive(0);
|
|
12
|
-
let name = reactive('
|
|
6
|
+
let name = reactive('test');
|
|
13
7
|
let flag = reactive(true);
|
|
8
|
+
let nullable = reactive<string | null>(null);
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
// Computed creation
|
|
11
|
+
let doubled = reactive(() => count * 2);
|
|
12
|
+
let greeting = reactive(() => `Hello ${name}!`);
|
|
13
|
+
let complex = reactive(() => flag ? count : 0);
|
|
18
14
|
|
|
19
|
-
//
|
|
15
|
+
// Read access
|
|
16
|
+
console.log(count);
|
|
17
|
+
console.log(name);
|
|
18
|
+
console.log(doubled);
|
|
19
|
+
|
|
20
|
+
// Write access - simple assignment
|
|
20
21
|
count = 10;
|
|
21
|
-
name = '
|
|
22
|
+
name = 'world';
|
|
22
23
|
flag = false;
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
console.log('\n=== Increment/Decrement ===');
|
|
72
|
-
|
|
73
|
-
let counter = reactive(0);
|
|
74
|
-
|
|
75
|
-
counter++;
|
|
76
|
-
console.log('After counter++:', counter);
|
|
77
|
-
|
|
78
|
-
++counter;
|
|
79
|
-
console.log('After ++counter:', counter);
|
|
80
|
-
|
|
81
|
-
counter--;
|
|
82
|
-
console.log('After counter--:', counter);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// =============================================================================
|
|
86
|
-
// Mixed Standalone and Object Primitives
|
|
87
|
-
// =============================================================================
|
|
88
|
-
|
|
89
|
-
console.log('\n=== Mixed Standalone and Object ===');
|
|
90
|
-
|
|
91
|
-
let multiplier = reactive(2);
|
|
92
|
-
|
|
93
|
-
let obj = reactive({
|
|
94
|
-
value: 10,
|
|
95
|
-
scaled: () => obj.value * multiplier
|
|
25
|
+
// Compound assignment operators
|
|
26
|
+
count += 5;
|
|
27
|
+
count -= 2;
|
|
28
|
+
count *= 3;
|
|
29
|
+
count /= 2;
|
|
30
|
+
count %= 7;
|
|
31
|
+
count **= 2;
|
|
32
|
+
count &= 0xFF;
|
|
33
|
+
count |= 0x0F;
|
|
34
|
+
count ^= 0xAA;
|
|
35
|
+
count <<= 2;
|
|
36
|
+
count >>= 1;
|
|
37
|
+
count >>>= 1;
|
|
38
|
+
count &&= 1;
|
|
39
|
+
count ||= 0;
|
|
40
|
+
count ??= 42;
|
|
41
|
+
|
|
42
|
+
// Increment/decrement - statement context
|
|
43
|
+
count++;
|
|
44
|
+
count--;
|
|
45
|
+
++count;
|
|
46
|
+
--count;
|
|
47
|
+
|
|
48
|
+
// Increment/decrement - expression context (prefix)
|
|
49
|
+
let a = ++count;
|
|
50
|
+
let b = --count;
|
|
51
|
+
console.log(a, b);
|
|
52
|
+
|
|
53
|
+
// Increment/decrement - expression context (postfix)
|
|
54
|
+
let c = count++;
|
|
55
|
+
let d = count--;
|
|
56
|
+
console.log(c, d);
|
|
57
|
+
|
|
58
|
+
// Nested reads in computed
|
|
59
|
+
let x = reactive(1);
|
|
60
|
+
let y = reactive(2);
|
|
61
|
+
let sum = reactive(() => x + y);
|
|
62
|
+
let product = reactive(() => x * y);
|
|
63
|
+
let nested = reactive(() => sum + product);
|
|
64
|
+
|
|
65
|
+
// Conditional reads
|
|
66
|
+
let conditional = reactive(() => {
|
|
67
|
+
if (flag) {
|
|
68
|
+
return x + y;
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
96
71
|
});
|
|
97
72
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
console.log('After multiplier = 3:');
|
|
103
|
-
console.log(' obj.scaled:', obj.scaled);
|
|
104
|
-
|
|
105
|
-
obj.value = 20;
|
|
106
|
-
console.log('After obj.value = 20:');
|
|
107
|
-
console.log(' obj.scaled:', obj.scaled);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// =============================================================================
|
|
111
|
-
// Effects with Standalone Primitives
|
|
112
|
-
// =============================================================================
|
|
113
|
-
|
|
114
|
-
console.log('\n=== Effects with Standalone Primitives ===');
|
|
115
|
-
|
|
116
|
-
let effectCount = 0;
|
|
117
|
-
let watched = reactive(0);
|
|
118
|
-
|
|
119
|
-
let cleanup = effect(() => {
|
|
120
|
-
effectCount++;
|
|
121
|
-
console.log(`Effect #${effectCount}: watched = ${watched}`);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
watched = 1;
|
|
125
|
-
watched = 2;
|
|
126
|
-
watched = 3;
|
|
127
|
-
|
|
128
|
-
cleanup();
|
|
129
|
-
|
|
130
|
-
watched = 4; // Should not trigger effect
|
|
131
|
-
console.log('After cleanup, watched set to 4 (no effect should run)');
|
|
132
|
-
console.log('Total effect runs:', effectCount);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// =============================================================================
|
|
136
|
-
// String Template Computeds
|
|
137
|
-
// =============================================================================
|
|
138
|
-
|
|
139
|
-
console.log('\n=== String Template Computeds ===');
|
|
140
|
-
|
|
141
|
-
let firstName = reactive('John');
|
|
142
|
-
let lastName = reactive('Doe');
|
|
143
|
-
let fullName = reactive(() => `${firstName} ${lastName}`);
|
|
144
|
-
|
|
145
|
-
console.log('Full name:', fullName);
|
|
146
|
-
|
|
147
|
-
firstName = 'Jane';
|
|
148
|
-
console.log('After firstName = Jane:', fullName);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// =============================================================================
|
|
152
|
-
// Object-wrapped Primitives (original tests)
|
|
153
|
-
// =============================================================================
|
|
154
|
-
|
|
155
|
-
console.log('\n=== Object-wrapped Primitives ===');
|
|
156
|
-
|
|
157
|
-
let state = reactive({
|
|
158
|
-
count: 0,
|
|
159
|
-
flag: true,
|
|
160
|
-
name: 'initial'
|
|
161
|
-
});
|
|
73
|
+
// Function with reactive reads
|
|
74
|
+
function calculate() {
|
|
75
|
+
return count + x + y;
|
|
76
|
+
}
|
|
162
77
|
|
|
163
|
-
|
|
78
|
+
// Arrow function with reactive reads
|
|
79
|
+
const calc = () => count * 2;
|
|
164
80
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
81
|
+
// Reactive in loop
|
|
82
|
+
for (let i = 0; i < 10; i++) {
|
|
83
|
+
count += i;
|
|
84
|
+
}
|
|
168
85
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
console.log('After assignment - flag:', state.flag);
|
|
86
|
+
// Reassignment with new reactive
|
|
87
|
+
count = reactive(100);
|