@esportsplus/reactivity 0.24.5 → 0.25.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.
@@ -1,33 +1,42 @@
1
1
  import { uid } from '@esportsplus/typescript/transformer';
2
- import type { Bindings, Namespaces } from '~/types';
2
+ import type { Bindings } from '~/types';
3
+ import { addMissingImports, applyReplacements, ExtraImport, Replacement } from './utilities';
3
4
  import { ts } from '@esportsplus/typescript';
4
5
 
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
+
6
15
  interface AnalyzedProperty {
7
- elements?: ts.Expression[];
8
16
  key: string;
9
17
  type: 'array' | 'computed' | 'signal';
10
- value: ts.Expression;
18
+ valueText: string;
11
19
  }
12
20
 
13
- interface GeneratedClass {
14
- classDecl: ts.ClassDeclaration;
15
- className: string;
21
+ interface ReactiveObjectCall {
22
+ end: number;
23
+ generatedClass: string;
16
24
  needsImports: Set<string>;
25
+ start: number;
26
+ varName: string | null;
17
27
  }
18
28
 
19
29
  interface TransformContext {
30
+ allNeededImports: Set<string>;
20
31
  bindings: Bindings;
21
- context: ts.TransformationContext;
22
- factory: ts.NodeFactory;
23
- generatedClasses: GeneratedClass[];
32
+ calls: ReactiveObjectCall[];
24
33
  hasReactiveImport: boolean;
25
- neededImports: Set<string>;
26
- ns: Namespaces;
34
+ lastImportEnd: number;
35
+ sourceFile: ts.SourceFile;
27
36
  }
28
37
 
29
38
 
30
- function analyzeProperty(prop: ts.ObjectLiteralElementLike): AnalyzedProperty | null {
39
+ function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.SourceFile): AnalyzedProperty | null {
31
40
  if (!ts.isPropertyAssignment(prop)) {
32
41
  return null;
33
42
  }
@@ -41,246 +50,66 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike): AnalyzedProperty |
41
50
  return null;
42
51
  }
43
52
 
44
- let value = prop.initializer;
53
+ let value = prop.initializer,
54
+ valueText = value.getText(sourceFile);
45
55
 
46
56
  if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
47
- return { key, type: 'computed', value };
57
+ return { key, type: 'computed', valueText };
48
58
  }
49
59
 
50
60
  if (ts.isArrayLiteralExpression(value)) {
51
- return { elements: [...value.elements], key, type: 'array', value };
61
+ return { key, type: 'array', valueText };
52
62
  }
53
63
 
54
- return { key, type: 'signal', value };
64
+ return { key, type: 'signal', valueText };
55
65
  }
56
66
 
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
- );
67
+ function buildClassCode(className: string, properties: AnalyzedProperty[]): string {
68
+ let accessors: string[] = [],
69
+ disposeStatements: string[] = [],
70
+ fields: string[] = [];
81
71
 
82
- let disposeStatements: ts.Statement[] = [];
72
+ fields.push(`[REACTIVE_OBJECT] = true;`);
83
73
 
84
74
  for (let i = 0, n = properties.length; i < n; i++) {
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
- );
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}); }`);
149
83
  }
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
- }
84
+ else if (type === 'array') {
85
+ let elements = valueText.slice(1, -1);
172
86
 
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
- );
87
+ fields.push(`${key} = new ReactiveArray(${elements});`);
88
+ disposeStatements.push(`this.${key}.dispose();`);
186
89
  }
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
- );
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});`);
252
94
  }
253
95
  }
254
96
 
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
- );
268
-
269
- // Store needed imports
270
- needsImports.forEach(imp => ctx.neededImports.add(imp));
97
+ return `
98
+ class ${className} {
99
+ ${fields.join('\n')}
100
+ ${accessors.join('\n')}
271
101
 
272
- return factory.createClassDeclaration(
273
- undefined,
274
- className,
275
- undefined,
276
- undefined,
277
- members
278
- );
102
+ dispose() {
103
+ ${disposeStatements.length > 0 ? disposeStatements.join('\n') : ''}
104
+ }
105
+ }
106
+ `;
279
107
  }
280
108
 
