UNPKG

8.89 kBPlain TextView Raw
1import { IContext } from "./context";
2import { Graph, alg } from "graphlib";
3import { sha1 } from "object-hash";
4import { RollingCache } from "./rollingcache";
5import { ICache } from "./icache";
6import * as _ from "lodash";
7import { tsModule } from "./tsproxy";
8import * as tsTypes from "typescript";
9import { blue, yellow, green } from "colors/safe";
10import { emptyDirSync, pathExistsSync } from "fs-extra";
11import { formatHost } from "./diagnostics-format-host";
12import { NoCache } from "./nocache";
13
14export interface ICode
15{
16 code?: string;
17 map?: string;
18 dts?: tsTypes.OutputFile;
19 dtsmap?: tsTypes.OutputFile;
20}
21
22export interface IRollupCode
23{
24 code: string | undefined;
25 map: { mappings: string };
26}
27
28interface INodeLabel
29{
30 dirty: boolean;
31}
32
33export interface IDiagnostics
34{
35 flatMessage: string;
36 formatted: string;
37 fileLine?: string;
38 category: tsTypes.DiagnosticCategory;
39 code: number;
40 type: string;
41}
42
43interface ITypeSnapshot
44{
45 id: string;
46 snapshot: tsTypes.IScriptSnapshot | undefined;
47}
48
49export function convertEmitOutput(output: tsTypes.EmitOutput): ICode
50{
51 const out: ICode = { };
52
53 output.outputFiles.forEach((e) =>
54 {
55 if (_.endsWith(e.name, ".d.ts"))
56 out.dts = e;
57 else if (_.endsWith(e.name, ".d.ts.map"))
58 out.dtsmap = e;
59 else if (_.endsWith(e.name, ".map"))
60 out.map = e.text;
61 else
62 out.code = e.text;
63 });
64
65 return out;
66}
67
68export function convertDiagnostic(type: string, data: tsTypes.Diagnostic[]): IDiagnostics[]
69{
70 return _.map(data, (diagnostic) =>
71 {
72 const entry: IDiagnostics =
73 {
74 flatMessage: tsModule.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
75 formatted: tsModule.formatDiagnosticsWithColorAndContext(data, formatHost),
76 category: diagnostic.category,
77 code: diagnostic.code,
78 type,
79 };
80
81 if (diagnostic.file && diagnostic.start !== undefined)
82 {
83 const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
84 entry.fileLine = `${diagnostic.file.fileName}(${line + 1},${character + 1})`;
85 }
86
87 return entry;
88 });
89}
90
91export class TsCache
92{
93 private cacheVersion = "7";
94 private dependencyTree: Graph;
95 private ambientTypes: ITypeSnapshot[];
96 private ambientTypesDirty = false;
97 private cacheDir: string;
98 private codeCache!: ICache<ICode | undefined>;
99 private typesCache!: ICache<string>;
100 private semanticDiagnosticsCache!: ICache<IDiagnostics[]>;
101 private syntacticDiagnosticsCache!: ICache<IDiagnostics[]>;
102
103 constructor(private noCache: boolean, private host: tsTypes.LanguageServiceHost, cache: string, private options: tsTypes.CompilerOptions, private rollupConfig: any, rootFilenames: string[], private context: IContext)
104 {
105 this.cacheDir = `${cache}/${sha1({
106 version: this.cacheVersion,
107 rootFilenames,
108 options: this.options,
109 rollupConfig: this.rollupConfig,
110 tsVersion: tsModule.version,
111 })}`;
112
113 this.dependencyTree = new Graph({ directed: true });
114 this.dependencyTree.setDefaultNodeLabel((_node: string) => ({ dirty: false }));
115
116 const automaticTypes = _.map(tsModule.getAutomaticTypeDirectiveNames(options, tsModule.sys), (entry) => tsModule.resolveTypeReferenceDirective(entry, undefined, options, tsModule.sys))
117 .filter((entry) => entry.resolvedTypeReferenceDirective && entry.resolvedTypeReferenceDirective.resolvedFileName)
118 .map((entry) => entry.resolvedTypeReferenceDirective!.resolvedFileName!);
119
120 this.ambientTypes = _.filter(rootFilenames, (file) => _.endsWith(file, ".d.ts"))
121 .concat(automaticTypes)
122 .map((id) => ({ id, snapshot: this.host.getScriptSnapshot(id) }));
123
124 this.init();
125
126 this.checkAmbientTypes();
127 }
128
129 public clean()
130 {
131 if (pathExistsSync(this.cacheDir))
132 {
133 this.context.info(blue(`cleaning cache: ${this.cacheDir}`));
134 emptyDirSync(this.cacheDir);
135 }
136
137 this.init();
138 }
139
140 public setDependency(importee: string, importer: string): void
141 {
142 // importee -> importer
143 this.context.debug(`${blue("dependency")} '${importee}'`);
144 this.context.debug(` imported by '${importer}'`);
145 this.dependencyTree.setEdge(importer, importee);
146 }
147
148 public walkTree(cb: (id: string) => void | false): void
149 {
150 const acyclic = alg.isAcyclic(this.dependencyTree);
151
152 if (acyclic)
153 {
154 _.each(alg.topsort(this.dependencyTree), (id: string) => cb(id));
155 return;
156 }
157
158 this.context.info(yellow("import tree has cycles"));
159
160 _.each(this.dependencyTree.nodes(), (id: string) => cb(id));
161 }
162
163 public done()
164 {
165 this.context.info(blue("rolling caches"));
166 this.codeCache.roll();
167 this.semanticDiagnosticsCache.roll();
168 this.syntacticDiagnosticsCache.roll();
169 this.typesCache.roll();
170 }
171
172 public getCompiled(id: string, snapshot: tsTypes.IScriptSnapshot, transform: () => ICode | undefined): ICode | undefined
173 {
174 const name = this.makeName(id, snapshot);
175
176 this.context.info(`${blue("transpiling")} '${id}'`);
177 this.context.debug(` cache: '${this.codeCache.path(name)}'`);
178
179 if (this.codeCache.exists(name) && !this.isDirty(id, false))
180 {
181 this.context.debug(green(" cache hit"));
182 const data = this.codeCache.read(name);
183 if (data)
184 {
185 this.codeCache.write(name, data);
186 return data;
187 }
188 else
189 this.context.warn(yellow(" cache broken, discarding"));
190 }
191
192 this.context.debug(yellow(" cache miss"));
193
194 const transformedData = transform();
195 this.codeCache.write(name, transformedData);
196 this.markAsDirty(id);
197 return transformedData;
198 }
199
200 public getSyntacticDiagnostics(id: string, snapshot: tsTypes.IScriptSnapshot, check: () => tsTypes.Diagnostic[]): IDiagnostics[]
201 {
202 return this.getDiagnostics("syntax", this.syntacticDiagnosticsCache, id, snapshot, check);
203 }
204
205 public getSemanticDiagnostics(id: string, snapshot: tsTypes.IScriptSnapshot, check: () => tsTypes.Diagnostic[]): IDiagnostics[]
206 {
207 return this.getDiagnostics("semantic", this.semanticDiagnosticsCache, id, snapshot, check);
208 }
209
210 private checkAmbientTypes(): void
211 {
212 this.context.debug(blue("Ambient types:"));
213 const typeNames = _.filter(this.ambientTypes, (snapshot) => snapshot.snapshot !== undefined)
214 .map((snapshot) =>
215 {
216 this.context.debug(` ${snapshot.id}`);
217 return this.makeName(snapshot.id, snapshot.snapshot!);
218 });
219 // types dirty if any d.ts changed, added or removed
220 this.ambientTypesDirty = !this.typesCache.match(typeNames);
221
222 if (this.ambientTypesDirty)
223 this.context.info(yellow("ambient types changed, redoing all semantic diagnostics"));
224
225 _.each(typeNames, (name) => this.typesCache.touch(name));
226 }
227
228 private getDiagnostics(type: string, cache: ICache<IDiagnostics[]>, id: string, snapshot: tsTypes.IScriptSnapshot, check: () => tsTypes.Diagnostic[]): IDiagnostics[]
229 {
230 const name = this.makeName(id, snapshot);
231
232 this.context.debug(` cache: '${cache.path(name)}'`);
233
234 if (cache.exists(name) && !this.isDirty(id, true))
235 {
236 this.context.debug(green(" cache hit"));
237
238 const data = cache.read(name);
239 if (data)
240 {
241 cache.write(name, data);
242 return data;
243 }
244 else
245 this.context.warn(yellow(" cache broken, discarding"));
246 }
247
248 this.context.debug(yellow(" cache miss"));
249
250 const convertedData = convertDiagnostic(type, check());
251 cache.write(name, convertedData);
252 this.markAsDirty(id);
253 return convertedData;
254 }
255
256 private init()
257 {
258 if (this.noCache)
259 {
260 this.codeCache = new NoCache<ICode>();
261 this.typesCache = new NoCache<string>();
262 this.syntacticDiagnosticsCache = new NoCache<IDiagnostics[]>();
263 this.semanticDiagnosticsCache = new NoCache<IDiagnostics[]>();
264 }
265 else
266 {
267 this.codeCache = new RollingCache<ICode>(`${this.cacheDir}/code`, true);
268 this.typesCache = new RollingCache<string>(`${this.cacheDir}/types`, true);
269 this.syntacticDiagnosticsCache = new RollingCache<IDiagnostics[]>(`${this.cacheDir}/syntacticDiagnostics`, true);
270 this.semanticDiagnosticsCache = new RollingCache<IDiagnostics[]>(`${this.cacheDir}/semanticDiagnostics`, true);
271 }
272 }
273
274 private markAsDirty(id: string): void
275 {
276 this.dependencyTree.setNode(id, { dirty: true });
277 }
278
279 // returns true if node or any of its imports or any of global types changed
280 private isDirty(id: string, checkImports: boolean): boolean
281 {
282 const label = this.dependencyTree.node(id) as INodeLabel;
283
284 if (!label)
285 return false;
286
287 if (!checkImports || label.dirty)
288 return label.dirty;
289
290 if (this.ambientTypesDirty)
291 return true;
292
293 const dependencies = alg.dijkstra(this.dependencyTree, id);
294
295 return _.some(dependencies, (dependency, node) =>
296 {
297 if (!node || dependency.distance === Infinity)
298 return false;
299
300 const l = this.dependencyTree.node(node) as INodeLabel | undefined;
301 const dirty = l === undefined ? true : l.dirty;
302
303 if (dirty)
304 this.context.debug(` import changed: ${node}`);
305
306 return dirty;
307 });
308 }
309
310 private makeName(id: string, snapshot: tsTypes.IScriptSnapshot)
311 {
312 const data = snapshot.getText(0, snapshot.getLength());
313 return sha1({ data, id });
314 }
315}