@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.
- package/dist/commands/check.js +40 -16
- package/dist/commands/sync.js +40 -38
- package/dist/commands/watch.js +90 -36
- package/dist/engine/callResolver.js +52 -4
- package/dist/engine/engine.js +66 -32
- package/dist/engine/exportResolver.js +16 -4
- package/dist/engine/functionAnalyzer.js +66 -13
- package/dist/engine/graph.js +40 -4
- package/dist/engine/languageService.js +83 -20
- package/dist/engine/resolver.js +29 -21
- package/dist/engine/scanner.js +57 -46
- package/dist/engine/trackingDetector.js +68 -0
- package/dist/utils/getVirtualFile.js +8 -0
- package/dist/utils/processFile.js +0 -9
- package/dist/utils/universal.js +25 -87
- package/package.json +1 -1
- package/src/commands/check.ts +49 -29
- package/src/commands/sync.ts +48 -47
- package/src/commands/watch.ts +105 -37
- package/src/engine/callResolver.ts +77 -6
- package/src/engine/engine.ts +80 -33
- package/src/engine/exportResolver.ts +22 -5
- package/src/engine/functionAnalyzer.ts +76 -16
- package/src/engine/graph.ts +45 -6
- package/src/engine/languageService.ts +100 -29
- package/src/engine/resolver.ts +36 -27
- package/src/engine/scanner.ts +72 -50
- package/src/engine/trackingDetector.ts +94 -0
- package/src/types.ts +0 -3
- package/src/utils/getVirtualFile.ts +5 -0
- package/src/utils/processFile.ts +0 -9
- package/src/utils/universal.ts +26 -102
package/src/engine/engine.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
1
4
|
import { TSService } from "./languageService";
|
|
2
5
|
import { ScanResult, scanSource } from "./scanner";
|
|
3
6
|
import { DependencyGraph } from "./graph";
|
|
@@ -19,39 +22,56 @@ export class EventraEngine {
|
|
|
19
22
|
this.graph = new DependencyGraph(baseUrl, paths);
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
private normalize(file: string) {
|
|
26
|
+
return path.resolve(file).replace(/\\/g, "/");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private empty(): ScanResult {
|
|
30
|
+
return {
|
|
31
|
+
events: new Set(),
|
|
32
|
+
detectedFunctionWrappers: new Set(),
|
|
33
|
+
detectedComponentWrappers: new Map(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// PRELOAD
|
|
38
|
+
preloadFile(file: string, content: string) {
|
|
39
|
+
file = this.normalize(file);
|
|
40
|
+
|
|
41
|
+
this.ts.updateFile(file, content);
|
|
42
|
+
|
|
43
|
+
const source = this.ts.getSourceFile(file);
|
|
44
|
+
if (source) {
|
|
45
|
+
this.graph.update(file, source);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
// SCAN FILE
|
|
23
50
|
scanFile(
|
|
24
51
|
file: string,
|
|
25
52
|
content: string,
|
|
26
53
|
config: EventraConfig
|
|
27
54
|
): ScanResult {
|
|
55
|
+
file = this.normalize(file);
|
|
56
|
+
|
|
28
57
|
const h = hash(content);
|
|
29
58
|
|
|
30
|
-
// skip unchanged
|
|
31
59
|
if (this.fileHash.get(file) === h) {
|
|
32
|
-
return (
|
|
33
|
-
this.fileResults.get(file) ?? {
|
|
34
|
-
events: new Set(),
|
|
35
|
-
detectedFunctionWrappers: new Set(),
|
|
36
|
-
detectedComponentWrappers: new Map(),
|
|
37
|
-
}
|
|
38
|
-
);
|
|
60
|
+
return this.fileResults.get(file) ?? this.empty();
|
|
39
61
|
}
|
|
40
62
|
|
|
41
63
|
this.fileHash.set(file, h);
|
|
64
|
+
|
|
42
65
|
this.ts.updateFile(file, content);
|
|
43
66
|
|
|
44
67
|
const source = this.ts.getSourceFile(file);
|
|
45
68
|
const checker = this.ts.getChecker();
|
|
46
69
|
|
|
47
70
|
if (!source || !checker) {
|
|
48
|
-
return
|
|
49
|
-
events: new Set(),
|
|
50
|
-
detectedFunctionWrappers: new Set(),
|
|
51
|
-
detectedComponentWrappers: new Map(),
|
|
52
|
-
};
|
|
71
|
+
return this.empty();
|
|
53
72
|
}
|
|
54
73
|
|
|
74
|
+
// обновляем graph
|
|
55
75
|
this.graph.update(file, source);
|
|
56
76
|
|
|
57
77
|
const res = scanSource(source, checker, config);
|
|
@@ -66,33 +86,46 @@ export class EventraEngine {
|
|
|
66
86
|
content: string,
|
|
67
87
|
config: EventraConfig
|
|
68
88
|
) {
|
|
69
|
-
|
|
89
|
+
file = this.normalize(file);
|
|
90
|
+
|
|
70
91
|
const res = this.scanFile(file, content, config);
|
|
71
92
|
|
|
72
93
|
const dependents = this.graph.getAllDependentsDeep(file);
|
|
73
94
|
|
|
74
95
|
for (const dep of dependents) {
|
|
75
|
-
|
|
96
|
+
const abs = this.normalize(dep);
|
|
97
|
+
|
|
98
|
+
if (isExternalFile(abs)) continue;
|
|
76
99
|
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
let depContent = this.ts.getFileContent(abs);
|
|
101
|
+
|
|
102
|
+
if (!depContent) {
|
|
103
|
+
try {
|
|
104
|
+
depContent = fs.readFileSync(abs, "utf-8");
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
79
109
|
|
|
80
110
|
const h = hash(depContent);
|
|
81
111
|
|
|
82
|
-
if (this.fileHash.get(
|
|
112
|
+
if (this.fileHash.get(abs) === h) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
83
115
|
|
|
84
|
-
this.fileHash.set(
|
|
85
|
-
this.ts.updateFile(dep, depContent);
|
|
116
|
+
this.fileHash.set(abs, h);
|
|
86
117
|
|
|
87
|
-
|
|
118
|
+
this.ts.updateFile(abs, depContent);
|
|
119
|
+
|
|
120
|
+
const source = this.ts.getSourceFile(abs);
|
|
88
121
|
const checker = this.ts.getChecker();
|
|
89
122
|
|
|
90
123
|
if (!source || !checker) continue;
|
|
91
124
|
|
|
92
|
-
this.graph.update(
|
|
125
|
+
this.graph.update(abs, source);
|
|
93
126
|
|
|
94
127
|
const r = scanSource(source, checker, config);
|
|
95
|
-
this.fileResults.set(
|
|
128
|
+
this.fileResults.set(abs, r);
|
|
96
129
|
}
|
|
97
130
|
|
|
98
131
|
return res;
|
|
@@ -111,31 +144,45 @@ export class EventraEngine {
|
|
|
111
144
|
|
|
112
145
|
// REMOVE FILE
|
|
113
146
|
removeFile(file: string, config?: EventraConfig) {
|
|
147
|
+
file = this.normalize(file);
|
|
148
|
+
|
|
149
|
+
const dependents = this.graph.getAllDependentsDeep(file);
|
|
150
|
+
|
|
114
151
|
this.ts.removeFile(file);
|
|
115
152
|
this.fileResults.delete(file);
|
|
116
153
|
this.fileHash.delete(file);
|
|
117
154
|
this.graph.remove(file);
|
|
118
155
|
|
|
119
|
-
const dependents = this.graph.getAllDependentsDeep(file);
|
|
120
|
-
|
|
121
156
|
for (const dep of dependents) {
|
|
122
|
-
|
|
157
|
+
const abs = this.normalize(dep);
|
|
123
158
|
|
|
124
|
-
|
|
125
|
-
|
|
159
|
+
if (isExternalFile(abs)) continue;
|
|
160
|
+
|
|
161
|
+
let content = this.ts.getFileContent(abs);
|
|
162
|
+
|
|
163
|
+
if (!content) {
|
|
164
|
+
try {
|
|
165
|
+
content = fs.readFileSync(abs, "utf-8");
|
|
166
|
+
} catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
126
170
|
|
|
127
171
|
const h = hash(content);
|
|
128
172
|
|
|
129
|
-
if (this.fileHash.get(
|
|
173
|
+
if (this.fileHash.get(abs) === h) continue;
|
|
174
|
+
|
|
175
|
+
this.fileHash.set(abs, h);
|
|
130
176
|
|
|
131
|
-
this.
|
|
132
|
-
this.ts.updateFile(dep, content);
|
|
177
|
+
this.ts.updateFile(abs, content);
|
|
133
178
|
|
|
134
|
-
const source = this.ts.getSourceFile(
|
|
179
|
+
const source = this.ts.getSourceFile(abs);
|
|
135
180
|
const checker = this.ts.getChecker();
|
|
136
181
|
|
|
137
182
|
if (!source || !checker) continue;
|
|
138
183
|
|
|
184
|
+
this.graph.update(abs, source);
|
|
185
|
+
|
|
139
186
|
const r = scanSource(
|
|
140
187
|
source,
|
|
141
188
|
checker,
|
|
@@ -147,7 +194,7 @@ export class EventraEngine {
|
|
|
147
194
|
}
|
|
148
195
|
);
|
|
149
196
|
|
|
150
|
-
this.fileResults.set(
|
|
197
|
+
this.fileResults.set(abs, r);
|
|
151
198
|
}
|
|
152
199
|
}
|
|
153
200
|
}
|
|
@@ -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
|
-
|
|
17
|
-
|
|
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
|
|
32
|
+
const decls = symbol.getDeclarations() ?? [];
|
|
33
|
+
|
|
34
|
+
if (decls.some(isRealDeclaration)) {
|
|
35
|
+
return symbol;
|
|
36
|
+
}
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
// exports (re-export)
|
|
39
|
+
for (const decl of decls) {
|
|
23
40
|
const source = decl.getSourceFile();
|
|
24
|
-
const moduleSymbol =
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|
package/src/engine/graph.ts
CHANGED
|
@@ -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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
11
|
-
private
|
|
12
|
-
private
|
|
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.
|
|
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
|
-
|
|
45
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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,
|