@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,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,29 +180,58 @@ 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 - return early to avoid visiting import children
193
- if (ts.isImportDeclaration(node)) {
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
+
194
197
  if (
195
- ts.isStringLiteral(node.moduleSpecifier) &&
196
- node.moduleSpecifier.text.includes('@esportsplus/reactivity')
198
+ op === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
199
+ op === ts.SyntaxKind.BarBarEqualsToken ||
200
+ op === ts.SyntaxKind.QuestionQuestionEqualsToken
197
201
  ) {
198
- let clause = node.importClause;
202
+ return 'compound';
203
+ }
204
+ }
199
205
 
200
- if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
201
- for (let i = 0, n = clause.namedBindings.elements.length; i < n; i++) {
202
- if (clause.namedBindings.elements[i].name.text === 'reactive') {
203
- ctx.hasReactiveImport = true;
204
- break;
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 {
218
+ if (
219
+ ts.isImportDeclaration(node) &&
220
+ ts.isStringLiteral(node.moduleSpecifier) &&
221
+ node.moduleSpecifier.text.includes('@esportsplus/reactivity')
222
+ ) {
223
+ let clause = node.importClause;
224
+
225
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
226
+ for (let i = 0, n = clause.namedBindings.elements.length; i < n; i++) {
227
+ if (clause.namedBindings.elements[i].name.text === 'reactive') {
228
+ ctx.hasReactiveImport = true;
229
+ break;
206
230
  }
207
231
  }
208
232
  }
209
-
210
- return node;
211
233
  }
212
234
 
213
- // Transform reactive() calls to signal() or computed()
214
235
  if (
215
236
  ctx.hasReactiveImport &&
216
237
  ts.isCallExpression(node) &&
@@ -224,11 +245,10 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
224
245
  if (classification) {
225
246
  let varName: string | null = null;
226
247
 
227
- if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
248
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
228
249
  varName = node.parent.name.text;
229
250
  }
230
251
  else if (
231
- node.parent &&
232
252
  ts.isBinaryExpression(node.parent) &&
233
253
  node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
234
254
  ts.isIdentifier(node.parent.left)
@@ -239,184 +259,155 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
239
259
  if (varName) {
240
260
  let scope = findEnclosingScope(node);
241
261
 
242
- ctx.bindings.set(varName, classification);
243
262
  ctx.scopedBindings.push({ name: varName, scope, type: classification });
263
+ ctx.bindings.set(varName, classification);
244
264
  }
245
265
 
246
266
  if (classification === 'computed') {
247
- ctx.neededImports.add('computed');
248
-
249
- // Transform the function body to wrap reactive reads
250
- let transformedArg = ts.visitEachChild(arg, n => visitComputedArg(ctx, n), ctx.context);
251
-
252
- return createComputedCall(ctx.factory, ctx.ns.reactivity, transformedArg as ts.Expression);
253
- }
254
- else {
255
- ctx.neededImports.add('signal');
256
-
257
- return createSignalCall(ctx.factory, ctx.ns.reactivity, arg);
258
- }
259
- }
260
- }
261
-
262
- // Handle binary expressions (assignments) with reactive left side
263
- if (ts.isBinaryExpression(node) && ts.isIdentifier(node.left)) {
264
- let assignType = isAssignmentOperator(node.operatorToken.kind);
265
-
266
- if (assignType) {
267
- let binding = findBinding(ctx.scopedBindings, node.left.text, node.left);
267
+ ctx.computedArgRanges.push({
268
+ end: arg.end,
269
+ start: arg.getStart(ctx.sourceFile)
270
+ });
268
271
 
269
- if (binding && binding.type !== 'computed' && !isReactiveReassignment(node)) {
270
- ctx.neededImports.add('set');
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
+ };
271
279
 
272
- let factory = ctx.factory,
273
- name = node.left.text,
274
- signalIdent = factory.createIdentifier(name),
275
- transformedRight = ts.visitEachChild(node.right, n => visit(ctx, n), ctx.context) as ts.Expression;
280
+ visitArg(argCtx, arg);
276
281
 
277
- if (assignType === 'simple') {
278
- // x = value → ns.set(x, value)
279
- return createSetCall(factory, ctx.ns.reactivity, signalIdent, transformedRight);
280
- }
281
- else {
282
- // x += value → ns.set(x, x.value + value)
283
- let op = getCompoundOperator(node.operatorToken.kind),
284
- valueAccess = factory.createPropertyAccessExpression(signalIdent, 'value');
285
-
286
- return createSetCall(
287
- factory,
288
- ctx.ns.reactivity,
289
- signalIdent,
290
- factory.createBinaryExpression(valueAccess, op, transformedRight)
291
- );
292
- }
293
- }
294
- }
295
- }
282
+ let argText = applyReplacements(arg.getText(ctx.sourceFile), argCtx.innerReplacements);
296
283
 
297
- // Handle prefix unary expressions (++x, --x) with reactive operand
298
- if (ts.isPrefixUnaryExpression(node) && ts.isIdentifier(node.operand)) {
299
- let op = node.operator;
284
+ ctx.replacements.push({
285
+ end: node.end,
286
+ newText: `computed(${argText})`,
287
+ start: node.pos
288
+ });
300
289
 
301
- if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken) {
302
- let binding = findBinding(ctx.scopedBindings, node.operand.text, node.operand);
303
-
304
- if (binding && binding.type !== 'computed') {
305
- ctx.neededImports.add('set');
306
-
307
- let delta = op === ts.SyntaxKind.PlusPlusToken ? ts.SyntaxKind.PlusToken : ts.SyntaxKind.MinusToken,
308
- factory = ctx.factory,
309
- name = node.operand.text,
310
- signalIdent = factory.createIdentifier(name),
311
- valueAccess = factory.createPropertyAccessExpression(signalIdent, 'value');
312
-
313
- if (node.parent && ts.isExpressionStatement(node.parent)) {
314
- // ++x as statement → ns.set(x, x.value + 1)
315
- return createSetCall(
316
- factory,
317
- ctx.ns.reactivity,
318
- signalIdent,
319
- factory.createBinaryExpression(valueAccess, delta, factory.createNumericLiteral(1))
320
- );
321
- }
322
- else {
323
- // ++x in expression → (ns.set(x, x.value + 1), x.value)
324
- return createCommaExpr(
325
- factory,
326
- createSetCall(
327
- factory,
328
- ctx.ns.reactivity,
329
- signalIdent,
330
- factory.createBinaryExpression(valueAccess, delta, factory.createNumericLiteral(1))
331
- ),
332
- factory.createPropertyAccessExpression(factory.createIdentifier(name), 'value')
333
- );
334
- }
290
+ ctx.neededImports.add('computed');
335
291
  }
336
- }
337
- }
292
+ else {
293
+ let argText = arg.getText(ctx.sourceFile);
338
294
 
339
- // Handle postfix unary expressions (x++, x--) with reactive operand
340
- if (ts.isPostfixUnaryExpression(node) && ts.isIdentifier(node.operand)) {
341
- let op = node.operator;
295
+ ctx.replacements.push({
296
+ end: node.end,
297
+ newText: `signal(${argText})`,
298
+ start: node.pos
299
+ });
342
300
 
343
- if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken) {
344
- let binding = findBinding(ctx.scopedBindings, node.operand.text, node.operand);
345
-
346
- if (binding && binding.type !== 'computed') {
347
- ctx.neededImports.add('set');
348
-
349
- let delta = op === ts.SyntaxKind.PlusPlusToken ? ts.SyntaxKind.PlusToken : ts.SyntaxKind.MinusToken,
350
- factory = ctx.factory,
351
- name = node.operand.text,
352
- signalIdent = factory.createIdentifier(name),
353
- valueAccess = factory.createPropertyAccessExpression(signalIdent, 'value');
354
-
355
- if (node.parent && ts.isExpressionStatement(node.parent)) {
356
- // x++ as statement → ns.set(x, x.value + 1)
357
- return createSetCall(
358
- factory,
359
- ctx.ns.reactivity,
360
- signalIdent,
361
- factory.createBinaryExpression(valueAccess, delta, factory.createNumericLiteral(1))
362
- );
363
- }
364
- else {
365
- // x++ in expression → ((tmp) => (ns.set(x, tmp + 1), tmp))(x.value)
366
- return createPostfixIncrementExpr(factory, ctx.ns.reactivity, uid('tmp'), name, delta);
367
- }
301
+ ctx.neededImports.add('signal');
368
302
  }
369
303
  }
370
304
  }
