@eventra_dev/eventra-cli 0.1.34 → 0.2.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/dist/commands/check.js +14 -30
- package/dist/commands/sync.js +9 -27
- package/dist/commands/watch.js +38 -121
- package/dist/engine/engine.js +14 -9
- package/dist/engine/languageService.js +29 -24
- package/dist/engine/scanner.js +41 -28
- package/dist/utils/processFile.js +17 -0
- package/dist/utils/universal.js +1 -1
- package/package.json +1 -1
- package/src/commands/check.ts +17 -48
- package/src/commands/sync.ts +14 -44
- package/src/commands/watch.ts +48 -158
- package/src/engine/engine.ts +20 -12
- package/src/engine/languageService.ts +35 -32
- package/src/engine/scanner.ts +67 -27
- package/src/utils/processFile.ts +19 -0
- package/src/utils/universal.ts +1 -1
package/src/commands/watch.ts
CHANGED
|
@@ -9,92 +9,39 @@ import { EventraEngine } from "../engine/engine";
|
|
|
9
9
|
import { processFile } from "../utils/processFile";
|
|
10
10
|
import { getVirtualFile } from "../utils/getVirtualFile";
|
|
11
11
|
|
|
12
|
-
type QueueItem = {
|
|
13
|
-
real: string;
|
|
14
|
-
virtual: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function normalize(file: string) {
|
|
18
|
-
return path.resolve(file).replace(/\\/g, "/");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
12
|
export async function watch() {
|
|
22
13
|
const config = await loadConfig();
|
|
23
14
|
if (!config) return;
|
|
24
15
|
|
|
25
16
|
console.log(chalk.blue("Watching...\n"));
|
|
26
17
|
|
|
27
|
-
const files = await fg(config.sync.include, {
|
|
28
|
-
ignore: config.sync.exclude,
|
|
29
|
-
absolute: true,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
18
|
const engine = new EventraEngine(process.cwd());
|
|
19
|
+
const fileDeps = new Map<string, Set<string>>();
|
|
20
|
+
const cache = new Map<string, { content: string; deps: string[] }>();
|
|
33
21
|
const watchedDeps = new Set<string>();
|
|
34
22
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
{ content: string; deps: string[] }
|
|
38
|
-
>();
|
|
23
|
+
const normalize = (f: string) =>
|
|
24
|
+
path.resolve(f).replace(/\\/g, "/");
|
|
39
25
|
|
|
40
|
-
|
|
26
|
+
// INITIAL LOAD
|
|
27
|
+
const files = await fg(config.sync.include, {
|
|
28
|
+
ignore: config.sync.exclude,
|
|
29
|
+
absolute: true,
|
|
30
|
+
});
|
|
41
31
|
|
|
42
|
-
// COLLECT + CACHE
|
|
43
32
|
for (const file of files) {
|
|
44
33
|
try {
|
|
45
34
|
const abs = normalize(file);
|
|
46
35
|
const raw = await fs.readFile(abs, "utf-8");
|
|
47
36
|
|
|
48
|
-
if (raw.length > 2_000_000) continue;
|
|
49
|
-
|
|
50
37
|
const parsed = processFile(abs, raw);
|
|
51
38
|
|
|
52
39
|
cache.set(abs, parsed);
|
|
53
|
-
toScan.add(abs);
|
|
54
40
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
41
|
+
engine.preloadFile(getVirtualFile(abs), parsed.content);
|
|
42
|
+
engine.updateFile(getVirtualFile(abs), parsed.content, config);
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
console.log(chalk.gray(`skip: ${file}`));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// PRELOAD
|
|
65
|
-
for (const file of toScan) {
|
|
66
|
-
try {
|
|
67
|
-
let parsed = cache.get(file);
|
|
68
|
-
|
|
69
|
-
if (!parsed) {
|
|
70
|
-
const raw = await fs.readFile(file, "utf-8");
|
|
71
|
-
parsed = processFile(file, raw);
|
|
72
|
-
cache.set(file, parsed);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const virtual = getVirtualFile(file);
|
|
76
|
-
engine.preloadFile?.(virtual, parsed.content);
|
|
77
|
-
|
|
78
|
-
} catch {}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// INITIAL SCAN
|
|
82
|
-
for (const file of toScan) {
|
|
83
|
-
try {
|
|
84
|
-
let parsed = cache.get(file);
|
|
85
|
-
|
|
86
|
-
if (!parsed) {
|
|
87
|
-
const raw = await fs.readFile(file, "utf-8");
|
|
88
|
-
parsed = processFile(file, raw);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const virtual = getVirtualFile(file);
|
|
92
|
-
|
|
93
|
-
engine.scanFile(virtual, parsed.content, config);
|
|
94
|
-
|
|
95
|
-
for (const dep of parsed.deps) {
|
|
96
|
-
watchedDeps.add(normalize(dep));
|
|
97
|
-
}
|
|
44
|
+
fileDeps.set(abs, new Set(parsed.deps.map(d => normalize(d))));
|
|
98
45
|
|
|
99
46
|
} catch {}
|
|
100
47
|
}
|
|
@@ -102,152 +49,95 @@ export async function watch() {
|
|
|
102
49
|
config.events = engine.getAllEvents().sort();
|
|
103
50
|
await saveConfig(config);
|
|
104
51
|
|
|
105
|
-
console.log(
|
|
106
|
-
|
|
52
|
+
console.log(chalk.gray(`Initial: ${config.events.length}\n`));
|
|
53
|
+
|
|
54
|
+
// WATCHER
|
|
55
|
+
const watcher = chokidar.watch(
|
|
56
|
+
[...files.map(normalize), ...watchedDeps],
|
|
57
|
+
{ ignoreInitial: true }
|
|
107
58
|
);
|
|
108
59
|
|
|
109
|
-
|
|
110
|
-
let locked = false;
|
|
111
|
-
const queued = new Map<string, QueueItem>();
|
|
60
|
+
const queue = new Map<string, boolean>();
|
|
112
61
|
let timer: NodeJS.Timeout | null = null;
|
|
113
62
|
|
|
114
63
|
const run = async () => {
|
|
115
|
-
|
|
116
|
-
|
|
64
|
+
const batch = [...queue.keys()];
|
|
65
|
+
queue.clear();
|
|
117
66
|
|
|
118
|
-
const
|
|
119
|
-
queued.clear();
|
|
120
|
-
|
|
121
|
-
for (const item of batch) {
|
|
67
|
+
for (const file of batch) {
|
|
122
68
|
try {
|
|
123
|
-
const raw = await fs.readFile(
|
|
124
|
-
|
|
125
|
-
if (raw.length > 2_000_000) continue;
|
|
69
|
+
const raw = await fs.readFile(file, "utf-8");
|
|
70
|
+
const parsed = processFile(file, raw);
|
|
126
71
|
|
|
127
|
-
|
|
128
|
-
cache.set(item.real, parsed);
|
|
72
|
+
cache.set(file, parsed);
|
|
129
73
|
|
|
130
|
-
engine.updateFile(
|
|
74
|
+
engine.updateFile(
|
|
75
|
+
getVirtualFile(file),
|
|
76
|
+
parsed.content,
|
|
77
|
+
config
|
|
78
|
+
);
|
|
131
79
|
|
|
132
|
-
//
|
|
133
|
-
const
|
|
80
|
+
//dependency diff
|
|
81
|
+
const abs = normalize(file);
|
|
134
82
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
83
|
+
const prevDeps = fileDeps.get(abs) ?? new Set();
|
|
84
|
+
const nextDeps = new Set(parsed.deps.map(d => normalize(d)));
|
|
138
85
|
|
|
139
86
|
// add new deps
|
|
140
87
|
for (const dep of nextDeps) {
|
|
141
|
-
if (!
|
|
88
|
+
if (!prevDeps.has(dep)) {
|
|
142
89
|
watcher.add(dep);
|
|
143
90
|
watchedDeps.add(dep);
|
|
144
91
|
}
|
|
145
92
|
}
|
|
146
93
|
|
|
147
94
|
// remove old deps
|
|
148
|
-
for (const dep of
|
|
95
|
+
for (const dep of prevDeps) {
|
|
149
96
|
if (!nextDeps.has(dep)) {
|
|
150
97
|
watcher.unwatch(dep);
|
|
151
98
|
watchedDeps.delete(dep);
|
|
152
99
|
}
|
|
153
100
|
}
|
|
154
101
|
|
|
155
|
-
|
|
156
|
-
for (const dep of nextDeps) {
|
|
157
|
-
try {
|
|
158
|
-
const raw = await fs.readFile(dep, "utf-8");
|
|
159
|
-
const parsedDep = processFile(dep, raw);
|
|
160
|
-
|
|
161
|
-
cache.set(dep, parsedDep);
|
|
162
|
-
|
|
163
|
-
const virtual = getVirtualFile(dep);
|
|
164
|
-
|
|
165
|
-
engine.updateFile(virtual, parsedDep.content, config);
|
|
166
|
-
} catch {}
|
|
167
|
-
}
|
|
102
|
+
fileDeps.set(abs, nextDeps);
|
|
168
103
|
|
|
169
104
|
} catch {
|
|
170
|
-
console.log(chalk.gray(`skip: ${
|
|
105
|
+
console.log(chalk.gray(`skip: ${file}`));
|
|
171
106
|
}
|
|
172
107
|
}
|
|
173
108
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const added = next.filter(e => !config.events.includes(e));
|
|
178
|
-
const removed = config.events.filter(e => !next.includes(e));
|
|
179
|
-
|
|
180
|
-
if (added.length || removed.length) {
|
|
181
|
-
added.forEach(e => console.log(chalk.green(`+ ${e}`)));
|
|
182
|
-
removed.forEach(e => console.log(chalk.red(`- ${e}`)));
|
|
109
|
+
const next = engine.getAllEvents().sort();
|
|
110
|
+
config.events = next;
|
|
183
111
|
|
|
184
|
-
|
|
185
|
-
await saveConfig(config);
|
|
186
|
-
|
|
187
|
-
console.log(
|
|
188
|
-
chalk.gray(`Total: ${config.events.length} events\n`)
|
|
189
|
-
);
|
|
190
|
-
}
|
|
112
|
+
await saveConfig(config);
|
|
191
113
|
|
|
192
|
-
|
|
114
|
+
console.log(chalk.green(`Updated: ${next.length}`));
|
|
193
115
|
};
|
|
194
116
|
|
|
195
|
-
// SCHEDULER
|
|
196
117
|
const schedule = (file: string) => {
|
|
197
118
|
const abs = normalize(file);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
console.log(chalk.gray(`change: ${file}`));
|
|
201
|
-
|
|
202
|
-
queued.set(abs, { real: abs, virtual });
|
|
119
|
+
queue.set(abs, true);
|
|
203
120
|
|
|
204
121
|
if (timer) clearTimeout(timer);
|
|
205
122
|
timer = setTimeout(run, 200);
|
|
206
123
|
};
|
|
207
124
|
|
|
208
|
-
// WATCHER
|
|
209
|
-
const watcher = chokidar.watch(config.sync.include, {
|
|
210
|
-
ignored: config.sync.exclude,
|
|
211
|
-
ignoreInitial: true,
|
|
212
|
-
persistent: true,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
125
|
watcher
|
|
216
|
-
.on("add", schedule)
|
|
217
126
|
.on("change", schedule)
|
|
127
|
+
.on("add", schedule)
|
|
218
128
|
.on("unlink", async (file) => {
|
|
219
129
|
const abs = normalize(file);
|
|
220
130
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const virtual = getVirtualFile(abs);
|
|
224
|
-
|
|
225
|
-
engine.removeFile(virtual, config);
|
|
131
|
+
engine.removeFile(getVirtualFile(abs), config);
|
|
226
132
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const next = engine.getAllEvents();
|
|
231
|
-
|
|
232
|
-
config.events = next.sort();
|
|
133
|
+
const next = engine.getAllEvents().sort();
|
|
134
|
+
config.events = next;
|
|
233
135
|
await saveConfig(config);
|
|
234
136
|
|
|
235
|
-
console.log(
|
|
236
|
-
chalk.gray(`Total: ${config.events.length} events\n`)
|
|
237
|
-
);
|
|
238
|
-
})
|
|
239
|
-
.on("error", (err) => {
|
|
240
|
-
console.error(chalk.red("Watcher error:"), err);
|
|
137
|
+
console.log(chalk.red(`Removed → ${next.length}`));
|
|
241
138
|
});
|
|
242
139
|
|
|
243
|
-
// WATCH DEPS INIT
|
|
244
|
-
for (const dep of watchedDeps) {
|
|
245
|
-
watcher.add(dep);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// EXIT
|
|
249
140
|
process.on("SIGINT", async () => {
|
|
250
|
-
console.log(chalk.yellow("\nStopping watcher..."));
|
|
251
141
|
await watcher.close();
|
|
252
142
|
process.exit(0);
|
|
253
143
|
});
|
package/src/engine/engine.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { TSService } from "./languageService";
|
|
|
5
5
|
import { scanSource } from "./scanner";
|
|
6
6
|
import { DependencyGraph } from "./graph";
|
|
7
7
|
import { hash } from "./hash";
|
|
8
|
-
import {EventraConfig, ScanResult} from "../types";
|
|
8
|
+
import { EventraConfig, ScanResult } from "../types";
|
|
9
9
|
import { isExternalFile } from "./boundary";
|
|
10
10
|
|
|
11
11
|
export class EventraEngine {
|
|
@@ -46,7 +46,7 @@ export class EventraEngine {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// SCAN FILE
|
|
49
|
+
// SCAN FILE (cached)
|
|
50
50
|
scanFile(
|
|
51
51
|
file: string,
|
|
52
52
|
content: string,
|
|
@@ -71,7 +71,6 @@ export class EventraEngine {
|
|
|
71
71
|
return this.empty();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// update graph
|
|
75
74
|
this.graph.update(file, source);
|
|
76
75
|
|
|
77
76
|
const res = scanSource(
|
|
@@ -82,6 +81,7 @@ export class EventraEngine {
|
|
|
82
81
|
undefined,
|
|
83
82
|
this.wrapperCache
|
|
84
83
|
);
|
|
84
|
+
|
|
85
85
|
this.fileResults.set(file, res);
|
|
86
86
|
|
|
87
87
|
return res;
|
|
@@ -92,10 +92,17 @@ export class EventraEngine {
|
|
|
92
92
|
file: string,
|
|
93
93
|
content: string,
|
|
94
94
|
config: EventraConfig
|
|
95
|
-
) {
|
|
96
|
-
this.wrapperCache.clear();
|
|
95
|
+
): ScanResult {
|
|
97
96
|
file = this.normalize(file);
|
|
98
97
|
|
|
98
|
+
// EARLY EXIT
|
|
99
|
+
if (this.fileHash.get(file) === hash(content)) {
|
|
100
|
+
return this.fileResults.get(file) ?? this.empty();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// invalidate wrapper cache
|
|
104
|
+
this.wrapperCache.clear();
|
|
105
|
+
|
|
99
106
|
const res = this.scanFile(file, content, config);
|
|
100
107
|
|
|
101
108
|
const dependents = this.graph.getAllDependentsDeep(file);
|
|
@@ -103,6 +110,7 @@ export class EventraEngine {
|
|
|
103
110
|
for (const dep of dependents) {
|
|
104
111
|
const abs = this.normalize(dep);
|
|
105
112
|
|
|
113
|
+
if (abs === file) continue; // self-skip
|
|
106
114
|
if (isExternalFile(abs)) continue;
|
|
107
115
|
|
|
108
116
|
let depContent = this.ts.getFileContent(abs);
|
|
@@ -140,6 +148,7 @@ export class EventraEngine {
|
|
|
140
148
|
undefined,
|
|
141
149
|
this.wrapperCache
|
|
142
150
|
);
|
|
151
|
+
|
|
143
152
|
this.fileResults.set(abs, r);
|
|
144
153
|
}
|
|
145
154
|
|
|
@@ -159,19 +168,22 @@ export class EventraEngine {
|
|
|
159
168
|
|
|
160
169
|
// REMOVE FILE
|
|
161
170
|
removeFile(file: string, config?: EventraConfig) {
|
|
162
|
-
this.wrapperCache.clear();
|
|
163
171
|
file = this.normalize(file);
|
|
164
172
|
|
|
173
|
+
this.wrapperCache.clear();
|
|
174
|
+
|
|
165
175
|
const dependents = this.graph.getAllDependentsDeep(file);
|
|
166
176
|
|
|
177
|
+
this.graph.remove(file);
|
|
178
|
+
|
|
167
179
|
this.ts.removeFile(file);
|
|
168
180
|
this.fileResults.delete(file);
|
|
169
181
|
this.fileHash.delete(file);
|
|
170
|
-
this.graph.remove(file);
|
|
171
182
|
|
|
172
183
|
for (const dep of dependents) {
|
|
173
184
|
const abs = this.normalize(dep);
|
|
174
185
|
|
|
186
|
+
if (abs === file) continue;
|
|
175
187
|
if (isExternalFile(abs)) continue;
|
|
176
188
|
|
|
177
189
|
let content = this.ts.getFileContent(abs);
|
|
@@ -184,11 +196,7 @@ export class EventraEngine {
|
|
|
184
196
|
}
|
|
185
197
|
}
|
|
186
198
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (this.fileHash.get(abs) === h) continue;
|
|
190
|
-
|
|
191
|
-
this.fileHash.set(abs, h);
|
|
199
|
+
this.fileHash.delete(abs);
|
|
192
200
|
|
|
193
201
|
this.ts.updateFile(abs, content);
|
|
194
202
|
|
|
@@ -21,7 +21,7 @@ export class TSService {
|
|
|
21
21
|
let compilerOptions: ts.CompilerOptions = {
|
|
22
22
|
allowJs: true,
|
|
23
23
|
allowNonTsExtensions: true,
|
|
24
|
-
jsx: ts.JsxEmit.
|
|
24
|
+
jsx: ts.JsxEmit.Preserve,
|
|
25
25
|
jsxImportSource: "react",
|
|
26
26
|
moduleResolution: ts.ModuleResolutionKind.Node16,
|
|
27
27
|
target: ts.ScriptTarget.ESNext,
|
|
@@ -59,7 +59,15 @@ export class TSService {
|
|
|
59
59
|
|
|
60
60
|
const allFiles = ts.sys.readDirectory(
|
|
61
61
|
root,
|
|
62
|
-
[
|
|
62
|
+
[
|
|
63
|
+
".ts",
|
|
64
|
+
".tsx",
|
|
65
|
+
".js",
|
|
66
|
+
".jsx",
|
|
67
|
+
".vue",
|
|
68
|
+
".svelte",
|
|
69
|
+
".astro"
|
|
70
|
+
],
|
|
63
71
|
undefined,
|
|
64
72
|
undefined
|
|
65
73
|
);
|
|
@@ -77,34 +85,39 @@ export class TSService {
|
|
|
77
85
|
file = this.normalize(file);
|
|
78
86
|
return this.files.get(file)?.version.toString() ?? "0";
|
|
79
87
|
},
|
|
88
|
+
getScriptKind: (fileName) => {
|
|
89
|
+
fileName = this.normalize(fileName);
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
fileName.endsWith(".tsx") ||
|
|
93
|
+
fileName.endsWith(".vue") ||
|
|
94
|
+
fileName.endsWith(".svelte") ||
|
|
95
|
+
fileName.endsWith(".astro")
|
|
96
|
+
) {
|
|
97
|
+
return ts.ScriptKind.TSX;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (fileName.endsWith(".ts")) {
|
|
101
|
+
return ts.ScriptKind.TS;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (fileName.endsWith(".jsx")) {
|
|
105
|
+
return ts.ScriptKind.JSX;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (fileName.endsWith(".js")) {
|
|
109
|
+
return ts.ScriptKind.JS;
|
|
110
|
+
}
|
|
80
111
|
|
|
112
|
+
return ts.ScriptKind.Unknown;
|
|
113
|
+
},
|
|
81
114
|
getScriptSnapshot: (file) => {
|
|
82
115
|
file = this.normalize(file);
|
|
83
|
-
|
|
84
|
-
// из памяти
|
|
85
116
|
const f = this.files.get(file);
|
|
86
117
|
if (f) {
|
|
87
118
|
return ts.ScriptSnapshot.fromString(f.content);
|
|
88
119
|
}
|
|
89
120
|
|
|
90
|
-
// virtual TSX (vue/svelte/astro)
|
|
91
|
-
if (file.endsWith(".tsx")) {
|
|
92
|
-
const original = file.replace(/\.tsx$/, "");
|
|
93
|
-
|
|
94
|
-
if (fs.existsSync(original)) {
|
|
95
|
-
const content = fs.readFileSync(original, "utf-8");
|
|
96
|
-
|
|
97
|
-
this.files.set(file, {
|
|
98
|
-
content,
|
|
99
|
-
version: 1,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
this.fileNames.add(file);
|
|
103
|
-
|
|
104
|
-
return ts.ScriptSnapshot.fromString(content);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
121
|
try {
|
|
109
122
|
if (fs.existsSync(file)) {
|
|
110
123
|
const content = fs.readFileSync(file, "utf-8");
|
|
@@ -148,12 +161,6 @@ export class TSService {
|
|
|
148
161
|
return path.resolve(file).replace(/\\/g, "/");
|
|
149
162
|
}
|
|
150
163
|
|
|
151
|
-
private ensureProgram() {
|
|
152
|
-
try {
|
|
153
|
-
this.service.getProgram();
|
|
154
|
-
} catch {}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
164
|
// PUBLIC API
|
|
158
165
|
updateFile(file: string, content: string) {
|
|
159
166
|
file = this.normalize(file);
|
|
@@ -166,8 +173,6 @@ export class TSService {
|
|
|
166
173
|
});
|
|
167
174
|
|
|
168
175
|
this.fileNames.add(file);
|
|
169
|
-
|
|
170
|
-
this.ensureProgram();
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
removeFile(file: string) {
|
|
@@ -175,8 +180,6 @@ export class TSService {
|
|
|
175
180
|
|
|
176
181
|
this.files.delete(file);
|
|
177
182
|
this.fileNames.delete(file);
|
|
178
|
-
|
|
179
|
-
this.ensureProgram();
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
getProgram() {
|
package/src/engine/scanner.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveFunctionFromCall } from "./callResolver";
|
|
|
4
4
|
import { resolveExportedSymbol } from "./exportResolver";
|
|
5
5
|
import { EventraConfig, ScanResult } from "../types";
|
|
6
6
|
|
|
7
|
+
// HELPERS
|
|
7
8
|
function isValidEvent(v: string) {
|
|
8
9
|
return (
|
|
9
10
|
v &&
|
|
@@ -32,6 +33,7 @@ function getJsxAttrName(name: ts.JsxAttributeName): string {
|
|
|
32
33
|
return "";
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// MAIN
|
|
35
37
|
export function scanSource(
|
|
36
38
|
source: ts.SourceFile,
|
|
37
39
|
checker: ts.TypeChecker,
|
|
@@ -47,35 +49,52 @@ export function scanSource(
|
|
|
47
49
|
if (visited.has(source.fileName)) {
|
|
48
50
|
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
49
51
|
}
|
|
52
|
+
|
|
50
53
|
visited.add(source.fileName);
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
// TRACK DETECTION
|
|
56
|
+
function isTrackCall(expr: ts.Expression) {
|
|
57
|
+
if (ts.isIdentifier(expr)) {
|
|
58
|
+
return expr.text === "track";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
62
|
+
return expr.name.text === "track";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractTrackCall(
|
|
69
|
+
node: ts.CallExpression,
|
|
70
|
+
paramMap: Map<string, ts.Expression>
|
|
71
|
+
) {
|
|
72
|
+
if (!isTrackCall(node.expression)) return;
|
|
73
|
+
|
|
74
|
+
const arg = node.arguments[0];
|
|
75
|
+
if (!arg) return;
|
|
54
76
|
|
|
55
|
-
|
|
56
|
-
const arg = node.arguments[0];
|
|
57
|
-
if (!arg) return;
|
|
77
|
+
const res = resolveNodeValue(arg, checker, paramMap);
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
for (const v of res.values) {
|
|
80
|
+
if (isValidEvent(v)) {
|
|
81
|
+
events.add(v);
|
|
62
82
|
}
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
// WRAPPER DETECTION
|
|
66
87
|
function isWrapperFunction(fn: ts.FunctionLikeDeclaration) {
|
|
67
88
|
let found = false;
|
|
68
89
|
|
|
69
90
|
function walk(n: ts.Node) {
|
|
70
91
|
if (found) return;
|
|
71
92
|
|
|
72
|
-
if (ts.isCallExpression(n)) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
found = true;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
93
|
+
if (ts.isCallExpression(n) && isTrackCall(n.expression)) {
|
|
94
|
+
found = true;
|
|
95
|
+
return;
|
|
78
96
|
}
|
|
97
|
+
|
|
79
98
|
ts.forEachChild(n, walk);
|
|
80
99
|
}
|
|
81
100
|
|
|
@@ -83,15 +102,20 @@ export function scanSource(
|
|
|
83
102
|
return found;
|
|
84
103
|
}
|
|
85
104
|
|
|
105
|
+
// COMPONENT WRAPPER
|
|
86
106
|
function detectComponentWrapper(tag: ts.JsxTagNameExpression): string | null {
|
|
87
107
|
const tagName = getJsxTagName(tag);
|
|
88
108
|
|
|
89
|
-
if (wrapperCache.has(tagName))
|
|
109
|
+
if (wrapperCache.has(tagName)) {
|
|
110
|
+
return wrapperCache.get(tagName)!;
|
|
111
|
+
}
|
|
90
112
|
|
|
91
113
|
let symbol = checker.getSymbolAtLocation(tag);
|
|
114
|
+
|
|
92
115
|
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
93
116
|
symbol = checker.getAliasedSymbol(symbol);
|
|
94
117
|
}
|
|
118
|
+
|
|
95
119
|
if (!symbol) {
|
|
96
120
|
wrapperCache.set(tagName, null);
|
|
97
121
|
return null;
|
|
@@ -120,21 +144,20 @@ export function scanSource(
|
|
|
120
144
|
function walk(n: ts.Node) {
|
|
121
145
|
if (prop) return;
|
|
122
146
|
|
|
123
|
-
if (ts.isCallExpression(n)) {
|
|
124
|
-
const
|
|
147
|
+
if (ts.isCallExpression(n) && isTrackCall(n.expression)) {
|
|
148
|
+
const arg = n.arguments[0];
|
|
125
149
|
|
|
126
|
-
if (ts.isPropertyAccessExpression(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
prop = arg.name.text;
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
150
|
+
if (ts.isPropertyAccessExpression(arg)) {
|
|
151
|
+
prop = arg.name.text;
|
|
152
|
+
return;
|
|
132
153
|
}
|
|
133
154
|
}
|
|
155
|
+
|
|
134
156
|
ts.forEachChild(n, walk);
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
walk(fn.body);
|
|
160
|
+
|
|
138
161
|
wrapperCache.set(tagName, prop);
|
|
139
162
|
|
|
140
163
|
if (prop) {
|
|
@@ -147,6 +170,7 @@ export function scanSource(
|
|
|
147
170
|
return null;
|
|
148
171
|
}
|
|
149
172
|
|
|
173
|
+
// VISITOR
|
|
150
174
|
function visit(node: ts.Node) {
|
|
151
175
|
// JSX
|
|
152
176
|
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
@@ -205,29 +229,45 @@ export function scanSource(
|
|
|
205
229
|
resolvedFn.parameters.forEach((p, i) => {
|
|
206
230
|
const arg = node.arguments[i];
|
|
207
231
|
if (!arg) return;
|
|
232
|
+
|
|
208
233
|
if (ts.isIdentifier(p.name)) {
|
|
209
234
|
paramMap.set(p.name.text, arg);
|
|
210
235
|
}
|
|
211
236
|
});
|
|
212
237
|
}
|
|
213
238
|
|
|
214
|
-
// direct track
|
|
215
239
|
extractTrackCall(node, paramMap);
|
|
216
240
|
|
|
217
|
-
// wrapper
|
|
218
241
|
if (resolvedFn && isWrapperFunction(resolvedFn)) {
|
|
219
242
|
const name = getCallName(node.expression);
|
|
243
|
+
|
|
220
244
|
if (name && name !== "track") {
|
|
221
245
|
detectedFunctionWrappers.add(name);
|
|
222
246
|
}
|
|
247
|
+
|
|
223
248
|
if (resolvedFn.body) {
|
|
224
249
|
visit(resolvedFn.body);
|
|
225
250
|
}
|
|
226
251
|
}
|
|
227
252
|
|
|
228
|
-
|
|
229
|
-
|
|
253
|
+
if (resolvedFn && resolvedFn.body) {
|
|
254
|
+
const sf = resolvedFn.getSourceFile();
|
|
255
|
+
|
|
256
|
+
if (!visited.has(sf.fileName)) {
|
|
257
|
+
const res = scanSource(
|
|
258
|
+
sf,
|
|
259
|
+
checker,
|
|
260
|
+
config,
|
|
261
|
+
visited,
|
|
262
|
+
paramMap,
|
|
263
|
+
wrapperCache
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
res.events.forEach(e => events.add(e));
|
|
267
|
+
}
|
|
230
268
|
}
|
|
269
|
+
|
|
270
|
+
node.arguments.forEach(visit);
|
|
231
271
|
visit(node.expression);
|
|
232
272
|
|
|
233
273
|
return;
|