@esportsplus/reactivity 0.28.0 → 0.28.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,6 +1,19 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import { ast } from '@esportsplus/typescript/compiler';
3
3
  import { COMPILER_NAMESPACE, COMPILER_TYPES } from '../constants.js';
4
+ function getElementTypeText(typeNode, sourceFile) {
5
+ if (ts.isArrayTypeNode(typeNode)) {
6
+ return typeNode.elementType.getText(sourceFile);
7
+ }
8
+ if (ts.isTypeReferenceNode(typeNode) &&
9
+ ts.isIdentifier(typeNode.typeName) &&
10
+ typeNode.typeName.text === 'Array' &&
11
+ typeNode.typeArguments &&
12
+ typeNode.typeArguments.length > 0) {
13
+ return typeNode.typeArguments[0].getText(sourceFile);
14
+ }
15
+ return null;
16
+ }
4
17
  function isReactiveCall(node) {
5
18
  return ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
6
19
  }
@@ -8,14 +21,22 @@ function visit(ctx, node) {
8
21
  if (ts.isCallExpression(node) && isReactiveCall(node) && node.arguments.length > 0) {
9
22
  let arg = node.arguments[0], expression = ts.isAsExpression(arg) ? arg.expression : arg;
10
23
  if (ts.isArrayLiteralExpression(expression)) {
24
+ let elementType = null;
25
+ if (ts.isAsExpression(arg) && arg.type) {
26
+ elementType = getElementTypeText(arg.type, ctx.sourceFile);
27
+ }
28
+ else if (node.parent && ts.isVariableDeclaration(node.parent) && node.parent.type) {
29
+ elementType = getElementTypeText(node.parent.type, ctx.sourceFile);
30
+ }
11
31
  if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
12
32
  ctx.bindings.set(node.parent.name.text, COMPILER_TYPES.Array);
13
33
  }
34
+ let typeParam = elementType ? `<${elementType}>` : '';
14
35
  ctx.replacements.push({
15
36
  node,
16
37
  generate: (sf) => expression.elements.length > 0
17
- ? ` new ${COMPILER_NAMESPACE}.ReactiveArray(...${expression.getText(sf)})`
18
- : ` new ${COMPILER_NAMESPACE}.ReactiveArray()`
38
+ ? ` new ${COMPILER_NAMESPACE}.ReactiveArray${typeParam}(...${expression.getText(sf)})`
39
+ : ` new ${COMPILER_NAMESPACE}.ReactiveArray${typeParam}()`
19
40
  });
20
41
  }
21
42
  }
