@esportsplus/reactivity 0.34.0 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +9 -7
  2. package/bench/reactive/array.bench.ts +17 -17
  3. package/build/compiler/array.d.ts +2 -2
  4. package/build/compiler/array.js +10 -52
  5. package/build/compiler/constants.d.ts +4 -4
  6. package/build/compiler/constants.js +20 -4
  7. package/build/compiler/index.js +25 -38
  8. package/build/compiler/object.d.ts +2 -2
  9. package/build/compiler/object.js +5 -16
  10. package/build/compiler/primitives.d.ts +6 -2
  11. package/build/compiler/primitives.js +31 -38
  12. package/build/compiler/types.d.ts +3 -1
  13. package/build/constants.d.ts +2 -2
  14. package/build/constants.js +2 -2
  15. package/build/reactive/array.d.ts +1 -1
  16. package/build/reactive/array.js +26 -15
  17. package/build/reactive/index.js +5 -14
  18. package/build/reactive/object.js +1 -1
  19. package/build/system.d.ts +1 -4
  20. package/build/system.js +105 -83
  21. package/build/types.d.ts +2 -7
  22. package/package.json +5 -5
  23. package/pnpm-workspace.yaml +2 -1
  24. package/src/compiler/array.ts +12 -62
  25. package/src/compiler/constants.ts +21 -5
  26. package/src/compiler/index.ts +38 -69
  27. package/src/compiler/object.ts +7 -24
  28. package/src/compiler/primitives.ts +51 -49
  29. package/src/compiler/types.ts +4 -1
  30. package/src/constants.ts +4 -4
  31. package/src/reactive/array.ts +33 -17
  32. package/src/reactive/index.ts +5 -17
  33. package/src/reactive/object.ts +1 -1
  34. package/src/system.ts +138 -101
  35. package/src/types.ts +2 -9
  36. package/test/async-computed.test.ts +2 -2
  37. package/test/compiler/compiler.test.ts +38 -22
  38. package/test/effects.test.ts +37 -1
  39. package/test/lib/uncaught.ts +26 -0
  40. package/test/reactive/array.test.ts +169 -99
  41. package/test/reactive/nested.test.ts +14 -14
  42. package/test/reactive/objects.test.ts +1 -1
  43. package/test/reactive/reactive.test.ts +49 -0
  44. package/test/system.test.ts +130 -22
@@ -1,34 +1,18 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { ast, imports } from '@esportsplus/typescript/compiler';
2
+ import { ast } from '@esportsplus/typescript/compiler';
3
3
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
4
- import { ENTRYPOINT, NAMESPACE, PACKAGE_NAME, TYPES } from './constants';
5
- import type { Bindings } from './types';
4
+ import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants';
5
+ import type { Bindings, IsReactiveCall } from './types';
6
6
 
7
7
 
8
8
  type VisitContext = {
9
9
  bindings: Bindings;
10
- checker: ts.Checker | undefined;
10
+ isReactiveCall: IsReactiveCall;
11
11
  replacements: ReplacementIntent[];
12
12
  sourceFile: ts.SourceFile;
13
13
  };
14
14
 
15
15
 
16
- function isReactiveCall(checker: ts.Checker | undefined, node: ts.Node): node is ts.CallExpression {
17
- if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
18
- return false;
19
- }
20
-
21
- let expr = node.expression;
22
-
23
- // Use checker to verify symbol origin (handles re-exports)
24
- if (checker) {
25
- return imports.includes(checker, expr, PACKAGE_NAME, ENTRYPOINT);
26
- }
27
-
28
- // Fallback without checker: match by name only
29
- return expr.text === ENTRYPOINT;
30
- }
31
-
32
16
  function getElementTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): string | null {
33
17
  if (ts.isArrayTypeNode(typeNode)) {
34
18
  return typeNode.elementType.getText(sourceFile);
@@ -47,42 +31,8 @@ function getElementTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): s
47
31
  return null;
48
32
  }
49
33
 
