@eventra_dev/eventra-cli 0.1.24 → 0.1.26

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.
@@ -10,7 +10,7 @@ const callResolver_1 = require("./callResolver");
10
10
  const exportResolver_1 = require("./exportResolver");
11
11
  const functionAnalyzer_1 = require("./functionAnalyzer");
12
12
  const boundary_1 = require("./boundary");
13
- const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
13
+ const trackingDetector_1 = require("./trackingDetector");
14
14
  function getCallName(expr) {
15
15
  if (typescript_1.default.isIdentifier(expr))
16
16
  return expr.text;
@@ -25,6 +25,16 @@ function getJsxAttrName(name) {
25
25
  return name.name.text;
26
26
  return null;
27
27
  }
28
+ // unwrap
29
+ function unwrap(expr) {
30
+ while (typescript_1.default.isAsExpression(expr) ||
31
+ typescript_1.default.isParenthesizedExpression(expr) ||
32
+ typescript_1.default.isTypeAssertionExpression(expr) ||
33
+ typescript_1.default.isNonNullExpression(expr)) {
34
+ expr = expr.expression;
35
+ }
36
+ return expr;
37
+ }
28
38
  function isValidEvent(v) {
29
39
  return (v &&
30
40
  v.length > 1 &&
@@ -32,70 +42,75 @@ function isValidEvent(v) {
32
42
  !v.includes(" ") &&
33
43
  /^[a-zA-Z0-9._:-]+$/.test(v));
34
44
  }
35
- function isPropUsedInTracking(fn, prop) {
45
+ function isPropUsedInTracking(fn, checker, prop) {
36
46
  let found = false;
37
47
  function visit(node) {
38
48
  if (typescript_1.default.isCallExpression(node)) {
39
- const name = getCallName(node.expression);
40
- if (!TRACKING_NAMES.has(name)) {
49
+ if (!(0, trackingDetector_1.isTrackingCall)(node, checker)) {
41
50
  typescript_1.default.forEachChild(node, visit);
42
51
  return;
43
52
  }
44
53
  for (const arg of node.arguments) {
45
- if ((typescript_1.default.isIdentifier(arg) && arg.text === prop) ||
46
- (typescript_1.default.isPropertyAccessExpression(arg) &&
47
- arg.name.text === prop)) {
54
+ const a = unwrap(arg);
55
+ if ((typescript_1.default.isIdentifier(a) && a.text === prop) ||
56
+ (typescript_1.default.isPropertyAccessExpression(a) &&
57
+ a.name.text === prop)) {
48
58
  found = true;
49
59
  }
50
60
  }
51
61
  }
52
62
  typescript_1.default.forEachChild(node, visit);
53
63
  }
54
- visit(fn);
64
+ if (fn.body) {
65
+ visit(fn.body);
66
+ }
55
67
  return found;
56
68
  }
57
69
  function scanSource(source, checker, config, visited = new Set(), depth = 0, inheritedParamMap) {
58
70
  const events = new Set();
59
71
  const detectedFunctionWrappers = new Set();
60
72
  const detectedComponentWrappers = new Map();
61
- if (depth > 5 || visited.has(source.fileName)) {
73
+ if (visited.has(source.fileName) ||
74
+ depth > 10 ||
75
+ visited.size > 200) {
62
76
  return { events, detectedFunctionWrappers, detectedComponentWrappers };
63
77
  }
64
78
  visited.add(source.fileName);
65
79
  function visit(node) {
66
80
  // CALL EXPRESSIONS
67
81
  if (typescript_1.default.isCallExpression(node)) {
68
- const name = getCallName(node.expression);
82
+ const expr = unwrap(node.expression);
83
+ const name = getCallName(expr);
69
84
  const isKnownWrapper = config.functionWrappers.some(w => w.name === name);
70
- let resolvedFn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
71
- // fallback: tracker.track(...)
72
- if (!resolvedFn && typescript_1.default.isPropertyAccessExpression(node.expression)) {
73
- const symbol = checker.getSymbolAtLocation(node.expression.name);
74
- if (symbol) {
75
- const decls = symbol.getDeclarations();
76
- if (decls?.length) {
77
- const d = decls[0];
78
- if (typescript_1.default.isMethodDeclaration(d) ||
79
- typescript_1.default.isFunctionDeclaration(d) ||
80
- typescript_1.default.isFunctionExpression(d) ||
81
- typescript_1.default.isArrowFunction(d)) {
82
- resolvedFn = d;
83
- }
84
- }
85
- }
86
- }
85
+ const isTrackingByType = (0, trackingDetector_1.isTrackingCall)(node, checker);
86
+ let resolvedFn = (0, callResolver_1.resolveFunctionFromCall)(expr, checker);
87
87
  const isAutoWrapper = resolvedFn && (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn) !== null;
88
- const isTracking = isKnownWrapper ||
89
- TRACKING_NAMES.has(name) ||
90
- name.startsWith("$") || // NUXT FIX
88
+ let isTracking = isKnownWrapper ||
89
+ isTrackingByType ||
90
+ name.startsWith("$") ||
91
91
  (isAutoWrapper &&
92
92
  resolvedFn &&
93
93
  !(0, boundary_1.isExternalFile)(resolvedFn.getSourceFile().fileName));
94
+ // fallback
95
+ if (!isTracking) {
96
+ if (typescript_1.default.isPropertyAccessExpression(expr) &&
97
+ expr.name.text === "track") {
98
+ isTracking = true;
99
+ }
100
+ else if (typescript_1.default.isIdentifier(expr) &&
101
+ expr.text === "track") {
102
+ isTracking = true;
103
+ }
104
+ }
94
105
  if (!isTracking) {
95
106
  typescript_1.default.forEachChild(node, visit);
96
107
  return;
97
108
  }
98
- if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
109
+ // detect wrapper
110
+ if (!isKnownWrapper &&
111
+ isAutoWrapper &&
112
+ !isTrackingByType &&
113
+ !name.startsWith("track")) {
99
114
  detectedFunctionWrappers.add(name);
100
115
  }
101
116
  const paramMap = new Map(inheritedParamMap ?? []);
@@ -114,26 +129,22 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
114
129
  if (resolvedFn) {
115
130
  const idx = (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn);
116
131
  if (idx !== null && node.arguments[idx]) {
117
- const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
118
- if (res && res.values.length) {
119
- res.values.forEach(v => {
120
- if (isValidEvent(v))
121
- events.add(v);
122
- });
123
- }
132
+ const res = (0, resolver_1.resolveNodeValue)(unwrap(node.arguments[idx]), checker, paramMap);
133
+ res.values.forEach(v => {
134
+ if (isValidEvent(v))
135
+ events.add(v);
136
+ });
124
137
  handled = true;
125
138
  }
126
139
  }
127
140
  // FALLBACK
128
141
  if (!handled) {
129
142
  for (const arg of node.arguments) {
130
- const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
131
- if (res && res.values.length) {
132
- res.values.forEach(v => {
133
- if (isValidEvent(v))
134
- events.add(v);
135
- });
136
- }
143
+ const res = (0, resolver_1.resolveNodeValue)(unwrap(arg), checker, paramMap);
144
+ res.values.forEach(v => {
145
+ if (isValidEvent(v))
146
+ events.add(v);
147
+ });
137
148
  }
138
149
  }
139
150
  // CROSS FILE
@@ -183,7 +194,7 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
183
194
  continue;
184
195
  for (const el of param.name.elements) {
185
196
  const prop = el.name.getText();
186
- const isEvent = isPropUsedInTracking(fn, prop) ||
197
+ const isEvent = isPropUsedInTracking(fn, checker, prop) ||
187
198
  config.wrappers.some(w => w.name === tagName && w.prop === prop);
188
199
  if (!isEvent)
189
200
  continue;
@@ -0,0 +1,68 @@
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.isTrackingCall = isTrackingCall;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ function isStringLike(type) {
9
+ return ((type.flags & typescript_1.default.TypeFlags.StringLike) !== 0 ||
10
+ (type.flags & typescript_1.default.TypeFlags.StringLiteral) !== 0);
11
+ }
12
+ function hasTrackLikeSignature(type, checker) {
13
+ const sigs = type.getCallSignatures();
14
+ for (const sig of sigs) {
15
+ const params = sig.getParameters();
16
+ if (!params.length)
17
+ continue;
18
+ const t = checker.getTypeOfSymbolAtLocation(params[0], params[0].valueDeclaration);
19
+ if (isStringLike(t)) {
20
+ return true;
21
+ }
22
+ }
23
+ return false;
24
+ }
25
+ function isEventraLikeType(type, checker) {
26
+ const props = type.getProperties();
27
+ for (const prop of props) {
28
+ const decl = prop.valueDeclaration ?? prop.declarations?.[0];
29
+ if (!decl)
30
+ continue;
31
+ const t = checker.getTypeOfSymbolAtLocation(prop, decl);
32
+ if (hasTrackLikeSignature(t, checker)) {
33
+ return true;
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+ function isTrackingCall(node, checker) {
39
+ const expr = node.expression;
40
+ // fn(...)
41
+ if (typescript_1.default.isIdentifier(expr)) {
42
+ const type = checker.getTypeAtLocation(expr);
43
+ if (hasTrackLikeSignature(type, checker)) {
44
+ return true;
45
+ }
46
+ }
47
+ // obj.method(...)
48
+ if (typescript_1.default.isPropertyAccessExpression(expr)) {
49
+ const objType = checker.getTypeAtLocation(expr.expression);
50
+ // объект похож на tracker
51
+ if (isEventraLikeType(objType, checker)) {
52
+ return true;
53
+ }
54
+ // fallback
55
+ const methodType = checker.getTypeAtLocation(expr.name);
56
+ if (hasTrackLikeSignature(methodType, checker)) {
57
+ return true;
58
+ }
59
+ }
60
+ // obj["track"](...)
61
+ if (typescript_1.default.isElementAccessExpression(expr)) {
62
+ const objType = checker.getTypeAtLocation(expr.expression);
63
+ if (isEventraLikeType(objType, checker)) {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getVirtualFile = getVirtualFile;
4
+ function getVirtualFile(file) {
5
+ return /\.(vue|svelte|astro|html)$/i.test(file)
6
+ ? file + ".tsx"
7
+ : file;
8
+ }
@@ -1,20 +1,11 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.processFile = processFile;
7
- const path_1 = __importDefault(require("path"));
8
4
  const universal_1 = require("./universal");
9
5
  function processFile(file, content) {
10
6
  const { code, deps } = (0, universal_1.parseUniversal)(content, file);
11
- const ext = path_1.default.extname(file);
12
- const virtualFile = /\.(vue|svelte|astro|html)$/i.test(file)
13
- ? file.replace(ext, "") + ".tsx"
14
- : file;
15
7
  return {
16
8
  content: code || "",
17
- virtualFile,
18
9
  deps: [...new Set(deps.filter(Boolean))],
19
10
  };
20
11
  }
@@ -15,7 +15,7 @@ function stripComments(input) {
15
15
  .replace(/\/\/.*$/gm, "");
16
16
  }
17
17
  /**
18
- * Extract JS expressions safely
18
+ * Extract JS expressions
19
19
  */
20
20
  function extractJS(input) {
21
21
  const result = [];
@@ -25,7 +25,6 @@ function extractJS(input) {
25
25
  let escape = false;
26
26
  while (i < len) {
27
27
  const ch = input[i];
28
- // STRING
29
28
  if (inString) {
30
29
  if (escape)
31
30
  escape = false;
@@ -41,95 +40,31 @@ function extractJS(input) {
41
40
  i++;
42
41
  continue;
43
42
  }
44
- // { ... }
45
43
  if (ch === "{") {
46
44
  let depth = 1;
47
45
  let j = i + 1;
48
- let innerStr = null;
49
- let innerEsc = false;
50
46
  while (j < len && depth > 0) {
51
- const c = input[j];
52
- if (innerStr) {
53
- if (innerEsc)
54
- innerEsc = false;
55
- else if (c === "\\")
56
- innerEsc = true;
57
- else if (c === innerStr)
58
- innerStr = null;
59
- j++;
60
- continue;
61
- }
62
- if (c === '"' || c === "'" || c === "`") {
63
- innerStr = c;
64
- j++;
65
- continue;
66
- }
67
- if (c === "{")
47
+ if (input[j] === "{")
68
48
  depth++;
69
- else if (c === "}")
49
+ else if (input[j] === "}")
70
50
  depth--;
71
51
  j++;
72
52
  }
73
53
  if (depth === 0) {
74
54
  const expr = input.slice(i + 1, j - 1).trim();
75
- if (expr &&
76
- (expr.includes("(") ||
77
- expr.includes("=") ||
78
- expr.includes(".") ||
79
- expr.includes("track"))) {
55
+ if (expr && /[a-zA-Z0-9_$]\s*\(/.test(expr)) {
80
56
  result.push(expr);
81
57
  }
82
58
  i = j;
83
59
  continue;
84
60
  }
85
61
  }
86
- // attr="..."
87
- if (ch === "=" && (input[i + 1] === '"' || input[i + 1] === "'")) {
88
- const quote = input[i + 1];
89
- let j = i + 2;
90
- let val = "";
91
- while (j < len) {
92
- const c = input[j];
93
- if (c === "\\" && input[j + 1]) {
94
- val += c + input[j + 1];
95
- j += 2;
96
- continue;
97
- }
98
- if (c === quote)
99
- break;
100
- val += c;
101
- j++;
102
- }
103
- if (val.trim() &&
104
- (val.includes("(") ||
105
- val.includes("=>") ||
106
- val.includes("track"))) {
107
- result.push(val.trim());
108
- }
109
- i = j + 1;
110
- continue;
111
- }
112
62
  i++;
113
63
  }
114
- // Vue / Svelte / JSX handlers
115
- const extra = [
116
- /@[\w-]+\s*=\s*["']([\s\S]*?)["']/g,
117
- /v-on:[\w-]+\s*=\s*["']([\s\S]*?)["']/g,
118
- /on:[\w-]+\s*=\s*{([\s\S]*?)}/g,
119
- /on\w+\s*=\s*{([\s\S]*?)}/g,
120
- ];
121
- for (const r of extra) {
122
- const m = input.matchAll(r);
123
- for (const x of m) {
124
- const v = x[1]?.trim();
125
- if (v)
126
- result.push(v);
127
- }
128
- }
129
64
  return result;
130
65
  }
131
66
  /**
132
- * Extract loose JS but less noisy
67
+ * Loose JS extractor
133
68
  */
134
69
  function extractLooseJS(input) {
135
70
  const result = [];
@@ -140,28 +75,31 @@ function extractLooseJS(input) {
140
75
  continue;
141
76
  if (l.startsWith("<"))
142
77
  continue;
143
- // strict filter
144
- if (l.includes("track") ||
145
- l.includes(".track(") ||
146
- l.includes("=>") ||
147
- l.startsWith("function") ||
148
- l.includes("(")) {
78
+ if (/[a-zA-Z0-9_$]\s*\(/.test(l)) {
149
79
  result.push(l);
150
80
  }
151
81
  }
152
82
  return result;
153
83
  }
154
84
  /**
155
- * Universal parser
85
+ * UNIVERSAL PARSER
86
+ * Support:
87
+ * - React / JSX
88
+ * - Vue
89
+ * - Svelte
90
+ * - Astro
91
+ * - HTML + JS
92
+ * - SSR
156
93
  */
157
94
  function parseUniversal(content, file) {
158
95
  const parts = [];
159
96
  const deps = [];
160
97
  const clean = stripComments(content);
161
- // Astro frontmatter
98
+ // ASTRO FRONTMATTER
162
99
  const fm = clean.match(/^---([\s\S]*?)---/);
163
- if (fm?.[1])
100
+ if (fm?.[1]) {
164
101
  parts.push(fm[1]);
102
+ }
165
103
  // <script src="">
166
104
  const scriptSrc = clean.matchAll(/<script\b[^>]*?\bsrc=["'](.+?)["'][^>]*>/g);
167
105
  for (const s of scriptSrc) {
@@ -171,29 +109,29 @@ function parseUniversal(content, file) {
171
109
  : path_1.default.resolve(path_1.default.dirname(file), src);
172
110
  deps.push(resolved);
173
111
  }
174
- // inline <script>
112
+ // INLINE <script>
175
113
  const scripts = clean.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/g);
176
114
  for (const m of scripts) {
177
115
  const attrs = m[1];
178
- // skip JSON / data
116
+ // skip JSON
179
117
  if (/type=["']application\/json/.test(attrs))
180
118
  continue;
181
119
  if (m[2].trim()) {
182
120
  parts.push(m[2]);
183
121
  }
184
122
  }
185
- // remove scripts
123
+ // REMOVE SCRIPTS → TEMPLATE PART
186
124
  const noScripts = clean.replace(/<script[\s\S]*?<\/script>/g, "");
187
- // loose JS
188
- const loose = extractLooseJS(noScripts);
189
- parts.push(...loose);
190
- // expressions
125
+ // LOOSE JS
126
+ parts.push(...extractLooseJS(noScripts));
127
+ // EXPRESSIONS
191
128
  const extracted = extractJS(clean);
192
129
  for (const e of extracted) {
193
130
  parts.push(e + ";");
194
131
  }
132
+ // RESULT
195
133
  return {
196
134
  code: [...new Set(parts)].join("\n"),
197
- deps,
135
+ deps: [...new Set(deps.filter(Boolean))],
198
136
  };
199
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,10 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import fg from "fast-glob";
3
3
  import fs from "fs/promises";
4
+ import path from "path";
4
5
 
5
6
  import { loadConfig, saveConfig } from "../utils/config";
6
7
  import { EventraEngine } from "../engine/engine";
7
8
  import { processFile } from "../utils/processFile";
9
+ import { getVirtualFile } from "../utils/getVirtualFile";
8
10
 
9
11
  export async function check({ fix = false }: { fix?: boolean }) {
10
12
  const config = await loadConfig();
@@ -18,36 +20,68 @@ export async function check({ fix = false }: { fix?: boolean }) {
18
20
 
19
21
  const files = await fg(config.sync.include, {
20
22
  ignore: config.sync.exclude,
23
+ absolute: true,
21
24
  });
22
25
 
23
26
  const engine = new EventraEngine(process.cwd());
24
27
 
28
+ const toScan = new Set<string>();
29
+
30
+ const cache = new Map<
31
+ string,
32
+ { content: string; deps: string[] }
33
+ >();
34
+
35
+ // COLLECT + CACHE
25
36
  for (const file of files) {
26
37
  try {
27
38
  const raw = await fs.readFile(file, "utf-8");
28
39
 
29
- const { content, virtualFile, deps } = processFile(file, raw);
40
+ if (raw.length > 2_000_000) continue;
41
+
42
+ const parsed = processFile(file, raw);
30
43
 
31
- engine.scanFile(virtualFile, content, config);
44
+ cache.set(file, parsed);
45
+ toScan.add(file);
32
46
 
33
- for (const dep of deps) {
34
- try {
35
- const depRaw = await fs.readFile(dep, "utf-8");
47
+ for (const dep of parsed.deps) {
48
+ toScan.add(path.resolve(dep));
49
+ }
50
+
51
+ } catch {}
52
+ }
36
53
 
37
- const {
38
- content: depContent,
39
- virtualFile: depVirtual,
40
- } = processFile(dep, depRaw);
54
+ // PRELOAD
55
+ for (const file of toScan) {
56
+ try {
57
+ let parsed = cache.get(file);
41
58
 
42
- engine.scanFile(depVirtual, depContent, config);
43
- } catch {}
59
+ if (!parsed) {
60
+ const raw = await fs.readFile(file, "utf-8");
61
+ parsed = processFile(file, raw);
62
+ cache.set(file, parsed);
44
63
  }
45
64
 
46
- } catch (err) {
47
- console.log(chalk.yellow(`Skipped: ${file}`));
48
- }
65
+ const virtual = getVirtualFile(file);
66
+ engine.preloadFile(virtual, parsed.content);
67
+
68
+ } catch {}
49
69
  }
50
70
 
71
+ // SCAN
72
+ for (const file of toScan) {
73
+ try {
74
+ const parsed = cache.get(file);
75
+ if (!parsed) continue;
76
+
77
+ const virtual = getVirtualFile(file);
78
+
79
+ engine.scanFile(virtual, parsed.content, config);
80
+
81
+ } catch {}
82
+ }
83
+
84
+ // DIFF
51
85
  const found = new Set(engine.getAllEvents());
52
86
  const known = new Set(config.events ?? []);
53
87
 
@@ -56,26 +90,12 @@ export async function check({ fix = false }: { fix?: boolean }) {
56
90
 
57
91
  // FIX MODE
58
92
  if (fix) {
59
- const next = new Set<string>();
60
-
61
- found.forEach(e => next.add(e));
62
-
63
- newEvents.forEach(e =>
64
- console.log(chalk.green(`+ ${e}`))
65
- );
66
-
67
- removed.forEach(e =>
68
- console.log(chalk.red(`- ${e}`))
69
- );
70
-
71
- config.events = [...next].sort();
72
-
93
+ config.events = [...found].sort();
73
94
  await saveConfig(config);
74
95
 
75
96
  console.log(
76
97
  chalk.green(`\nSynced (${config.events.length} events)`)
77
98
  );
78
-
79
99
  return;
80
100
  }
81
101