281
- function visit(ctx: TransformContext, node: ts.Node): ts.Node | ts.Node[] {
282
- // Check for reactive import - return early to avoid visiting import children
109
+ function visit(ctx: TransformContext, node: ts.Node): void {
283
110
  if (ts.isImportDeclaration(node)) {
111
+ ctx.lastImportEnd = node.end;
112
+
284
113
  if (
285
114
  ts.isStringLiteral(node.moduleSpecifier) &&
286
115
  node.moduleSpecifier.text.includes('@esportsplus/reactivity')
@@ -298,11 +127,8 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node | ts.Node[] {
298
127
  }
299
128
  }
300
129
  }
301
-
302
- return node;
303
130
  }
304
131
 
305
- // Transform reactive({ ... }) or reactive([...]) calls
306
132
  if (
307
133
  ctx.hasReactiveImport &&
308
134
  ts.isCallExpression(node) &&
@@ -311,25 +137,6 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node | ts.Node[] {
311
137
  ) {
312
138
  let arg = node.arguments[0];
313
139
 
314
- // Handle reactive([...]) → new ns.array.ReactiveArray(...)
315
- if (arg && ts.isArrayLiteralExpression(arg)) {
316
- let varName: string | null = null;
317
-
318
- if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
319
- varName = node.parent.name.text;
320
- ctx.bindings.set(varName, 'array');
321
- }
322
-
323
- ctx.neededImports.add('ReactiveArray');
324
-
325
- return ctx.factory.createNewExpression(
326
- ctx.factory.createPropertyAccessExpression(ctx.factory.createIdentifier(ctx.ns.array), 'ReactiveArray'),
327
- undefined,
328
- [...arg.elements]
329
- );
330
- }
331
-
332
- // Handle reactive({ ... }) → new ReactiveObject class
333
140
  if (arg && ts.isObjectLiteralExpression(arg)) {
334
141
  let varName: string | null = null;
335
142
 
@@ -338,71 +145,107 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node | ts.Node[] {
338
145
  ctx.bindings.set(varName, 'object');
339
146
  }
340
147
 
341
- let properties: AnalyzedProperty[] = [],
342
- props = arg.properties;
148
+ let needsImports = new Set<string>(),
149
+ properties: AnalyzedProperty[] = [];
150
+
151
+ needsImports.add('REACTIVE_OBJECT');
152
+
153
+ let props = arg.properties;
343
154
 
344
155
  for (let i = 0, n = props.length; i < n; i++) {
345
156
  let prop = props[i];
346
157
 
347
- // Bail out on spread assignments
348
158
  if (ts.isSpreadAssignment(prop)) {
349
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
159
+ ts.forEachChild(node, n => visit(ctx, n));
160
+ return;
350
161
  }
351
162
 
352
- let analyzed = analyzeProperty(prop);
163
+ let analyzed = analyzeProperty(prop, ctx.sourceFile);
353
164
 
354
165
  if (!analyzed) {
355
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
166
+ ts.forEachChild(node, n => visit(ctx, n));
167
+ return;
356
168
  }
357
169
 
358
170
  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
+ }
359
189
  }
360
190
 
361
- let className = uid('ReactiveObject'),
362
- classDecl = buildReactiveClass(ctx, className, properties, varName);
191
+ needsImports.forEach(imp => ctx.allNeededImports.add(imp));
363
192
 
364
- ctx.generatedClasses.push({
365
- classDecl,
366
- className,
367
- needsImports: new Set(ctx.neededImports)
193
+ ctx.calls.push({
194
+ end: node.end,
195
+ generatedClass: buildClassCode(uid('ReactiveObject'), properties),
196
+ needsImports,
197
+ start: node.pos,
198
+ varName
368
199
  });
369
-
370
- // Replace reactive({...}) with new ClassName()
371
- return ctx.factory.createNewExpression(
372
- ctx.factory.createIdentifier(className),
373
- undefined,
374
- []
375
- );
376
200
  }
377
201
  }
378
202
 
379
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
203
+ ts.forEachChild(node, n => visit(ctx, n));
380
204
  }
381
205
 
382
206
 
383
- const createObjectTransformer = (
384
- bindings: Bindings,
385
- neededImports: Set<string>,
386
- generatedClasses: GeneratedClass[],
387
- ns: Namespaces
388
- ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile => {
389
- return (context: ts.TransformationContext) => {
390
- return (sourceFile: ts.SourceFile): ts.SourceFile => {
391
- let ctx: TransformContext = {
392
- bindings,
393
- context,
394
- factory: context.factory,
395
- generatedClasses,
396
- hasReactiveImport: false,
397
- neededImports,
398
- ns
399
- };
400
-
401
- return ts.visitNode(sourceFile, n => visit(ctx, n)) as ts.SourceFile;
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
402
216
  };
403
- };
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
+ );
404
248
  };
405
249
 
406
250
 
407
- export { createObjectTransformer };
408
- export type { GeneratedClass };
251
+ export { transformReactiveObjects };