@esportsplus/reactivity 0.24.1 → 0.24.2

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.
@@ -1,42 +1,32 @@
1
1
  import { uid } from '@esportsplus/typescript/transformer';
2
2
  import type { Bindings } from '~/types';
3
- import { addMissingImports, applyReplacements, ExtraImport, Replacement } from './utilities';
4
3
  import { ts } from '@esportsplus/typescript';
5
4
 
6
5
 
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
6
  interface AnalyzedProperty {
7
+ elements?: ts.Expression[];
16
8
  key: string;
17
9
  type: 'array' | 'computed' | 'signal';
18
- valueText: string;
10
+ value: ts.Expression;
19
11
  }
20
12
 
21
- interface ReactiveObjectCall {
22
- end: number;
23
- generatedClass: string;
13
+ interface GeneratedClass {
14
+ classDecl: ts.ClassDeclaration;
15
+ className: string;
24
16
  needsImports: Set<string>;
25
- start: number;
26
- varName: string | null;
27
17
  }
28
18
 
29
19
  interface TransformContext {
30
- allNeededImports: Set<string>;
31
20
  bindings: Bindings;
32
- calls: ReactiveObjectCall[];
21
+ context: ts.TransformationContext;
22
+ factory: ts.NodeFactory;
23
+ generatedClasses: GeneratedClass[];
33
24
  hasReactiveImport: boolean;
34
- lastImportEnd: number;
35
- sourceFile: ts.SourceFile;
25
+ neededImports: Set<string>;
36
26
  }
37
27
 
38
28
 
39
- function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.SourceFile): AnalyzedProperty | null {
29
+ function analyzeProperty(prop: ts.ObjectLiteralElementLike): AnalyzedProperty | null {
40
30
  if (!ts.isPropertyAssignment(prop)) {
41
31
  return null;
42
32
  }
@@ -50,66 +40,244 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.Sourc
50
40
  return null;
51
41
  }
52
42
 
53
- let value = prop.initializer,
54
- valueText = value.getText(sourceFile);
43
+ let value = prop.initializer;
55
44
 
56
45
  if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
57
- return { key, type: 'computed', valueText };
46
+ return { key, type: 'computed', value };
58
47
  }
59
48
 
60
49
  if (ts.isArrayLiteralExpression(value)) {
61
- return { key, type: 'array', valueText };
50
+ return { elements: [...value.elements], key, type: 'array', value };
62
51
  }
63
52
 
64
- return { key, type: 'signal', valueText };
53
+ return { key, type: 'signal', value };
65
54
  }
66
55
 
67
- function buildClassCode(className: string, properties: AnalyzedProperty[]): string {
68
- let accessors: string[] = [],
69
- disposeStatements: string[] = [],
70
- fields: string[] = [];
56
+ function buildReactiveClass(
57
+ ctx: TransformContext,
58
+ className: string,
59
+ properties: AnalyzedProperty[],
60
+ varName: string | null
61
+ ): ts.ClassDeclaration {
62
+ let factory = ctx.factory,
63
+ members: ts.ClassElement[] = [],
64
+ needsImports = new Set<string>();
65
+
66
+ needsImports.add('REACTIVE_OBJECT');
67
+
68
+ // [REACTIVE_OBJECT] = true
69
+ members.push(
70
+ factory.createPropertyDeclaration(
71
+ undefined,
72
+ factory.createComputedPropertyName(factory.createIdentifier('REACTIVE_OBJECT')),
73
+ undefined,
74
+ undefined,
75
+ factory.createTrue()
76
+ )
77
+ );
71
78
 
72
- fields.push(`[REACTIVE_OBJECT] = true;`);
79
+ let disposeStatements: ts.Statement[] = [];
73
80
 
74
81
  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}); }`);
82
+ let prop = properties[i];
83
+
84
+ if (prop.type === 'signal') {
85
+ needsImports.add('read');
86
+ needsImports.add('set');
87
+ needsImports.add('signal');
88
+
89
+ let privateName = factory.createPrivateIdentifier(`#${prop.key}`),
90
+ paramName = uid('v');
91
+
92
+ // Private field: #key = signal(value)
93
+ members.push(
94
+ factory.createPropertyDeclaration(
95
+ undefined,
96
+ privateName,
97
+ undefined,
98
+ undefined,
99
+ factory.createCallExpression(
100
+ factory.createIdentifier('signal'),
101
+ undefined,
102
+ [prop.value]
103
+ )
104
+ )
105
+ );
106
+
107
+ // Getter: get key() { return read(this.#key); }
108
+ members.push(
109
+ factory.createGetAccessorDeclaration(
110
+ undefined,
111
+ factory.createIdentifier(prop.key),
112
+ [],
113
+ undefined,
114
+ factory.createBlock([
115
+ factory.createReturnStatement(
116
+ factory.createCallExpression(
117
+ factory.createIdentifier('read'),
118
+ undefined,
119
+ [factory.createPropertyAccessExpression(factory.createThis(), privateName)]
120
+ )
121
+ )
122
+ ], true)
123
+ )
124
+ );
125
+
126
+ // Setter: set key(v) { set(this.#key, v); }
127
+ members.push(
128
+ factory.createSetAccessorDeclaration(
129
+ undefined,
130
+ factory.createIdentifier(prop.key),
131
+ [factory.createParameterDeclaration(undefined, undefined, paramName)],
132
+ factory.createBlock([
133
+ factory.createExpressionStatement(
134
+ factory.createCallExpression(
135
+ factory.createIdentifier('set'),
136
+ undefined,
137
+ [
138
+ factory.createPropertyAccessExpression(factory.createThis(), privateName),
139
+ factory.createIdentifier(paramName)
140
+ ]
141
+ )
142
+ )
143
+ ], true)
144
+ )
145
+ );
83
146
  }
