@eventra_dev/eventra-cli 0.0.19 → 0.1.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 +17 -117
- package/dist/commands/sync.js +20 -115
- package/dist/commands/watch.js +107 -69
- package/dist/engine/callResolver.js +34 -0
- package/dist/engine/engine.js +47 -0
- package/dist/engine/graph.js +25 -0
- package/dist/engine/hash.js +10 -0
- package/dist/engine/languageService.js +67 -0
- package/dist/engine/resolver.js +69 -0
- package/dist/engine/scanner.js +50 -0
- package/dist/utils/parsers/vue.js +16 -12
- package/dist/utils/processFile.js +20 -0
- package/package.json +1 -1
- package/src/commands/check.ts +19 -151
- package/src/commands/sync.ts +23 -194
- package/src/commands/watch.ts +119 -66
- package/src/engine/callResolver.ts +43 -0
- package/src/engine/engine.ts +60 -0
- package/src/engine/graph.ts +23 -0
- package/src/engine/hash.ts +5 -0
- package/src/engine/languageService.ts +73 -0
- package/src/engine/resolver.ts +86 -0
- package/src/engine/scanner.ts +61 -0
- package/src/types.ts +5 -6
- package/src/utils/parsers/vue.ts +19 -20
- package/src/utils/processFile.ts +18 -0
- package/tests/fixtures/backend/express/app.ts +167 -12
- package/tests/fixtures/wrappers/component/test.tsx +1 -1
- package/tests/run.ts +4 -17
- package/dist/utils/detect-aliases.js +0 -39
- package/dist/utils/detect-wrappers.js +0 -84
- package/dist/utils/extract.js +0 -56
- package/dist/utils/scanners/component-wrappers.js +0 -73
- package/dist/utils/scanners/function-wrappers.js +0 -83
- package/dist/utils/scanners/track.js +0 -64
- package/src/utils/detect-aliases.ts +0 -46
- package/src/utils/detect-wrappers.ts +0 -119
- package/src/utils/extract.ts +0 -72
- package/src/utils/scanners/component-wrappers.ts +0 -109
- package/src/utils/scanners/function-wrappers.ts +0 -137
- package/src/utils/scanners/track.ts +0 -99
package/src/commands/sync.ts
CHANGED
|
@@ -1,217 +1,46 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
1
|
import fg from "fast-glob";
|
|
3
|
-
import {Project} from "ts-morph";
|
|
4
2
|
import fs from "fs/promises";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import {loadConfig, saveConfig} from "../utils/config";
|
|
8
|
-
|
|
9
|
-
import {detectWrappers} from "../utils/detect-wrappers";
|
|
10
|
-
import {detectAliases} from "../utils/detect-aliases";
|
|
11
|
-
|
|
12
|
-
import {detectParser} from "../utils/parsers/router";
|
|
13
|
-
import {parseVue} from "../utils/parsers/vue";
|
|
14
|
-
import {parseSvelte} from "../utils/parsers/svelte";
|
|
15
|
-
import {parseAstro} from "../utils/parsers/astro";
|
|
16
|
-
|
|
17
|
-
import {scanTrack} from "../utils/scanners/track";
|
|
18
|
-
import {scanFunctionWrappers} from "../utils/scanners/function-wrappers";
|
|
19
|
-
import {scanComponentWrappers} from "../utils/scanners/component-wrappers";
|
|
3
|
+
import chalk from "chalk";
|
|
20
4
|
|
|
21
|
-
import {
|
|
5
|
+
import { loadConfig, saveConfig } from "../utils/config";
|
|
6
|
+
import { EventraEngine } from "../engine/engine";
|
|
7
|
+
import { processFile } from "../utils/processFile";
|
|
22
8
|
|
|
23
9
|
export async function sync() {
|
|
24
10
|
const config = await loadConfig();
|
|
11
|
+
if (!config) return;
|
|
25
12
|
|
|
26
|
-
|
|
27
|
-
console.log(chalk.red("Run 'eventra init'"));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
console.log(chalk.blue("Scanning project..."));
|
|
32
|
-
|
|
33
|
-
const project = new Project();
|
|
34
|
-
|
|
35
|
-
const events = new Set<string>();
|
|
36
|
-
const aliases = config.aliases ?? {};
|
|
37
|
-
const asked = new Set<string>();
|
|
13
|
+
console.log(chalk.blue("Scanning..."));
|
|
38
14
|
|
|
39
15
|
const files = await fg(config.sync.include, {
|
|
40
|
-
ignore: config.sync.exclude
|
|
16
|
+
ignore: config.sync.exclude,
|
|
41
17
|
});
|
|
42
18
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let content = await fs.readFile(file, "utf-8");
|
|
48
|
-
|
|
49
|
-
if (parser === "vue") content = parseVue(content);
|
|
50
|
-
if (parser === "svelte") content = parseSvelte(content);
|
|
51
|
-
if (parser === "astro") content = parseAstro(content);
|
|
52
|
-
|
|
53
|
-
const virtualFile =
|
|
54
|
-
parser === "ts" ? file : file + ".tsx";
|
|
55
|
-
|
|
56
|
-
const source = project.createSourceFile(
|
|
57
|
-
virtualFile,
|
|
58
|
-
content,
|
|
59
|
-
{overwrite: true}
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// AUTO ALIASES
|
|
64
|
-
const detectedAliases = detectAliases(source);
|
|
65
|
-
|
|
66
|
-
for (const key in detectedAliases) {
|
|
67
|
-
if (!(key in aliases)) {
|
|
68
|
-
aliases[key] = detectedAliases[key];
|
|
69
|
-
|
|
70
|
-
console.log(
|
|
71
|
-
chalk.green(
|
|
72
|
-
`Detected alias: ${key} → ${detectedAliases[key]}`
|
|
73
|
-
)
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// AUTO WRAPPERS
|
|
80
|
-
const detected = detectWrappers(source);
|
|
81
|
-
|
|
82
|
-
// function wrappers
|
|
83
|
-
for (const fn of detected.functions) {
|
|
84
|
-
if (
|
|
85
|
-
!config.functionWrappers?.some(
|
|
86
|
-
(w) => w.name === fn.name
|
|
87
|
-
)
|
|
88
|
-
) {
|
|
89
|
-
config.functionWrappers.push(fn);
|
|
90
|
-
|
|
91
|
-
console.log(
|
|
92
|
-
chalk.green(
|
|
93
|
-
`Detected function wrapper: ${fn.name}`
|
|
94
|
-
)
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// component wrappers
|
|
100
|
-
for (const comp of detected.components) {
|
|
101
|
-
if (
|
|
102
|
-
!config.wrappers?.some(
|
|
103
|
-
(w) => w.name === comp.name
|
|
104
|
-
)
|
|
105
|
-
) {
|
|
106
|
-
config.wrappers.push(comp);
|
|
107
|
-
|
|
108
|
-
console.log(
|
|
109
|
-
chalk.green(
|
|
110
|
-
`Detected component wrapper: ${comp.name} (prop: ${comp.prop})`
|
|
111
|
-
)
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// SCAN EVENTS
|
|
117
|
-
const found: ExtractedEvent[] = [
|
|
118
|
-
...scanTrack(source, config, aliases),
|
|
119
|
-
...scanFunctionWrappers(
|
|
120
|
-
source,
|
|
121
|
-
config.functionWrappers ?? [],
|
|
122
|
-
aliases
|
|
123
|
-
),
|
|
124
|
-
...scanComponentWrappers(
|
|
125
|
-
source,
|
|
126
|
-
config.wrappers ?? [],
|
|
127
|
-
aliases
|
|
128
|
-
)
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
for (const event of found) {
|
|
132
|
-
const {value, dynamic} = event;
|
|
133
|
-
|
|
134
|
-
if (!value) continue;
|
|
135
|
-
|
|
136
|
-
// skip explicit ignore
|
|
137
|
-
if (value in aliases && aliases[value] === "__skip__") {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// resolve alias
|
|
142
|
-
if (value in aliases) {
|
|
143
|
-
events.add(aliases[value]);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// static
|
|
148
|
-
if (!dynamic) {
|
|
149
|
-
events.add(value);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// already handled
|
|
154
|
-
if (asked.has(value)) continue;
|
|
155
|
-
|
|
156
|
-
// ASK ONLY IF NEEDED
|
|
157
|
-
console.log(
|
|
158
|
-
chalk.yellow("\nDynamic event detected:")
|
|
159
|
-
);
|
|
160
|
-
console.log(chalk.gray(value));
|
|
161
|
-
|
|
162
|
-
const {name} = await inquirer.prompt([
|
|
163
|
-
{
|
|
164
|
-
type: "input",
|
|
165
|
-
name: "name",
|
|
166
|
-
message:
|
|
167
|
-
"Enter event name (or type 'skip'):"
|
|
168
|
-
}
|
|
169
|
-
]);
|
|
170
|
-
|
|
171
|
-
const normalized = name?.trim();
|
|
172
|
-
asked.add(value);
|
|
173
|
-
|
|
174
|
-
if (!normalized || normalized === "skip") {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
19
|
+
if (!files.length) {
|
|
20
|
+
console.log(chalk.yellow("No files found"));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
177
23
|
|
|
178
|
-
|
|
179
|
-
aliases[value] = normalized;
|
|
24
|
+
const engine = new EventraEngine(process.cwd());
|
|
180
25
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
);
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readFile(file, "utf-8");
|
|
186
29
|
|
|
187
|
-
|
|
188
|
-
}
|
|
30
|
+
const { content, virtualFile } = processFile(file, raw);
|
|
189
31
|
|
|
190
|
-
|
|
191
|
-
project.removeSourceFile(source);
|
|
32
|
+
engine.scanFile(virtualFile, content);
|
|
192
33
|
|
|
193
|
-
} catch {
|
|
194
|
-
console.log(
|
|
195
|
-
|
|
196
|
-
);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.log(chalk.red(`ERROR: ${file}`));
|
|
36
|
+
console.error(e);
|
|
197
37
|
}
|
|
198
38
|
}
|
|
199
|
-
// SAVE CONFIG
|
|
200
|
-
config.aliases = Object.fromEntries(
|
|
201
|
-
Object.entries(aliases).sort()
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
// MERGE
|
|
205
|
-
const merged = new Set([
|
|
206
|
-
...config.events,
|
|
207
|
-
...events
|
|
208
|
-
]);
|
|
209
39
|
|
|
210
|
-
|
|
40
|
+
const events = engine.getAllEvents().sort();
|
|
211
41
|
|
|
42
|
+
config.events = events;
|
|
212
43
|
await saveConfig(config);
|
|
213
44
|
|
|
214
|
-
console.log(
|
|
215
|
-
chalk.green(`\nFound ${config.events.length} events`)
|
|
216
|
-
);
|
|
45
|
+
console.log(chalk.green(`Found ${events.length} events`));
|
|
217
46
|
}
|
package/src/commands/watch.ts
CHANGED
|
@@ -1,113 +1,166 @@
|
|
|
1
1
|
import chokidar from "chokidar";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { Project } from "ts-morph";
|
|
4
3
|
import fs from "fs/promises";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
5
|
|
|
6
6
|
import { loadConfig, saveConfig } from "../utils/config";
|
|
7
|
+
import { EventraEngine } from "../engine/engine";
|
|
7
8
|
|
|
8
9
|
import { detectParser } from "../utils/parsers/router";
|
|
9
10
|
import { parseVue } from "../utils/parsers/vue";
|
|
10
11
|
import { parseSvelte } from "../utils/parsers/svelte";
|
|
11
12
|
import { parseAstro } from "../utils/parsers/astro";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// PREPROCESS (CONTENT)
|
|
15
|
+
function preprocess(file: string, content: string) {
|
|
16
|
+
const parser = detectParser(file);
|
|
16
17
|
|
|
18
|
+
if (parser === "vue") return parseVue(content);
|
|
19
|
+
if (parser === "svelte") return parseSvelte(content);
|
|
20
|
+
if (parser === "astro") return parseAstro(content);
|
|
21
|
+
|
|
22
|
+
return content;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// VIRTUAL FILE (PATH)
|
|
26
|
+
function toVirtualFile(file: string) {
|
|
27
|
+
return file.endsWith(".vue")
|
|
28
|
+
? file + ".tsx"
|
|
29
|
+
: file;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// WATCH
|
|
17
33
|
export async function watch() {
|
|
18
34
|
const config = await loadConfig();
|
|
19
|
-
|
|
20
|
-
if (!config) {
|
|
21
|
-
console.log(chalk.red("Run init first"));
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
35
|
+
if (!config) return;
|
|
24
36
|
|
|
25
37
|
console.log(chalk.blue("Watching...\n"));
|
|
26
38
|
|
|
27
|
-
const
|
|
39
|
+
const files = await fg(config.sync.include, {
|
|
40
|
+
ignore: config.sync.exclude,
|
|
41
|
+
});
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
if (!files.length) {
|
|
44
|
+
console.log(chalk.yellow("No files found"));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
const engine = new EventraEngine(process.cwd());
|
|
49
|
+
|
|
50
|
+
// INITIAL SCAN
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
try {
|
|
53
|
+
const raw = await fs.readFile(file, "utf-8");
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
const content = preprocess(file, raw);
|
|
56
|
+
const virtualFile = toVirtualFile(file);
|
|
36
57
|
|
|
37
|
-
|
|
38
|
-
const files = [...queue];
|
|
39
|
-
queue.clear();
|
|
58
|
+
engine.scanFile(virtualFile, content);
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
} catch {
|
|
61
|
+
console.log(chalk.gray(`skip: ${file}`));
|
|
43
62
|
}
|
|
63
|
+
}
|
|
44
64
|
|
|
45
|
-
|
|
65
|
+
config.events = engine.getAllEvents().sort();
|
|
66
|
+
await saveConfig(config);
|
|
46
67
|
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
console.log(
|
|
69
|
+
chalk.gray(`Initial: ${config.events.length} events\n`)
|
|
70
|
+
);
|
|
49
71
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const parser = detectParser(file);
|
|
72
|
+
// STATE
|
|
73
|
+
let locked = false;
|
|
74
|
+
let queued = new Set<string>();
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
const run = async () => {
|
|
77
|
+
if (locked) return;
|
|
78
|
+
locked = true;
|
|
59
79
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
80
|
+
const batch = [...queued];
|
|
81
|
+
queued.clear();
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
...scanComponentWrappers(source, config.wrappers ?? [], aliases)
|
|
68
|
-
];
|
|
83
|
+
for (const file of batch) {
|
|
84
|
+
try {
|
|
85
|
+
const raw = await fs.readFile(file, "utf-8");
|
|
69
86
|
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
const content = preprocess(file, raw);
|
|
88
|
+
const virtualFile = toVirtualFile(file);
|
|
72
89
|
|
|
73
|
-
|
|
90
|
+
engine.updateFile(virtualFile, content);
|
|
74
91
|
|
|
75
|
-
|
|
92
|
+
} catch {
|
|
93
|
+
console.log(chalk.gray(`skip: ${file}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
76
96
|
|
|
77
|
-
|
|
97
|
+
const next = engine.getAllEvents();
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
const added = next.filter(e => !config.events.includes(e));
|
|
100
|
+
const removed = config.events.filter(e => !next.includes(e));
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} else {
|
|
85
|
-
console.log(chalk.yellow(`Dynamic: ${e.value}`));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
102
|
+
if (added.length || removed.length) {
|
|
103
|
+
added.forEach(e => console.log(chalk.green(`+ ${e}`)));
|
|
104
|
+
removed.forEach(e => console.log(chalk.red(`- ${e}`)));
|
|
88
105
|
|
|
89
|
-
|
|
106
|
+
config.events = next.sort();
|
|
107
|
+
await saveConfig(config);
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.gray(`Total: ${config.events.length} events\n`)
|
|
111
|
+
);
|
|
93
112
|
}
|
|
113
|
+
|
|
114
|
+
locked = false;
|
|
94
115
|
};
|
|
95
116
|
|
|
117
|
+
const schedule = (file: string) => {
|
|
118
|
+
queued.add(file);
|
|
119
|
+
setTimeout(run, 50);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// WATCHER
|
|
96
123
|
const watcher = chokidar.watch(config.sync.include, {
|
|
97
|
-
ignored:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
ignoreInitial: true
|
|
124
|
+
ignored: config.sync.exclude,
|
|
125
|
+
ignoreInitial: true,
|
|
126
|
+
awaitWriteFinish: {
|
|
127
|
+
stabilityThreshold: 50,
|
|
128
|
+
pollInterval: 10,
|
|
129
|
+
},
|
|
104
130
|
});
|
|
105
131
|
|
|
106
|
-
watcher
|
|
107
|
-
|
|
132
|
+
watcher
|
|
133
|
+
.on("add", (file) => {
|
|
134
|
+
console.log(chalk.gray(`add: ${file}`));
|
|
135
|
+
schedule(file);
|
|
136
|
+
})
|
|
137
|
+
.on("change", (file) => {
|
|
138
|
+
console.log(chalk.gray(`change: ${file}`));
|
|
139
|
+
schedule(file);
|
|
140
|
+
})
|
|
141
|
+
.on("unlink", async (file) => {
|
|
142
|
+
console.log(chalk.gray(`remove: ${file}`));
|
|
143
|
+
|
|
144
|
+
const virtualFile = toVirtualFile(file);
|
|
145
|
+
|
|
146
|
+
engine.removeFile?.(virtualFile);
|
|
108
147
|
|
|
109
|
-
|
|
148
|
+
const next = engine.getAllEvents();
|
|
149
|
+
|
|
150
|
+
const removed = config.events.filter(e => !next.includes(e));
|
|
151
|
+
|
|
152
|
+
if (removed.length) {
|
|
153
|
+
removed.forEach(e => console.log(chalk.red(`- ${e}`)));
|
|
154
|
+
|
|
155
|
+
config.events = next.sort();
|
|
156
|
+
await saveConfig(config);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
110
159
|
|
|
111
|
-
|
|
160
|
+
// EXIT
|
|
161
|
+
process.on("SIGINT", async () => {
|
|
162
|
+
console.log(chalk.yellow("\nStopping watcher..."));
|
|
163
|
+
await watcher.close();
|
|
164
|
+
process.exit(0);
|
|
112
165
|
});
|
|
113
166
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
export function resolveFunctionFromCall(
|
|
4
|
+
expr: ts.Expression,
|
|
5
|
+
checker: ts.TypeChecker
|
|
6
|
+
): ts.FunctionLikeDeclaration | null {
|
|
7
|
+
|
|
8
|
+
let symbol = checker.getSymbolAtLocation(expr);
|
|
9
|
+
|
|
10
|
+
if (!symbol && ts.isPropertyAccessExpression(expr)) {
|
|
11
|
+
symbol = checker.getSymbolAtLocation(expr.name);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!symbol) return null;
|
|
15
|
+
|
|
16
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
17
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
21
|
+
if (
|
|
22
|
+
ts.isFunctionDeclaration(decl) ||
|
|
23
|
+
ts.isMethodDeclaration(decl) ||
|
|
24
|
+
ts.isFunctionExpression(decl) ||
|
|
25
|
+
ts.isArrowFunction(decl)
|
|
26
|
+
) {
|
|
27
|
+
return decl;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
31
|
+
const init = decl.initializer;
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
ts.isArrowFunction(init) ||
|
|
35
|
+
ts.isFunctionExpression(init)
|
|
36
|
+
) {
|
|
37
|
+
return init;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { TSService } from "./languageService";
|
|
2
|
+
import { scanSource } from "./scanner";
|
|
3
|
+
import { DependencyGraph } from "./graph";
|
|
4
|
+
import { hash } from "./hash";
|
|
5
|
+
|
|
6
|
+
export class EventraEngine {
|
|
7
|
+
private ts: TSService;
|
|
8
|
+
private graph = new DependencyGraph();
|
|
9
|
+
|
|
10
|
+
private fileEvents = new Map<string, Set<string>>();
|
|
11
|
+
private fileHash = new Map<string, string>();
|
|
12
|
+
|
|
13
|
+
constructor(root: string) {
|
|
14
|
+
this.ts = new TSService(root);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
scanFile(file: string, content: string) {
|
|
18
|
+
const h = hash(content);
|
|
19
|
+
|
|
20
|
+
if (this.fileHash.get(file) === h) {
|
|
21
|
+
return this.fileEvents.get(file) ?? new Set();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.fileHash.set(file, h);
|
|
25
|
+
|
|
26
|
+
this.ts.updateFile(file, content);
|
|
27
|
+
|
|
28
|
+
const source = this.ts.getSourceFile(file);
|
|
29
|
+
const checker = this.ts.getChecker();
|
|
30
|
+
|
|
31
|
+
if (!source || !checker) return new Set();
|
|
32
|
+
|
|
33
|
+
this.graph.update(file, source);
|
|
34
|
+
|
|
35
|
+
const events = scanSource(source, checker);
|
|
36
|
+
|
|
37
|
+
this.fileEvents.set(file, events);
|
|
38
|
+
|
|
39
|
+
return events;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
updateFile(file: string, content: string) {
|
|
43
|
+
return this.scanFile(file, content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getAllEvents() {
|
|
47
|
+
const all = new Set<string>();
|
|
48
|
+
|
|
49
|
+
for (const set of this.fileEvents.values()) {
|
|
50
|
+
set.forEach(e => all.add(e));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [...all];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
removeFile(file: string) {
|
|
57
|
+
this.fileEvents.delete(file);
|
|
58
|
+
this.fileHash.delete(file);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
export class DependencyGraph {
|
|
4
|
+
private reverse = new Map<string, Set<string>>();
|
|
5
|
+
|
|
6
|
+
update(file: string, source: ts.SourceFile) {
|
|
7
|
+
for (const stmt of source.statements) {
|
|
8
|
+
if (!ts.isImportDeclaration(stmt)) continue;
|
|
9
|
+
|
|
10
|
+
const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
11
|
+
|
|
12
|
+
if (!this.reverse.has(module)) {
|
|
13
|
+
this.reverse.set(module, new Set());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.reverse.get(module)!.add(file);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getDependents(file: string) {
|
|
21
|
+
return [...(this.reverse.get(file) ?? [])];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
export class TSService {
|
|
5
|
+
private files = new Map<string, { version: number; content: string }>();
|
|
6
|
+
private service: ts.LanguageService;
|
|
7
|
+
|
|
8
|
+
constructor(private root: string) {
|
|
9
|
+
const host: ts.LanguageServiceHost = {
|
|
10
|
+
getScriptFileNames: () => [...this.files.keys()],
|
|
11
|
+
getScriptVersion: (file) =>
|
|
12
|
+
this.files.get(file)?.version.toString() ?? "0",
|
|
13
|
+
getScriptSnapshot: (file) => {
|
|
14
|
+
const f = this.files.get(file);
|
|
15
|
+
|
|
16
|
+
if (f) {
|
|
17
|
+
return ts.ScriptSnapshot.fromString(f.content);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(file)) {
|
|
21
|
+
return ts.ScriptSnapshot.fromString(
|
|
22
|
+
fs.readFileSync(file, "utf-8" as BufferEncoding)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return undefined;
|
|
27
|
+
},
|
|
28
|
+
getCurrentDirectory: () => this.root,
|
|
29
|
+
getCompilationSettings: () => ({
|
|
30
|
+
allowJs: true,
|
|
31
|
+
jsx: ts.JsxEmit.Preserve,
|
|
32
|
+
moduleResolution: ts.ModuleResolutionKind.Node16,
|
|
33
|
+
target: ts.ScriptTarget.ESNext,
|
|
34
|
+
esModuleInterop: true,
|
|
35
|
+
allowSyntheticDefaultImports: true,
|
|
36
|
+
}),
|
|
37
|
+
getDefaultLibFileName: (opts) => ts.getDefaultLibFilePath(opts),
|
|
38
|
+
|
|
39
|
+
readFile: (fileName: string, encoding?: string) => {
|
|
40
|
+
try {
|
|
41
|
+
return fs.readFileSync(fileName, encoding as BufferEncoding);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
fileExists: fs.existsSync,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.service = ts.createLanguageService(host);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
updateFile(file: string, content: string) {
|
|
54
|
+
const prev = this.files.get(file);
|
|
55
|
+
|
|
56
|
+
this.files.set(file, {
|
|
57
|
+
content,
|
|
58
|
+
version: prev ? prev.version + 1 : 1,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getProgram() {
|
|
63
|
+
return this.service.getProgram();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getChecker() {
|
|
67
|
+
return this.getProgram()?.getTypeChecker();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getSourceFile(file: string) {
|
|
71
|
+
return this.getProgram()?.getSourceFile(file);
|
|
72
|
+
}
|
|
73
|
+
}
|