@eventra_dev/eventra-cli 0.3.6 → 0.3.10
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 +69 -185
- package/dist/analysis/engine/analyzeFile.js +12 -0
- package/dist/analysis/extractor/extractor.js +4 -5
- package/dist/analysis/extractor/propagationExtractor.js +4 -5
- package/dist/analysis/resolver/callResolver.js +4 -0
- package/dist/analysis/resolver/exportResolver.js +3 -0
- package/dist/analysis/scanner/scanner.js +1 -1
- package/dist/analysis/scanner/sinkDetector.js +17 -37
- package/dist/analysis/sdk/eventraSdk.js +42 -0
- package/dist/analysis/sdk/isEventraTrackCall.js +171 -0
- package/dist/analysis/shared/eventValidation.js +7 -0
- package/dist/analysis/symbols/wrapperRegistry.js +9 -15
- package/dist/cli/check.js +29 -47
- package/dist/cli/init.js +5 -6
- package/dist/cli/sync.js +5 -3
- package/dist/cli/watch.js +27 -33
- package/dist/compiler/compilerContext.js +71 -49
- package/dist/config/config.js +13 -2
- package/dist/core/EventraEngine.js +89 -37
- package/dist/core/projectScanner.js +13 -12
- package/dist/core/scanResults.js +18 -0
- package/dist/index.js +5 -1
- package/package.json +2 -2
- package/dist/analysis/engine/recursiveWrapperAnalyzer.js +0 -76
|
@@ -14,11 +14,12 @@ class CompilerContext {
|
|
|
14
14
|
registry = new documentRegistry_1.DocumentRegistry();
|
|
15
15
|
compilerOptions;
|
|
16
16
|
host;
|
|
17
|
-
|
|
17
|
+
program = typescript_1.default.createProgram([], {});
|
|
18
18
|
rootNames = new Set();
|
|
19
19
|
constructor(rootDir) {
|
|
20
20
|
this.rootDir = rootDir;
|
|
21
|
-
const
|
|
21
|
+
const localTsconfig = path_1.default.join(rootDir, "tsconfig.json");
|
|
22
|
+
const configPath = fs_1.default.existsSync(localTsconfig) ? localTsconfig : undefined;
|
|
22
23
|
let compilerOptions = {
|
|
23
24
|
target: typescript_1.default.ScriptTarget.ESNext,
|
|
24
25
|
module: typescript_1.default.ModuleKind.ESNext,
|
|
@@ -33,7 +34,7 @@ class CompilerContext {
|
|
|
33
34
|
esModuleInterop: true,
|
|
34
35
|
resolveJsonModule: true,
|
|
35
36
|
allowNonTsExtensions: true,
|
|
36
|
-
incremental:
|
|
37
|
+
incremental: false,
|
|
37
38
|
};
|
|
38
39
|
if (configPath) {
|
|
39
40
|
const config = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
|
|
@@ -52,30 +53,59 @@ class CompilerContext {
|
|
|
52
53
|
this.compilerOptions = compilerOptions;
|
|
53
54
|
this.host = typescript_1.default.createCompilerHost(this.compilerOptions, true);
|
|
54
55
|
this.patchCompilerHost();
|
|
55
|
-
this.
|
|
56
|
+
this.rebuildProgram();
|
|
56
57
|
}
|
|
57
58
|
normalize(fileName) {
|
|
58
59
|
return this.registry.normalize(fileName);
|
|
59
60
|
}
|
|
61
|
+
resolveLanguageVersion(languageVersionOrOptions) {
|
|
62
|
+
if (typeof languageVersionOrOptions === "object") {
|
|
63
|
+
return languageVersionOrOptions.languageVersion;
|
|
64
|
+
}
|
|
65
|
+
return languageVersionOrOptions;
|
|
66
|
+
}
|
|
67
|
+
attachVersion(sourceFile, fileName) {
|
|
68
|
+
const normalized = this.normalize(fileName);
|
|
69
|
+
sourceFile.version =
|
|
70
|
+
this.registry.getVersion(normalized);
|
|
71
|
+
return sourceFile;
|
|
72
|
+
}
|
|
73
|
+
createSourceFileFromRegistry(fileName, languageVersion) {
|
|
74
|
+
const normalized = this.normalize(fileName);
|
|
75
|
+
const snapshot = this.registry.getSnapshot(normalized);
|
|
76
|
+
if (!snapshot) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const sourceFile = typescript_1.default.createSourceFile(normalized, snapshot.getText(0, snapshot.getLength()), languageVersion, true, this.getScriptKind(normalized));
|
|
80
|
+
return this.attachVersion(sourceFile, normalized);
|
|
81
|
+
}
|
|
60
82
|
patchCompilerHost() {
|
|
61
|
-
const originalGetSourceFile = this.host.getSourceFile;
|
|
62
|
-
this.host.getSourceFile = (fileName,
|
|
83
|
+
const originalGetSourceFile = this.host.getSourceFile.bind(this.host);
|
|
84
|
+
this.host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
|
|
85
|
+
const languageVersion = this.resolveLanguageVersion(languageVersionOrOptions);
|
|
63
86
|
const normalized = this.normalize(fileName);
|
|
64
87
|
this.registry.ensure(normalized);
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
return
|
|
88
|
+
const fromRegistry = this.createSourceFileFromRegistry(normalized, languageVersion);
|
|
89
|
+
if (fromRegistry) {
|
|
90
|
+
return fromRegistry;
|
|
91
|
+
}
|
|
92
|
+
const fromDisk = originalGetSourceFile(normalized, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
93
|
+
if (!fromDisk) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
if (!this.registry.has(normalized)) {
|
|
97
|
+
this.registry.update(normalized, fromDisk.getFullText());
|
|
68
98
|
}
|
|
69
|
-
return
|
|
99
|
+
return this.attachVersion(fromDisk, normalized);
|
|
70
100
|
};
|
|
71
101
|
this.host.readFile = (fileName) => {
|
|
72
102
|
const normalized = this.normalize(fileName);
|
|
73
103
|
this.registry.ensure(normalized);
|
|
74
|
-
return
|
|
104
|
+
return this.registry.getContent(normalized) ?? fs_1.default.readFileSync(normalized, "utf8");
|
|
75
105
|
};
|
|
76
106
|
this.host.fileExists = (fileName) => {
|
|
77
107
|
const normalized = this.normalize(fileName);
|
|
78
|
-
return
|
|
108
|
+
return this.registry.has(normalized) || fs_1.default.existsSync(normalized);
|
|
79
109
|
};
|
|
80
110
|
this.host.getCanonicalFileName = (f) => this.normalize(f);
|
|
81
111
|
this.host.useCaseSensitiveFileNames = () => true;
|
|
@@ -83,64 +113,62 @@ class CompilerContext {
|
|
|
83
113
|
this.host.getNewLine = () => "\n";
|
|
84
114
|
}
|
|
85
115
|
getScriptKind(fileName) {
|
|
86
|
-
if (fileName.endsWith(".tsx"))
|
|
116
|
+
if (fileName.endsWith(".tsx"))
|
|
87
117
|
return typescript_1.default.ScriptKind.TSX;
|
|
88
|
-
|
|
89
|
-
if (fileName.endsWith(".ts")) {
|
|
118
|
+
if (fileName.endsWith(".ts"))
|
|
90
119
|
return typescript_1.default.ScriptKind.TS;
|
|
91
|
-
|
|
92
|
-
if (fileName.endsWith(".jsx")) {
|
|
120
|
+
if (fileName.endsWith(".jsx"))
|
|
93
121
|
return typescript_1.default.ScriptKind.JSX;
|
|
94
|
-
|
|
95
|
-
if (fileName.endsWith(".js")) {
|
|
122
|
+
if (fileName.endsWith(".js"))
|
|
96
123
|
return typescript_1.default.ScriptKind.JS;
|
|
97
|
-
|
|
98
|
-
if (fileName.endsWith(".json")) {
|
|
124
|
+
if (fileName.endsWith(".json"))
|
|
99
125
|
return typescript_1.default.ScriptKind.JSON;
|
|
100
|
-
}
|
|
101
126
|
return typescript_1.default.ScriptKind.Unknown;
|
|
102
127
|
}
|
|
103
|
-
|
|
104
|
-
this.
|
|
128
|
+
rebuildProgram() {
|
|
129
|
+
this.program = typescript_1.default.createProgram([...this.rootNames], this.compilerOptions, this.host);
|
|
130
|
+
}
|
|
131
|
+
/** Register file content without rebuilding the TS program (batch via {@link flushUpdates}). */
|
|
132
|
+
stageFile(fileName, content) {
|
|
133
|
+
const normalized = this.normalize(fileName);
|
|
134
|
+
this.registry.update(normalized, content);
|
|
135
|
+
this.rootNames.add(normalized);
|
|
136
|
+
}
|
|
137
|
+
flushUpdates() {
|
|
138
|
+
this.rebuildProgram();
|
|
105
139
|
}
|
|
106
140
|
async updateFile(fileName, content) {
|
|
107
141
|
const normalized = this.normalize(fileName);
|
|
108
142
|
const processed = await (0, processFile_1.processFile)(normalized, content);
|
|
109
|
-
this.
|
|
110
|
-
this.
|
|
111
|
-
this.rebuild();
|
|
143
|
+
this.stageFile(processed.fileName, processed.content);
|
|
144
|
+
this.rebuildProgram();
|
|
112
145
|
}
|
|
113
146
|
removeFile(fileName) {
|
|
114
147
|
const normalized = this.normalize(fileName);
|
|
115
148
|
this.registry.remove(normalized);
|
|
116
149
|
this.rootNames.delete(normalized);
|
|
117
|
-
this.
|
|
118
|
-
}
|
|
119
|
-
invalidate() {
|
|
120
|
-
this.registry.invalidate();
|
|
121
|
-
this.rebuild();
|
|
150
|
+
this.rebuildProgram();
|
|
122
151
|
}
|
|
123
152
|
getProgram() {
|
|
124
|
-
return this.
|
|
153
|
+
return this.program;
|
|
125
154
|
}
|
|
126
155
|
getChecker() {
|
|
127
|
-
return this.
|
|
156
|
+
return this.program.getTypeChecker();
|
|
128
157
|
}
|
|
129
158
|
getSourceFile(fileName) {
|
|
130
|
-
return this.
|
|
159
|
+
return this.program.getSourceFile(this.normalize(fileName));
|
|
131
160
|
}
|
|
132
161
|
getSemanticDiagnostics(fileName) {
|
|
133
|
-
const program = this.builder.getProgram();
|
|
134
162
|
if (!fileName) {
|
|
135
|
-
return typescript_1.default.getPreEmitDiagnostics(program);
|
|
163
|
+
return typescript_1.default.getPreEmitDiagnostics(this.program);
|
|
136
164
|
}
|
|
137
165
|
const sourceFile = this.getSourceFile(fileName);
|
|
138
166
|
if (!sourceFile) {
|
|
139
167
|
return [];
|
|
140
168
|
}
|
|
141
169
|
return [
|
|
142
|
-
...program.getSemanticDiagnostics(sourceFile),
|
|
143
|
-
...program.getSyntacticDiagnostics(sourceFile),
|
|
170
|
+
...this.program.getSemanticDiagnostics(sourceFile),
|
|
171
|
+
...this.program.getSyntacticDiagnostics(sourceFile),
|
|
144
172
|
];
|
|
145
173
|
}
|
|
146
174
|
resolveModule(moduleName, fromFile) {
|
|
@@ -162,20 +190,14 @@ class CompilerContext {
|
|
|
162
190
|
continue;
|
|
163
191
|
}
|
|
164
192
|
const resolved = this.resolveModule(specifier.text, source.fileName);
|
|
165
|
-
if (
|
|
166
|
-
|
|
193
|
+
if (resolved) {
|
|
194
|
+
modules.add(this.normalize(resolved));
|
|
167
195
|
}
|
|
168
|
-
modules.add(this.normalize(resolved));
|
|
169
196
|
}
|
|
170
|
-
return [
|
|
171
|
-
...modules,
|
|
172
|
-
];
|
|
197
|
+
return [...modules];
|
|
173
198
|
}
|
|
174
199
|
getAllSourceFiles() {
|
|
175
|
-
return this.
|
|
176
|
-
}
|
|
177
|
-
getRealFileName(fileName) {
|
|
178
|
-
return fileName;
|
|
200
|
+
return this.program.getSourceFiles().filter((sf) => !sf.isDeclarationFile);
|
|
179
201
|
}
|
|
180
202
|
}
|
|
181
203
|
exports.CompilerContext = CompilerContext;
|
package/dist/config/config.js
CHANGED
|
@@ -13,10 +13,21 @@ exports.CONFIG_NAME = "eventra.json";
|
|
|
13
13
|
const DEFAULT_INCLUDE = ["**/*.{ts,tsx,js,jsx}"];
|
|
14
14
|
const DEFAULT_EXCLUDE = ["node_modules", "dist", ".next", ".git"];
|
|
15
15
|
function normalizeConfig(config) {
|
|
16
|
+
const functionWrappers = (config.functionWrappers ?? [])
|
|
17
|
+
.filter((w) => w?.name)
|
|
18
|
+
.map((w) => ({ name: w.name }));
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const uniqueWrappers = functionWrappers.filter((w) => {
|
|
21
|
+
if (seen.has(w.name))
|
|
22
|
+
return false;
|
|
23
|
+
seen.add(w.name);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
16
26
|
return {
|
|
17
27
|
apiKey: config.apiKey ?? "",
|
|
18
28
|
endpoint: config.endpoint ?? "",
|
|
19
|
-
events: config.events ?? [],
|
|
29
|
+
events: [...(config.events ?? [])].sort(),
|
|
30
|
+
functionWrappers: uniqueWrappers.sort((a, b) => a.name.localeCompare(b.name)),
|
|
20
31
|
sync: {
|
|
21
32
|
include: config.sync?.include ?? DEFAULT_INCLUDE,
|
|
22
33
|
exclude: config.sync?.exclude ?? DEFAULT_EXCLUDE,
|
|
@@ -25,7 +36,7 @@ function normalizeConfig(config) {
|
|
|
25
36
|
}
|
|
26
37
|
async function loadConfig() {
|
|
27
38
|
const configPath = path_1.default.join(process.cwd(), exports.CONFIG_NAME);
|
|
28
|
-
if (!await fs_extra_1.default.pathExists(configPath)) {
|
|
39
|
+
if (!(await fs_extra_1.default.pathExists(configPath))) {
|
|
29
40
|
return null;
|
|
30
41
|
}
|
|
31
42
|
try {
|
|
@@ -14,8 +14,9 @@ const resolvedCallCache_1 = require("../analysis/cache/resolvedCallCache");
|
|
|
14
14
|
const resolvedExportCache_1 = require("../analysis/cache/resolvedExportCache");
|
|
15
15
|
const returnPropagationCache_1 = require("../analysis/cache/returnPropagationCache");
|
|
16
16
|
const symbolInvalidation_1 = require("../analysis/cache/symbolInvalidation");
|
|
17
|
+
const analyzeFile_1 = require("../analysis/engine/analyzeFile");
|
|
17
18
|
const wrapperRegistry_1 = require("../analysis/symbols/wrapperRegistry");
|
|
18
|
-
const
|
|
19
|
+
const processFile_1 = require("../filesystem/processFile");
|
|
19
20
|
const EMPTY_RESULT = () => ({
|
|
20
21
|
events: new Set(),
|
|
21
22
|
detectedFunctionWrappers: new Set(),
|
|
@@ -29,11 +30,13 @@ class EventraEngine {
|
|
|
29
30
|
returnPropagationCache = new returnPropagationCache_1.ReturnPropagationCache();
|
|
30
31
|
resolvedExportCache = new resolvedExportCache_1.ResolvedExportCache();
|
|
31
32
|
wrapperRegistry;
|
|
33
|
+
fileIndices = new Map();
|
|
32
34
|
fileResults = new Map();
|
|
33
35
|
normalizedPathCache = new Map();
|
|
34
36
|
isPreloading = false;
|
|
35
37
|
lastConfig = {
|
|
36
38
|
events: [],
|
|
39
|
+
functionWrappers: [],
|
|
37
40
|
sync: {
|
|
38
41
|
include: [],
|
|
39
42
|
exclude: [],
|
|
@@ -43,7 +46,7 @@ class EventraEngine {
|
|
|
43
46
|
this.compiler = new compilerContext_1.CompilerContext(rootDir);
|
|
44
47
|
this.wrapperRegistry = new wrapperRegistry_1.WrapperRegistry(this.compiler.getChecker(), this.resolvedExportCache);
|
|
45
48
|
this.scheduler = new scheduler_1.Scheduler(async (updates) => {
|
|
46
|
-
|
|
49
|
+
const affected = new Set();
|
|
47
50
|
for (const [file, content] of updates) {
|
|
48
51
|
const existing = this.compiler.getSourceFile(file);
|
|
49
52
|
if (existing) {
|
|
@@ -51,27 +54,17 @@ class EventraEngine {
|
|
|
51
54
|
}
|
|
52
55
|
await this.compiler.updateFile(file, content);
|
|
53
56
|
this.updateImportGraph(file);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const affected = new Set();
|
|
57
|
-
for (const file of updates.keys()) {
|
|
58
|
-
const dependents = this.importGraph.collectDependents(file);
|
|
59
|
-
for (const dependent of dependents) {
|
|
57
|
+
affected.add(file);
|
|
58
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
60
59
|
affected.add(dependent);
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
|
-
|
|
64
|
-
for (const file of affected) {
|
|
65
|
-
await this.analyzeFile(file, this.lastConfig);
|
|
66
|
-
}
|
|
62
|
+
await this.reanalyzeFiles([...affected], this.lastConfig);
|
|
67
63
|
});
|
|
68
64
|
}
|
|
69
65
|
beginPreload() {
|
|
70
66
|
this.isPreloading = true;
|
|
71
67
|
}
|
|
72
|
-
endPreload() {
|
|
73
|
-
this.isPreloading = false;
|
|
74
|
-
}
|
|
75
68
|
normalize(fileName) {
|
|
76
69
|
const cached = this.normalizedPathCache.get(fileName);
|
|
77
70
|
if (cached) {
|
|
@@ -113,45 +106,92 @@ class EventraEngine {
|
|
|
113
106
|
throw new Error("preload phase not active");
|
|
114
107
|
}
|
|
115
108
|
const normalized = this.normalize(fileName);
|
|
116
|
-
await
|
|
117
|
-
this.
|
|
109
|
+
const processed = await (0, processFile_1.processFile)(normalized, content);
|
|
110
|
+
this.compiler.stageFile(processed.fileName, processed.content);
|
|
111
|
+
}
|
|
112
|
+
endPreload() {
|
|
113
|
+
this.isPreloading = false;
|
|
114
|
+
this.compiler.flushUpdates();
|
|
115
|
+
for (const sourceFile of this.compiler.getAllSourceFiles()) {
|
|
116
|
+
this.updateImportGraph(sourceFile.fileName);
|
|
117
|
+
}
|
|
118
118
|
}
|
|
119
119
|
async updateFile(fileName, content, config) {
|
|
120
120
|
this.lastConfig = config;
|
|
121
121
|
const normalized = this.normalize(fileName);
|
|
122
122
|
await this.scheduler.enqueue(normalized, content);
|
|
123
123
|
}
|
|
124
|
+
refreshChecker() {
|
|
125
|
+
this.wrapperRegistry.setChecker(this.compiler.getChecker());
|
|
126
|
+
}
|
|
127
|
+
async runFullAnalysis(fileNames, config) {
|
|
128
|
+
this.lastConfig = config;
|
|
129
|
+
this.refreshChecker();
|
|
130
|
+
const normalized = fileNames.map((f) => this.normalize(f));
|
|
131
|
+
for (const file of normalized) {
|
|
132
|
+
this.indexFile(file, config);
|
|
133
|
+
}
|
|
134
|
+
for (const file of normalized) {
|
|
135
|
+
this.extractFile(file, config);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async reanalyzeFiles(fileNames, config) {
|
|
139
|
+
this.lastConfig = config;
|
|
140
|
+
this.refreshChecker();
|
|
141
|
+
const normalized = fileNames.map((f) => this.normalize(f));
|
|
142
|
+
for (const file of normalized) {
|
|
143
|
+
this.indexFile(file, config);
|
|
144
|
+
}
|
|
145
|
+
for (const file of normalized) {
|
|
146
|
+
this.extractFile(file, config);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
indexFile(fileName, _config) {
|
|
150
|
+
const normalized = this.normalize(fileName);
|
|
151
|
+
const source = this.compiler.getSourceFile(normalized);
|
|
152
|
+
if (!source) {
|
|
153
|
+
this.fileIndices.delete(normalized);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const index = (0, analyzeFile_1.indexSourceFile)(source, this.compiler.getChecker(), this.wrapperRegistry);
|
|
157
|
+
this.fileIndices.set(normalized, index);
|
|
158
|
+
}
|
|
159
|
+
extractFile(fileName, config) {
|
|
160
|
+
const normalized = this.normalize(fileName);
|
|
161
|
+
const index = this.fileIndices.get(normalized);
|
|
162
|
+
if (!index) {
|
|
163
|
+
this.fileResults.delete(normalized);
|
|
164
|
+
return EMPTY_RESULT();
|
|
165
|
+
}
|
|
166
|
+
const result = (0, analyzeFile_1.extractFromIndex)(index, config, this.compiler.getChecker(), this.wrapperRegistry, this.evaluationCache, this.resolvedCallCache, this.returnPropagationCache, this.resolvedExportCache);
|
|
167
|
+
this.fileResults.set(normalized, result);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
124
170
|
async scanFile(fileName, config) {
|
|
125
171
|
this.lastConfig = config;
|
|
126
|
-
|
|
172
|
+
this.refreshChecker();
|
|
173
|
+
this.indexFile(fileName, config);
|
|
174
|
+
return this.extractFile(fileName, config);
|
|
127
175
|
}
|
|
128
176
|
async removeFile(fileName, config) {
|
|
129
177
|
const normalized = this.normalize(fileName);
|
|
178
|
+
const existing = this.compiler.getSourceFile(normalized);
|
|
179
|
+
if (existing) {
|
|
180
|
+
(0, symbolInvalidation_1.invalidateSourceFileSymbols)(existing, this.compiler.getChecker(), this.evaluationCache, this.resolvedCallCache, this.resolvedExportCache, this.returnPropagationCache, this.wrapperRegistry);
|
|
181
|
+
}
|
|
130
182
|
const affected = this.importGraph.collectDependents(normalized);
|
|
131
183
|
this.importGraph.removeFile(normalized);
|
|
132
184
|
this.compiler.removeFile(normalized);
|
|
185
|
+
this.fileIndices.delete(normalized);
|
|
133
186
|
this.fileResults.delete(normalized);
|
|
134
187
|
this.normalizedPathCache.delete(fileName);
|
|
135
188
|
this.normalizedPathCache.delete(normalized);
|
|
136
189
|
const nextConfig = config ?? this.lastConfig;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
await this.analyzeFile(file, nextConfig);
|
|
190
|
+
const toReanalyze = [...affected].filter((f) => f !== normalized);
|
|
191
|
+
if (toReanalyze.length > 0) {
|
|
192
|
+
await this.reanalyzeFiles(toReanalyze, nextConfig);
|
|
142
193
|
}
|
|
143
194
|
}
|
|
144
|
-
async analyzeFile(fileName, config) {
|
|
145
|
-
const normalized = this.normalize(fileName);
|
|
146
|
-
const source = this.compiler.getSourceFile(normalized);
|
|
147
|
-
if (!source) {
|
|
148
|
-
this.fileResults.delete(normalized);
|
|
149
|
-
return EMPTY_RESULT();
|
|
150
|
-
}
|
|
151
|
-
const result = await (0, recursiveWrapperAnalyzer_1.analyzeFileRecursive)(source, config, this.wrapperRegistry, this.evaluationCache, this.resolvedCallCache, this.returnPropagationCache, this.resolvedExportCache);
|
|
152
|
-
this.fileResults.set(normalized, result);
|
|
153
|
-
return result;
|
|
154
|
-
}
|
|
155
195
|
getAllEvents() {
|
|
156
196
|
const events = new Set();
|
|
157
197
|
for (const result of this.fileResults.values()) {
|
|
@@ -159,9 +199,21 @@ class EventraEngine {
|
|
|
159
199
|
events.add(event);
|
|
160
200
|
}
|
|
161
201
|
}
|
|
162
|
-
return [
|
|
163
|
-
|
|
164
|
-
|
|
202
|
+
return [...events].sort();
|
|
203
|
+
}
|
|
204
|
+
getAllFunctionWrappers() {
|
|
205
|
+
const wrappers = new Set();
|
|
206
|
+
for (const result of this.fileResults.values()) {
|
|
207
|
+
for (const name of result.detectedFunctionWrappers) {
|
|
208
|
+
if (name && name !== "anonymous" && !name.startsWith("__")) {
|
|
209
|
+
wrappers.add(name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return [...wrappers].sort();
|
|
214
|
+
}
|
|
215
|
+
getScanResult(fileName) {
|
|
216
|
+
return this.fileResults.get(this.normalize(fileName)) ?? EMPTY_RESULT();
|
|
165
217
|
}
|
|
166
218
|
}
|
|
167
219
|
exports.EventraEngine = EventraEngine;
|
|
@@ -7,9 +7,11 @@ exports.scanProject = scanProject;
|
|
|
7
7
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const eventraSdk_1 = require("../analysis/sdk/eventraSdk");
|
|
10
11
|
const EventraEngine_1 = require("./EventraEngine");
|
|
11
12
|
const processFile_1 = require("../filesystem/processFile");
|
|
12
13
|
const getVirtualFile_1 = require("../filesystem/getVirtualFile");
|
|
14
|
+
const SDK_TYPES_FILE = "__eventra_sdk_types__.d.ts";
|
|
13
15
|
async function getParsedFile(file, cache) {
|
|
14
16
|
const existing = cache.get(file);
|
|
15
17
|
if (existing)
|
|
@@ -27,7 +29,6 @@ async function scanProject(config) {
|
|
|
27
29
|
const engine = new EventraEngine_1.EventraEngine(process.cwd());
|
|
28
30
|
const cache = new Map();
|
|
29
31
|
const toScan = new Set();
|
|
30
|
-
// COLLECT
|
|
31
32
|
for (const file of files) {
|
|
32
33
|
try {
|
|
33
34
|
const raw = await promises_1.default.readFile(file, "utf8");
|
|
@@ -37,32 +38,32 @@ async function scanProject(config) {
|
|
|
37
38
|
cache.set(file, parsed);
|
|
38
39
|
toScan.add(file);
|
|
39
40
|
for (const dep of parsed.dependencies) {
|
|
40
|
-
|
|
41
|
+
if (dep.startsWith(".") || dep.startsWith("/")) {
|
|
42
|
+
toScan.add(path_1.default.resolve(path_1.default.dirname(file), dep));
|
|
43
|
+
}
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
|
-
catch {
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.error(`skip: ${file}`, err instanceof Error ? err.message : err);
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
|
-
|
|
50
|
+
const virtualFiles = [SDK_TYPES_FILE, ...toScan].map((f) => (0, getVirtualFile_1.getVirtualFile)(f));
|
|
47
51
|
engine.beginPreload();
|
|
52
|
+
await engine.preloadFile(SDK_TYPES_FILE, eventraSdk_1.EVENTRA_SDK_SHIM);
|
|
48
53
|
for (const file of toScan) {
|
|
49
54
|
try {
|
|
50
55
|
const parsed = await getParsedFile(file, cache);
|
|
51
56
|
await engine.preloadFile((0, getVirtualFile_1.getVirtualFile)(file), parsed.content);
|
|
52
57
|
}
|
|
53
|
-
catch {
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error(`preload skip: ${file}`, err instanceof Error ? err.message : err);
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
engine.endPreload();
|
|
57
|
-
|
|
63
|
+
await engine.runFullAnalysis(virtualFiles, config);
|
|
58
64
|
const results = new Map();
|
|
59
65
|
for (const file of toScan) {
|
|
60
|
-
|
|
61
|
-
const result = await engine.scanFile((0, getVirtualFile_1.getVirtualFile)(file), config);
|
|
62
|
-
results.set(file, result);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
}
|
|
66
|
+
results.set(file, engine.getScanResult((0, getVirtualFile_1.getVirtualFile)(file)));
|
|
66
67
|
}
|
|
67
68
|
return {
|
|
68
69
|
engine,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildConfigFromScan = buildConfigFromScan;
|
|
4
|
+
exports.persistScanResults = persistScanResults;
|
|
5
|
+
const config_1 = require("../config/config");
|
|
6
|
+
function buildConfigFromScan(config, engine) {
|
|
7
|
+
const functionWrappers = engine
|
|
8
|
+
.getAllFunctionWrappers()
|
|
9
|
+
.map((name) => ({ name }));
|
|
10
|
+
return {
|
|
11
|
+
...config,
|
|
12
|
+
events: engine.getAllEvents(),
|
|
13
|
+
functionWrappers,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function persistScanResults(config, engine) {
|
|
17
|
+
await (0, config_1.saveConfig)(buildConfigFromScan(config, engine));
|
|
18
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -33,13 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
return result;
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
36
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
40
|
const commander_1 = require("commander");
|
|
41
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
38
42
|
const program = new commander_1.Command();
|
|
39
43
|
program
|
|
40
44
|
.name("eventra")
|
|
41
45
|
.description("Eventra CLI")
|
|
42
|
-
.version(
|
|
46
|
+
.version(package_json_1.default.version);
|
|
43
47
|
program
|
|
44
48
|
.command("init")
|
|
45
49
|
.action(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventra_dev/eventra-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsc -p tsconfig.json",
|
|
27
27
|
"dev": "tsx src/index.ts",
|
|
28
|
-
"test": "tsx tests/run.ts",
|
|
28
|
+
"test": "pnpm run clean && pnpm run build && tsx tests/run.ts",
|
|
29
29
|
"clean": "rm -rf dist"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.analyzeFileRecursive = analyzeFileRecursive;
|
|
7
|
-
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
-
const propagationAnalyzer_1 = require("../scanner/analyzer/propagationAnalyzer");
|
|
9
|
-
const extractor_1 = require("../extractor/extractor");
|
|
10
|
-
async function analyzeFileRecursive(sourceFile, config, wrapperRegistry, evaluationCache, resolvedCallCache, returnPropagationCache, resolvedExportCache) {
|
|
11
|
-
const checker = wrapperRegistry["checker"];
|
|
12
|
-
const sinks = [];
|
|
13
|
-
const wrappers = [];
|
|
14
|
-
const trackCalls = [];
|
|
15
|
-
// Support empty functionWrappers
|
|
16
|
-
const wrapperNames = new Set(config.functionWrappers?.map(w => w.name) ?? []);
|
|
17
|
-
function visit(node, parentFunction) {
|
|
18
|
-
// Detect calls to configured wrapper functions
|
|
19
|
-
if (typescript_1.default.isCallExpression(node)) {
|
|
20
|
-
const callName = getCallName(node.expression);
|
|
21
|
-
if (wrapperNames.has(callName)) {
|
|
22
|
-
// Track this wrapper call
|
|
23
|
-
sinks.push({ call: node, trackedArguments: [{ index: 0, propertyPath: [] }] });
|
|
24
|
-
trackCalls.push({
|
|
25
|
-
node,
|
|
26
|
-
sourceFile,
|
|
27
|
-
trackedArguments: node.arguments.filter(typescript_1.default.isExpression)
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// Detect and analyze function declarations / expressions / methods
|
|
32
|
-
if (typescript_1.default.isFunctionDeclaration(node) ||
|
|
33
|
-
typescript_1.default.isFunctionExpression(node) ||
|
|
34
|
-
typescript_1.default.isArrowFunction(node) ||
|
|
35
|
-
typescript_1.default.isMethodDeclaration(node)) {
|
|
36
|
-
const localSinks = sinks.filter(s => isNodeInside(s.call, node));
|
|
37
|
-
const wrapper = (0, propagationAnalyzer_1.analyzeWrapperPropagation)(node, checker, localSinks);
|
|
38
|
-
if (wrapper) {
|
|
39
|
-
wrappers.push(wrapper);
|
|
40
|
-
wrapperRegistry.set(wrapper);
|
|
41
|
-
}
|
|
42
|
-
// Recurse into function body
|
|
43
|
-
if (node.body) {
|
|
44
|
-
typescript_1.default.forEachChild(node.body, child => visit(child, node));
|
|
45
|
-
}
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
// Recurse into child nodes
|
|
49
|
-
typescript_1.default.forEachChild(node, child => visit(child, parentFunction));
|
|
50
|
-
}
|
|
51
|
-
visit(sourceFile);
|
|
52
|
-
// Extract events from tracked calls and wrapper metadata
|
|
53
|
-
return (0, extractor_1.extractEvents)({
|
|
54
|
-
fileName: sourceFile.fileName,
|
|
55
|
-
sourceFile,
|
|
56
|
-
sinks,
|
|
57
|
-
trackCalls,
|
|
58
|
-
wrappers,
|
|
59
|
-
}, checker, config, evaluationCache, resolvedExportCache, resolvedCallCache, returnPropagationCache, wrapperRegistry);
|
|
60
|
-
}
|
|
61
|
-
function isNodeInside(child, fn) {
|
|
62
|
-
let current = child;
|
|
63
|
-
while (current) {
|
|
64
|
-
if (current === fn)
|
|
65
|
-
return true;
|
|
66
|
-
current = current.parent;
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
function getCallName(expr) {
|
|
71
|
-
if (typescript_1.default.isIdentifier(expr))
|
|
72
|
-
return expr.text;
|
|
73
|
-
if (typescript_1.default.isPropertyAccessExpression(expr) || typescript_1.default.isPropertyAccessChain(expr))
|
|
74
|
-
return expr.name.text;
|
|
75
|
-
return "";
|
|
76
|
-
}
|