@esportsplus/reactivity 0.24.1 → 0.24.2

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,67 +1,193 @@
1
- import type { Bindings, TransformResult } from '~/types';
1
+ import type { Bindings } from '~/types';
2
+ import { createArrayTransformer } from './transforms/array';
3
+ import { createObjectTransformer, type GeneratedClass } from './transforms/object';
4
+ import { createPrimitivesTransformer } from './transforms/primitives';
2
5
  import { mightNeedTransform } from './detector';
3
- import { transformReactiveArrays } from './transforms/array';
4
- import { transformReactiveObjects } from './transforms/object';
5
- import { transformReactivePrimitives } from './transforms/primitives';
6
6
  import { ts } from '@esportsplus/typescript';
7
7
 
8
8
 
9
- const createTransformer = (): ts.TransformerFactory<ts.SourceFile> => {
10
- return () => {
9
+ interface ExtraImport {
10
+ module: string;
11
+ specifier: string;
12
+ }
13
+
14
+ const EXTRA_IMPORTS: ExtraImport[] = [
15
+ { module: '@esportsplus/reactivity/constants', specifier: 'REACTIVE_OBJECT' },
16
+ { module: '@esportsplus/reactivity/reactive/array', specifier: 'ReactiveArray' }
17
+ ];
18
+
19
+
20
+ function addImportsTransformer(
21
+ neededImports: Set<string>,
22
+ extraImports: ExtraImport[]
23
+ ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
24
+ return (context: ts.TransformationContext) => {
11
25
  return (sourceFile: ts.SourceFile): ts.SourceFile => {
12
- let result = transform(sourceFile);
26
+ if (neededImports.size === 0) {
27
+ return sourceFile;
28
+ }
29
+
30
+ let extraSpecifiers = new Set<string>(),
31
+ factory = context.factory,
32
+ newStatements: ts.Statement[] = [],
33
+ reactivitySpecifiers: string[] = [];
34
+
35
+ for (let i = 0, n = extraImports.length; i < n; i++) {
36
+ extraSpecifiers.add(extraImports[i].specifier);
37
+ }
38
+
39
+ for (let imp of neededImports) {
40
+ if (!extraSpecifiers.has(imp)) {
41
+ reactivitySpecifiers.push(imp);
42
+ }
43
+ }
44
+
45
+ // Add @esportsplus/reactivity imports
46
+ if (reactivitySpecifiers.length > 0) {
47
+ newStatements.push(
48
+ factory.createImportDeclaration(
49
+ undefined,
50
+ factory.createImportClause(
51
+ false,
52
+ undefined,
53
+ factory.createNamedImports(
54
+ reactivitySpecifiers.map(s =>
55
+ factory.createImportSpecifier(false, undefined, factory.createIdentifier(s))
56
+ )
57
+ )
58
+ ),
59
+ factory.createStringLiteral('@esportsplus/reactivity')
60
+ )
61
+ );
62
+ }
13
63
 
14
- return result.transformed ? result.sourceFile : sourceFile;
64
+ // Add extra imports (REACTIVE_OBJECT, ReactiveArray)
65
+ for (let i = 0, n = extraImports.length; i < n; i++) {
66
+ let extra = extraImports[i];
67
+
68
+ if (neededImports.has(extra.specifier)) {
69
+ newStatements.push(
70
+ factory.createImportDeclaration(
71
+ undefined,
72
+ factory.createImportClause(
73
+ false,
74
+ undefined,
75
+ factory.createNamedImports([
76
+ factory.createImportSpecifier(false, undefined, factory.createIdentifier(extra.specifier))
77
+ ])
78
+ ),
79
+ factory.createStringLiteral(extra.module)
80
+ )
81
+ );
82
+ }
83
+ }
84
+
85
+ // Insert new imports after existing imports
86
+ let insertIndex = 0,
87
+ statements = sourceFile.statements;
88
+
89
+ for (let i = 0, n = statements.length; i < n; i++) {
90
+ if (ts.isImportDeclaration(statements[i])) {
91
+ insertIndex = i + 1;
92
+ }
93
+ else {
94
+ break;
95
+ }
96
+ }
97
+
98
+ let updatedStatements = [
99
+ ...statements.slice(0, insertIndex),
100
+ ...newStatements,
101
+ ...statements.slice(insertIndex)
102
+ ];
103
+
104
+ return factory.updateSourceFile(sourceFile, updatedStatements);
15
105
  };
16
106
  };
17
- };
107
+ }
18
108
 
19
- const transform = (sourceFile: ts.SourceFile): TransformResult => {
20
- let bindings: Bindings = new Map(),
21
- code = sourceFile.getFullText(),
22
- current = sourceFile,
23
- original = code,
24
- result: string;
109
+ function insertClassesTransformer(
110
+ generatedClasses: GeneratedClass[]
111
+ ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
112
+ return (context: ts.TransformationContext) => {
113
+ return (sourceFile: ts.SourceFile): ts.SourceFile => {
114
+ if (generatedClasses.length === 0) {
115
+ return sourceFile;
116
+ }
25
117
 
26
- if (!mightNeedTransform(code)) {
27
- return { code, sourceFile, transformed: false };
28
- }
118
+ let factory = context.factory;
29
119
 
30
- // Run all transforms, only re-parse between transforms if code changed
31
- result = transformReactiveObjects(current, bindings);
120
+ // Find position after imports
121
+ let insertIndex = 0,
122
+ statements = sourceFile.statements;
32
123
 
33
- if (result !== code) {
34
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
35
- code = result;
36
- }
124
+ for (let i = 0, n = statements.length; i < n; i++) {
125
+ if (ts.isImportDeclaration(statements[i])) {
126
+ insertIndex = i + 1;
127
+ }
128
+ else {
129
+ break;
130
+ }
131
+ }
37
132
 
38
- result = transformReactiveArrays(current, bindings);
133
+ let classDecls = generatedClasses.map(gc => gc.classDecl),
134
+ updatedStatements = [
135
+ ...statements.slice(0, insertIndex),
136
+ ...classDecls,
137
+ ...statements.slice(insertIndex)
138
+ ];
39
139
 
40
- if (result !== code) {
41
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
42
- code = result;
43
- }
140
+ return factory.updateSourceFile(sourceFile, updatedStatements);
141
+ };
142
+ };
143
+ }
144
+
145
+
146
+ const createTransformer = (): ts.TransformerFactory<ts.SourceFile> => {
147
+ return (context: ts.TransformationContext) => {
148
+ return (sourceFile: ts.SourceFile): ts.SourceFile => {
149
+ let code = sourceFile.getFullText();
150
+
151
+ if (!mightNeedTransform(code)) {
152
+ return sourceFile;
153
+ }
154
+
155
+ let bindings: Bindings = new Map(),
156
+ generatedClasses: GeneratedClass[] = [],
157
+ neededImports = new Set<string>();
158
+
159
+ // Run object transformer first (generates classes, tracks array bindings)
160
+ let objectTransformer = createObjectTransformer(bindings, neededImports, generatedClasses)(context);
44
161
 
45
- result = transformReactivePrimitives(current, bindings);
162
+ sourceFile = objectTransformer(sourceFile);
46
163
 
47
- if (result !== code) {
48
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
49
- code = result;
50
- }
164
+ // Run array transformer (handles array.length, array[i] = v)
165
+ let arrayTransformer = createArrayTransformer(bindings)(context);
51
166
 
52
- if (code === original) {
53
- return { code, sourceFile, transformed: false };
54
- }
167
+ sourceFile = arrayTransformer(sourceFile);
55
168
 
56
- return {
57
- code,
58
- sourceFile: current,
59
- transformed: true
169
+ // Run primitives transformer (handles signal/computed, reads/writes)
170
+ let primitivesTransformer = createPrimitivesTransformer(bindings, neededImports)(context);
171
+
172
+ sourceFile = primitivesTransformer(sourceFile);
173
+
174
+ // Insert generated classes after imports
175
+ let classInserter = insertClassesTransformer(generatedClasses)(context);
176
+
177
+ sourceFile = classInserter(sourceFile);
178
+
179
+ // Add missing imports
180
+ let importAdder = addImportsTransformer(neededImports, EXTRA_IMPORTS)(context);
181
+
182
+ sourceFile = importAdder(sourceFile);
183
+
184
+ return sourceFile;
185
+ };
60
186
  };
61
187
  };
62
188
 
63
189
 
64
- export { createTransformer, mightNeedTransform, transform };
65
- export { transformReactiveArrays } from './transforms/array';
66
- export { transformReactiveObjects } from './transforms/object';
67
- export { transformReactivePrimitives } from './transforms/primitives';
190
+ export { createTransformer, mightNeedTransform };
191
+ export { createArrayTransformer } from './transforms/array';
192
+ export { createObjectTransformer } from './transforms/object';
193
+ export { createPrimitivesTransformer } from './transforms/primitives';
@@ -1,5 +1,5 @@
1
1
  import { TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
2
- import { mightNeedTransform, transform } from '~/transformer';
2
+ import { createTransformer, mightNeedTransform } from '~/transformer';
3
3
  import type { Plugin } from 'vite';
4
4
  import { ts } from '@esportsplus/typescript';
5
5
 
@@ -19,14 +19,22 @@ export default (): Plugin => {
19
19
  }
20
20
 
21
21
  try {
22
- let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true),
23
- result = transform(sourceFile);
22
+ let printer = ts.createPrinter(),
23
+ sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true),
24
+ transformer = createTransformer(),
25
+ result = ts.transform(sourceFile, [transformer]),
26
+ transformed = result.transformed[0];
24
27
 
25
- if (!result.transformed) {
28
+ if (transformed === sourceFile) {
29
+ result.dispose();
26
30
  return null;
27
31
  }
28
32
 
29
- return { code: result.code, map: null };
33
+ let output = printer.printFile(transformed);
34
+
35
+ result.dispose();
36
+
37
+ return { code: output, map: null };
30
38
  }
31
39
  catch (error) {
32
40
  console.error(`@esportsplus/reactivity: Error transforming ${id}:`, error);
@@ -1,12 +1,12 @@
1
1
  import type { Bindings } from '~/types';
2
- import { applyReplacements, Replacement } from './utilities';
2
+ import { createArrayLengthCall, createArraySetCall } from '../factory';
3
3
  import { ts } from '@esportsplus/typescript';
4
4
 
5
5
 
6
6
  interface TransformContext {
7
7
  bindings: Bindings;
8
- replacements: Replacement[];
9
- sourceFile: ts.SourceFile;
8
+ context: ts.TransformationContext;
9
+ factory: ts.NodeFactory;
10
10
  }
11
11
 
12
12
 
@@ -42,6 +42,10 @@ function getPropertyPath(node: ts.PropertyAccessExpression): string | null {
42
42
  function isAssignmentTarget(node: ts.Node): boolean {
43
43
  let parent = node.parent;
44
44
 
45
+ if (!parent) {
46
+ return false;
47
+ }
48
+
45
49
  if (
46
50
  (ts.isBinaryExpression(parent) && parent.left === node) ||
47
51
  ts.isPostfixUnaryExpression(parent) ||
@@ -53,7 +57,8 @@ function isAssignmentTarget(node: ts.Node): boolean {
53
57
  return false;
54
58
  }
55
59
 
56
- function visit(ctx: TransformContext, node: ts.Node): void {
60
+ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
61
+ // Track array bindings from variable declarations
57
62
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
58
63
  if (ts.isIdentifier(node.initializer) && ctx.bindings.get(node.initializer.text) === 'array') {
59
64
  ctx.bindings.set(node.name.text, 'array');
@@ -68,6 +73,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
68
73
  }
69
74
  }
70
75
 
76
+ // Track array bindings from function parameters with ReactiveArray type
71
77
  if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node)) && node.parameters) {
72
78
  for (let i = 0, n = node.parameters.length; i < n; i++) {
73
79
  let param = node.parameters[i];
@@ -83,6 +89,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
83
89
  }
84
90
  }
85
91
 
92
+ // Transform array.length → array.$length()
86
93
  if (
87
94
  ts.isPropertyAccessExpression(node) &&
88
95
  node.name.text === 'length' &&
@@ -91,16 +98,14 @@ function visit(ctx: TransformContext, node: ts.Node): void {
91
98
  let name = getExpressionName(node.expression);
92
99
 
93
100
  if (name && ctx.bindings.get(name) === 'array') {
94
- let objText = node.expression.getText(ctx.sourceFile);
101
+ // First visit children to transform the expression if needed
102
+ let transformedExpr = ts.visitEachChild(node.expression, n => visit(ctx, n), ctx.context) as ts.Expression;
95
103
 
96
- ctx.replacements.push({
97
- end: node.end,
98
- newText: `${objText}.$length()`,
99
- start: node.pos
100
- });
104
+ return createArrayLengthCall(ctx.factory, transformedExpr);
101
105
  }
102
106
  }
103
107
 
108
+ // Transform array[index] = value → array.$set(index, value)
104
109
  if (
105
110
  ts.isBinaryExpression(node) &&
106
111
  node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
@@ -110,34 +115,33 @@ function visit(ctx: TransformContext, node: ts.Node): void {
110
115
  objName = getExpressionName(elemAccess.expression);
111
116
 
112
117
  if (objName && ctx.bindings.get(objName) === 'array') {
113
- let indexText = elemAccess.argumentExpression.getText(ctx.sourceFile),
114
- objText = elemAccess.expression.getText(ctx.sourceFile),
115
- valueText = node.right.getText(ctx.sourceFile);
116
-
117
- ctx.replacements.push({
118
- end: node.end,
119
- newText: `${objText}.$set(${indexText}, ${valueText})`,
120
- start: node.pos
121
- });
118
+ let transformedArray = ts.visitEachChild(elemAccess.expression, n => visit(ctx, n), ctx.context) as ts.Expression,
119
+ transformedIndex = ts.visitEachChild(elemAccess.argumentExpression, n => visit(ctx, n), ctx.context) as ts.Expression,
120
+ transformedValue = ts.visitEachChild(node.right, n => visit(ctx, n), ctx.context) as ts.Expression;
121
+
122
+ return createArraySetCall(ctx.factory, transformedArray, transformedIndex, transformedValue);
122
123
  }
123
124
  }
124
125
 
125
- ts.forEachChild(node, n => visit(ctx, n));
126
+ return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
126
127
  }
127
128
 
128
129
 
129
- const transformReactiveArrays = (sourceFile: ts.SourceFile, bindings: Bindings): string => {
130
- let code = sourceFile.getFullText(),
131
- ctx: TransformContext = {
132
- bindings,
133
- replacements: [],
134
- sourceFile
135
- };
136
-
137
- visit(ctx, sourceFile);
130
+ const createArrayTransformer = (
131
+ bindings: Bindings
132
+ ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile => {
133
+ return (context: ts.TransformationContext) => {
134
+ return (sourceFile: ts.SourceFile): ts.SourceFile => {
135
+ let ctx: TransformContext = {
136
+ bindings,
137
+ context,
138
+ factory: context.factory
139
+ };
138
140
 
139
- return applyReplacements(code, ctx.replacements);
141
+ return ts.visitNode(sourceFile, n => visit(ctx, n)) as ts.SourceFile;
142
+ };
143
+ };
140
144
  };
141
145
 
142
146
 
143
- export { transformReactiveArrays };
147
+ export { createArrayTransformer };