@esportsplus/reactivity 0.28.1 → 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.
@@ -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;
@@ -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,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
- 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
  }
@@ -1,13 +1,51 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { ast, imports } from '@esportsplus/typescript/compiler';
2
+ 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
  }
9
- function isReactiveCallNode(node) {
10
- return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === COMPILER_ENTRYPOINT;
10
+ function isReactiveSymbol(checker, node) {
11
+ let symbol = checker.getSymbolAtLocation(node);
12
+ if (!symbol) {
13
+ return false;
14
+ }
15
+ if (symbol.flags & ts.SymbolFlags.Alias) {
16
+ symbol = checker.getAliasedSymbol(symbol);
17
+ }
18
+ let declarations = symbol.getDeclarations();
19
+ if (!declarations || declarations.length === 0) {
20
+ return false;
21
+ }
22
+ for (let i = 0, n = declarations.length; i < n; i++) {
23
+ let decl = declarations[i], sourceFile = decl.getSourceFile();
24
+ if (sourceFile.fileName.includes(PACKAGE) || sourceFile.fileName.includes('reactivity')) {
25
+ if (symbol.name === COMPILER_ENTRYPOINT) {
26
+ return true;
27
+ }
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+ function isReactiveCallExpression(checker, node) {
33
+ if (!ts.isCallExpression(node)) {
34
+ return false;
35
+ }
36
+ let expr = node.expression;
37
+ if (ts.isIdentifier(expr)) {
38
+ if (expr.text === COMPILER_ENTRYPOINT) {
39
+ return true;
40
+ }
41
+ if (checker) {
42
+ return isReactiveSymbol(checker, expr);
43
+ }
44
+ }
45
+ if (ts.isPropertyAccessExpression(expr) && expr.name.text === COMPILER_ENTRYPOINT && checker) {
46
+ return isReactiveSymbol(checker, expr);
47
+ }
48
+ return false;
11
49
  }
12
50
  const plugin = {
13
51
  patterns: ['reactive(', 'reactive<'],
@@ -15,21 +53,30 @@ const plugin = {
15
53
  if (!hasReactiveImport(ctx.sourceFile)) {
16
54
  return {};
17
55
  }
18
- 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));
19
58
  let objectResult = object(ctx.sourceFile, bindings, ctx.checker);
20
59
  prepend.push(...objectResult.prepend);
21
- replacements.push(...objectResult.replacements);
22
- let arrayResult = array(ctx.sourceFile, bindings, ctx.checker);
23
- replacements.push(...arrayResult);
24
- if (replacements.length > 0 || prepend.length > 0) {
25
- let remove = [];
26
- if (!ast.hasMatch(ctx.sourceFile, isReactiveCallNode) || replacements.length > 0) {
27
- remove.push(COMPILER_ENTRYPOINT);
60
+ replacements.push(...objectResult.replacements, ...array(ctx.sourceFile, bindings, ctx.checker));
61
+ let transformedNodes = new Set(replacements.map(r => r.node));
62
+ function findRemainingReactiveCalls(node) {
63
+ if (isReactiveCall(node)) {
64
+ let call = node;
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
+ }
28
71
  }
72
+ ts.forEachChild(node, findRemainingReactiveCalls);
73
+ }
74
+ findRemainingReactiveCalls(ctx.sourceFile);
75
+ if (replacements.length > 0 || prepend.length > 0) {
29
76
  importsIntent.push({
30
77
  namespace: COMPILER_NAMESPACE,
31
78
  package: PACKAGE,
32
- remove
79
+ remove: [COMPILER_ENTRYPOINT]
33
80
  });
34
81
  }
35
82
  return {
@@ -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;
@@ -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
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "type": "module",
33
33
  "types": "build/index.d.ts",
34
- "version": "0.28.1",
34
+ "version": "0.29.0",
35
35
  "scripts": {
36
36
  "build": "tsc",
37
37
  "build:test": "pnpm build && vite build --config test/vite.config.ts",
@@ -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 (ts.isCallExpression(node) && isReactiveCall(node) && node.arguments.length > 0) {
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
- (!!parent && (
103
- (ts.isBinaryExpression(parent) && parent.left === node) ||
104
- ts.isPostfixUnaryExpression(parent) ||
105
- ts.isPrefixUnaryExpression(parent)
106
- )) === false
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
- let index = element.argumentExpression.getText(sf),
131
- value = node.right.getText(sf);
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
  }
@@ -1,18 +1,77 @@
1
1
  import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
- import { ast, imports } from '@esportsplus/typescript/compiler';
3
+ import { imports } from '@esportsplus/typescript/compiler';
4
4
  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 {
11
12
  return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
12
13
  }
13
14
 
14
- function isReactiveCallNode(node: ts.Node): boolean {
15
- return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === COMPILER_ENTRYPOINT;
15
+ function isReactiveSymbol(checker: ts.TypeChecker, node: ts.Node): boolean {
16
+ let symbol = checker.getSymbolAtLocation(node);
17
+
18
+ if (!symbol) {
19
+ return false;
20
+ }
21
+
22
+ // Follow aliases to original symbol
23
+ if (symbol.flags & ts.SymbolFlags.Alias) {
24
+ symbol = checker.getAliasedSymbol(symbol);
25
+ }
26
+
27
+ let declarations = symbol.getDeclarations();
28
+
29
+ if (!declarations || declarations.length === 0) {
30
+ return false;
31
+ }
32
+
33
+ for (let i = 0, n = declarations.length; i < n; i++) {
34
+ let decl = declarations[i],
35
+ sourceFile = decl.getSourceFile();
36
+
37
+ // Check if declaration is from our package
38
+ if (sourceFile.fileName.includes(PACKAGE) || sourceFile.fileName.includes('reactivity')) {
39
+ // Verify it's the reactive export
40
+ if (symbol.name === COMPILER_ENTRYPOINT) {
41
+ return true;
42
+ }
43
+ }
44
+ }
45
+
46
+ return false;
47
+ }
48
+
49
+ function isReactiveCallExpression(checker: ts.TypeChecker | undefined, node: ts.Node): node is ts.CallExpression {
50
+ if (!ts.isCallExpression(node)) {
51
+ return false;
52
+ }
53
+
54
+ let expr = node.expression;
55
+
56
+ // Direct call: reactive(...) or aliasedName(...)
57
+ if (ts.isIdentifier(expr)) {
58
+ // Fast path: literal "reactive"
59
+ if (expr.text === COMPILER_ENTRYPOINT) {
60
+ return true;
61
+ }
62
+
63
+ // Use checker to resolve aliases
64
+ if (checker) {
65
+ return isReactiveSymbol(checker, expr);
66
+ }
67
+ }
68
+
69
+ // Property access: ns.reactive(...)
70
+ if (ts.isPropertyAccessExpression(expr) && expr.name.text === COMPILER_ENTRYPOINT && checker) {
71
+ return isReactiveSymbol(checker, expr);
72
+ }
73
+
74
+ return false;
16
75
  }
17
76
 
18
77
 
@@ -26,34 +85,46 @@ const plugin: Plugin = {
26
85
 
27
86
  let bindings: Bindings = new Map(),
28
87
  importsIntent: ImportIntent[] = [],
88
+ isReactiveCall = (node: ts.Node) => isReactiveCallExpression(ctx.checker, node),
29
89
  prepend: string[] = [],
30
90
  replacements: ReplacementIntent[] = [];
31
91
 
92
+ // Run primitives transform first (tracks bindings for signal/computed)
93
+ replacements.push(...primitives(ctx.sourceFile, bindings, isReactiveCall, ctx.checker));
94
+
32
95
  // Run object transform
33
96
  let objectResult = object(ctx.sourceFile, bindings, ctx.checker);
34
97
 
35
98
  prepend.push(...objectResult.prepend);
36
- replacements.push(...objectResult.replacements);
99
+ replacements.push(...objectResult.replacements, ...array(ctx.sourceFile, bindings, ctx.checker));
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 (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
+ }
115
+ }
37
116
 
38
- // Run array transform
39
- let arrayResult = array(ctx.sourceFile, bindings, ctx.checker);
117
+ ts.forEachChild(node, findRemainingReactiveCalls);
118
+ }
40
119
 
41
- replacements.push(...arrayResult);
120
+ findRemainingReactiveCalls(ctx.sourceFile);
42
121
 
43
122
  // Build import intent
44
123
  if (replacements.length > 0 || prepend.length > 0) {
45
- let remove: string[] = [];
46
-
47
- // Check if we still have reactive() calls after transform
48
- // This is a heuristic - if we have no replacements for reactive calls, keep the import
49
- if (!ast.hasMatch(ctx.sourceFile, isReactiveCallNode) || replacements.length > 0) {
50
- remove.push(COMPILER_ENTRYPOINT);
51
- }
52
-
53
124
  importsIntent.push({
54
125
  namespace: COMPILER_NAMESPACE,
55
126
  package: PACKAGE,
56
- remove
127
+ remove: [COMPILER_ENTRYPOINT]
57
128
  });
58
129
  }
59
130
 
@@ -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) && isReactiveCall(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
- .filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
255
- .map(p => p.valueText)
256
- .join(', ')
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
+ };
@@ -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);