84
- else if (type === 'array') {
85
- let elements = valueText.slice(1, -1);
147
+ else if (prop.type === 'array') {
148
+ needsImports.add('ReactiveArray');
149
+
150
+ // Public field: key = new ReactiveArray(elements...)
151
+ members.push(
152
+ factory.createPropertyDeclaration(
153
+ undefined,
154
+ factory.createIdentifier(prop.key),
155
+ undefined,
156
+ undefined,
157
+ factory.createNewExpression(
158
+ factory.createIdentifier('ReactiveArray'),
159
+ undefined,
160
+ prop.elements || []
161
+ )
162
+ )
163
+ );
164
+
165
+ // Track as array binding
166
+ if (varName) {
167
+ ctx.bindings.set(`${varName}.${prop.key}`, 'array');
168
+ }
86
169
 
87
- fields.push(`${key} = new ReactiveArray(${elements});`);
88
- disposeStatements.push(`this.${key}.dispose();`);
170
+ // dispose: this.key.dispose()
171
+ disposeStatements.push(
172
+ factory.createExpressionStatement(
173
+ factory.createCallExpression(
174
+ factory.createPropertyAccessExpression(
175
+ factory.createPropertyAccessExpression(factory.createThis(), prop.key),
176
+ 'dispose'
177
+ ),
178
+ undefined,
179
+ []
180
+ )
181
+ )
182
+ );
89
183
  }
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});`);
184
+ else if (prop.type === 'computed') {
185
+ needsImports.add('computed');
186
+ needsImports.add('dispose');
187
+ needsImports.add('read');
188
+
189
+ let privateName = factory.createPrivateIdentifier(`#${prop.key}`);
190
+
191
+ // Private field: #key: Computed<unknown> | null = null
192
+ members.push(
193
+ factory.createPropertyDeclaration(
194
+ undefined,
195
+ privateName,
196
+ undefined,
197
+ factory.createUnionTypeNode([
198
+ factory.createTypeReferenceNode('Computed', [
199
+ factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
200
+ ]),
201
+ factory.createLiteralTypeNode(factory.createNull())
202
+ ]),
203
+ factory.createNull()
204
+ )
205
+ );
206
+
207
+ // Getter: get key() { return read(this.#key ??= computed(fn)); }
208
+ members.push(
209
+ factory.createGetAccessorDeclaration(
210
+ undefined,
211
+ factory.createIdentifier(prop.key),
212
+ [],
213
+ undefined,
214
+ factory.createBlock([
215
+ factory.createReturnStatement(
216
+ factory.createCallExpression(
217
+ factory.createIdentifier('read'),
218
+ undefined,
219
+ [
220
+ factory.createBinaryExpression(
221
+ factory.createPropertyAccessExpression(factory.createThis(), privateName),
222
+ ts.SyntaxKind.QuestionQuestionEqualsToken,
223
+ factory.createCallExpression(
224
+ factory.createIdentifier('computed'),
225
+ undefined,
226
+ [prop.value]
227
+ )
228
+ )
229
+ ]
230
+ )
231
+ )
232
+ ], true)
233
+ )
234
+ );
235
+
236
+ // dispose: if (this.#key) dispose(this.#key)
237
+ disposeStatements.push(
238
+ factory.createIfStatement(
239
+ factory.createPropertyAccessExpression(factory.createThis(), privateName),
240
+ factory.createExpressionStatement(
241
+ factory.createCallExpression(
242
+ factory.createIdentifier('dispose'),
243
+ undefined,
244
+ [factory.createPropertyAccessExpression(factory.createThis(), privateName)]
245
+ )
246
+ )
247
+ )
248
+ );
94
249
  }
