263 lines
7.5 KiB
TypeScript

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<ICode | undefined>;
private typesCache: ICache<string>;
private semanticDiagnosticsCache: ICache<IDiagnostics[]>;
private syntacticDiagnosticsCache: ICache<IDiagnostics[]>;
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<IDiagnostics[]>, 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<ICode>(`${this.cacheDir}/code`, true);
this.typesCache = new RollingCache<string>(`${this.cacheDir}/types`, true);
this.syntacticDiagnosticsCache = new RollingCache<IDiagnostics[]>(`${this.cacheDir}/syntacticDiagnostics`, true);
this.semanticDiagnosticsCache = new RollingCache<IDiagnostics[]>(`${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 });
}
}