@esportsplus/reactivity 0.23.0 → 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 (39) hide show
  1. package/build/transformer/detector.js +38 -0
  2. package/build/transformer/{core/index.d.ts → index.d.ts} +1 -1
  3. package/build/transformer/plugins/esbuild.js +3 -2
  4. package/build/transformer/plugins/tsc.js +1 -1
  5. package/build/transformer/plugins/vite.js +2 -2
  6. package/build/transformer/transforms/auto-dispose.js +119 -0
  7. package/build/transformer/{core/transforms → transforms}/reactive-array.d.ts +1 -1
  8. package/build/transformer/transforms/reactive-array.js +93 -0
  9. package/build/transformer/{core/transforms → transforms}/reactive-object.d.ts +1 -1
  10. package/build/transformer/transforms/reactive-object.js +164 -0
  11. package/build/transformer/{core/transforms → transforms}/reactive-primitives.d.ts +1 -1
  12. package/build/transformer/transforms/reactive-primitives.js +335 -0
  13. package/build/transformer/{core/transforms → transforms}/utilities.d.ts +1 -2
  14. package/build/transformer/transforms/utilities.js +73 -0
  15. package/package.json +8 -12
  16. package/src/transformer/detector.ts +65 -0
  17. package/src/transformer/{core/index.ts → index.ts} +1 -5
  18. package/src/transformer/plugins/esbuild.ts +3 -2
  19. package/src/transformer/plugins/tsc.ts +1 -1
  20. package/src/transformer/plugins/vite.ts +2 -4
  21. package/src/transformer/transforms/auto-dispose.ts +191 -0
  22. package/src/transformer/transforms/reactive-array.ts +143 -0
  23. package/src/transformer/{core/transforms → transforms}/reactive-object.ts +101 -92
  24. package/src/transformer/transforms/reactive-primitives.ts +461 -0
  25. package/src/transformer/transforms/utilities.ts +119 -0
  26. package/build/transformer/core/detector.js +0 -6
  27. package/build/transformer/core/transforms/auto-dispose.js +0 -116
  28. package/build/transformer/core/transforms/reactive-array.js +0 -89
  29. package/build/transformer/core/transforms/reactive-object.js +0 -155
  30. package/build/transformer/core/transforms/reactive-primitives.js +0 -325
  31. package/build/transformer/core/transforms/utilities.js +0 -57
  32. package/src/transformer/core/detector.ts +0 -12
  33. package/src/transformer/core/transforms/auto-dispose.ts +0 -194
  34. package/src/transformer/core/transforms/reactive-array.ts +0 -140
  35. package/src/transformer/core/transforms/reactive-primitives.ts +0 -459
  36. package/src/transformer/core/transforms/utilities.ts +0 -95
  37. /package/build/transformer/{core/detector.d.ts → detector.d.ts} +0 -0
  38. /package/build/transformer/{core/index.js → index.js} +0 -0
  39. /package/build/transformer/{core/transforms → transforms}/auto-dispose.d.ts +0 -0
@@ -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 };
@@ -1,7 +1,7 @@
1
- import type { Bindings } from '~/types';
2
1
  import { uid } from '@esportsplus/typescript/transformer';
3
- import ts from 'typescript';
2
+ import type { Bindings } from '~/types';
4
3
  import { addMissingImports, applyReplacements, ExtraImport, Replacement } from './utilities';
4
+ import ts from 'typescript';
5
5
 
6
6
 
7
7
  const CLASS_NAME_REGEX = /class (\w+)/;
@@ -26,6 +26,14 @@ interface ReactiveObjectCall {
26
26
  varName: string | null;
27
27
  }
28
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
+ }
29
37
 
30
38
 
