@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
package/README.md CHANGED
@@ -332,13 +332,17 @@ All standard array methods (`push`, `pop`, `shift`, `unshift`, `splice`, `sort`,
332
332
 
333
333
  ```bash
334
334
  pnpm install
335
- pnpm build # tsc
336
- pnpm test # vitest run
337
- pnpm bench # vitest bench --run
335
+ pnpm build # tsc -p tsconfig.build.json
336
+ pnpm typecheck # tsc --noEmit over src, test and bench
337
+ pnpm lint # oxlint, type-aware, warnings denied
338
+ pnpm test # vitest run
339
+ pnpm bench # vitest bench --run (add --reporter=verbose to print results when piped)
340
+ pnpm verify # build, typecheck, lint and test
338
341
  ```
339
342
 
340
- `agent:test` (`tsc --noEmit && vitest run`) and `agent:bench` (`vitest bench --run`) are the
341
- CI-facing aliases of the same build/test/bench commands.
343
+ `agent:test` (`pnpm typecheck && vitest run`) and `agent:bench` (`vitest bench --run`) are the
344
+ CI-facing aliases of the same test/bench commands. `test/compiler/output.test.ts` pins whole
345
+ compiled modules; a compiler change that alters them must update the snapshot deliberately.
342
346
 
343
347
  ## Changes in 0.35.0
344
348
 
@@ -0,0 +1,115 @@
1
+ import { afterAll, test } from 'vitest';
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { coordinator, languageService } from '@esportsplus/typescript/compiler';
6
+
7
+ import pipeline from '~/compiler/index';
8
+
9
+
10
+ // A small app: a state module, a central barrel, consumers reading through it, and modules the
11
+ // compiler must leave alone (no import at all, or imports that never reach reactive)
12
+ const FILES: Record<string, string> = {
13
+ 'app.ts': [
14
+ "export { reactive as make } from '@esportsplus/reactivity';",
15
+ "export * from './state';"
16
+ ].join('\n'),
17
+ 'consumer.ts': [
18
+ "import { count, list, make, store } from './app';",
19
+ "import * as app from './app';",
20
+ 'export let local = make(0);',
21
+ 'export let a = () => count + app.count + local;',
22
+ 'export let b = () => list.length + store.items.length;',
23
+ 'export let c = () => { list[0] = count; local += count; };'
24
+ ].join('\n'),
25
+ 'helpers.ts': [
26
+ "import { join } from 'node:path';",
27
+ 'export let path = (a: string, b: string) => join(a, b);',
28
+ 'export let sum = (values: number[]) => values.reduce((total, value) => total + value, 0);'
29
+ ].join('\n'),
30
+ 'plain.ts': [
31
+ 'export let clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));',
32
+ 'export let range = (n: number) => Array.from({ length: n }, (_, i) => i);'
33
+ ].join('\n'),
34
+ 'runtime.ts': [
35
+ "import { reactive } from '@esportsplus/reactivity';",
36
+ 'let count = reactive(0);',
37
+ 'let doubled = reactive(() => count * 2);',
38
+ 'let list = reactive([1, 2, 3]);',
39
+ 'let store = reactive({ items: [1], n: 1 });',
40
+ 'export let run = (): number => {',
41
+ ' let total = 0;',
42
+ ' for (let i = 0; i < 100; i++) {',
43
+ ' count++;',
44
+ ' list[0] = i;',
45
+ ' store.n = i;',
46
+ ' total += doubled + list.length + store.n;',
47
+ ' }',
48
+ ' return total;',
49
+ '};'
50
+ ].join('\n'),
51
+ 'state.ts': [
52
+ "import { reactive } from '@esportsplus/reactivity';",
53
+ 'export let count = reactive(0);',
54
+ 'export let list = reactive([1, 2]);',
55
+ 'export let store = reactive({ items: [1], n: 1 });'
56
+ ].join('\n')
57
+ };
58
+
59
+ const NAMES = Object.keys(FILES);
60
+
61
+
62
+ let directory = mkdtempSync(join(process.cwd(), '.fixture-bench-')).replace(/\\/g, '/'),
63
+ config = directory + '/tsconfig.json';
64
+
65
+ for (let i = 0, n = NAMES.length; i < n; i++) {
66
+ writeFileSync(join(directory, NAMES[i]), FILES[NAMES[i]]);
67
+ }
68
+
69
+ writeFileSync(config, JSON.stringify({
70
+ compilerOptions: { module: 'esnext', moduleResolution: 'bundler', strict: true, target: 'esnext', types: [] },
71
+ files: NAMES.map(name => './' + name)
72
+ }));
73
+
74
+
75
+ // Each pass hands the coordinator a checker it has not seen, so its memo starts empty and every
76
+ // query a transform needs reaches tsgo, as when a build transforms each module once
77
+ function transform(name: string): string {
78
+ let id = directory + '/' + name,
79
+ { checker, program } = languageService.update(config, id, FILES[name]);
80
+
81
+ return coordinator.transform([pipeline], FILES[name], program.getSourceFile(id)!, { checker: new Proxy(checker, {}), program }, directory, new Map()).code;
82
+ }
83
+
84
+
85
+ afterAll(() => {
86
+ languageService.dispose();
87
+ rmSync(directory, { force: true, recursive: true });
88
+ });
89
+
90
+
91
+ test('compiler transform', async ({ bench }) => {
92
+ await bench.compare(
93
+ bench('every module of the app', () => {
94
+ for (let i = 0, n = NAMES.length; i < n; i++) {
95
+ transform(NAMES[i]);
96
+ }
97
+ }),
98
+ bench('modules without reactive (no import, unrelated import)', () => {
99
+ transform('helpers.ts');
100
+ transform('plain.ts');
101
+ })
102
+ );
103
+ });
104
+
105
+ test('compiled output', async ({ bench }) => {
106
+ let file = directory + '/runtime.compiled.ts';
107
+
108
+ writeFileSync(file, transform('runtime.ts'));
109
+
110
+ let { run } = await import(pathToFileURL(file).href) as { run: () => number };
111
+
112
+ await bench('signal, computed, array and object reads and writes', () => {
113
+ run();
114
+ }).run();
115
+ });
@@ -157,7 +157,7 @@ function diamond() {
157
157
  }
158
158
 
159
159
  function mux() {
160
- let heads = new Array(100).fill(null).map(() => framework.signal(0));
160
+ let heads = Array.from({ length: 100 }, () => framework.signal(0));
161
161
 
162
162
  let muxed = framework.computed(() => Object.fromEntries(heads.map((h) => h.read()).entries()));
163
163
 
@@ -231,6 +231,7 @@ test('ReactiveArray reactive length', async ({ bench }) => {
231
231
  let arr = new ReactiveArray([1, 2, 3]);
232
232
 
233
233
  let stop = effect(() => {
234
+ // oxlint-disable-next-line no-unused-expressions -- the tracked read is what is measured
234
235
  arr.$length;
235
236
  });
236
237
 
@@ -16,8 +16,8 @@ test('ReactiveObject creation', async ({ bench }) => {
16
16
  }),
17
17
  bench('create with async computed property', () => {
18
18
  new ReactiveObject({
19
- value: 1,
20
- async: () => Promise.resolve(0)
19
+ async: () => Promise.resolve(0),
20
+ value: 1
21
21
  });
22
22
  })
23
23
  );
@@ -29,6 +29,7 @@ test('ReactiveObject read/write', async ({ bench }) => {
29
29
  bench('read property (signal-backed)', () => {
30
30
  let obj = new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 });
31
31
 
32
+ // oxlint-disable-next-line no-unused-expressions -- the getter read is what is measured
32
33
  (obj as any).a;
33
34
  }),
34
35
  bench('write property (signal-backed)', () => {
@@ -32,10 +32,10 @@ function declare(ctx, node) {
32
32
  }
33
33
  let typeParam = elementType ? `<${elementType}>` : '';
34
34
  ctx.replacements.push({
35
- node,
36
- generate: (sf) => expression.elements.length > 0
37
- ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${expression.getText(sf)})`
38
- : ` new ${NAMESPACE}.ReactiveArray${typeParam}()`
35
+ generate: (_sourceFile, text) => expression.elements.length > 0
36
+ ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${text(expression)})`
37
+ : ` new ${NAMESPACE}.ReactiveArray${typeParam}()`,
38
+ node
39
39
  });
40
40
  }
41
41
  }
@@ -73,10 +73,10 @@ function length(access, replacements) {
73
73
  if (parent && ts.isBinaryExpression(parent) && parent.left === access && (parent.operatorToken.kind === ts.SyntaxKind.EqualsToken || COMPOUND_OPERATORS.has(parent.operatorToken.kind))) {
74
74
  let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
75
75
  replacements.push({
76
- node: parent,
77
- generate: (sf) => op === undefined
78
- ? `${expr.getText(sf)}.$length = ${parent.right.getText(sf)}`
79
- : `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} ${parent.right.getText(sf)}`
76
+ generate: (_sourceFile, text) => op === undefined
77
+ ? `${text(expr)}.$length = ${text(parent.right)}`
78
+ : `${text(expr)}.$length = ${text(expr)}.length ${op} ${text(parent.right)}`,
79
+ node: parent
80
80
  });
81
81
  }
82
82
  else if (parent &&
@@ -84,19 +84,19 @@ function length(access, replacements) {
84
84
  (parent.operator === ts.SyntaxKind.PlusPlusToken || parent.operator === ts.SyntaxKind.MinusMinusToken)) {
85
85
  let op = parent.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
86
86
  replacements.push({
87
- node: parent,
88
- generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} 1`
87
+ generate: (_sourceFile, text) => `${text(expr)}.$length = ${text(expr)}.length ${op} 1`,
88
+ node: parent
89
89
  });
90
90
  }
91
91
  else {
92
92
  replacements.push({
93
- node: access,
94
- generate: (sf) => `${expr.getText(sf)}.$length`
93
+ generate: (_sourceFile, text) => `${text(expr)}.$length`,
94
+ node: access
95
95
  });
96
96
  }
97
97
  }
98
98
  function write(access, ctx) {
99
- let parent = access.parent, array = (sf) => access.expression.getText(sf), index = (sf) => access.argumentExpression.getText(sf);
99
+ let parent = access.parent;
100
100
  if (ts.isDeleteExpression(parent)) {
101
101
  ctx.failures.push({ message: 'an element of a reactive array cannot be deleted; use splice()', node: parent });
102
102
  return;
@@ -104,19 +104,22 @@ function write(access, ctx) {
104
104
  if (ts.isBinaryExpression(parent)) {
105
105
  let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind), right = parent.right;
106
106
  ctx.replacements.push({
107
- node: parent,
108
- generate: (sf) => op === undefined
109
- ? `${array(sf)}.$set(${index(sf)}, ${right.getText(sf)})`
110
- : `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${right.getText(sf)})))(${array(sf)}, ${index(sf)})`
107
+ generate: (_sourceFile, text) => op === undefined
108
+ ? `${text(access.expression)}.$set(${text(access.argumentExpression)}, ${text(right)})`
109
+ : `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${text(right)})))(${text(access.expression)}, ${text(access.argumentExpression)})`,
110
+ node: parent
111
111
  });
112
112
  return;
113
113
  }
114
114
  let unary = parent, op = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
115
115
  ctx.replacements.push({
116
- node: unary,
117
- generate: (sf) => ts.isPrefixUnaryExpression(unary)
118
- ? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array(sf)}, ${index(sf)})`
119
- : `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array(sf)}, ${index(sf)}, ${array(sf)}[${index(sf)}])`
116
+ generate: (_sourceFile, text) => {
117
+ let array = text(access.expression), index = text(access.argumentExpression);
118
+ return ts.isPrefixUnaryExpression(unary)
119
+ ? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array}, ${index})`
120
+ : `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array}, ${index}, ${array}[${index}])`;
121
+ },
122
+ node: unary
120
123
  });
121
124
  }
122
125
  export default (sourceFile, bindings, isReactiveCall) => {
@@ -147,10 +150,10 @@ export default (sourceFile, bindings, isReactiveCall) => {
147
150
  }
148
151
  let name = assignment.left.text, elements = scope.unwrap(assignment.right);
149
152
  result.replacements.push({
150
- node: assignment,
151
- generate: (sf) => elements.elements.length > 0
152
- ? `${name}.splice(0, ${name}.length, ...${elements.getText(sf)})`
153
- : `${name}.splice(0, ${name}.length)`
153
+ generate: (_sourceFile, text) => elements.elements.length > 0
154
+ ? `${name}.splice(0, ${name}.length, ...${text(elements)})`
155
+ : `${name}.splice(0, ${name}.length)`,
156
+ node: assignment
154
157
  });
155
158
  }
156
159
  return result;
@@ -4,12 +4,13 @@ import { TYPES } from './constants.js';
4
4
  import type { Bindings } from './types.js';
5
5
  declare const arrays: (bindings: Bindings, nodes: ts.Node[]) => Set<ts.Node>;
6
6
  declare const classify: (bindings: Bindings, call: ts.CallExpression) => TYPES | null;
7
- declare const create: (checker: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile) => Bindings;
7
+ declare const create: (queries: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile) => Bindings;
8
8
  declare const denotes: (bindings: Bindings, expression: ts.Expression) => boolean;
9
9
  declare const holds: (bindings: Bindings, found: Origin | null) => boolean;
10
10
  declare const isReactiveCall: (bindings: Bindings, node: ts.Node) => node is ts.CallExpression;
11
11
  declare const kind: (bindings: Bindings, found: Origin | null) => TYPES | null;
12
12
  declare const origin: (bindings: Bindings, node: ts.Node) => Origin | null;
13
+ declare const prefetch: (bindings: Bindings, calls: ts.CallExpression[]) => void;
13
14
  declare const unwrap: (expression: ts.Expression) => ts.Expression;
14
15
  declare const _default: {
15
16
  arrays: typeof arrays;
@@ -20,6 +21,7 @@ declare const _default: {
20
21
  isReactiveCall: typeof isReactiveCall;
21
22
  kind: typeof kind;
22
23
  origin: typeof origin;
24
+ prefetch: typeof prefetch;
23
25
  unwrap: typeof unwrap;
24
26
  };
25
27
  export default _default;
@@ -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 { ENTRYPOINT, PACKAGE_NAME, TYPES } from './constants.js';
4
4
  const ARRAY_BRAND = 'once';
5
5
  const REACTIVE_ARRAY = 'ReactiveArray';
@@ -121,13 +121,16 @@ const classify = (bindings, call) => {
121
121
  let type = bindings.checker.getTypeAtLocation(arg);
122
122
  return type ? kindOf(bindings.checker, type) : null;
123
123
  };
124
- const create = (checker, program, sourceFile) => ({
125
- checker,
126
- entries: new Set(references.exported(checker, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
127
- origins: references.origins(checker, program, sourceFile),
128
- program,
129
- sourceFile
130
- });
124
+ const create = (queries, program, sourceFile) => {
125
+ let memoized = checker.cached(queries, program);
126
+ return {
127
+ checker: memoized,
128
+ entries: new Set(references.exported(memoized, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
129
+ origins: references.origins(memoized, program, sourceFile),
130
+ program,
131
+ sourceFile
132
+ };
133
+ };
131
134
  const denotes = (bindings, expression) => {
132
135
  return references.denotes(bindings.checker, bindings.program, expression, bindings.entries);
133
136
  };
@@ -179,7 +182,27 @@ const kind = (bindings, found) => {
179
182
  const origin = (bindings, node) => {
180
183
  return bindings.origins.get(node) ?? references.origin(bindings.checker, bindings.program, node);
181
184
  };
185
+ const prefetch = (bindings, calls) => {
186
+ let nodes = [];
187
+ for (let i = 0, n = calls.length; i < n; i++) {
188
+ let arg = calls[i].arguments[0];
189
+ if (!arg || calls[i].arguments.length !== 1 || ts.isSpreadElement(arg)) {
190
+ continue;
191
+ }
192
+ let value = unwrap(arg);
193
+ if (ts.isArrayLiteralExpression(value) ||
194
+ ts.isArrowFunction(value) ||
195
+ ts.isFunctionExpression(value) ||
196
+ ts.isObjectLiteralExpression(value)) {
197
+ continue;
198
+ }
199
+ nodes.push(arg);
200
+ }
201
+ if (nodes.length > 0) {
202
+ bindings.checker.getTypeAtLocation(nodes);
203
+ }
204
+ };
182
205
  const unwrap = (expression) => {
183
206
  return references.unwrap(expression);
184
207
  };
185
- export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, unwrap };
208
+ export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, prefetch, unwrap };
@@ -1,9 +1,10 @@
1
1
  import type { ImportIntent, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
2
  declare const _default: {
3
+ name: string;
3
4
  transform: (ctx: TransformContext) => {
4
- dependencies: string[];
5
- } | {
6
5
  dependencies?: undefined;
6
+ } | {
7
+ dependencies: string[];
7
8
  } | {
8
9
  imports: ImportIntent[];
9
10
  prepend: string[];
@@ -4,6 +4,7 @@ import array from './array.js';
4
4
  import object from './object.js';
5
5
  import primitives from './primitives.js';
6
6
  import scope from './bindings.js';
7
+ const REGEX_IMPORT = /\bimport\b|\brequire\s*\(/;
7
8
  function failed(sourceFile, failures) {
8
9
  let lines = [];
9
10
  for (let i = 0, n = failures.length; i < n; i++) {
@@ -42,27 +43,23 @@ function isLowered(site) {
42
43
  function siteOf(identifier) {
43
44
  let parent = identifier.parent;
44
45
  if ((ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
45
- (ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)) {
46
+ (ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier && (ts.isStringLiteral(identifier) || ts.isNoSubstitutionTemplateLiteral(identifier)))) {
46
47
  return parent;
47
48
  }
48
49
  return identifier;
49
50
  }
50
51
  export default {
52
+ name: PACKAGE_NAME,
51
53
  transform: (ctx) => {
52
- let { checker, program, sourceFile } = ctx, bindings = scope.create(checker, program, sourceFile), calls = new Set(), dependencies = new Set(), failures = [], self = sourceFile.fileName.toLowerCase();
54
+ if (!REGEX_IMPORT.test(ctx.code)) {
55
+ return {};
56
+ }
57
+ let { checker, program, sourceFile } = ctx, bindings = scope.create(checker, program, sourceFile), calls = new Set(), dependencies = new Set(), entries = new Set(), failures = [], self = sourceFile.fileName.toLowerCase();
53
58
  for (let [identifier, found] of bindings.origins) {
54
- let entry = scope.holds(bindings, found);
55
- if (!entry && scope.kind(bindings, found) === null) {
56
- continue;
57
- }
58
- for (let file of [...found.through, found.declaration.path]) {
59
- if (file.toLowerCase() !== self) {
60
- dependencies.add(file);
61
- }
62
- }
63
- if (!entry) {
59
+ if (!scope.holds(bindings, found)) {
64
60
  continue;
65
61
  }
62
+ entries.add(identifier);
66
63
  let site = siteOf(identifier), parent = site.parent;
67
64
  while (parent && ts.isParenthesizedExpression(parent)) {
68
65
  parent = parent.parent;
@@ -77,6 +74,17 @@ export default {
77
74
  });
78
75
  }
79
76
  }
77
+ scope.prefetch(bindings, [...calls]);
78
+ for (let [identifier, found] of bindings.origins) {
79
+ if (!entries.has(identifier) && scope.kind(bindings, found) === null) {
80
+ continue;
81
+ }
82
+ for (let file of [...found.through, found.declaration.path]) {
83
+ if (file.toLowerCase() !== self) {
84
+ dependencies.add(file);
85
+ }
86
+ }
87
+ }
80
88
  let isReactiveCall = (node) => calls.has(node), intents = {
81
89
  imports: [],
82
90
  prepend: [],
@@ -111,7 +119,7 @@ export default {
111
119
  continue;
112
120
  }
113
121
  intents.replacements.push({
114
- generate: () => `${NAMESPACE}.reactive(${call.arguments.map(a => a.getText(sourceFile)).join(', ')})`,
122
+ generate: (_sourceFile, text) => `${NAMESPACE}.reactive(${call.arguments.map(text).join(', ')})`,
115
123
  node: call
116
124
  });
117
125
  }
@@ -2,7 +2,6 @@ import { ts } from '@esportsplus/typescript';
2
2
  import { code, uid } from '@esportsplus/typescript/compiler';
3
3
  import { NAMESPACE, TYPES } from './constants.js';
4
4
  function analyzeProperty(prop, bindings) {
5
- let sourceFile = bindings.sourceFile;
6
5
  if (!ts.isPropertyAssignment(prop)) {
7
6
  return null;
8
7
  }
@@ -13,7 +12,7 @@ function analyzeProperty(prop, bindings) {
13
12
  else {
14
13
  return null;
15
14
  }
16
- let unwrapped = prop.initializer, value = unwrapped, valueText = value.getText(sourceFile);
15
+ let unwrapped = prop.initializer, value = unwrapped;
17
16
  while (ts.isAsExpression(unwrapped) || ts.isTypeAssertion(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
18
17
  unwrapped = unwrapped.expression;
19
18
  }
@@ -23,11 +22,11 @@ function analyzeProperty(prop, bindings) {
23
22
  (ts.isTypeReferenceNode(type) &&
24
23
  ts.isIdentifier(type.typeName) &&
25
24
  type.typeName.text === 'Array')) {
26
- return { isStatic: false, key, type: TYPES.Array, valueText };
25
+ return { isStatic: false, key, type: TYPES.Array, value };
27
26
  }
28
27
  }
29
28
  if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) {
30
- return { isStatic: false, key, type: TYPES.Computed, valueText };
29
+ return { isStatic: false, key, type: TYPES.Computed, value };
31
30
  }
32
31
  if (ts.isArrayLiteralExpression(unwrapped)) {
33
32
  let elements = unwrapped.elements, isStatic = value === unwrapped;
@@ -36,23 +35,23 @@ function analyzeProperty(prop, bindings) {
36
35
  isStatic = false;
37
36
  }
38
37
  }
39
- return { isStatic, key, type: TYPES.Array, valueText };
38
+ return { isStatic, key, type: TYPES.Array, value };
40
39
  }
41
40
  let type = isStaticValue(unwrapped) ? undefined : bindings.checker.getTypeAtLocation(value);
42
41
  if (type && (bindings.checker.isArrayType(type) || bindings.checker.isTupleType(type))) {
43
- return { isStatic: false, key, type: TYPES.Array, valueText };
42
+ return { isStatic: false, key, type: TYPES.Array, value };
44
43
  }
45
44
  return {
46
45
  isStatic: isStaticValue(value),
47
46
  key,
48
47
  type: TYPES.Signal,
49
- valueText
48
+ value
50
49
  };
51
50
  }
52
- function buildClassCode(classname, properties, typehint) {
51
+ function buildClassCode(classname, properties, sourceFile, typehint) {
53
52
  let accessors = [], body = [], constraint = [], fields = [], generics = [], parameters = [], setters = 0;
54
53
  for (let i = 0, n = properties.length; i < n; i++) {
55
- let { isStatic, key, type, valueText } = properties[i], generic = typehint ? `T['${key}']` : `T${parameters.length}`, parameter = `_p${parameters.length}`;
54
+ let { isStatic, key, type, value: initializer } = properties[i], generic = typehint ? `T['${key}']` : `T${parameters.length}`, parameter = `_p${parameters.length}`;
56
55
  if (typehint && type === TYPES.Signal) {
57
56
  constraint.push(`'${key}'?: unknown`);
58
57
  isStatic = false;
@@ -68,7 +67,7 @@ function buildClassCode(classname, properties, typehint) {
68
67
  ${NAMESPACE}.write(this.#${key}, ${value});
69
68
  }
70
69
  `);
