@eventra_dev/eventra-cli 0.0.18 → 0.1.1
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 +74 -166
- package/dist/commands/check.js +25 -88
- package/dist/commands/sync.js +22 -77
- package/dist/commands/watch.js +135 -0
- package/dist/engine/callResolver.js +31 -0
- package/dist/engine/engine.js +33 -0
- package/dist/engine/extract.js +53 -0
- package/dist/engine/flow.js +38 -0
- package/dist/engine/graph.js +22 -0
- package/dist/engine/project.js +74 -0
- package/dist/engine/resolver.js +38 -0
- package/dist/engine/scanner.js +78 -0
- package/dist/engine/symbol.js +31 -0
- package/dist/index.js +10 -6
- package/dist/utils/parsers/vue.js +16 -12
- package/dist/utils/processFile.js +20 -0
- package/package.json +2 -1
- package/src/commands/check.ts +29 -136
- package/src/commands/sync.ts +24 -135
- package/src/commands/watch.ts +166 -0
- package/src/engine/callResolver.ts +37 -0
- package/src/engine/engine.ts +39 -0
- package/src/engine/extract.ts +74 -0
- package/src/engine/flow.ts +54 -0
- package/src/engine/graph.ts +23 -0
- package/src/engine/project.ts +75 -0
- package/src/engine/resolver.ts +53 -0
- package/src/engine/scanner.ts +90 -0
- package/src/engine/symbol.ts +42 -0
- package/src/index.ts +11 -6
- package/src/types.ts +3 -5
- 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/extract.js +0 -136
- package/dist/utils/scanners/component-wrappers.js +0 -70
- package/dist/utils/scanners/function-wrappers.js +0 -85
- package/dist/utils/scanners/track.js +0 -52
- package/src/utils/extract.ts +0 -185
- package/src/utils/scanners/component-wrappers.ts +0 -107
- package/src/utils/scanners/function-wrappers.ts +0 -180
- package/src/utils/scanners/track.ts +0 -84
package/src/commands/check.ts
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import fg from "fast-glob";
|
|
3
|
-
import { Project } from "ts-morph";
|
|
4
3
|
import fs from "fs/promises";
|
|
5
4
|
|
|
6
|
-
import { loadConfig } from "../utils/config";
|
|
5
|
+
import { loadConfig, saveConfig } from "../utils/config";
|
|
6
|
+
import { EventraEngine } from "../engine/engine";
|
|
7
|
+
import { processFile } from "../utils/processFile";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
import { parseVue } from "../utils/parsers/vue";
|
|
10
|
-
import { parseSvelte } from "../utils/parsers/svelte";
|
|
11
|
-
import { parseAstro } from "../utils/parsers/astro";
|
|
12
|
-
|
|
13
|
-
import { scanTrack } from "../utils/scanners/track";
|
|
14
|
-
import { scanFunctionWrappers } from "../utils/scanners/function-wrappers";
|
|
15
|
-
import { scanComponentWrappers } from "../utils/scanners/component-wrappers";
|
|
16
|
-
|
|
17
|
-
import { ExtractedEvent } from "../types";
|
|
18
|
-
|
|
19
|
-
export async function check() {
|
|
9
|
+
export async function check({ fix = false }: { fix?: boolean }) {
|
|
20
10
|
const config = await loadConfig();
|
|
21
11
|
|
|
22
12
|
if (!config) {
|
|
@@ -26,147 +16,50 @@ export async function check() {
|
|
|
26
16
|
|
|
27
17
|
console.log(chalk.blue("Checking events..."));
|
|
28
18
|
|
|
29
|
-
const project = new Project();
|
|
30
|
-
|
|
31
|
-
const knownEvents = new Set(config.events ?? []);
|
|
32
|
-
const aliases = config.aliases ?? {};
|
|
33
|
-
|
|
34
|
-
const foundEvents = new Set<string>();
|
|
35
|
-
const newEvents = new Set<string>();
|
|
36
|
-
const unresolvedDynamic = new Set<string>();
|
|
37
|
-
|
|
38
19
|
const files = await fg(config.sync.include, {
|
|
39
|
-
ignore: config.sync.exclude
|
|
20
|
+
ignore: config.sync.exclude,
|
|
40
21
|
});
|
|
41
22
|
|
|
23
|
+
const engine = new EventraEngine(process.cwd());
|
|
24
|
+
|
|
42
25
|
for (const file of files) {
|
|
43
26
|
try {
|
|
44
|
-
|
|
45
|
-
const stat = await fs.stat(file);
|
|
46
|
-
if (stat.size > 2 * 1024 * 1024) {
|
|
47
|
-
console.log(chalk.gray(`Skipped large file: ${file}`));
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const parser = detectParser(file);
|
|
52
|
-
|
|
53
|
-
let content = await fs.readFile(file, "utf-8");
|
|
54
|
-
|
|
55
|
-
if (parser === "vue") content = parseVue(content);
|
|
56
|
-
if (parser === "svelte") content = parseSvelte(content);
|
|
57
|
-
if (parser === "astro") content = parseAstro(content);
|
|
58
|
-
|
|
59
|
-
const virtualFile =
|
|
60
|
-
parser === "ts" ? file : file + ".tsx";
|
|
61
|
-
|
|
62
|
-
const source = project.createSourceFile(
|
|
63
|
-
virtualFile,
|
|
64
|
-
content,
|
|
65
|
-
{ overwrite: true }
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const found: ExtractedEvent[] = [
|
|
69
|
-
...scanTrack(source, config),
|
|
70
|
-
...scanFunctionWrappers(
|
|
71
|
-
source,
|
|
72
|
-
config.functionWrappers ?? []
|
|
73
|
-
),
|
|
74
|
-
...scanComponentWrappers(
|
|
75
|
-
source,
|
|
76
|
-
config.wrappers ?? []
|
|
77
|
-
)
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
for (const event of found) {
|
|
81
|
-
const { value, dynamic } = event;
|
|
82
|
-
|
|
83
|
-
if (!value) continue;
|
|
84
|
-
|
|
85
|
-
// alias skip
|
|
86
|
-
if (aliases[value] === "__skip__") {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// alias resolve
|
|
91
|
-
if (aliases[value]) {
|
|
92
|
-
foundEvents.add(aliases[value]);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// dynamic without alias
|
|
97
|
-
if (dynamic) {
|
|
98
|
-
unresolvedDynamic.add(value);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// static
|
|
103
|
-
foundEvents.add(value);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// clear memory
|
|
107
|
-
project.removeSourceFile(source);
|
|
27
|
+
const raw = await fs.readFile(file, "utf-8");
|
|
108
28
|
|
|
109
|
-
|
|
110
|
-
console.log(
|
|
111
|
-
chalk.gray(`Skipped file (error): ${file}`)
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
29
|
+
const { content, virtualFile } = processFile(file, raw);
|
|
115
30
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
31
|
+
engine.scanFile(virtualFile, content);
|
|
32
|
+
|
|
33
|
+
} catch {
|
|
34
|
+
console.log(chalk.gray(`Skipped: ${file}`));
|
|
120
35
|
}
|
|
121
36
|
}
|
|
122
37
|
|
|
123
|
-
|
|
38
|
+
const found = new Set(engine.getAllEvents());
|
|
39
|
+
const known = new Set(config.events ?? []);
|
|
124
40
|
|
|
125
|
-
|
|
126
|
-
if (unresolvedDynamic.size > 0) {
|
|
127
|
-
hasError = true;
|
|
41
|
+
const newEvents = [...found].filter(e => !known.has(e));
|
|
128
42
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
43
|
+
if (fix) {
|
|
44
|
+
newEvents.forEach(e => {
|
|
45
|
+
config.events.push(e);
|
|
46
|
+
console.log(chalk.green(`+ ${e}`));
|
|
47
|
+
});
|
|
132
48
|
|
|
133
|
-
[...
|
|
134
|
-
.sort()
|
|
135
|
-
.forEach((e) =>
|
|
136
|
-
console.log(chalk.red(`- ${e}`))
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// new events
|
|
141
|
-
if (newEvents.size > 0) {
|
|
142
|
-
hasError = true;
|
|
49
|
+
config.events = [...new Set(config.events)].sort();
|
|
143
50
|
|
|
144
|
-
|
|
145
|
-
chalk.red("\nNew events detected:")
|
|
146
|
-
);
|
|
51
|
+
await saveConfig(config);
|
|
147
52
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.forEach((e) =>
|
|
151
|
-
console.log(chalk.red(`+ ${e}`))
|
|
152
|
-
);
|
|
53
|
+
console.log(chalk.green("\nFixed"));
|
|
54
|
+
return;
|
|
153
55
|
}
|
|
154
56
|
|
|
155
|
-
if (
|
|
156
|
-
console.log("");
|
|
157
|
-
|
|
158
|
-
console.log(chalk.red("Event check failed"));
|
|
159
|
-
|
|
160
|
-
console.log(
|
|
161
|
-
chalk.red(
|
|
162
|
-
`dynamic: ${unresolvedDynamic.size}, new: ${newEvents.size}`
|
|
163
|
-
)
|
|
164
|
-
);
|
|
57
|
+
if (newEvents.length) {
|
|
58
|
+
console.log(chalk.red("\nNew events:"));
|
|
59
|
+
newEvents.forEach(e => console.log(chalk.red(`+ ${e}`)));
|
|
165
60
|
|
|
166
61
|
process.exit(1);
|
|
167
62
|
}
|
|
168
63
|
|
|
169
|
-
console.log(
|
|
170
|
-
chalk.green("\nAll events are valid")
|
|
171
|
-
);
|
|
64
|
+
console.log(chalk.green("\nAll good"));
|
|
172
65
|
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,157 +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 {
|
|
8
|
-
loadConfig,
|
|
9
|
-
saveConfig
|
|
10
|
-
} from "../utils/config";
|
|
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(
|
|
28
|
-
chalk.red("Run 'eventra init'")
|
|
29
|
-
);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
console.log(
|
|
34
|
-
chalk.blue("Scanning project...")
|
|
35
|
-
);
|
|
13
|
+
console.log(chalk.blue("Scanning..."));
|
|
36
14
|
|
|
37
|
-
const
|
|
38
|
-
|
|
15
|
+
const files = await fg(config.sync.include, {
|
|
16
|
+
ignore: config.sync.exclude,
|
|
17
|
+
});
|
|
39
18
|
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
if (!files.length) {
|
|
20
|
+
console.log(chalk.yellow("No files found"));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
42
23
|
|
|
43
|
-
const
|
|
44
|
-
config.sync.include,
|
|
45
|
-
{
|
|
46
|
-
ignore: config.sync.exclude
|
|
47
|
-
}
|
|
48
|
-
);
|
|
24
|
+
const engine = new EventraEngine(process.cwd());
|
|
49
25
|
|
|
50
26
|
for (const file of files) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let content = await fs.readFile(
|
|
54
|
-
file,
|
|
55
|
-
"utf-8"
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
if (parser === "vue")
|
|
59
|
-
content = parseVue(content);
|
|
60
|
-
|
|
61
|
-
if (parser === "svelte")
|
|
62
|
-
content = parseSvelte(content);
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readFile(file, "utf-8");
|
|
63
29
|
|
|
64
|
-
|
|
65
|
-
content = parseAstro(content);
|
|
30
|
+
const { content, virtualFile } = processFile(file, raw);
|
|
66
31
|
|
|
67
|
-
|
|
68
|
-
parser === "ts"
|
|
69
|
-
? file
|
|
70
|
-
: file + ".tsx";
|
|
32
|
+
engine.scanFile(virtualFile, content);
|
|
71
33
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{ overwrite: true }
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const found: ExtractedEvent[] = [
|
|
79
|
-
...scanTrack(source, config),
|
|
80
|
-
...scanFunctionWrappers(
|
|
81
|
-
source,
|
|
82
|
-
config.functionWrappers ?? []
|
|
83
|
-
),
|
|
84
|
-
...scanComponentWrappers(
|
|
85
|
-
source,
|
|
86
|
-
config.wrappers ?? []
|
|
87
|
-
)
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
for (const event of found) {
|
|
91
|
-
const { value, dynamic } = event;
|
|
92
|
-
|
|
93
|
-
if (!value) continue;
|
|
94
|
-
|
|
95
|
-
// alias
|
|
96
|
-
if (aliases[value]) {
|
|
97
|
-
console.log(
|
|
98
|
-
chalk.gray(`alias: ${value} → ${aliases[value]}`)
|
|
99
|
-
);
|
|
100
|
-
events.add(aliases[value]);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// static
|
|
105
|
-
if (!dynamic) {
|
|
106
|
-
events.add(value);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// already asked
|
|
111
|
-
if (asked.has(value)) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ask user
|
|
116
|
-
console.log(
|
|
117
|
-
chalk.yellow("\nDynamic event detected:")
|
|
118
|
-
);
|
|
119
|
-
console.log(chalk.gray(value));
|
|
120
|
-
|
|
121
|
-
const { name } = await inquirer.prompt([
|
|
122
|
-
{
|
|
123
|
-
type: "input",
|
|
124
|
-
name: "name",
|
|
125
|
-
message:
|
|
126
|
-
"Enter event name (or type 'skip'):"
|
|
127
|
-
}
|
|
128
|
-
]);
|
|
129
|
-
|
|
130
|
-
const normalized = name?.trim();
|
|
131
|
-
|
|
132
|
-
asked.add(value);
|
|
133
|
-
|
|
134
|
-
if (!normalized || normalized === "skip") {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// save alias
|
|
139
|
-
aliases[value] = normalized;
|
|
140
|
-
|
|
141
|
-
events.add(normalized);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.log(chalk.red(`ERROR: ${file}`));
|
|
36
|
+
console.error(e);
|
|
142
37
|
}
|
|
143
38
|
}
|
|
144
39
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const list = [...events].sort();
|
|
148
|
-
config.events = list;
|
|
40
|
+
const events = engine.getAllEvents().sort();
|
|
149
41
|
|
|
42
|
+
config.events = events;
|
|
150
43
|
await saveConfig(config);
|
|
151
44
|
|
|
152
|
-
console.log(
|
|
153
|
-
chalk.green(
|
|
154
|
-
`Found ${list.length} events`
|
|
155
|
-
)
|
|
156
|
-
);
|
|
45
|
+
console.log(chalk.green(`Found ${events.length} events`));
|
|
157
46
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import chokidar from "chokidar";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
|
+
|
|
6
|
+
import { loadConfig, saveConfig } from "../utils/config";
|
|
7
|
+
import { EventraEngine } from "../engine/engine";
|
|
8
|
+
|
|
9
|
+
import { detectParser } from "../utils/parsers/router";
|
|
10
|
+
import { parseVue } from "../utils/parsers/vue";
|
|
11
|
+
import { parseSvelte } from "../utils/parsers/svelte";
|
|
12
|
+
import { parseAstro } from "../utils/parsers/astro";
|
|
13
|
+
|
|
14
|
+
// PREPROCESS (CONTENT)
|
|
15
|
+
function preprocess(file: string, content: string) {
|
|
16
|
+
const parser = detectParser(file);
|
|
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
|
|
33
|
+
export async function watch() {
|
|
34
|
+
const config = await loadConfig();
|
|
35
|
+
if (!config) return;
|
|
36
|
+
|
|
37
|
+
console.log(chalk.blue("Watching...\n"));
|
|
38
|
+
|
|
39
|
+
const files = await fg(config.sync.include, {
|
|
40
|
+
ignore: config.sync.exclude,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!files.length) {
|
|
44
|
+
console.log(chalk.yellow("No files found"));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
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");
|
|
54
|
+
|
|
55
|
+
const content = preprocess(file, raw);
|
|
56
|
+
const virtualFile = toVirtualFile(file);
|
|
57
|
+
|
|
58
|
+
engine.scanFile(virtualFile, content);
|
|
59
|
+
|
|
60
|
+
} catch {
|
|
61
|
+
console.log(chalk.gray(`skip: ${file}`));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
config.events = engine.getAllEvents().sort();
|
|
66
|
+
await saveConfig(config);
|
|
67
|
+
|
|
68
|
+
console.log(
|
|
69
|
+
chalk.gray(`Initial: ${config.events.length} events\n`)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// STATE
|
|
73
|
+
let locked = false;
|
|
74
|
+
let queued = new Set<string>();
|
|
75
|
+
|
|
76
|
+
const run = async () => {
|
|
77
|
+
if (locked) return;
|
|
78
|
+
locked = true;
|
|
79
|
+
|
|
80
|
+
const batch = [...queued];
|
|
81
|
+
queued.clear();
|
|
82
|
+
|
|
83
|
+
for (const file of batch) {
|
|
84
|
+
try {
|
|
85
|
+
const raw = await fs.readFile(file, "utf-8");
|
|
86
|
+
|
|
87
|
+
const content = preprocess(file, raw);
|
|
88
|
+
const virtualFile = toVirtualFile(file);
|
|
89
|
+
|
|
90
|
+
engine.updateFile(virtualFile, content);
|
|
91
|
+
|
|
92
|
+
} catch {
|
|
93
|
+
console.log(chalk.gray(`skip: ${file}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const next = engine.getAllEvents();
|
|
98
|
+
|
|
99
|
+
const added = next.filter(e => !config.events.includes(e));
|
|
100
|
+
const removed = config.events.filter(e => !next.includes(e));
|
|
101
|
+
|
|
102
|
+
if (added.length || removed.length) {
|
|
103
|
+
added.forEach(e => console.log(chalk.green(`+ ${e}`)));
|
|
104
|
+
removed.forEach(e => console.log(chalk.red(`- ${e}`)));
|
|
105
|
+
|
|
106
|
+
config.events = next.sort();
|
|
107
|
+
await saveConfig(config);
|
|
108
|
+
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.gray(`Total: ${config.events.length} events\n`)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
locked = false;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const schedule = (file: string) => {
|
|
118
|
+
queued.add(file);
|
|
119
|
+
setTimeout(run, 50);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// WATCHER
|
|
123
|
+
const watcher = chokidar.watch(config.sync.include, {
|
|
124
|
+
ignored: config.sync.exclude,
|
|
125
|
+
ignoreInitial: true,
|
|
126
|
+
awaitWriteFinish: {
|
|
127
|
+
stabilityThreshold: 50,
|
|
128
|
+
pollInterval: 10,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
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);
|
|
147
|
+
|
|
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
|
+
});
|
|
159
|
+
|
|
160
|
+
// EXIT
|
|
161
|
+
process.on("SIGINT", async () => {
|
|
162
|
+
console.log(chalk.yellow("\nStopping watcher..."));
|
|
163
|
+
await watcher.close();
|
|
164
|
+
process.exit(0);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Node } from "ts-morph";
|
|
2
|
+
|
|
3
|
+
export function resolveFunctionFromCall(expr: Node) {
|
|
4
|
+
let symbol = expr.getSymbol();
|
|
5
|
+
|
|
6
|
+
if (!symbol && Node.isPropertyAccessExpression(expr)) {
|
|
7
|
+
symbol = expr.getNameNode().getSymbol();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!symbol) return null;
|
|
11
|
+
|
|
12
|
+
const decls = symbol.getDeclarations() ?? [];
|
|
13
|
+
|
|
14
|
+
for (const d of decls) {
|
|
15
|
+
if (Node.isFunctionDeclaration(d)) return d;
|
|
16
|
+
|
|
17
|
+
if (Node.isVariableDeclaration(d)) {
|
|
18
|
+
const init = d.getInitializer();
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
init &&
|
|
22
|
+
(Node.isArrowFunction(init) ||
|
|
23
|
+
Node.isFunctionExpression(init))
|
|
24
|
+
) {
|
|
25
|
+
return init;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (Node.isMethodDeclaration(d)) return d;
|
|
30
|
+
|
|
31
|
+
if (Node.isFunctionExpression(d) || Node.isArrowFunction(d)) {
|
|
32
|
+
return d;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ProjectGraph } from "./project";
|
|
2
|
+
import { scanSource } from "./scanner";
|
|
3
|
+
|
|
4
|
+
export class EventraEngine {
|
|
5
|
+
private project: ProjectGraph;
|
|
6
|
+
private fileEvents = new Map<string, Set<string>>();
|
|
7
|
+
|
|
8
|
+
constructor(entry: string) {
|
|
9
|
+
this.project = new ProjectGraph(entry);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
scanFile(file: string, content: string) {
|
|
13
|
+
const source = this.project.getSource(file, content);
|
|
14
|
+
const checker = this.project.getChecker();
|
|
15
|
+
|
|
16
|
+
const events = scanSource(source, checker);
|
|
17
|
+
|
|
18
|
+
this.fileEvents.set(file, events);
|
|
19
|
+
return events;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
updateFile(file: string, content: string) {
|
|
23
|
+
return this.scanFile(file, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getAllEvents() {
|
|
27
|
+
const all = new Set<string>();
|
|
28
|
+
|
|
29
|
+
for (const set of this.fileEvents.values()) {
|
|
30
|
+
set.forEach(e => all.add(e));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [...all];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
removeFile(file: string) {
|
|
37
|
+
this.fileEvents.delete(file);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Node, ObjectLiteralExpression, ts } from "ts-morph";
|
|
2
|
+
import { resolveNodeValue } from "./resolver";
|
|
3
|
+
|
|
4
|
+
export type Extracted = {
|
|
5
|
+
value: string;
|
|
6
|
+
dynamic: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function extractEventsFromArgs(
|
|
10
|
+
args: Node[],
|
|
11
|
+
checker: ts.TypeChecker
|
|
12
|
+
): Extracted[] {
|
|
13
|
+
return args.flatMap(arg => extract(arg, checker));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extract(node: Node, checker: ts.TypeChecker): Extracted[] {
|
|
17
|
+
if (Node.isStringLiteral(node)) {
|
|
18
|
+
return [{ value: node.getLiteralText(), dynamic: false }];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
22
|
+
return [{ value: node.getLiteralText(), dynamic: false }];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Node.isTemplateExpression(node)) {
|
|
26
|
+
let result = node.getHead().getLiteralText();
|
|
27
|
+
|
|
28
|
+
for (const span of node.getTemplateSpans()) {
|
|
29
|
+
result += "*";
|
|
30
|
+
result += span.getLiteral().getLiteralText();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [{ value: result, dynamic: true }];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Node.isObjectLiteralExpression(node)) {
|
|
37
|
+
return extractObject(node, checker);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const resolved = resolveNodeValue(node, checker);
|
|
41
|
+
if (resolved) {
|
|
42
|
+
return resolved.values.map(v => ({
|
|
43
|
+
value: v,
|
|
44
|
+
dynamic: resolved.dynamic,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractObject(
|
|
52
|
+
obj: ObjectLiteralExpression,
|
|
53
|
+
checker: ts.TypeChecker
|
|
54
|
+
): Extracted[] {
|
|
55
|
+
const res: Extracted[] = [];
|
|
56
|
+
|
|
57
|
+
for (const prop of obj.getProperties()) {
|
|
58
|
+
if (!Node.isPropertyAssignment(prop)) continue;
|
|
59
|
+
|
|
60
|
+
const name = prop.getName();
|
|
61
|
+
const init = prop.getInitializer();
|
|
62
|
+
if (!init) continue;
|
|
63
|
+
|
|
64
|
+
if (["event", "name", "action", "type"].includes(name)) {
|
|
65
|
+
res.push(...extract(init, checker));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (Node.isObjectLiteralExpression(init)) {
|
|
69
|
+
res.push(...extractObject(init, checker));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return res;
|
|
74
|
+
}
|