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