371
305
 
372
- // Handle reactive variable reads (not in write context)
373
- if (ts.isIdentifier(node) && node.parent && !isInDeclarationInit(node.parent)) {
374
- // Skip property names in property access expressions
306
+ if (ts.isIdentifier(node) && !isInDeclarationInit(node.parent)) {
375
307
  if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
376
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
377
- }
378
-
379
- // Skip if this identifier is the left side of an assignment (handled above)
380
- if (ts.isBinaryExpression(node.parent) && node.parent.left === node) {
381
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
308
+ ts.forEachChild(node, n => visit(ctx, n));
309
+ return;
382
310
  }
383
311
 
384
- // Skip if this is the operand of a unary expression (handled above)
385
- if (
386
- (ts.isPrefixUnaryExpression(node.parent) || ts.isPostfixUnaryExpression(node.parent)) &&
387
- node.parent.operand === node
388
- ) {
389
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
390
- }
312
+ let nodeStart = node.getStart(ctx.sourceFile);
391
313
 
392
- // Skip typeof checks
393
- if (ts.isTypeOfExpression(node.parent) && node.parent.expression === node) {
394
- 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;
395
317
  }
396
318
 
397
- let binding = findBinding(ctx.scopedBindings, node.text, node);
319
+ let binding = findBinding(ctx.scopedBindings, node.text, node),
320
+ name = node.text;
398
321
 
399
322
  if (binding) {
400
- // Read access → ns.read(x)
401
- 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');
402
387
 
403
- 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
+ }
404
395
  }
