@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,16 +1,17 @@
1
- import { uid } from '@esportsplus/typescript/transformer';
2
- import type { BindingType, Bindings, Namespaces } from '~/types';
3
- import {
4
- createCommaExpr,
5
- createComputedCall,
6
- createPostfixIncrementExpr,
7
- createReadCall,
8
- createSetCall,
9
- createSignalCall
10
- } from '../factory';
1
+ import { uid, type Range } from '@esportsplus/typescript/transformer';
2
+ import type { BindingType, Bindings } from '~/types';
3
+ import { addMissingImports, applyReplacements, Replacement } from './utilities';
11
4
  import { ts } from '@esportsplus/typescript';
12
5
 
13
6
 
7
+ interface ArgContext {
8
+ argStart: number;
9
+ innerReplacements: Replacement[];
10
+ neededImports: Set<string>;
11
+ scopedBindings: ScopeBinding[];
12
+ sourceFile: ts.SourceFile;
13
+ }
14
+
14
15
  interface ScopeBinding {
15
16
  name: string;
16
17
  scope: ts.Node;
@@ -19,12 +20,12 @@ interface ScopeBinding {
19
20
 
20
21
  interface TransformContext {
21
22
  bindings: Bindings;
22
- context: ts.TransformationContext;
23
- factory: ts.NodeFactory;
23
+ computedArgRanges: Range[];
24
24
  hasReactiveImport: boolean;
25
25
  neededImports: Set<string>;
26
- ns: Namespaces;
26
+ replacements: Replacement[];
27
27
  scopedBindings: ScopeBinding[];
28
+ sourceFile: ts.SourceFile;
28
29
  }
29
30
 
30
31
 
@@ -57,14 +58,14 @@ function findEnclosingScope(node: ts.Node): ts.Node {
57
58
 
58
59
  while (current) {
59
60
  if (
60
- ts.isArrowFunction(current) ||
61
61
  ts.isBlock(current) ||
62
- ts.isForInStatement(current) ||
63
- ts.isForOfStatement(current) ||
64
- ts.isForStatement(current) ||
62
+ ts.isSourceFile(current) ||
65
63
  ts.isFunctionDeclaration(current) ||
66
64
  ts.isFunctionExpression(current) ||
67
- ts.isSourceFile(current)
65
+ ts.isArrowFunction(current) ||
66
+ ts.isForStatement(current) ||
67
+ ts.isForInStatement(current) ||
68
+ ts.isForOfStatement(current)
68
69
  ) {
69
70
  return current;
70
71
  }
@@ -75,82 +76,70 @@ function findEnclosingScope(node: ts.Node): ts.Node {
75
76
  return node.getSourceFile();
76
77
  }
77
78
 
78
- function getCompoundOperator(kind: ts.SyntaxKind): ts.BinaryOperator {
79
+ function getCompoundOperator(kind: ts.SyntaxKind): string {
79
80
  if (kind === ts.SyntaxKind.PlusEqualsToken) {
80
- return ts.SyntaxKind.PlusToken;
81
+ return '+';
81
82
  }
82
83
  else if (kind === ts.SyntaxKind.MinusEqualsToken) {
83
- return ts.SyntaxKind.MinusToken;
84
+ return '-';
84
85
  }
85
86
  else if (kind === ts.SyntaxKind.AsteriskEqualsToken) {
86
- return ts.SyntaxKind.AsteriskToken;
87
+ return '*';
87
88
  }
88
89
  else if (kind === ts.SyntaxKind.SlashEqualsToken) {
89
- return ts.SyntaxKind.SlashToken;
90
+ return '/';
90
91
  }
91
92
  else if (kind === ts.SyntaxKind.PercentEqualsToken) {
92
- return ts.SyntaxKind.PercentToken;
93
+ return '%';
93
94
  }
94
95
  else if (kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken) {
95
- return ts.SyntaxKind.AsteriskAsteriskToken;
96
+ return '**';
96
97
  }
97
98
  else if (kind === ts.SyntaxKind.AmpersandEqualsToken) {
98
- return ts.SyntaxKind.AmpersandToken;
99
+ return '&';
99
100
  }
100
101
  else if (kind === ts.SyntaxKind.BarEqualsToken) {
101
- return ts.SyntaxKind.BarToken;
102
+ return '|';
102
103
  }
103
104
  else if (kind === ts.SyntaxKind.CaretEqualsToken) {
104
- return ts.SyntaxKind.CaretToken;
105
+ return '^';
105
106
  }
106
107
  else if (kind === ts.SyntaxKind.LessThanLessThanEqualsToken) {
107
- return ts.SyntaxKind.LessThanLessThanToken;
108
+ return '<<';
108
109
  }
109
110
  else if (kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken) {
110
- return ts.SyntaxKind.GreaterThanGreaterThanToken;
111
+ return '>>';
111
112
  }
112
113
  else if (kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken) {
113
- return ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken;
114
+ return '>>>';
114
115
  }
115
116
  else if (kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) {
116
- return ts.SyntaxKind.AmpersandAmpersandToken;
117
+ return '&&';
117
118
  }
118
119
  else if (kind === ts.SyntaxKind.BarBarEqualsToken) {
119
- return ts.SyntaxKind.BarBarToken;
120
+ return '||';
120
121
  }
121
122
  else if (kind === ts.SyntaxKind.QuestionQuestionEqualsToken) {
122
- return ts.SyntaxKind.QuestionQuestionToken;
123
+ return '??';
123
124
  }
124
125
  else {
125
- return ts.SyntaxKind.PlusToken;
126
+ return '+';
126
127
  }
127
128
  }
128
129
 
129
- function isAssignmentOperator(kind: ts.SyntaxKind): 'compound' | 'simple' | false {
130
- if (kind === ts.SyntaxKind.EqualsToken) {
131
- return 'simple';
132
- }
133
-
134
- if (kind >= ts.SyntaxKind.PlusEqualsToken && kind <= ts.SyntaxKind.CaretEqualsToken) {
135
- return 'compound';
136
- }
130
+ function isInComputedRange(ranges: Range[], start: number, end: number): boolean {
131
+ for (let i = 0, n = ranges.length; i < n; i++) {
132
+ let r = ranges[i];
137
133
 
138
- if (
139
- kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
140
- kind === ts.SyntaxKind.BarBarEqualsToken ||
141
- kind === ts.SyntaxKind.QuestionQuestionEqualsToken
142
- ) {
143
- return 'compound';
134
+ if (start >= r.start && end <= r.end) {
135
+ return true;
136
+ }
144
137
  }
145
138
 
146
139
  return false;
147
140
  }
148
141
 
149
- function isInDeclarationInit(node: ts.Node | undefined): boolean {
150
- if (!node || !node.parent) {
151
- return false;
152
- }
153
-
142
+ function isInDeclarationInit(node: ts.Node): boolean {
154
143
  let parent = node.parent;
155
144
 
156
145
  if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
@@ -174,13 +163,16 @@ function isInScope(reference: ts.Node, binding: ScopeBinding): boolean {
174
163
  return false;
175
164
  }
176
165
 
177
- function isReactiveReassignment(node: ts.BinaryExpression): boolean {
178
- let right = node.right;
166
+ function isReactiveReassignment(node: ts.Node): boolean {
167
+ let parent = node.parent;
179
168
 
180
169
  if (
181
- ts.isCallExpression(right) &&
182
- ts.isIdentifier(right.expression) &&
183
- right.expression.text === 'reactive'
170
+ ts.isBinaryExpression(parent) &&
171
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
172
+ parent.right === node &&
173
+ ts.isCallExpression(node) &&
174
+ ts.isIdentifier((node as ts.CallExpression).expression) &&
175
+ ((node as ts.CallExpression).expression as ts.Identifier).text === 'reactive'
184
176
  ) {
185
177
  return true;
186
178
  }
@@ -188,8 +180,41 @@ function isReactiveReassignment(node: ts.BinaryExpression): boolean {
188
180
  return false;
189
181
  }
190
182
 
191
- function visit(ctx: TransformContext, node: ts.Node): ts.Node {
192
- // Check for reactive import
183
+ function isWriteContext(node: ts.Identifier): 'simple' | 'compound' | 'increment' | false {
184
+ let parent = node.parent;
185
+
186
+ if (ts.isBinaryExpression(parent) && parent.left === node) {
187
+ let op = parent.operatorToken.kind;
188
+
189
+ if (op === ts.SyntaxKind.EqualsToken) {
190
+ return 'simple';
191
+ }
192
+
193
+ if (op >= ts.SyntaxKind.PlusEqualsToken && op <= ts.SyntaxKind.CaretEqualsToken) {
194
+ return 'compound';
195
+ }
196
+
197
+ if (
198
+ op === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
199
+ op === ts.SyntaxKind.BarBarEqualsToken ||
200
+ op === ts.SyntaxKind.QuestionQuestionEqualsToken
201
+ ) {
202
+ return 'compound';
203
+ }
204
+ }
205
+
206
+ if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
207
+ let op = parent.operator;
208
+
209
+ if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken) {
210
+ return 'increment';
211
+ }
212
+ }
213
+
214
+ return false;
215
+ }
216
+
217
+ function visit(ctx: TransformContext, node: ts.Node): void {
193
218
  if (
194
219
  ts.isImportDeclaration(node) &&
195
220
  ts.isStringLiteral(node.moduleSpecifier) &&
@@ -207,7 +232,6 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
207
232
  }
208
233
  }
209
234
 
210
- // Transform reactive() calls to signal() or computed()
211
235
  if (
212
236
  ctx.hasReactiveImport &&
213
237
  ts.isCallExpression(node) &&
@@ -221,11 +245,10 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
221
245
  if (classification) {
222
246
  let varName: string | null = null;
223
247
 
224
- if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
248
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
225
249
  varName = node.parent.name.text;
226
250
  }
227
251
  else if (
228
- node.parent &&
229
252
  ts.isBinaryExpression(node.parent) &&
230
253
  node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
231
254
  ts.isIdentifier(node.parent.left)
@@ -236,184 +259,155 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
236
259
  if (varName) {
237
260
  let scope = findEnclosingScope(node);
238
261
 
239
- ctx.bindings.set(varName, classification);
240
262
  ctx.scopedBindings.push({ name: varName, scope, type: classification });
263
+ ctx.bindings.set(varName, classification);
241
264
  }
242
265
 
243
266
  if (classification === 'computed') {
244
- ctx.neededImports.add('computed');
267
+ ctx.computedArgRanges.push({
268
+ end: arg.end,
269
+ start: arg.getStart(ctx.sourceFile)
270
+ });
245
271
 
246
- // Transform the function body to wrap reactive reads
247
- let transformedArg = ts.visitEachChild(arg, n => visitComputedArg(ctx, n), ctx.context);
272
+ let argCtx: ArgContext = {
273
+ argStart: arg.getStart(ctx.sourceFile),
274
+ innerReplacements: [],
275
+ neededImports: ctx.neededImports,
276
+ scopedBindings: ctx.scopedBindings,
277
+ sourceFile: ctx.sourceFile
278
+ };
248
279
 
249
- return createComputedCall(ctx.factory, ctx.ns.reactivity, transformedArg as ts.Expression);
250
- }
251
- else {
252
- ctx.neededImports.add('signal');
280
+ visitArg(argCtx, arg);
253
281
 
254
- return createSignalCall(ctx.factory, ctx.ns.reactivity, arg);
255
- }
256
- }
257
- }
258
-
259
- // Handle binary expressions (assignments) with reactive left side
260
- if (ts.isBinaryExpression(node) && ts.isIdentifier(node.left)) {
261
- let assignType = isAssignmentOperator(node.operatorToken.kind);
262
-
263
- if (assignType) {
264
- let binding = findBinding(ctx.scopedBindings, node.left.text, node.left);
265
-
266
- if (binding && binding.type !== 'computed' && !isReactiveReassignment(node)) {
267
- ctx.neededImports.add('set');
282
+ let argText = applyReplacements(arg.getText(ctx.sourceFile), argCtx.innerReplacements);
268
283
 
269
- let factory = ctx.factory,
270
- name = node.left.text,
271
- signalIdent = factory.createIdentifier(name),
272
- transformedRight = ts.visitEachChild(node.right, n => visit(ctx, n), ctx.context) as ts.Expression;
284
+ ctx.replacements.push({
285
+ end: node.end,
286
+ newText: `computed(${argText})`,
287
+ start: node.pos
288
+ });
273
289
 
274
- if (assignType === 'simple') {
275
- // x = value → ns.set(x, value)
276
- return createSetCall(factory, ctx.ns.reactivity, signalIdent, transformedRight);
277
- }
278
- else {
279
- // x += value → ns.set(x, x.value + value)
280
- let op = getCompoundOperator(node.operatorToken.kind),
281
- valueAccess = factory.createPropertyAccessExpression(signalIdent, 'value');
282
-
283
- return createSetCall(
284
- factory,
285
- ctx.ns.reactivity,
286
- signalIdent,
287
- factory.createBinaryExpression(valueAccess, op, transformedRight)
288
- );
289
- }
290
- }
291
- }
292
- }
293
-
294
- // Handle prefix unary expressions (++x, --x) with reactive operand
295
- if (ts.isPrefixUnaryExpression(node) && ts.isIdentifier(node.operand)) {
296
- let op = node.operator;
297
-
298
- if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken) {
299
- let binding = findBinding(ctx.scopedBindings, node.operand.text, node.operand);
300
-
301
- if (binding && binding.type !== 'computed') {
302
- ctx.neededImports.add('set');
303
-
304
- let delta = op === ts.SyntaxKind.PlusPlusToken ? ts.SyntaxKind.PlusToken : ts.SyntaxKind.MinusToken,
305
- factory = ctx.factory,
306
- name = node.operand.text,
307
- signalIdent = factory.createIdentifier(name),
308
- valueAccess = factory.createPropertyAccessExpression(signalIdent, 'value');
309
-
310
- if (node.parent && ts.isExpressionStatement(node.parent)) {
311
- // ++x as statement → ns.set(x, x.value + 1)
312
- return createSetCall(
313
- factory,
314
- ctx.ns.reactivity,
315
- signalIdent,
316
- factory.createBinaryExpression(valueAccess, delta, factory.createNumericLiteral(1))
317
- );
318
- }
319
- else {
320
- // ++x in expression → (ns.set(x, x.value + 1), x.value)
321
- return createCommaExpr(
322
- factory,
323
- createSetCall(
324
- factory,
325
- ctx.ns.reactivity,
326
- signalIdent,
327
- factory.createBinaryExpression(valueAccess, delta, factory.createNumericLiteral(1))
328
- ),
329
- factory.createPropertyAccessExpression(factory.createIdentifier(name), 'value')
330
- );
331
- }
290
+ ctx.neededImports.add('computed');
332
291
  }
333
- }
334
- }
292
+ else {
293
+ let argText = arg.getText(ctx.sourceFile);
335
294
 
336
- // Handle postfix unary expressions (x++, x--) with reactive operand
337
- if (ts.isPostfixUnaryExpression(node) && ts.isIdentifier(node.operand)) {
338
- let op = node.operator;
295
+ ctx.replacements.push({
296
+ end: node.end,
297
+ newText: `signal(${argText})`,
298
+ start: node.pos
299
+ });
339
300
 
340
- if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken) {
341
- let binding = findBinding(ctx.scopedBindings, node.operand.text, node.operand);
342
-
343
- if (binding && binding.type !== 'computed') {
344
- ctx.neededImports.add('set');
345
-
346
- let delta = op === ts.SyntaxKind.PlusPlusToken ? ts.SyntaxKind.PlusToken : ts.SyntaxKind.MinusToken,
347
- factory = ctx.factory,
348
- name = node.operand.text,
349
- signalIdent = factory.createIdentifier(name),
350
- valueAccess = factory.createPropertyAccessExpression(signalIdent, 'value');
351
-
352
- if (node.parent && ts.isExpressionStatement(node.parent)) {
353
- // x++ as statement → ns.set(x, x.value + 1)
354
- return createSetCall(
355
- factory,
356
- ctx.ns.reactivity,
357
- signalIdent,
358
- factory.createBinaryExpression(valueAccess, delta, factory.createNumericLiteral(1))
359
- );
360
- }
361
- else {
362
- // x++ in expression → ((tmp) => (ns.set(x, tmp + 1), tmp))(x.value)
363
- return createPostfixIncrementExpr(factory, ctx.ns.reactivity, uid('tmp'), name, delta);
364
- }
301
+ ctx.neededImports.add('signal');
365
302
  }
366
303
  }
367
304
  }
368
305
 
369
- // Handle reactive variable reads (not in write context)
370
- if (ts.isIdentifier(node) && node.parent && !isInDeclarationInit(node.parent)) {
371
- // Skip property names in property access expressions
306
+ if (ts.isIdentifier(node) && !isInDeclarationInit(node.parent)) {
372
307
  if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
373
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
374
- }
375
-
376
- // Skip if this identifier is the left side of an assignment (handled above)
377
- if (ts.isBinaryExpression(node.parent) && node.parent.left === node) {
378
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
308
+ ts.forEachChild(node, n => visit(ctx, n));
309
+ return;
379
310
  }
380
311
 
381
- // Skip if this is the operand of a unary expression (handled above)
382
- if (
383
- (ts.isPrefixUnaryExpression(node.parent) || ts.isPostfixUnaryExpression(node.parent)) &&
384
- node.parent.operand === node
385
- ) {
386
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
387
- }
312
+ let nodeStart = node.getStart(ctx.sourceFile);
388
313
 
389
- // Skip typeof checks
390
- if (ts.isTypeOfExpression(node.parent) && node.parent.expression === node) {
391
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
314
+ if (isInComputedRange(ctx.computedArgRanges, nodeStart, node.end)) {
315
+ ts.forEachChild(node, n => visit(ctx, n));
316
+ return;
392
317
  }
393
318
 
394
- let binding = findBinding(ctx.scopedBindings, node.text, node);
319
+ let binding = findBinding(ctx.scopedBindings, node.text, node),
320
+ name = node.text;
395
321
 
396
322
  if (binding) {
397
- // Read access → ns.read(x)
398
- ctx.neededImports.add('read');
323
+ if (
324
+ !isReactiveReassignment(node.parent) &&
325
+ !(ts.isTypeOfExpression(node.parent) && node.parent.expression === node)
326
+ ) {
327
+ let writeCtx = isWriteContext(node);
328
+
329
+ if (writeCtx) {
330
+ if (binding.type !== 'computed') {
331
+ ctx.neededImports.add('set');
332
+
333
+ let parent = node.parent;
334
+
335
+ if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
336
+ let valueText = parent.right.getText(ctx.sourceFile);
337
+
338
+ ctx.replacements.push({
339
+ end: parent.end,
340
+ newText: `set(${name}, ${valueText})`,
341
+ start: parent.pos
342
+ });
343
+ }
344
+ else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
345
+ let op = getCompoundOperator(parent.operatorToken.kind),
346
+ valueText = parent.right.getText(ctx.sourceFile);
347
+
348
+ ctx.replacements.push({
349
+ end: parent.end,
350
+ newText: `set(${name}, ${name}.value ${op} ${valueText})`,
351
+ start: parent.pos
352
+ });
353
+ }
354
+ else if (writeCtx === 'increment') {
355
+ let isPrefix = ts.isPrefixUnaryExpression(parent),
356
+ op = (parent as ts.PrefixUnaryExpression | ts.PostfixUnaryExpression).operator,
357
+ delta = op === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1';
358
+
359
+ if (ts.isExpressionStatement(parent.parent)) {
360
+ ctx.replacements.push({
361
+ end: parent.end,
362
+ newText: `set(${name}, ${name}.value ${delta})`,
363
+ start: parent.pos
364
+ });
365
+ }
366
+ else if (isPrefix) {
367
+ ctx.replacements.push({
368
+ end: parent.end,
369
+ newText: `(set(${name}, ${name}.value ${delta}), ${name}.value)`,
370
+ start: parent.pos
371
+ });
372
+ }
373
+ else {
374
+ let tmp = uid('tmp');
375
+
376
+ ctx.replacements.push({
377
+ end: parent.end,
378
+ newText: `((${tmp}) => (set(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
379
+ start: parent.pos
380
+ });
381
+ }
382
+ }
383
+ }
384
+ }
385
+ else {
386
+ ctx.neededImports.add('read');
399
387
 
400
- return createReadCall(ctx.factory, ctx.ns.reactivity, ctx.factory.createIdentifier(node.text));
388
+ ctx.replacements.push({
389
+ end: node.end,
390
+ newText: `read(${name})`,
391
+ start: node.pos
392
+ });
393
+ }
394
+ }
401
395
  }
402
396
  }
403
397
 
404
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
398
+ ts.forEachChild(node, n => visit(ctx, n));
405
399
  }
406
400
 
407
- function visitComputedArg(ctx: TransformContext, node: ts.Node): ts.Node {
408
- // Skip property names in property access
409
- if (ts.isIdentifier(node) && node.parent) {
401
+ function visitArg(ctx: ArgContext, node: ts.Node): void {
402
+ if (ts.isIdentifier(node)) {
410
403
  if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
411
- return ts.visitEachChild(node, n => visitComputedArg(ctx, n), ctx.context);
404
+ ts.forEachChild(node, n => visitArg(ctx, n));
405
+ return;
412
406
  }
413
407
 
414
- // Skip function call expressions
415
408
  if (ts.isCallExpression(node.parent) && node.parent.expression === node) {
416
- return ts.visitEachChild(node, n => visitComputedArg(ctx, n), ctx.context);
409
+ ts.forEachChild(node, n => visitArg(ctx, n));
410
+ return;
417
411
  }
418
412
 
419
413
  let binding = findBinding(ctx.scopedBindings, node.text, node);
@@ -421,35 +415,47 @@ function visitComputedArg(ctx: TransformContext, node: ts.Node): ts.Node {
421
415
  if (binding) {
422
416
  ctx.neededImports.add('read');
423
417
 
424
- return createReadCall(ctx.factory, ctx.ns.reactivity, ctx.factory.createIdentifier(node.text));
418
+ ctx.innerReplacements.push({
419
+ end: node.end - ctx.argStart,
420
+ newText: `read(${node.text})`,
421
+ start: node.getStart(ctx.sourceFile) - ctx.argStart
422
+ });
425
423
  }
426
424
  }
427
425
 
428
- return ts.visitEachChild(node, n => visitComputedArg(ctx, n), ctx.context);
426
+ ts.forEachChild(node, n => visitArg(ctx, n));
429
427
  }
430
428
 
431
429
 
432
- const createPrimitivesTransformer = (
433
- bindings: Bindings,
434
- neededImports: Set<string>,
435
- ns: Namespaces
436
- ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile => {
437
- return (context: ts.TransformationContext) => {
438
- return (sourceFile: ts.SourceFile): ts.SourceFile => {
439
- let ctx: TransformContext = {
440
- bindings,
441
- context,
442
- factory: context.factory,
443
- hasReactiveImport: false,
444
- neededImports,
445
- ns,
446
- scopedBindings: []
447
- };
448
-
449
- return ts.visitNode(sourceFile, n => visit(ctx, n)) as ts.SourceFile;
430
+ const transformReactivePrimitives = (
431
+ sourceFile: ts.SourceFile,
432
+ bindings: Bindings
433
+ ): string => {
434
+ let code = sourceFile.getFullText(),
435
+ ctx: TransformContext = {
436
+ bindings,
437
+ computedArgRanges: [],
438
+ hasReactiveImport: false,
439
+ neededImports: new Set<string>(),
440
+ replacements: [],
441
+ scopedBindings: [],
442
+ sourceFile
450
443
  };
451
- };
444
+
445
+ visit(ctx, sourceFile);
446
+
447
+ if (ctx.replacements.length === 0) {
448
+ return code;
449
+ }
450
+
451
+ let result = applyReplacements(code, ctx.replacements);
452
+
453
+ if (ctx.neededImports.size > 0) {
454
+ result = addMissingImports(result, ctx.neededImports);
455
+ }
456
+
457
+ return result;
452
458
  };
453
459
 
454
460
 
455
- export { createPrimitivesTransformer };
461
+ export { transformReactivePrimitives };
@@ -0,0 +1,45 @@
1
+ import { addImport, applyReplacements, type Replacement } from '@esportsplus/typescript/transformer';
2
+
3
+
4
+ type ExtraImport = {
5
+ module: string;
6
+ specifier: string;
7
+ };
8
+
9
+
10
+ const addMissingImports = (code: string, needed: Set<string>, extraImports?: ExtraImport[]): string => {
11
+ let extraSpecifiers = new Set<string>(),
12
+ reactivitySpecifiers: string[] = [];
13
+
14
+ if (extraImports) {
15
+ for (let i = 0, n = extraImports.length; i < n; i++) {
16
+ extraSpecifiers.add(extraImports[i].specifier);
17
+ }
18
+ }
19
+
20
+ for (let imp of needed) {
21
+ if (!extraSpecifiers.has(imp)) {
22
+ reactivitySpecifiers.push(imp);
23
+ }
24
+ }
25
+
26
+ if (reactivitySpecifiers.length > 0) {
27
+ code = addImport(code, '@esportsplus/reactivity', reactivitySpecifiers);
28
+ }
29
+
30
+ if (extraImports) {
31
+ for (let i = 0, n = extraImports.length; i < n; i++) {
32
+ let extra = extraImports[i];
33
+
34
+ if (needed.has(extra.specifier)) {
35
+ code = addImport(code, extra.module, [extra.specifier]);
36
+ }
37
+ }
38
+ }
39
+
40
+ return code;
41
+ };
42
+
43
+
44
+ export { addMissingImports, applyReplacements };
45
+ export type { ExtraImport, Replacement };