@esportsplus/reactivity 0.40.0 → 0.41.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.
Files changed (44) hide show
  1. package/README.md +9 -5
  2. package/bench/compiler.bench.ts +115 -0
  3. package/bench/kairo.bench.ts +1 -1
  4. package/bench/reactive/array.bench.ts +1 -0
  5. package/bench/reactive/reactive-object.bench.ts +3 -2
  6. package/build/compiler/array.js +28 -25
  7. package/build/compiler/bindings.d.ts +3 -1
  8. package/build/compiler/bindings.js +32 -9
  9. package/build/compiler/index.d.ts +3 -2
  10. package/build/compiler/index.js +21 -13
  11. package/build/compiler/object.js +12 -13
  12. package/build/compiler/plugins/vite.d.ts +13 -4
  13. package/build/compiler/primitives.js +9 -9
  14. package/build/reactive/index.d.ts +1 -1
  15. package/build/reactive/object.d.ts +3 -3
  16. package/build/reactive/object.js +1 -1
  17. package/oxlintrc.json +3 -0
  18. package/package.json +15 -7
  19. package/pnpm-workspace.yaml +4 -7
  20. package/src/compiler/array.ts +30 -27
  21. package/src/compiler/bindings.ts +49 -10
  22. package/src/compiler/index.ts +37 -20
  23. package/src/compiler/object.ts +14 -16
  24. package/src/compiler/primitives.ts +13 -10
  25. package/src/reactive/array.ts +5 -3
  26. package/src/reactive/index.ts +3 -3
  27. package/src/reactive/object.ts +4 -4
  28. package/src/system.ts +2 -0
  29. package/test/compiler/__snapshots__/output.test.ts.snap +157 -0
  30. package/test/compiler/compiler.test.ts +51 -9
  31. package/test/compiler/composition.test.ts +110 -0
  32. package/test/compiler/coverage.test.ts +22 -22
  33. package/test/compiler/origins.test.ts +156 -0
  34. package/test/compiler/output.test.ts +116 -0
  35. package/test/ownership.test.ts +1 -1
  36. package/test/reactive/array.test.ts +8 -7
  37. package/test/reactive/reactive.test.ts +4 -4
  38. package/test/system.test.ts +3 -4
  39. package/test/untrack.test.ts +1 -1
  40. package/tsconfig.build.json +3 -0
  41. package/tsconfig.json +2 -2
  42. package/vitest.config.ts +2 -2
  43. package/bench/tsconfig.json +0 -16
  44. package/test/tsconfig.json +0 -16
@@ -1,12 +1,12 @@
1
1
  import { COMPUTED, REACTIVE_ARRAY, SIGNAL } from '../constants.js';
2
- import { Computed } from '../types.js';
3
2
  import { ReactiveArray } from './array.js';
3
+ import type { Computed } from '../types.js';
4
4
  declare class ReactiveObject<T extends Record<PropertyKey, unknown>> {
5
5
  protected disposers: VoidFunction[] | null;
6
6
  constructor(data: T | null);
7
- protected [COMPUTED]<T extends Computed<ReturnType<T>>['fn']>(value: T): import("../types.js").ComputedResult<ReturnType<T>>;
7
+ protected [COMPUTED]<T extends Computed<ReturnType<T>>['fn']>(value: T): import("../index.js").ComputedResult<ReturnType<T>>;
8
8
  protected [REACTIVE_ARRAY]<U>(value: U[]): ReactiveArray<U>;
9
- protected [SIGNAL]<T>(value: T): import("../types.js").Signal<T>;
9
+ protected [SIGNAL]<T>(value: T): import("../index.js").Signal<T>;
10
10
  dispose(): void;
11
11
  }
12
12
  declare const isReactiveObject: (value: unknown) => value is ReactiveObject<Record<PropertyKey, unknown>>;
@@ -58,7 +58,7 @@ class ReactiveObject {
58
58
  if (!disposers) {
59
59
  return;
60
60
  }
61
- while (disposer = disposers.pop()) {
61
+ while ((disposer = disposers.pop())) {
62
62
  disposer();
63
63
  }
64
64
  }
package/oxlintrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["./node_modules/@esportsplus/typescript/oxlintrc.json"]
3
+ }
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
+ "@esportsplus/typescript": "^0.39.0",
4
5
  "@esportsplus/utilities": "^0.28.6"