405
396
  }
406
397
 
407
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
398
+ ts.forEachChild(node, n => visit(ctx, n));
408
399
  }
409
400
 
410
- function visitComputedArg(ctx: TransformContext, node: ts.Node): ts.Node {
411
- // Skip property names in property access
412
- if (ts.isIdentifier(node) && node.parent) {
401
+ function visitArg(ctx: ArgContext, node: ts.Node): void {
402
+ if (ts.isIdentifier(node)) {
413
403
  if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
414
- return ts.visitEachChild(node, n => visitComputedArg(ctx, n), ctx.context);
404
+ ts.forEachChild(node, n => visitArg(ctx, n));
405
+ return;
415
406
  }
416
407
 
417
- // Skip function call expressions
418
408
  if (ts.isCallExpression(node.parent) && node.parent.expression === node) {
419
- return ts.visitEachChild(node, n => visitComputedArg(ctx, n), ctx.context);
409
+ ts.forEachChild(node, n => visitArg(ctx, n));
410
+ return;
420
411
  }
421
412
 
422
413
  let binding = findBinding(ctx.scopedBindings, node.text, node);
@@ -424,35 +415,47 @@ function visitComputedArg(ctx: TransformContext, node: ts.Node): ts.Node {
424
415
  if (binding) {
425
416
  ctx.neededImports.add('read');
426
417
 
427
- 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
+ });
428
423
  }
429
424
  }
430
425
 
431
- return ts.visitEachChild(node, n => visitComputedArg(ctx, n), ctx.context);
426
+ ts.forEachChild(node, n => visitArg(ctx, n));
432
427
  }
433
428
 
434
429
 
435
- const createPrimitivesTransformer = (
436
- bindings: Bindings,
437
- neededImports: Set<string>,
438
- ns: Namespaces
439
- ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile => {
440
- return (context: ts.TransformationContext) => {
441
- return (sourceFile: ts.SourceFile): ts.SourceFile => {
442
- let ctx: TransformContext = {
443
- bindings,
444
- context,
445
- factory: context.factory,
446
- hasReactiveImport: false,
447
- neededImports,
448
- ns,
449
- scopedBindings: []
450
- };
451
-
452
- 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
453
443
  };
454
- };
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;
455
458
  };
456
459
 
457
460
 
458
- export { createPrimitivesTransformer };
461
+ export { transformReactivePrimitives };