95
250
  }
96
251
 
97
- return `
98
- class ${className} {
99
- ${fields.join('\n')}
100
- ${accessors.join('\n')}
252
+ // dispose() method
253
+ members.push(
254
+ factory.createMethodDeclaration(
255
+ undefined,
256
+ undefined,
257
+ 'dispose',
258
+ undefined,
259
+ undefined,
260
+ [],
261
+ undefined,
262
+ factory.createBlock(disposeStatements, true)
263
+ )
264
+ );
101
265
 
102
- dispose() {
103
- ${disposeStatements.length > 0 ? disposeStatements.join('\n') : ''}
104
- }
105
- }
106
- `;
266
+ // Store needed imports
267
+ needsImports.forEach(imp => ctx.neededImports.add(imp));
268
+
269
+ return factory.createClassDeclaration(
270
+ undefined,
271
+ className,
272
+ undefined,
273
+ undefined,
274
+ members
275
+ );
107
276
  }
108
277
 
109
- function visit(ctx: TransformContext, node: ts.Node): void {
278
+ function visit(ctx: TransformContext, node: ts.Node): ts.Node | ts.Node[] {
279
+ // Check for reactive import
110
280
  if (ts.isImportDeclaration(node)) {
111
- ctx.lastImportEnd = node.end;
112
-
113
281
  if (
114
282
  ts.isStringLiteral(node.moduleSpecifier) &&
115
283
  node.moduleSpecifier.text.includes('@esportsplus/reactivity')
@@ -129,6 +297,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
129
297
  }
130
298
  }
131
299
 
300
+ // Transform reactive({ ... }) or reactive([...]) calls
132
301
  if (
133
302
  ctx.hasReactiveImport &&
134
303
  ts.isCallExpression(node) &&
@@ -137,6 +306,25 @@ function visit(ctx: TransformContext, node: ts.Node): void {
137
306
  ) {
138
307
  let arg = node.arguments[0];
139
308
 
309
+ // Handle reactive([...]) → new ReactiveArray(...)
310
+ if (arg && ts.isArrayLiteralExpression(arg)) {
311
+ let varName: string | null = null;
312
+
313
+ if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
314
+ varName = node.parent.name.text;
315
+ ctx.bindings.set(varName, 'array');
316
+ }
317
+
318
+ ctx.neededImports.add('ReactiveArray');
319
+
320
+ return ctx.factory.createNewExpression(
321
+ ctx.factory.createIdentifier('ReactiveArray'),
322
+ undefined,
323
+ [...arg.elements]
324
+ );
325
+ }
326
+
327
+ // Handle reactive({ ... }) → new ReactiveObject class
140
328
  if (arg && ts.isObjectLiteralExpression(arg)) {
141
329
  let varName: string | null = null;
142
330
 
@@ -145,107 +333,69 @@ function visit(ctx: TransformContext, node: ts.Node): void {
145
333
  ctx.bindings.set(varName, 'object');
146
334
  }
147
335
 
148
- let needsImports = new Set<string>(),
149
- properties: AnalyzedProperty[] = [];
150
-
151
- needsImports.add('REACTIVE_OBJECT');
152
-
153
- let props = arg.properties;
336
+ let properties: AnalyzedProperty[] = [],
337
+ props = arg.properties;
154
338
 
155
339
  for (let i = 0, n = props.length; i < n; i++) {
156
340
  let prop = props[i];
157
341
 
342
+ // Bail out on spread assignments
158
343
  if (ts.isSpreadAssignment(prop)) {
159
- ts.forEachChild(node, n => visit(ctx, n));
160
- return;
344
+ return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
161
345
  }
162
346
 
163
- let analyzed = analyzeProperty(prop, ctx.sourceFile);
347
+ let analyzed = analyzeProperty(prop);
164
348
 
165
349
  if (!analyzed) {
166
- ts.forEachChild(node, n => visit(ctx, n));
167
- return;
350
+ return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
168
351
  }
169
352
 
170
353
  properties.push(analyzed);
171
-
172
- if (analyzed.type === 'signal') {
173
- needsImports.add('read');
174
- needsImports.add('set');
175
- needsImports.add('signal');
176
- }
177
- else if (analyzed.type === 'array') {
178
- needsImports.add('ReactiveArray');
179
-
180
- if (varName) {
181
- ctx.bindings.set(`${varName}.${analyzed.key}`, 'array');
182
- }
183
- }
184
- else if (analyzed.type === 'computed') {
185
- needsImports.add('computed');
186
- needsImports.add('dispose');
187
- needsImports.add('read');
188
- }
189
354
  }
190
355
 
191
- needsImports.forEach(imp => ctx.allNeededImports.add(imp));
356
+ let className = uid('ReactiveObject'),
357
+ classDecl = buildReactiveClass(ctx, className, properties, varName);
192
358
 
193
- ctx.calls.push({
194
- end: node.end,
195
- generatedClass: buildClassCode(uid('ReactiveObject'), properties),
196
- needsImports,
197
- start: node.pos,
198
- varName
359
+ ctx.generatedClasses.push({
360
+ classDecl,
361
+ className,
362
+ needsImports: new Set(ctx.neededImports)
199
363
  });
364
+
365
+ // Replace reactive({...}) with new ClassName()
366
+ return ctx.factory.createNewExpression(
367
+ ctx.factory.createIdentifier(className),
368
+ undefined,
369
+ []
370
+ );
200
371
  }
201
372
  }
202
373
 
203
- ts.forEachChild(node, n => visit(ctx, n));
374
+ return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
204
375
  }
205
376
 
206
377
 
207
- const transformReactiveObjects = (sourceFile: ts.SourceFile, bindings: Bindings): string => {
208
- let code = sourceFile.getFullText(),
209
- ctx: TransformContext = {
210
- allNeededImports: new Set<string>(),
211
- bindings,
212
- calls: [],
213
- hasReactiveImport: false,
214
- lastImportEnd: 0,
215
- sourceFile
378
+ const createObjectTransformer = (
379
+ bindings: Bindings,
380
+ neededImports: Set<string>,
381
+ generatedClasses: GeneratedClass[]
382
+ ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile => {
383
+ return (context: ts.TransformationContext) => {
384
+ return (sourceFile: ts.SourceFile): ts.SourceFile => {
385
+ let ctx: TransformContext = {
386
+ bindings,
387
+ context,
388
+ factory: context.factory,
389
+ generatedClasses,
390
+ hasReactiveImport: false,
391
+ neededImports
392
+ };
393
+
394
+ return ts.visitNode(sourceFile, n => visit(ctx, n)) as ts.SourceFile;
216
395
  };
217
-
218
- visit(ctx, sourceFile);
219
-
220
- if (ctx.calls.length === 0) {
221
- return code;
222
- }
223
-
224
- let replacements: Replacement[] = [];
225
-
226
- replacements.push({
227
- end: ctx.lastImportEnd,
228
- newText: code.substring(0, ctx.lastImportEnd) + '\n' + ctx.calls.map(c => c.generatedClass).join('\n') + '\n',
229
- start: 0
230
- });
231
-
232
- for (let i = 0, n = ctx.calls.length; i < n; i++) {
233
- let call = ctx.calls[i],
234
- classMatch = call.generatedClass.match(CLASS_NAME_REGEX);
235
-
236
- replacements.push({
237
- end: call.end,
238
- newText: ` new ${classMatch ? classMatch[1] : 'ReactiveObject'}()`,
239
- start: call.start
240
- });
241
- }
242
-
243
- return addMissingImports(
244
- applyReplacements(code, replacements),
245
- ctx.allNeededImports,
246
- EXTRA_IMPORTS
247
- );
396
+ };
248
397
  };
249
398
 
250
399
 
251
- export { transformReactiveObjects };
400
+ export { createObjectTransformer };
401
+ export type { GeneratedClass };