@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.
@@ -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, checker?: ts.TypeChecker) => ReplacementIntent[];
4
+ declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings) => ReplacementIntent[];
5
5
  export default _default;
@@ -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) && isReactiveCall(node) && node.arguments.length > 0) {
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.getPropertyPathString(node.initializer);
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
- (!!parent && ((ts.isBinaryExpression(parent) && parent.left === node) ||
69
- ts.isPostfixUnaryExpression(parent) ||
70
- ts.isPrefixUnaryExpression(parent))) === false) {
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
- let index = element.argumentExpression.getText(sf), value = node.right.getText(sf);
88
- return `${element.expression.getText(sf)}.$set(${index}, ${value})`;
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, checker) => {
95
+ export default (sourceFile, bindings) => {
96
96
  let ctx = {
97
97
  bindings,
98
- checker,
99
98
  replacements: [],
100
99
  sourceFile
101
100
  };
@@ -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
- function hasReactiveImport(sourceFile) {
7
- return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
8
- }
9
- function isReactiveSymbol(checker, node) {
10
- let symbol = checker.getSymbolAtLocation(node);
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 isReactiveSymbol(checker, expr);
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 isReactiveSymbol(checker, expr);
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 (!hasReactiveImport(ctx.sourceFile)) {
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
- let objectResult = object(ctx.sourceFile, bindings, ctx.checker);
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, checker?: ts.TypeChecker) => ObjectTransformResult;
8
+ declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings) => ObjectTransformResult;
9
9
  export default _default;
@@ -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) && isReactiveCall(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, checker) => {
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.25.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.28.2",
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",
@@ -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 isReactiveCall(node: ts.CallExpression): boolean {
35
- return ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
36
- }
37
-
38
- function visit(ctx: VisitContext, node: ts.Node): void {
39
- if (ts.isCallExpression(node) && isReactiveCall(node) && node.arguments.length > 0) {
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.getPropertyPathString(node.initializer);
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
- (!!parent && (
103
- (ts.isBinaryExpression(parent) && parent.left === node) ||
104
- ts.isPostfixUnaryExpression(parent) ||
105
- ts.isPrefixUnaryExpression(parent)
106
- )) === false
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
- let index = element.argumentExpression.getText(sf),
131
- value = node.right.getText(sf);
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, checker?: ts.TypeChecker): ReplacementIntent[] => {
144
- let ctx: VisitContext = {
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
  };
@@ -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
- function hasReactiveImport(sourceFile: ts.SourceFile): boolean {
11
- return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
12
- }
13
-
14
- function isReactiveSymbol(checker: ts.TypeChecker, node: ts.Node): boolean {
15
- let symbol = checker.getSymbolAtLocation(node);
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
- for (let i = 0, n = declarations.length; i < n; i++) {
33
- let decl = declarations[i],
34
- sourceFile = decl.getSourceFile();
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
- // Check if declaration is from our package
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 false;
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 isReactiveSymbol(checker, expr);
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 isReactiveSymbol(checker, expr);
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 (!hasReactiveImport(ctx.sourceFile)) {
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, ctx.checker);
89
+ let objectResult = object(ctx.sourceFile, bindings);
92
90
 
93
91
  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);
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) {
@@ -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) && isReactiveCall(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, checker?: ts.TypeChecker): ObjectTransformResult => {
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
+ };
@@ -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 extends unknown[] | Record<PropertyKey, unknown>>(input: T): 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;
@@ -1,171 +1,87 @@
1
- // Test: Reactive Primitives (Standalone + Object-wrapped)
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('initial');
6
+ let name = reactive('test');
13
7
  let flag = reactive(true);
8
+ let nullable = reactive<string | null>(null);
14
9
 
15
- console.log('Initial count:', count);
16
- console.log('Initial name:', name);
17
- console.log('Initial flag:', flag);
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
- // Simple assignment
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 = 'updated';
22
+ name = 'world';
22
23
  flag = false;
23
24
 
24
- console.log('After assignment - count:', count);
25
- console.log('After assignment - name:', name);
26
- console.log('After assignment - flag:', flag);
27
-
28
-
29
- // =============================================================================
30
- // Standalone Computed Primitives
31
- // =============================================================================
32
-
33
- console.log('\n=== Standalone Computed Primitives ===');
34
-
35
- let base = reactive(10);
36
- let doubled = reactive(() => base * 2);
37
- let quadrupled = reactive(() => doubled * 2);
38
-
39
- console.log('base:', base);
40
- console.log('doubled:', doubled);
41
- console.log('quadrupled:', quadrupled);
42
-
43
- base = 5;
44
- console.log('After base = 5:');
45
- console.log(' doubled:', doubled);
46
- console.log(' quadrupled:', quadrupled);
47
-
48
-
49
- // =============================================================================
50
- // Compound Assignments with Standalone Primitives
51
- // =============================================================================
52
-
53
- console.log('\n=== Compound Assignments ===');
54
-
55
- let value = reactive(10);
56
-
57
- value += 5;
58
- console.log('After += 5:', value);
59
-
60
- value -= 3;
61
- console.log('After -= 3:', value);
62
-
63
- value *= 2;
64
- console.log('After *= 2:', value);
65
-
66
-
67
- // =============================================================================
68
- // Increment/Decrement with Standalone Primitives
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
- console.log('obj.value:', obj.value);
99
- console.log('obj.scaled:', obj.scaled);
100
-
101
- multiplier = 3;
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
- console.log('Initial count:', state.count);
78
+ // Arrow function with reactive reads
79
+ const calc = () => count * 2;
164
80
 
165
- state.count = 10;
166
- state.name = 'updated';
167
- state.flag = false;
81
+ // Reactive in loop
82
+ for (let i = 0; i < 10; i++) {
83
+ count += i;
84
+ }
168
85
 
169
- console.log('After assignment - count:', state.count);
170
- console.log('After assignment - name:', state.name);
171
- console.log('After assignment - flag:', state.flag);
86
+ // Reassignment with new reactive
87
+ count = reactive(100);