@esportsplus/reactivity 0.22.3 → 0.23.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 (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/detector.d.ts +2 -0
  9. package/build/transformer/detector.js +38 -0
  10. package/build/transformer/index.d.ts +10 -0
  11. package/build/transformer/index.js +55 -0
  12. package/build/transformer/plugins/esbuild.d.ts +5 -0
  13. package/build/transformer/plugins/esbuild.js +31 -0
  14. package/build/transformer/plugins/tsc.d.ts +3 -0
  15. package/build/transformer/plugins/tsc.js +4 -0
  16. package/build/transformer/plugins/vite.d.ts +5 -0
  17. package/build/transformer/plugins/vite.js +28 -0
  18. package/build/transformer/transforms/auto-dispose.d.ts +3 -0
  19. package/build/transformer/transforms/auto-dispose.js +119 -0
  20. package/build/transformer/transforms/reactive-array.d.ts +4 -0
  21. package/build/transformer/transforms/reactive-array.js +93 -0
  22. package/build/transformer/transforms/reactive-object.d.ts +4 -0
  23. package/build/transformer/transforms/reactive-object.js +164 -0
  24. package/build/transformer/transforms/reactive-primitives.d.ts +4 -0
  25. package/build/transformer/transforms/reactive-primitives.js +335 -0
  26. package/build/transformer/transforms/utilities.d.ts +8 -0
  27. package/build/transformer/transforms/utilities.js +73 -0
  28. package/build/types.d.ts +14 -4
  29. package/package.json +30 -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/detector.ts +65 -0
  37. package/src/transformer/index.ts +78 -0
  38. package/src/transformer/plugins/esbuild.ts +47 -0
  39. package/src/transformer/plugins/tsc.ts +8 -0
  40. package/src/transformer/plugins/vite.ts +39 -0
  41. package/src/transformer/transforms/auto-dispose.ts +191 -0
  42. package/src/transformer/transforms/reactive-array.ts +143 -0
  43. package/src/transformer/transforms/reactive-object.ts +253 -0
  44. package/src/transformer/transforms/reactive-primitives.ts +461 -0
  45. package/src/transformer/transforms/utilities.ts +119 -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
@@ -0,0 +1,47 @@
1
+ import { TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
2
+ import { mightNeedTransform, transform } from '~/transformer';
3
+ import type { OnLoadArgs, Plugin, PluginBuild } from 'esbuild';
4
+ import type { TransformOptions } from '~/types';
5
+ import fs from 'fs';
6
+ import ts from 'typescript';
7
+
8
+
9
+ export default (options?: TransformOptions): Plugin => {
10
+ return {
11
+ name: '@esportsplus/reactivity/plugin-esbuild',
12
+
13
+ setup(build: PluginBuild) {
14
+ build.onLoad({ filter: TRANSFORM_PATTERN }, async (args: OnLoadArgs) => {
15
+ let code = await fs.promises.readFile(args.path, 'utf8');
16
+
17
+ if (!mightNeedTransform(code)) {
18
+ return null;
19
+ }
20
+
21
+ try {
22
+ let sourceFile = ts.createSourceFile(
23
+ args.path,
24
+ code,
25
+ ts.ScriptTarget.Latest,
26
+ true
27
+ ),
28
+ result = transform(sourceFile, options);
29
+
30
+ if (!result.transformed) {
31
+ return null;
32
+ }
33
+
34
+ return {
35
+ contents: result.code,
36
+ loader: args.path.endsWith('x') ? 'tsx' : 'ts'
37
+ };
38
+ }
39
+ catch (error) {
40
+ console.error(`@esportsplus/reactivity: Error transforming ${args.path}:`, error);
41
+ return null;
42
+ }
43
+ });
44
+ }
45
+ };
46
+ };
47
+ export type { TransformOptions as PluginOptions };
@@ -0,0 +1,8 @@
1
+ import { createTransformer } from '~/transformer';
2
+ import ts from 'typescript';
3
+
4
+
5
+ // TypeScript custom transformers API requires program parameter, but we don't use it
6
+ export default (_program: ts.Program): ts.TransformerFactory<ts.SourceFile> => {
7
+ return createTransformer();
8
+ };
@@ -0,0 +1,39 @@
1
+ import { TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
2
+ import { mightNeedTransform, transform } from '~/transformer';
3
+ import type { Plugin } from 'vite';
4
+ import type { TransformOptions } from '~/types';
5
+ import ts from 'typescript';
6
+
7
+
8
+ export default (options?: TransformOptions): Plugin => {
9
+ return {
10
+ enforce: 'pre',
11
+ name: '@esportsplus/reactivity/plugin-vite',
12
+
13
+ transform(code: string, id: string) {
14
+ if (!TRANSFORM_PATTERN.test(id) || id.includes('node_modules')) {
15
+ return null;
16
+ }
17
+
18
+ if (!mightNeedTransform(code)) {
19
+ return null;
20
+ }
21
+
22
+ try {
23
+ let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true),
24
+ result = transform(sourceFile, options);
25
+
26
+ if (!result.transformed) {
27
+ return null;
28
+ }
29
+
30
+ return { code: result.code, map: null };
31
+ }
32
+ catch (error) {
33
+ console.error(`@esportsplus/reactivity: Error transforming ${id}:`, error);
34
+ return null;
35
+ }
36
+ }
37
+ };
38
+ };
39
+ export type { TransformOptions as PluginOptions };
@@ -0,0 +1,191 @@
1
+ import { uid, TRAILING_SEMICOLON } from '@esportsplus/typescript/transformer';
2
+ import { applyReplacements, Replacement } from './utilities';
3
+ import ts from 'typescript';
4
+
5
+
6
+ interface BodyContext {
7
+ disposables: Disposable[];
8
+ effectsToCapture: { end: number; name: string; start: number }[];
9
+ parentBody: ts.Node;
10
+ returnStatement: ts.ReturnStatement | null;
11
+ }
12
+
13
+ interface Disposable {
14
+ name: string;
15
+ type: 'effect' | 'reactive';
16
+ }
17
+
18
+ interface FunctionEdit {
19
+ cleanupBodyEnd: number;
20
+ cleanupBodyStart: number;
21
+ disposeCode: string;
22
+ effectsToCapture: { end: number; name: string; start: number }[];
23
+ }
24
+
25
+ interface MainContext {
26
+ edits: FunctionEdit[];
27
+ sourceFile: ts.SourceFile;
28
+ }
29
+
30
+
31
+ function processFunction(
32
+ node: ts.ArrowFunction | ts.FunctionDeclaration | ts.FunctionExpression,
33
+ sourceFile: ts.SourceFile,
34
+ edits: FunctionEdit[]
35
+ ): void {
36
+ if (!node.body || !ts.isBlock(node.body)) {
37
+ return;
38
+ }
39
+
40
+ let ctx: BodyContext = {
41
+ disposables: [],
42
+ effectsToCapture: [],
43
+ parentBody: node.body,
44
+ returnStatement: null
45
+ };
46
+
47
+ visitBody(ctx, node.body);
48
+
49
+ if (ctx.disposables.length === 0 || !ctx.returnStatement || !ctx.returnStatement.expression) {
50
+ return;
51
+ }
52
+
53
+ let cleanupFn = ctx.returnStatement.expression as ts.ArrowFunction | ts.FunctionExpression;
54
+
55
+ if (!cleanupFn.body) {
56
+ return;
57
+ }
58
+
59
+ let disposeStatements: string[] = [];
60
+
61
+ for (let i = ctx.disposables.length - 1; i >= 0; i--) {
62
+ let d = ctx.disposables[i];
63
+
64
+ if (d.type === 'reactive') {
65
+ disposeStatements.push(`${d.name}.dispose();`);
66
+ }
67
+ else {
68
+ disposeStatements.push(`${d.name}();`);
69
+ }
70
+ }
71
+
72
+ let disposeCode = disposeStatements.join('\n');
73
+
74
+ if (ts.isBlock(cleanupFn.body)) {
75
+ edits.push({
76
+ cleanupBodyEnd: cleanupFn.body.statements[0]?.pos ?? cleanupFn.body.end - 1,
77
+ cleanupBodyStart: cleanupFn.body.pos + 1,
78
+ disposeCode,
79
+ effectsToCapture: ctx.effectsToCapture
80
+ });
81
+ }
82
+ else {
83
+ edits.push({
84
+ cleanupBodyEnd: cleanupFn.body.end,
85
+ cleanupBodyStart: cleanupFn.body.pos,
86
+ disposeCode: `{ ${disposeCode}\n return ${cleanupFn.body.getText(sourceFile)}; }`,
87
+ effectsToCapture: ctx.effectsToCapture
88
+ });
89
+ }
90
+ }
91
+
92
+ function visitBody(ctx: BodyContext, node: ts.Node): void {
93
+ if (
94
+ ts.isVariableDeclaration(node) &&
95
+ ts.isIdentifier(node.name) &&
96
+ node.initializer &&
97
+ ts.isCallExpression(node.initializer) &&
98
+ ts.isIdentifier(node.initializer.expression) &&
99
+ node.initializer.expression.text === 'reactive'
100
+ ) {
101
+ ctx.disposables.push({ name: node.name.text, type: 'reactive' });
102
+ }
103
+
104
+ if (
105
+ ts.isCallExpression(node) &&
106
+ ts.isIdentifier(node.expression) &&
107
+ node.expression.text === 'effect'
108
+ ) {
109
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
110
+ ctx.disposables.push({ name: node.parent.name.text, type: 'effect' });
111
+ }
112
+ else if (ts.isExpressionStatement(node.parent)) {
113
+ let name = uid('effect');
114
+
115
+ ctx.effectsToCapture.push({
116
+ end: node.parent.end,
117
+ name,
118
+ start: node.parent.pos
119
+ });
120
+
121
+ ctx.disposables.push({ name, type: 'effect' });
122
+ }
123
+ }
124
+
125
+ if (
126
+ ts.isReturnStatement(node) &&
127
+ node.expression &&
128
+ (ts.isArrowFunction(node.expression) || ts.isFunctionExpression(node.expression)) &&
129
+ node.parent === ctx.parentBody
130
+ ) {
131
+ ctx.returnStatement = node;
132
+ }
133
+
134
+ ts.forEachChild(node, n => visitBody(ctx, n));
135
+ }
136
+
137
+ function visitMain(ctx: MainContext, node: ts.Node): void {
138
+ if (
139
+ ts.isFunctionDeclaration(node) ||
140
+ ts.isFunctionExpression(node) ||
141
+ ts.isArrowFunction(node)
142
+ ) {
143
+ processFunction(node, ctx.sourceFile, ctx.edits);
144
+ }
145
+
146
+ ts.forEachChild(node, n => visitMain(ctx, n));
147
+ }
148
+
149
+
150
+ const injectAutoDispose = (sourceFile: ts.SourceFile): string => {
151
+ let code = sourceFile.getFullText(),
152
+ ctx: MainContext = {
153
+ edits: [],
154
+ sourceFile
155
+ };
156
+
157
+ visitMain(ctx, sourceFile);
158
+
159
+ if (ctx.edits.length === 0) {
160
+ return code;
161
+ }
162
+
163
+ let replacements: Replacement[] = [];
164
+
165
+ for (let i = 0, n = ctx.edits.length; i < n; i++) {
166
+ let edit = ctx.edits[i],
167
+ effects = edit.effectsToCapture;
168
+
169
+ for (let j = 0, m = effects.length; j < m; j++) {
170
+ let effect = effects[j],
171
+ original = code.substring(effect.start, effect.end).trim();
172
+
173
+ replacements.push({
174
+ end: effect.end,
175
+ newText: `const ${effect.name} = ${original.replace(TRAILING_SEMICOLON, '')}`,
176
+ start: effect.start
177
+ });
178
+ }
179
+
180
+ replacements.push({
181
+ end: edit.cleanupBodyEnd,
182
+ newText: `\n${edit.disposeCode}`,
183
+ start: edit.cleanupBodyStart
184
+ });
185
+ }
186
+
187
+ return applyReplacements(code, replacements);
188
+ };
189
+
190
+
191
+ export { injectAutoDispose };
@@ -0,0 +1,143 @@
1
+ import type { Bindings } from '~/types';
2
+ import { applyReplacements, Replacement } from './utilities';
3
+ import ts from 'typescript';
4
+
5
+
6
+ interface TransformContext {
7
+ bindings: Bindings;
8
+ replacements: Replacement[];
9
+ sourceFile: ts.SourceFile;
10
+ }
11
+
12
+
13
+ function getExpressionName(node: ts.Expression): string | null {
14
+ if (ts.isIdentifier(node)) {
15
+ return node.text;
16
+ }
17
+
18
+ if (ts.isPropertyAccessExpression(node)) {
19
+ return getPropertyPath(node);
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ function getPropertyPath(node: ts.PropertyAccessExpression): string | null {
26
+ let current: ts.Node = node,
27
+ parts: string[] = [];
28
+
29
+ while (ts.isPropertyAccessExpression(current)) {
30
+ parts.unshift(current.name.text);
31
+ current = current.expression;
32
+ }
33
+
34
+ if (ts.isIdentifier(current)) {
35
+ parts.unshift(current.text);
36
+ return parts.join('.');
37
+ }
38
+
39
+ return null;
40
+ }
41
+
42
+ function isAssignmentTarget(node: ts.Node): boolean {
43
+ let parent = node.parent;
44
+
45
+ if (
46
+ (ts.isBinaryExpression(parent) && parent.left === node) ||
47
+ ts.isPostfixUnaryExpression(parent) ||
48
+ ts.isPrefixUnaryExpression(parent)
49
+ ) {
50
+ return true;
51
+ }
52
+
53
+ return false;
54
+ }
55
+
56
+ function visit(ctx: TransformContext, node: ts.Node): void {
57
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
58
+ if (ts.isIdentifier(node.initializer) && ctx.bindings.get(node.initializer.text) === 'array') {
59
+ ctx.bindings.set(node.name.text, 'array');
60
+ }
61
+
62
+ if (ts.isPropertyAccessExpression(node.initializer)) {
63
+ let path = getPropertyPath(node.initializer);
64
+
65
+ if (path && ctx.bindings.get(path) === 'array') {
66
+ ctx.bindings.set(node.name.text, 'array');
67
+ }
68
+ }
69
+ }
70
+
71
+ if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node)) && node.parameters) {
72
+ for (let i = 0, n = node.parameters.length; i < n; i++) {
73
+ let param = node.parameters[i];
74
+
75
+ if (
76
+ (ts.isIdentifier(param.name) && param.type) &&
77
+ ts.isTypeReferenceNode(param.type) &&
78
+ ts.isIdentifier(param.type.typeName) &&
79
+ param.type.typeName.text === 'ReactiveArray'
80
+ ) {
81
+ ctx.bindings.set(param.name.text, 'array');
82
+ }
83
+ }
84
+ }
85
+
86
+ if (
87
+ ts.isPropertyAccessExpression(node) &&
88
+ node.name.text === 'length' &&
89
+ !isAssignmentTarget(node)
90
+ ) {
91
+ let name = getExpressionName(node.expression);
92
+
93
+ if (name && ctx.bindings.get(name) === 'array') {
94
+ let objText = node.expression.getText(ctx.sourceFile);
95
+
96
+ ctx.replacements.push({
97
+ end: node.end,
98
+ newText: `${objText}.$length()`,
99
+ start: node.pos
100
+ });
101
+ }
102
+ }
103
+
104
+ if (
105
+ ts.isBinaryExpression(node) &&
106
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
107
+ ts.isElementAccessExpression(node.left)
108
+ ) {
109
+ let elemAccess = node.left,
110
+ objName = getExpressionName(elemAccess.expression);
111
+
112
+ if (objName && ctx.bindings.get(objName) === 'array') {
113
+ let indexText = elemAccess.argumentExpression.getText(ctx.sourceFile),
114
+ objText = elemAccess.expression.getText(ctx.sourceFile),
115
+ valueText = node.right.getText(ctx.sourceFile);
116
+
117
+ ctx.replacements.push({
118
+ end: node.end,
119
+ newText: `${objText}.$set(${indexText}, ${valueText})`,
120
+ start: node.pos
121
+ });
122
+ }
123
+ }
124
+
125
+ ts.forEachChild(node, n => visit(ctx, n));
126
+ }
127
+
128
+
129
+ const transformReactiveArrays = (sourceFile: ts.SourceFile, bindings: Bindings): string => {
130
+ let code = sourceFile.getFullText(),
131
+ ctx: TransformContext = {
132
+ bindings,
133
+ replacements: [],
134
+ sourceFile
135
+ };
136
+
137
+ visit(ctx, sourceFile);
138
+
139
+ return applyReplacements(code, ctx.replacements);
140
+ };
141
+
142
+
143
+ export { transformReactiveArrays };
@@ -0,0 +1,253 @@
1
+ import { uid } from '@esportsplus/typescript/transformer';
2
+ import type { Bindings } from '~/types';
3
+ import { addMissingImports, applyReplacements, ExtraImport, Replacement } from './utilities';
4
+ import ts from 'typescript';
5
+
6
+
7
+ const CLASS_NAME_REGEX = /class (\w+)/;
8
+
9
+ const EXTRA_IMPORTS: ExtraImport[] = [
10
+ { module: '@esportsplus/reactivity/constants', specifier: 'REACTIVE_OBJECT' },
11
+ { module: '@esportsplus/reactivity/reactive/array', specifier: 'ReactiveArray' }
12
+ ];
13
+
14
+
15
+ interface AnalyzedProperty {
16
+ key: string;
17
+ type: 'array' | 'computed' | 'signal';
18
+ valueText: string;
19
+ }
20
+
21
+ interface ReactiveObjectCall {
22
+ end: number;
23
+ generatedClass: string;
24
+ needsImports: Set<string>;
25
+ start: number;
26
+ varName: string | null;
27
+ }
28
+
29
+ interface TransformContext {
30
+ allNeededImports: Set<string>;
31
+ bindings: Bindings;
32
+ calls: ReactiveObjectCall[];
33
+ hasReactiveImport: boolean;
34
+ lastImportEnd: number;
35
+ sourceFile: ts.SourceFile;
36
+ }
37
+
38
+
39
+ function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.SourceFile): AnalyzedProperty | null {
40
+ if (!ts.isPropertyAssignment(prop)) {
41
+ return null;
42
+ }
43
+
44
+ let key: string;
45
+
46
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
47
+ key = prop.name.text;
48
+ }
49
+ else {
50
+ return null;
51
+ }
52
+
53
+ let value = prop.initializer,
54
+ valueText = value.getText(sourceFile);
55
+
56
+ if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
57
+ return { key, type: 'computed', valueText };
58
+ }
59
+
60
+ if (ts.isArrayLiteralExpression(value)) {
61
+ return { key, type: 'array', valueText };
62
+ }
63
+
64
+ return { key, type: 'signal', valueText };
65
+ }
66
+
67
+ function buildClassCode(className: string, properties: AnalyzedProperty[]): string {
68
+ let accessors: string[] = [],
69
+ disposeStatements: string[] = [],
70
+ fields: string[] = [];
71
+
72
+ fields.push(`[REACTIVE_OBJECT] = true;`);
73
+
74
+ for (let i = 0, n = properties.length; i < n; i++) {
75
+ let { key, type, valueText } = properties[i];
76
+
77
+ if (type === 'signal') {
78
+ let param = uid('v');
79
+
80
+ fields.push(`#${key} = signal(${valueText});`);
81
+ accessors.push(`get ${key}() { return read(this.#${key}); }`);
82
+ accessors.push(`set ${key}(${param}) { set(this.#${key}, ${param}); }`);
83
+ }
84
+ else if (type === 'array') {
85
+ let elements = valueText.slice(1, -1);
86
+
87
+ fields.push(`${key} = new ReactiveArray(${elements});`);
88
+ disposeStatements.push(`this.${key}.dispose();`);
89
+ }
90
+ else if (type === 'computed') {
91
+ fields.push(`#${key}: Computed<unknown> | null = null;`);
92
+ accessors.push(`get ${key}() { return read(this.#${key} ??= computed(${valueText})); }`);
93
+ disposeStatements.push(`if (this.#${key}) dispose(this.#${key});`);
94
+ }
95
+ }
96
+
97
+ let disposeBody = disposeStatements.length > 0 ? disposeStatements.join('\n') : '';
98
+
99
+ return `
100
+ class ${className} {
101
+ ${fields.join('\n')}
102
+ ${accessors.join('\n')}
103
+
104
+ dispose() {
105
+ ${disposeBody}
106
+ }
107
+ }
108
+ `;
109
+ }
110
+
111
+ function visit(ctx: TransformContext, node: ts.Node): void {
112
+ if (ts.isImportDeclaration(node)) {
113
+ ctx.lastImportEnd = node.end;
114
+
115
+ if (
116
+ ts.isStringLiteral(node.moduleSpecifier) &&
117
+ node.moduleSpecifier.text.includes('@esportsplus/reactivity')
118
+ ) {
119
+ let clause = node.importClause;
120
+
121
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
122
+ let elements = clause.namedBindings.elements;
123
+
124
+ for (let i = 0, n = elements.length; i < n; i++) {
125
+ if (elements[i].name.text === 'reactive') {
126
+ ctx.hasReactiveImport = true;
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ if (
135
+ ctx.hasReactiveImport &&
136
+ ts.isCallExpression(node) &&
137
+ ts.isIdentifier(node.expression) &&
138
+ node.expression.text === 'reactive'
139
+ ) {
140
+ let arg = node.arguments[0];
141
+
142
+ if (arg && ts.isObjectLiteralExpression(arg)) {
143
+ let varName: string | null = null;
144
+
145
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
146
+ varName = node.parent.name.text;
147
+ ctx.bindings.set(varName, 'object');
148
+ }
149
+
150
+ let needsImports = new Set<string>(),
151
+ properties: AnalyzedProperty[] = [];
152
+
153
+ needsImports.add('REACTIVE_OBJECT');
154
+
155
+ let props = arg.properties;
156
+
157
+ for (let i = 0, n = props.length; i < n; i++) {
158
+ let prop = props[i];
159
+
160
+ if (ts.isSpreadAssignment(prop)) {
161
+ ts.forEachChild(node, n => visit(ctx, n));
162
+ return;
163
+ }
164
+
165
+ let analyzed = analyzeProperty(prop, ctx.sourceFile);
166
+
167
+ if (!analyzed) {
168
+ ts.forEachChild(node, n => visit(ctx, n));
169
+ return;
170
+ }
171
+
172
+ properties.push(analyzed);
173
+
174
+ if (analyzed.type === 'signal') {
175
+ needsImports.add('read');
176
+ needsImports.add('set');
177
+ needsImports.add('signal');
178
+ }
179
+ else if (analyzed.type === 'array') {
180
+ needsImports.add('ReactiveArray');
181
+
182
+ if (varName) {
183
+ ctx.bindings.set(`${varName}.${analyzed.key}`, 'array');
184
+ }
185
+ }
186
+ else if (analyzed.type === 'computed') {
187
+ needsImports.add('computed');
188
+ needsImports.add('dispose');
189
+ needsImports.add('read');
190
+ }
191
+ }
192
+
193
+ needsImports.forEach(imp => ctx.allNeededImports.add(imp));
194
+
195
+ ctx.calls.push({
196
+ end: node.end,
197
+ generatedClass: buildClassCode(uid('ReactiveObject'), properties),
198
+ needsImports,
199
+ start: node.pos,
200
+ varName
201
+ });
202
+ }
203
+ }
204
+
205
+ ts.forEachChild(node, n => visit(ctx, n));
206
+ }
207
+
208
+
209
+ const transformReactiveObjects = (sourceFile: ts.SourceFile, bindings: Bindings): string => {
210
+ let code = sourceFile.getFullText(),
211
+ ctx: TransformContext = {
212
+ allNeededImports: new Set<string>(),
213
+ bindings,
214
+ calls: [],
215
+ hasReactiveImport: false,
216
+ lastImportEnd: 0,
217
+ sourceFile
218
+ };
219
+
220
+ visit(ctx, sourceFile);
221
+
222
+ if (ctx.calls.length === 0) {
223
+ return code;
224
+ }
225
+
226
+ let replacements: Replacement[] = [];
227
+
228
+ replacements.push({
229
+ end: ctx.lastImportEnd,
230
+ newText: code.substring(0, ctx.lastImportEnd) + '\n' + ctx.calls.map(c => c.generatedClass).join('\n') + '\n',
231
+ start: 0
232
+ });
233
+
234
+ for (let i = 0, n = ctx.calls.length; i < n; i++) {
235
+ let call = ctx.calls[i],
236
+ classMatch = call.generatedClass.match(CLASS_NAME_REGEX);
237
+
238
+ replacements.push({
239
+ end: call.end,
240
+ newText: ` new ${classMatch ? classMatch[1] : 'ReactiveObject'}()`,
241
+ start: call.start
242
+ });
243
+ }
244
+
245
+ return addMissingImports(
246
+ applyReplacements(code, replacements),
247
+ ctx.allNeededImports,
248
+ EXTRA_IMPORTS
249
+ );
250
+ };
251
+
252
+
253
+ export { transformReactiveObjects };