71
- fields.push(`#${key} = this[${NAMESPACE}.SIGNAL](${valueText});`);
70
+ fields.push(`#${key} = this[${NAMESPACE}.SIGNAL](${initializer.getText(sourceFile)});`);
72
71
  }
73
72
  else {
74
73
  accessors.push(`
@@ -186,12 +185,12 @@ export default (sourceFile, bindings, isReactiveCall) => {
186
185
  let prepend = [], replacements = [];
187
186
  for (let i = 0, n = ctx.calls.length; i < n; i++) {
188
187
  let call = ctx.calls[i], typehint = call.typehint;
189
- prepend.push(buildClassCode(call.classname, call.properties, typehint));
188
+ prepend.push(buildClassCode(call.classname, call.properties, sourceFile, typehint));
190
189
  replacements.push({
191
- generate: () => {
190
+ generate: (_sourceFile, text) => {
192
191
  let args = call.properties
193
192
  .filter(({ isStatic, type }) => typehint || !isStatic || type !== TYPES.Signal)
194
- .map(p => p.valueText)
193
+ .map(p => text(p.value))
195
194
  .join(', ');
196
195
  return typehint
197
196
  ? ` new ${call.classname}<${typehint}>(${args})`
@@ -1,6 +1,7 @@
1
1
  declare const _default: ({ root }?: {
2
2
  root?: string;
3
3
  }) => {
4
+ buildStart: () => void;
4
5
  closeBundle: () => void;
5
6
  closeWatcher: () => void;
6
7
  configResolved: (config: unknown) => void;
@@ -20,10 +21,18 @@ declare const _default: ({ root }?: {
20
21
  renderChunk: (code: string, chunk: {
21
22
  fileName: string;
22
23
  }) => null;
23
- transform: (code: string, id: string) => {
24
- code: string;
25
- map: import("@esportsplus/typescript/compiler").SourceMapV3;
26
- } | null;
24
+ transform: {
25
+ filter: {
26
+ id: {
27
+ exclude: RegExp[];
28
+ include: RegExp;
29
+ };
30
+ };
31
+ handler: (code: string, id: string) => {
32
+ code: string;
33
+ map: import("@esportsplus/typescript/compiler").SourceMapV3;
34
+ } | null;
35
+ };
27
36
  watchChange: (id: string) => void;
28
37
  };
29
38
  export default _default;
@@ -86,7 +86,7 @@ function isPatternTarget(node) {
86
86
  function readNode(identifier) {
87
87
  let parent = identifier.parent;
88
88
  if ((ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
89
- (ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier)) {
89
+ (ts.isElementAccessExpression(parent) && parent.argumentExpression === identifier && (ts.isStringLiteral(identifier) || ts.isNoSubstitutionTemplateLiteral(identifier)))) {
90
90
  return parent;
91
91
  }
92
92
  return identifier;
@@ -103,10 +103,10 @@ function reference(ctx, node, kind) {
103
103
  ctx.failures.push({ message: 'a reactive binding cannot be assigned through a destructuring pattern; assign it directly', node });
104
104
  return;
105
105
  }
106
- let text = node.getText(ctx.bindings.sourceFile);
106
+ let binding = node.getText(ctx.bindings.sourceFile);
107
107
  if (ts.isShorthandPropertyAssignment(parent)) {
108
108
  ctx.replacements.push({
109
- generate: () => `${text}: ${NAMESPACE}.read(${text})`,
109
+ generate: () => `${binding}: ${NAMESPACE}.read(${binding})`,
110
110
  node: parent
111
111
  });
112
112
  return;
@@ -129,7 +129,7 @@ function reference(ctx, node, kind) {
129
129
  }
130
130
  if (!writeCtx) {
131
131
  ctx.replacements.push({
132
- generate: () => `${NAMESPACE}.read(${text})`,
132
+ generate: () => `${NAMESPACE}.read(${binding})`,
133
133
  node
134
134
  });
135
135
  return;
@@ -141,14 +141,14 @@ function reference(ctx, node, kind) {
141
141
  if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
142
142
  let right = parent.right;
143
143
  ctx.replacements.push({
144
- generate: (sf) => `${NAMESPACE}.write(${text}, ${right.getText(sf)})`,
144
+ generate: (_sourceFile, text) => `${NAMESPACE}.write(${binding}, ${text(right)})`,
145
145
  node: parent
146
146
  });
147
147
  }
148
148
  else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
149
149
  let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind) ?? '+', right = parent.right;
150
150
  ctx.replacements.push({
151
- generate: (sf) => `${NAMESPACE}.write(${text}, ${text}.value ${op} ${right.getText(sf)})`,
151
+ generate: (_sourceFile, text) => `${NAMESPACE}.write(${binding}, ${binding}.value ${op} ${text(right)})`,
152
152
  node: parent
153
153
  });
154
154
  }
@@ -156,20 +156,20 @@ function reference(ctx, node, kind) {
156
156
  let unary = parent, delta = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1';
157
157
  if (ts.isExpressionStatement(unary.parent)) {
158
158
  ctx.replacements.push({
159
- generate: () => `${NAMESPACE}.write(${text}, ${text}.value ${delta})`,
159
+ generate: () => `${NAMESPACE}.write(${binding}, ${binding}.value ${delta})`,
160
160
  node: unary
161
161
  });
162
162
  }
163
163
  else if (ts.isPrefixUnaryExpression(unary)) {
164
164
  ctx.replacements.push({
165
- generate: () => `(${NAMESPACE}.write(${text}, ${text}.value ${delta}), ${text}.value)`,
165
+ generate: () => `(${NAMESPACE}.write(${binding}, ${binding}.value ${delta}), ${binding}.value)`,
166
166
  node: unary
167
167
  });
168
168
  }
169
169
  else {
170
170
  let tmp = `_t${ctx.tmpCounter++}`;
171
171
  ctx.replacements.push({
172
- generate: () => `((${tmp}) => (${NAMESPACE}.write(${text}, ${tmp} ${delta}), ${tmp}))(${text}.value)`,
172
+ generate: () => `((${tmp}) => (${NAMESPACE}.write(${binding}, ${tmp} ${delta}), ${tmp}))(${binding}.value)`,
173
173
  node: unary
174
174
  });
175
175
  }
@@ -1,6 +1,6 @@
1
- import type { Reactive } from '../types.js';
2
1
  import { ReactiveArray } from './array.js';
3
2
  import { ReactiveObject } from './object.js';
3
+ import type { Reactive } from '../types.js';
4
4
  type Guard<T> = T extends Record<PropertyKey, unknown> ? T extends {
5
5
  dispose: unknown;
6
6
  } ? {