@esportsplus/reactivity 0.24.1 → 0.24.3

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,194 @@
1
- import type { Bindings, TransformResult } from '~/types';
1
+ import { uid } from '@esportsplus/typescript/transformer';
2
+ import type { Bindings, Namespaces } from '~/types';
3
+ import { createArrayTransformer } from './transforms/array';
4
+ import { createObjectTransformer, type GeneratedClass } from './transforms/object';
5
+ import { createPrimitivesTransformer } from './transforms/primitives';
2
6
  import { mightNeedTransform } from './detector';
3
- import { transformReactiveArrays } from './transforms/array';
4
- import { transformReactiveObjects } from './transforms/object';
5
- import { transformReactivePrimitives } from './transforms/primitives';
6
7
  import { ts } from '@esportsplus/typescript';
7
8
 
8
9
 
9
- const createTransformer = (): ts.TransformerFactory<ts.SourceFile> => {
10
- return () => {
10
+ function addImportsTransformer(
11
+ neededImports: Set<string>,
12
+ ns: Namespaces
13
+ ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
14
+ return (context: ts.TransformationContext) => {
11
15
  return (sourceFile: ts.SourceFile): ts.SourceFile => {
12
- let result = transform(sourceFile);
16
+ if (neededImports.size === 0) {
17
+ return sourceFile;
18
+ }
19
+
20
+ let factory = context.factory,
21
+ needsArray = false,
22
+ needsConstants = false,
23
+ needsReactivity = false,
24
+ newStatements: ts.Statement[] = [];
25
+
26
+ for (let imp of neededImports) {
27
+ if (imp === 'ReactiveArray') {
28
+ needsArray = true;
29
+ }
30
+ else if (imp === 'REACTIVE_OBJECT') {
31
+ needsConstants = true;
32
+ }
33
+ else {
34
+ needsReactivity = true;
35
+ }
36
+ }
37
+
38
+ // Add namespace imports
39
+ if (needsReactivity) {
40
+ newStatements.push(
41
+ factory.createImportDeclaration(
42
+ undefined,
43
+ factory.createImportClause(
44
+ false,
45
+ undefined,
46
+ factory.createNamespaceImport(factory.createIdentifier(ns.reactivity))
47
+ ),
48
+ factory.createStringLiteral('@esportsplus/reactivity')
49
+ )
50
+ );
51
+ }
52
+
53
+ if (needsArray) {
54
+ newStatements.push(
55
+ factory.createImportDeclaration(
56
+ undefined,
57
+ factory.createImportClause(
58
+ false,
59
+ undefined,
60
+ factory.createNamespaceImport(factory.createIdentifier(ns.array))
61
+ ),
62
+ factory.createStringLiteral('@esportsplus/reactivity/reactive/array')
63
+ )
64
+ );
65
+ }
66
+
67
+ if (needsConstants) {
68
+ newStatements.push(
69
+ factory.createImportDeclaration(
70
+ undefined,
71
+ factory.createImportClause(
72
+ false,
73
+ undefined,
74
+ factory.createNamespaceImport(factory.createIdentifier(ns.constants))
75
+ ),
76
+ factory.createStringLiteral('@esportsplus/reactivity/constants')
77
+ )
78
+ );
79
+ }
80
+
81
+ // Insert new imports after existing imports
82
+ let insertIndex = 0,
83
+ statements = sourceFile.statements;
13
84
 
14
- return result.transformed ? result.sourceFile : sourceFile;
85
+ for (let i = 0, n = statements.length; i < n; i++) {
86
+ if (ts.isImportDeclaration(statements[i])) {
87
+ insertIndex = i + 1;
88
+ }
89
+ else {
90
+ break;
91
+ }
92
+ }
93
+
94
+ let updatedStatements = [
95
+ ...statements.slice(0, insertIndex),
96
+ ...newStatements,
97
+ ...statements.slice(insertIndex)
98
+ ];
99
+
100
+ return factory.updateSourceFile(sourceFile, updatedStatements);
15
101
  };
16
102
  };
17
- };
103
+ }
18
104
 
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;
105
+ function insertClassesTransformer(
106
+ generatedClasses: GeneratedClass[]
107
+ ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
108
+ return (context: ts.TransformationContext) => {
109
+ return (sourceFile: ts.SourceFile): ts.SourceFile => {
110
+ if (generatedClasses.length === 0) {
111
+ return sourceFile;
112
+ }
25
113
 
26
- if (!mightNeedTransform(code)) {
27
- return { code, sourceFile, transformed: false };
28
- }
114
+ let factory = context.factory;
29
115
 
30
- // Run all transforms, only re-parse between transforms if code changed
31
- result = transformReactiveObjects(current, bindings);
116
+ // Find position after imports
117
+ let insertIndex = 0,
118
+ statements = sourceFile.statements;
32
119
 
33
- if (result !== code) {
34
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
35
- code = result;
36
- }
120
+ for (let i = 0, n = statements.length; i < n; i++) {
121
+ if (ts.isImportDeclaration(statements[i])) {
122
+ insertIndex = i + 1;
123
+ }
124
+ else {
125
+ break;
126
+ }
127
+ }
37
128
 
38
- result = transformReactiveArrays(current, bindings);
129
+ let classDecls = generatedClasses.map(gc => gc.classDecl),
130
+ updatedStatements = [
131
+ ...statements.slice(0, insertIndex),
132
+ ...classDecls,
133
+ ...statements.slice(insertIndex)
134
+ ];
39
135
 
40
- if (result !== code) {
41
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
42
- code = result;
43
- }
136
+ return factory.updateSourceFile(sourceFile, updatedStatements);
137
+ };
138
+ };
139
+ }
44
140
 
45
- result = transformReactivePrimitives(current, bindings);
46
141
 
47
- if (result !== code) {
48
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
49
- code = result;
50
- }
142
+ const createTransformer = (): ts.TransformerFactory<ts.SourceFile> => {
143
+ return (context: ts.TransformationContext) => {
144
+ return (sourceFile: ts.SourceFile): ts.SourceFile => {
145
+ let code = sourceFile.getFullText();
146
+
147
+ if (!mightNeedTransform(code)) {
148
+ return sourceFile;
149
+ }
150
+
151
+ let bindings: Bindings = new Map(),
152
+ generatedClasses: GeneratedClass[] = [],
153
+ neededImports = new Set<string>(),
154
+ ns: Namespaces = {
155
+ array: uid('ra'),
156
+ constants: uid('rc'),
157
+ reactivity: uid('r')
158
+ };
159
+
160
+ // Run object transformer first (generates classes, tracks array bindings)
161
+ let objectTransformer = createObjectTransformer(bindings, neededImports, generatedClasses, ns)(context);
162
+
163
+ sourceFile = objectTransformer(sourceFile);
164
+
165
+ // Run array transformer (handles array.length, array[i] = v)
166
+ let arrayTransformer = createArrayTransformer(bindings)(context);
167
+
168
+ sourceFile = arrayTransformer(sourceFile);
51
169
 
52
- if (code === original) {
53
- return { code, sourceFile, transformed: false };
54
- }
170
+ // Run primitives transformer (handles signal/computed, reads/writes)
171
+ let primitivesTransformer = createPrimitivesTransformer(bindings, neededImports, ns)(context);
55
172
 
56
- return {
57
- code,
58
- sourceFile: current,
59
- transformed: true
173
+ sourceFile = primitivesTransformer(sourceFile);
174
+
175
+ // Insert generated classes after imports
176
+ let classInserter = insertClassesTransformer(generatedClasses)(context);
177
+
178
+ sourceFile = classInserter(sourceFile);
179
+
180
+ // Add namespace imports
181
+ let importAdder = addImportsTransformer(neededImports, ns)(context);
182
+
183
+ sourceFile = importAdder(sourceFile);
184
+
185
+ return sourceFile;
186
+ };
60
187
  };
61
188
  };
62
189
 
63
190
 
64
- export { createTransformer, mightNeedTransform, transform };
65
- export { transformReactiveArrays } from './transforms/array';
66
- export { transformReactiveObjects } from './transforms/object';
67
- export { transformReactivePrimitives } from './transforms/primitives';
191
+ export { createTransformer, mightNeedTransform };
192
+ export { createArrayTransformer } from './transforms/array';
193
+ export { createObjectTransformer } from './transforms/object';
194
+ 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 };