5
6
  },
6
7
  "devDependencies": {
7
- "@esportsplus/typescript": "^0.38.0",
8
- "@types/node": "^26.4.1",
9
- "vite": "^8.2.2",
10
- "vitest": "^5.0.0"
8
+ "@types/node": "^26.6.6",
9
+ "oxlint": "^1.90.0",
10
+ "oxlint-tsgolint": "^7.0.2005",
11
+ "vite": "^8.3.5",
12
+ "vitest": "^5.1.0"
13
+ },
14
+ "engines": {
15
+ "node": ">=26"
11
16
  },
12
17
  "exports": {
13
18
  ".": {
@@ -36,13 +41,16 @@
36
41
  },
37
42
  "type": "module",
38
43
  "types": "build/index.d.ts",
39
- "version": "0.40.0",
44
+ "version": "0.41.0",
40
45
  "scripts": {
41
46
  "agent:bench": "vitest bench --run",
42
- "agent:test": "tsc --noEmit && vitest run",
47
+ "agent:test": "pnpm typecheck && vitest run",
43
48
  "bench": "vitest bench --run",
44
- "build": "tsc",
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "lint": "oxlint -c oxlintrc.json --type-aware --deny-warnings",
45
51
  "test": "vitest run",
52
+ "typecheck": "tsc --noEmit",
53
+ "verify": "pnpm build && pnpm typecheck && pnpm lint && pnpm test",
46
54
  "-": "-"
47
55
  }
48
56
  }
@@ -1,10 +1,7 @@
1
1
  allowBuilds:
2
2
  esbuild: true
3
3
  minimumReleaseAgeExclude:
4
- - '@esportsplus/utilities@0.28.1 || 0.28.3 || 0.28.4 || 0.28.6'
5
- - '@esportsplus/typescript@0.31.0 || 0.32.0 || 0.33.0 || 0.33.2 || 0.33.4 || 0.34.1 || 0.34.3 || 0.34.4 || 0.36.0 || 0.36.1 || 0.37.0 || 0.38.0'
6
- - tsc-alias@1.9.4
7
- - '@vitest/mocker@5.0.0'
8
- - '@vitest/spy@5.0.0'
9
- - vitest@5.0.0
10
- - '@esportsplus/cli-passthrough@0.0.18'
4
+ - '@esportsplus/*'
5
+ - '@vitest/mocker@5.1.0'
6
+ - '@vitest/spy@5.1.0'
7
+ - vitest@5.1.0
@@ -68,10 +68,10 @@ function declare(ctx: { isReactiveCall: IsReactiveCall; replacements: Replacemen
68
68
  let typeParam = elementType ? `<${elementType}>` : '';
69
69
 
70
70
  ctx.replacements.push({
71
- node,
72
- generate: (sf) => expression.elements.length > 0
73
- ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${expression.getText(sf)})`
74
- : ` new ${NAMESPACE}.ReactiveArray${typeParam}()`
71
+ generate: (_sourceFile, text) => expression.elements.length > 0
72
+ ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${text(expression)})`
73
+ : ` new ${NAMESPACE}.ReactiveArray${typeParam}()`,
74
+ node
75
75
  });
76
76
  }
77
77
  }
@@ -126,10 +126,10 @@ function length(access: ts.PropertyAccessExpression, replacements: ReplacementIn
126
126
  let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
127
127
 
128
128
  replacements.push({
129
- node: parent,
130
- generate: (sf) => op === undefined
131
- ? `${expr.getText(sf)}.$length = ${parent.right.getText(sf)}`
132
- : `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} ${parent.right.getText(sf)}`
129
+ generate: (_sourceFile, text) => op === undefined
130
+ ? `${text(expr)}.$length = ${text(parent.right)}`
131
+ : `${text(expr)}.$length = ${text(expr)}.length ${op} ${text(parent.right)}`,
132
+ node: parent
133
133
  });
134
134
  }
