@eventra_dev/eventra-cli 0.0.19 → 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/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 +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/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 +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/types.ts +3 -9
- 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,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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Node,
|
|
3
|
+
SyntaxKind,
|
|
4
|
+
FunctionDeclaration,
|
|
5
|
+
ArrowFunction,
|
|
6
|
+
FunctionExpression
|
|
7
|
+
} from "ts-morph";
|
|
8
|
+
|
|
9
|
+
type Fn =
|
|
10
|
+
| FunctionDeclaration
|
|
11
|
+
| ArrowFunction
|
|
12
|
+
| FunctionExpression;
|
|
13
|
+
|
|
14
|
+
function getBodyNodes(fn: Fn) {
|
|
15
|
+
const body = fn.getBody();
|
|
16
|
+
if (!body) return [];
|
|
17
|
+
|
|
18
|
+
if (Node.isBlock(body)) return body.getStatements();
|
|
19
|
+
return [body];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getForwardedParamIndex(fn: Node): number | null {
|
|
23
|
+
if (
|
|
24
|
+
!Node.isFunctionDeclaration(fn) &&
|
|
25
|
+
!Node.isArrowFunction(fn) &&
|
|
26
|
+
!Node.isFunctionExpression(fn)
|
|
27
|
+
) return null;
|
|
28
|
+
|
|
29
|
+
const f = fn as Fn;
|
|
30
|
+
const params = f.getParameters();
|
|
31
|
+
|
|
32
|
+
for (const node of getBodyNodes(f)) {
|
|
33
|
+
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
34
|
+
|
|
35
|
+
for (const call of calls) {
|
|
36
|
+
const name = call.getExpression().getText();
|
|
37
|
+
|
|
38
|
+
if (!/(track|event|log|analytics|capture|send)/i.test(name)) continue;
|
|
39
|
+
|
|
40
|
+
const args = call.getArguments();
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
const arg = args[i];
|
|
44
|
+
|
|
45
|
+
if (Node.isIdentifier(arg)) {
|
|
46
|
+
const idx = params.findIndex(p => p.getName() === arg.getText());
|
|
47
|
+
if (idx !== -1) return idx;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|