@esportsplus/reactivity 0.27.2 → 0.28.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,4 +1,5 @@
1
+ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
3
  import type { Bindings } from '../types.js';
3
- declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker) => string;
4
+ declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker) => ReplacementIntent[];
4
5
  export default _default;
@@ -1,20 +1,21 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { ast, code as c } from '@esportsplus/typescript/compiler';
2
+ import { ast } from '@esportsplus/typescript/compiler';
3
3
  import { COMPILER_NAMESPACE, COMPILER_TYPES } from '../constants.js';
4
- import { isReactiveCall } from './index.js';
4
+ function isReactiveCall(node) {
5
+ return ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
6
+ }
5
7
  function visit(ctx, node) {
6
- if (ts.isCallExpression(node) && isReactiveCall(node, ctx.checker) && node.arguments.length > 0) {
8
+ if (ts.isCallExpression(node) && isReactiveCall(node) && node.arguments.length > 0) {
7
9
  let arg = node.arguments[0], expression = ts.isAsExpression(arg) ? arg.expression : arg;
8
10
  if (ts.isArrayLiteralExpression(expression)) {
9
11
  if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
10
12
  ctx.bindings.set(node.parent.name.text, COMPILER_TYPES.Array);
11
13
  }
12
14
  ctx.replacements.push({
13
- end: node.end,
14
- newText: expression.elements.length > 0
15
- ? ` new ${COMPILER_NAMESPACE}.ReactiveArray(...${expression.getText(ctx.sourceFile)})`
16
- : ` new ${COMPILER_NAMESPACE}.ReactiveArray()`,
17
- start: node.pos
15
+ node,
16
+ generate: (sf) => expression.elements.length > 0
17
+ ? ` new ${COMPILER_NAMESPACE}.ReactiveArray(...${expression.getText(sf)})`
18
+ : ` new ${COMPILER_NAMESPACE}.ReactiveArray()`
18
19
  });
19
20
  }
20
21
  }
@@ -49,9 +50,8 @@ function visit(ctx, node) {
49
50
  let name = ast.getExpressionName(node.expression);
50
51
  if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
51
52
  ctx.replacements.push({
52
- end: node.end,
53
- newText: `${node.expression.getText(ctx.sourceFile)}.$length()`,
54
- start: node.pos
53
+ node,
54
+ generate: (sf) => `${node.expression.getText(sf)}.$length()`
55
55
  });
56
56
  }
57
57
  }
@@ -60,26 +60,24 @@ function visit(ctx, node) {
60
60
  ts.isElementAccessExpression(node.left)) {
61
61
  let element = node.left, name = ast.getExpressionName(element.expression);
62
62
  if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
63
- let index = element.argumentExpression.getText(ctx.sourceFile), value = node.right.getText(ctx.sourceFile);
64
63
  ctx.replacements.push({
65
- end: node.end,
66
- newText: `${element.expression.getText(ctx.sourceFile)}.$set(${index}, ${value})`,
67
- start: node.pos
64
+ node,
65
+ generate: (sf) => {
66
+ let index = element.argumentExpression.getText(sf), value = node.right.getText(sf);
67
+ return `${element.expression.getText(sf)}.$set(${index}, ${value})`;
68
+ }
68
69
  });
69
70
  }
70
71
  }
71
72
  ts.forEachChild(node, n => visit(ctx, n));
72
73
  }
