@eventra_dev/eventra-cli 0.2.4 → 0.3.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.
- package/README.md +143 -32
- package/dist/analysis/cache/evaluationCache.js +21 -0
- package/dist/analysis/cache/resolvedCallCache.js +21 -0
- package/dist/analysis/cache/resolvedExportCache.js +21 -0
- package/dist/analysis/cache/semanticDependencyGraph.js +25 -0
- package/dist/analysis/cache/symbolInvalidation.js +20 -0
- package/dist/analysis/extractor/extractor.js +47 -0
- package/dist/analysis/extractor/propagationExtractor.js +52 -0
- package/dist/analysis/pipeline/semanticPass.js +2 -0
- package/dist/analysis/resolver/callResolver.js +125 -0
- package/dist/analysis/resolver/exportResolver.js +75 -0
- package/dist/analysis/resolver/resolver.js +133 -0
- package/dist/analysis/scanner/analyzer/propagationAnalyzer.js +193 -0
- package/dist/analysis/scanner/analyzer/returnPropagationAnalyzer.js +105 -0
- package/dist/analysis/scanner/scanner.js +117 -0
- package/dist/analysis/scanner/sinkDetector.js +91 -0
- package/dist/analysis/shared/evaluationContext.js +9 -0
- package/dist/analysis/shared/propagation.js +3 -0
- package/dist/analysis/shared/returnPropagation.js +2 -0
- package/dist/analysis/shared/types.js +2 -0
- package/dist/analysis/shared/utils.js +62 -0
- package/dist/analysis/symbols/symbolUtils.js +68 -0
- package/dist/analysis/symbols/wrapperRegistry.js +55 -0
- package/dist/cli/check.js +73 -0
- package/dist/{commands → cli}/init.js +1 -1
- package/dist/{commands → cli}/send.js +1 -1
- package/dist/cli/sync.js +56 -0
- package/dist/{commands → cli}/watch.js +41 -25
- package/dist/compiler/compilerContext.js +204 -0
- package/dist/compiler/documentRegistry.js +78 -0
- package/dist/compiler/importGraph.js +83 -0
- package/dist/compiler/scheduler.js +29 -0
- package/dist/core/EventraEngine.js +234 -0
- package/dist/core/projectScanner.js +73 -0
- package/dist/{utils → filesystem}/getVirtualFile.js +1 -3
- package/dist/filesystem/processFile.js +38 -0
- package/dist/index.js +5 -5
- package/dist/shared/buildSyntheticTsx.js +20 -0
- package/dist/shared/templateExpressionExtractor.js +22 -0
- package/package.json +32 -9
- package/.github/workflows/release.yml +0 -43
- package/ARCHITECTURE.md +0 -88
- package/assets/eventra-icon-animated.svg +0 -19
- package/dist/commands/check.js +0 -78
- package/dist/commands/sync.js +0 -79
- package/dist/engine/boundary.js +0 -14
- package/dist/engine/callResolver.js +0 -58
- package/dist/engine/engine.js +0 -154
- package/dist/engine/exportResolver.js +0 -46
- package/dist/engine/functionAnalyzer.js +0 -71
- package/dist/engine/graph.js +0 -168
- package/dist/engine/languageService.js +0 -152
- package/dist/engine/resolver.js +0 -149
- package/dist/engine/scanner.js +0 -230
- package/dist/utils/processFile.js +0 -28
- package/dist/utils/universal.js +0 -190
- package/src/commands/check.ts +0 -91
- package/src/commands/init.ts +0 -174
- package/src/commands/send.ts +0 -213
- package/src/commands/sync.ts +0 -106
- package/src/commands/watch.ts +0 -144
- package/src/engine/boundary.ts +0 -12
- package/src/engine/callResolver.ts +0 -65
- package/src/engine/engine.ts +0 -227
- package/src/engine/exportResolver.ts +0 -56
- package/src/engine/functionAnalyzer.ts +0 -81
- package/src/engine/graph.ts +0 -193
- package/src/engine/hash.ts +0 -5
- package/src/engine/languageService.ts +0 -197
- package/src/engine/resolver.ts +0 -177
- package/src/engine/scanner.ts +0 -298
- package/src/index.ts +0 -49
- package/src/types.ts +0 -35
- package/src/utils/config.ts +0 -53
- package/src/utils/getVirtualFile.ts +0 -5
- package/src/utils/processFile.ts +0 -29
- package/src/utils/universal.ts +0 -249
- package/tests/fixtures/backend/express/app.ts +0 -233
- package/tests/fixtures/backend/nest/service.ts +0 -70
- package/tests/fixtures/backend/node/index.ts +0 -63
- package/tests/fixtures/frontend/next/page.tsx +0 -101
- package/tests/fixtures/frontend/react/App.tsx +0 -104
- package/tests/fixtures/frontend/vue/App.vue +0 -83
- package/tests/fixtures/wrappers/component/test.tsx +0 -62
- package/tests/fixtures/wrappers/function/test.ts +0 -60
- package/tests/run.ts +0 -106
- package/tsconfig.json +0 -14
- /package/dist/{utils → config}/config.js +0 -0
- /package/dist/{engine → shared}/hash.js +0 -0
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
export class TSService {
|
|
6
|
-
private files = new Map<string, { version: number; content: string }>();
|
|
7
|
-
private fileNames = new Set<string>();
|
|
8
|
-
private service: ts.LanguageService;
|
|
9
|
-
|
|
10
|
-
private compilerOptions: ts.CompilerOptions;
|
|
11
|
-
private baseUrl: string;
|
|
12
|
-
private paths: Record<string, string[]>;
|
|
13
|
-
|
|
14
|
-
constructor(private root: string) {
|
|
15
|
-
const configPath = ts.findConfigFile(
|
|
16
|
-
root,
|
|
17
|
-
ts.sys.fileExists,
|
|
18
|
-
"tsconfig.json"
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
let compilerOptions: ts.CompilerOptions = {
|
|
22
|
-
allowJs: true,
|
|
23
|
-
allowNonTsExtensions: true,
|
|
24
|
-
jsx: ts.JsxEmit.Preserve,
|
|
25
|
-
jsxImportSource: "react",
|
|
26
|
-
moduleResolution: ts.ModuleResolutionKind.Node16,
|
|
27
|
-
target: ts.ScriptTarget.ESNext,
|
|
28
|
-
esModuleInterop: true,
|
|
29
|
-
allowSyntheticDefaultImports: true,
|
|
30
|
-
skipLibCheck: true,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
let baseUrl = root;
|
|
34
|
-
let paths: Record<string, string[]> = {};
|
|
35
|
-
|
|
36
|
-
if (configPath) {
|
|
37
|
-
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
38
|
-
|
|
39
|
-
const parsed = ts.parseJsonConfigFileContent(
|
|
40
|
-
configFile.config,
|
|
41
|
-
ts.sys,
|
|
42
|
-
root
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
compilerOptions = {
|
|
46
|
-
...compilerOptions,
|
|
47
|
-
...parsed.options,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
paths = parsed.options.paths || {};
|
|
51
|
-
baseUrl = parsed.options.baseUrl
|
|
52
|
-
? path.resolve(root, parsed.options.baseUrl)
|
|
53
|
-
: root;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
this.compilerOptions = compilerOptions;
|
|
57
|
-
this.baseUrl = baseUrl;
|
|
58
|
-
this.paths = paths;
|
|
59
|
-
|
|
60
|
-
const allFiles = ts.sys.readDirectory(
|
|
61
|
-
root,
|
|
62
|
-
[".ts", ".tsx", ".js", ".jsx"],
|
|
63
|
-
undefined,
|
|
64
|
-
undefined
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
for (const f of allFiles) {
|
|
68
|
-
this.fileNames.add(this.normalize(f));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const host: ts.LanguageServiceHost = {
|
|
72
|
-
getScriptFileNames: () => {
|
|
73
|
-
return Array.from(this.fileNames);
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
getScriptVersion: (file) => {
|
|
77
|
-
file = this.normalize(file);
|
|
78
|
-
return this.files.get(file)?.version.toString() ?? "0";
|
|
79
|
-
},
|
|
80
|
-
getScriptKind: (fileName) => {
|
|
81
|
-
fileName = this.normalize(fileName);
|
|
82
|
-
|
|
83
|
-
if (fileName.endsWith(".tsx")) {
|
|
84
|
-
return ts.ScriptKind.TSX;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (fileName.endsWith(".ts")) {
|
|
88
|
-
return ts.ScriptKind.TS;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (fileName.endsWith(".jsx")) {
|
|
92
|
-
return ts.ScriptKind.JSX;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (fileName.endsWith(".js")) {
|
|
96
|
-
return ts.ScriptKind.JS;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return ts.ScriptKind.Unknown;
|
|
100
|
-
},
|
|
101
|
-
getScriptSnapshot: (file) => {
|
|
102
|
-
file = this.normalize(file);
|
|
103
|
-
const f = this.files.get(file);
|
|
104
|
-
if (f) {
|
|
105
|
-
return ts.ScriptSnapshot.fromString(f.content);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
if (fs.existsSync(file)) {
|
|
110
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
111
|
-
|
|
112
|
-
this.files.set(file, {
|
|
113
|
-
content,
|
|
114
|
-
version: 1,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
this.fileNames.add(file);
|
|
118
|
-
|
|
119
|
-
return ts.ScriptSnapshot.fromString(content);
|
|
120
|
-
}
|
|
121
|
-
} catch {
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return ts.ScriptSnapshot.fromString("");
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
getCurrentDirectory: () => this.root,
|
|
128
|
-
getCompilationSettings: () => this.compilerOptions,
|
|
129
|
-
getDefaultLibFileName: (opts) =>
|
|
130
|
-
ts.getDefaultLibFilePath(opts),
|
|
131
|
-
|
|
132
|
-
fileExists: (file) => {
|
|
133
|
-
file = this.normalize(file);
|
|
134
|
-
return this.files.has(file) || ts.sys.fileExists(file);
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
readFile: (file) => {
|
|
138
|
-
file = this.normalize(file);
|
|
139
|
-
return this.files.get(file)?.content ?? ts.sys.readFile(file);
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
readDirectory: ts.sys.readDirectory,
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
this.service = ts.createLanguageService(host);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private normalize(file: string) {
|
|
149
|
-
return path.resolve(file).replace(/\\/g, "/");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// PUBLIC API
|
|
153
|
-
updateFile(file: string, content: string) {
|
|
154
|
-
file = this.normalize(file);
|
|
155
|
-
|
|
156
|
-
const prev = this.files.get(file);
|
|
157
|
-
|
|
158
|
-
this.files.set(file, {
|
|
159
|
-
content,
|
|
160
|
-
version: prev ? prev.version + 1 : 1,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
this.fileNames.add(file);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
removeFile(file: string) {
|
|
167
|
-
file = this.normalize(file);
|
|
168
|
-
|
|
169
|
-
this.files.delete(file);
|
|
170
|
-
this.fileNames.delete(file);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
getProgram() {
|
|
174
|
-
return this.service.getProgram();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
getChecker() {
|
|
178
|
-
return this.getProgram()?.getTypeChecker();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
getSourceFile(file: string) {
|
|
182
|
-
file = this.normalize(file);
|
|
183
|
-
return this.getProgram()?.getSourceFile(file);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
getFileContent(file: string) {
|
|
187
|
-
file = this.normalize(file);
|
|
188
|
-
return this.files.get(file)?.content;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
getModuleResolutionConfig() {
|
|
192
|
-
return {
|
|
193
|
-
baseUrl: this.baseUrl,
|
|
194
|
-
paths: this.paths,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
}
|
package/src/engine/resolver.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { resolveExportedSymbol } from "./exportResolver";
|
|
3
|
-
import { resolveFunctionFromCall } from "./callResolver";
|
|
4
|
-
|
|
5
|
-
export type ResolveResult = {
|
|
6
|
-
values: string[];
|
|
7
|
-
dynamic: boolean;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function resolveNodeValue(
|
|
11
|
-
node: ts.Node,
|
|
12
|
-
checker: ts.TypeChecker,
|
|
13
|
-
paramMap?: Map<string, ts.Expression>,
|
|
14
|
-
seen = new Set<ts.Node>()
|
|
15
|
-
): ResolveResult {
|
|
16
|
-
if (seen.has(node)) return { values: [], dynamic: true };
|
|
17
|
-
seen.add(node);
|
|
18
|
-
|
|
19
|
-
// param passthrough
|
|
20
|
-
if (ts.isIdentifier(node) && paramMap?.has(node.text)) {
|
|
21
|
-
return resolveNodeValue(paramMap.get(node.text)!, checker, paramMap, seen);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// string
|
|
25
|
-
if (ts.isStringLiteral(node)) {
|
|
26
|
-
return { values: [node.text], dynamic: false };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// template
|
|
30
|
-
if (ts.isTemplateExpression(node)) {
|
|
31
|
-
let results = node.head.text ? [node.head.text] : [""];
|
|
32
|
-
for (const span of node.templateSpans) {
|
|
33
|
-
const r = resolveNodeValue(span.expression, checker, paramMap, seen);
|
|
34
|
-
results = results.flatMap(s =>
|
|
35
|
-
r.values.length ? r.values.map(v => s + v) : [s + "*"]
|
|
36
|
-
);
|
|
37
|
-
results = results.map(s => s + span.literal.text);
|
|
38
|
-
}
|
|
39
|
-
return { values: results, dynamic: true };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ternary
|
|
43
|
-
if (ts.isConditionalExpression(node)) {
|
|
44
|
-
const a = resolveNodeValue(node.whenTrue, checker, paramMap, seen);
|
|
45
|
-
const b = resolveNodeValue(node.whenFalse, checker, paramMap, seen);
|
|
46
|
-
return {
|
|
47
|
-
values: [...new Set([...a.values, ...b.values])],
|
|
48
|
-
dynamic: true,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// object (event/name priority)
|
|
53
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
54
|
-
for (const p of node.properties) {
|
|
55
|
-
if (ts.isPropertyAssignment(p)) {
|
|
56
|
-
const key = p.name.getText();
|
|
57
|
-
if (key === "event" || key === "name") {
|
|
58
|
-
return resolveNodeValue(p.initializer, checker, paramMap, seen);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const vals: string[] = [];
|
|
63
|
-
for (const p of node.properties) {
|
|
64
|
-
if (!ts.isPropertyAssignment(p)) continue;
|
|
65
|
-
const r = resolveNodeValue(p.initializer, checker, paramMap, seen);
|
|
66
|
-
vals.push(...r.values);
|
|
67
|
-
}
|
|
68
|
-
return { values: [...new Set(vals)], dynamic: true };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// array
|
|
72
|
-
if (ts.isArrayLiteralExpression(node)) {
|
|
73
|
-
const vals: string[] = [];
|
|
74
|
-
for (const el of node.elements) {
|
|
75
|
-
const r = resolveNodeValue(el, checker, paramMap, seen);
|
|
76
|
-
vals.push(...r.values);
|
|
77
|
-
}
|
|
78
|
-
return { values: [...new Set(vals)], dynamic: true };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// concat
|
|
82
|
-
if (ts.isBinaryExpression(node) &&
|
|
83
|
-
node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
84
|
-
const l = resolveNodeValue(node.left, checker, paramMap, seen);
|
|
85
|
-
const r = resolveNodeValue(node.right, checker, paramMap, seen);
|
|
86
|
-
return {
|
|
87
|
-
values: l.values.flatMap(a =>
|
|
88
|
-
r.values.length ? r.values.map(b => a + b) : [a + "*"]
|
|
89
|
-
),
|
|
90
|
-
dynamic: true,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// identifier (const / enum)
|
|
95
|
-
if (ts.isIdentifier(node)) {
|
|
96
|
-
let s = checker.getSymbolAtLocation(node);
|
|
97
|
-
if (!s) return { values: [], dynamic: true };
|
|
98
|
-
|
|
99
|
-
s = resolveExportedSymbol(s, checker) ?? s;
|
|
100
|
-
|
|
101
|
-
for (const d of s.getDeclarations() ?? []) {
|
|
102
|
-
if (ts.isVariableDeclaration(d) && d.initializer) {
|
|
103
|
-
return resolveNodeValue(d.initializer, checker, paramMap, seen);
|
|
104
|
-
}
|
|
105
|
-
if (ts.isEnumMember(d) && d.initializer && ts.isStringLiteral(d.initializer)) {
|
|
106
|
-
return { values: [d.initializer.text], dynamic: false };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return { values: [], dynamic: true };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// PROPERTY ACCESS (OBJECT + ENUM)
|
|
113
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
114
|
-
const obj = node.expression;
|
|
115
|
-
const prop = node.name.text;
|
|
116
|
-
|
|
117
|
-
// object literal lookup
|
|
118
|
-
if (ts.isIdentifier(obj)) {
|
|
119
|
-
const sym = checker.getSymbolAtLocation(obj);
|
|
120
|
-
if (sym) {
|
|
121
|
-
const s = resolveExportedSymbol(sym, checker) ?? sym;
|
|
122
|
-
for (const d of s.getDeclarations() ?? []) {
|
|
123
|
-
if (
|
|
124
|
-
ts.isVariableDeclaration(d) &&
|
|
125
|
-
d.initializer &&
|
|
126
|
-
ts.isObjectLiteralExpression(d.initializer)
|
|
127
|
-
) {
|
|
128
|
-
for (const p of d.initializer.properties) {
|
|
129
|
-
if (
|
|
130
|
-
ts.isPropertyAssignment(p) &&
|
|
131
|
-
p.name.getText() === prop
|
|
132
|
-
) {
|
|
133
|
-
return resolveNodeValue(p.initializer, checker, paramMap, seen);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// enum fallback
|
|
142
|
-
const sym = checker.getSymbolAtLocation(node);
|
|
143
|
-
if (sym) {
|
|
144
|
-
let s = resolveExportedSymbol(sym, checker) ?? sym;
|
|
145
|
-
if (s.flags & ts.SymbolFlags.Alias) s = checker.getAliasedSymbol(s);
|
|
146
|
-
for (const d of s.getDeclarations() ?? []) {
|
|
147
|
-
if (ts.isEnumMember(d) && d.initializer && ts.isStringLiteral(d.initializer)) {
|
|
148
|
-
return { values: [d.initializer.text], dynamic: false };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return { values: [], dynamic: true };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// CALL (unwrap + dive into wrapper body)
|
|
157
|
-
if (ts.isCallExpression(node)) {
|
|
158
|
-
const vals: string[] = [];
|
|
159
|
-
|
|
160
|
-
// args
|
|
161
|
-
for (const a of node.arguments) {
|
|
162
|
-
const r = resolveNodeValue(a, checker, paramMap, seen);
|
|
163
|
-
vals.push(...r.values);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// dive into called function (wrapper support)
|
|
167
|
-
const fn = resolveFunctionFromCall(node.expression, checker);
|
|
168
|
-
if (fn?.body) {
|
|
169
|
-
const r = resolveNodeValue(fn.body, checker, paramMap, seen);
|
|
170
|
-
vals.push(...r.values);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return { values: [...new Set(vals)], dynamic: true };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { values: [], dynamic: true };
|
|
177
|
-
}
|
package/src/engine/scanner.ts
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { resolveNodeValue } from "./resolver";
|
|
3
|
-
import { resolveFunctionFromCall } from "./callResolver";
|
|
4
|
-
import { resolveExportedSymbol } from "./exportResolver";
|
|
5
|
-
import { EventraConfig, ScanResult } from "../types";
|
|
6
|
-
|
|
7
|
-
// HELPERS
|
|
8
|
-
function isValidEvent(v: string) {
|
|
9
|
-
return (
|
|
10
|
-
v &&
|
|
11
|
-
v.length > 1 &&
|
|
12
|
-
v.length < 120 &&
|
|
13
|
-
!v.includes(" ") &&
|
|
14
|
-
/^[a-zA-Z0-9._:-]+$/.test(v)
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getCallName(expr: ts.Expression): string {
|
|
19
|
-
if (ts.isIdentifier(expr)) return expr.text;
|
|
20
|
-
if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
|
|
21
|
-
return "";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function getJsxTagName(tag: ts.JsxTagNameExpression): string {
|
|
25
|
-
if (ts.isIdentifier(tag)) return tag.text;
|
|
26
|
-
if (ts.isPropertyAccessExpression(tag)) return tag.name.text;
|
|
27
|
-
return tag.getText();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function getJsxAttrName(name: ts.JsxAttributeName): string {
|
|
31
|
-
if (ts.isIdentifier(name)) return name.text;
|
|
32
|
-
if (ts.isJsxNamespacedName(name)) return name.name.text;
|
|
33
|
-
return "";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// MAIN
|
|
37
|
-
export function scanSource(
|
|
38
|
-
source: ts.SourceFile,
|
|
39
|
-
checker: ts.TypeChecker,
|
|
40
|
-
config: EventraConfig,
|
|
41
|
-
visited = new Set<string>(),
|
|
42
|
-
inheritedParamMap?: Map<string, ts.Expression>,
|
|
43
|
-
wrapperCache = new Map<string, string | null>()
|
|
44
|
-
): ScanResult {
|
|
45
|
-
const events = new Set<string>();
|
|
46
|
-
const detectedFunctionWrappers = new Set<string>();
|
|
47
|
-
const detectedComponentWrappers = new Map<string, string>();
|
|
48
|
-
|
|
49
|
-
if (visited.has(source.fileName)) {
|
|
50
|
-
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
visited.add(source.fileName);
|
|
54
|
-
|
|
55
|
-
// TRACK DETECTION
|
|
56
|
-
function isTrackCall(expr: ts.Expression) {
|
|
57
|
-
const name = getCallName(expr);
|
|
58
|
-
|
|
59
|
-
// direct track()
|
|
60
|
-
if (name === "track") {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// configured wrappers
|
|
65
|
-
if (
|
|
66
|
-
config.functionWrappers?.some(
|
|
67
|
-
w => w.name === name
|
|
68
|
-
)
|
|
69
|
-
) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// *.track()
|
|
74
|
-
return ts.isPropertyAccessExpression(expr) &&
|
|
75
|
-
expr.name.text === "track";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function extractTrackCall(
|
|
81
|
-
node: ts.CallExpression,
|
|
82
|
-
paramMap: Map<string, ts.Expression>
|
|
83
|
-
) {
|
|
84
|
-
if (!isTrackCall(node.expression)) return;
|
|
85
|
-
|
|
86
|
-
const arg = node.arguments[0];
|
|
87
|
-
if (!arg) return;
|
|
88
|
-
|
|
89
|
-
const res = resolveNodeValue(arg, checker, paramMap);
|
|
90
|
-
|
|
91
|
-
for (const v of res.values) {
|
|
92
|
-
if (isValidEvent(v)) {
|
|
93
|
-
events.add(v);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// WRAPPER DETECTION
|
|
99
|
-
function isWrapperFunction(fn: ts.FunctionLikeDeclaration) {
|
|
100
|
-
let found = false;
|
|
101
|
-
|
|
102
|
-
function walk(n: ts.Node) {
|
|
103
|
-
if (found) return;
|
|
104
|
-
|
|
105
|
-
if (ts.isCallExpression(n) && isTrackCall(n.expression)) {
|
|
106
|
-
found = true;
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
ts.forEachChild(n, walk);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (fn.body) walk(fn.body);
|
|
114
|
-
return found;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// COMPONENT WRAPPER
|
|
118
|
-
function detectComponentWrapper(tag: ts.JsxTagNameExpression): string | null {
|
|
119
|
-
const tagName = getJsxTagName(tag);
|
|
120
|
-
|
|
121
|
-
if (wrapperCache.has(tagName)) {
|
|
122
|
-
return wrapperCache.get(tagName)!;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let symbol = checker.getSymbolAtLocation(tag);
|
|
126
|
-
|
|
127
|
-
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
128
|
-
symbol = checker.getAliasedSymbol(symbol);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!symbol) {
|
|
132
|
-
wrapperCache.set(tagName, null);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
137
|
-
|
|
138
|
-
for (const decl of symbol.getDeclarations() ?? []) {
|
|
139
|
-
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
140
|
-
|
|
141
|
-
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
ts.isVariableDeclaration(decl) &&
|
|
145
|
-
decl.initializer &&
|
|
146
|
-
(ts.isArrowFunction(decl.initializer) ||
|
|
147
|
-
ts.isFunctionExpression(decl.initializer))
|
|
148
|
-
) {
|
|
149
|
-
fn = decl.initializer;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!fn || !fn.body) continue;
|
|
153
|
-
|
|
154
|
-
let prop: string | null = null;
|
|
155
|
-
|
|
156
|
-
function walk(n: ts.Node) {
|
|
157
|
-
if (prop) return;
|
|
158
|
-
|
|
159
|
-
if (ts.isCallExpression(n) && isTrackCall(n.expression)) {
|
|
160
|
-
const arg = n.arguments[0];
|
|
161
|
-
|
|
162
|
-
if (ts.isPropertyAccessExpression(arg)) {
|
|
163
|
-
prop = arg.name.text;
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
ts.forEachChild(n, walk);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
walk(fn.body);
|
|
172
|
-
|
|
173
|
-
wrapperCache.set(tagName, prop);
|
|
174
|
-
|
|
175
|
-
if (prop) {
|
|
176
|
-
detectedComponentWrappers.set(tagName, prop);
|
|
177
|
-
return prop;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
wrapperCache.set(tagName, null);
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// VISITOR
|
|
186
|
-
function visit(node: ts.Node) {
|
|
187
|
-
// JSX
|
|
188
|
-
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
189
|
-
const tagName = getJsxTagName(node.tagName);
|
|
190
|
-
|
|
191
|
-
if (tagName[0] === tagName[0].toLowerCase()) {
|
|
192
|
-
ts.forEachChild(node, visit);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
let wrapper = config.wrappers.find(w => w.name === tagName);
|
|
197
|
-
|
|
198
|
-
if (!wrapper) {
|
|
199
|
-
const prop = detectComponentWrapper(node.tagName);
|
|
200
|
-
if (prop) wrapper = { name: tagName, prop };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (wrapper) {
|
|
204
|
-
for (const attr of node.attributes.properties) {
|
|
205
|
-
if (!ts.isJsxAttribute(attr)) continue;
|
|
206
|
-
|
|
207
|
-
const name = getJsxAttrName(attr.name);
|
|
208
|
-
if (name !== wrapper.prop) continue;
|
|
209
|
-
|
|
210
|
-
if (!attr.initializer) continue;
|
|
211
|
-
|
|
212
|
-
if (ts.isStringLiteral(attr.initializer)) {
|
|
213
|
-
if (isValidEvent(attr.initializer.text)) {
|
|
214
|
-
events.add(attr.initializer.text);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (ts.isJsxExpression(attr.initializer)) {
|
|
219
|
-
const expr = attr.initializer.expression;
|
|
220
|
-
if (!expr) continue;
|
|
221
|
-
|
|
222
|
-
const res = resolveNodeValue(expr, checker);
|
|
223
|
-
res.values.forEach(v => {
|
|
224
|
-
if (isValidEvent(v)) events.add(v);
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
ts.forEachChild(node, visit);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// CALL
|
|
235
|
-
if (ts.isCallExpression(node)) {
|
|
236
|
-
const resolvedFn = resolveFunctionFromCall(node.expression, checker);
|
|
237
|
-
|
|
238
|
-
const paramMap = new Map(inheritedParamMap ?? []);
|
|
239
|
-
|
|
240
|
-
if (resolvedFn) {
|
|
241
|
-
resolvedFn.parameters.forEach((p, i) => {
|
|
242
|
-
const arg = node.arguments[i];
|
|
243
|
-
if (!arg) return;
|
|
244
|
-
|
|
245
|
-
if (ts.isIdentifier(p.name)) {
|
|
246
|
-
paramMap.set(p.name.text, arg);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
extractTrackCall(node, paramMap);
|
|
252
|
-
|
|
253
|
-
if (resolvedFn && isWrapperFunction(resolvedFn)) {
|
|
254
|
-
const name = getCallName(node.expression);
|
|
255
|
-
|
|
256
|
-
if (name && name !== "track") {
|
|
257
|
-
detectedFunctionWrappers.add(name);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (resolvedFn.body) {
|
|
261
|
-
visit(resolvedFn.body);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (resolvedFn && resolvedFn.body) {
|
|
266
|
-
const sf = resolvedFn.getSourceFile();
|
|
267
|
-
|
|
268
|
-
if (!visited.has(sf.fileName)) {
|
|
269
|
-
const res = scanSource(
|
|
270
|
-
sf,
|
|
271
|
-
checker,
|
|
272
|
-
config,
|
|
273
|
-
visited,
|
|
274
|
-
paramMap,
|
|
275
|
-
wrapperCache
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
res.events.forEach(e => events.add(e));
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
node.arguments.forEach(visit);
|
|
283
|
-
visit(node.expression);
|
|
284
|
-
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
ts.forEachChild(node, visit);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
visit(source);
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
events,
|
|
295
|
-
detectedFunctionWrappers,
|
|
296
|
-
detectedComponentWrappers,
|
|
297
|
-
};
|
|
298
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from "commander";
|
|
4
|
-
|
|
5
|
-
const program = new Command();
|
|
6
|
-
|
|
7
|
-
program
|
|
8
|
-
.name("eventra")
|
|
9
|
-
.description("Eventra CLI")
|
|
10
|
-
.version("0.0.1");
|
|
11
|
-
|
|
12
|
-
program
|
|
13
|
-
.command("init")
|
|
14
|
-
.action(async () => {
|
|
15
|
-
const { init } = await import("./commands/init");
|
|
16
|
-
await init();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
program
|
|
20
|
-
.command("sync")
|
|
21
|
-
.action(async () => {
|
|
22
|
-
const { sync } = await import("./commands/sync");
|
|
23
|
-
await sync();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
program
|
|
27
|
-
.command("check")
|
|
28
|
-
.option("--fix", "auto fix issues")
|
|
29
|
-
.action(async (opts) => {
|
|
30
|
-
const { check } = await import("./commands/check");
|
|
31
|
-
await check({ fix: opts.fix });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
program
|
|
35
|
-
.command("watch")
|
|
36
|
-
.description("Watch files and detect events in real-time")
|
|
37
|
-
.action(async () => {
|
|
38
|
-
const { watch } = await import("./commands/watch");
|
|
39
|
-
await watch();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
program
|
|
43
|
-
.command("send")
|
|
44
|
-
.action(async () => {
|
|
45
|
-
const { send } = await import("./commands/send");
|
|
46
|
-
await send();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
program.parse();
|