@esportsplus/reactivity 0.22.3 → 0.23.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 (56) hide show
  1. package/build/index.d.ts +1 -1
  2. package/build/index.js +1 -1
  3. package/build/reactive/array.d.ts +3 -0
  4. package/build/reactive/array.js +32 -2
  5. package/build/reactive/index.d.ts +17 -14
  6. package/build/reactive/index.js +7 -25
  7. package/build/system.js +1 -1
  8. package/build/transformer/core/detector.d.ts +2 -0
  9. package/build/transformer/core/detector.js +6 -0
  10. package/build/transformer/core/index.d.ts +10 -0
  11. package/build/transformer/core/index.js +55 -0
  12. package/build/transformer/core/transforms/auto-dispose.d.ts +3 -0
  13. package/build/transformer/core/transforms/auto-dispose.js +116 -0
  14. package/build/transformer/core/transforms/reactive-array.d.ts +4 -0
  15. package/build/transformer/core/transforms/reactive-array.js +89 -0
  16. package/build/transformer/core/transforms/reactive-object.d.ts +4 -0
  17. package/build/transformer/core/transforms/reactive-object.js +155 -0
  18. package/build/transformer/core/transforms/reactive-primitives.d.ts +4 -0
  19. package/build/transformer/core/transforms/reactive-primitives.js +325 -0
  20. package/build/transformer/core/transforms/utilities.d.ts +9 -0
  21. package/build/transformer/core/transforms/utilities.js +57 -0
  22. package/build/transformer/plugins/esbuild.d.ts +5 -0
  23. package/build/transformer/plugins/esbuild.js +30 -0
  24. package/build/transformer/plugins/tsc.d.ts +3 -0
  25. package/build/transformer/plugins/tsc.js +4 -0
  26. package/build/transformer/plugins/vite.d.ts +5 -0
  27. package/build/transformer/plugins/vite.js +28 -0
  28. package/build/types.d.ts +14 -4
  29. package/package.json +34 -3
  30. package/readme.md +276 -2
  31. package/src/constants.ts +1 -1
  32. package/src/index.ts +1 -1
  33. package/src/reactive/array.ts +49 -2
  34. package/src/reactive/index.ts +33 -57
  35. package/src/system.ts +14 -5
  36. package/src/transformer/core/detector.ts +12 -0
  37. package/src/transformer/core/index.ts +82 -0
  38. package/src/transformer/core/transforms/auto-dispose.ts +194 -0
  39. package/src/transformer/core/transforms/reactive-array.ts +140 -0
  40. package/src/transformer/core/transforms/reactive-object.ts +244 -0
  41. package/src/transformer/core/transforms/reactive-primitives.ts +459 -0
  42. package/src/transformer/core/transforms/utilities.ts +95 -0
  43. package/src/transformer/plugins/esbuild.ts +46 -0
  44. package/src/transformer/plugins/tsc.ts +8 -0
  45. package/src/transformer/plugins/vite.ts +41 -0
  46. package/src/types.ts +24 -5
  47. package/test/arrays.ts +146 -0
  48. package/test/effects.ts +168 -0
  49. package/test/index.ts +8 -0
  50. package/test/nested.ts +201 -0
  51. package/test/objects.ts +106 -0
  52. package/test/primitives.ts +171 -0
  53. package/test/vite.config.ts +40 -0
  54. package/build/reactive/object.d.ts +0 -7
  55. package/build/reactive/object.js +0 -79
  56. package/src/reactive/object.ts +0 -116
package/build/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { default as reactive } from './reactive/index.js';
2
1
  export { STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED } from './constants.js';
2
+ export { default as reactive } from './reactive/index.js';
3
3
  export * from './system.js';
4
4
  export * from './types.js';
package/build/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { default as reactive } from './reactive/index.js';
2
1
  export { STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED } from './constants.js';
2
+ export { default as reactive } from './reactive/index.js';
3
3
  export * from './system.js';
4
4
  export * from './types.js';
@@ -35,8 +35,11 @@ type Listener<V> = {
35
35
  };