135
135
  // arr.length++ / arr.length-- / ++arr.length / --arr.length
@@ -141,24 +141,22 @@ function length(access: ts.PropertyAccessExpression, replacements: ReplacementIn
141
141
  let op = parent.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
142
142
 
143
143
  replacements.push({
144
- node: parent,
145
- generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} 1`
144
+ generate: (_sourceFile, text) => `${text(expr)}.$length = ${text(expr)}.length ${op} 1`,
145
+ node: parent
146
146
  });
147
147
  }
148
148
  // Read-only: arr.length -> arr.$length
149
149
  else {
150
150
  replacements.push({
151
- node: access,
152
- generate: (sf) => `${expr.getText(sf)}.$length`
151
+ generate: (_sourceFile, text) => `${text(expr)}.$length`,
152
+ node: access
153
153
  });
154
154
  }
155
155
  }
156
156
 
157
157
  // An element write must notify, so it goes through $set; operands are evaluated once
158
158
  function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): void {
159
- let parent = access.parent!,
160
- array = (sf: ts.SourceFile) => access.expression.getText(sf),
161
- index = (sf: ts.SourceFile) => access.argumentExpression.getText(sf);
159
+ let parent = access.parent!;
162
160
 
163
161
  if (ts.isDeleteExpression(parent)) {
164
162
  ctx.failures.push({ message: 'an element of a reactive array cannot be deleted; use splice()', node: parent });
@@ -170,10 +168,10 @@ function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): v
170
168
  right = parent.right;
171
169
 
172
170
  ctx.replacements.push({
173
- node: parent,
174
- generate: (sf) => op === undefined
175
- ? `${array(sf)}.$set(${index(sf)}, ${right.getText(sf)})`
176
- : `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${right.getText(sf)})))(${array(sf)}, ${index(sf)})`
171
+ generate: (_sourceFile, text) => op === undefined
172
+ ? `${text(access.expression)}.$set(${text(access.argumentExpression)}, ${text(right)})`
173
+ : `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${text(right)})))(${text(access.expression)}, ${text(access.argumentExpression)})`,
174
+ node: parent
177
175
  });
178
176
 
179
177
  return;
@@ -183,10 +181,15 @@ function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): v
183
181
  op = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
184
182
 
185
183
  ctx.replacements.push({
186
- node: unary,
187
- generate: (sf) => ts.isPrefixUnaryExpression(unary)
188
- ? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array(sf)}, ${index(sf)})`
189
- : `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array(sf)}, ${index(sf)}, ${array(sf)}[${index(sf)}])`
184
+ generate: (_sourceFile, text) => {
185
+ let array = text(access.expression),
186
+ index = text(access.argumentExpression);
187
+
188
+ return ts.isPrefixUnaryExpression(unary)
189
+ ? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array}, ${index})`
190
+ : `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array}, ${index}, ${array}[${index}])`;
191
+ },
192
+ node: unary
190
193
  });
191
194
  }
192
195
 
@@ -234,10 +237,10 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: I
234
237
  elements = scope.unwrap(assignment.right) as ts.ArrayLiteralExpression;
235
238
 
236
239
  result.replacements.push({
237
- node: assignment,
238
- generate: (sf) => elements.elements.length > 0
239
- ? `${name}.splice(0, ${name}.length, ...${elements.getText(sf)})`
240
- : `${name}.splice(0, ${name}.length)`
240
+ generate: (_sourceFile, text) => elements.elements.length > 0
241
+ ? `${name}.splice(0, ${name}.length, ...${text(elements)})`
242
+ : `${name}.splice(0, ${name}.length)`,
243
+ node: assignment
241
244
  });
242
245
  }
243
246
 
@@ -1,5 +1,5 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { references } from '@esportsplus/typescript/compiler';
2
+ import { checker, references } from '@esportsplus/typescript/compiler';
3
3
  import type { Origin } from '@esportsplus/typescript/compiler';
4
4
  import { ENTRYPOINT, PACKAGE_NAME, TYPES } from './constants';
5
5
  import type { Bindings } from './types';
@@ -7,7 +7,9 @@ import type { Bindings } from './types';
7
7
 
8
8
  type Assignment = { call: ts.CallExpression; target: ts.Identifier };
9
9
 
10
- // Per program snapshot: answers about declarations and types hold for the snapshot's lifetime
10
+ // Per program snapshot: answers about declarations and types hold for the snapshot's lifetime. `arrays`
11
+ // and `kinds` hold answers built from queries the shared checker does not memoize (getPropertyOfType,
12
+ // getNonNullableType, isArrayType), so dropping them costs a round-trip per array candidate or read
11
13
  type Caches = {
12
14
  arrays: Map<number, boolean>;
13
15
  assignments: Map<string, Map<string, Assignment[]>>;
@@ -204,13 +206,19 @@ const classify = (bindings: Bindings, call: ts.CallExpression): TYPES | null =>
204
206
  return type ? kindOf(bindings.checker, type) : null;
205
207
  };
206
208
 
207
- const create = (checker: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile): Bindings => ({
208
- checker,
209
- entries: new Set(references.exported(checker, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
210
- origins: references.origins(checker, program, sourceFile),
211
- program,
212
- sourceFile
213
- });
209
+ // Memoized per program (an already memoized checker is kept as is), so batched and repeated queries
210
+ // share one cache whoever hands the checker in
211
+ const create = (queries: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile): Bindings => {
212
+ let memoized = checker.cached(queries, program);
213
+
214
+ return {
215
+ checker: memoized,
216
+ entries: new Set(references.exported(memoized, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
217
+ origins: references.origins(memoized, program, sourceFile),
218
+ program,
219
+ sourceFile
220
+ };
221
+ };
214
222
 
215
223
  // Whether a callee expression denotes `reactive`: `reactive`, an alias, `ns.reactive`, `ns['reactive']`
216
224
  const denotes = (bindings: Bindings, expression: ts.Expression): boolean => {
@@ -293,6 +301,37 @@ const origin = (bindings: Bindings, node: ts.Node): Origin | null => {
293
301
  return bindings.origins.get(node) ?? references.origin(bindings.checker, bindings.program, node);
294
302
  };
295
303
 
304
+ // Types of the arguments classify() reads from the checker, requested in one round-trip; the
305
+ // memoized checker then answers each classify() from that request
306
+ const prefetch = (bindings: Bindings, calls: ts.CallExpression[]): void => {
307
+ let nodes: ts.Node[] = [];
308
+
309
+ for (let i = 0, n = calls.length; i < n; i++) {
310
+ let arg = calls[i].arguments[0];
311
+
312
+ if (!arg || calls[i].arguments.length !== 1 || ts.isSpreadElement(arg)) {
313
+ continue;
314
+ }
315
+
316
+ let value = unwrap(arg);
317
+
318
+ if (
319
+ ts.isArrayLiteralExpression(value) ||
320
+ ts.isArrowFunction(value) ||
321
+ ts.isFunctionExpression(value) ||
322
+ ts.isObjectLiteralExpression(value)
323
+ ) {
324
+ continue;
325
+ }
326
+
327
+ nodes.push(arg);
328
+ }
329
+
330
+ if (nodes.length > 0) {
331
+ bindings.checker.getTypeAtLocation(nodes);
332
+ }
333
+ };
334
+
296
335
 
297
336
  // The expression a value comes from, without parentheses, `!` and type assertions
298
337
  const unwrap = (expression: ts.Expression): ts.Expression => {
@@ -300,4 +339,4 @@ const unwrap = (expression: ts.Expression): ts.Expression => {
300
339
  };
301
340
 
302
341
 
303
- export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, unwrap };
342
+ export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, prefetch, unwrap };
@@ -9,6 +9,11 @@ import primitives from './primitives';
9
9
  import scope from './bindings';
10
10
 
11
11
 
12
+ // A binding from this package is only ever reached through an import, so a module without one has
13
+ // nothing to compile and is not resolved at all
14
+ const REGEX_IMPORT = /\bimport\b|\brequire\s*\(/;
15
+
16
+
12
17
  // Every use the compiler cannot lower fails the build, all of them reported at once: shipping one
13
18
  // would leave an uncompiled reactive() or an unread signal in the output
14
19
  function failed(sourceFile: ts.SourceFile, failures: Failure[]): Error {
@@ -64,13 +69,14 @@ function isLowered(site: ts.Node): boolean {
64
69
  }
65
70
 
66
71
 
67
- // `reactive`, or the member access naming it (`ns.reactive`, `ns['reactive']`)
72
+ // `reactive`, or the member access naming it (`ns.reactive`, `ns['reactive']`); in `list[reactive]`
73
+ // the key is the use
68
74
  function siteOf(identifier: ts.Node): ts.Node {
69
75
  let parent = identifier.parent!;
70
76
 
71
77
  if (
72
78
  (ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
73
- (ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)
79
+ (ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier && (ts.isStringLiteral(identifier) || ts.isNoSubstitutionTemplateLiteral(identifier)))
74
80
  ) {
75
81
  return parent;
76
82
  }
@@ -79,34 +85,30 @@ function siteOf(identifier: ts.Node): ts.Node {
79
85
  }
80
86
 
81
87
 
82
- // No text patterns: a module can reach `reactive`, or read a reactive binding, under any name
83
- // (a barrel's rename, a default re-export, a namespace), so every module is resolved against the
84
- // program and compiled exactly where its values resolve to reactive declarations
88
+ // No name patterns: a module can reach `reactive`, or read a reactive binding, under any name
89
+ // (a barrel's rename, a default re-export, a namespace), so every module that imports anything is
90
+ // resolved against the program and compiled exactly where its values resolve to reactive declarations
85
91
  export default {
92
+ name: PACKAGE_NAME,
86
93
  transform: (ctx: TransformContext) => {
94
+ if (!REGEX_IMPORT.test(ctx.code)) {
95
+ return {};
96
+ }
97
+
87
98
  let { checker, program, sourceFile } = ctx,
88
99
  bindings = scope.create(checker, program, sourceFile),
89
- calls = new Set<ts.Node>(),
100
+ calls = new Set<ts.CallExpression>(),
90
101
  dependencies = new Set<string>(),
102
+ entries = new Set<ts.Node>(),
91
103
  failures: Failure[] = [],
92
104
  self = sourceFile.fileName.toLowerCase();
93
105
 
94
106
  for (let [identifier, found] of bindings.origins) {
95
- let entry = scope.holds(bindings, found);
96
-
97
- if (!entry && scope.kind(bindings, found) === null) {
107
+ if (!scope.holds(bindings, found)) {
98
108
  continue;
99
109
  }
100
110
 
101
- for (let file of [...found.through, found.declaration.path]) {
102
- if (file.toLowerCase() !== self) {
103
- dependencies.add(file);
104
- }
105
- }
106
-
107
- if (!entry) {
108
- continue;
109
- }
111
+ entries.add(identifier);
110
112
 
111
113
  let site = siteOf(identifier),
112
114
  parent = site.parent;
@@ -126,7 +128,22 @@ export default {
126
128
  }
127
129
  }
128
130
 
129
- let isReactiveCall = (node: ts.Node): node is ts.CallExpression => calls.has(node),
131
+ // Every call is known before any binding is classified, so their argument types arrive in one request
132
+ scope.prefetch(bindings, [...calls]);
133
+
134
+ for (let [identifier, found] of bindings.origins) {
135
+ if (!entries.has(identifier) && scope.kind(bindings, found) === null) {
136
+ continue;
137
+ }
138
+
139
+ for (let file of [...found.through, found.declaration.path]) {
140
+ if (file.toLowerCase() !== self) {
141
+ dependencies.add(file);
142
+ }
143
+ }
144
+ }
145
+
146
+ let isReactiveCall = (node: ts.Node): node is ts.CallExpression => calls.has(node as ts.CallExpression),
130
147
  intents = {
131
148
  imports: [] as ImportIntent[],
132
149
  prepend: [] as string[],
@@ -179,7 +196,7 @@ export default {
179
196
  }
180
197
 
181
198
  intents.replacements.push({
182
- generate: () => `${NAMESPACE}.reactive(${call.arguments.map(a => a.getText(sourceFile)).join(', ')})`,
199
+ generate: (_sourceFile, text) => `${NAMESPACE}.reactive(${call.arguments.map(text).join(', ')})`,
183
200
  node: call
184
201
  });
185
202
  }
@@ -9,7 +9,7 @@ interface AnalyzedProperty {
9
9
  isStatic: boolean;
10
10
  key: string;
11
11
  type: TYPES;
12
- valueText: string;
12
+ value: ts.Expression;
13
13
  }
14
14
 
15
15
  type ObjectTransformResult = {
@@ -34,8 +34,6 @@ interface VisitContext {
34
34
 
35
35
 
36
36
  function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings): AnalyzedProperty | null {
37
- let sourceFile = bindings.sourceFile;
38
-
39
37
  if (!ts.isPropertyAssignment(prop)) {
40
38
  return null;
41
39
  }
@@ -50,8 +48,7 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings):
50
48
  }
51
49
 
52
50
  let unwrapped = prop.initializer,
53
- value = unwrapped,
54
- valueText = value.getText(sourceFile);
51
+ value = unwrapped;
55
52
 
56
53
  while (ts.isAsExpression(unwrapped) || ts.isTypeAssertion(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
57
54
  unwrapped = unwrapped.expression;
@@ -68,12 +65,12 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings):
68
65
  type.typeName.text === 'Array'
69
66
  )
70
67
  ) {
71
- return { isStatic: false, key, type: TYPES.Array, valueText };
68
+ return { isStatic: false, key, type: TYPES.Array, value };
72
69
  }
73
70
  }
74
71
 
75
72
  if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) {
76
- return { isStatic: false, key, type: TYPES.Computed, valueText };
73
+ return { isStatic: false, key, type: TYPES.Computed, value };
77
74
  }
78
75
 
79
76
  if (ts.isArrayLiteralExpression(unwrapped)) {
@@ -86,7 +83,7 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings):
86
83
  }
87
84
  }
88
85
 
89
- return { isStatic, key, type: TYPES.Array, valueText };
86
+ return { isStatic, key, type: TYPES.Array, value };
90
87
  }
91
88
 
92
89
  // A non-literal array is typed Reactive<T[]> on the object, so it must become a ReactiveArray
@@ -94,18 +91,18 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, bindings: Bindings):
94
91
  let type = isStaticValue(unwrapped) ? undefined : bindings.checker.getTypeAtLocation(value);
95
92
 
96
93
  if (type && (bindings.checker.isArrayType(type) || bindings.checker.isTupleType(type))) {
97
- return { isStatic: false, key, type: TYPES.Array, valueText };
94
+ return { isStatic: false, key, type: TYPES.Array, value };
98
95
  }
99
96
 
100
97
  return {
101
98
  isStatic: isStaticValue(value),
102
99
  key,
103
100
  type: TYPES.Signal,
104
- valueText
101
+ value
105
102
  };
106
103
  }
107
104
 
108
- function buildClassCode(classname: string, properties: AnalyzedProperty[], typehint: string | null): string {
105
+ function buildClassCode(classname: string, properties: AnalyzedProperty[], sourceFile: ts.SourceFile, typehint: string | null): string {
109
106
  let accessors: string[] = [],
110
107
  body: string[] = [],
111
108
  constraint: string[] = [],
@@ -115,7 +112,7 @@ function buildClassCode(classname: string, properties: AnalyzedProperty[], typeh
115
112
  setters = 0;
116
113
 
117
114
  for (let i = 0, n = properties.length; i < n; i++) {
118
- let { isStatic, key, type, valueText } = properties[i],
115
+ let { isStatic, key, type, value: initializer } = properties[i],
119
116
  generic = typehint ? `T['${key}']` : `T${parameters.length}`,
120
117
  parameter = `_p${parameters.length}`;
121
118
 
@@ -137,7 +134,8 @@ function buildClassCode(classname: string, properties: AnalyzedProperty[], typeh
137
134
  ${NAMESPACE}.write(this.#${key}, ${value});
138
135
  }
139
136
  `);
