@esportsplus/reactivity 0.24.5 → 0.25.1

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,13 +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
- // Skip import declarations - don't visit their children
62
- if (ts.isImportDeclaration(node)) {
63
- return node;
64
- }
65
-
66
- // Track array bindings from variable declarations
56
+ function visit(ctx: TransformContext, node: ts.Node): void {
67
57
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
68
58
  if (ts.isIdentifier(node.initializer) && ctx.bindings.get(node.initializer.text) === 'array') {
69
59
  ctx.bindings.set(node.name.text, 'array');
@@ -78,7 +68,6 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
78
68
  }
79
69
  }
80
70
 
81
- // Track array bindings from function parameters with ReactiveArray type
82
71
  if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node)) && node.parameters) {
83
72
  for (let i = 0, n = node.parameters.length; i < n; i++) {
84
73
  let param = node.parameters[i];
@@ -94,7 +83,6 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
94
83
  }
95
84
  }
96
85
 
97
- // Transform array.length → array.$length()
98
86
  if (
99
87
  ts.isPropertyAccessExpression(node) &&
100
88
  node.name.text === 'length' &&
@@ -103,14 +91,16 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
103
91
  let name = getExpressionName(node.expression);
104
92
 
105
93
  if (name && ctx.bindings.get(name) === 'array') {
106
- // First visit children to transform the expression if needed
107
- let transformedExpr = ts.visitEachChild(node.expression, n => visit(ctx, n), ctx.context) as ts.Expression;
94
+ let objText = node.expression.getText(ctx.sourceFile);
108
95
 
109
- return createArrayLengthCall(ctx.factory, transformedExpr);
96
+ ctx.replacements.push({
97
+ end: node.end,
98
+ newText: `${objText}.$length()`,
99
+ start: node.pos
100
+ });
110
101
  }
111
102
  }
112
103
 
113
- // Transform array[index] = value → array.$set(index, value)
114
104
  if (
115
105
  ts.isBinaryExpression(node) &&
116
106
  node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
@@ -120,33 +110,34 @@ function visit(ctx: TransformContext, node: ts.Node): ts.Node {
120
110
  objName = getExpressionName(elemAccess.expression);
121
111
 
122
112
  if (objName && ctx.bindings.get(objName) === 'array') {
123
- let transformedArray = ts.visitEachChild(elemAccess.expression, n => visit(ctx, n), ctx.context) as ts.Expression,
124
- transformedIndex = ts.visitEachChild(elemAccess.argumentExpression, n => visit(ctx, n), ctx.context) as ts.Expression,
125
- transformedValue = ts.visitEachChild(node.right, n => visit(ctx, n), ctx.context) as ts.Expression;
126
-
127
- 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
+ });
128
122
  }
129
123
  }
130
124
 
131
- return ts.visitEachChild(node, n => visit(ctx, n), ctx.context);
125
+ ts.forEachChild(node, n => visit(ctx, n));
132
126
  }
133
127
 
134
128
 
135
- const createArrayTransformer = (
136
- bindings: Bindings
137
- ): (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile => {
138
- return (context: ts.TransformationContext) => {
139
- return (sourceFile: ts.SourceFile): ts.SourceFile => {
140
- let ctx: TransformContext = {
141
- bindings,
142
- context,
143
- factory: context.factory
144
- };
145
-
146
- 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
147
135
  };
148
- };
136
+
137
+ visit(ctx, sourceFile);
138
+
139
+ return applyReplacements(code, ctx.replacements);
149
140
  };
150
141
 
151
142
 
152
- export { createArrayTransformer };
143
+ export { transformReactiveArrays };