36
36
  type Listeners = Record<string, (Listener<any> | null)[]>;
37
37
  declare class ReactiveArray<T> extends Array<T> {
38
+ private _length;
38
39
  listeners: Listeners;
39
40
  constructor(...items: T[]);
41
+ $length(): number;
42
+ $set(i: number, value: T): void;
40
43
  clear(): void;
41
44
  concat(...items: ConcatArray<T>[]): ReactiveArray<T>;
42
45
  concat(...items: (T | ConcatArray<T>)[]): ReactiveArray<T>;
@@ -1,13 +1,33 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
- import { REACTIVE_ARRAY } from '../constants.js';
3
- import { isReactiveObject } from './object.js';
2
+ import { read, set, signal } from '../system.js';
3
+ import { REACTIVE_ARRAY, REACTIVE_OBJECT } from '../constants.js';
4
+ function isReactiveObject(value) {
5
+ return value !== null && typeof value === 'object' && value[REACTIVE_OBJECT] === true;
6
+ }
4
7
  class ReactiveArray extends Array {
8
+ _length;
5
9
  listeners = {};
6
10
  constructor(...items) {
7
11
  super(...items);
12
+ this._length = signal(items.length);
13
+ }
14
+ $length() {
15
+ return read(this._length);
16
+ }
17
+ $set(i, value) {
18
+ let prev = this[i];
19
+ if (prev === value) {
20
+ return;
21
+ }
22
+ this[i] = value;
23
+ if (i >= super.length) {
24
+ set(this._length, i + 1);
25
+ }
26
+ this.dispatch('set', { index: i, item: value });
8
27
  }
9
28
  clear() {
10
29
  this.dispose();
30
+ set(this._length, 0);
11
31
  this.dispatch('clear');
12
32
  }
13
33
  concat(...items) {
@@ -26,6 +46,7 @@ class ReactiveArray extends Array {
26
46
  }
27
47
  }
28
48
  if (added.length) {
49
+ set(this._length, super.length);
29
50
  this.dispatch('concat', { items: added });
30
51
  }
31
52
  return this;
@@ -62,6 +83,7 @@ class ReactiveArray extends Array {
62
83
  item.dispose();
63
84
  }
64
85
  }
86
+ set(this._length, 0);
65
87
  }