140
- fields.push(`#${key} = this[${NAMESPACE}.SIGNAL](${valueText});`);
137
+ // A static value is a literal, which no plugin rewrites, so its source is the output
138
+ fields.push(`#${key} = this[${NAMESPACE}.SIGNAL](${initializer.getText(sourceFile)});`);
141
139
  }
142
140
  else {
143
141
  accessors.push(`
@@ -286,13 +284,13 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: I
286
284
  let call = ctx.calls[i],
287
285
  typehint = call.typehint;
288
286
 
289
- prepend.push(buildClassCode(call.classname, call.properties, typehint));
287
+ prepend.push(buildClassCode(call.classname, call.properties, sourceFile, typehint));
290
288
  replacements.push({
291
- generate: () => {
289
+ generate: (_sourceFile, text) => {
292
290
  // Every property but a static signal (baked into its field) is a constructor parameter
293
291
  let args = call.properties
294
292
  .filter(({ isStatic, type }) => typehint || !isStatic || type !== TYPES.Signal)
295
- .map(p => p.valueText)
293
+ .map(p => text(p.value))
296
294
  .join(', ');
297
295
 
298
296
  return typehint
@@ -144,13 +144,14 @@ function isPatternTarget(node: ts.Node): boolean {
144
144
  ((ts.isForOfStatement(parent) || ts.isForInStatement(parent)) && parent.initializer === current);
145
145
  }
146
146
 
147
- // A binding read as `ns.count` / `ns['count']` is rewritten as a whole
147
+ // A binding read as `ns.count` / `ns['count']` is rewritten as a whole; in `list[count]` the key is
148
+ // the read
148
149
  function readNode(identifier: ts.Node): ts.Expression {
149
150
  let parent = identifier.parent!;
150
151
 
151
152
  if (
152
153
  (ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
153
- (ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)
154
+ (ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier && (ts.isStringLiteral(identifier) || ts.isNoSubstitutionTemplateLiteral(identifier)))
154
155
  ) {
155
156
  return parent;
156
157
  }
@@ -175,11 +176,13 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
175
176
  return;
176
177
  }
177
178
 
178
- let text = node.getText(ctx.bindings.sourceFile);
179
+ // The binding itself (`count`, `ns.count`, `ns['count']`) holds nothing another rewrite replaces,
180
+ // so its source is copied as is: text() walks the file's intents on every call
181
+ let binding = node.getText(ctx.bindings.sourceFile);
179
182
 
180
183
  if (ts.isShorthandPropertyAssignment(parent)) {
181
184
  ctx.replacements.push({
182
- generate: () => `${text}: ${NAMESPACE}.read(${text})`,
185
+ generate: () => `${binding}: ${NAMESPACE}.read(${binding})`,
183
186
  node: parent
184
187
  });
185
188
 
@@ -208,7 +211,7 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
208
211
 
209
212
  if (!writeCtx) {
210
213
  ctx.replacements.push({
211
- generate: () => `${NAMESPACE}.read(${text})`,
214
+ generate: () => `${NAMESPACE}.read(${binding})`,
212
215
  node
213
216
  });
214
217
 
@@ -224,7 +227,7 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
224
227
  let right = parent.right;
225
228
 
226
229
  ctx.replacements.push({
227
- generate: (sf) => `${NAMESPACE}.write(${text}, ${right.getText(sf)})`,
230
+ generate: (_sourceFile, text) => `${NAMESPACE}.write(${binding}, ${text(right)})`,
228
231
  node: parent
229
232
  });
230
233
  }
@@ -233,7 +236,7 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
233
236
  right = parent.right;
234
237
 
235
238
  ctx.replacements.push({
236
- generate: (sf) => `${NAMESPACE}.write(${text}, ${text}.value ${op} ${right.getText(sf)})`,
239
+ generate: (_sourceFile, text) => `${NAMESPACE}.write(${binding}, ${binding}.value ${op} ${text(right)})`,
237
240
  node: parent
238
241
  });
239
242
  }
@@ -243,13 +246,13 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
243
246
 
244
247
  if (ts.isExpressionStatement(unary.parent)) {
245
248
  ctx.replacements.push({
246
- generate: () => `${NAMESPACE}.write(${text}, ${text}.value ${delta})`,
249
+ generate: () => `${NAMESPACE}.write(${binding}, ${binding}.value ${delta})`,
247
250
  node: unary
248
251
  });
249
252
  }
250
253
  else if (ts.isPrefixUnaryExpression(unary)) {
251
254
  ctx.replacements.push({
252
- generate: () => `(${NAMESPACE}.write(${text}, ${text}.value ${delta}), ${text}.value)`,
255
+ generate: () => `(${NAMESPACE}.write(${binding}, ${binding}.value ${delta}), ${binding}.value)`,
253
256
  node: unary
254
257
  });
255
258
  }
@@ -257,7 +260,7 @@ function reference(ctx: TransformContext, node: ts.Expression, kind: TYPES): voi
257
260
  let tmp = `_t${ctx.tmpCounter++}`;
258
261
 
259
262
  ctx.replacements.push({
260
- generate: () => `((${tmp}) => (${NAMESPACE}.write(${text}, ${tmp} ${delta}), ${tmp}))(${text}.value)`,
263
+ generate: () => `((${tmp}) => (${NAMESPACE}.write(${binding}, ${tmp} ${delta}), ${tmp}))(${binding}.value)`,
261
264
  node: unary
262
265
  });
263
266
  }
@@ -1,8 +1,8 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
- import { PACKAGE_NAME, REACTIVE_ARRAY } from '~/constants';
3
- import { effect, read, root, signal, write } from '~/system';
4
- import type { Signal } from '~/types';
2
+ import { PACKAGE_NAME, REACTIVE_ARRAY } from '../constants';
3
+ import { effect, read, root, signal, write } from '../system';
5
4
  import { isReactiveObject } from './object';
5
+ import type { Signal } from '../types';
6
6
 
7
7
 
8
8
  // One mutation as the batch event reports it: the matching event's payload, named by 'op'
@@ -431,6 +431,7 @@ class ReactiveArray<T> extends Array<T> {
431
431
  }
432
432
  else {
433
433
  let n = this.length,
434
+ // oxlint-disable-next-line unicorn/no-new-array -- preallocated to n, every slot is written next
434
435
  before = new Array(n) as T[];
435
436
 
436
437
  for (let i = 0; i < n; i++) {
@@ -440,6 +441,7 @@ class ReactiveArray<T> extends Array<T> {
440
441
  super.sort(fn);
441
442
 
442
443
  let buckets = new Map<T, { indices: number[]; next: number }>(),
444
+ // oxlint-disable-next-line unicorn/no-new-array -- preallocated to n, every slot is written next
443
445
  order = new Array(n);
444
446
 
445
447
  for (let i = 0; i < n; i++) {
@@ -1,9 +1,9 @@
1
1
  import { isArray, isObject } from '@esportsplus/utilities';
2
- import { PACKAGE_NAME } from '~/constants';
3
- import { onCleanup, root } from '~/system';
4
- import type { Reactive } from '~/types';
2
+ import { PACKAGE_NAME } from '../constants';
3
+ import { onCleanup, root } from '../system';
5
4
  import { ReactiveArray } from './array';
6
5
  import { ReactiveObject } from './object';
6
+ import type { Reactive } from '../types';
7
7
 
8
8
 
9
9
  type Guard<T> =
@@ -1,8 +1,8 @@
1
1
  import { defineProperty, isArray } from '@esportsplus/utilities';
2
- import { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from '~/constants';
3
- import { computed, dispose, read, root, signal, write } from '~/system';
4
- import { Computed } from '~/types';
2
+ import { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from '../constants';
3
+ import { computed, dispose, read, root, signal, write } from '../system';
5
4
  import { ReactiveArray } from './array';
5
+ import type { Computed } from '../types';
6
6
 
7
7
 
8
8
  class ReactiveObject<T extends Record<PropertyKey, unknown>> {
@@ -87,7 +87,7 @@ class ReactiveObject<T extends Record<PropertyKey, unknown>> {
87
87
  return;
88
88
  }
89
89
 
90
- while (disposer = disposers.pop()) {
90
+ while ((disposer = disposers.pop())) {
91
91
  disposer();
92
92
  }
93
93
  }