73
74
  export default (sourceFile, bindings, checker) => {
74
- let code = sourceFile.getFullText(), ctx = {
75
+ let ctx = {
75
76
  bindings,
76
77
  checker,
77
78
  replacements: [],
78
79
  sourceFile
79
80
  };
80
81
  visit(ctx, sourceFile);
81
- if (ctx.replacements.length === 0) {
82
- return code;
83
- }
84
- return c.replace(code, ctx.replacements);
82
+ return ctx.replacements;
85
83
  };
@@ -1,7 +1,3 @@
1
- import type { PluginContext } from '@esportsplus/typescript/compiler';
2
- import { ts } from '@esportsplus/typescript';
3
- import type { TransformResult } from '../types.js';
4
- declare const analyze: (sourceFile: ts.SourceFile, _program: ts.Program, context: PluginContext) => void;
5
- declare const isReactiveCall: (node: ts.CallExpression, _checker?: ts.TypeChecker) => boolean;
6
- declare const transform: (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext) => TransformResult;
7
- export { analyze, isReactiveCall, transform };
1
+ import type { Plugin } from '@esportsplus/typescript/compiler';
2
+ declare const plugin: Plugin;
3
+ export default plugin;
@@ -3,63 +3,40 @@ import { ast, imports } from '@esportsplus/typescript/compiler';
3
3
  import { COMPILER_ENTRYPOINT, COMPILER_NAMESPACE, PACKAGE } from '../constants.js';
4
4
  import array from './array.js';
5
5
  import object from './object.js';
6
- const CONTEXT_KEY = 'reactivity:analyzed';
7
- let transforms = [object, array];
8
- function getAnalyzedFile(context, filename) {
9
- return context?.get(CONTEXT_KEY)?.get(filename);
10
- }
11
6
  function hasReactiveImport(sourceFile) {
12
7
  return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
13
8
  }
14
9
  function isReactiveCallNode(node) {
15
10
  return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === COMPILER_ENTRYPOINT;
16
11
  }
17
- const analyze = (sourceFile, _program, context) => {
18
- if (!hasReactiveImport(sourceFile)) {
19
- return;
20
- }
21
- let files = context.get(CONTEXT_KEY);
22
- if (!files) {
23
- files = new Map();
24
- context.set(CONTEXT_KEY, files);
25
- }
26
- files.set(sourceFile.fileName, {
27
- hasReactiveImport: true
28
- });
29
- };
30
- const isReactiveCall = (node, _checker) => {
31
- if (!ts.isIdentifier(node.expression)) {
32
- return false;
33
- }
34
- return node.expression.text === COMPILER_ENTRYPOINT;
35
- };
36
- const transform = (sourceFile, program, context) => {
37
- let bindings = new Map(), changed = false, checker = program.getTypeChecker(), code = sourceFile.getFullText(), current = sourceFile, filename = sourceFile.fileName, result;
38
- let analyzed = getAnalyzedFile(context, filename);
39
- if (!analyzed) {
40
- if (!hasReactiveImport(sourceFile)) {
41
- return { changed: false, code, sourceFile };
12
+ const plugin = {
13
+ patterns: ['reactive(', 'reactive<'],
14
+ transform: (ctx) => {
15
+ if (!hasReactiveImport(ctx.sourceFile)) {
16
+ return {};
42
17
  }
43
- }
44
- else if (!analyzed.hasReactiveImport) {
45
- return { changed: false, code, sourceFile };
46
- }
47
- for (let i = 0, n = transforms.length; i < n; i++) {
48
- result = transforms[i](current, bindings, checker);
49
- if (result !== code) {
50
- code = result;
51
- changed = true;
52
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
53
- }
54
- }
55
- if (changed) {
56
- let remove = [];
57
- if (!ast.hasMatch(current, isReactiveCallNode)) {
58
- remove.push(COMPILER_ENTRYPOINT);
18
+ let bindings = new Map(), importsIntent = [], prepend = [], replacements = [];
19
+ let objectResult = object(ctx.sourceFile, bindings, ctx.checker);
20
+ prepend.push(...objectResult.prepend);
21
+ replacements.push(...objectResult.replacements);
22
+ let arrayResult = array(ctx.sourceFile, bindings, ctx.checker);
23
+ replacements.push(...arrayResult);
24
+ if (replacements.length > 0 || prepend.length > 0) {
25
+ let remove = [];
26
+ if (!ast.hasMatch(ctx.sourceFile, isReactiveCallNode) || replacements.length > 0) {
27
+ remove.push(COMPILER_ENTRYPOINT);
28
+ }
29
+ importsIntent.push({
30
+ namespace: COMPILER_NAMESPACE,
31
+ package: PACKAGE,
32
+ remove
33
+ });
59
34
  }
60
- code = imports.modify(code, current, PACKAGE, { namespace: COMPILER_NAMESPACE, remove });
61
- sourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
35
+ return {
36
+ imports: importsIntent,
37
+ prepend,
38
+ replacements
39
+ };
62
40
  }
63
- return { changed, code, sourceFile };
64
41
  };
65
- export { analyze, isReactiveCall, transform };
42
+ export default plugin;
@@ -1,4 +1,9 @@
1
+ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
3
  import type { Bindings } from '../types.js';
3
- declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker) => string;
4
+ type ObjectTransformResult = {
5
+ prepend: string[];
6
+ replacements: ReplacementIntent[];
7
+ };
8
+ declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker) => ObjectTransformResult;
4
9
  export default _default;
@@ -1,7 +1,6 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { code as c, uid } from '@esportsplus/typescript/compiler';
2
+ import { uid } from '@esportsplus/typescript/compiler';
3
3
  import { COMPILER_NAMESPACE, COMPILER_TYPES } from '../constants.js';
4
- import { isReactiveCall } from './index.js';
5
4
  function analyzeProperty(prop, sourceFile) {
6
5
  if (!ts.isPropertyAssignment(prop)) {
7
6
  return null;
@@ -111,12 +110,11 @@ function isStaticValue(node) {
111
110
  }
112
111
  return false;
113
112
  }
113
+ function isReactiveCall(node) {
114
+ return ts.isIdentifier(node.expression) && node.expression.text === "reactive";
115
+ }
114
116
  function visit(ctx, node) {
115
- if (ts.isImportDeclaration(node)) {
116
- ctx.lastImportEnd = node.end;
117
- }
118
- if (ts.isCallExpression(node) &&
119
- isReactiveCall(node, ctx.checker)) {
117
+ if (ts.isCallExpression(node) && isReactiveCall(node)) {
120
118
  let arg = node.arguments[0];
121
119
  if (arg && ts.isObjectLiteralExpression(arg)) {
122
120
  let properties = [], props = arg.properties, varName = null;
@@ -142,9 +140,8 @@ function visit(ctx, node) {
142
140
  }
143
141
  ctx.calls.push({
144
142
  className: uid('ReactiveObject'),
145
- end: node.end,
143
+ node,
146
144
  properties,
147
- start: node.pos,
148
145
  varName
149
146
  });
150
147
  }
@@ -152,34 +149,27 @@ function visit(ctx, node) {
152
149
  ts.forEachChild(node, n => visit(ctx, n));
153
150
  }
154
151
  export default (sourceFile, bindings, checker) => {
155
- let code = sourceFile.getFullText(), ctx = {
152
+ let ctx = {
156
153
  bindings,
157
154
  calls: [],
158
155
  checker,
159
- classCounter: 0,
160
- lastImportEnd: 0,
161
156
  sourceFile
162
157
  };
163
158
  visit(ctx, sourceFile);
164
159
  if (ctx.calls.length === 0) {
165
- return code;
160
+ return { prepend: [], replacements: [] };
166
161
  }
167
- let classes = ctx.calls.map(c => buildClassCode(c.className, c.properties)).join('\n'), replacements = [];
168
- replacements.push({
169
- end: ctx.lastImportEnd,
170
- newText: code.substring(0, ctx.lastImportEnd) + '\n' + classes + '\n',
171
- start: 0
172
- });
162
+ let prepend = [], replacements = [];
173
163
  for (let i = 0, n = ctx.calls.length; i < n; i++) {
174
164
  let call = ctx.calls[i];
165
+ prepend.push(buildClassCode(call.className, call.properties));
175
166
  replacements.push({
176
- end: call.end,
177
- newText: ` new ${call.className}(${call.properties
167
+ node: call.node,
168
+ generate: () => ` new ${call.className}(${call.properties
178
169
  .filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
179
170
  .map(p => p.valueText)
180
- .join(', ')})`,
181
- start: call.start
171
+ .join(', ')})`
182
172
  });
183
173
  }
184
- return c.replace(code, replacements);
174
+ return { prepend, replacements };
185
175
  };
@@ -1,3 +1,2 @@
1
- import { plugin } from '@esportsplus/typescript/compiler';
2
- declare const _default: ReturnType<typeof plugin.tsc>;
3
- export default _default;
1
+ import plugin from '../index.js';
2
+ export default plugin;
@@ -1,3 +1,2 @@
1
- import { plugin } from '@esportsplus/typescript/compiler';
2
- import { analyze, transform } from '../index.js';
3
- export default plugin.tsc({ analyze, transform });
1
+ import plugin from '../index.js';
2
+ export default plugin;
@@ -1,13 +1,13 @@
1
1
  declare const _default: ({ root }?: {
2
2
  root?: string;
3
3
  }) => {
4
- configResolved(config: import("vite").ResolvedConfig): void;
5
- enforce: string;
4
+ configResolved: (config: unknown) => void;
5
+ enforce: "pre";
6
6
  name: string;
7
- transform(code: string, id: string): {
7
+ transform: (code: string, id: string) => {
8
8
  code: string;
9
9
  map: null;
10
10
  } | null;
11
- watchChange(id: string): void;
11
+ watchChange: (id: string) => void;
12
12
  };
13
13
  export default _default;
@@ -1,8 +1,7 @@
1
- import { PACKAGE } from '../../constants.js';
2
1
  import { plugin } from '@esportsplus/typescript/compiler';
3
- import { analyze, transform } from '../index.js';
2
+ import { PACKAGE } from '../../constants.js';
3
+ import reactivityPlugin from '../index.js';
4
4
  export default plugin.vite({
5
- analyze,
6
5
  name: PACKAGE,
7
- transform
6
+ plugins: [reactivityPlugin]
8
7
  });
package/package.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "@esportsplus/utilities": "^0.27.2"
5
5
  },
6
6
  "devDependencies": {
7
- "@esportsplus/typescript": "^0.24.1",
7
+ "@esportsplus/typescript": "^0.25.0",
8
8
  "@types/node": "^25.0.3",
9
- "vite": "^7.3.0"
9
+ "vite": "^7.3.1"
10
10
  },
11
11
  "exports": {
12
12
  ".": {
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "type": "module",
33
33
  "types": "build/index.d.ts",
34
- "version": "0.27.2",
34
+ "version": "0.28.0",
35
35
  "scripts": {
36
36
  "build": "tsc",
37
37
  "build:test": "pnpm build && vite build --config test/vite.config.ts",
@@ -1,20 +1,24 @@
1
+ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
- import { ast, code as c, type Replacement } from '@esportsplus/typescript/compiler';
3
+ import { ast } from '@esportsplus/typescript/compiler';
3
4
  import { COMPILER_NAMESPACE, COMPILER_TYPES } from '~/constants';
4
5
  import type { Bindings } from '~/types';
5
- import { isReactiveCall } from '.';
6
6
 
7
7
 
8
- interface TransformContext {
8
+ interface VisitContext {
9
9
  bindings: Bindings;
10
10
  checker?: ts.TypeChecker;
11
- replacements: Replacement[];
11
+ replacements: ReplacementIntent[];
12
12
  sourceFile: ts.SourceFile;
13
13
  }
14
14
 
15
15
 
16
- function visit(ctx: TransformContext, node: ts.Node): void {
17
- if (ts.isCallExpression(node) && isReactiveCall(node, ctx.checker) && node.arguments.length > 0) {
16
+ function isReactiveCall(node: ts.CallExpression): boolean {
17
+ return ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
18
+ }
19
+
20
+ function visit(ctx: VisitContext, node: ts.Node): void {
21
+ if (ts.isCallExpression(node) && isReactiveCall(node) && node.arguments.length > 0) {
18
22
  let arg = node.arguments[0],
19
23
  expression = ts.isAsExpression(arg) ? arg.expression : arg;
20
24
 
@@ -24,11 +28,10 @@ function visit(ctx: TransformContext, node: ts.Node): void {
24
28
  }
25
29
 
26
30
  ctx.replacements.push({
27
- end: node.end,
28
- newText: expression.elements.length > 0
29
- ? ` new ${COMPILER_NAMESPACE}.ReactiveArray(...${expression.getText(ctx.sourceFile)})`
30
- : ` new ${COMPILER_NAMESPACE}.ReactiveArray()`,
31
- start: node.pos
31
+ node,
32
+ generate: (sf) => expression.elements.length > 0
33
+ ? ` new ${COMPILER_NAMESPACE}.ReactiveArray(...${expression.getText(sf)})`
34
+ : ` new ${COMPILER_NAMESPACE}.ReactiveArray()`
32
35
  });
33
36
  }
34
37
  }
@@ -77,9 +80,8 @@ function visit(ctx: TransformContext, node: ts.Node): void {
77
80
 
78
81
  if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
79
82
  ctx.replacements.push({
80
- end: node.end,
81
- newText: `${node.expression.getText(ctx.sourceFile)}.$length()`,
82
- start: node.pos
83
+ node,
84
+ generate: (sf) => `${node.expression.getText(sf)}.$length()`
83
85
  });
84
86
  }
85
87
  }
@@ -93,13 +95,14 @@ function visit(ctx: TransformContext, node: ts.Node): void {
93
95
  name = ast.getExpressionName(element.expression);
94
96
 
95
97
  if (name && ctx.bindings.get(name) === COMPILER_TYPES.Array) {
96
- let index = element.argumentExpression.getText(ctx.sourceFile),
97
- value = node.right.getText(ctx.sourceFile);
98
-
99
98
  ctx.replacements.push({
100
- end: node.end,
101
- newText: `${element.expression.getText(ctx.sourceFile)}.$set(${index}, ${value})`,
102
- start: node.pos
99
+ node,
100
+ generate: (sf) => {
101
+ let index = element.argumentExpression.getText(sf),
102
+ value = node.right.getText(sf);
103
+
104
+ return `${element.expression.getText(sf)}.$set(${index}, ${value})`;
105
+ }
103
106
  });
104
107
  }
105
108
  }
@@ -108,9 +111,8 @@ function visit(ctx: TransformContext, node: ts.Node): void {
108
111
  }
109
112
 
110
113
 
111
- export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): string => {
112
- let code = sourceFile.getFullText(),
113
- ctx: TransformContext = {
114
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): ReplacementIntent[] => {
115
+ let ctx: VisitContext = {
114
116
  bindings,
115
117
  checker,
116
118
  replacements: [],
@@ -119,9 +121,5 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Type
119
121
 
120
122
  visit(ctx, sourceFile);
121
123
 
122
- if (ctx.replacements.length === 0) {
123
- return code;
124
- }
125
-
126
- return c.replace(code, ctx.replacements);
124
+ return ctx.replacements;
127
125
  };
@@ -1,27 +1,12 @@
1
- import type { PluginContext } from '@esportsplus/typescript/compiler';
1
+ import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
3
  import { ast, imports } from '@esportsplus/typescript/compiler';
4
4
  import { COMPILER_ENTRYPOINT, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
5
- import type { Bindings, TransformResult } from '~/types';
5
+ import type { Bindings } from '~/types';
6
6
  import array from './array';
7
7
  import object from './object';
8
8
 
9
9
 
10
- type AnalyzedFile = {
11
- hasReactiveImport: boolean;
12
- };
13
-
14
-
15
- const CONTEXT_KEY = 'reactivity:analyzed';
16
-
17
-
18
- let transforms = [object, array];
19
-
20
-
21
- function getAnalyzedFile(context: PluginContext | undefined, filename: string): AnalyzedFile | undefined {
22
- return (context?.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined)?.get(filename);
23
- }
24
-
25
10
  function hasReactiveImport(sourceFile: ts.SourceFile): boolean {
26
11
  return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
27
12
  }
@@ -31,76 +16,54 @@ function isReactiveCallNode(node: ts.Node): boolean {
31
16
  }
32
17
 
33
18
 
34
- const analyze = (sourceFile: ts.SourceFile, _program: ts.Program, context: PluginContext): void => {
35
- if (!hasReactiveImport(sourceFile)) {
36
- return;
37
- }
38
-
39
- let files = context.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined;
19
+ const plugin: Plugin = {
20
+ patterns: ['reactive(', 'reactive<'],
40
21
 
41
- if (!files) {
42
- files = new Map();
43
- context.set(CONTEXT_KEY, files);
44
- }
22
+ transform: (ctx: TransformContext) => {
23
+ if (!hasReactiveImport(ctx.sourceFile)) {
24
+ return {};
25
+ }
45
26
 
46
- files.set(sourceFile.fileName, {
47
- hasReactiveImport: true
48
- });
49
- };
27
+ let bindings: Bindings = new Map(),
28
+ importsIntent: ImportIntent[] = [],
29
+ prepend: string[] = [],
30
+ replacements: ReplacementIntent[] = [];
50
31
 
51
- const isReactiveCall = (node: ts.CallExpression, _checker?: ts.TypeChecker): boolean => {
52
- if (!ts.isIdentifier(node.expression)) {
53
- return false;
54
- }
32
+ // Run object transform
33
+ let objectResult = object(ctx.sourceFile, bindings, ctx.checker);
55
34
 
56
- return node.expression.text === COMPILER_ENTRYPOINT;
57
- };
35
+ prepend.push(...objectResult.prepend);
36
+ replacements.push(...objectResult.replacements);
58
37
 
59
- const transform = (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext): TransformResult => {
60
- let bindings: Bindings = new Map(),
61
- changed = false,
62
- checker = program.getTypeChecker(),
63
- code = sourceFile.getFullText(),
64
- current = sourceFile,
65
- filename = sourceFile.fileName,
66
- result: string;
67
-
68
- // Try to get pre-analyzed data from context
69
- let analyzed = getAnalyzedFile(context, filename);
70
-
71
- // Fall back to inline check (for Vite or when context unavailable)
72
- if (!analyzed) {
73
- if (!hasReactiveImport(sourceFile)) {
74
- return { changed: false, code, sourceFile };
75
- }
76
- }
77
- else if (!analyzed.hasReactiveImport) {
78
- return { changed: false, code, sourceFile };
79
- }
38
+ // Run array transform
39
+ let arrayResult = array(ctx.sourceFile, bindings, ctx.checker);
80
40
 
81
- for (let i = 0, n = transforms.length; i < n; i++) {
82
- result = transforms[i](current, bindings, checker);
41
+ replacements.push(...arrayResult);
83
42
 
84
- if (result !== code) {
85
- code = result;
86
- changed = true;
87
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
88
- }
89
- }
43
+ // Build import intent
44
+ if (replacements.length > 0 || prepend.length > 0) {
45
+ let remove: string[] = [];
90
46
 
91
- if (changed) {
92
- let remove: string[] = [];
47
+ // Check if we still have reactive() calls after transform
48
+ // This is a heuristic - if we have no replacements for reactive calls, keep the import
49
+ if (!ast.hasMatch(ctx.sourceFile, isReactiveCallNode) || replacements.length > 0) {
50
+ remove.push(COMPILER_ENTRYPOINT);
51
+ }
93
52
 
94
- if (!ast.hasMatch(current, isReactiveCallNode)) {
95
- remove.push(COMPILER_ENTRYPOINT);
53
+ importsIntent.push({
54
+ namespace: COMPILER_NAMESPACE,
55
+ package: PACKAGE,
56
+ remove
57
+ });
96
58
  }
97
59
 
98
- code = imports.modify(code, current, PACKAGE, { namespace: COMPILER_NAMESPACE, remove });
99
- sourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
60
+ return {
61
+ imports: importsIntent,
62
+ prepend,
63
+ replacements
64
+ };
100
65
  }
101
-
102
- return { changed, code, sourceFile };
103
66
  };
104
67
 
105
68
 
106
- export { analyze, isReactiveCall, transform };
69
+ export default plugin;
@@ -1,8 +1,8 @@
1
+ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
- import { code as c, uid, type Replacement } from '@esportsplus/typescript/compiler';
3
+ import { uid } from '@esportsplus/typescript/compiler';
3
4
  import { COMPILER_NAMESPACE, COMPILER_TYPES } from '~/constants';
4
5
  import type { Bindings } from '~/types';
5
- import { isReactiveCall } from '.';
6
6
 
7
7
 
8
8
  interface AnalyzedProperty {
@@ -14,18 +14,15 @@ interface AnalyzedProperty {
14
14
 
15
15
  interface ReactiveObjectCall {
16
16
  className: string;
17
- end: number;
17
+ node: ts.CallExpression;
18
18
  properties: AnalyzedProperty[];
19
- start: number;
20
19
  varName: string | null;
21
20
  }
22
21
 
23
- interface TransformContext {
22
+ interface VisitContext {
24
23
  bindings: Bindings;
25
24
  calls: ReactiveObjectCall[];
26
25
  checker?: ts.TypeChecker;
27
- classCounter: number;
28
- lastImportEnd: number;
29
26
  sourceFile: ts.SourceFile;
30
27
  }
31
28
 
@@ -169,15 +166,12 @@ function isStaticValue(node: ts.Node): boolean {
169
166
  return false;
170
167
  }
171
168
 
172
- function visit(ctx: TransformContext, node: ts.Node): void {
173
- if (ts.isImportDeclaration(node)) {
174
- ctx.lastImportEnd = node.end;
175
- }
169
+ function isReactiveCall(node: ts.CallExpression): boolean {
170
+ return ts.isIdentifier(node.expression) && node.expression.text === "reactive";
171
+ }
176
172
 
177
- if (
178
- ts.isCallExpression(node) &&
179
- isReactiveCall(node, ctx.checker)
180
- ) {
173
+ function visit(ctx: VisitContext, node: ts.Node): void {
174
+ if (ts.isCallExpression(node) && isReactiveCall(node)) {
181
175
  let arg = node.arguments[0];
182
176
 
183
177
  if (arg && ts.isObjectLiteralExpression(arg)) {
@@ -214,9 +208,8 @@ function visit(ctx: TransformContext, node: ts.Node): void {
214
208
 
215
209
  ctx.calls.push({
216
210
  className: uid('ReactiveObject'),
217
- end: node.end,
211
+ node,
218
212
  properties,
219
- start: node.pos,
220
213
  varName
221
214
  });
222
215
  }
@@ -226,46 +219,44 @@ function visit(ctx: TransformContext, node: ts.Node): void {
226
219
  }
227
220
 
228
221
 
229
- export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): string => {
230
- let code = sourceFile.getFullText(),
231
- ctx: TransformContext = {
222
+ type ObjectTransformResult = {
223
+ prepend: string[];
224
+ replacements: ReplacementIntent[];
225
+ };
226
+
227
+
228
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): ObjectTransformResult => {
229
+ let ctx: VisitContext = {
232
230
  bindings,
233
231
  calls: [],
234
232
  checker,
235
- classCounter: 0,
236
- lastImportEnd: 0,
237
233
  sourceFile
238
234
  };
239
235
 
240
236
  visit(ctx, sourceFile);
241
237
 
242
238
  if (ctx.calls.length === 0) {
243
- return code;
239
+ return { prepend: [], replacements: [] };
244
240
  }
245
241
 
246
- let classes = ctx.calls.map(c => buildClassCode(c.className, c.properties)).join('\n'),
247
- replacements: Replacement[] = [];
248
-
249
- replacements.push({
250
- end: ctx.lastImportEnd,
251
- newText: code.substring(0, ctx.lastImportEnd) + '\n' + classes + '\n',
252
- start: 0
253
- });
242
+ let prepend: string[] = [],
243
+ replacements: ReplacementIntent[] = [];
254
244
 
255
245
  for (let i = 0, n = ctx.calls.length; i < n; i++) {
256
246
  let call = ctx.calls[i];
257
247
 
248
+ prepend.push(buildClassCode(call.className, call.properties));
249
+
258
250
  replacements.push({
259
- end: call.end,
260
- newText: ` new ${call.className}(${
251
+ node: call.node,
252
+ generate: () => ` new ${call.className}(${
261
253
  call.properties
262
254
  .filter(({ isStatic, type }) => !isStatic || type === COMPILER_TYPES.Computed)
263
255
  .map(p => p.valueText)
264
256
  .join(', ')
265
- })`,
266
- start: call.start
257
+ })`
267
258
  });
268
259
  }
269
260
 
270
- return c.replace(code, replacements);
261
+ return { prepend, replacements };
271
262
  };
@@ -1,5 +1,4 @@
1
- import { plugin } from '@esportsplus/typescript/compiler';
2
- import { analyze, transform } from '..';
1
+ import plugin from '..';
3
2
 
4
3
 
5
- export default plugin.tsc({ analyze, transform }) as ReturnType<typeof plugin.tsc>;
4
+ export default plugin;
@@ -1,10 +1,9 @@
1
- import { PACKAGE } from '../../constants';
2
1
  import { plugin } from '@esportsplus/typescript/compiler';
3
- import { analyze, transform } from '..';
2
+ import { PACKAGE } from '~/constants';
3
+ import reactivityPlugin from '..';
4
4
 
5
5
 
6
6
  export default plugin.vite({
7
- analyze,
8
7
  name: PACKAGE,
9
- transform
10
- });
8
+ plugins: [reactivityPlugin]
9
+ });