awesome-typescript-loader
Version:
Awesome TS loader for webpack
266 lines (215 loc) • 8.31 kB
text/typescript
import { State } from './host';
type FileSet = {[fileName: string]: boolean};
export interface IDependency {
add(fileName: string): void;
clear(): void;
}
export interface IExternals {
[key: string]: string;
}
export type Exclude = string[];
export function isTypeDeclaration(fileName: string): boolean {
return /\.d.ts$/.test(fileName);
}
export class FileAnalyzer {
dependencies = new DependencyManager();
validFiles = new ValidFilesManager();
state: State;
constructor(state: State) {
this.state = state;
}
checkDependencies(fileName: string, isDefaultLib = false): boolean {
let isValid = this.validFiles.isFileValid(fileName);
if (isValid) {
return isValid;
}
this.validFiles.markFileValid(fileName);
this.dependencies.clearDependencies(fileName);
let changed = false;
try {
if (!this.state.hasFile(fileName)) {
this.state.readFileAndAdd(fileName, isDefaultLib);
changed = true;
}
this.checkDependenciesInternal(fileName);
} catch (err) {
this.validFiles.markFileInvalid(fileName);
throw err;
}
return changed;
}
checkDependenciesInternal(fileName: string): void {
let imports = this.findImportDeclarations(fileName);
imports.forEach(imp => {
this.dependencies.addDependency(fileName, imp);
this.checkDependencies(imp);
});
return null;
}
findImportDeclarations(fileName: string) {
let sourceFile = this.state.getSourceFile(fileName);
let isJavaScript = sourceFile.flags & this.state.ts.NodeFlags.JavaScriptFile;
let info = this.state.ts.preProcessFile(sourceFile.text, true, !!isJavaScript);
let options = this.state.compilerConfig.options;
let ts = this.state.ts;
let deps = this.state.fileAnalyzer.dependencies;
let imports: string[] = [];
imports.push.apply(imports, info.importedFiles
.map(file => file.fileName)
.map(depName => {
let { resolvedModule } = ts.resolveModuleName(depName, fileName, options, ts.sys);
if (resolvedModule) {
deps.addModuleResolution(fileName, depName, resolvedModule);
return resolvedModule.resolvedFileName;
}
})
.filter(Boolean));
imports.push.apply(imports, info.referencedFiles
.map(file => file.fileName)
.map(depName => {
return /^[a-z0-9].*\.d\.ts$/.test(depName) ? './' + depName : depName;
})
.map(depName => {
let { resolvedModule } = ts.resolveModuleName(depName, fileName, options, ts.sys);
if (resolvedModule) {
deps.addModuleResolution(fileName, depName, resolvedModule);
return resolvedModule.resolvedFileName;
}
})
.filter(Boolean));
if (info.typeReferenceDirectives) {
imports.push.apply(imports, info.typeReferenceDirectives
.map(file => file.fileName)
.map(depName => {
let { resolvedTypeReferenceDirective } = ts.resolveTypeReferenceDirective(depName, fileName, options, ts.sys);
if (resolvedTypeReferenceDirective) {
deps.addTypeReferenceResolution(fileName, depName, resolvedTypeReferenceDirective);
return resolvedTypeReferenceDirective.resolvedFileName;
}
})
.filter(Boolean));
}
return imports;
}
}
export interface IDependencyGraphItem {
fileName: string;
dependencies: IDependencyGraphItem[];
}
export class DependencyManager {
dependencies: {[fileName: string]: string[]};
moduleResolutions: {[cacheKey: string]: ts.ResolvedModule};
typeReferenceResolutions: {[cacheKey: string]: ts.ResolvedTypeReferenceDirective};
compiledModules: {[fileName: string]: string[]};
constructor() {
this.dependencies = {};
this.compiledModules = {};
this.moduleResolutions = {};
this.typeReferenceResolutions = {};
}
addModuleResolution(fileName: string, depName: string, resolvedModule: ts.ResolvedModule) {
this.moduleResolutions[`${fileName}::${depName}`] = resolvedModule;
}
addTypeReferenceResolution(fileName: string, depName: string, resolvedModule: ts.ResolvedTypeReferenceDirective) {
this.typeReferenceResolutions[`${fileName}::${depName}`] = resolvedModule;
}
getModuleResolution(fileName: string, depName: string): ts.ResolvedModule {
return this.moduleResolutions[`${fileName}::${depName}`];
}
getTypeReferenceResolution(fileName: string, depName: string): ts.ResolvedTypeReferenceDirective {
return this.typeReferenceResolutions[`${fileName}::${depName}`];
}
addDependency(fileName: string, dep: string): void {
if (!this.dependencies.hasOwnProperty(fileName)) {
this.clearDependencies(fileName);
}
this.dependencies[fileName].push(dep);
}
addCompiledModule(fileName: string, depFileName: string): void {
if (!this.compiledModules.hasOwnProperty(fileName)) {
this.clearCompiledModules(fileName);
}
let store = this.compiledModules[fileName];
if (store.indexOf(depFileName) === -1) {
store.push(depFileName);
}
}
clearDependencies(fileName: string): void {
this.dependencies[fileName] = [];
}
clearCompiledModules(fileName: string): void {
this.compiledModules[fileName] = [];
}
getDependencies(fileName: string): string[] {
if (!this.dependencies.hasOwnProperty(fileName)) {
this.clearDependencies(fileName);
}
return this.dependencies[fileName].slice();
}
getDependencyGraph(fileName: string): IDependencyGraphItem {
let appliedDeps: {[fileName: string]: boolean} = {};
let result: IDependencyGraphItem = {
fileName,
dependencies: []
};
let walk = (fileName: string, context: IDependencyGraphItem) => {
this.getDependencies(fileName).forEach((depFileName) => {
let depContext = {
fileName: depFileName,
dependencies: []
};
context.dependencies.push(depContext);
if (!appliedDeps[fileName]) {
appliedDeps[fileName] = true;
walk(fileName, depContext);
}
});
};
walk(fileName, result);
return result;
}
applyCompiledFiles(fileName: string, deps: IDependency) {
if (!this.compiledModules.hasOwnProperty(fileName)) {
this.clearCompiledModules(fileName);
}
this.compiledModules[fileName].forEach((mod) => {
deps.add(mod);
});
}
applyChain(fileName: string, deps: IDependency) {
if (!this.dependencies.hasOwnProperty(fileName)) {
this.clearDependencies(fileName);
}
let appliedDeps: FileSet = {};
let graph = this.getDependencyGraph(fileName);
let walk = (item: IDependencyGraphItem) => {
let itemFileName = item.fileName;
if (!appliedDeps[itemFileName]) {
appliedDeps[itemFileName] = true;
deps.add(itemFileName);
item.dependencies.forEach((dep) => walk(dep));
}
};
walk(graph);
}
}
export class ValidFilesManager {
files: {[fileName: string]: boolean} = {};
isFileValid(fileName: string): boolean {
return this.files[fileName];
}
markFileValid(fileName: string) {
this.files[fileName] = true;
}
markFileInvalid(fileName: string) {
this.files[fileName] = false;
}
}
/**
* Emit compilation result for a specified fileName.
*/
export class ResolutionError extends Error {
message: string;
fileName: string;
cause: Error;
}