@esportsplus/reactivity 0.24.4 → 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
- );
97
+ return `
98
+ class ${className} {
99
+ ${fields.join('\n')}
100
+ ${accessors.join('\n')}
268
101
 
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
- );
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
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')
@@ -300,7 +129,6 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node | ts.Node[] {
300
129
  }
301
130
  }
302
131
 
303
- // Transform reactive({ ... }) or reactive([...]) calls
304
132
  if (
305
133
  ctx.hasReactiveImport &&
306
134
  ts.isCallExpression(node) &&
@@ -309,25 +137,6 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node | ts.Node[] {
309
137
  ) {
310
138
  let arg = node.arguments[0];
311
139
 
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
331
140
  if (arg && ts.isObjectLiteralExpression(arg)) {
332
141
  let varName: string | null = null;
333
142
 
@@ -336,71 +145,107 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node | ts.Node[] {
336
145
  ctx.bindings.set(varName, 'object');
337
146
  }
338
147
 
339
- let properties: AnalyzedProperty[] = [],
340
- props = arg.properties;
148
+ let needsImports = new Set<string>(),
149
+ properties: AnalyzedProperty[] = [];
150
+
151
+ needsImports.add('REACTIVE_OBJECT');
152
+
153
+ let props = arg.properties;
341
154
 
342
155
  for (let i = 0, n = props.length; i < n; i++) {
343
156
  let prop = props[i];
344
157
 
345
- // Bail out on spread assignments
346
158
  if (ts.isSpreadAssignment(prop)) {
347
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
159
+ ts.forEachChild(node, n => visit(ctx, n));
160
+ return;
348
161
  }
349
162
 
350
- let analyzed = analyzeProperty(prop);
163
+ let analyzed = analyzeProperty(prop, ctx.sourceFile);
351
164
 
352
165
  if (!analyzed) {
353
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
166
+ ts.forEachChild(node, n => visit(ctx, n));
167
+ return;
354
168
  }
355
169
 
356
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
+ }
357
189
  }
358
190
 
359
- let className = uid('ReactiveObject'),
360
- classDecl = buildReactiveClass(ctx, className, properties, varName);
191
+ needsImports.forEach(imp => ctx.allNeededImports.add(imp));
361
192
 
362
- ctx.generatedClasses.push({
363
- classDecl,
364
- className,
365
- 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
366
199
  });
367
-
368
- // Replace reactive({...}) with new ClassName()
369
- return ctx.factory.createNewExpression(
370
- ctx.factory.createIdentifier(className),
371
- undefined,
372
- []
373
- );
374
200
  }
375
201
  }
376
202
 
377
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
203
+ ts.forEachChild(node, n => visit(ctx, n));
378
204
  }
379
205
 
380
206
 
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;
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
400
216
  };
401
- };
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
+ );
402
248
  };
403
249
 
404
250
 
405
- export { createObjectTransformer };
406
- export type { GeneratedClass };
251
+ export { transformReactiveObjects };