@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
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # @esportsplus/reactivity
2
2
 
3
- Project doc bundle: [docs/index.md](./docs/index.md) — rejected dead-ends, skip reasons, completed-work index.
4
-
5
3
  A fine-grained reactivity system with compile-time transformations. Write reactive code with natural JavaScript syntax while the compiler generates optimized signal-based code.
6
4
 
7
5
  ## Installation
@@ -223,7 +221,7 @@ let user = new ReactiveObject_1();
223
221
  | `reactive(() => expr)` | Creates a computed value (compile-time only) |
224
222
  | `reactive({...})` | Creates a reactive object with signals and computeds |
225
223
  | `reactive([...])` | Creates a reactive array |
226
- | `effect(fn, onError?)` | Runs a function that re-executes when dependencies change. Returns a dispose function. Optional `onError` receives thrown or rejected errors |
224
+ | `effect(fn, apply?)` | Runs a function that re-executes when dependencies change. Returns a dispose function. Optional `apply(value, prev)` runs untracked after each run with `fn`'s return value |
227
225
  | `root(fn)` | Creates an untracked scope. If `fn` accepts an argument, a dispose function is provided |
228
226
  | `onCleanup(fn)` | Registers a cleanup function for the current effect/computed |
229
227
 
@@ -259,7 +257,7 @@ For advanced use cases, the underlying classes are exported:
259
257
 
260
258
  | Class | Description |
261
259
  |-------|-------------|
262
- | `ReactiveArray<T>` | Array subclass with reactivity and event dispatching |
260
+ | `ReactiveArray<T>` | Array subclass with reactivity and event dispatching. `new ReactiveArray(items?: T[])` takes the initial items as one array |
263
261
  | `ReactiveObject<T>` | Base class for reactive objects |
264
262
 
265
263
  ### Constants
@@ -283,7 +281,6 @@ Symbol constants for type identification:
283
281
  | `SelectorSignal<T>` | Signal variant backing `signal.selector` per-key subscriptions |
284
282
  | `Link` | Dependency graph link between nodes |
285
283
  | `Reactive<T>` | Utility type for inferring reactive object/array types |
286
- | `TransformResult` | Compiler transform output metadata |
287
284
 
288
285
  ## ReactiveArray
289
286
 
@@ -341,8 +338,13 @@ pnpm bench # vitest bench --run
341
338
  `agent:test` (`tsc --noEmit && vitest run`) and `agent:bench` (`vitest bench --run`) are the
342
339
  CI-facing aliases of the same build/test/bench commands.
343
340
 
341
+ ## Changes in 0.35.0
342
+
343
+ - `new ReactiveArray(items?)` takes an array instead of variadic items. `new ReactiveArray(5)` previously produced a length-5 holey array, and spreading large arrays overflowed the call stack.
344
+ - `root.disposables` is removed. `reactive()` now always registers its disposal with the enclosing effect or root scope.
345
+ - `ReactiveArray` listener errors are still detached but are now rethrown on a microtask instead of swallowed.
346
+ - `TransformResult` is no longer exported.
347
+
344
348
  ## License
345
349
 
346
350
  MIT
347
-
348
- <!-- claude-code:readme-source-hash: 189167a17e9be8c6 -->
@@ -9,7 +9,7 @@ describe('ReactiveArray creation', () => {
9
9
  });
10
10
 
11
11
  bench('create with 10 items', () => {
12
- new ReactiveArray(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
12
+ new ReactiveArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
13
13
  });
14
14
 
15
15
  bench('create with 100 items', () => {
@@ -19,7 +19,7 @@ describe('ReactiveArray creation', () => {
19
19
  items.push(i);
20
20
  }
21
21
 
22
- new ReactiveArray(...items);
22
+ new ReactiveArray(items);
23
23
  });
24
24
  });
25
25
 