31
39
  function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.SourceFile): AnalyzedProperty | null {
@@ -100,130 +108,131 @@ function buildClassCode(className: string, properties: AnalyzedProperty[]): stri
100
108
  `;
101
109
  }
102
110
 
111
+ function visit(ctx: TransformContext, node: ts.Node): void {
112
+ if (ts.isImportDeclaration(node)) {
113
+ ctx.lastImportEnd = node.end;
103
114
 
104
- const transformReactiveObjects = (sourceFile: ts.SourceFile, bindings: Bindings): string => {
105
- let allNeededImports = new Set<string>(),
106
- calls: ReactiveObjectCall[] = [],
107
- code = sourceFile.getFullText(),
108
- hasReactiveImport = false,
109
- lastImportEnd = 0;
110
-
111
- function visit(node: ts.Node): void {
112
- // Track imports (always at top of file)
113
- if (ts.isImportDeclaration(node)) {
114
- lastImportEnd = node.end;
115
-
116
- if (
117
- ts.isStringLiteral(node.moduleSpecifier) &&
118
- node.moduleSpecifier.text.includes('@esportsplus/reactivity')
119
- ) {
120
- let clause = node.importClause;
121
-
122
- if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
123
- let elements = clause.namedBindings.elements;
124
-
125
- for (let i = 0, n = elements.length; i < n; i++) {
126
- if (elements[i].name.text === 'reactive') {
127
- hasReactiveImport = true;
128
- break;
129
- }
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;
130
128
  }
131
129
  }
132
130
  }
133
131
  }
132
+ }
134
133
 
135
- // Process reactive() calls (only if import was found)
136
- if (
137
- hasReactiveImport &&
138
- ts.isCallExpression(node) &&
139
- ts.isIdentifier(node.expression) &&
140
- node.expression.text === 'reactive'
141
- ) {
142
-
143
- let arg = node.arguments[0];
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];
144
141
 
145
- if (arg && ts.isObjectLiteralExpression(arg)) {
146
- let varName: string | null = null;
142
+ if (arg && ts.isObjectLiteralExpression(arg)) {
143
+ let varName: string | null = null;
147
144
 
148
- if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
149
- varName = node.parent.name.text;
150
- bindings.set(varName, 'object');
151
- }
145
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
146
+ varName = node.parent.name.text;
147
+ ctx.bindings.set(varName, 'object');
148
+ }
152
149
 
153
- let needsImports = new Set<string>(),
154
- properties: AnalyzedProperty[] = [];
150
+ let needsImports = new Set<string>(),
151
+ properties: AnalyzedProperty[] = [];
155
152
 
156
- needsImports.add('REACTIVE_OBJECT');
153
+ needsImports.add('REACTIVE_OBJECT');
157
154
 
158
- let props = arg.properties;
155
+ let props = arg.properties;
159
156
 
160
- for (let i = 0, n = props.length; i < n; i++) {
161
- let prop = props[i];
157
+ for (let i = 0, n = props.length; i < n; i++) {
158
+ let prop = props[i];
162
159
 
163
- if (ts.isSpreadAssignment(prop)) {
164
- return;
165
- }
160
+ if (ts.isSpreadAssignment(prop)) {
161
+ ts.forEachChild(node, n => visit(ctx, n));
162
+ return;
163
+ }
166
164
 
167
- let analyzed = analyzeProperty(prop, sourceFile);
165
+ let analyzed = analyzeProperty(prop, ctx.sourceFile);
168
166
 
169
- if (!analyzed) {
170
- return;
171
- }
167
+ if (!analyzed) {
168
+ ts.forEachChild(node, n => visit(ctx, n));
169
+ return;
170
+ }
172
171
 
173
- properties.push(analyzed);
172
+ properties.push(analyzed);
174
173
 
175
- if (analyzed.type === 'signal') {
176
- needsImports.add('read');
177
- needsImports.add('set');
178
- needsImports.add('signal');
179
- }
180
- else if (analyzed.type === 'array') {
181
- needsImports.add('ReactiveArray');
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');
182
181
 
183
- if (varName) {
184
- bindings.set(`${varName}.${analyzed.key}`, 'array');
185
- }
186
- }
187
- else if (analyzed.type === 'computed') {
188
- needsImports.add('computed');
189
- needsImports.add('dispose');
190
- needsImports.add('read');
182
+ if (varName) {
183
+ ctx.bindings.set(`${varName}.${analyzed.key}`, 'array');
191
184
  }
192
185
  }
186
+ else if (analyzed.type === 'computed') {
187
+ needsImports.add('computed');
188
+ needsImports.add('dispose');
189
+ needsImports.add('read');
190
+ }
191
+ }
193
192
 
194
- needsImports.forEach(imp => allNeededImports.add(imp));
193
+ needsImports.forEach(imp => ctx.allNeededImports.add(imp));
195
194
 
196
- calls.push({
197
- end: node.end,
198
- generatedClass: buildClassCode(uid('ReactiveObject'), properties),
199
- needsImports,
200
- start: node.pos,
201
- varName
202
- });
203
- }
195
+ ctx.calls.push({
196
+ end: node.end,
197
+ generatedClass: buildClassCode(uid('ReactiveObject'), properties),
198
+ needsImports,
199
+ start: node.pos,
200
+ varName
201
+ });
204
202
  }
205
-
206
- ts.forEachChild(node, visit);
207
203
  }
208
204
 
209
- visit(sourceFile);
205
+ ts.forEachChild(node, n => visit(ctx, n));
206
+ }
207
+
210
208
 
211
- if (calls.length === 0) {
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) {
212
223
  return code;
213
224
  }
214
225
 
215
226
  let replacements: Replacement[] = [];
216
227
 
217
- // Insert generated classes after imports
218
228
  replacements.push({
219
- end: lastImportEnd,
220
- newText: code.substring(0, lastImportEnd) + '\n' + calls.map(c => c.generatedClass).join('\n') + '\n',
229
+ end: ctx.lastImportEnd,
230
+ newText: code.substring(0, ctx.lastImportEnd) + '\n' + ctx.calls.map(c => c.generatedClass).join('\n') + '\n',
221
231
  start: 0
222
232
  });
223
233
 
224
- // Replace each reactive() call with new ClassName()
225
- for (let i = 0, n = calls.length; i < n; i++) {
226
- let call = calls[i],
234
+ for (let i = 0, n = ctx.calls.length; i < n; i++) {
235
+ let call = ctx.calls[i],
227
236
  classMatch = call.generatedClass.match(CLASS_NAME_REGEX);
228
237
 
229
238
  replacements.push({
@@ -235,7 +244,7 @@ const transformReactiveObjects = (sourceFile: ts.SourceFile, bindings: Bindings)
235
244
 
236
245
  return addMissingImports(
237
246
  applyReplacements(code, replacements),
238
- allNeededImports,
247
+ ctx.allNeededImports,
239
248
  EXTRA_IMPORTS
240
249
  );
241
250
  };