@eventra_dev/eventra-cli 0.1.30 → 0.1.32

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.
@@ -5,67 +5,146 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.parseUniversal = parseUniversal;
7
7
  const path_1 = __importDefault(require("path"));
8
- // Remove comments
8
+ /* ---------------------------------- */
9
+ /* COMMENT STRIP (SAFE) */
10
+ /* ---------------------------------- */
9
11
  function stripComments(input) {
10
12
  return input
11
13
  .replace(/<!--[\s\S]*?-->/g, "")
12
- .replace(/\/\*[\s\S]*?\*\//g, "")
13
- .replace(/\/\/.*$/gm, "");
14
+ .replace(/\/\*[\s\S]*?\*\//g, "");
14
15
  }
15
- // Basic filter
16
+ /* ---------------------------------- */
17
+ /* TRACK DETECTOR */
18
+ /* ---------------------------------- */
16
19
  function looksLikeTracking(code) {
17
- return /track/i.test(code);
20
+ return /\btrack\s*\(|\.track\s*\(/.test(code);
18
21
  }
19
- // HTML inline handlers (onclick="")
22
+ /* ---------------------------------- */
23
+ /* SAFE {} PARSER (JSX / TEMPLATES) */
24
+ /* ---------------------------------- */
25
+ function extractBracedExpressions(input) {
26
+ const result = [];
27
+ let depth = 0;
28
+ let start = -1;
29
+ let inString = null;
30
+ for (let i = 0; i < input.length; i++) {
31
+ const char = input[i];
32
+ // handle strings
33
+ if (inString) {
34
+ if (char === inString && input[i - 1] !== "\\") {
35
+ inString = null;
36
+ }
37
+ continue;
38
+ }
39
+ if (char === '"' || char === "'" || char === "`") {
40
+ inString = char;
41
+ continue;
42
+ }
43
+ if (char === "{") {
44
+ if (depth === 0)
45
+ start = i + 1;
46
+ depth++;
47
+ }
48
+ else if (char === "}") {
49
+ depth--;
50
+ if (depth === 0 && start !== -1) {
51
+ const expr = input.slice(start, i);
52
+ if (looksLikeTracking(expr)) {
53
+ result.push(expr);
54
+ }
55
+ start = -1;
56
+ }
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+ /* ---------------------------------- */
62
+ /* HTML HANDLERS */
63
+ /* ---------------------------------- */
20
64
  function extractHTMLHandlers(input) {
21
65
  const result = [];
22
- const matches = input.matchAll(/\bon\w+\s*=\s*["']([^"']+)["']/g);
23
- for (const m of matches) {
66
+ const regex = /\bon\w+\s*=\s*["']([^"']+)["']/g;
67
+ for (const m of input.matchAll(regex)) {
24
68
  if (looksLikeTracking(m[1])) {
25
69
  result.push(m[1]);
26
70
  }
27
71
  }
28
72
  return result;
29
73
  }
30
- // Vue handlers (@click / v-on)
74
+ /* ---------------------------------- */
75
+ /* VUE HANDLERS */
76
+ /* ---------------------------------- */
31
77
  function extractVueHandlers(input) {
32
78
  const result = [];
33
- const matches = input.matchAll(/(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g);
34
- for (const m of matches) {
79
+ const regex = /(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g;
80
+ for (const m of input.matchAll(regex)) {
35
81
  if (looksLikeTracking(m[1])) {
36
82
  result.push(m[1]);
37
83
  }
38
84
  }
39
85
  return result;
40
86
  }
41
- // Svelte handlers (on:click)
87
+ /* ---------------------------------- */
88
+ /* SVELTE HANDLERS */
89
+ /* ---------------------------------- */
42
90
  function extractSvelteHandlers(input) {
43
91
  const result = [];
44
- const matches = input.matchAll(/\bon:[\w]+\s*=\s*{([^}]+)}/g);
45
- for (const m of matches) {
46
- if (looksLikeTracking(m[1])) {
47
- result.push(m[1]);
48
- }
92
+ const regex = /\bon:[\w]+\s*=\s*{([\s\S]*?)}/g;
93
+ for (const m of input.matchAll(regex)) {
94
+ const expr = m[1];
95
+ const nested = extractBracedExpressions(`{${expr}}`);
96
+ result.push(...nested);
49
97
  }
50
98
  return result;
51
99
  }
52
- // JSX / template { ... }
53
- function extractJSExpressions(input) {
54
- const result = [];
55
- const matches = input.matchAll(/\{([^}]+)\}/g);
56
- for (const m of matches) {
57
- if (looksLikeTracking(m[1])) {
58
- result.push(m[1]);
100
+ /* ---------------------------------- */
101
+ /* SCRIPT EXTRACTION */
102
+ /* ---------------------------------- */
103
+ function extractScripts(input, file) {
104
+ const scripts = [];
105
+ const deps = [];
106
+ // <script src="">
107
+ const srcRegex = /<script\b[^>]*?\bsrc=["'](.+?)["'][^>]*>/gi;
108
+ for (const m of input.matchAll(srcRegex)) {
109
+ const src = m[1];
110
+ const resolved = src.startsWith("/")
111
+ ? path_1.default.resolve(process.cwd(), "." + src)
112
+ : path_1.default.resolve(path_1.default.dirname(file), src);
113
+ deps.push(resolved);
114
+ }
115
+ // inline <script>
116
+ const scriptRegex = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
117
+ for (const m of input.matchAll(scriptRegex)) {
118
+ if (m[1]?.trim()) {
119
+ scripts.push(m[1]);
59
120
  }
60
121
  }
61
- return result;
122
+ const noScripts = input.replace(scriptRegex, "");
123
+ return { scripts, deps, noScripts };
124
+ }
125
+ /* ---------------------------------- */
126
+ /* STATIC IMPORTS */
127
+ /* ---------------------------------- */
128
+ function extractStaticImports(input, file) {
129
+ const deps = [];
130
+ const regex = /import\s+(?:.+?\s+from\s+)?["'](.+?)["']/g;
131
+ for (const m of input.matchAll(regex)) {
132
+ const mod = m[1];
133
+ if (!mod.startsWith(".") && !mod.startsWith("/"))
134
+ continue;
135
+ const resolved = path_1.default.resolve(path_1.default.dirname(file), mod);
136
+ deps.push(resolved);
137
+ }
138
+ return deps;
62
139
  }
63
- // dynamic import()
140
+ /* ---------------------------------- */
141
+ /* DYNAMIC IMPORTS */
142
+ /* ---------------------------------- */
64
143
  function extractDynamicImports(input, file) {
65
144
  const deps = [];
66
145
  const calls = [];
67
- const matches = input.matchAll(/import\(\s*["'](.+?)["']\s*\)/g);
68
- for (const m of matches) {
146
+ const regex = /import\(\s*["'](.+?)["']\s*\)/g;
147
+ for (const m of input.matchAll(regex)) {
69
148
  const mod = m[1];
70
149
  const resolved = mod.startsWith("/")
71
150
  ? path_1.default.resolve(process.cwd(), "." + mod)
@@ -77,41 +156,30 @@ function extractDynamicImports(input, file) {
77
156
  }
78
157
  return { deps, calls };
79
158
  }
80
- // UNIVERSAL PARSER
159
+ /* ---------------------------------- */
160
+ /* MAIN */
161
+ /* ---------------------------------- */
81
162
  function parseUniversal(content, file) {
82
163
  const parts = [];
83
164
  const deps = [];
84
165
  const clean = stripComments(content);
85
- // ASTRO FRONTMATTER
86
- const fm = clean.match(/^---([\s\S]*?)---/);
166
+ /* ---------- ASTRO FRONTMATTER ---------- */
167
+ const fm = clean.match(/^\s*---([\s\S]*?)---/);
87
168
  if (fm?.[1]) {
88
169
  parts.push(fm[1]);
89
170
  }
90
- // <script src="">
91
- const scriptSrc = clean.matchAll(/<script\b[^>]*?\bsrc=["'](.+?)["'][^>]*>/g);
92
- for (const s of scriptSrc) {
93
- const src = s[1];
94
- const resolved = src.startsWith("/")
95
- ? path_1.default.resolve(process.cwd(), "." + src)
96
- : path_1.default.resolve(path_1.default.dirname(file), src);
97
- deps.push(resolved);
98
- }
99
- // INLINE <script>
100
- const scripts = clean.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/g);
101
- for (const m of scripts) {
102
- if (m[2].trim()) {
103
- parts.push(m[2]);
104
- }
105
- }
106
- // TEMPLATE PART
107
- const noScripts = clean.replace(/<script[\s\S]*?<\/script>/g, "");
108
- // handlers
171
+ /* ---------- SCRIPTS ---------- */
172
+ const { scripts, deps: scriptDeps, noScripts } = extractScripts(clean, file);
173
+ parts.push(...scripts);
174
+ deps.push(...scriptDeps);
175
+ /* ---------- HANDLERS ---------- */
109
176
  parts.push(...extractHTMLHandlers(noScripts));
110
177
  parts.push(...extractVueHandlers(noScripts));
111
178
  parts.push(...extractSvelteHandlers(noScripts));
112
- // JSX / Astro / Vue expressions
113
- parts.push(...extractJSExpressions(clean));
114
- // dynamic imports
179
+ /* ---------- JSX / TEMPLATE ---------- */
180
+ parts.push(...extractBracedExpressions(clean));
181
+ /* ---------- IMPORTS ---------- */
182
+ deps.push(...extractStaticImports(clean, file));
115
183
  const dyn = extractDynamicImports(clean, file);
116
184
  deps.push(...dyn.deps);
117
185
  parts.push(...dyn.calls);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  import ts from "typescript";
2
2
 
3
+ // HELPERS
3
4
  function isFunctionLike(
4
5
  node: ts.Node
5
6
  ): node is ts.FunctionLikeDeclaration {
@@ -12,40 +13,39 @@ function isFunctionLike(
12
13
  );
13
14
  }
14
15
 
16
+ // symbol → function
15
17
  function resolveFromSymbol(
16
18
  symbol: ts.Symbol,
17
19
  checker: ts.TypeChecker,
18
20
  visited = new Set<ts.Symbol>(),
19
21
  depth = 0
20
22
  ): ts.FunctionLikeDeclaration | null {
23
+ if (!symbol) return null;
21
24
  if (visited.has(symbol)) return null;
22
- if (depth > 5) return null;
25
+ if (depth > 10) return null;
23
26
 
24
27
  visited.add(symbol);
25
28
 
26
- // unwrap alias
29
+ // Alias unwrap (import/export)
27
30
  if (symbol.flags & ts.SymbolFlags.Alias) {
28
- const aliased = checker.getAliasedSymbol(symbol);
29
- return resolveFromSymbol(aliased, checker, visited, depth + 1);
30
- }
31
-
32
- // re-export support
33
- if ((symbol as any).exports) {
34
- for (const exp of (symbol as any).exports.values()) {
35
- const resolved = resolveFromSymbol(exp, checker, visited, depth + 1);
36
- if (resolved) return resolved;
37
- }
31
+ try {
32
+ const aliased = checker.getAliasedSymbol(symbol);
33
+ return resolveFromSymbol(aliased, checker, visited, depth + 1);
34
+ } catch {}
38
35
  }
39
36
 
37
+ // Declarations
40
38
  const decls = symbol.getDeclarations() ?? [];
41
39
 
42
40
  for (const decl of decls) {
43
- // direct function
41
+ /**
42
+ * direct function
43
+ */
44
44
  if (isFunctionLike(decl)) {
45
45
  return decl;
46
46
  }
47
47
 
48
- // VARIABLE → FUNCTION
48
+ // VARIABLE → FUNCTION const fn = () => {}
49
49
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
50
50
  if (isFunctionLike(decl.initializer)) {
51
51
  return decl.initializer;
@@ -63,7 +63,12 @@ function resolveFromSymbol(
63
63
  }
64
64
  }
65
65
 
66
- // MethodSignature (interface / type)
66
+ // CLASS METHOD
67
+ if (ts.isMethodDeclaration(decl)) {
68
+ return decl;
69
+ }
70
+
71
+ // interface / type method
67
72
  if (ts.isMethodSignature(decl)) {
68
73
  const parent = decl.parent;
69
74
 
@@ -71,14 +76,10 @@ function resolveFromSymbol(
71
76
  ts.isInterfaceDeclaration(parent) ||
72
77
  ts.isTypeLiteralNode(parent)
73
78
  ) {
74
- const name = decl.name.getText();
75
-
76
79
  const type = checker.getTypeAtLocation(parent);
77
80
  const props = type.getProperties();
78
81
 
79
82
  for (const prop of props) {
80
- if (prop.name !== name) continue;
81
-
82
83
  const resolved = resolveFromSymbol(
83
84
  prop,
84
85
  checker,
@@ -91,10 +92,24 @@ function resolveFromSymbol(
91
92
  }
92
93
 
93
94
  // fallback recursion
94
- const s = checker.getSymbolAtLocation(decl);
95
- if (s && s !== symbol) {
95
+ const inner = checker.getSymbolAtLocation(decl);
96
+ if (inner && inner !== symbol) {
96
97
  const resolved = resolveFromSymbol(
97
- s,
98
+ inner,
99
+ checker,
100
+ visited,
101
+ depth + 1
102
+ );
103
+ if (resolved) return resolved;
104
+ }
105
+ }
106
+
107
+ // Exports (re-export)
108
+ const exports = (symbol as any).exports;
109
+ if (exports) {
110
+ for (const exp of exports.values()) {
111
+ const resolved = resolveFromSymbol(
112
+ exp,
98
113
  checker,
99
114
  visited,
100
115
  depth + 1
@@ -106,17 +121,19 @@ function resolveFromSymbol(
106
121
  return null;
107
122
  }
108
123
 
124
+ // MAIN FUNCTION
109
125
  export function resolveFunctionFromCall(
110
126
  expr: ts.Expression,
111
127
  checker: ts.TypeChecker
112
128
  ): ts.FunctionLikeDeclaration | null {
113
-
114
- // tracker.track
129
+ // obj.method()
115
130
  if (ts.isPropertyAccessExpression(expr)) {
116
131
  let symbol = checker.getSymbolAtLocation(expr.name);
117
132
 
118
133
  if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
119
- symbol = checker.getAliasedSymbol(symbol);
134
+ try {
135
+ symbol = checker.getAliasedSymbol(symbol);
136
+ } catch {}
120
137
  }
121
138
 
122
139
  if (symbol) {
@@ -125,7 +142,7 @@ export function resolveFunctionFromCall(
125
142
  }
126
143
  }
127
144
 
128
- // tracker["track"]
145
+ // obj["method"]()
129
146
  if (ts.isElementAccessExpression(expr)) {
130
147
  const symbol = checker.getSymbolAtLocation(expr.argumentExpression);
131
148
 
@@ -135,7 +152,7 @@ export function resolveFunctionFromCall(
135
152
  }
136
153
  }
137
154
 
138
- // normal call signatures
155
+ // Call signatures
139
156
  const type = checker.getTypeAtLocation(expr);
140
157
  const signatures = type.getCallSignatures();
141
158
 
@@ -157,7 +174,23 @@ export function resolveFunctionFromCall(
157
174
  }
158
175
  }
159
176
 
160
- // fallback
177
+ // Identifier fallback
178
+ if (ts.isIdentifier(expr)) {
179
+ let symbol = checker.getSymbolAtLocation(expr);
180
+
181
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
182
+ try {
183
+ symbol = checker.getAliasedSymbol(symbol);
184
+ } catch {}
185
+ }
186
+
187
+ if (symbol) {
188
+ const resolved = resolveFromSymbol(symbol, checker);
189
+ if (resolved) return resolved;
190
+ }
191
+ }
192
+
193
+ // LAST fallback
161
194
  const symbol = checker.getSymbolAtLocation(expr);
162
195
  if (symbol) {
163
196
  return resolveFromSymbol(symbol, checker);
@@ -2,16 +2,16 @@ import path from "path";
2
2
  import fs from "fs";
3
3
 
4
4
  import { TSService } from "./languageService";
5
- import { ScanResult, scanSource } from "./scanner";
5
+ import { scanSource } from "./scanner";
6
6
  import { DependencyGraph } from "./graph";
7
7
  import { hash } from "./hash";
8
- import { EventraConfig } from "../types";
8
+ import {EventraConfig, ScanResult} from "../types";
9
9
  import { isExternalFile } from "./boundary";
10
10
 
11
11
  export class EventraEngine {
12
12
  private ts: TSService;
13
13
  private graph: DependencyGraph;
14
-
14
+ private wrapperCache = new Map<string, string | null>();
15
15
  private fileResults = new Map<string, ScanResult>();
16
16
  private fileHash = new Map<string, string>();
17
17
 
@@ -71,10 +71,17 @@ export class EventraEngine {
71
71
  return this.empty();
72
72
  }
73
73
 
74
- // обновляем graph
74
+ // update graph
75
75
  this.graph.update(file, source);
76
76
 
77
- const res = scanSource(source, checker, config);
77
+ const res = scanSource(
78
+ source,
79
+ checker,
80
+ config,
81
+ new Set(),
82
+ undefined,
83
+ this.wrapperCache
84
+ );
78
85
  this.fileResults.set(file, res);
79
86
 
80
87
  return res;
@@ -86,6 +93,7 @@ export class EventraEngine {
86
93
  content: string,
87
94
  config: EventraConfig
88
95
  ) {
96
+ this.wrapperCache.clear();
89
97
  file = this.normalize(file);
90
98
 
91
99
  const res = this.scanFile(file, content, config);
@@ -124,7 +132,14 @@ export class EventraEngine {
124
132
 
125
133
  this.graph.update(abs, source);
126
134
 
127
- const r = scanSource(source, checker, config);
135
+ const r = scanSource(
136
+ source,
137
+ checker,
138
+ config,
139
+ new Set(),
140
+ undefined,
141
+ this.wrapperCache
142
+ );
128
143
  this.fileResults.set(abs, r);
129
144
  }
130
145
 
@@ -144,6 +159,7 @@ export class EventraEngine {
144
159
 
145
160
  // REMOVE FILE
146
161
  removeFile(file: string, config?: EventraConfig) {
162
+ this.wrapperCache.clear();
147
163
  file = this.normalize(file);
148
164
 
149
165
  const dependents = this.graph.getAllDependentsDeep(file);
@@ -191,7 +207,10 @@ export class EventraEngine {
191
207
  wrappers: [],
192
208
  functionWrappers: [],
193
209
  sync: { include: [], exclude: [] },
194
- }
210
+ },
211
+ new Set(),
212
+ undefined,
213
+ this.wrapperCache
195
214
  );
196
215
 
197
216
  this.fileResults.set(abs, r);
@@ -7,9 +7,10 @@ export class TSService {
7
7
  private fileNames = new Set<string>();
8
8
  private service: ts.LanguageService;
9
9
 
10
- private compilerOptions: ts.CompilerOptions;
11
- private baseUrl: string;
12
- private paths: Record<string, string[]>;
10
+ private readonly compilerOptions: ts.CompilerOptions;
11
+ private readonly baseUrl: string;
12
+ private readonly paths: Record<string, string[]>;
13
+
13
14
  private usage = new Map<string, number>();
14
15
  private tick = 0;
15
16
  private MAX_FILES = 2000;
@@ -23,6 +24,7 @@ export class TSService {
23
24
 
24
25
  let compilerOptions: ts.CompilerOptions = {
25
26
  allowJs: true,
27
+ allowNonTsExtensions: true,
26
28
  jsx: ts.JsxEmit.React,
27
29
  jsxImportSource: "react",
28
30
  moduleResolution: ts.ModuleResolutionKind.Node16,
@@ -60,7 +62,17 @@ export class TSService {
60
62
  this.paths = paths;
61
63
 
62
64
  const host: ts.LanguageServiceHost = {
63
- getScriptFileNames: () => Array.from(this.fileNames),
65
+ getScriptFileNames: () => {
66
+ if (this.fileNames.size === 0) {
67
+ return ts.sys.readDirectory(
68
+ this.root,
69
+ [".ts", ".tsx", ".js", ".jsx"],
70
+ undefined,
71
+ undefined
72
+ );
73
+ }
74
+ return Array.from(this.fileNames);
75
+ },
64
76
 
65
77
  getScriptVersion: (file) => {
66
78
  file = this.normalize(file);
@@ -69,13 +81,29 @@ export class TSService {
69
81
 
70
82
  getScriptSnapshot: (file) => {
71
83
  file = this.normalize(file);
72
-
73
84
  const f = this.files.get(file);
74
85
  if (f) {
75
86
  this.touch(file);
76
87
  return ts.ScriptSnapshot.fromString(f.content);
77
88
  }
89
+ if (file.endsWith(".tsx")) {
90
+ const original = file.replace(/\.tsx$/, "");
91
+
92
+ if (fs.existsSync(original)) {
93
+ const content = fs.readFileSync(original, "utf-8");
94
+
95
+ this.files.set(file, {
96
+ content,
97
+ version: 1,
98
+ });
99
+
100
+ this.fileNames.add(file);
101
+ this.touch(file);
102
+ this.evictIfNeeded();
78
103
 
104
+ return ts.ScriptSnapshot.fromString(content);
105
+ }
106
+ }
79
107
  try {
80
108
  if (fs.existsSync(file)) {
81
109
  const content = fs.readFileSync(file, "utf-8");
@@ -93,16 +121,22 @@ export class TSService {
93
121
  }
94
122
  } catch {}
95
123
 
96
- // fallback
97
124
  return ts.ScriptSnapshot.fromString("");
98
125
  },
99
-
100
126
  getCurrentDirectory: () => this.root,
101
127
  getCompilationSettings: () => this.compilerOptions,
102
128
  getDefaultLibFileName: (opts) =>
103
129
  ts.getDefaultLibFilePath(opts),
104
- fileExists: ts.sys.fileExists,
105
- readFile: ts.sys.readFile,
130
+ fileExists: (file) => {
131
+ file = this.normalize(file);
132
+ return this.files.has(file) || ts.sys.fileExists(file);
133
+ },
134
+
135
+ readFile: (file) => {
136
+ file = this.normalize(file);
137
+ return this.files.get(file)?.content ?? ts.sys.readFile(file);
138
+ },
139
+
106
140
  readDirectory: ts.sys.readDirectory,
107
141
  };
108
142
 
@@ -122,7 +156,7 @@ export class TSService {
122
156
  private evictIfNeeded() {
123
157
  if (this.fileNames.size <= this.MAX_FILES) return;
124
158
 
125
- let oldestFile: string | null = null;
159
+ let oldest: string | null = null;
126
160
  let oldestTick = Infinity;
127
161
 
128
162
  for (const f of this.fileNames) {
@@ -130,20 +164,20 @@ export class TSService {
130
164
 
131
165
  if (t < oldestTick) {
132
166
  oldestTick = t;
133
- oldestFile = f;
167
+ oldest = f;
134
168
  }
135
169
  }
136
170
 
137
- if (oldestFile) {
138
- this.files.delete(oldestFile);
139
- this.fileNames.delete(oldestFile);
140
- this.usage.delete(oldestFile);
171
+ if (oldest) {
172
+ this.files.delete(oldest);
173
+ this.fileNames.delete(oldest);
174
+ this.usage.delete(oldest);
141
175
  }
142
176
  }
143
177
 
144
178
  private ensureProgram() {
145
179
  try {
146
- this.service.getProgram();
180
+ this.service.getProgram()?.getTypeChecker();
147
181
  } catch {}
148
182
  }
149
183