@eventra_dev/eventra-cli 0.1.6 → 0.1.8

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.
@@ -25,7 +25,7 @@ async function check({ fix = false }) {
25
25
  try {
26
26
  const raw = await promises_1.default.readFile(file, "utf-8");
27
27
  const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
28
- engine.scanFile(virtualFile, content);
28
+ engine.scanFile(virtualFile, content, config);
29
29
  }
30
30
  catch (err) {
31
31
  console.log(chalk_1.default.yellow(`Skipped: ${file}`));
@@ -23,11 +23,19 @@ async function sync() {
23
23
  return;
24
24
  }
25
25
  const engine = new engine_1.EventraEngine(process.cwd());
26
+ const detectedFn = new Set();
27
+ const detectedCmp = new Map();
26
28
  for (const file of files) {
27
29
  try {
28
30
  const raw = await promises_1.default.readFile(file, "utf-8");
29
31
  const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
30
- engine.scanFile(virtualFile, content);
32
+ const res = engine.scanFile(virtualFile, content, config);
33
+ for (const w of res.detectedFunctionWrappers) {
34
+ detectedFn.add(w);
35
+ }
36
+ for (const [k, v] of res.detectedComponentWrappers) {
37
+ detectedCmp.set(k, v);
38
+ }
31
39
  }
32
40
  catch (err) {
33
41
  console.log(chalk_1.default.yellow(`Skipped: ${file}`));
@@ -36,8 +44,34 @@ async function sync() {
36
44
  }
37
45
  }
38
46
  }
47
+ // EVENTS
39
48
  const events = engine.getAllEvents().sort();
40
- config.events = events;
49
+ const mergedEvents = new Set(config.events ?? []);
50
+ events.forEach(e => mergedEvents.add(e));
51
+ config.events = [...mergedEvents].sort();
52
+ // FUNCTION WRAPPERS
53
+ config.functionWrappers = mergeUnique(config.functionWrappers ?? [], [...detectedFn].map(name => ({ name })), w => w.name);
54
+ // COMPONENT WRAPPERS
55
+ config.wrappers = mergeUnique(config.wrappers ?? [], [...detectedCmp.entries()].map(([name, prop]) => ({
56
+ name,
57
+ prop,
58
+ })), w => `${w.name}:${w.prop}`);
41
59
  await (0, config_1.saveConfig)(config);
42
60
  console.log(chalk_1.default.green(`Found ${events.length} events`));
61
+ if (detectedFn.size) {
62
+ console.log(chalk_1.default.blue("\nDetected function wrappers:"));
63
+ detectedFn.forEach(w => console.log(chalk_1.default.gray(`• ${w}`)));
64
+ }
65
+ if (detectedCmp.size) {
66
+ console.log(chalk_1.default.blue("\nDetected component wrappers:"));
67
+ detectedCmp.forEach((prop, name) => console.log(chalk_1.default.gray(`• ${name} (${prop})`)));
68
+ }
69
+ }
70
+ // HELPERS
71
+ function mergeUnique(a, b, key) {
72
+ const map = new Map();
73
+ [...b, ...a].forEach(item => {
74
+ map.set(key(item), item);
75
+ });
76
+ return [...map.values()];
43
77
  }
@@ -24,7 +24,7 @@ async function watch() {
24
24
  try {
25
25
  const raw = await promises_1.default.readFile(file, "utf-8");
26
26
  const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
27
- engine.scanFile(virtualFile, content);
27
+ engine.scanFile(virtualFile, content, config);
28
28
  }
29
29
  catch {
30
30
  console.log(chalk_1.default.gray(`skip: ${file}`));
@@ -46,7 +46,7 @@ async function watch() {
46
46
  try {
47
47
  const raw = await promises_1.default.readFile(file, "utf-8");
48
48
  const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
49
- engine.updateFile(virtualFile, content);
49
+ engine.updateFile(virtualFile, content, config);
50
50
  }
51
51
  catch {
52
52
  console.log(chalk_1.default.gray(`skip: ${file}`));
@@ -0,0 +1,14 @@
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.isExternalFile = isExternalFile;
7
+ const path_1 = __importDefault(require("path"));
8
+ function isExternalFile(file) {
9
+ const normalized = path_1.default.resolve(file);
10
+ return (normalized.includes(`${path_1.default.sep}node_modules${path_1.default.sep}`) ||
11
+ normalized.includes(`${path_1.default.sep}dist${path_1.default.sep}`) ||
12
+ normalized.includes(`${path_1.default.sep}build${path_1.default.sep}`) ||
13
+ normalized.endsWith(".d.ts"));
14
+ }
@@ -5,18 +5,74 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveFunctionFromCall = resolveFunctionFromCall;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
+ function isFunctionLike(node) {
9
+ return (typescript_1.default.isFunctionDeclaration(node) ||
10
+ typescript_1.default.isMethodDeclaration(node) ||
11
+ typescript_1.default.isConstructorDeclaration(node) ||
12
+ typescript_1.default.isFunctionExpression(node) ||
13
+ typescript_1.default.isArrowFunction(node));
14
+ }
15
+ function resolveFromSymbol(symbol, checker, visited = new Set(), depth = 0) {
16
+ if (visited.has(symbol))
17
+ return null;
18
+ if (depth > 5)
19
+ return null;
20
+ visited.add(symbol);
21
+ // unwrap alias
22
+ if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
23
+ const aliased = checker.getAliasedSymbol(symbol);
24
+ return resolveFromSymbol(aliased, checker, visited, depth + 1);
25
+ }
26
+ const decls = symbol.getDeclarations() ?? [];
27
+ for (const decl of decls) {
28
+ if (isFunctionLike(decl)) {
29
+ return decl;
30
+ }
31
+ // MethodSignature
32
+ if (typescript_1.default.isMethodSignature(decl)) {
33
+ const parent = decl.parent;
34
+ if (typescript_1.default.isInterfaceDeclaration(parent) || typescript_1.default.isTypeLiteralNode(parent)) {
35
+ const name = decl.name.getText();
36
+ const type = checker.getTypeAtLocation(parent);
37
+ const props = type.getProperties();
38
+ for (const prop of props) {
39
+ if (prop.name !== name)
40
+ continue;
41
+ const resolved = resolveFromSymbol(prop, checker, visited, depth + 1);
42
+ if (resolved)
43
+ return resolved;
44
+ }
45
+ }
46
+ }
47
+ // SAFE fallback
48
+ const s = checker.getSymbolAtLocation(decl);
49
+ if (s && s !== symbol) {
50
+ const resolved = resolveFromSymbol(s, checker, visited, depth + 1);
51
+ if (resolved)
52
+ return resolved;
53
+ }
54
+ }
55
+ return null;
56
+ }
8
57
  function resolveFunctionFromCall(expr, checker) {
9
58
  const type = checker.getTypeAtLocation(expr);
10
59
  const signatures = type.getCallSignatures();
11
60
  for (const sig of signatures) {
12
61
  const decl = sig.getDeclaration();
13
- if (decl &&
14
- (typescript_1.default.isFunctionDeclaration(decl) ||
15
- typescript_1.default.isMethodDeclaration(decl) ||
16
- typescript_1.default.isFunctionExpression(decl) ||
17
- typescript_1.default.isArrowFunction(decl))) {
62
+ if (!decl)
63
+ continue;
64
+ if (isFunctionLike(decl)) {
18
65
  return decl;
19
66
  }
67
+ // interface method
68
+ if (typescript_1.default.isMethodSignature(decl)) {
69
+ const symbol = checker.getSymbolAtLocation(decl.name);
70
+ if (symbol) {
71
+ const resolved = resolveFromSymbol(symbol, checker);
72
+ if (resolved)
73
+ return resolved;
74
+ }
75
+ }
20
76
  }
21
77
  return null;
22
78
  }
@@ -5,58 +5,109 @@ const languageService_1 = require("./languageService");
5
5
  const scanner_1 = require("./scanner");
6
6
  const graph_1 = require("./graph");
7
7
  const hash_1 = require("./hash");
8
+ const boundary_1 = require("./boundary");
8
9
  class EventraEngine {
9
10
  ts;
10
11
  graph;
11
- fileEvents = new Map();
12
+ fileResults = new Map();
12
13
  fileHash = new Map();
13
14
  constructor(root) {
14
15
  this.ts = new languageService_1.TSService(root);
15
16
  const { baseUrl, paths } = this.ts.getModuleResolutionConfig();
16
17
  this.graph = new graph_1.DependencyGraph(baseUrl, paths);
17
18
  }
18
- scanFile(file, content) {
19
+ // SCAN FILE
20
+ scanFile(file, content, config) {
19
21
  const h = (0, hash_1.hash)(content);
20
- // cache
22
+ // skip unchanged
21
23
  if (this.fileHash.get(file) === h) {
22
- return this.fileEvents.get(file) ?? new Set();
24
+ return (this.fileResults.get(file) ?? {
25
+ events: new Set(),
26
+ detectedFunctionWrappers: new Set(),
27
+ detectedComponentWrappers: new Map(),
28
+ });
23
29
  }
24
30
  this.fileHash.set(file, h);
25
31
  this.ts.updateFile(file, content);
26
32
  const source = this.ts.getSourceFile(file);
27
33
  const checker = this.ts.getChecker();
28
- if (!source || !checker)
29
- return new Set();
30
- // обновляем graph
34
+ if (!source || !checker) {
35
+ return {
36
+ events: new Set(),
37
+ detectedFunctionWrappers: new Set(),
38
+ detectedComponentWrappers: new Map(),
39
+ };
40
+ }
41
+ // update dependency graph
31
42
  this.graph.update(file, source);
32
- const events = (0, scanner_1.scanSource)(source, checker);
33
- this.fileEvents.set(file, events);
34
- return events;
43
+ const res = (0, scanner_1.scanSource)(source, checker, config);
44
+ this.fileResults.set(file, res);
45
+ return res;
35
46
  }
36
- updateFile(file, content) {
37
- const events = this.scanFile(file, content);
38
- const dependents = this.graph.getDependents(file);
47
+ // UPDATE FILE (incremental)
48
+ updateFile(file, content, config) {
49
+ const res = this.scanFile(file, content, config);
50
+ const dependents = this.graph.getAllDependentsDeep(file);
39
51
  for (const dep of dependents) {
52
+ // skip external files
53
+ if ((0, boundary_1.isExternalFile)(dep))
54
+ continue;
40
55
  const source = this.ts.getSourceFile(dep);
41
56
  const checker = this.ts.getChecker();
42
57
  if (!source || !checker)
43
58
  continue;
44
- const ev = (0, scanner_1.scanSource)(source, checker);
45
- this.fileEvents.set(dep, ev);
59
+ const depContent = this.ts.getFileContent(dep);
60
+ if (!depContent)
61
+ continue;
62
+ const h = (0, hash_1.hash)(depContent);
63
+ // skip unchanged dependents
64
+ if (this.fileHash.get(dep) === h)
65
+ continue;
66
+ this.fileHash.set(dep, h);
67
+ this.graph.update(dep, source);
68
+ const r = (0, scanner_1.scanSource)(source, checker, config);
69
+ this.fileResults.set(dep, r);
46
70
  }
47
- return events;
71
+ return res;
48
72
  }
73
+ // GET ALL EVENTS
49
74
  getAllEvents() {
50
75
  const all = new Set();
51
- for (const set of this.fileEvents.values()) {
52
- set.forEach(e => all.add(e));
76
+ for (const r of this.fileResults.values()) {
77
+ r.events.forEach(e => all.add(e));
53
78
  }
54
79
  return [...all];
55
80
  }
56
- removeFile(file) {
57
- this.fileEvents.delete(file);
81
+ // REMOVE FILE
82
+ removeFile(file, config) {
83
+ this.ts.removeFile(file);
84
+ this.fileResults.delete(file);
58
85
  this.fileHash.delete(file);
59
86
  this.graph.remove(file);
87
+ const dependents = this.graph.getAllDependentsDeep(file);
88
+ for (const dep of dependents) {
89
+ if ((0, boundary_1.isExternalFile)(dep))
90
+ continue;
91
+ const source = this.ts.getSourceFile(dep);
92
+ const checker = this.ts.getChecker();
93
+ if (!source || !checker)
94
+ continue;
95
+ const content = this.ts.getFileContent(dep);
96
+ if (!content)
97
+ continue;
98
+ const h = (0, hash_1.hash)(content);
99
+ // skip unchanged
100
+ if (this.fileHash.get(dep) === h)
101
+ continue;
102
+ this.fileHash.set(dep, h);
103
+ const r = (0, scanner_1.scanSource)(source, checker, config ?? {
104
+ events: [],
105
+ wrappers: [],
106
+ functionWrappers: [],
107
+ sync: { include: [], exclude: [] },
108
+ });
109
+ this.fileResults.set(dep, r);
110
+ }
60
111
  }
61
112
  }
62
113
  exports.EventraEngine = EventraEngine;
@@ -12,6 +12,7 @@ class DependencyGraph {
12
12
  paths;
13
13
  reverse = new Map();
14
14
  exports = new Map();
15
+ fileCache = new Map();
15
16
  constructor(baseUrl, paths) {
16
17
  this.baseUrl = baseUrl;
17
18
  this.paths = paths;
@@ -19,37 +20,36 @@ class DependencyGraph {
19
20
  normalize(file) {
20
21
  return path_1.default.resolve(file);
21
22
  }
22
- // TS-like resolver
23
23
  resolveModule(module, from) {
24
- // relative
25
24
  if (module.startsWith(".")) {
26
25
  const abs = path_1.default.resolve(path_1.default.dirname(from), module);
27
26
  return this.resolveFile(abs);
28
27
  }
29
- // paths mapping
30
28
  for (const key in this.paths) {
31
- const pattern = key.replace("*", "(.*)");
29
+ const pattern = key.replace(/\*/g, "(.*)");
32
30
  const regex = new RegExp("^" + pattern + "$");
33
31
  const match = module.match(regex);
34
32
  if (!match)
35
33
  continue;
36
- const wildcard = match[1] || "";
34
+ const wildcard = match.slice(1);
37
35
  for (const target of this.paths[key]) {
38
- const replaced = target.replace("*", wildcard);
36
+ let replaced = target;
37
+ wildcard.forEach(w => {
38
+ replaced = replaced.replace("*", w);
39
+ });
39
40
  const abs = path_1.default.resolve(this.baseUrl, replaced);
40
41
  const resolved = this.resolveFile(abs);
41
42
  if (resolved)
42
43
  return resolved;
43
44
  }
44
45
  }
45
- // baseUrl fallback
46
46
  const abs = path_1.default.resolve(this.baseUrl, module);
47
- const resolved = this.resolveFile(abs);
48
- if (resolved)
49
- return resolved;
50
- return null;
47
+ return this.resolveFile(abs);
51
48
  }
52
49
  resolveFile(base) {
50
+ if (this.fileCache.has(base)) {
51
+ return this.fileCache.get(base);
52
+ }
53
53
  const tryFiles = [
54
54
  base,
55
55
  base + ".ts",
@@ -62,16 +62,18 @@ class DependencyGraph {
62
62
  ];
63
63
  for (const f of tryFiles) {
64
64
  if (fs_1.default.existsSync(f)) {
65
- return this.normalize(f);
65
+ const normalized = this.normalize(f);
66
+ this.fileCache.set(base, normalized);
67
+ return normalized;
66
68
  }
67
69
  }
70
+ this.fileCache.set(base, null);
68
71
  return null;
69
72
  }
70
73
  update(file, source) {
71
74
  const normalizedFile = this.normalize(file);
72
75
  this.exports.set(normalizedFile, new Set());
73
76
  for (const stmt of source.statements) {
74
- // IMPORT
75
77
  if (typescript_1.default.isImportDeclaration(stmt)) {
76
78
  const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
77
79
  const resolved = this.resolveModule(module, normalizedFile);
@@ -82,16 +84,12 @@ class DependencyGraph {
82
84
  }
83
85
  this.reverse.get(resolved).add(normalizedFile);
84
86
  }
85
- // EXPORT *
86
87
  if (typescript_1.default.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
87
88
  const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
88
89
  const resolved = this.resolveModule(module, normalizedFile);
89
90
  if (!resolved)
90
91
  continue;
91
- if (!this.exports.has(normalizedFile)) {
92
- this.exports.set(normalizedFile, new Set());
93
- }
94
- this.exports.get(normalizedFile).add(resolved);
92
+ this.exports.get(normalizedFile)?.add(resolved);
95
93
  if (!this.reverse.has(resolved)) {
96
94
  this.reverse.set(resolved, new Set());
97
95
  }
@@ -99,15 +97,36 @@ class DependencyGraph {
99
97
  }
100
98
  }
101
99
  }
102
- getDependents(file) {
103
- return [...(this.reverse.get(this.normalize(file)) ?? [])];
100
+ getAllDependentsDeep(file) {
101
+ const start = this.normalize(file);
102
+ const visited = new Set();
103
+ const result = new Set();
104
+ const walk = (f) => {
105
+ if (visited.has(f))
106
+ return;
107
+ visited.add(f);
108
+ const deps = this.reverse.get(f);
109
+ deps?.forEach(d => {
110
+ result.add(d);
111
+ walk(d);
112
+ });
113
+ const ex = this.exports.get(f);
114
+ ex?.forEach(e => walk(e));
115
+ };
116
+ walk(start);
117
+ return [...result];
104
118
  }
105
119
  remove(file) {
106
120
  const normalized = this.normalize(file);
121
+ this.reverse.delete(normalized);
107
122
  this.exports.delete(normalized);
123
+ this.fileCache.delete(normalized);
108
124
  for (const set of this.reverse.values()) {
109
125
  set.delete(normalized);
110
126
  }
127
+ for (const set of this.exports.values()) {
128
+ set.delete(normalized);
129
+ }
111
130
  }
112
131
  }
113
132
  exports.DependencyGraph = DependencyGraph;
@@ -48,6 +48,9 @@ class TSService {
48
48
  },
49
49
  getScriptVersion: (file) => this.files.get(file)?.version.toString() ?? "0",
50
50
  getScriptSnapshot: (file) => {
51
+ if (!this.fileNames.has(file)) {
52
+ return undefined;
53
+ }
51
54
  const f = this.files.get(file);
52
55
  if (f) {
53
56
  return typescript_1.default.ScriptSnapshot.fromString(f.content);
@@ -83,6 +86,13 @@ class TSService {
83
86
  getSourceFile(file) {
84
87
  return this.getProgram()?.getSourceFile(file);
85
88
  }
89
+ getFileContent(file) {
90
+ return this.files.get(file)?.content;
91
+ }
92
+ removeFile(file) {
93
+ this.files.delete(file);
94
+ this.fileNames.delete(file);
95
+ }
86
96
  getModuleResolutionConfig() {
87
97
  return {
88
98
  baseUrl: this.baseUrl,
@@ -12,7 +12,7 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
12
12
  seen.add(node);
13
13
  // param mapping
14
14
  if (typescript_1.default.isIdentifier(node) && paramMap?.has(node.text)) {
15
- return resolveNodeValue(paramMap.get(node.text), checker, paramMap);
15
+ return resolveNodeValue(paramMap.get(node.text), checker, paramMap, seen);
16
16
  }
17
17
  if (typescript_1.default.isStringLiteral(node)) {
18
18
  return { values: [node.text], dynamic: false };
@@ -37,8 +37,16 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
37
37
  let symbol = checker.getSymbolAtLocation(node);
38
38
  if (!symbol)
39
39
  return null;
40
+ // variable inline resolve FIRST
41
+ for (const decl of symbol.getDeclarations() ?? []) {
42
+ if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
43
+ return resolveNodeValue(decl.initializer, checker, paramMap, seen);
44
+ }
45
+ }
46
+ const original = symbol;
40
47
  symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
41
- if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
48
+ // avoid alias loop
49
+ if (symbol !== original && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
42
50
  symbol = checker.getAliasedSymbol(symbol);
43
51
  }
44
52
  for (const decl of symbol.getDeclarations() ?? []) {