import { IContext } from "./context"; import {Graph, alg} from "graphlib"; import {sha1} from "object-hash"; import { RollingCache } from "./rollingcache"; import { ICache } from "./icache"; import {map, endsWith, filter, each, some} from "lodash"; import {Diagnostic, DiagnosticCategory, IScriptSnapshot, OutputFile, LanguageServiceHost, version, getAutomaticTypeDirectiveNames, sys, resolveTypeReferenceDirective, flattenDiagnosticMessageText, CompilerOptions} from "typescript"; import {blue, yellow, green} from "colors/safe"; import {emptyDirSync} from "fs-extra"; export interface ICode { code: string | undefined; map: string | undefined; dts?: OutputFile | undefined; } interface INodeLabel { dirty: boolean; } export interface IDiagnostics { flatMessage: string; fileLine?: string; category: DiagnosticCategory; code: number; type: string; } interface ITypeSnapshot { id: string; snapshot: IScriptSnapshot | undefined; } export function convertDiagnostic(type: string, data: Diagnostic[]): IDiagnostics[] { return map(data, (diagnostic) => { const entry: IDiagnostics = { flatMessage: flattenDiagnosticMessageText(diagnostic.messageText, "\n"), category: diagnostic.category, code: diagnostic.code, type, }; if (diagnostic.file && diagnostic.start !== undefined) { const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); entry.fileLine = `${diagnostic.file.fileName} (${line + 1},${character + 1})`; } return entry; }); } export class TsCache { private cacheVersion = "6"; private dependencyTree: Graph; private ambientTypes: ITypeSnapshot[]; private ambientTypesDirty = false; private cacheDir: string; private codeCache: ICache; private typesCache: ICache; private semanticDiagnosticsCache: ICache; private syntacticDiagnosticsCache: ICache; constructor(private host: LanguageServiceHost, cache: string, private options: CompilerOptions, private rollupConfig: any, rootFilenames: string[], private context: IContext) { this.cacheDir = `${cache}/${sha1({ version: this.cacheVersion, rootFilenames, options: this.options, rollupConfig: this.rollupConfig, tsVersion : version, })}`; this.dependencyTree = new Graph({ directed: true }); this.dependencyTree.setDefaultNodeLabel((_node: string) => ({ dirty: false }) ); const automaticTypes = map(getAutomaticTypeDirectiveNames(options, sys), (entry) => resolveTypeReferenceDirective(entry, undefined, options, sys)) .filter((entry) => entry.resolvedTypeReferenceDirective && entry.resolvedTypeReferenceDirective.resolvedFileName) .map((entry) => entry.resolvedTypeReferenceDirective!.resolvedFileName!); this.ambientTypes = filter(rootFilenames, (file) => endsWith(file, ".d.ts")) .concat(automaticTypes) .map((id) => ({ id, snapshot: this.host.getScriptSnapshot(id) })); this.init(); this.checkAmbientTypes(); } public clean() { this.context.info(blue(`cleaning cache: ${this.cacheDir}`)); emptyDirSync(this.cacheDir); this.init(); } public setDependency(importee: string, importer: string): void { // importee -> importer this.context.debug(`${blue("dependency")} '${importee}'`); this.context.debug(` imported by '${importer}'`); this.dependencyTree.setEdge(importer, importee); } public walkTree(cb: (id: string) => void | false): void { const acyclic = alg.isAcyclic(this.dependencyTree); if (acyclic) { each(alg.topsort(this.dependencyTree), (id: string) => cb(id)); return; } this.context.info(yellow("import tree has cycles")); each(this.dependencyTree.nodes(), (id: string) => cb(id)); } public done() { this.context.info(blue("rolling caches")); this.codeCache.roll(); this.semanticDiagnosticsCache.roll(); this.syntacticDiagnosticsCache.roll(); this.typesCache.roll(); } public getCompiled(id: string, snapshot: IScriptSnapshot, transform: () => ICode | undefined): ICode | undefined { const name = this.makeName(id, snapshot); this.context.info(`${blue("transpiling")} '${id}'`); this.context.debug(` cache: '${this.codeCache.path(name)}'`); if (!this.codeCache.exists(name) || this.isDirty(id, false)) { this.context.debug(yellow(" cache miss")); const transformedData = transform(); this.codeCache.write(name, transformedData); this.markAsDirty(id); return transformedData; } this.context.debug(green(" cache hit")); const data = this.codeCache.read(name); this.codeCache.write(name, data); return data; } public getSyntacticDiagnostics(id: string, snapshot: IScriptSnapshot, check: () => Diagnostic[]): IDiagnostics[] { return this.getDiagnostics("syntax", this.syntacticDiagnosticsCache, id, snapshot, check); } public getSemanticDiagnostics(id: string, snapshot: IScriptSnapshot, check: () => Diagnostic[]): IDiagnostics[] { return this.getDiagnostics("semantic", this.semanticDiagnosticsCache, id, snapshot, check); } private checkAmbientTypes(): void { this.context.debug(blue("Ambient types:")); const typeNames = filter(this.ambientTypes, (snapshot) => snapshot.snapshot !== undefined) .map((snapshot) => { this.context.debug(` ${snapshot.id}`); return this.makeName(snapshot.id, snapshot.snapshot!); }); // types dirty if any d.ts changed, added or removed this.ambientTypesDirty = !this.typesCache.match(typeNames); if (this.ambientTypesDirty) this.context.info(yellow("ambient types changed, redoing all semantic diagnostics")); each(typeNames, (name) => this.typesCache.touch(name)); } private getDiagnostics(type: string, cache: ICache, id: string, snapshot: IScriptSnapshot, check: () => Diagnostic[]): IDiagnostics[] { const name = this.makeName(id, snapshot); this.context.debug(` cache: '${cache.path(name)}'`); if (!cache.exists(name) || this.isDirty(id, true)) { this.context.debug(yellow(" cache miss")); const convertedData = convertDiagnostic(type, check()); cache.write(name, convertedData); this.markAsDirty(id); return convertedData; } this.context.debug(green(" cache hit")); const data = cache.read(name); cache.write(name, data); return data; } private init() { this.codeCache = new RollingCache(`${this.cacheDir}/code`, true); this.typesCache = new RollingCache(`${this.cacheDir}/types`, true); this.syntacticDiagnosticsCache = new RollingCache(`${this.cacheDir}/syntacticDiagnostics`, true); this.semanticDiagnosticsCache = new RollingCache(`${this.cacheDir}/semanticDiagnostics`, true); } private markAsDirty(id: string): void { this.dependencyTree.setNode(id, { dirty: true }); } // returns true if node or any of its imports or any of global types changed private isDirty(id: string, checkImports: boolean): boolean { const label = this.dependencyTree.node(id) as INodeLabel; if (!label) return false; if (!checkImports || label.dirty) return label.dirty; if (this.ambientTypesDirty) return true; const dependencies = alg.dijkstra(this.dependencyTree, id); return some(dependencies, (dependency, node) => { if (!node || dependency.distance === Infinity) return false; const l = this.dependencyTree.node(node) as INodeLabel | undefined; const dirty = l === undefined ? true : l.dirty; if (dirty) this.context.debug(` import changed: ${node}`); return dirty; }); } private makeName(id: string, snapshot: IScriptSnapshot) { const data = snapshot.getText(0, snapshot.getLength()); return sha1({ data, id }); } }