@eventra_dev/eventra-cli 0.1.2 → 0.1.3

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.
@@ -7,7 +7,9 @@ exports.scanSource = scanSource;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  const resolver_1 = require("./resolver");
9
9
  const callResolver_1 = require("./callResolver");
10
- function scanSource(source, checker, visited = new Set()) {
10
+ function scanSource(source, checker, visited = new Set(), depth = 0) {
11
+ if (depth > 5)
12
+ return new Set();
11
13
  const events = new Set();
12
14
  if (visited.has(source.fileName))
13
15
  return events;
@@ -16,19 +18,35 @@ function scanSource(source, checker, visited = new Set()) {
16
18
  if (typescript_1.default.isCallExpression(node)) {
17
19
  const name = getCallName(node.expression);
18
20
  if (/(track|event|capture|send)/i.test(name)) {
19
- let target = node.arguments[0];
20
21
  const fn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
21
- // 🔥 cross-file
22
+ // param mapping
23
+ let paramMap = new Map();
24
+ if (fn && typescript_1.default.isFunctionLike(fn)) {
25
+ fn.parameters.forEach((param, index) => {
26
+ const arg = node.arguments[index];
27
+ if (!arg)
28
+ return;
29
+ if (typescript_1.default.isIdentifier(param.name)) {
30
+ paramMap.set(param.name.text, arg);
31
+ }
32
+ });
33
+ }
34
+ // cross-file traversal
22
35
  if (fn) {
23
36
  const sf = fn.getSourceFile();
24
37
  if (!visited.has(sf.fileName)) {
25
- scanSource(sf, checker, visited).forEach(e => events.add(e));
38
+ scanSource(sf, checker, visited, depth + 1)
39
+ .forEach(e => events.add(e));
26
40
  }
27
41
  }
28
- if (target) {
29
- const res = (0, resolver_1.resolveNodeValue)(target, checker);
42
+ for (const arg of node.arguments) {
43
+ const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
30
44
  res?.values.forEach(v => {
31
- if (v && v.length > 1 && !v.includes(" ")) {
45
+ if (v &&
46
+ v.length > 1 &&
47
+ v.length < 100 &&
48
+ !v.includes(" ") &&
49
+ /^[a-zA-Z0-9._:-]+$/.test(v)) {
32
50
  events.add(v);
33
51
  }
34
52
  });
@@ -36,8 +36,13 @@ async function loadConfig() {
36
36
  if (!(await fs_extra_1.default.pathExists(configPath))) {
37
37
  return null;
38
38
  }
39
- const config = await fs_extra_1.default.readJSON(configPath);
40
- return normalizeConfig(config);
39
+ try {
40
+ const config = await fs_extra_1.default.readJSON(configPath);
41
+ return normalizeConfig(config);
42
+ }
43
+ catch {
44
+ return null;
45
+ }
41
46
  }
42
47
  async function saveConfig(config) {
43
48
  const configPath = path_1.default.join(process.cwd(), exports.CONFIG_NAME);
@@ -2,5 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseAstro = parseAstro;
4
4
  function parseAstro(content) {
5
- return content;
5
+ const frontmatter = content.match(/---([\s\S]*?)---/);
6
+ const scripts = [
7
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
8
+ ];
9
+ return [
10
+ frontmatter?.[1] ?? "",
11
+ ...scripts.map(s => s[1])
12
+ ].join("\n");
6
13
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectParser = detectParser;
4
4
  function detectParser(file) {
5
+ file = file.toLowerCase();
5
6
  if (file.endsWith(".vue"))
6
7
  return "vue";
7
8
  if (file.endsWith(".svelte"))
@@ -2,5 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseSvelte = parseSvelte;
4
4
  function parseSvelte(content) {
5
- return content;
5
+ const scriptMatches = [
6
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
7
+ ];
8
+ return scriptMatches.map(m => m[1]).join("\n");
6
9
  }
@@ -2,15 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseVue = parseVue;
4
4
  function parseVue(content) {
5
- const scriptMatch = content.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/);
5
+ const scriptMatches = [
6
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
7
+ ];
8
+ const script = scriptMatches.map(m => m[1]).join("\n");
9
+ // template
6
10
  const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
7
- const script = scriptMatch?.[1] ?? "";
8
11
  let template = templateMatch?.[1] ?? "";
9
12
  template = template.replace(/<!--[\s\S]*?-->/g, "");
13
+ // Vue → JSX
10
14
  template = template
15
+ .replace(/\bclass=/g, "className=")
16
+ .replace(/\bfor=/g, "htmlFor=")
17
+ .replace(/@(\w+)=/g, (_, e) => {
18
+ const name = e.charAt(0).toUpperCase() + e.slice(1);
19
+ return `on${name}=`;
20
+ })
11
21
  .replace(/v-if="[^"]+"/g, "")
12
- .replace(/v-for="[^"]+"/g, "")
13
- .replace(/@click=/g, "onClick=");
22
+ .replace(/v-for="[^"]+"/g, "");
14
23
  const jsx = `
15
24
  const __VUE_JSX__ = (
16
25
  <>
@@ -13,7 +13,9 @@ function processFile(file, content) {
13
13
  content = (0, svelte_1.parseSvelte)(content);
14
14
  if (parser === "astro")
15
15
  content = (0, astro_1.parseAstro)(content);
16
- const virtualFile = file.endsWith(".vue")
16
+ const virtualFile = file.endsWith(".vue") ||
17
+ file.endsWith(".svelte") ||
18
+ file.endsWith(".astro")
17
19
  ? file + ".tsx"
18
20
  : file;
19
21
  return { content, virtualFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -30,8 +30,12 @@ export async function check({ fix = false }: { fix?: boolean }) {
30
30
 
31
31
  engine.scanFile(virtualFile, content);
32
32
 
33
- } catch {
34
- console.log(chalk.gray(`Skipped: ${file}`));
33
+ } catch (err) {
34
+ console.log(chalk.yellow(`Skipped: ${file}`));
35
+
36
+ if (err instanceof Error) {
37
+ console.log(chalk.gray(err.message));
38
+ }
35
39
  }
36
40
  }
37
41
 
@@ -39,14 +43,17 @@ export async function check({ fix = false }: { fix?: boolean }) {
39
43
  const known = new Set(config.events ?? []);
40
44
 
41
45
  const newEvents = [...found].filter(e => !known.has(e));
46
+ const removed = [...known].filter(e => !found.has(e));
42
47
 
43
48
  if (fix) {
49
+ const merged = new Set(config.events ?? []);
50
+
44
51
  newEvents.forEach(e => {
45
- config.events.push(e);
52
+ merged.add(e);
46
53
  console.log(chalk.green(`+ ${e}`));
47
54
  });
48
55
 
49
- config.events = [...new Set(config.events)].sort();
56
+ config.events = [...merged].sort();
50
57
 
51
58
  await saveConfig(config);
52
59
 
@@ -54,9 +61,16 @@ export async function check({ fix = false }: { fix?: boolean }) {
54
61
  return;
55
62
  }
56
63
 
57
- if (newEvents.length) {
58
- console.log(chalk.red("\nNew events:"));
59
- newEvents.forEach(e => console.log(chalk.red(`+ ${e}`)));
64
+ if (newEvents.length || removed.length) {
65
+ if (newEvents.length) {
66
+ console.log(chalk.red("\nNew events:"));
67
+ newEvents.forEach(e => console.log(chalk.red(`+ ${e}`)));
68
+ }
69
+
70
+ if (removed.length) {
71
+ console.log(chalk.yellow("\nRemoved events:"));
72
+ removed.forEach(e => console.log(chalk.yellow(`- ${e}`)));
73
+ }
60
74
 
61
75
  process.exit(1);
62
76
  }
@@ -81,6 +81,9 @@ export async function send() {
81
81
  )
82
82
  );
83
83
 
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), 10000);
86
+
84
87
  try {
85
88
  const res = await fetch(
86
89
  endpoint,
@@ -99,8 +102,9 @@ export async function send() {
99
102
  version: CLI_VERSION,
100
103
  runtime: "node"
101
104
  }
102
- })
103
- }
105
+ }),
106
+ signal: controller.signal
107
+ },
104
108
  );
105
109
 
106
110
  if (!res.ok) {
@@ -203,5 +207,7 @@ export async function send() {
203
207
  )
204
208
  );
205
209
  }
210
+ } finally {
211
+ clearTimeout(timeout);
206
212
  }
207
213
  }
@@ -31,9 +31,12 @@ export async function sync() {
31
31
 
32
32
  engine.scanFile(virtualFile, content);
33
33
 
34
- } catch (e) {
35
- console.log(chalk.red(`ERROR: ${file}`));
36
- console.error(e);
34
+ } catch (err) {
35
+ console.log(chalk.yellow(`Skipped: ${file}`));
36
+
37
+ if (err instanceof Error) {
38
+ console.log(chalk.gray(err.message));
39
+ }
37
40
  }
38
41
  }
39
42
 
@@ -5,31 +5,8 @@ import fg from "fast-glob";
5
5
 
6
6
  import { loadConfig, saveConfig } from "../utils/config";
7
7
  import { EventraEngine } from "../engine/engine";
8
+ import { processFile } from "../utils/processFile";
8
9
 
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
10
  export async function watch() {
34
11
  const config = await loadConfig();
35
12
  if (!config) return;
@@ -40,20 +17,13 @@ export async function watch() {
40
17
  ignore: config.sync.exclude,
41
18
  });
42
19
 
43
- if (!files.length) {
44
- console.log(chalk.yellow("No files found"));
45
- return;
46
- }
47
-
48
20
  const engine = new EventraEngine(process.cwd());
49
21
 
50
- // INITIAL SCAN
51
22
  for (const file of files) {
52
23
  try {
53
24
  const raw = await fs.readFile(file, "utf-8");
54
25
 
55
- const content = preprocess(file, raw);
56
- const virtualFile = toVirtualFile(file);
26
+ const { content, virtualFile } = processFile(file, raw);
57
27
 
58
28
  engine.scanFile(virtualFile, content);
59
29
 
@@ -69,9 +39,9 @@ export async function watch() {
69
39
  chalk.gray(`Initial: ${config.events.length} events\n`)
70
40
  );
71
41
 
72
- // STATE
73
42
  let locked = false;
74
43
  let queued = new Set<string>();
44
+ let timer: NodeJS.Timeout | null = null;
75
45
 
76
46
  const run = async () => {
77
47
  if (locked) return;
@@ -84,8 +54,7 @@ export async function watch() {
84
54
  try {
85
55
  const raw = await fs.readFile(file, "utf-8");
86
56
 
87
- const content = preprocess(file, raw);
88
- const virtualFile = toVirtualFile(file);
57
+ const { content, virtualFile } = processFile(file, raw);
89
58
 
90
59
  engine.updateFile(virtualFile, content);
91
60
 
@@ -116,48 +85,33 @@ export async function watch() {
116
85
 
117
86
  const schedule = (file: string) => {
118
87
  queued.add(file);
119
- setTimeout(run, 50);
88
+
89
+ if (timer) clearTimeout(timer);
90
+
91
+ timer = setTimeout(run, 100);
120
92
  };
121
93
 
122
- // WATCHER
123
94
  const watcher = chokidar.watch(config.sync.include, {
124
95
  ignored: config.sync.exclude,
125
96
  ignoreInitial: true,
126
- awaitWriteFinish: {
127
- stabilityThreshold: 50,
128
- pollInterval: 10,
129
- },
130
97
  });
131
98
 
132
99
  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
- })
100
+ .on("add", schedule)
101
+ .on("change", schedule)
141
102
  .on("unlink", async (file) => {
142
103
  console.log(chalk.gray(`remove: ${file}`));
143
104
 
144
- const virtualFile = toVirtualFile(file);
105
+ const { virtualFile } = processFile(file, "");
145
106
 
146
- engine.removeFile?.(virtualFile);
107
+ engine.removeFile(virtualFile);
147
108
 
148
109
  const next = engine.getAllEvents();
149
110
 
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
- }
111
+ config.events = next.sort();
112
+ await saveConfig(config);
158
113
  });
159
114
 
160
- // EXIT
161
115
  process.on("SIGINT", async () => {
162
116
  console.log(chalk.yellow("\nStopping watcher..."));
163
117
  await watcher.close();
@@ -5,38 +5,21 @@ export function resolveFunctionFromCall(
5
5
  checker: ts.TypeChecker
6
6
  ): ts.FunctionLikeDeclaration | null {
7
7
 
8
- let symbol = checker.getSymbolAtLocation(expr);
8
+ const type = checker.getTypeAtLocation(expr);
9
+ const signatures = type.getCallSignatures();
9
10
 
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
- }
11
+ for (const sig of signatures) {
12
+ const decl = sig.getDeclaration();
19
13
 
20
- for (const decl of symbol.getDeclarations() ?? []) {
21
14
  if (
22
- ts.isFunctionDeclaration(decl) ||
23
- ts.isMethodDeclaration(decl) ||
24
- ts.isFunctionExpression(decl) ||
25
- ts.isArrowFunction(decl)
15
+ decl &&
16
+ (ts.isFunctionDeclaration(decl) ||
17
+ ts.isMethodDeclaration(decl) ||
18
+ ts.isFunctionExpression(decl) ||
19
+ ts.isArrowFunction(decl))
26
20
  ) {
27
21
  return decl;
28
22
  }
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
23
  }
41
24
 
42
25
  return null;
@@ -22,7 +22,6 @@ export class EventraEngine {
22
22
  }
23
23
 
24
24
  this.fileHash.set(file, h);
25
-
26
25
  this.ts.updateFile(file, content);
27
26
 
28
27
  const source = this.ts.getSourceFile(file);
@@ -31,7 +30,6 @@ export class EventraEngine {
31
30
  if (!source || !checker) return new Set();
32
31
 
33
32
  this.graph.update(file, source);
34
-
35
33
  const events = scanSource(source, checker);
36
34
 
37
35
  this.fileEvents.set(file, events);
@@ -40,7 +38,20 @@ export class EventraEngine {
40
38
  }
41
39
 
42
40
  updateFile(file: string, content: string) {
43
- return this.scanFile(file, content);
41
+ const events = this.scanFile(file, content);
42
+ const dependents = this.graph.getDependents(file);
43
+
44
+ for (const dep of dependents) {
45
+ const source = this.ts.getSourceFile(dep);
46
+ const checker = this.ts.getChecker();
47
+
48
+ if (!source || !checker) continue;
49
+
50
+ const ev = scanSource(source, checker);
51
+ this.fileEvents.set(dep, ev);
52
+ }
53
+
54
+ return events;
44
55
  }
45
56
 
46
57
  getAllEvents() {
@@ -56,5 +67,6 @@ export class EventraEngine {
56
67
  removeFile(file: string) {
57
68
  this.fileEvents.delete(file);
58
69
  this.fileHash.delete(file);
70
+ this.graph.remove(file);
59
71
  }
60
72
  }
@@ -1,23 +1,70 @@
1
1
  import ts from "typescript";
2
+ import path from "path";
3
+ import fs from "fs";
2
4
 
3
5
  export class DependencyGraph {
4
6
  private reverse = new Map<string, Set<string>>();
5
7
 
8
+ private normalize(file: string) {
9
+ return path.resolve(file);
10
+ }
11
+
12
+ private resolveImport(from: string, module: string): string | null {
13
+ if (!module.startsWith(".")) return null;
14
+
15
+ const base = path.resolve(path.dirname(from), module);
16
+
17
+ const tryFiles = [
18
+ base,
19
+ base + ".ts",
20
+ base + ".tsx",
21
+ base + ".js",
22
+ base + ".jsx",
23
+ path.join(base, "index.ts"),
24
+ path.join(base, "index.tsx"),
25
+ path.join(base, "index.js"),
26
+ ];
27
+
28
+ for (const f of tryFiles) {
29
+ if (fs.existsSync(f)) {
30
+ return this.normalize(f);
31
+ }
32
+ }
33
+
34
+ return this.normalize(base);
35
+ }
36
+
6
37
  update(file: string, source: ts.SourceFile) {
38
+ const normalizedFile = this.normalize(file);
39
+
7
40
  for (const stmt of source.statements) {
8
41
  if (!ts.isImportDeclaration(stmt)) continue;
9
42
 
10
- const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
43
+ const module = stmt.moduleSpecifier
44
+ .getText()
45
+ .replace(/['"]/g, "");
46
+
47
+ const resolved = this.resolveImport(normalizedFile, module);
48
+
49
+ if (!resolved) continue;
11
50
 
12
- if (!this.reverse.has(module)) {
13
- this.reverse.set(module, new Set());
51
+ if (!this.reverse.has(resolved)) {
52
+ this.reverse.set(resolved, new Set());
14
53
  }
15
54
 
16
- this.reverse.get(module)!.add(file);
55
+ this.reverse.get(resolved)!.add(normalizedFile);
17
56
  }
18
57
  }
19
58
 
20
59
  getDependents(file: string) {
21
- return [...(this.reverse.get(file) ?? [])];
60
+ return [...(this.reverse.get(this.normalize(file)) ?? [])];
61
+ }
62
+
63
+ remove(file: string) {
64
+ const normalized = this.normalize(file);
65
+
66
+ for (const set of this.reverse.values()) {
67
+ set.delete(normalized);
68
+ }
22
69
  }
23
70
  }
@@ -6,10 +6,51 @@ export class TSService {
6
6
  private service: ts.LanguageService;
7
7
 
8
8
  constructor(private root: string) {
9
+ const configPath = ts.findConfigFile(
10
+ root,
11
+ ts.sys.fileExists,
12
+ "tsconfig.json"
13
+ );
14
+
15
+ let compilerOptions: ts.CompilerOptions = {
16
+ allowJs: true,
17
+ jsx: ts.JsxEmit.Preserve,
18
+ moduleResolution: ts.ModuleResolutionKind.Node16,
19
+ target: ts.ScriptTarget.ESNext,
20
+ esModuleInterop: true,
21
+ allowSyntheticDefaultImports: true,
22
+ };
23
+
24
+ if (configPath) {
25
+ const configFile = ts.readConfigFile(
26
+ configPath,
27
+ ts.sys.readFile
28
+ );
29
+
30
+ const parsed = ts.parseJsonConfigFileContent(
31
+ configFile.config,
32
+ ts.sys,
33
+ root
34
+ );
35
+
36
+ compilerOptions = {
37
+ ...compilerOptions,
38
+ ...parsed.options,
39
+ };
40
+ }
41
+
9
42
  const host: ts.LanguageServiceHost = {
10
- getScriptFileNames: () => [...this.files.keys()],
43
+ getScriptFileNames: () => [
44
+ ...this.files.keys(),
45
+ ...ts.sys.readDirectory(
46
+ this.root,
47
+ [".ts", ".tsx"],
48
+ ["node_modules", "dist", ".git"]
49
+ )
50
+ ],
11
51
  getScriptVersion: (file) =>
12
52
  this.files.get(file)?.version.toString() ?? "0",
53
+
13
54
  getScriptSnapshot: (file) => {
14
55
  const f = this.files.get(file);
15
56
 
@@ -19,32 +60,21 @@ export class TSService {
19
60
 
20
61
  if (fs.existsSync(file)) {
21
62
  return ts.ScriptSnapshot.fromString(
22
- fs.readFileSync(file, "utf-8" as BufferEncoding)
63
+ fs.readFileSync(file, "utf-8")
23
64
  );
24
65
  }
25
66
 
26
67
  return undefined;
27
68
  },
69
+
28
70
  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
- },
71
+ getCompilationSettings: () => compilerOptions,
72
+ getDefaultLibFileName: (opts) =>
73
+ ts.getDefaultLibFilePath(opts),
46
74
 
47
- fileExists: fs.existsSync,
75
+ readFile: ts.sys.readFile,
76
+ fileExists: ts.sys.fileExists,
77
+ readDirectory: ts.sys.readDirectory,
48
78
  };
49
79
 
50
80
  this.service = ts.createLanguageService(host);