50
- function getOperator(kind: ts.SyntaxKind) {
51
- switch (kind) {
52
- case ts.SyntaxKind.PlusEqualsToken: return '+';
53
- case ts.SyntaxKind.MinusEqualsToken: return '-';
54
- case ts.SyntaxKind.AsteriskEqualsToken: return '*';
55
- case ts.SyntaxKind.SlashEqualsToken: return '/';
56
- case ts.SyntaxKind.PercentEqualsToken: return '%';
57
- case ts.SyntaxKind.AsteriskAsteriskEqualsToken: return '**';
58
- case ts.SyntaxKind.AmpersandEqualsToken: return '&';
59
- case ts.SyntaxKind.BarEqualsToken: return '|';
60
- case ts.SyntaxKind.CaretEqualsToken: return '^';
61
- default: return '+';
62
- }
63
- }
64
-
65
- function isAssignmentOperator(kind: ts.SyntaxKind) {
66
- return kind === ts.SyntaxKind.EqualsToken ||
67
- kind === ts.SyntaxKind.PlusEqualsToken ||
68
- kind === ts.SyntaxKind.MinusEqualsToken ||
69
- kind === ts.SyntaxKind.AsteriskEqualsToken ||
70
- kind === ts.SyntaxKind.SlashEqualsToken ||
71
- kind === ts.SyntaxKind.PercentEqualsToken ||
72
- kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken ||
73
- kind === ts.SyntaxKind.LessThanLessThanEqualsToken ||
74
- kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken ||
75
- kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken ||
76
- kind === ts.SyntaxKind.AmpersandEqualsToken ||
77
- kind === ts.SyntaxKind.BarEqualsToken ||
78
- kind === ts.SyntaxKind.CaretEqualsToken ||
79
- kind === ts.SyntaxKind.BarBarEqualsToken ||
80
- kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
81
- kind === ts.SyntaxKind.QuestionQuestionEqualsToken;
82
- }
83
-
84
34
  function visit(ctx: VisitContext, node: ts.Node): void {
85
- if (isReactiveCall(ctx.checker, node) && node.arguments.length > 0) {
35
+ if (ctx.isReactiveCall(node) && node.arguments.length > 0) {
86
36
  let arg = node.arguments[0],
87
37
  expression = ts.isAsExpression(arg) ? arg.expression : arg;
88
38
 
@@ -105,7 +55,7 @@ function visit(ctx: VisitContext, node: ts.Node): void {
105
55
  ctx.replacements.push({
106
56
  node,
107
57
  generate: (sf) => expression.elements.length > 0
108
- ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(...${expression.getText(sf)})`
58
+ ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${expression.getText(sf)})`
109
59
  : ` new ${NAMESPACE}.ReactiveArray${typeParam}()`
110
60
  });
111
61
  }
@@ -148,10 +98,10 @@ function visit(ctx: VisitContext, node: ts.Node): void {
148
98
  parent = node.parent;
149
99
 
150
100
  // arr.length = value OR arr.length += value
151
- if (parent && ts.isBinaryExpression(parent) && parent.left === node && isAssignmentOperator(parent.operatorToken.kind)) {
152
- let op = parent.operatorToken.kind;
101
+ if (parent && ts.isBinaryExpression(parent) && parent.left === node && (parent.operatorToken.kind === ts.SyntaxKind.EqualsToken || COMPOUND_OPERATORS.has(parent.operatorToken.kind))) {
102
+ let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
153
103
 
154
- if (op === ts.SyntaxKind.EqualsToken) {
104
+ if (op === undefined) {
155
105
  ctx.replacements.push({
156
106
  node: parent,
157
107
  generate: (sf) => `${expr.getText(sf)}.$length = ${parent.right.getText(sf)}`
@@ -160,7 +110,7 @@ function visit(ctx: VisitContext, node: ts.Node): void {
160
110
  else {
161
111
  ctx.replacements.push({
162
112
  node: parent,
163
- generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${getOperator(op)} ${parent.right.getText(sf)}`
113
+ generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} ${parent.right.getText(sf)}`
164
114
  });
165
115
  }
166
116
  }
@@ -238,10 +188,10 @@ function visit(ctx: VisitContext, node: ts.Node): void {
238
188
  }
239
189
 
240
190
 
241
- export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Checker): ReplacementIntent[] => {
191
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): ReplacementIntent[] => {
242
192
  let ctx: VisitContext = {
243
193
  bindings,
244
- checker,
194
+ isReactiveCall,
245
195
  replacements: [],
246
196
  sourceFile
247
197
  };
@@ -1,9 +1,26 @@
1
+ import { ts } from '@esportsplus/typescript';
1
2
  import { uid } from '@esportsplus/typescript/compiler';
2
3
 
3
4
 
4
- const ENTRYPOINT = 'reactive';
5
+ const COMPOUND_OPERATORS = new Map<ts.SyntaxKind, string>([
6
+ [ts.SyntaxKind.AmpersandAmpersandEqualsToken, '&&'],
7
+ [ts.SyntaxKind.AmpersandEqualsToken, '&'],
8
+ [ts.SyntaxKind.AsteriskAsteriskEqualsToken, '**'],
9
+ [ts.SyntaxKind.AsteriskEqualsToken, '*'],
10
+ [ts.SyntaxKind.BarBarEqualsToken, '||'],
11
+ [ts.SyntaxKind.BarEqualsToken, '|'],
12
+ [ts.SyntaxKind.CaretEqualsToken, '^'],
13
+ [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, '>>'],
14
+ [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, '>>>'],
15
+ [ts.SyntaxKind.LessThanLessThanEqualsToken, '<<'],
16
+ [ts.SyntaxKind.MinusEqualsToken, '-'],
17
+ [ts.SyntaxKind.PercentEqualsToken, '%'],
18
+ [ts.SyntaxKind.PlusEqualsToken, '+'],
19
+ [ts.SyntaxKind.QuestionQuestionEqualsToken, '??'],
20
+ [ts.SyntaxKind.SlashEqualsToken, '/']
21
+ ]);
5
22
 
6
- const ENTRYPOINT_REGEX = /\breactive\b/;
23
+ const ENTRYPOINT = 'reactive';
7
24
 
8
25
  const NAMESPACE = uid('reactivity');
9
26
 
@@ -11,12 +28,11 @@ const NAMESPACE = uid('reactivity');
11
28
  const TYPES = {
12
29
  Array: 0,
13
30
  Computed: 1,
14
- Object: 2,
15
- Signal: 3
31
+ Signal: 2
16
32
  } as const;
17
33
 
18
34
  type TYPES = typeof TYPES[keyof typeof TYPES];
19
35
 
20
36
 
21
- export { ENTRYPOINT, ENTRYPOINT_REGEX, NAMESPACE, TYPES };
37
+ export { COMPOUND_OPERATORS, ENTRYPOINT, NAMESPACE, TYPES };
22
38
  export { PACKAGE_NAME } from '../constants';
@@ -9,26 +9,6 @@ import object from './object';
9
9
  import primitives from './primitives';
10
10
 
11
11
 
12
- type FindRemainingContext = {
13
- checker: ts.Checker;
14
- replacements: ReplacementIntent[];
15
- sourceFile: ts.SourceFile;
16
- transformedNodes: Set<ts.Node>;
17
- };
18
-
19
-
20
- function findRemainingCalls(
21
- checker: ts.Checker,
22
- sourceFile: ts.SourceFile,
23
- transformedNodes: Set<ts.Node>
24
- ): ReplacementIntent[] {
25
- let ctx: FindRemainingContext = { checker, replacements: [], sourceFile, transformedNodes };
26
-
27
- visit(ctx, sourceFile);
28
-
29
- return ctx.replacements;
30
- }
31
-
32
12
  function isReactiveCallExpression(checker: ts.Checker, node: ts.Node): node is ts.CallExpression {
33
13
  if (!ts.isCallExpression(node)) {
34
14
  return false;
@@ -49,38 +29,13 @@ function isReactiveCallExpression(checker: ts.Checker, node: ts.Node): node is t
49
29
  return false;
50
30
  }
51
31
 
52
- function hasReactiveCalls(checker: ts.Checker, node: ts.Node): boolean {
53
- if (isReactiveCallExpression(checker, node)) {
54
- return true;
55
- }
56
-
57
- let found = false;
58
-
59
- node.forEachChild(child => {
60
- if (!found && hasReactiveCalls(checker, child)) {
61
- found = true;
62
- }
63
- });
64
-
65
- return found;
66
- }
67
-
68
- function visit(ctx: FindRemainingContext, node: ts.Node): void {
69
- if (isReactiveCallExpression(ctx.checker, node) && !ctx.transformedNodes.has(node) && !ctx.transformedNodes.has(node.expression)) {
70
- ctx.replacements.push({
71
- generate: () => `${NAMESPACE}.reactive(${node.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
72
- node
73
- });
74
- }
75
-
76
- node.forEachChild(n => visit(ctx, n));
77
- }
78
-
79
32
 
80
33
  export default {
81
34
  patterns: ['reactive(', 'reactive<'],
82
35
  transform: (ctx: TransformContext) => {
83
- if (!ctx.checker || !hasReactiveCalls(ctx.checker, ctx.sourceFile)) {
36
+ let checker = ctx.checker;
37
+
38
+ if (!checker) {
84
39
  return {};
85
40
  }
86
41
 
@@ -89,36 +44,50 @@ export default {
89
44
  imports: [] as ImportIntent[],
90
45
  prepend: [] as string[],
91
46
  replacements: [] as ReplacementIntent[]
92
- };
47
+ },
48
+ isReactiveCall = (node: ts.Node): node is ts.CallExpression => isReactiveCallExpression(checker, node),
49
+ sourceFile = ctx.sourceFile;
93
50
 
94
- // Run primitives transform first (tracks bindings for signal/computed)
95
- intents.replacements.push(
96
- ...primitives(ctx.sourceFile, bindings, (node: ts.Node) => isReactiveCallExpression(ctx.checker!, node))
97
- );
51
+ // Run primitives transform first (tracks bindings for signal/computed, collects every call)
52
+ let { calls, replacements } = primitives(sourceFile, bindings, isReactiveCall);
98
53
 
99
- // Run object transform
100
- let { prepend, replacements } = object(ctx.sourceFile, bindings, ctx.checker);
54
+ if (calls.length === 0) {
55
+ return {};
56
+ }
101
57
 
102
- intents.prepend.push(...prepend);
103
58
  intents.replacements.push(...replacements);
104
59
 
60
+ // Run object transform
61
+ let objects = object(sourceFile, bindings, isReactiveCall);
62
+
63
+ intents.prepend.push(...objects.prepend);
64
+ intents.replacements.push(...objects.replacements);
65
+
105
66
  // Run array transform separately ( avoid race conditions )
106
- intents.replacements.push(...array(ctx.sourceFile, bindings, ctx.checker));
107
-
108
- // Find remaining reactive() calls that weren't transformed and replace with namespace version
109
- intents.replacements.push(
110
- ...findRemainingCalls(ctx.checker, ctx.sourceFile, new Set(intents.replacements.map(r => r.node)))
111
- );
112
-
113
- // Build import intent
114
- if (intents.replacements.length > 0 || intents.prepend.length > 0) {
115
- intents.imports.push({
116
- namespace: NAMESPACE,
117
- package: PACKAGE_NAME,
118
- remove: [ENTRYPOINT]
67
+ intents.replacements.push(...array(sourceFile, bindings, isReactiveCall));
68
+
69
+ // Calls no transform claimed fall through to the runtime reactive()
70
+ let transformed = new Set(intents.replacements.map(r => r.node));
71
+
72
+ for (let i = 0, n = calls.length; i < n; i++) {
73
+ let call = calls[i];
74
+
75
+ if (transformed.has(call) || transformed.has(call.expression)) {
76
+ continue;
77
+ }
78
+
79
+ intents.replacements.push({
80
+ generate: () => `${NAMESPACE}.reactive(${call.arguments.map(a => a.getText(sourceFile)).join(', ')})`,
81
+ node: call
119
82
  });
120
83
  }
121
84
 
85
+ intents.imports.push({
86
+ namespace: NAMESPACE,
87
+ package: PACKAGE_NAME,
88
+ remove: [ENTRYPOINT]
89
+ });
90
+
122
91
  return intents;
123
92
  }
124
93
  };
@@ -1,8 +1,8 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { code, imports, uid } from '@esportsplus/typescript/compiler';
2
+ import { code, uid } from '@esportsplus/typescript/compiler';
3
3
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
4
- import { ENTRYPOINT, NAMESPACE, PACKAGE_NAME, TYPES } from './constants';
5
- import type { Bindings } from './types';
4
+ import { NAMESPACE, TYPES } from './constants';
5
+ import type { Bindings, IsReactiveCall } from './types';
6
6
 
7
7
 
8
8
  interface AnalyzedProperty {
@@ -28,7 +28,7 @@ interface ReactiveObjectCall {
28
28
  interface VisitContext {
29
29
  bindings: Bindings;
30
30
  calls: ReactiveObjectCall[];
31
- checker: ts.Checker | undefined;
31
+ isReactiveCall: IsReactiveCall;
32
32
  sourceFile: ts.SourceFile;
33
33
  }
34
34
 
@@ -216,24 +216,8 @@ function isStaticValue(node: ts.Node): boolean {
216
216
  (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand));
217
217
  }
218
218
 
219
- function isReactiveCall(checker: ts.Checker | undefined, node: ts.Node): node is ts.CallExpression {
220
- if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
221
- return false;
222
- }
223
-
224
- let expr = node.expression;
225
-
226
- // Use checker to verify symbol origin (handles re-exports)
227
- if (checker) {
228
- return imports.includes(checker, expr, PACKAGE_NAME, ENTRYPOINT);
229
- }
230
-
231
- // Fallback without checker: match by name only
232
- return expr.text === ENTRYPOINT;
233
- }
234
-
235
219
  function visit(ctx: VisitContext, node: ts.Node): void {
236
- if (isReactiveCall(ctx.checker, node)) {
220
+ if (ctx.isReactiveCall(node)) {
237
221
  let arg = node.arguments[0];
238
222
 
239
223
  if (arg && ts.isObjectLiteralExpression(arg)) {
@@ -243,7 +227,6 @@ function visit(ctx: VisitContext, node: ts.Node): void {
243
227
 
244
228
  if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
245
229
  varname = node.parent.name.text;
246
- ctx.bindings.set(varname, TYPES.Object);
247
230
  }
248
231
 
249
232
  for (let i = 0, n = props.length; i < n; i++) {
@@ -282,11 +265,11 @@ function visit(ctx: VisitContext, node: ts.Node): void {
282
265
  }
283
266
 
284
267
 
285
- export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Checker): ObjectTransformResult => {
268
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): ObjectTransformResult => {
286
269
  let ctx: VisitContext = {
287
270
  bindings,
288
271
  calls: [],
289
- checker,
272
+ isReactiveCall,
290
273
  sourceFile
291
274
  };
292
275
 
@@ -1,10 +1,11 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
3
- import { NAMESPACE, TYPES } from './constants';
4
- import type { Bindings } from './types';
3
+ import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants';
4
+ import type { Bindings, IsReactiveCall } from './types';
5
5
 
6
6
 
7
7
  interface ScopeBinding {
8
+ depth: number;
8
9
  name: string;
9
10
  scope: ts.Node;
10
11
  type: TYPES;
@@ -12,31 +13,18 @@ interface ScopeBinding {
12
13
 
13
14
  interface TransformContext {
14
15
  bindings: Bindings;
15
- isReactiveCall: (node: ts.Node) => boolean;
16
+ calls: ts.CallExpression[];
17
+ isReactiveCall: IsReactiveCall;
16
18
  replacements: ReplacementIntent[];
17
19
  scopedBindings: ScopeBinding[];
18
20
  sourceFile: ts.SourceFile;
19
21
  tmpCounter: number;
20
22
  }
21
23
 
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
- ]);
24
+ type PrimitivesTransformResult = {
25
+ calls: ts.CallExpression[];
26
+ replacements: ReplacementIntent[];
27
+ };
40
28
 
41
29
 
42
30
  function inScope(reference: ts.Node, binding: ScopeBinding): boolean {
@@ -53,9 +41,44 @@ function inScope(reference: ts.Node, binding: ScopeBinding): boolean {
53
41
  return false;
54
42
  }
55
43
 
44
+ function isScope(node: ts.Node): boolean {
45
+ return ts.isArrowFunction(node) ||
46
+ ts.isBlock(node) ||
47
+ ts.isCatchClause(node) ||
48
+ ts.isForInStatement(node) ||
49
+ ts.isForOfStatement(node) ||
50
+ ts.isForStatement(node) ||
51
+ ts.isFunctionDeclaration(node) ||
52
+ ts.isFunctionExpression(node) ||
53
+ ts.isSourceFile(node);
54
+ }
55
+
56
+ // Innermost enclosing scope plus its nesting depth, so shadowed names resolve to the closest binding
57
+ function scopeOf(node: ts.Node): { depth: number; scope: ts.Node } {
58
+ let current: ts.Node | undefined = node.parent,
59
+ depth = 0,
60
+ scope: ts.Node = node.getSourceFile();
61
+
62
+ while (current) {
63
+ if (isScope(current)) {
64
+ if (scope === node.getSourceFile() && !ts.isSourceFile(current)) {
65
+ scope = current;
66
+ }
67
+
68
+ depth++;
69
+ }
70
+
71
+ current = current.parent;
72
+ }
73
+
74
+ return { depth, scope };
75
+ }
76
+
56
77
  function visit(ctx: TransformContext, node: ts.Node): void {
57
78
  if (ctx.isReactiveCall(node)) {
58
- let call = node as ts.CallExpression;
79
+ let call = node;
80
+
81
+ ctx.calls.push(call);
59
82
 
60
83
  if (call.arguments.length > 0) {
61
84
  let arg = call.arguments[0],
@@ -101,32 +124,10 @@ function visit(ctx: TransformContext, node: ts.Node): void {
101
124
  }
102
125
 
103
126
  if (varname) {
104
- let current = call.parent,
105
- scope;
106
-
107
- while (current) {
108
- if (
109
- ts.isArrowFunction(current) ||
110
- ts.isBlock(current) ||
111
- ts.isForInStatement(current) ||
112
- ts.isForOfStatement(current) ||
113
- ts.isForStatement(current) ||
114
- ts.isFunctionDeclaration(current) ||
115
- ts.isFunctionExpression(current) ||
116
- ts.isSourceFile(current)
117
- ) {
118
- scope = current;
119
- }
120
-
121
- current = current.parent;
122
- }
123
-
124
- if (!scope) {
125
- scope = call.getSourceFile();
126
- }
127
+ let { depth, scope } = scopeOf(call);
127
128
 
128
129
  ctx.bindings.set(varname, classification);
129
- ctx.scopedBindings.push({ name: varname, scope, type: classification });
130
+ ctx.scopedBindings.push({ depth, name: varname, scope, type: classification });
130
131
  }
131
132
 
132
133
  // Replace just the 'reactive' identifier with the appropriate namespace function
@@ -159,7 +160,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
159
160
  for (let i = 0, n = bindings.length; i < n; i++) {
160
161
  let b = bindings[i];
161
162
 
162
- if (b.name === name && inScope(node, b)) {
163
+ if (b.name === name && (!binding || b.depth >= binding.depth) && inScope(node, b)) {
163
164
  binding = b;
164
165
  }
165
166
  }
@@ -255,9 +256,10 @@ function visit(ctx: TransformContext, node: ts.Node): void {
255
256
  }
256
257
 
257
258
 
258
- export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (node: ts.Node) => boolean) => {
259
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): PrimitivesTransformResult => {
259
260
  let ctx: TransformContext = {
260
261
  bindings,
262
+ calls: [],
261
263
  isReactiveCall,
262
264
  replacements: [],
263
265
  scopedBindings: [],
@@ -267,5 +269,5 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (
267
269
 
268
270
  visit(ctx, sourceFile);
269
271
 
270
- return ctx.replacements;
272
+ return { calls: ctx.calls, replacements: ctx.replacements };
271
273
  };
@@ -1,7 +1,10 @@
1
+ import type { ts } from '@esportsplus/typescript';
1
2
  import { TYPES } from './constants';
2
3
 
3
4
 
4
5
  type Bindings = Map<string, TYPES>;
5
6
 
7
+ type IsReactiveCall = (node: ts.Node) => node is ts.CallExpression;
6
8
 
7
- export type { Bindings };
9
+
10
+ export type { Bindings, IsReactiveCall };
package/src/constants.ts CHANGED
@@ -8,6 +8,8 @@ const REACTIVE_OBJECT = Symbol('reactivity.reactive.object');
8
8
 
9
9
  const SIGNAL = Symbol('reactivity.signal');
10
10
 
11
+ const STABILIZER_DEFERRED = 4;
12
+
11
13
  const STABILIZER_IDLE = 0;
12
14
 
13
15
  const STABILIZER_RESCHEDULE = 1;
@@ -16,8 +18,6 @@ const STABILIZER_RUNNING = 2;
16
18
 
17
19
  const STABILIZER_SCHEDULED = 3;
18
20
 
19
- const STATE_NONE = 0;
20
-
21
21
  const STATE_CHECK = 1 << 0;
22
22
 
23
23
  const STATE_DIRTY = 1 << 1;
@@ -40,6 +40,6 @@ export {
40
40
  PACKAGE_NAME,
41
41
  REACTIVE_ARRAY, REACTIVE_OBJECT,
42
42
  SIGNAL,
43
- STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
44
- STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NONE, STATE_NOTIFY_MASK, STATE_RECOMPUTING
43
+ STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
44
+ STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING
45
45
  };