@@ -1,13 +1,50 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { ast, imports } from '@esportsplus/typescript/compiler';
2
+ import { 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
6
  function hasReactiveImport(sourceFile) {
7
7
  return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
8
8
  }
9
- function isReactiveCallNode(node) {
10
- return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === COMPILER_ENTRYPOINT;
9
+ function isReactiveSymbol(checker, node) {
10
+ let symbol = checker.getSymbolAtLocation(node);
11
+ if (!symbol) {
12
+ return false;
13
+ }
14
+ if (symbol.flags & ts.SymbolFlags.Alias) {
15
+ symbol = checker.getAliasedSymbol(symbol);
16
+ }
17
+ let declarations = symbol.getDeclarations();
18
+ if (!declarations || declarations.length === 0) {
19
+ return false;
20
+ }
21
+ for (let i = 0, n = declarations.length; i < n; i++) {
22
+ let decl = declarations[i], sourceFile = decl.getSourceFile();
23
+ if (sourceFile.fileName.includes(PACKAGE) || sourceFile.fileName.includes('reactivity')) {
24
+ if (symbol.name === COMPILER_ENTRYPOINT) {
25
+ return true;
26
+ }
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+ function isReactiveCallExpression(checker, node) {
32
+ if (!ts.isCallExpression(node)) {
33
+ return false;
34
+ }
35
+ let expr = node.expression;
36
+ if (ts.isIdentifier(expr)) {
37
+ if (expr.text === COMPILER_ENTRYPOINT) {
38
+ return true;
39
+ }
40
+ if (checker) {
41
+ return isReactiveSymbol(checker, expr);
42
+ }
43
+ }
44
+ if (ts.isPropertyAccessExpression(expr) && expr.name.text === COMPILER_ENTRYPOINT && checker) {
45
+ return isReactiveSymbol(checker, expr);
46
+ }
47
+ return false;
11
48
  }
12
49
  const plugin = {
13
50
  patterns: ['reactive(', 'reactive<'],
@@ -21,15 +58,23 @@ const plugin = {
21
58
  replacements.push(...objectResult.replacements);
22
59
  let arrayResult = array(ctx.sourceFile, bindings, ctx.checker);
23
60
  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);
61
+ let transformedNodes = new Set(replacements.map(r => r.node));
62
+ function findRemainingReactiveCalls(node) {
63
+ if (isReactiveCallExpression(ctx.checker, node) && !transformedNodes.has(node)) {
64
+ let call = node;
65
+ replacements.push({
66
+ generate: () => `${COMPILER_NAMESPACE}.reactive(${call.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
67
+ node: call
68
+ });
28
69
  }
70
+ ts.forEachChild(node, findRemainingReactiveCalls);
71
+ }
72
+ findRemainingReactiveCalls(ctx.sourceFile);
73
+ if (replacements.length > 0 || prepend.length > 0) {
29
74
  importsIntent.push({
30
75
  namespace: COMPILER_NAMESPACE,
31
76
  package: PACKAGE,
32
- remove
77
+ remove: [COMPILER_ENTRYPOINT]
33
78
  });
34
79
  }
35
80
  return {
package/package.json CHANGED
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "type": "module",
33
33
  "types": "build/index.d.ts",
34
- "version": "0.28.0",
34
+ "version": "0.28.2",
35
35
  "scripts": {
36
36
  "build": "tsc",
37
37
  "build:test": "pnpm build && vite build --config test/vite.config.ts",
@@ -13,6 +13,24 @@ interface VisitContext {
13
13
  }
14
14
 
15
15
 
16
+ function getElementTypeText(typeNode: ts.TypeNode, sourceFile: ts.SourceFile): string | null {
17
+ if (ts.isArrayTypeNode(typeNode)) {
18
+ return typeNode.elementType.getText(sourceFile);
19
+ }
20
+
21
+ if (
22
+ ts.isTypeReferenceNode(typeNode) &&
23
+ ts.isIdentifier(typeNode.typeName) &&
24
+ typeNode.typeName.text === 'Array' &&
25
+ typeNode.typeArguments &&
26
+ typeNode.typeArguments.length > 0
27
+ ) {
28
+ return typeNode.typeArguments[0].getText(sourceFile);
29
+ }
30
+
31
+ return null;
32
+ }
33
+
16
34
  function isReactiveCall(node: ts.CallExpression): boolean {
17
35
  return ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
18
36
  }
@@ -23,15 +41,26 @@ function visit(ctx: VisitContext, node: ts.Node): void {
23
41
  expression = ts.isAsExpression(arg) ? arg.expression : arg;
24
42
 
25
43
  if (ts.isArrayLiteralExpression(expression)) {
44
+ let elementType: string | null = null;
45
+
46
+ if (ts.isAsExpression(arg) && arg.type) {
47
+ elementType = getElementTypeText(arg.type, ctx.sourceFile);
48
+ }
49
+ else if (node.parent && ts.isVariableDeclaration(node.parent) && node.parent.type) {
50
+ elementType = getElementTypeText(node.parent.type, ctx.sourceFile);
51
+ }
52
+
26
53
  if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
27
54
  ctx.bindings.set(node.parent.name.text, COMPILER_TYPES.Array);
28
55
  }
29
56
 
57
+ let typeParam = elementType ? `<${elementType}>` : '';
58
+
30
59
  ctx.replacements.push({
31
60
  node,
32
61
  generate: (sf) => expression.elements.length > 0
33
- ? ` new ${COMPILER_NAMESPACE}.ReactiveArray(...${expression.getText(sf)})`
34
- : ` new ${COMPILER_NAMESPACE}.ReactiveArray()`
62
+ ? ` new ${COMPILER_NAMESPACE}.ReactiveArray${typeParam}(...${expression.getText(sf)})`
63
+ : ` new ${COMPILER_NAMESPACE}.ReactiveArray${typeParam}()`
35
64
  });
36
65
  }
37
66
  }
@@ -1,6 +1,6 @@
1
1
  import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
- import { ast, imports } from '@esportsplus/typescript/compiler';
3
+ import { imports } from '@esportsplus/typescript/compiler';
4
4
  import { COMPILER_ENTRYPOINT, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
5
5
  import type { Bindings } from '~/types';
6
6
  import array from './array';
@@ -11,8 +11,66 @@ function hasReactiveImport(sourceFile: ts.SourceFile): boolean {
11
11
  return imports.find(sourceFile, PACKAGE).some(i => i.specifiers.has(COMPILER_ENTRYPOINT));
12
12
  }
13
13
 
14
- function isReactiveCallNode(node: ts.Node): boolean {
15
- return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === COMPILER_ENTRYPOINT;
14
+ function isReactiveSymbol(checker: ts.TypeChecker, node: ts.Node): boolean {
15
+ let symbol = checker.getSymbolAtLocation(node);
16
+
17
+ if (!symbol) {
18
+ return false;
19
+ }
20
+
21
+ // Follow aliases to original symbol
22
+ if (symbol.flags & ts.SymbolFlags.Alias) {
23
+ symbol = checker.getAliasedSymbol(symbol);
24
+ }
25
+
26
+ let declarations = symbol.getDeclarations();
27
+
28
+ if (!declarations || declarations.length === 0) {
29
+ return false;
30
+ }
31
+
32
+ for (let i = 0, n = declarations.length; i < n; i++) {
33
+ let decl = declarations[i],
34
+ sourceFile = decl.getSourceFile();
35
+
36
+ // Check if declaration is from our package
37
+ if (sourceFile.fileName.includes(PACKAGE) || sourceFile.fileName.includes('reactivity')) {
38
+ // Verify it's the reactive export
39
+ if (symbol.name === COMPILER_ENTRYPOINT) {
40
+ return true;
41
+ }
42
+ }
43
+ }
44
+
45
+ return false;
46
+ }
47
+
48
+ function isReactiveCallExpression(checker: ts.TypeChecker | undefined, node: ts.Node): node is ts.CallExpression {
49
+ if (!ts.isCallExpression(node)) {
50
+ return false;
51
+ }
52
+
53
+ let expr = node.expression;
54
+
55
+ // Direct call: reactive(...) or aliasedName(...)
56
+ if (ts.isIdentifier(expr)) {
57
+ // Fast path: literal "reactive"
58
+ if (expr.text === COMPILER_ENTRYPOINT) {
59
+ return true;
60
+ }
61
+
62
+ // Use checker to resolve aliases
63
+ if (checker) {
64
+ return isReactiveSymbol(checker, expr);
65
+ }
66
+ }
67
+
68
+ // Property access: ns.reactive(...)
69
+ if (ts.isPropertyAccessExpression(expr) && expr.name.text === COMPILER_ENTRYPOINT && checker) {
70
+ return isReactiveSymbol(checker, expr);
71
+ }
72
+
73
+ return false;
16
74
  }
17
75
 
18
76
 
@@ -40,20 +98,30 @@ const plugin: Plugin = {
40
98
 
41
99
  replacements.push(...arrayResult);
42
100
 
43
- // Build import intent
44
- if (replacements.length > 0 || prepend.length > 0) {
45
- let remove: string[] = [];
101
+ // Find remaining reactive() calls that weren't transformed and replace with namespace version
102
+ let transformedNodes = new Set(replacements.map(r => r.node));
46
103
 
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);
104
+ function findRemainingReactiveCalls(node: ts.Node): void {
105
+ if (isReactiveCallExpression(ctx.checker, node) && !transformedNodes.has(node)) {
106
+ let call = node;
107
+
108
+ replacements.push({
109
+ generate: () => `${COMPILER_NAMESPACE}.reactive(${call.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
110
+ node: call
111
+ });
51
112
  }
52
113
 
114
+ ts.forEachChild(node, findRemainingReactiveCalls);
115
+ }
116
+
117
+ findRemainingReactiveCalls(ctx.sourceFile);
118
+
119
+ // Build import intent
120
+ if (replacements.length > 0 || prepend.length > 0) {
53
121
  importsIntent.push({
54
122
  namespace: COMPILER_NAMESPACE,
55
123
  package: PACKAGE,
56
- remove
124
+ remove: [COMPILER_ENTRYPOINT]
57
125
  });
58
126
  }
59
127