@eventra_dev/eventra-cli 0.1.25 → 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.
@@ -1,5 +1,13 @@
1
1
  import ts from "typescript";
2
2
 
3
+ function isRealDeclaration(node: ts.Node) {
4
+ return (
5
+ ts.isFunctionDeclaration(node) ||
6
+ ts.isVariableDeclaration(node) ||
7
+ ts.isClassDeclaration(node)
8
+ );
9
+ }
10
+
3
11
  export function resolveExportedSymbol(
4
12
  symbol: ts.Symbol,
5
13
  checker: ts.TypeChecker,
@@ -8,20 +16,29 @@ export function resolveExportedSymbol(
8
16
 
9
17
  if (visited.has(symbol)) return null;
10
18
  visited.add(symbol);
19
+
20
+ // unwrap alias
11
21
  if (symbol.flags & ts.SymbolFlags.Alias) {
12
22
  const aliased = checker.getAliasedSymbol(symbol);
13
23
  return resolveExportedSymbol(aliased, checker, visited);
14
24
  }
15
25
 
16
- if (symbol.getDeclarations()?.length) {
17
- return symbol;
26
+ // export symbol
27
+ const exportSymbol = checker.getExportSymbolOfSymbol(symbol);
28
+ if (exportSymbol && exportSymbol !== symbol) {
29
+ return resolveExportedSymbol(exportSymbol, checker, visited);
18
30
  }
19
31
 
20
- const declarations = symbol.getDeclarations() ?? [];
32
+ const decls = symbol.getDeclarations() ?? [];
33
+
34
+ if (decls.some(isRealDeclaration)) {
35
+ return symbol;
36
+ }
21
37
 
22
- for (const decl of declarations) {
38
+ // exports (re-export)
39
+ for (const decl of decls) {
23
40
  const source = decl.getSourceFile();
24
- const moduleSymbol = checker.getSymbolAtLocation(source);
41
+ const moduleSymbol = (source as any).symbol;
25
42
 
26
43
  if (!moduleSymbol) continue;
27
44
 
@@ -8,24 +8,95 @@ export function findTrackParamIndex(
8
8
  fn.parameters.forEach((p, i) => {
9
9
  if (ts.isIdentifier(p.name)) {
10
10
  paramIndex.set(p.name.text, i);
11
+
12
+ // default param: (name = "x")
13
+ if (p.initializer && ts.isStringLiteral(p.initializer)) {
14
+ if (i === 0) return 0;
15
+ }
11
16
  }
12
17
  });
13
18
 
14
19
  let result: number | null = null;
15
20
 
21
+ function unwrap(node: ts.Expression): ts.Expression {
22
+ // as / parentheses
23
+ while (
24
+ ts.isAsExpression(node) ||
25
+ ts.isParenthesizedExpression(node)
26
+ ) {
27
+ node = node.expression;
28
+ }
29
+ return node;
30
+ }
31
+
32
+ function checkArg(arg: ts.Expression) {
33
+ arg = unwrap(arg);
34
+
35
+ // identifier → param
36
+ if (ts.isIdentifier(arg)) {
37
+ const idx = paramIndex.get(arg.text);
38
+ if (idx !== undefined) {
39
+ result = idx;
40
+ return true;
41
+ }
42
+ }
43
+
44
+ // string literal
45
+ if (ts.isStringLiteral(arg)) {
46
+ result = 0;
47
+ return true;
48
+ }
49
+
50
+ // template `${name}`
51
+ if (ts.isTemplateExpression(arg)) {
52
+ for (const span of arg.templateSpans) {
53
+ if (ts.isIdentifier(span.expression)) {
54
+ const idx = paramIndex.get(span.expression.text);
55
+ if (idx !== undefined) {
56
+ result = idx;
57
+ return true;
58
+ }
59
+ }
60
+ }
61
+ result = 0;
62
+ return true;
63
+ }
64
+
65
+ // object { event: name }
66
+ if (ts.isObjectLiteralExpression(arg)) {
67
+ for (const prop of arg.properties) {
68
+ if (!ts.isPropertyAssignment(prop)) continue;
69
+
70
+ const name = prop.name.getText();
71
+
72
+ if (["event", "name", "type"].includes(name)) {
73
+ return checkArg(prop.initializer);
74
+ }
75
+ }
76
+ }
77
+
78
+ // call: String(name)
79
+ if (ts.isCallExpression(arg)) {
80
+ for (const inner of arg.arguments) {
81
+ if (checkArg(inner)) return true;
82
+ }
83
+ }
84
+
85
+ return false;
86
+ }
87
+
16
88
  function visit(node: ts.Node) {
89
+ if (result !== null) return;
90
+
17
91
  if (ts.isCallExpression(node)) {
18
92
  let isTrackCall = false;
19
93
 
20
94
  if (ts.isPropertyAccessExpression(node.expression)) {
21
- const method = node.expression.name.text;
22
-
23
- if (method === "track") {
95
+ if (node.expression.name.text === "track") {
24
96
  isTrackCall = true;
25
97
  }
26
98
  }
27
99
 
28
- // fallback
29
100
  if (ts.isIdentifier(node.expression)) {
30
101
  if (node.expression.text === "track") {
31
102
  isTrackCall = true;
@@ -34,18 +105,7 @@ export function findTrackParamIndex(
34
105
 
35
106
  if (isTrackCall) {
36
107
  for (const arg of node.arguments) {
37
- if (ts.isIdentifier(arg)) {
38
- const idx = paramIndex.get(arg.text);
39
- if (idx !== undefined) {
40
- result = idx;
41
- return;
42
- }
43
- }
44
-
45
- if (ts.isStringLiteral(arg)) {
46
- result = 0;
47
- return;
48
- }
108
+ if (checkArg(arg)) return;
49
109
  }
50
110
  }
51
111
  }
@@ -13,15 +13,18 @@ export class DependencyGraph {
13
13
  ) {}
14
14
 
15
15
  private normalize(file: string) {
16
- return path.resolve(file);
16
+ return path.resolve(file).replace(/\\/g, "/");
17
17
  }
18
18
 
19
+ // MODULE RESOLVE
19
20
  private resolveModule(module: string, from: string): string | null {
21
+ // relative import
20
22
  if (module.startsWith(".")) {
21
23
  const abs = path.resolve(path.dirname(from), module);
22
24
  return this.resolveFile(abs);
23
25
  }
24
26
 
27
+ // tsconfig paths
25
28
  for (const key in this.paths) {
26
29
  const pattern = key.replace(/\*/g, "(.*)");
27
30
  const regex = new RegExp("^" + pattern + "$");
@@ -44,10 +47,21 @@ export class DependencyGraph {
44
47
  }
45
48
  }
46
49
 
50
+ // node resolution (safe)
51
+ try {
52
+ const resolved = require.resolve(module, {
53
+ paths: [path.dirname(from)],
54
+ });
55
+
56
+ return this.normalize(resolved);
57
+ } catch {}
58
+
59
+ // fallback
47
60
  const abs = path.resolve(this.baseUrl, module);
48
61
  return this.resolveFile(abs);
49
62
  }
50
63
 
64
+ // FILE RESOLVE
51
65
  private resolveFile(base: string): string | null {
52
66
  if (this.fileCache.has(base)) {
53
67
  return this.fileCache.get(base)!;
@@ -59,6 +73,12 @@ export class DependencyGraph {
59
73
  base + ".tsx",
60
74
  base + ".js",
61
75
  base + ".jsx",
76
+ base + ".mjs",
77
+ base + ".cjs",
78
+ base + ".d.ts",
79
+ base + ".vue",
80
+ base + ".svelte",
81
+ base + ".astro",
62
82
  path.join(base, "index.ts"),
63
83
  path.join(base, "index.tsx"),
64
84
  path.join(base, "index.js"),
@@ -76,16 +96,28 @@ export class DependencyGraph {
76
96
  return null;
77
97
  }
78
98
 
99
+ // UPDATE GRAPH
79
100
  update(file: string, source: ts.SourceFile) {
80
101
  const normalizedFile = this.normalize(file);
81
102
 
103
+ for (const deps of this.reverse.values()) {
104
+ deps.delete(normalizedFile);
105
+ }
106
+
82
107
  this.exports.set(normalizedFile, new Set());
83
108
 
84
109
  for (const stmt of source.statements) {
110
+
111
+ // IMPORT
85
112
  if (ts.isImportDeclaration(stmt)) {
86
- const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
87
- const resolved = this.resolveModule(module, normalizedFile);
113
+ // ignore import type
114
+ if ((stmt.importClause as any)?.importKind === "type") continue;
115
+
116
+ const module = stmt.moduleSpecifier
117
+ .getText()
118
+ .replace(/['"]/g, "");
88
119
 
120
+ const resolved = this.resolveModule(module, normalizedFile);
89
121
  if (!resolved) continue;
90
122
 
91
123
  if (!this.reverse.has(resolved)) {
@@ -95,10 +127,15 @@ export class DependencyGraph {
95
127
  this.reverse.get(resolved)!.add(normalizedFile);
96
128
  }
97
129
 
98
- if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
99
- const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
100
- const resolved = this.resolveModule(module, normalizedFile);
130
+ // EXPORT (re-export)
131
+ if (ts.isExportDeclaration(stmt)) {
132
+ if (!stmt.moduleSpecifier) continue;
133
+
134
+ const module = stmt.moduleSpecifier
135
+ .getText()
136
+ .replace(/['"]/g, "");
101
137
 
138
+ const resolved = this.resolveModule(module, normalizedFile);
102
139
  if (!resolved) continue;
103
140
 
104
141
  this.exports.get(normalizedFile)?.add(resolved);
@@ -112,6 +149,7 @@ export class DependencyGraph {
112
149
  }
113
150
  }
114
151
 
152
+ // DEPENDENTS
115
153
  getAllDependentsDeep(file: string): string[] {
116
154
  const start = this.normalize(file);
117
155
  const visited = new Set<string>();
@@ -136,6 +174,7 @@ export class DependencyGraph {
136
174
  return [...result];
137
175
  }
138
176
 
177
+ // REMOVE
139
178
  remove(file: string) {
140
179
  const normalized = this.normalize(file);
141
180
 
@@ -7,9 +7,12 @@ export class TSService {
7
7
  private fileNames = new Set<string>();
8
8
  private service: ts.LanguageService;
9
9
 
10
- private readonly compilerOptions: ts.CompilerOptions = {};
11
- private readonly baseUrl: string = "";
12
- private readonly paths: Record<string, string[]> = {};
10
+ private compilerOptions: ts.CompilerOptions;
11
+ private baseUrl: string;
12
+ private paths: Record<string, string[]>;
13
+ private usage = new Map<string, number>();
14
+ private tick = 0;
15
+ private MAX_FILES = 2000;
13
16
 
14
17
  constructor(private root: string) {
15
18
  const configPath = ts.findConfigFile(
@@ -20,13 +23,18 @@ export class TSService {
20
23
 
21
24
  let compilerOptions: ts.CompilerOptions = {
22
25
  allowJs: true,
23
- jsx: ts.JsxEmit.Preserve,
26
+ jsx: ts.JsxEmit.React,
27
+ jsxImportSource: "react",
24
28
  moduleResolution: ts.ModuleResolutionKind.Node16,
25
29
  target: ts.ScriptTarget.ESNext,
26
30
  esModuleInterop: true,
27
31
  allowSyntheticDefaultImports: true,
32
+ skipLibCheck: true,
28
33
  };
29
34
 
35
+ let baseUrl = root;
36
+ let paths: Record<string, string[]> = {};
37
+
30
38
  if (configPath) {
31
39
  const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
32
40
 
@@ -41,56 +49,108 @@ export class TSService {
41
49
  ...parsed.options,
42
50
  };
43
51
 
44
- this.paths = parsed.options.paths || {};
45
- this.baseUrl = parsed.options.baseUrl
52
+ paths = parsed.options.paths || {};
53
+ baseUrl = parsed.options.baseUrl
46
54
  ? path.resolve(root, parsed.options.baseUrl)
47
55
  : root;
48
- } else {
49
- this.baseUrl = root;
50
56
  }
51
57
 
52
58
  this.compilerOptions = compilerOptions;
59
+ this.baseUrl = baseUrl;
60
+ this.paths = paths;
53
61
 
54
62
  const host: ts.LanguageServiceHost = {
55
- getScriptFileNames: () => {
56
- return [...new Set(this.fileNames)].sort((a, b) =>
57
- a.localeCompare(b)
58
- );
59
- },
63
+ getScriptFileNames: () => Array.from(this.fileNames),
60
64
 
61
- getScriptVersion: (file) =>
62
- this.files.get(file)?.version.toString() ?? "0",
65
+ getScriptVersion: (file) => {
66
+ file = this.normalize(file);
67
+ return this.files.get(file)?.version.toString() ?? "0";
68
+ },
63
69
 
64
70
  getScriptSnapshot: (file) => {
65
- const f = this.files.get(file);
71
+ file = this.normalize(file);
66
72
 
73
+ const f = this.files.get(file);
67
74
  if (f) {
75
+ this.touch(file);
68
76
  return ts.ScriptSnapshot.fromString(f.content);
69
77
  }
70
78
 
71
- if (fs.existsSync(file)) {
72
- return ts.ScriptSnapshot.fromString(
73
- fs.readFileSync(file, "utf-8")
74
- );
75
- }
79
+ try {
80
+ if (fs.existsSync(file)) {
81
+ const content = fs.readFileSync(file, "utf-8");
82
+
83
+ this.files.set(file, {
84
+ content,
85
+ version: 1,
86
+ });
87
+
88
+ this.fileNames.add(file);
89
+ this.touch(file);
90
+ this.evictIfNeeded();
76
91
 
77
- return undefined;
92
+ return ts.ScriptSnapshot.fromString(content);
93
+ }
94
+ } catch {}
95
+
96
+ // fallback
97
+ return ts.ScriptSnapshot.fromString("");
78
98
  },
79
99
 
80
100
  getCurrentDirectory: () => this.root,
81
101
  getCompilationSettings: () => this.compilerOptions,
82
102
  getDefaultLibFileName: (opts) =>
83
103
  ts.getDefaultLibFilePath(opts),
84
-
85
- readFile: ts.sys.readFile,
86
104
  fileExists: ts.sys.fileExists,
105
+ readFile: ts.sys.readFile,
87
106
  readDirectory: ts.sys.readDirectory,
88
107
  };
89
108
 
90
109
  this.service = ts.createLanguageService(host);
91
110
  }
92
111
 
112
+ // INTERNAL
113
+ private normalize(file: string) {
114
+ return path.resolve(file).replace(/\\/g, "/");
115
+ }
116
+
117
+ private touch(file: string) {
118
+ this.tick++;
119
+ this.usage.set(file, this.tick);
120
+ }
121
+
122
+ private evictIfNeeded() {
123
+ if (this.fileNames.size <= this.MAX_FILES) return;
124
+
125
+ let oldestFile: string | null = null;
126
+ let oldestTick = Infinity;
127
+
128
+ for (const f of this.fileNames) {
129
+ const t = this.usage.get(f) ?? 0;
130
+
131
+ if (t < oldestTick) {
132
+ oldestTick = t;
133
+ oldestFile = f;
134
+ }
135
+ }
136
+
137
+ if (oldestFile) {
138
+ this.files.delete(oldestFile);
139
+ this.fileNames.delete(oldestFile);
140
+ this.usage.delete(oldestFile);
141
+ }
142
+ }
143
+
144
+ private ensureProgram() {
145
+ try {
146
+ this.service.getProgram();
147
+ } catch {}
148
+ }
149
+
150
+ // PUBLIC API
93
151
  updateFile(file: string, content: string) {
152
+ file = this.normalize(file);
153
+
94
154
  const prev = this.files.get(file);
95
155
 
96
156
  this.files.set(file, {
@@ -99,6 +159,20 @@ export class TSService {
99
159
  });
100
160
 
101
161
  this.fileNames.add(file);
162
+ this.touch(file);
163
+ this.evictIfNeeded();
164
+
165
+ this.ensureProgram();
166
+ }
167
+
168
+ removeFile(file: string) {
169
+ file = this.normalize(file);
170
+
171
+ this.files.delete(file);
172
+ this.fileNames.delete(file);
173
+ this.usage.delete(file);
174
+
175
+ this.ensureProgram();
102
176
  }
103
177
 
104
178
  getProgram() {
@@ -110,18 +184,15 @@ export class TSService {
110
184
  }
111
185
 
112
186
  getSourceFile(file: string) {
187
+ file = this.normalize(file);
113
188
  return this.getProgram()?.getSourceFile(file);
114
189
  }
115
190
 
116
191
  getFileContent(file: string) {
192
+ file = this.normalize(file);
117
193
  return this.files.get(file)?.content;
118
194
  }
119
195
 
120
- removeFile(file: string) {
121
- this.files.delete(file);
122
- this.fileNames.delete(file);
123
- }
124
-
125
196
  getModuleResolutionConfig() {
126
197
  return {
127
198
  baseUrl: this.baseUrl,
@@ -12,12 +12,13 @@ export function resolveNodeValue(
12
12
  paramMap?: Map<string, ts.Expression>,
13
13
  seen = new Set<ts.Node>()
14
14
  ): ResolveResult {
15
+
15
16
  if (seen.has(node)) {
16
17
  return { values: [], dynamic: true };
17
18
  }
18
19
  seen.add(node);
19
20
 
20
- // param mapping
21
+ // PARAM MAPPING
21
22
  if (ts.isIdentifier(node) && paramMap?.has(node.text)) {
22
23
  return resolveNodeValue(
23
24
  paramMap.get(node.text)!,
@@ -43,12 +44,15 @@ export function resolveNodeValue(
43
44
  }
44
45
  }
45
46
 
46
- return { values, dynamic: true };
47
+ return {
48
+ values: [...new Set(values)],
49
+ dynamic: true
50
+ };
47
51
  }
48
52
 
49
53
  // TEMPLATE
50
54
  if (ts.isTemplateExpression(node)) {
51
- let results = [node.head.text];
55
+ let results = node.head.text ? [node.head.text] : [""];
52
56
 
53
57
  for (const span of node.templateSpans) {
54
58
  const res = resolveNodeValue(span.expression, checker, paramMap, seen);
@@ -83,14 +87,7 @@ export function resolveNodeValue(
83
87
  let symbol = checker.getSymbolAtLocation(node);
84
88
  if (!symbol) return { values: [], dynamic: true };
85
89
 
86
- // unwrap export
87
- symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
88
-
89
- if (symbol.flags & ts.SymbolFlags.Alias) {
90
- symbol = checker.getAliasedSymbol(symbol);
91
- }
92
-
93
- // inline variable
90
+ // INLINE FIRST
94
91
  for (const decl of symbol.getDeclarations() ?? []) {
95
92
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
96
93
  return resolveNodeValue(
@@ -100,7 +97,16 @@ export function resolveNodeValue(
100
97
  seen
101
98
  );
102
99
  }
100
+ }
101
+
102
+ // unwrap export
103
+ symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
104
+
105
+ if (symbol.flags & ts.SymbolFlags.Alias) {
106
+ symbol = checker.getAliasedSymbol(symbol);
107
+ }
103
108
 
109
+ for (const decl of symbol.getDeclarations() ?? []) {
104
110
  if (ts.isEnumMember(decl) && decl.initializer) {
105
111
  if (ts.isStringLiteral(decl.initializer)) {
106
112
  return {
@@ -111,15 +117,15 @@ export function resolveNodeValue(
111
117
  }
112
118
  }
113
119
 
114
- // UNION TYPES
115
120
  const type = checker.getTypeAtLocation(node);
116
121
 
122
+ // UNION
117
123
  if (type.isUnion()) {
118
124
  const values: string[] = [];
119
125
 
120
126
  for (const t of type.types) {
121
- if ((t as any).isStringLiteral?.()) {
122
- values.push((t as any).value);
127
+ if (t.flags & ts.TypeFlags.StringLiteral) {
128
+ values.push((t as ts.StringLiteralType).value);
123
129
  }
124
130
  }
125
131
 
@@ -128,15 +134,18 @@ export function resolveNodeValue(
128
134
  }
129
135
  }
130
136
 
131
- // TYPE FALLBACK (as const)
132
- if ((type as any).isStringLiteral?.()) {
133
- return { values: [(type as any).value], dynamic: false };
137
+ // STRING LITERAL TYPE
138
+ if (type.flags & ts.TypeFlags.StringLiteral) {
139
+ return {
140
+ values: [(type as ts.StringLiteralType).value],
141
+ dynamic: false
142
+ };
134
143
  }
135
144
 
136
145
  return { values: [], dynamic: true };
137
146
  }
138
147
 
139
- // PROPERTY ACCESS (obj.a.b)
148
+ // PROPERTY ACCESS
140
149
  if (ts.isPropertyAccessExpression(node)) {
141
150
 
142
151
  const symbol = checker.getSymbolAtLocation(node);
@@ -160,18 +169,16 @@ export function resolveNodeValue(
160
169
  }
161
170
  }
162
171
 
163
- // constant enum / readonly
164
172
  const constantValue = checker.getConstantValue(node as any);
165
173
  if (typeof constantValue === "string") {
166
174
  return { values: [constantValue], dynamic: false };
167
175
  }
168
176
 
169
- // type fallback
170
177
  const type = checker.getTypeAtLocation(node);
171
178
 
172
- if ((type as any).isStringLiteral?.()) {
179
+ if (type.flags & ts.TypeFlags.StringLiteral) {
173
180
  return {
174
- values: [(type as any).value],
181
+ values: [(type as ts.StringLiteralType).value],
175
182
  dynamic: false,
176
183
  };
177
184
  }
@@ -206,10 +213,11 @@ export function resolveNodeValue(
206
213
  }
207
214
  }
208
215
 
209
- return { values: [], dynamic: true };
216
+ // fallback
217
+ return resolveNodeValue(node.name, checker, paramMap, seen);
210
218
  }
211
219
 
212
- // ARRAY ACCESS (arr[0])
220
+ // ARRAY ACCESS
213
221
  if (ts.isElementAccessExpression(node)) {
214
222
  const obj = resolveNodeValue(node.expression, checker, paramMap, seen);
215
223
 
@@ -224,7 +232,6 @@ export function resolveNodeValue(
224
232
  seen
225
233
  );
226
234
 
227
- // if index known
228
235
  if (index.values.length === 1) {
229
236
  const i = Number(index.values[0]);
230
237
 
@@ -236,7 +243,6 @@ export function resolveNodeValue(
236
243
  }
237
244
  }
238
245
 
239
- // fallback → dynamic
240
246
  return obj.values.length
241
247
  ? { values: obj.values, dynamic: true }
242
248
  : { values: [], dynamic: true };
@@ -257,7 +263,10 @@ export function resolveNodeValue(
257
263
  }
258
264
  }
259
265
 
260
- return { values, dynamic: true };
266
+ return {
267
+ values: [...new Set(values)],
268
+ dynamic: true
269
+ };
261
270
  }
262
271
 
263
272
  // BINARY ("btn_" + name)