@esportsplus/reactivity 0.28.2 → 0.29.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.js +12 -12
- package/build/compiler/index.js +11 -9
- package/build/compiler/object.js +3 -6
- package/build/compiler/primitives.d.ts +5 -0
- package/build/compiler/primitives.js +181 -0
- package/build/reactive/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/compiler/array.ts +18 -18
- package/src/compiler/index.ts +16 -13
- package/src/compiler/object.ts +6 -11
- package/src/compiler/primitives.ts +261 -0
- package/src/reactive/index.ts +3 -2
- package/test/primitives.ts +73 -157
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;
|
|
@@ -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,10 +83,10 @@ 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
|
}
|
package/build/compiler/index.js
CHANGED
|
@@ -3,6 +3,7 @@ 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
|
+
import primitives from './primitives.js';
|
|
6
7
|
function hasReactiveImport(sourceFile) {
|
|
7
8
|
return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
|
|
8
9
|
}
|
|
@@ -52,20 +53,21 @@ const plugin = {
|
|
|
52
53
|
if (!hasReactiveImport(ctx.sourceFile)) {
|
|
53
54
|
return {};
|
|
54
55
|
}
|
|
55
|
-
let bindings = new Map(), importsIntent = [], prepend = [], replacements = [];
|
|
56
|
+
let bindings = new Map(), importsIntent = [], isReactiveCall = (node) => isReactiveCallExpression(ctx.checker, node), prepend = [], replacements = [];
|
|
57
|
+
replacements.push(...primitives(ctx.sourceFile, bindings, isReactiveCall, ctx.checker));
|
|
56
58
|
let objectResult = object(ctx.sourceFile, bindings, ctx.checker);
|
|
57
59
|
prepend.push(...objectResult.prepend);
|
|
58
|
-
replacements.push(...objectResult.replacements);
|
|
59
|
-
let arrayResult = array(ctx.sourceFile, bindings, ctx.checker);
|
|
60
|
-
replacements.push(...arrayResult);
|
|
60
|
+
replacements.push(...objectResult.replacements, ...array(ctx.sourceFile, bindings, ctx.checker));
|
|
61
61
|
let transformedNodes = new Set(replacements.map(r => r.node));
|
|
62
62
|
function findRemainingReactiveCalls(node) {
|
|
63
|
-
if (
|
|
63
|
+
if (isReactiveCall(node)) {
|
|
64
64
|
let call = node;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
if (!transformedNodes.has(call) && !transformedNodes.has(call.expression)) {
|
|
66
|
+
replacements.push({
|
|
67
|
+
generate: () => `${COMPILER_NAMESPACE}.reactive(${call.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
|
|
68
|
+
node: call
|
|
69
|
+
});
|
|
70
|
+
}
|
|
69
71
|
}
|
|
70
72
|
ts.forEachChild(node, findRemainingReactiveCalls);
|
|
71
73
|
}
|
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;
|
|
@@ -164,11 +161,11 @@ export default (sourceFile, bindings, checker) => {
|
|
|
164
161
|
let call = ctx.calls[i];
|
|
165
162
|
prepend.push(buildClassCode(call.className, call.properties));
|
|
166
163
|
replacements.push({
|
|
167
|
-
node: call.node,
|
|
168
164
|
generate: () => ` new ${call.className}(${call.properties
|
|
169
165
|
.filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
|
|
170
166
|
.map(p => p.valueText)
|
|
171
|
-
.join(', ')})
|
|
167
|
+
.join(', ')})`,
|
|
168
|
+
node: call.node,
|
|
172
169
|
});
|
|
173
170
|
}
|
|
174
171
|
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, checker?: ts.TypeChecker) => ReplacementIntent[];
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,181 @@
|
|
|
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, checker) => {
|
|
170
|
+
let ctx = {
|
|
171
|
+
bindings,
|
|
172
|
+
checker,
|
|
173
|
+
isReactiveCall,
|
|
174
|
+
replacements: [],
|
|
175
|
+
scopedBindings: [],
|
|
176
|
+
sourceFile,
|
|
177
|
+
tmpCounter: 0
|
|
178
|
+
};
|
|
179
|
+
visit(ctx, sourceFile);
|
|
180
|
+
return ctx.replacements;
|
|
181
|
+
};
|
|
@@ -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
package/src/compiler/array.ts
CHANGED
|
@@ -31,12 +31,13 @@ function getElementTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): s
|
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function isReactiveCall(node: ts.CallExpression): boolean {
|
|
35
|
-
return ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
function visit(ctx: VisitContext, node: ts.Node): void {
|
|
39
|
-
if (
|
|
35
|
+
if (
|
|
36
|
+
ts.isCallExpression(node) &&
|
|
37
|
+
ts.isIdentifier(node.expression) &&
|
|
38
|
+
node.expression.text === 'reactive' &&
|
|
39
|
+
node.arguments.length > 0
|
|
40
|
+
) {
|
|
40
41
|
let arg = node.arguments[0],
|
|
41
42
|
expression = ts.isAsExpression(arg) ? arg.expression : arg;
|
|
42
43
|
|
|
@@ -94,16 +95,17 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
let parent = node.parent;
|
|
98
|
-
|
|
99
98
|
if (
|
|
100
99
|
ts.isPropertyAccessExpression(node) &&
|
|
101
100
|
node.name.text === 'length' &&
|
|
102
|
-
(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
(
|
|
102
|
+
!node.parent ||
|
|
103
|
+
(
|
|
104
|
+
!(ts.isBinaryExpression(node.parent) && node.parent.left === node) &&
|
|
105
|
+
!ts.isPostfixUnaryExpression(node.parent) &&
|
|
106
|
+
!ts.isPrefixUnaryExpression(node.parent)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
107
109
|
) {
|
|
108
110
|
let name = ast.getExpressionName(node.expression);
|
|
109
111
|
|
|
@@ -126,12 +128,10 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
126
128
|
if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
|
|
127
129
|
ctx.replacements.push({
|
|
128
130
|
node,
|
|
129
|
-
generate: (sf) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return `${element.expression.getText(sf)}.$set(${index}, ${value})`;
|
|
134
|
-
}
|
|
131
|
+
generate: (sf) => `${element.expression.getText(sf)}.$set(
|
|
132
|
+
${element.argumentExpression.getText(sf)},
|
|
133
|
+
${node.right.getText(sf)}
|
|
134
|
+
)`
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
}
|
package/src/compiler/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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
|
function hasReactiveImport(sourceFile: ts.SourceFile): boolean {
|
|
@@ -84,31 +85,33 @@ const plugin: Plugin = {
|
|
|
84
85
|
|
|
85
86
|
let bindings: Bindings = new Map(),
|
|
86
87
|
importsIntent: ImportIntent[] = [],
|
|
88
|
+
isReactiveCall = (node: ts.Node) => isReactiveCallExpression(ctx.checker, node),
|
|
87
89
|
prepend: string[] = [],
|
|
88
90
|
replacements: ReplacementIntent[] = [];
|
|
89
91
|
|
|
92
|
+
// Run primitives transform first (tracks bindings for signal/computed)
|
|
93
|
+
replacements.push(...primitives(ctx.sourceFile, bindings, isReactiveCall, ctx.checker));
|
|
94
|
+
|
|
90
95
|
// Run object transform
|
|
91
96
|
let objectResult = object(ctx.sourceFile, bindings, ctx.checker);
|
|
92
97
|
|
|
93
98
|
prepend.push(...objectResult.prepend);
|
|
94
|
-
replacements.push(...objectResult.replacements);
|
|
95
|
-
|
|
96
|
-
// Run array transform
|
|
97
|
-
let arrayResult = array(ctx.sourceFile, bindings, ctx.checker);
|
|
98
|
-
|
|
99
|
-
replacements.push(...arrayResult);
|
|
99
|
+
replacements.push(...objectResult.replacements, ...array(ctx.sourceFile, bindings, ctx.checker));
|
|
100
100
|
|
|
101
101
|
// Find remaining reactive() calls that weren't transformed and replace with namespace version
|
|
102
102
|
let transformedNodes = new Set(replacements.map(r => r.node));
|
|
103
103
|
|
|
104
104
|
function findRemainingReactiveCalls(node: ts.Node): void {
|
|
105
|
-
if (
|
|
106
|
-
let call = node;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
if (isReactiveCall(node)) {
|
|
106
|
+
let call = node as ts.CallExpression;
|
|
107
|
+
|
|
108
|
+
// Check if call or its expression has already been transformed
|
|
109
|
+
if (!transformedNodes.has(call) && !transformedNodes.has(call.expression)) {
|
|
110
|
+
replacements.push({
|
|
111
|
+
generate: () => `${COMPILER_NAMESPACE}.reactive(${call.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
|
|
112
|
+
node: call
|
|
113
|
+
});
|
|
114
|
+
}
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
ts.forEachChild(node, findRemainingReactiveCalls);
|
package/src/compiler/object.ts
CHANGED
|
@@ -166,12 +166,8 @@ function isStaticValue(node: ts.Node): boolean {
|
|
|
166
166
|
return false;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
function isReactiveCall(node: ts.CallExpression): boolean {
|
|
170
|
-
return ts.isIdentifier(node.expression) && node.expression.text === "reactive";
|
|
171
|
-
}
|
|
172
|
-
|
|
173
169
|
function visit(ctx: VisitContext, node: ts.Node): void {
|
|
174
|
-
if (ts.isCallExpression(node) &&
|
|
170
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive') {
|
|
175
171
|
let arg = node.arguments[0];
|
|
176
172
|
|
|
177
173
|
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
@@ -246,15 +242,14 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Type
|
|
|
246
242
|
let call = ctx.calls[i];
|
|
247
243
|
|
|
248
244
|
prepend.push(buildClassCode(call.className, call.properties));
|
|
249
|
-
|
|
250
245
|
replacements.push({
|
|
251
|
-
node: call.node,
|
|
252
246
|
generate: () => ` new ${call.className}(${
|
|
253
247
|
call.properties
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
})
|
|
248
|
+
.filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
|
|
249
|
+
.map(p => p.valueText)
|
|
250
|
+
.join(', ')
|
|
251
|
+
})`,
|
|
252
|
+
node: call.node,
|
|
258
253
|
});
|
|
259
254
|
}
|
|
260
255
|
|
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
checker?: ts.TypeChecker;
|
|
16
|
+
isReactiveCall: (node: ts.Node) => boolean;
|
|
17
|
+
replacements: ReplacementIntent[];
|
|
18
|
+
scopedBindings: ScopeBinding[];
|
|
19
|
+
sourceFile: ts.SourceFile;
|
|
20
|
+
tmpCounter: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const COMPOUND_OPERATORS = new Map<ts.SyntaxKind, string>([
|
|
25
|
+
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, '&&'],
|
|
26
|
+
[ts.SyntaxKind.AmpersandEqualsToken, '&'],
|
|
27
|
+
[ts.SyntaxKind.AsteriskAsteriskEqualsToken, '**'],
|
|
28
|
+
[ts.SyntaxKind.AsteriskEqualsToken, '*'],
|
|
29
|
+
[ts.SyntaxKind.BarBarEqualsToken, '||'],
|
|
30
|
+
[ts.SyntaxKind.BarEqualsToken, '|'],
|
|
31
|
+
[ts.SyntaxKind.CaretEqualsToken, '^'],
|
|
32
|
+
[ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, '>>'],
|
|
33
|
+
[ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, '>>>'],
|
|
34
|
+
[ts.SyntaxKind.LessThanLessThanEqualsToken, '<<'],
|
|
35
|
+
[ts.SyntaxKind.MinusEqualsToken, '-'],
|
|
36
|
+
[ts.SyntaxKind.PercentEqualsToken, '%'],
|
|
37
|
+
[ts.SyntaxKind.PlusEqualsToken, '+'],
|
|
38
|
+
[ts.SyntaxKind.QuestionQuestionEqualsToken, '??'],
|
|
39
|
+
[ts.SyntaxKind.SlashEqualsToken, '/']
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function isInScope(reference: ts.Node, binding: ScopeBinding): boolean {
|
|
44
|
+
let current: ts.Node | undefined = reference;
|
|
45
|
+
|
|
46
|
+
while (current) {
|
|
47
|
+
if (current === binding.scope) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
current = current.parent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function visit(ctx: TransformContext, node: ts.Node): void {
|
|
58
|
+
if (ctx.isReactiveCall(node)) {
|
|
59
|
+
let call = node as ts.CallExpression;
|
|
60
|
+
|
|
61
|
+
if (call.arguments.length > 0) {
|
|
62
|
+
let arg = call.arguments[0],
|
|
63
|
+
classification: COMPILER_TYPES | null = COMPILER_TYPES.Signal;
|
|
64
|
+
|
|
65
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
66
|
+
classification = COMPILER_TYPES.Computed;
|
|
67
|
+
}
|
|
68
|
+
else if (ts.isArrayLiteralExpression(arg) || ts.isObjectLiteralExpression(arg)) {
|
|
69
|
+
classification = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (classification) {
|
|
73
|
+
let varName: string | null = null;
|
|
74
|
+
|
|
75
|
+
if (call.parent && ts.isVariableDeclaration(call.parent) && ts.isIdentifier(call.parent.name)) {
|
|
76
|
+
varName = call.parent.name.text;
|
|
77
|
+
}
|
|
78
|
+
else if (
|
|
79
|
+
call.parent &&
|
|
80
|
+
ts.isBinaryExpression(call.parent) &&
|
|
81
|
+
call.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
82
|
+
ts.isIdentifier(call.parent.left)
|
|
83
|
+
) {
|
|
84
|
+
varName = call.parent.left.text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (varName) {
|
|
88
|
+
let current = call.parent,
|
|
89
|
+
scope;
|
|
90
|
+
|
|
91
|
+
while (current) {
|
|
92
|
+
if (
|
|
93
|
+
ts.isArrowFunction(current) ||
|
|
94
|
+
ts.isBlock(current) ||
|
|
95
|
+
ts.isForInStatement(current) ||
|
|
96
|
+
ts.isForOfStatement(current) ||
|
|
97
|
+
ts.isForStatement(current) ||
|
|
98
|
+
ts.isFunctionDeclaration(current) ||
|
|
99
|
+
ts.isFunctionExpression(current) ||
|
|
100
|
+
ts.isSourceFile(current)
|
|
101
|
+
) {
|
|
102
|
+
scope = current;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
current = current.parent;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!scope) {
|
|
109
|
+
scope = call.getSourceFile();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ctx.scopedBindings.push({ name: varName, scope, type: classification });
|
|
113
|
+
ctx.bindings.set(varName, classification);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Replace just the 'reactive' identifier with the appropriate namespace function
|
|
117
|
+
ctx.replacements.push({
|
|
118
|
+
generate: () => classification === COMPILER_TYPES.Computed
|
|
119
|
+
? `${COMPILER_NAMESPACE}.computed`
|
|
120
|
+
: `${COMPILER_NAMESPACE}.signal`,
|
|
121
|
+
node: call.expression
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Continue visiting children - inner identifiers will get their own ReplacementIntents
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
ts.isIdentifier(node) &&
|
|
131
|
+
node.parent &&
|
|
132
|
+
!(ts.isVariableDeclaration(node.parent) && node.parent.name === node)
|
|
133
|
+
) {
|
|
134
|
+
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
|
|
135
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let bindings = ctx.scopedBindings,
|
|
140
|
+
binding,
|
|
141
|
+
name = node.text;
|
|
142
|
+
|
|
143
|
+
for (let i = 0, n = bindings.length; i < n; i++) {
|
|
144
|
+
let b = bindings[i];
|
|
145
|
+
|
|
146
|
+
if (b.name === name && isInScope(node, b)) {
|
|
147
|
+
binding = b;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (binding && node.parent) {
|
|
152
|
+
let parent = node.parent;
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
!(
|
|
156
|
+
ts.isBinaryExpression(parent) &&
|
|
157
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
158
|
+
ctx.isReactiveCall(parent.right)
|
|
159
|
+
) &&
|
|
160
|
+
!(ts.isTypeOfExpression(parent) && parent.expression === node)
|
|
161
|
+
) {
|
|
162
|
+
let writeCtx;
|
|
163
|
+
|
|
164
|
+
if (ts.isBinaryExpression(parent) && parent.left === node) {
|
|
165
|
+
let op = parent.operatorToken.kind;
|
|
166
|
+
|
|
167
|
+
if (op === ts.SyntaxKind.EqualsToken) {
|
|
168
|
+
writeCtx = 'simple';
|
|
169
|
+
}
|
|
170
|
+
else if (COMPOUND_OPERATORS.has(op)) {
|
|
171
|
+
writeCtx = 'compound';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
|
|
175
|
+
let op = parent.operator;
|
|
176
|
+
|
|
177
|
+
if (op === ts.SyntaxKind.MinusMinusToken || op === ts.SyntaxKind.PlusPlusToken) {
|
|
178
|
+
writeCtx = 'increment';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (writeCtx) {
|
|
183
|
+
if (binding.type !== COMPILER_TYPES.Computed) {
|
|
184
|
+
if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
|
|
185
|
+
let right = parent.right;
|
|
186
|
+
|
|
187
|
+
ctx.replacements.push({
|
|
188
|
+
generate: (sf) => `${COMPILER_NAMESPACE}.write(${name}, ${right.getText(sf)})`,
|
|
189
|
+
node: parent
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
|
|
193
|
+
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind) ?? '+',
|
|
194
|
+
right = parent.right;
|
|
195
|
+
|
|
196
|
+
ctx.replacements.push({
|
|
197
|
+
generate: (sf) => `${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${op} ${right.getText(sf)})`,
|
|
198
|
+
node: parent
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (writeCtx === 'increment') {
|
|
202
|
+
let delta = (parent as ts.PostfixUnaryExpression | ts.PrefixUnaryExpression).operator === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1',
|
|
203
|
+
isPrefix = ts.isPrefixUnaryExpression(parent);
|
|
204
|
+
|
|
205
|
+
if (ts.isExpressionStatement(parent.parent)) {
|
|
206
|
+
ctx.replacements.push({
|
|
207
|
+
generate: () => `${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${delta})`,
|
|
208
|
+
node: parent
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else if (isPrefix) {
|
|
212
|
+
ctx.replacements.push({
|
|
213
|
+
generate: () => `(${COMPILER_NAMESPACE}.write(${name}, ${name}.value ${delta}), ${name}.value)`,
|
|
214
|
+
node: parent
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
let tmp = `_t${ctx.tmpCounter++}`;
|
|
219
|
+
|
|
220
|
+
ctx.replacements.push({
|
|
221
|
+
generate: () => `((${tmp}) => (${COMPILER_NAMESPACE}.write(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
|
|
222
|
+
node: parent
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
ctx.replacements.push({
|
|
230
|
+
generate: () => `${COMPILER_NAMESPACE}.read(${name})`,
|
|
231
|
+
node
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
ts.forEachChild(node, n => visit(ctx, n));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
export default (
|
|
243
|
+
sourceFile: ts.SourceFile,
|
|
244
|
+
bindings: Bindings,
|
|
245
|
+
isReactiveCall: (node: ts.Node) => boolean,
|
|
246
|
+
checker?: ts.TypeChecker
|
|
247
|
+
) => {
|
|
248
|
+
let ctx: TransformContext = {
|
|
249
|
+
bindings,
|
|
250
|
+
checker,
|
|
251
|
+
isReactiveCall,
|
|
252
|
+
replacements: [],
|
|
253
|
+
scopedBindings: [],
|
|
254
|
+
sourceFile,
|
|
255
|
+
tmpCounter: 0
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
visit(ctx, sourceFile);
|
|
259
|
+
|
|
260
|
+
return ctx.replacements;
|
|
261
|
+
};
|
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);
|