@eventra_dev/eventra-cli 0.1.2 → 0.1.4

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.
@@ -1,23 +1,110 @@
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>>();
7
+ private exports = new Map<string, Set<string>>();
8
+
9
+ private normalize(file: string) {
10
+ return path.resolve(file);
11
+ }
12
+
13
+ private resolveImport(from: string, module: string): string | null {
14
+ if (!module.startsWith(".")) return null;
15
+
16
+ const base = path.resolve(path.dirname(from), module);
17
+
18
+ const tryFiles = [
19
+ base,
20
+ base + ".ts",
21
+ base + ".tsx",
22
+ base + ".js",
23
+ base + ".jsx",
24
+ path.join(base, "index.ts"),
25
+ path.join(base, "index.tsx"),
26
+ path.join(base, "index.js"),
27
+ ];
28
+
29
+ for (const f of tryFiles) {
30
+ if (fs.existsSync(f)) {
31
+ return this.normalize(f);
32
+ }
33
+ }
34
+
35
+ return this.normalize(base);
36
+ }
5
37
 
6
38
  update(file: string, source: ts.SourceFile) {
39
+ const normalizedFile = this.normalize(file);
40
+
41
+ this.exports.set(normalizedFile, new Set());
42
+
7
43
  for (const stmt of source.statements) {
8
- if (!ts.isImportDeclaration(stmt)) continue;
44
+ // IMPORT
45
+ if (ts.isImportDeclaration(stmt)) {
46
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
47
+ const resolved = this.resolveImport(normalizedFile, module);
48
+
49
+ if (!resolved) continue;
50
+
51
+ if (!this.reverse.has(resolved)) {
52
+ this.reverse.set(resolved, new Set());
53
+ }
54
+
55
+ this.reverse.get(resolved)!.add(normalizedFile);
56
+ }
9
57
 
10
- const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
58
+ // EXPORT
59
+ if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
60
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
61
+ const resolved = this.resolveImport(normalizedFile, module);
11
62
 
12
- if (!this.reverse.has(module)) {
13
- this.reverse.set(module, new Set());
63
+ if (!resolved) continue;
64
+
65
+ if (!this.exports.has(normalizedFile)) {
66
+ this.exports.set(normalizedFile, new Set());
67
+ }
68
+
69
+ this.exports.get(normalizedFile)!.add(resolved);
70
+
71
+ if (!this.reverse.has(resolved)) {
72
+ this.reverse.set(resolved, new Set());
73
+ }
74
+
75
+ this.reverse.get(resolved)!.add(normalizedFile);
14
76
  }
77
+ }
78
+ }
79
+
80
+ getAllRelated(file: string, visited = new Set<string>()): Set<string> {
81
+ const normalized = this.normalize(file);
82
+
83
+ if (visited.has(normalized)) return visited;
84
+ visited.add(normalized);
15
85
 
16
- this.reverse.get(module)!.add(file);
86
+ const deps = this.exports.get(normalized);
87
+
88
+ if (!deps) return visited;
89
+
90
+ for (const dep of deps) {
91
+ this.getAllRelated(dep, visited);
17
92
  }
93
+
94
+ return visited;
18
95
  }
19
96
 
20
97
  getDependents(file: string) {
21
- return [...(this.reverse.get(file) ?? [])];
98
+ return [...(this.reverse.get(this.normalize(file)) ?? [])];
99
+ }
100
+
101
+ remove(file: string) {
102
+ const normalized = this.normalize(file);
103
+
104
+ this.exports.delete(normalized);
105
+
106
+ for (const set of this.reverse.values()) {
107
+ set.delete(normalized);
108
+ }
22
109
  }
23
110
  }
@@ -3,13 +3,49 @@ import fs from "fs";
3
3
 