66
88
  on(event, listener) {
67
89
  let listeners = this.listeners[event];
@@ -92,6 +114,7 @@ class ReactiveArray extends Array {
92
114
  pop() {
93
115
  let item = super.pop();
94
116
  if (item !== undefined) {
117
+ set(this._length, super.length);
95
118
  if (isReactiveObject(item)) {
96
119
  item.dispose();
97
120
  }
@@ -100,7 +123,11 @@ class ReactiveArray extends Array {
100
123
  return item;
101
124
  }
102
125
  push(...items) {
126
+ if (!items.length) {
127
+ return super.length;
128
+ }
103
129
  let length = super.push(...items);
130
+ set(this._length, length);
104
131
  this.dispatch('push', { items });
105
132
  return length;
106
133
  }
@@ -112,6 +139,7 @@ class ReactiveArray extends Array {
112
139
  shift() {
113
140
  let item = super.shift();
114
141
  if (item !== undefined) {
142
+ set(this._length, super.length);
115
143
  if (isReactiveObject(item)) {
116
144
  item.dispose();
117
145
  }
@@ -151,6 +179,7 @@ class ReactiveArray extends Array {
151
179
  splice(start, deleteCount = this.length, ...items) {
152
180
  let removed = super.splice(start, deleteCount, ...items);
153
181
  if (items.length > 0 || removed.length > 0) {
182
+ set(this._length, super.length);
154
183
  for (let i = 0, n = removed.length; i < n; i++) {
155
184
  let item = removed[i];
156
185
  if (isReactiveObject(item)) {
@@ -163,6 +192,7 @@ class ReactiveArray extends Array {
163
192
  }
164
193
  unshift(...items) {
165
194
  let length = super.unshift(...items);
195
+ set(this._length, length);
166
196
  this.dispatch('unshift', { items });
167
197
  return length;
168
198
  }
@@ -1,18 +1,21 @@
1
- import { Prettify } from '@esportsplus/utilities';
1
+ import { REACTIVE_OBJECT } from '../constants.js';
2
2
  import { ReactiveArray } from './array.js';
3
- import { ReactiveObject } from './object.js';
4
- type API<T> = T extends Record<PropertyKey, unknown> ? Prettify<{
5
- [K in keyof T]: Infer<T[K]>;
6
- } & {
7
- dispose: VoidFunction;
8
- }> : T extends (infer U)[] ? ReactiveArray<U> : never;
9
- type Guard<T> = T extends {
3
+ declare const READONLY: unique symbol;
4
+ type ReactiveObject<T extends Record<PropertyKey, unknown>> = T & {
5
+ [REACTIVE_OBJECT]: true;
6
+ dispose(): void;
7
+ };
8
+ type ReactiveObjectGuard<T> = T extends {
10
9
  dispose: any;
11
10
  } ? {
12
- never: '[ dispose ] are reserved keys';
11
+ never: '[ dispose ] is a reserved key';
13
12
  } : T;
14
- type Infer<T> = T extends (...args: unknown[]) => Promise<infer R> ? R | undefined : T extends (...args: any[]) => infer R ? R : T extends (infer U)[] ? ReactiveArray<U> : T extends ReactiveObject<any> ? T : T extends Record<PropertyKey, unknown> ? {
15
- [K in keyof T]: T[K];
16
- } : T;
17
- declare const _default: <T extends Record<PropertyKey, any> | unknown[]>(input: Guard<T>) => API<T>;
18
- export default _default;
13
+ declare function reactive<T extends () => unknown>(_input: T): ReturnType<T> & {
14
+ readonly [READONLY]: true;
15
+ };
16
+ declare function reactive<T extends Record<PropertyKey, any>>(_input: ReactiveObjectGuard<T>): ReactiveObject<T>;
17
+ declare function reactive<T>(_input: T[]): ReactiveArray<T>;
18
+ declare function reactive<T>(_input: T): T;
19
+ export default reactive;
20
+ export { reactive, ReactiveArray };
21
+ export type { ReactiveObject };
@@ -1,26 +1,8 @@
1
- import { isArray, isObject } from '@esportsplus/utilities';
2
- import { onCleanup, root } from '../system.js';
1
+ import { REACTIVE_OBJECT } from '../constants.js';
3
2
  import { ReactiveArray } from './array.js';
4
- import { ReactiveObject } from './object.js';
5
- export default (input) => {
6
- let dispose = false, value = root(() => {
7
- let response;
8
- if (isObject(input)) {
9
- response = new ReactiveObject(input);
10
- }
11
- else if (isArray(input)) {
12
- response = new ReactiveArray(...input);
13
- }
14
- if (response) {
15
- if (root.disposables) {
16
- dispose = true;
17
- }
18
- return response;
19
- }
20
- throw new Error(`@esportsplus/reactivity: 'reactive' received invalid input - ${JSON.stringify(input)}`);
21
- });
22
- if (dispose) {
23
- onCleanup(() => value.dispose());
24
- }
25
- return value;
26
- };
3
+ function reactive(_input) {
4
+ throw new Error('@esportsplus/reactivity: reactive() called at runtime. ' +
5
+ 'Ensure vite-plugin-reactivity-compile is configured.');
6
+ }
7
+ export default reactive;
8
+ export { reactive, ReactiveArray };
package/build/system.js CHANGED
@@ -1,5 +1,5 @@
1
- import { isObject } from '@esportsplus/utilities';
2
1
  import { COMPUTED, SIGNAL, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_DIRTY, STATE_IN_HEAP, STATE_NONE, STATE_NOTIFY_MASK, STATE_RECOMPUTING } from './constants.js';
2
+ import { isObject } from '@esportsplus/utilities';
3
3
  let depth = 0, heap = new Array(64), heap_i = 0, heap_n = 0, linkPool = [], linkPoolMax = 1000, microtask = queueMicrotask, notified = false, observer = null, scope = null, stabilizer = STABILIZER_IDLE, version = 0;
4
4
  function cleanup(computed) {
5
5
  if (!computed.cleanup) {
@@ -0,0 +1,2 @@
1
+ declare const mightNeedTransform: (code: string) => boolean;
2
+ export { mightNeedTransform };
@@ -0,0 +1,6 @@
1
+ import { mightNeedTransform as checkTransform } from '@esportsplus/typescript/transformer';
2
+ let regex = /import\s*\{[^}]*\breactive\b[^}]*\}\s*from\s*['"]@esportsplus\/reactivity/;
3
+ const mightNeedTransform = (code) => {
4
+ return checkTransform(code, { regex });
5
+ };
6
+ export { mightNeedTransform };
@@ -0,0 +1,10 @@
1
+ import type { TransformOptions, TransformResult } from '../../types.js';
2
+ import { mightNeedTransform } from './detector.js';
3
+ import ts from 'typescript';
4
+ declare const createTransformer: (options?: TransformOptions) => ts.TransformerFactory<ts.SourceFile>;
5
+ declare const transform: (sourceFile: ts.SourceFile, options?: TransformOptions) => TransformResult;
6
+ export { createTransformer, mightNeedTransform, transform };
7
+ export { injectAutoDispose } from './transforms/auto-dispose.js';
8
+ export { transformReactiveArrays } from './transforms/reactive-array.js';
9
+ export { transformReactiveObjects } from './transforms/reactive-object.js';
10
+ export { transformReactivePrimitives } from './transforms/reactive-primitives.js';
@@ -0,0 +1,55 @@
1
+ import { injectAutoDispose } from './transforms/auto-dispose.js';
2
+ import { mightNeedTransform } from './detector.js';
3
+ import { transformReactiveArrays } from './transforms/reactive-array.js';
4
+ import { transformReactiveObjects } from './transforms/reactive-object.js';
5
+ import { transformReactivePrimitives } from './transforms/reactive-primitives.js';
6
+ import ts from 'typescript';
7
+ const createTransformer = (options) => {
8
+ return () => {
9
+ return (sourceFile) => {
10
+ let result = transform(sourceFile, options);
11
+ return result.transformed ? result.sourceFile : sourceFile;
12
+ };
13
+ };
14
+ };
15
+ const transform = (sourceFile, options) => {
16
+ let bindings = new Map(), code = sourceFile.getFullText(), current = sourceFile, original = code, result;
17
+ if (!mightNeedTransform(code)) {
18
+ return { code, sourceFile, transformed: false };
19
+ }
20
+ result = transformReactiveObjects(current, bindings);
21
+ if (result !== code) {
22
+ current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
23
+ code = result;
24
+ }
25
+ result = transformReactiveArrays(current, bindings);
26
+ if (result !== code) {
27
+ current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
28
+ code = result;
29
+ }
30
+ result = transformReactivePrimitives(current, bindings);
31
+ if (result !== code) {
32
+ current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
33
+ code = result;
34
+ }
35
+ if (options?.autoDispose) {
36
+ result = injectAutoDispose(current);
37
+ if (result !== code) {
38
+ current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
39
+ code = result;
40
+ }
41
+ }
42
+ if (code === original) {
43
+ return { code, sourceFile, transformed: false };
44
+ }
45
+ return {
46
+ code,
47
+ sourceFile: current,
48
+ transformed: true
49
+ };
50
+ };
51
+ export { createTransformer, mightNeedTransform, transform };
52
+ export { injectAutoDispose } from './transforms/auto-dispose.js';
53
+ export { transformReactiveArrays } from './transforms/reactive-array.js';
54
+ export { transformReactiveObjects } from './transforms/reactive-object.js';
55
+ export { transformReactivePrimitives } from './transforms/reactive-primitives.js';
@@ -0,0 +1,3 @@
1
+ import ts from 'typescript';
2
+ declare const injectAutoDispose: (sourceFile: ts.SourceFile) => string;
3
+ export { injectAutoDispose };
@@ -0,0 +1,116 @@
1
+ import { uid } from '@esportsplus/typescript/transformer';
2
+ import ts from 'typescript';
3
+ import { applyReplacements } from './utilities.js';
4
+ const TRAILING_SEMICOLON = /;$/;
5
+ function visitFunctionBody(body, parentBody) {
6
+ let disposables = [], effectsToCapture = [], returnStatement = null;
7
+ function visit(n) {
8
+ if (ts.isVariableDeclaration(n) &&
9
+ ts.isIdentifier(n.name) &&
10
+ n.initializer &&
11
+ ts.isCallExpression(n.initializer) &&
12
+ ts.isIdentifier(n.initializer.expression) &&
13
+ n.initializer.expression.text === 'reactive') {
14
+ disposables.push({ name: n.name.text, type: 'reactive' });
15
+ }
16
+ if (ts.isCallExpression(n) &&
17
+ ts.isIdentifier(n.expression) &&
18
+ n.expression.text === 'effect') {
19
+ if (ts.isVariableDeclaration(n.parent) && ts.isIdentifier(n.parent.name)) {
20
+ disposables.push({ name: n.parent.name.text, type: 'effect' });
21
+ }
22
+ else if (ts.isExpressionStatement(n.parent)) {
23
+ let name = uid('effect');
24
+ effectsToCapture.push({
25
+ end: n.parent.end,
26
+ name,
27
+ start: n.parent.pos
28
+ });
29
+ disposables.push({ name, type: 'effect' });
30
+ }
31
+ }
32
+ if (ts.isReturnStatement(n) &&
33
+ n.expression &&
34
+ (ts.isArrowFunction(n.expression) || ts.isFunctionExpression(n.expression)) &&
35
+ n.parent === parentBody) {
36
+ returnStatement = n;
37
+ }
38
+ ts.forEachChild(n, visit);
39
+ }
40
+ visit(body);
41
+ return { disposables, effectsToCapture, returnStatement };
42
+ }
43
+ function processFunction(node, sourceFile, edits) {
44
+ if (!node.body || !ts.isBlock(node.body)) {
45
+ return;
46
+ }
47
+ let result = visitFunctionBody(node.body, node.body), disposables = result.disposables, effectsToCapture = result.effectsToCapture, returnStatement = result.returnStatement;
48
+ if (disposables.length === 0 || !returnStatement || !returnStatement.expression) {
49
+ return;
50
+ }
51
+ let cleanupFn = returnStatement.expression;
52
+ if (!cleanupFn.body) {
53
+ return;
54
+ }
55
+ let disposeStatements = [];
56
+ for (let i = disposables.length - 1; i >= 0; i--) {
57
+ let d = disposables[i];
58
+ if (d.type === 'reactive') {
59
+ disposeStatements.push(`${d.name}.dispose();`);
60
+ }
61
+ else {
62
+ disposeStatements.push(`${d.name}();`);
63
+ }
64
+ }
65
+ let disposeCode = disposeStatements.join('\n');
66
+ if (ts.isBlock(cleanupFn.body)) {
67
+ edits.push({
68
+ cleanupBodyEnd: cleanupFn.body.statements[0]?.pos ?? cleanupFn.body.end - 1,
69
+ cleanupBodyStart: cleanupFn.body.pos + 1,
70
+ disposeCode,
71
+ effectsToCapture
72
+ });
73
+ }
74
+ else {
75
+ edits.push({
76
+ cleanupBodyEnd: cleanupFn.body.end,
77
+ cleanupBodyStart: cleanupFn.body.pos,
78
+ disposeCode: `{ ${disposeCode}\n return ${cleanupFn.body.getText(sourceFile)}; }`,
79
+ effectsToCapture
80
+ });
81
+ }
82
+ }
83
+ const injectAutoDispose = (sourceFile) => {
84
+ let code = sourceFile.getFullText(), edits = [];
85
+ function visit(node) {
86
+ if (ts.isFunctionDeclaration(node) ||
87
+ ts.isFunctionExpression(node) ||
88
+ ts.isArrowFunction(node)) {
89
+ processFunction(node, sourceFile, edits);
90
+ }
91
+ ts.forEachChild(node, visit);
92
+ }
93
+ visit(sourceFile);
94
+ if (edits.length === 0) {
95
+ return code;
96
+ }
97
+ let replacements = [];
98
+ for (let i = 0, n = edits.length; i < n; i++) {
99
+ let edit = edits[i], effects = edit.effectsToCapture;
100
+ for (let j = 0, m = effects.length; j < m; j++) {
101
+ let effect = effects[j], original = code.substring(effect.start, effect.end).trim();
102
+ replacements.push({
103
+ end: effect.end,
104
+ newText: `const ${effect.name} = ${original.replace(TRAILING_SEMICOLON, '')}`,
105
+ start: effect.start
106
+ });
107
+ }
108
+ replacements.push({
109
+ end: edit.cleanupBodyEnd,
110
+ newText: `\n${edit.disposeCode}`,
111
+ start: edit.cleanupBodyStart
112
+ });
113
+ }
114
+ return applyReplacements(code, replacements);
115
+ };
116
+ export { injectAutoDispose };
@@ -0,0 +1,4 @@
1
+ import type { Bindings } from '../../../types.js';
2
+ import ts from 'typescript';
3
+ declare const transformReactiveArrays: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
+ export { transformReactiveArrays };
@@ -0,0 +1,89 @@
1
+ import ts from 'typescript';
2
+ import { applyReplacements } from './utilities.js';
3
+ function getPropertyPath(node) {
4
+ let current = node, parts = [];
5
+ while (ts.isPropertyAccessExpression(current)) {
6
+ parts.unshift(current.name.text);
7
+ current = current.expression;
8
+ }
9
+ if (ts.isIdentifier(current)) {
10
+ parts.unshift(current.text);
11
+ return parts.join('.');
12
+ }
13
+ return null;
14
+ }
15
+ function getExpressionName(node) {
16
+ if (ts.isIdentifier(node)) {
17
+ return node.text;
18
+ }
19
+ if (ts.isPropertyAccessExpression(node)) {
20
+ return getPropertyPath(node);
21
+ }
22
+ return null;
23
+ }
24
+ function isAssignmentTarget(node) {
25
+ let parent = node.parent;
26
+ if ((ts.isBinaryExpression(parent) && parent.left === node) ||
27
+ ts.isPostfixUnaryExpression(parent) ||
28
+ ts.isPrefixUnaryExpression(parent)) {
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ const transformReactiveArrays = (sourceFile, bindings) => {
34
+ let code = sourceFile.getFullText(), replacements = [];
35
+ function visit(node) {
36
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
37
+ if (ts.isIdentifier(node.initializer) && bindings.get(node.initializer.text) === 'array') {
38
+ bindings.set(node.name.text, 'array');
39
+ }
40
+ if (ts.isPropertyAccessExpression(node.initializer)) {
41
+ let path = getPropertyPath(node.initializer);
42
+ if (path && bindings.get(path) === 'array') {
43
+ bindings.set(node.name.text, 'array');
44
+ }
45
+ }
46
+ }
47
+ if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node)) && node.parameters) {
48
+ for (let i = 0, n = node.parameters.length; i < n; i++) {
49
+ let param = node.parameters[i];
50
+ if ((ts.isIdentifier(param.name) && param.type) &&
51
+ ts.isTypeReferenceNode(param.type) &&
52
+ ts.isIdentifier(param.type.typeName) &&
53
+ param.type.typeName.text === 'ReactiveArray') {
54
+ bindings.set(param.name.text, 'array');
55
+ }
56
+ }
57
+ }
58
+ if (ts.isPropertyAccessExpression(node) &&
59
+ node.name.text === 'length' &&
60
+ !isAssignmentTarget(node)) {
61
+ let objName = getExpressionName(node.expression);
62
+ if (objName && bindings.get(objName) === 'array') {
63
+ let objText = node.expression.getText(sourceFile);
64
+ replacements.push({
65
+ end: node.end,
66
+ newText: `${objText}.$length()`,
67
+ start: node.pos
68
+ });
69
+ }
70
+ }
71
+ if (ts.isBinaryExpression(node) &&
72
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
73
+ ts.isElementAccessExpression(node.left)) {
74
+ let elemAccess = node.left, objName = getExpressionName(elemAccess.expression);
75
+ if (objName && bindings.get(objName) === 'array') {
76
+ let indexText = elemAccess.argumentExpression.getText(sourceFile), objText = elemAccess.expression.getText(sourceFile), valueText = node.right.getText(sourceFile);
77
+ replacements.push({
78
+ end: node.end,
79
+ newText: `${objText}.$set(${indexText}, ${valueText})`,
80
+ start: node.pos
81
+ });
82
+ }
83
+ }
84
+ ts.forEachChild(node, visit);
85
+ }
86
+ visit(sourceFile);
87
+ return applyReplacements(code, replacements);
88
+ };
89
+ export { transformReactiveArrays };
@@ -0,0 +1,4 @@
1
+ import type { Bindings } from '../../../types.js';
2
+ import ts from 'typescript';
3
+ declare const transformReactiveObjects: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
+ export { transformReactiveObjects };
@@ -0,0 +1,155 @@
1
+ import { uid } from '@esportsplus/typescript/transformer';
2
+ import ts from 'typescript';
3
+ import { addMissingImports, applyReplacements } from './utilities.js';
4
+ const CLASS_NAME_REGEX = /class (\w+)/;
5
+ const EXTRA_IMPORTS = [
6
+ { module: '@esportsplus/reactivity/constants', specifier: 'REACTIVE_OBJECT' },
7
+ { module: '@esportsplus/reactivity/reactive/array', specifier: 'ReactiveArray' }
8
+ ];
9
+ function analyzeProperty(prop, sourceFile) {
10
+ if (!ts.isPropertyAssignment(prop)) {
11
+ return null;
12
+ }
13
+ let key;
14
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
15
+ key = prop.name.text;
16
+ }
17
+ else {
18
+ return null;
19
+ }
20
+ let value = prop.initializer, valueText = value.getText(sourceFile);
21
+ if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
22
+ return { key, type: 'computed', valueText };
23
+ }
24
+ if (ts.isArrayLiteralExpression(value)) {
25
+ return { key, type: 'array', valueText };
26
+ }
27
+ return { key, type: 'signal', valueText };
28
+ }
29
+ function buildClassCode(className, properties) {
30
+ let accessors = [], disposeStatements = [], fields = [];
31
+ fields.push(`[REACTIVE_OBJECT] = true;`);
32
+ for (let i = 0, n = properties.length; i < n; i++) {
33
+ let { key, type, valueText } = properties[i];
34
+ if (type === 'signal') {
35
+ let param = uid('v');
36
+ fields.push(`#${key} = signal(${valueText});`);
37
+ accessors.push(`get ${key}() { return read(this.#${key}); }`);
38
+ accessors.push(`set ${key}(${param}) { set(this.#${key}, ${param}); }`);
39
+ }
40
+ else if (type === 'array') {
41
+ let elements = valueText.slice(1, -1);
42
+ fields.push(`${key} = new ReactiveArray(${elements});`);
43
+ disposeStatements.push(`this.${key}.dispose();`);
44
+ }
45
+ else if (type === 'computed') {
46
+ fields.push(`#${key}: Computed<unknown> | null = null;`);
47
+ accessors.push(`get ${key}() { return read(this.#${key} ??= computed(${valueText})); }`);
48
+ disposeStatements.push(`if (this.#${key}) dispose(this.#${key});`);
49
+ }
50
+ }
51
+ let disposeBody = disposeStatements.length > 0 ? disposeStatements.join('\n') : '';
52
+ return `
53
+ class ${className} {
54
+ ${fields.join('\n')}
55
+ ${accessors.join('\n')}
56
+
57
+ dispose() {
58
+ ${disposeBody}
59
+ }
60
+ }
61
+ `;
62
+ }
63
+ const transformReactiveObjects = (sourceFile, bindings) => {
64
+ let allNeededImports = new Set(), calls = [], code = sourceFile.getFullText(), hasReactiveImport = false, lastImportEnd = 0;
65
+ function visit(node) {
66
+ if (ts.isImportDeclaration(node)) {
67
+ lastImportEnd = node.end;
68
+ if (ts.isStringLiteral(node.moduleSpecifier) &&
69
+ node.moduleSpecifier.text.includes('@esportsplus/reactivity')) {
70
+ let clause = node.importClause;
71
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
72
+ let elements = clause.namedBindings.elements;
73
+ for (let i = 0, n = elements.length; i < n; i++) {
74
+ if (elements[i].name.text === 'reactive') {
75
+ hasReactiveImport = true;
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ if (hasReactiveImport &&
83
+ ts.isCallExpression(node) &&
84
+ ts.isIdentifier(node.expression) &&
85
+ node.expression.text === 'reactive') {
86
+ let arg = node.arguments[0];
87
+ if (arg && ts.isObjectLiteralExpression(arg)) {
88
+ let varName = null;
89
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
90
+ varName = node.parent.name.text;
91
+ bindings.set(varName, 'object');
92
+ }
93
+ let needsImports = new Set(), properties = [];
94
+ needsImports.add('REACTIVE_OBJECT');
95
+ let props = arg.properties;
96
+ for (let i = 0, n = props.length; i < n; i++) {
97
+ let prop = props[i];
98
+ if (ts.isSpreadAssignment(prop)) {
99
+ return;
100
+ }
101
+ let analyzed = analyzeProperty(prop, sourceFile);
102
+ if (!analyzed) {
103
+ return;
104
+ }
105
+ properties.push(analyzed);
106
+ if (analyzed.type === 'signal') {
107
+ needsImports.add('read');
108
+ needsImports.add('set');
109
+ needsImports.add('signal');
110
+ }
111
+ else if (analyzed.type === 'array') {
112
+ needsImports.add('ReactiveArray');
113
+ if (varName) {
114
+ bindings.set(`${varName}.${analyzed.key}`, 'array');
115
+ }
116
+ }
117
+ else if (analyzed.type === 'computed') {
118
+ needsImports.add('computed');
119
+ needsImports.add('dispose');
120
+ needsImports.add('read');
121
+ }
122
+ }
123
+ needsImports.forEach(imp => allNeededImports.add(imp));
124
+ calls.push({
125
+ end: node.end,
126
+ generatedClass: buildClassCode(uid('ReactiveObject'), properties),
127
+ needsImports,
128
+ start: node.pos,
129
+ varName
130
+ });
131
+ }
132
+ }
133
+ ts.forEachChild(node, visit);
134
+ }
135
+ visit(sourceFile);
136
+ if (calls.length === 0) {
137
+ return code;
138
+ }
139
+ let replacements = [];
140
+ replacements.push({
141
+ end: lastImportEnd,
142
+ newText: code.substring(0, lastImportEnd) + '\n' + calls.map(c => c.generatedClass).join('\n') + '\n',
143
+ start: 0
144
+ });
145
+ for (let i = 0, n = calls.length; i < n; i++) {
146
+ let call = calls[i], classMatch = call.generatedClass.match(CLASS_NAME_REGEX);
147
+ replacements.push({
148
+ end: call.end,
149
+ newText: ` new ${classMatch ? classMatch[1] : 'ReactiveObject'}()`,
150
+ start: call.start
151
+ });
152
+ }
153
+ return addMissingImports(applyReplacements(code, replacements), allNeededImports, EXTRA_IMPORTS);
154
+ };
155
+ export { transformReactiveObjects };
@@ -0,0 +1,4 @@
1
+ import type { Bindings } from '../../../types.js';
2
+ import ts from 'typescript';
3
+ declare const transformReactivePrimitives: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
+ export { transformReactivePrimitives };