@esportsplus/reactivity 0.24.1 → 0.24.3

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