4
4
  export class TSService {
5
5
  private files = new Map<string, { version: number; content: string }>();
6
+ private fileNames = new Set<string>();
6
7
  private service: ts.LanguageService;
7
8
 
8
9
  constructor(private root: string) {
10
+ const configPath = ts.findConfigFile(
11
+ root,
12
+ ts.sys.fileExists,
13
+ "tsconfig.json"
14
+ );
15
+
16
+ let compilerOptions: ts.CompilerOptions = {
17
+ allowJs: true,
18
+ jsx: ts.JsxEmit.Preserve,
19
+ moduleResolution: ts.ModuleResolutionKind.Node16,
20
+ target: ts.ScriptTarget.ESNext,
21
+ esModuleInterop: true,
22
+ allowSyntheticDefaultImports: true,
23
+ };
24
+
25
+ if (configPath) {
26
+ const configFile = ts.readConfigFile(
27
+ configPath,
28
+ ts.sys.readFile
29
+ );
30
+
31
+ const parsed = ts.parseJsonConfigFileContent(
32
+ configFile.config,
33
+ ts.sys,
34
+ root
35
+ );
36
+
37
+ compilerOptions = {
38
+ ...compilerOptions,
39
+ ...parsed.options,
40
+ };
41
+ }
42
+
9
43
  const host: ts.LanguageServiceHost = {
10
- getScriptFileNames: () => [...this.files.keys()],
44
+ getScriptFileNames: () => [...this.fileNames],
45
+
11
46
  getScriptVersion: (file) =>
12
47
  this.files.get(file)?.version.toString() ?? "0",
48
+
13
49
  getScriptSnapshot: (file) => {
14
50
  const f = this.files.get(file);
15
51
 
@@ -19,32 +55,21 @@ export class TSService {
19
55
 
20
56
  if (fs.existsSync(file)) {
21
57
  return ts.ScriptSnapshot.fromString(
22
- fs.readFileSync(file, "utf-8" as BufferEncoding)
58
+ fs.readFileSync(file, "utf-8")
23
59
  );
24
60
  }
25
61
 
26
62
  return undefined;
27
63
  },
64
+
28
65
  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
- },
66
+ getCompilationSettings: () => compilerOptions,
67
+ getDefaultLibFileName: (opts) =>
68
+ ts.getDefaultLibFilePath(opts),
46
69
 
47
- fileExists: fs.existsSync,
70
+ readFile: ts.sys.readFile,
71
+ fileExists: ts.sys.fileExists,
72
+ readDirectory: ts.sys.readDirectory,
48
73
  };
49
74
 
50
75
  this.service = ts.createLanguageService(host);
@@ -57,6 +82,8 @@ export class TSService {
57
82
  content,
58
83
  version: prev ? prev.version + 1 : 1,
59
84
  });
85
+
86
+ this.fileNames.add(file);
60
87
  }
61
88
 
62
89
  getProgram() {
@@ -7,9 +7,23 @@ export type ResolveResult = {
7
7
 
8
8
  export function resolveNodeValue(
9
9
  node: ts.Node,
10
- checker: ts.TypeChecker
10
+ checker: ts.TypeChecker,
11
+ paramMap?: Map<string, ts.Expression>,
12
+ seen = new Set<ts.Node>()
11
13
  ): ResolveResult | null {
12
14
 
15
+ if (seen.has(node)) return null;
16
+ seen.add(node);
17
+
18
+ // param mapping
19
+ if (ts.isIdentifier(node) && paramMap?.has(node.text)) {
20
+ return resolveNodeValue(
21
+ paramMap.get(node.text)!,
22
+ checker,
23
+ paramMap
24
+ );
25
+ }
26
+
13
27
  if (ts.isStringLiteral(node)) {
14
28
  return { values: [node.text], dynamic: false };
15
29
  }
@@ -26,8 +40,8 @@ export function resolveNodeValue(
26
40
  }
27
41
 
28
42
  if (ts.isConditionalExpression(node)) {
29
- const a = resolveNodeValue(node.whenTrue, checker);
30
- const b = resolveNodeValue(node.whenFalse, checker);
43
+ const a = resolveNodeValue(node.whenTrue, checker, paramMap, seen);
44
+ const b = resolveNodeValue(node.whenFalse, checker, paramMap, seen);
31
45
 
32
46
  return {
33
47
  values: [...new Set([...(a?.values ?? []), ...(b?.values ?? [])])],
@@ -45,7 +59,7 @@ export function resolveNodeValue(
45
59
 
46
60
  for (const decl of symbol.getDeclarations() ?? []) {
47
61
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
48
- return resolveNodeValue(decl.initializer, checker);
62
+ return resolveNodeValue(decl.initializer, checker, paramMap, seen);
49
63
  }
50
64
 
51
65
  if (ts.isEnumMember(decl) && decl.initializer) {
@@ -82,5 +96,38 @@ export function resolveNodeValue(
82
96
  }
83
97
  }
84
98
 
99
+ if (ts.isObjectLiteralExpression(node)) {
100
+ const values: string[] = [];
101
+
102
+ for (const prop of node.properties) {
103
+ if (!ts.isPropertyAssignment(prop)) continue;
104
+
105
+ const name = prop.name.getText();
106
+
107
+ if (name === "event") {
108
+ const res = resolveNodeValue(prop.initializer, checker, paramMap, seen);
109
+ if (res) values.push(...res.values);
110
+ }
111
+ }
112
+
113
+ return { values, dynamic: true };
114
+ }
115
+
116
+ if (ts.isBinaryExpression(node)) {
117
+ if (node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
118
+ const left = resolveNodeValue(node.left, checker, paramMap, seen);
119
+ const right = resolveNodeValue(node.right, checker, paramMap, seen);
120
+
121
+ if (left && right) {
122
+ return {
123
+ values: left.values.flatMap(l =>
124
+ right.values.map(r => l + r)
125
+ ),
126
+ dynamic: true,
127
+ };
128
+ }
129
+ }
130
+ }
131
+
85
132
  return null;
86
133
  }
@@ -1,12 +1,52 @@
1
1
  import ts from "typescript";
2
2
  import { resolveNodeValue } from "./resolver";
3
3
  import { resolveFunctionFromCall } from "./callResolver";
4
+ import { findTrackParamIndex } from "./functionAnalyzer";
5
+
6
+ function getCallName(expr: ts.Expression): string {
7
+ if (ts.isIdentifier(expr)) return expr.text;
8
+
9
+ if (ts.isPropertyAccessExpression(expr)) {
10
+ return expr.name.text;
11
+ }
12
+
13
+ return "";
14
+ }
15
+
16
+ function pickBestArg(
17
+ args: readonly ts.Expression[],
18
+ checker: ts.TypeChecker,
19
+ paramMap: Map<string, ts.Expression>
20
+ ) {
21
+ let best: { score: number; node: ts.Expression } | null = null;
22
+
23
+ for (const arg of args) {
24
+ const res = resolveNodeValue(arg, checker, paramMap);
25
+
26
+ if (!res?.values?.length) continue;
27
+
28
+ let score = 0;
29
+
30
+ if (!res.dynamic) score += 3;
31
+ if (res.values.every(v => v.length < 50)) score += 2;
32
+ if (res.values.some(v => v.includes("*"))) score -= 2;
33
+
34
+ if (!best || score > best.score) {
35
+ best = { score, node: arg };
36
+ }
37
+ }
38
+
39
+ return best?.node;
40
+ }
4
41
 
5
42
  export function scanSource(
6
43
  source: ts.SourceFile,
7
44
  checker: ts.TypeChecker,
8
- visited = new Set<string>()
9
- ) {
45
+ visited = new Set<string>(),
46
+ depth = 0
47
+ ): Set<string> {
48
+ if (depth > 5) return new Set();
49
+
10
50
  const events = new Set<string>();
11
51
 
12
52
  if (visited.has(source.fileName)) return events;
@@ -17,24 +57,61 @@ export function scanSource(
17
57
  const name = getCallName(node.expression);
18
58
 
19
59
  if (/(track|event|capture|send)/i.test(name)) {
20
- let target = node.arguments[0];
21
-
22
60
  const fn = resolveFunctionFromCall(node.expression, checker);
23
61
 
24
- // 🔥 cross-file
25
- if (fn) {
26
- const sf = fn.getSourceFile();
62
+ const paramMap = new Map<string, ts.Expression>();
63
+
64
+ if (fn && ts.isFunctionLike(fn)) {
65
+ fn.parameters.forEach((param, index) => {
66
+ const arg = node.arguments[index];
67
+ if (!arg) return;
68
+
69
+ if (ts.isIdentifier(param.name)) {
70
+ paramMap.set(param.name.text, arg);
71
+ }
72
+ });
73
+ }
74
+
75
+ if (fn && ts.isFunctionLike(fn)) {
76
+ const idx = findTrackParamIndex(fn);
77
+
78
+ if (idx !== null) {
79
+ const realArg = node.arguments[idx];
27
80
 
28
- if (!visited.has(sf.fileName)) {
29
- scanSource(sf, checker, visited).forEach(e => events.add(e));
81
+ if (realArg) {
82
+ const res = resolveNodeValue(realArg, checker, paramMap);
83
+
84
+ res?.values.forEach(v => {
85
+ if (
86
+ v &&
87
+ v.length > 1 &&
88
+ v.length < 100 &&
89
+ !v.includes(" ") &&
90
+ /^[a-zA-Z0-9._:-]+$/.test(v)
91
+ ) {
92
+ events.add(v);
93
+ }
94
+ });
95
+
96
+ return;
97
+ }
30
98
  }
31
99
  }
32
100
 
101
+ // fallback (heuristic)
102
+ const target = pickBestArg(node.arguments, checker, paramMap);
103
+
33
104
  if (target) {
34
- const res = resolveNodeValue(target, checker);
105
+ const res = resolveNodeValue(target, checker, paramMap);
35
106
 
36
107
  res?.values.forEach(v => {
37
- if (v && v.length > 1 && !v.includes(" ")) {
108
+ if (
109
+ v &&
110
+ v.length > 1 &&
111
+ v.length < 100 &&
112
+ !v.includes(" ") &&
113
+ /^[a-zA-Z0-9._:-]+$/.test(v)
114
+ ) {
38
115
  events.add(v);
39
116
  }
40
117
  });
@@ -49,13 +126,3 @@ export function scanSource(
49
126
 
50
127
  return events;
51
128
  }
52
-
53
- function getCallName(expr: ts.Expression): string {
54
- if (ts.isIdentifier(expr)) return expr.text;
55
-
56
- if (ts.isPropertyAccessExpression(expr)) {
57
- return expr.name.text;
58
- }
59
-
60
- return "";
61
- }
@@ -12,8 +12,7 @@ export function normalizeConfig(
12
12
  events: config.events ?? [],
13
13
  endpoint: config.endpoint ?? "",
14
14
  wrappers: config.wrappers ?? [],
15
- functionWrappers:
16
- config.functionWrappers ?? [],
15
+ functionWrappers: config.functionWrappers ?? [],
17
16
  aliases: config.aliases ?? {},
18
17
  sync: config.sync ?? {
19
18
  include: [
@@ -30,43 +29,26 @@ export function normalizeConfig(
30
29
  }
31
30
 
32
31
  export async function loadConfig(): Promise<EventraConfig | null> {
33
- const configPath = path.join(
34
- process.cwd(),
35
- CONFIG_NAME
36
- );
32
+ const configPath = path.join(process.cwd(), CONFIG_NAME);
37
33
 
38
- if (
39
- !(await fs.pathExists(
40
- configPath
41
- ))
42
- ) {
34
+ if (!(await fs.pathExists(configPath))) {
43
35
  return null;
44
36
  }
45
37
 
46
- const config =
47
- await fs.readJSON(
48
- configPath
49
- );
50
-
51
- return normalizeConfig(config);
38
+ try {
39
+ const config = await fs.readJSON(configPath);
40
+ return normalizeConfig(config);
41
+ } catch {
42
+ return null;
43
+ }
52
44
  }
53
45
 
54
- export async function saveConfig(
55
- config: EventraConfig
56
- ) {
57
- const configPath = path.join(
58
- process.cwd(),
59
- CONFIG_NAME
60
- );
46
+ export async function saveConfig(config: EventraConfig) {
47
+ const configPath = path.join(process.cwd(), CONFIG_NAME);
61
48
 
62
- const normalized =
63
- normalizeConfig(config);
49
+ const normalized = normalizeConfig(config);
64
50
 
65
- await fs.writeJSON(
66
- configPath,
67
- normalized,
68
- {
69
- spaces: 2
70
- }
71
- );
51
+ await fs.writeJSON(configPath, normalized, {
52
+ spaces: 2
53
+ });
72
54
  }
@@ -1,5 +1,12 @@
1
- export function parseAstro(
2
- content: string
3
- ) {
4
- return content;
1
+ export function parseAstro(content: string) {
2
+ const frontmatter = content.match(/---([\s\S]*?)---/);
3
+
4
+ const scripts = [
5
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
6
+ ];
7
+
8
+ return [
9
+ frontmatter?.[1] ?? "",
10
+ ...scripts.map(s => s[1])
11
+ ].join("\n");
5
12
  }
@@ -1,14 +1,9 @@
1
- export function detectParser(
2
- file: string
3
- ) {
4
- if (file.endsWith(".vue"))
5
- return "vue";
1
+ export function detectParser(file: string) {
2
+ file = file.toLowerCase();
6
3
 
7
- if (file.endsWith(".svelte"))
8
- return "svelte";
9
-
10
- if (file.endsWith(".astro"))
11
- return "astro";
4
+ if (file.endsWith(".vue")) return "vue";
5
+ if (file.endsWith(".svelte")) return "svelte";
6
+ if (file.endsWith(".astro")) return "astro";
12
7
 
13
8
  return "ts";
14
9
  }
@@ -1,5 +1,7 @@
1
- export function parseSvelte(
2
- content: string
3
- ) {
4
- return content;
1
+ export function parseSvelte(content: string) {
2
+ const scriptMatches = [
3
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
4
+ ];
5
+
6
+ return scriptMatches.map(m => m[1]).join("\n");
5
7
  }
@@ -1,16 +1,26 @@
1
1
  export function parseVue(content: string) {
2
- const scriptMatch = content.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/);
3
- const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
2
+ const scriptMatches = [
3
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
4
+ ];
5
+
6
+ const script = scriptMatches.map(m => m[1]).join("\n");
4
7
 
5
- const script = scriptMatch?.[1] ?? "";
8
+ // template
9
+ const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
6
10
  let template = templateMatch?.[1] ?? "";
7
11
 
8
12
  template = template.replace(/<!--[\s\S]*?-->/g, "");
9
13
 
14
+ // Vue → JSX
10
15
  template = template
16
+ .replace(/\bclass=/g, "className=")
17
+ .replace(/\bfor=/g, "htmlFor=")
18
+ .replace(/@(\w+)=/g, (_, e) => {
19
+ const name = e.charAt(0).toUpperCase() + e.slice(1);
20
+ return `on${name}=`;
21
+ })
11
22
  .replace(/v-if="[^"]+"/g, "")
12
- .replace(/v-for="[^"]+"/g, "")
13
- .replace(/@click=/g, "onClick=");
23
+ .replace(/v-for="[^"]+"/g, "");
14
24
 
15
25
  const jsx = `
16
26
  const __VUE_JSX__ = (
@@ -10,9 +10,12 @@ export function processFile(file: string, content: string) {
10
10
  if (parser === "svelte") content = parseSvelte(content);
11
11
  if (parser === "astro") content = parseAstro(content);
12
12
 
13
- const virtualFile = file.endsWith(".vue")
14
- ? file + ".tsx"
15
- : file;
13
+ const virtualFile =
14
+ file.endsWith(".vue") ||
15
+ file.endsWith(".svelte") ||
16
+ file.endsWith(".astro")
17
+ ? file + ".tsx"
18
+ : file;
16
19
 
17
20
  return { content, virtualFile };
18
21
  }