@eventra_dev/eventra-cli 0.1.1 → 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.
@@ -1,4 +1,4 @@
1
- import { Node, ts } from "ts-morph";
1
+ import ts from "typescript";
2
2
 
3
3
  export type ResolveResult = {
4
4
  values: string[];
@@ -6,45 +6,78 @@ export type ResolveResult = {
6
6
  };
7
7
 
8
8
  export function resolveNodeValue(
9
- node: Node,
9
+ node: ts.Node,
10
10
  checker: ts.TypeChecker
11
11
  ): ResolveResult | null {
12
- if (!Node.isIdentifier(node)) return null;
13
12
 
14
- const symbol = checker.getSymbolAtLocation(node.compilerNode);
15
- if (!symbol) return null;
13
+ if (ts.isStringLiteral(node)) {
14
+ return { values: [node.text], dynamic: false };
15
+ }
16
16
 
17
- const decls = symbol.getDeclarations() ?? [];
17
+ if (ts.isTemplateExpression(node)) {
18
+ let result = node.head.text;
18
19
 
19
- for (const d of decls) {
20
- if (ts.isVariableDeclaration(d) && d.initializer) {
21
- const init = d.initializer;
20
+ for (const span of node.templateSpans) {
21
+ result += "*";
22
+ result += span.literal.text;
23
+ }
22
24
 
23
- if (ts.isStringLiteral(init)) {
24
- return { values: [init.text], dynamic: false };
25
- }
25
+ return { values: [result], dynamic: true };
26
+ }
26
27
 
27
- if (ts.isNoSubstitutionTemplateLiteral(init)) {
28
- return { values: [init.text], dynamic: false };
29
- }
28
+ if (ts.isConditionalExpression(node)) {
29
+ const a = resolveNodeValue(node.whenTrue, checker);
30
+ const b = resolveNodeValue(node.whenFalse, checker);
30
31
 
31
- if (ts.isTemplateExpression(init)) {
32
- let result = init.head.text;
32
+ return {
33
+ values: [...new Set([...(a?.values ?? []), ...(b?.values ?? [])])],
34
+ dynamic: true,
35
+ };
36
+ }
33
37
 
34
- for (const span of init.templateSpans) {
35
- result += "*";
36
- result += span.literal.text;
37
- }
38
+ if (ts.isIdentifier(node)) {
39
+ let symbol = checker.getSymbolAtLocation(node);
40
+ if (!symbol) return null;
41
+
42
+ if (symbol.flags & ts.SymbolFlags.Alias) {
43
+ symbol = checker.getAliasedSymbol(symbol);
44
+ }
38
45
 
39
- return { values: [result], dynamic: true };
46
+ for (const decl of symbol.getDeclarations() ?? []) {
47
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
48
+ return resolveNodeValue(decl.initializer, checker);
49
+ }
50
+
51
+ if (ts.isEnumMember(decl) && decl.initializer) {
52
+ if (ts.isStringLiteral(decl.initializer)) {
53
+ return {
54
+ values: [decl.initializer.text],
55
+ dynamic: false,
56
+ };
57
+ }
40
58
  }
41
59
  }
60
+ }
42
61
 
43
- if (ts.isParameter(d) && d.initializer) {
44
- const init = d.initializer;
62
+ if (ts.isPropertyAccessExpression(node)) {
63
+ const symbol = checker.getSymbolAtLocation(node);
45
64
 
46
- if (ts.isStringLiteral(init)) {
47
- return { values: [init.text], dynamic: false };
65
+ if (symbol) {
66
+ let s = symbol;
67
+
68
+ if (s.flags & ts.SymbolFlags.Alias) {
69
+ s = checker.getAliasedSymbol(s);
70
+ }
71
+
72
+ for (const decl of s.getDeclarations() ?? []) {
73
+ if (ts.isEnumMember(decl) && decl.initializer) {
74
+ if (ts.isStringLiteral(decl.initializer)) {
75
+ return {
76
+ values: [decl.initializer.text],
77
+ dynamic: false,
78
+ };
79
+ }
80
+ }
48
81
  }
49
82
  }
50
83
  }
@@ -1,90 +1,61 @@
1
- import { SourceFile, SyntaxKind, Node, ts } from "ts-morph";
2
- import { extractEventsFromArgs } from "./extract";
1
+ import ts from "typescript";
2
+ import { resolveNodeValue } from "./resolver";
3
3
  import { resolveFunctionFromCall } from "./callResolver";
4
- import { getForwardedParamIndex } from "./flow";
5
-
6
- function getCallName(expr: Node): string {
7
- if (Node.isIdentifier(expr)) return expr.getText();
8
- if (Node.isPropertyAccessExpression(expr)) return expr.getName();
9
- return expr.getText();
10
- }
11
-
12
- function isTrackLike(name: string) {
13
- return /(track|trackFeature)/i.test(name);
14
- }
15
-
16
- function pickArg(args: Node[]) {
17
- return (
18
- args.find(Node.isStringLiteral) ??
19
- args.find(Node.isTemplateExpression) ??
20
- args.find(Node.isObjectLiteralExpression) ??
21
- args[0]
22
- );
23
- }
24
4
 
25
5
  export function scanSource(
26
- source: SourceFile,
27
- checker: ts.TypeChecker
6
+ source: ts.SourceFile,
7
+ checker: ts.TypeChecker,
8
+ visited = new Set<string>()
28
9
  ) {
29
10
  const events = new Set<string>();
30
11
 
31
- // CALLS
32
- for (const call of source.getDescendantsOfKind(SyntaxKind.CallExpression)) {
33
- const expr = call.getExpression();
34
- const name = getCallName(expr);
12
+ if (visited.has(source.fileName)) return events;
13
+ visited.add(source.fileName);
14
+
15
+ function visit(node: ts.Node) {
16
+ if (ts.isCallExpression(node)) {
17
+ const name = getCallName(node.expression);
35
18
 
36
- if (!isTrackLike(name)) continue;
19
+ if (/(track|event|capture|send)/i.test(name)) {
20
+ let target = node.arguments[0];
37
21
 
38
- const args = call.getArguments();
39
- if (!args.length) continue;
22
+ const fn = resolveFunctionFromCall(node.expression, checker);
40
23
 
41
- let target = pickArg(args);
24
+ // 🔥 cross-file
25
+ if (fn) {
26
+ const sf = fn.getSourceFile();
42
27
 
43
- const fn = resolveFunctionFromCall(expr);
28
+ if (!visited.has(sf.fileName)) {
29
+ scanSource(sf, checker, visited).forEach(e => events.add(e));
30
+ }
31
+ }
44
32
 
45
- if (fn) {
46
- const index = getForwardedParamIndex(fn);
47
- if (index !== null && args[index]) {
48
- target = args[index];
33
+ if (target) {
34
+ const res = resolveNodeValue(target, checker);
35
+
36
+ res?.values.forEach(v => {
37
+ if (v && v.length > 1 && !v.includes(" ")) {
38
+ events.add(v);
39
+ }
40
+ });
41
+ }
49
42
  }
50
43
  }
51
44
 
52
- extractEventsFromArgs([target], checker).forEach(({ value }) => {
53
- if (isValid(value)) events.add(value);
54
- });
45
+ ts.forEachChild(node, visit);
55
46
  }
56
47
 
57
- // JSX
58
- const jsx = [
59
- ...source.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
60
- ...source.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
61
- ];
62
-
63
- for (const el of jsx) {
64
- for (const attr of el.getAttributes()) {
65
- if (!Node.isJsxAttribute(attr)) continue;
48
+ visit(source);
66
49
 
67
- const name = attr.getNameNode().getText();
68
- if (!/(event|name|action)/i.test(name)) continue;
69
-
70
- const init = attr.getInitializer();
71
- if (!init) continue;
72
-
73
- const expr = Node.isJsxExpression(init)
74
- ? init.getExpression()
75
- : init;
50
+ return events;
51
+ }
76
52
 
77
- if (!expr) continue;
53
+ function getCallName(expr: ts.Expression): string {
54
+ if (ts.isIdentifier(expr)) return expr.text;
78
55
 
79
- extractEventsFromArgs([expr], checker).forEach(({ value }) => {
80
- if (isValid(value)) events.add(value);
81
- });
82
- }
56
+ if (ts.isPropertyAccessExpression(expr)) {
57
+ return expr.name.text;
83
58
  }
84
59
 
85
- return events;
86
- }
87
-
88
- function isValid(v: string) {
89
- return !!v && v.length > 1 && !v.includes(" ");
60
+ return "";
90
61
  }
package/src/types.ts CHANGED
@@ -23,3 +23,8 @@ export type EventraConfig = {
23
23
  exclude: string[];
24
24
  };
25
25
  };
26
+
27
+ export type ResolveResult = {
28
+ values: string[];
29
+ dynamic: boolean;
30
+ };
@@ -1,53 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractEventsFromArgs = extractEventsFromArgs;
4
- const ts_morph_1 = require("ts-morph");
5
- const resolver_1 = require("./resolver");
6
- function extractEventsFromArgs(args, checker) {
7
- return args.flatMap(arg => extract(arg, checker));
8
- }
9
- function extract(node, checker) {
10
- if (ts_morph_1.Node.isStringLiteral(node)) {
11
- return [{ value: node.getLiteralText(), dynamic: false }];
12
- }
13
- if (ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node)) {
14
- return [{ value: node.getLiteralText(), dynamic: false }];
15
- }
16
- if (ts_morph_1.Node.isTemplateExpression(node)) {
17
- let result = node.getHead().getLiteralText();
18
- for (const span of node.getTemplateSpans()) {
19
- result += "*";
20
- result += span.getLiteral().getLiteralText();
21
- }
22
- return [{ value: result, dynamic: true }];
23
- }
24
- if (ts_morph_1.Node.isObjectLiteralExpression(node)) {
25
- return extractObject(node, checker);
26
- }
27
- const resolved = (0, resolver_1.resolveNodeValue)(node, checker);
28
- if (resolved) {
29
- return resolved.values.map(v => ({
30
- value: v,
31
- dynamic: resolved.dynamic,
32
- }));
33
- }
34
- return [];
35
- }
36
- function extractObject(obj, checker) {
37
- const res = [];
38
- for (const prop of obj.getProperties()) {
39
- if (!ts_morph_1.Node.isPropertyAssignment(prop))
40
- continue;
41
- const name = prop.getName();
42
- const init = prop.getInitializer();
43
- if (!init)
44
- continue;
45
- if (["event", "name", "action", "type"].includes(name)) {
46
- res.push(...extract(init, checker));
47
- }
48
- if (ts_morph_1.Node.isObjectLiteralExpression(init)) {
49
- res.push(...extractObject(init, checker));
50
- }
51
- }
52
- return res;
53
- }
@@ -1,38 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getForwardedParamIndex = getForwardedParamIndex;
4
- const ts_morph_1 = require("ts-morph");
5
- function getBodyNodes(fn) {
6
- const body = fn.getBody();
7
- if (!body)
8
- return [];
9
- if (ts_morph_1.Node.isBlock(body))
10
- return body.getStatements();
11
- return [body];
12
- }
13
- function getForwardedParamIndex(fn) {
14
- if (!ts_morph_1.Node.isFunctionDeclaration(fn) &&
15
- !ts_morph_1.Node.isArrowFunction(fn) &&
16
- !ts_morph_1.Node.isFunctionExpression(fn))
17
- return null;
18
- const f = fn;
19
- const params = f.getParameters();
20
- for (const node of getBodyNodes(f)) {
21
- const calls = node.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
22
- for (const call of calls) {
23
- const name = call.getExpression().getText();
24
- if (!/(track|event|log|analytics|capture|send)/i.test(name))
25
- continue;
26
- const args = call.getArguments();
27
- for (let i = 0; i < args.length; i++) {
28
- const arg = args[i];
29
- if (ts_morph_1.Node.isIdentifier(arg)) {
30
- const idx = params.findIndex(p => p.getName() === arg.getText());
31
- if (idx !== -1)
32
- return idx;
33
- }
34
- }
35
- }
36
- }
37
- return null;
38
- }
@@ -1,74 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ProjectGraph = void 0;
7
- const ts_morph_1 = require("ts-morph");
8
- const path_1 = __importDefault(require("path"));
9
- const fs_1 = __importDefault(require("fs"));
10
- function normalize(file) {
11
- return path_1.default.resolve(file);
12
- }
13
- function findTsConfig(start) {
14
- let dir = path_1.default.dirname(start);
15
- while (true) {
16
- const file = path_1.default.join(dir, "tsconfig.json");
17
- if (fs_1.default.existsSync(file))
18
- return file;
19
- const parent = path_1.default.dirname(dir);
20
- if (parent === dir)
21
- break;
22
- dir = parent;
23
- }
24
- return null;
25
- }
26
- function getScriptKind(file) {
27
- if (file.endsWith(".tsx"))
28
- return ts_morph_1.ts.ScriptKind.TSX;
29
- if (file.endsWith(".jsx"))
30
- return ts_morph_1.ts.ScriptKind.JSX;
31
- if (file.endsWith(".js"))
32
- return ts_morph_1.ts.ScriptKind.JS;
33
- if (file.endsWith(".vue"))
34
- return ts_morph_1.ts.ScriptKind.TSX;
35
- return ts_morph_1.ts.ScriptKind.TS;
36
- }
37
- class ProjectGraph {
38
- project;
39
- constructor(entry) {
40
- const tsconfig = findTsConfig(entry);
41
- this.project = tsconfig
42
- ? new ts_morph_1.Project({
43
- tsConfigFilePath: tsconfig,
44
- skipAddingFilesFromTsConfig: false,
45
- })
46
- : new ts_morph_1.Project({
47
- useInMemoryFileSystem: true,
48
- compilerOptions: {
49
- allowJs: true,
50
- jsx: ts_morph_1.ts.JsxEmit.Preserve,
51
- moduleResolution: ts_morph_1.ts.ModuleResolutionKind.NodeJs,
52
- target: ts_morph_1.ts.ScriptTarget.ESNext,
53
- },
54
- });
55
- }
56
- getSource(file, content) {
57
- const filePath = normalize(file);
58
- let source = this.project.getSourceFile(filePath);
59
- if (!source) {
60
- source = this.project.createSourceFile(filePath, content, {
61
- overwrite: true,
62
- scriptKind: getScriptKind(filePath),
63
- });
64
- }
65
- else {
66
- source.replaceWithText(content);
67
- }
68
- return source;
69
- }
70
- getChecker() {
71
- return this.project.getTypeChecker().compilerObject;
72
- }
73
- }
74
- exports.ProjectGraph = ProjectGraph;
@@ -1,31 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveNodeValue = resolveNodeValue;
4
- const ts_morph_1 = require("ts-morph");
5
- function resolveNodeValue(node, checker) {
6
- if (ts_morph_1.Node.isIdentifier(node)) {
7
- const symbol = checker.getSymbolAtLocation(node.compilerNode);
8
- if (!symbol)
9
- return null;
10
- const decls = symbol.getDeclarations() ?? [];
11
- for (const d of decls) {
12
- if (ts_morph_1.ts.isVariableDeclaration(d) && d.initializer) {
13
- if (ts_morph_1.Node.isStringLiteral(d.initializer)) {
14
- return {
15
- values: [d.initializer.text],
16
- dynamic: false
17
- };
18
- }
19
- }
20
- if (ts_morph_1.ts.isParameter(d) && d.initializer) {
21
- if (ts_morph_1.Node.isStringLiteral(d.initializer)) {
22
- return {
23
- values: [d.initializer.text],
24
- dynamic: false
25
- };
26
- }
27
- }
28
- }
29
- }
30
- return null;
31
- }
@@ -1,74 +0,0 @@
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
- }
@@ -1,54 +0,0 @@
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
- }
@@ -1,75 +0,0 @@
1
- import { Project, SourceFile, ts } from "ts-morph";
2
- import path from "path";
3
- import fs from "fs";
4
-
5
- function normalize(file: string) {
6
- return path.resolve(file);
7
- }
8
-
9
- function findTsConfig(start: string): string | null {
10
- let dir = path.dirname(start);
11
-
12
- while (true) {
13
- const file = path.join(dir, "tsconfig.json");
14
- if (fs.existsSync(file)) return file;
15
-
16
- const parent = path.dirname(dir);
17
- if (parent === dir) break;
18
-
19
- dir = parent;
20
- }
21
-
22
- return null;
23
- }
24
-
25
- function getScriptKind(file: string): ts.ScriptKind {
26
- if (file.endsWith(".tsx")) return ts.ScriptKind.TSX;
27
- if (file.endsWith(".jsx")) return ts.ScriptKind.JSX;
28
- if (file.endsWith(".js")) return ts.ScriptKind.JS;
29
- if (file.endsWith(".vue")) return ts.ScriptKind.TSX;
30
- return ts.ScriptKind.TS;
31
- }
32
-
33
- export class ProjectGraph {
34
- private project: Project;
35
-
36
- constructor(entry: string) {
37
- const tsconfig = findTsConfig(entry);
38
-
39
- this.project = tsconfig
40
- ? new Project({
41
- tsConfigFilePath: tsconfig,
42
- skipAddingFilesFromTsConfig: false,
43
- })
44
- : new Project({
45
- useInMemoryFileSystem: true,
46
- compilerOptions: {
47
- allowJs: true,
48
- jsx: ts.JsxEmit.Preserve,
49
- moduleResolution: ts.ModuleResolutionKind.NodeJs,
50
- target: ts.ScriptTarget.ESNext,
51
- },
52
- });
53
- }
54
-
55
- getSource(file: string, content: string): SourceFile {
56
- const filePath = normalize(file);
57
-
58
- let source = this.project.getSourceFile(filePath);
59
-
60
- if (!source) {
61
- source = this.project.createSourceFile(filePath, content, {
62
- overwrite: true,
63
- scriptKind: getScriptKind(filePath),
64
- });
65
- } else {
66
- source.replaceWithText(content);
67
- }
68
-
69
- return source;
70
- }
71
-
72
- getChecker() {
73
- return this.project.getTypeChecker().compilerObject;
74
- }
75
- }