@@ -56,7 +56,7 @@ describe('ReactiveArray push', () => {
56
56
 
57
57
  describe('ReactiveArray pop', () => {
58
58
  bench('pop', () => {
59
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
59
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
60
60
 
61
61
  arr.pop();
62
62
  });
@@ -65,19 +65,19 @@ describe('ReactiveArray pop', () => {
65
65
 
66
66
  describe('ReactiveArray splice', () => {
67
67
  bench('splice remove 1', () => {
68
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
68
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
69
69
 
70
70
  arr.splice(2, 1);
71
71
  });
72
72
 
73
73
  bench('splice insert 1', () => {
74
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
74
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
75
75
 
76
76
  arr.splice(2, 0, 99);
77
77
  });
78
78
 
79
79
  bench('splice replace 1', () => {
80
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
80
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
81
81
 
82
82
  arr.splice(2, 1, 99);
83
83
  });
@@ -86,7 +86,7 @@ describe('ReactiveArray splice', () => {
86
86
 
87
87
  describe('ReactiveArray sort', () => {
88
88
  bench('sort 10 items', () => {
89
- let arr = new ReactiveArray(5, 3, 8, 1, 9, 2, 7, 4, 6, 10);
89
+ let arr = new ReactiveArray([5, 3, 8, 1, 9, 2, 7, 4, 6, 10]);
90
90
 
91
91
  arr.sort((a, b) => a - b);
92
92
  });
@@ -98,7 +98,7 @@ describe('ReactiveArray sort', () => {
98
98
  items.push(i);
99
99
  }
100
100
 
101
- let arr = new ReactiveArray(...items);
101
+ let arr = new ReactiveArray(items);
102
102
 
103
103
  arr.sort((a, b) => a - b);
104
104
  });
@@ -110,7 +110,7 @@ describe('ReactiveArray sort', () => {
110
110
  items.push(i);
111
111
  }
112
112
 
113
- let arr = new ReactiveArray(...items);
113
+ let arr = new ReactiveArray(items);
114
114
 
115
115
  arr.sort((a, b) => a - b);
116
116
  });
@@ -119,13 +119,13 @@ describe('ReactiveArray sort', () => {
119
119
 
120
120
  describe('ReactiveArray $set', () => {
121
121
  bench('$set', () => {
122
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
122
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
123
123
 
124
124
  arr.$set(2, 99);
125
125
  });
126
126
 
127
127
  bench('$set same value (no-op)', () => {
128
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
128
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
129
129
 
130
130
  arr.$set(2, 3);
131
131
  });
@@ -169,7 +169,7 @@ describe('ReactiveArray dispose/clear', () => {
169
169
  items.push(i);
170
170
  }
171
171
 
172
- let arr = new ReactiveArray(...items);
172
+ let arr = new ReactiveArray(items);
173
173
 
174
174
  arr.dispose();
175
175
  });
@@ -181,7 +181,7 @@ describe('ReactiveArray dispose/clear', () => {
181
181
  items.push(i);
182
182
  }
183
183
 
184
- let arr = new ReactiveArray(...items);
184
+ let arr = new ReactiveArray(items);
185
185
 
186
186
  arr.clear();
187
187
  });
@@ -201,13 +201,13 @@ describe('ReactiveArray concat/unshift/shift/reverse', () => {
201
201
  });
202
202
 
203
203
  bench('unshift 10 items', () => {
204
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
204
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
205
205
 
206
206
  arr.unshift(10, 20, 30, 40, 50, 60, 70, 80, 90, 100);
207
207
  });
208
208
 
209
209
  bench('shift', () => {
210
- let arr = new ReactiveArray(1, 2, 3, 4, 5);
210
+ let arr = new ReactiveArray([1, 2, 3, 4, 5]);
211
211
 
212
212
  arr.shift();
213
213
  });
@@ -219,7 +219,7 @@ describe('ReactiveArray concat/unshift/shift/reverse', () => {
219
219
  items.push(i);
220
220
  }
221
221
 
222
- let arr = new ReactiveArray(...items);
222
+ let arr = new ReactiveArray(items);
223
223
 
224
224
  arr.reverse();
225
225
  });
@@ -228,7 +228,7 @@ describe('ReactiveArray concat/unshift/shift/reverse', () => {
228
228
 
229
229
  describe('ReactiveArray reactive length', () => {
230
230
  bench('read $length in effect', () => {
231
- let arr = new ReactiveArray(1, 2, 3);
231
+ let arr = new ReactiveArray([1, 2, 3]);
232
232
 
233
233
  let stop = effect(() => {
234
234
  arr.$length;
@@ -1,5 +1,5 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
3
- import type { Bindings } from './types.js';
3
+ import type { Bindings, IsReactiveCall } from './types.js';
4
4
  export default _default;
5
- declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Checker): ReplacementIntent[];
5
+ declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): ReplacementIntent[];
@@ -1,16 +1,6 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { ast, imports } from '@esportsplus/typescript/compiler';
3
- import { ENTRYPOINT, NAMESPACE, PACKAGE_NAME, TYPES } from './constants.js';
4
- function isReactiveCall(checker, node) {
5
- if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
6
- return false;
7
- }
8
- let expr = node.expression;
9
- if (checker) {
10
- return imports.includes(checker, expr, PACKAGE_NAME, ENTRYPOINT);
11
- }
12
- return expr.text === ENTRYPOINT;
13
- }
2
+ import { ast } from '@esportsplus/typescript/compiler';
3
+ import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants.js';
14
4
  function getElementTypeText(typeNode, sourceFile) {
15
5
  if (ts.isArrayTypeNode(typeNode)) {
16
6
  return typeNode.elementType.getText(sourceFile);
@@ -24,40 +14,8 @@ function getElementTypeText(typeNode, sourceFile) {
24
14
  }
25
15
  return null;
26
16
  }
27
- function getOperator(kind) {
28
- switch (kind) {
29
- case ts.SyntaxKind.PlusEqualsToken: return '+';
30
- case ts.SyntaxKind.MinusEqualsToken: return '-';
31
- case ts.SyntaxKind.AsteriskEqualsToken: return '*';
32
- case ts.SyntaxKind.SlashEqualsToken: return '/';
33
- case ts.SyntaxKind.PercentEqualsToken: return '%';
34
- case ts.SyntaxKind.AsteriskAsteriskEqualsToken: return '**';
35
- case ts.SyntaxKind.AmpersandEqualsToken: return '&';
36
- case ts.SyntaxKind.BarEqualsToken: return '|';
37
- case ts.SyntaxKind.CaretEqualsToken: return '^';
38
- default: return '+';
39
- }
40
- }
41
- function isAssignmentOperator(kind) {
42
- return kind === ts.SyntaxKind.EqualsToken ||
43
- kind === ts.SyntaxKind.PlusEqualsToken ||
44
- kind === ts.SyntaxKind.MinusEqualsToken ||
45
- kind === ts.SyntaxKind.AsteriskEqualsToken ||
46
- kind === ts.SyntaxKind.SlashEqualsToken ||
47
- kind === ts.SyntaxKind.PercentEqualsToken ||
48
- kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken ||
49
- kind === ts.SyntaxKind.LessThanLessThanEqualsToken ||
50
- kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken ||
51
- kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken ||
52
- kind === ts.SyntaxKind.AmpersandEqualsToken ||
53
- kind === ts.SyntaxKind.BarEqualsToken ||
54
- kind === ts.SyntaxKind.CaretEqualsToken ||
55
- kind === ts.SyntaxKind.BarBarEqualsToken ||
56
- kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
57
- kind === ts.SyntaxKind.QuestionQuestionEqualsToken;
58
- }
59
17
  function visit(ctx, node) {
60
- if (isReactiveCall(ctx.checker, node) && node.arguments.length > 0) {
18
+ if (ctx.isReactiveCall(node) && node.arguments.length > 0) {
61
19
  let arg = node.arguments[0], expression = ts.isAsExpression(arg) ? arg.expression : arg;
62
20
  if (ts.isArrayLiteralExpression(expression)) {
63
21
  let elementType = null;
@@ -74,7 +32,7 @@ function visit(ctx, node) {
74
32
  ctx.replacements.push({
75
33
  node,
76
34
  generate: (sf) => expression.elements.length > 0
77
- ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(...${expression.getText(sf)})`
35
+ ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${expression.getText(sf)})`
78
36
  : ` new ${NAMESPACE}.ReactiveArray${typeParam}()`
79
37
  });
80
38
  }
@@ -105,9 +63,9 @@ function visit(ctx, node) {
105
63
  let name = ast.expression.name(node.expression);
106
64
  if (name && ctx.bindings.get(name) === TYPES.Array) {
107
65
  let expr = node.expression, parent = node.parent;
108
- if (parent && ts.isBinaryExpression(parent) && parent.left === node && isAssignmentOperator(parent.operatorToken.kind)) {
109
- let op = parent.operatorToken.kind;
110
- if (op === ts.SyntaxKind.EqualsToken) {
66
+ if (parent && ts.isBinaryExpression(parent) && parent.left === node && (parent.operatorToken.kind === ts.SyntaxKind.EqualsToken || COMPOUND_OPERATORS.has(parent.operatorToken.kind))) {
67
+ let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
68
+ if (op === undefined) {
111
69
  ctx.replacements.push({
112
70
  node: parent,
113
71
  generate: (sf) => `${expr.getText(sf)}.$length = ${parent.right.getText(sf)}`
@@ -116,7 +74,7 @@ function visit(ctx, node) {
116
74
  else {
117
75
  ctx.replacements.push({
118
76
  node: parent,
119
- generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${getOperator(op)} ${parent.right.getText(sf)}`
77
+ generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} ${parent.right.getText(sf)}`
120
78
  });
121
79
  }
122
80
  }
@@ -174,10 +132,10 @@ function visit(ctx, node) {
174
132
  }
175
133
  node.forEachChild(n => visit(ctx, n));
176
134
  }
177
- export default (sourceFile, bindings, checker) => {
135
+ export default (sourceFile, bindings, isReactiveCall) => {
178
136
  let ctx = {
179
137
  bindings,
180
- checker,
138
+ isReactiveCall,
181
139
  replacements: [],
182
140
  sourceFile
183
141
  };
@@ -1,12 +1,12 @@
1
+ import { ts } from '@esportsplus/typescript';
2
+ declare const COMPOUND_OPERATORS: Map<ts.SyntaxKind, string>;
1
3
  declare const ENTRYPOINT = "reactive";
2
- declare const ENTRYPOINT_REGEX: RegExp;
3
4
  declare const NAMESPACE: string;
4
5
  declare const TYPES: {
5
6
  readonly Array: 0;
6
7
  readonly Computed: 1;
7
- readonly Object: 2;
8
- readonly Signal: 3;
8
+ readonly Signal: 2;
9
9
  };
10
10
  type TYPES = typeof TYPES[keyof typeof TYPES];
11
- export { ENTRYPOINT, ENTRYPOINT_REGEX, NAMESPACE, TYPES };
11
+ export { COMPOUND_OPERATORS, ENTRYPOINT, NAMESPACE, TYPES };
12
12
  export { PACKAGE_NAME } from '../constants.js';
@@ -1,12 +1,28 @@
1
+ import { ts } from '@esportsplus/typescript';
1
2
  import { uid } from '@esportsplus/typescript/compiler';
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
+ ]);
2
20
  const ENTRYPOINT = 'reactive';
3
- const ENTRYPOINT_REGEX = /\breactive\b/;
4
21
  const NAMESPACE = uid('reactivity');
5
22
  const TYPES = {
6
23
  Array: 0,
7
24
  Computed: 1,
8
- Object: 2,
9
- Signal: 3
25
+ Signal: 2
10
26
  };
11
- export { ENTRYPOINT, ENTRYPOINT_REGEX, NAMESPACE, TYPES };
27
+ export { COMPOUND_OPERATORS, ENTRYPOINT, NAMESPACE, TYPES };
12
28
  export { PACKAGE_NAME } from '../constants.js';
@@ -4,11 +4,6 @@ import { ENTRYPOINT, NAMESPACE, PACKAGE_NAME } from './constants.js';
4
4
  import array from './array.js';
5
5
  import object from './object.js';
6
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;
11
- }
12
7
  function isReactiveCallExpression(checker, node) {
13
8
  if (!ts.isCallExpression(node)) {
14
9
  return false;
@@ -22,51 +17,43 @@ function isReactiveCallExpression(checker, node) {
22
17
  }
23
18
  return false;
24
19
  }
25
- function hasReactiveCalls(checker, node) {
26
- if (isReactiveCallExpression(checker, node)) {
27
- return true;
28
- }
29
- let found = false;
30
- node.forEachChild(child => {
31
- if (!found && hasReactiveCalls(checker, child)) {
32
- found = true;
33
- }
34
- });
35
- return found;
36
- }
37
- function visit(ctx, node) {
38
- if (isReactiveCallExpression(ctx.checker, node) && !ctx.transformedNodes.has(node) && !ctx.transformedNodes.has(node.expression)) {
39
- ctx.replacements.push({
40
- generate: () => `${NAMESPACE}.reactive(${node.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
41
- node
42
- });
43
- }
44
- node.forEachChild(n => visit(ctx, n));
45
- }
46
20
  export default {
47
21
  patterns: ['reactive(', 'reactive<'],
48
22
  transform: (ctx) => {
49
- if (!ctx.checker || !hasReactiveCalls(ctx.checker, ctx.sourceFile)) {
23
+ let checker = ctx.checker;
24
+ if (!checker) {
50
25
  return {};
51
26
  }
52
27
  let bindings = new Map(), intents = {
53
28
  imports: [],
54
29
  prepend: [],
55
30
  replacements: []
56
- };
57
- intents.replacements.push(...primitives(ctx.sourceFile, bindings, (node) => isReactiveCallExpression(ctx.checker, node)));
58
- let { prepend, replacements } = object(ctx.sourceFile, bindings, ctx.checker);
59
- intents.prepend.push(...prepend);
31
+ }, isReactiveCall = (node) => isReactiveCallExpression(checker, node), sourceFile = ctx.sourceFile;
32
+ let { calls, replacements } = primitives(sourceFile, bindings, isReactiveCall);
33
+ if (calls.length === 0) {
34
+ return {};
35
+ }
60
36
  intents.replacements.push(...replacements);
61
- intents.replacements.push(...array(ctx.sourceFile, bindings, ctx.checker));
62
- intents.replacements.push(...findRemainingCalls(ctx.checker, ctx.sourceFile, new Set(intents.replacements.map(r => r.node))));
63
- if (intents.replacements.length > 0 || intents.prepend.length > 0) {
64
- intents.imports.push({
65
- namespace: NAMESPACE,
66
- package: PACKAGE_NAME,
67
- remove: [ENTRYPOINT]
37
+ let objects = object(sourceFile, bindings, isReactiveCall);
38
+ intents.prepend.push(...objects.prepend);
39
+ intents.replacements.push(...objects.replacements);
40
+ intents.replacements.push(...array(sourceFile, bindings, isReactiveCall));
41
+ let transformed = new Set(intents.replacements.map(r => r.node));
42
+ for (let i = 0, n = calls.length; i < n; i++) {
43
+ let call = calls[i];
44
+ if (transformed.has(call) || transformed.has(call.expression)) {
45
+ continue;
46
+ }
47
+ intents.replacements.push({
48
+ generate: () => `${NAMESPACE}.reactive(${call.arguments.map(a => a.getText(sourceFile)).join(', ')})`,
49
+ node: call
68
50
  });
69
51
  }
52
+ intents.imports.push({
53
+ namespace: NAMESPACE,
54
+ package: PACKAGE_NAME,
55
+ remove: [ENTRYPOINT]
56
+ });
70
57
  return intents;
71
58
  }
72
59
  };
@@ -1,9 +1,9 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
3
- import type { Bindings } from './types.js';
3
+ import type { Bindings, IsReactiveCall } from './types.js';
4
4
  type ObjectTransformResult = {
5
5
  prepend: string[];
6
6
  replacements: ReplacementIntent[];
7
7
  };
8
8
  export default _default;
9
- declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Checker): ObjectTransformResult;
9
+ declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): ObjectTransformResult;
@@ -1,6 +1,6 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { code, imports, uid } from '@esportsplus/typescript/compiler';
3
- import { ENTRYPOINT, NAMESPACE, PACKAGE_NAME, TYPES } from './constants.js';
2
+ import { code, uid } from '@esportsplus/typescript/compiler';
3
+ import { NAMESPACE, TYPES } from './constants.js';
4
4
  function analyzeProperty(prop, sourceFile) {
5
5
  if (!ts.isPropertyAssignment(prop)) {
6
6
  return null;
@@ -139,24 +139,13 @@ function isStaticValue(node) {
139
139
  node.kind === ts.SyntaxKind.UndefinedKeyword ||
140
140
  (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand));
141
141
  }
142
- function isReactiveCall(checker, node) {
143
- if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
144
- return false;
145
- }
146
- let expr = node.expression;
147
- if (checker) {
148
- return imports.includes(checker, expr, PACKAGE_NAME, ENTRYPOINT);
149
- }
150
- return expr.text === ENTRYPOINT;
151
- }
152
142
  function visit(ctx, node) {
153
- if (isReactiveCall(ctx.checker, node)) {
143
+ if (ctx.isReactiveCall(node)) {
154
144
  let arg = node.arguments[0];
155
145
  if (arg && ts.isObjectLiteralExpression(arg)) {
156
146
  let properties = [], props = arg.properties, varname = null;
157
147
  if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
158
148
  varname = node.parent.name.text;
159
- ctx.bindings.set(varname, TYPES.Object);
160
149
  }
161
150
  for (let i = 0, n = props.length; i < n; i++) {
162
151
  let prop = props[i];
@@ -185,11 +174,11 @@ function visit(ctx, node) {
185
174
  }
186
175
  node.forEachChild(n => visit(ctx, n));
187
176
  }
188
- export default (sourceFile, bindings, checker) => {
177
+ export default (sourceFile, bindings, isReactiveCall) => {
189
178
  let ctx = {
190
179
  bindings,
191
180
  calls: [],
192
- checker,
181
+ isReactiveCall,
193
182
  sourceFile
194
183
  };
195
184
  visit(ctx, sourceFile);
@@ -1,5 +1,9 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
3
- import type { Bindings } from './types.js';
3
+ import type { Bindings, IsReactiveCall } from './types.js';
4
+ type PrimitivesTransformResult = {
5
+ calls: ts.CallExpression[];
6
+ replacements: ReplacementIntent[];
7
+ };
4
8
  export default _default;
5
- declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (node: ts.Node) => boolean): ReplacementIntent[];
9
+ declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): PrimitivesTransformResult;
@@ -1,22 +1,5 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { NAMESPACE, 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
- ]);
2
+ import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants.js';
20
3
  function inScope(reference, binding) {
21
4
  let current = reference;
22
5
  while (current) {
@@ -27,9 +10,34 @@ function inScope(reference, binding) {
27
10
  }
28
11
  return false;
29
12
  }
13
+ function isScope(node) {
14
+ return ts.isArrowFunction(node) ||
15
+ ts.isBlock(node) ||
16
+ ts.isCatchClause(node) ||
17
+ ts.isForInStatement(node) ||
18
+ ts.isForOfStatement(node) ||
19
+ ts.isForStatement(node) ||
20
+ ts.isFunctionDeclaration(node) ||
21
+ ts.isFunctionExpression(node) ||
22
+ ts.isSourceFile(node);
23
+ }
24
+ function scopeOf(node) {
25
+ let current = node.parent, depth = 0, scope = node.getSourceFile();
26
+ while (current) {
27
+ if (isScope(current)) {
28
+ if (scope === node.getSourceFile() && !ts.isSourceFile(current)) {
29
+ scope = current;
30
+ }
31
+ depth++;
32
+ }
33
+ current = current.parent;
34
+ }
35
+ return { depth, scope };
36
+ }
30
37
  function visit(ctx, node) {
31
38
  if (ctx.isReactiveCall(node)) {
32
39
  let call = node;
40
+ ctx.calls.push(call);
33
41
  if (call.arguments.length > 0) {
34
42
  let arg = call.arguments[0], classification = TYPES.Signal;
35
43
  if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
@@ -64,25 +72,9 @@ function visit(ctx, node) {
64
72
  varname = call.parent.left.text;
65
73
  }
66
74
  if (varname) {
67
- let current = call.parent, scope;
68
- while (current) {
69
- if (ts.isArrowFunction(current) ||
70
- ts.isBlock(current) ||
71
- ts.isForInStatement(current) ||
72
- ts.isForOfStatement(current) ||
73
- ts.isForStatement(current) ||
74
- ts.isFunctionDeclaration(current) ||
75
- ts.isFunctionExpression(current) ||
76
- ts.isSourceFile(current)) {
77
- scope = current;
78
- }
79
- current = current.parent;
80
- }
81
- if (!scope) {
82
- scope = call.getSourceFile();
83
- }
75
+ let { depth, scope } = scopeOf(call);
84
76
  ctx.bindings.set(varname, classification);
85
- ctx.scopedBindings.push({ name: varname, scope, type: classification });
77
+ ctx.scopedBindings.push({ depth, name: varname, scope, type: classification });
86
78
  }
87
79
  ctx.replacements.push({
88
80
  generate: () => classification === TYPES.Computed
@@ -103,7 +95,7 @@ function visit(ctx, node) {
103
95
  let bindings = ctx.scopedBindings, binding, name = node.text;
104
96
  for (let i = 0, n = bindings.length; i < n; i++) {
105
97
  let b = bindings[i];
106
- if (b.name === name && inScope(node, b)) {
98
+ if (b.name === name && (!binding || b.depth >= binding.depth) && inScope(node, b)) {
107
99
  binding = b;
108
100
  }
109
101
  }
@@ -183,6 +175,7 @@ function visit(ctx, node) {
183
175
  export default (sourceFile, bindings, isReactiveCall) => {
184
176
  let ctx = {
185
177
  bindings,
178
+ calls: [],
186
179
  isReactiveCall,
187
180
  replacements: [],
188
181
  scopedBindings: [],
@@ -190,5 +183,5 @@ export default (sourceFile, bindings, isReactiveCall) => {
190
183
  tmpCounter: 0
191
184
  };
192
185
  visit(ctx, sourceFile);
193
- return ctx.replacements;
186
+ return { calls: ctx.calls, replacements: ctx.replacements };
194
187
  };
@@ -1,3 +1,5 @@
1
+ import type { ts } from '@esportsplus/typescript';
1
2
  import { TYPES } from './constants.js';
2
3
  type Bindings = Map<string, TYPES>;
3
- export type { Bindings };
4
+ type IsReactiveCall = (node: ts.Node) => node is ts.CallExpression;
5
+ export type { Bindings, IsReactiveCall };
@@ -3,11 +3,11 @@ declare const PACKAGE_NAME = "@esportsplus/reactivity";
3
3
  declare const REACTIVE_ARRAY: unique symbol;
4
4
  declare const REACTIVE_OBJECT: unique symbol;
5
5
  declare const SIGNAL: unique symbol;
6
+ declare const STABILIZER_DEFERRED = 4;
6
7
  declare const STABILIZER_IDLE = 0;
7
8
  declare const STABILIZER_RESCHEDULE = 1;
8
9
  declare const STABILIZER_RUNNING = 2;
9
10
  declare const STABILIZER_SCHEDULED = 3;
10
- declare const STATE_NONE = 0;
11
11
  declare const STATE_CHECK: number;
12
12
  declare const STATE_DIRTY: number;
13
13
  declare const STATE_RECOMPUTING: number;
@@ -16,4 +16,4 @@ declare const STATE_COMPUTED: number;
16
16
  declare const STATE_ERROR: number;
17
17
  declare const STATE_EFFECT: number;
18
18
  declare const STATE_NOTIFY_MASK: number;
19
- export { COMPUTED, PACKAGE_NAME, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NONE, STATE_NOTIFY_MASK, STATE_RECOMPUTING };
19
+ export { COMPUTED, PACKAGE_NAME, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL, STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING };