UNPKG

awesome-typescript-loader

Version:
364 lines (299 loc) 12.4 kB
import * as _ from 'lodash'; import * as util from 'util'; import * as path from 'path'; import * as Promise from 'bluebird'; import { State } from './host'; let objectAssign = require('object-assign'); type FileSet = {[fileName: string]: boolean}; export interface IResolver { (base: string, dep: string): Promise<string> } export interface IDependency { add(fileName: string): void; clear(): void } function isTypeDeclaration(fileName: string): boolean { return /\.d.ts$/.test(fileName); } function pathWithoutExt(fileName) { let extension = path.extname(fileName); return path.join( path.dirname(fileName), path.basename(fileName, extension) ); } function needRewrite(rewriteImports, importPath): boolean { return rewriteImports && _.any(rewriteImports.split(','), (i) => importPath.indexOf(i) !== -1) } function updateText(text, pos, end, newText): string { return text.slice(0, pos) + ` '${newText}'` + text.slice(end, text.length); } function isImportOrExportDeclaration(node: ts.Node) { return (!!(<any>node).exportClause || !!(<any>node).importClause) && (<any>node).moduleSpecifier; } function isImportEqualsDeclaration(node: ts.Node) { return !!(<any>node).moduleReference && (<any>node).moduleReference.hasOwnProperty('expression') } function isSourceFileDeclaration(node: ts.Node) { return !!(<any>node).referencedFiles } function isIgnoreDependency(absulutePath: string) { return absulutePath == '%%ignore'; } export class FileAnalyzer { dependencies = new DependencyManager(); validFiles = new ValidFilesManager(); state: State; constructor(state: State) { this.state = state; } checkDependencies(resolver: IResolver, fileName: string): Promise<boolean> { if (this.validFiles.isFileValid(fileName)) { return Promise.resolve(false); } this.dependencies.clearDependencies(fileName); let flow = this.state.hasFile(fileName) ? Promise.resolve(false) : this.state.readFileAndUpdate(fileName); this.validFiles.markFileValid(fileName); let wasChanged = false; return flow .then((changed) => { wasChanged = changed; return this.checkDependenciesInternal(resolver, fileName) }) .catch((err) => { this.validFiles.markFileInvalid(fileName); throw err }) .then(() => wasChanged); } private checkDependenciesInternal(resolver: IResolver, fileName: string): Promise<void> { let dependencies = this.findImportDeclarations(resolver, fileName) .then((deps) => { return deps.map(depFileName => { let result: Promise<string> = Promise.resolve(depFileName); let isDeclaration = isTypeDeclaration(depFileName); let isRequiredJs = /\.js$/.exec(depFileName) || depFileName.indexOf('.') === -1; if (isDeclaration) { let hasDeclaration = this.dependencies.hasTypeDeclaration(depFileName); if (!hasDeclaration) { this.dependencies.addTypeDeclaration(depFileName); return this.checkDependencies(resolver, depFileName).then(() => result) } } else if (isRequiredJs) { return Promise.resolve(null); } else { this.dependencies.addDependency(fileName, depFileName); return this.checkDependencies(resolver, depFileName); } return result; }); }); return Promise.all(dependencies).then((_) => {}); } private findImportDeclarations(resolver: IResolver, fileName: string): Promise<string[]> { let sourceFile = this.state.services.getSourceFile(fileName); let scriptSnapshot = (<any>sourceFile).scriptSnapshot.text; let isDeclaration = isTypeDeclaration(fileName); let rewrites: {pos: number, end: number, module: string}[] = []; let resolves: Promise<void>[] = []; let result = []; let visit = (node: ts.Node) => { if (!isDeclaration && isImportEqualsDeclaration(node)) { // we need this check to ensure that we have an external import let importPath = (<any>node).moduleReference.expression.text; resolves.push(this.resolve(resolver, fileName, importPath).then((absolutePath) => { if (needRewrite(this.state.options.rewriteImports, importPath)) { let { pos, end } = (<any>node).moduleReference.expression; let module = pathWithoutExt(absolutePath); rewrites.push({ pos, end, module }); } if (!isIgnoreDependency(absolutePath)) { result.push(absolutePath); } })); } else if (!isDeclaration && isImportOrExportDeclaration(node)) { let importPath = (<any>node).moduleSpecifier.text; resolves.push(this.resolve(resolver, fileName, importPath).then((absolutePath) => { if (needRewrite(this.state.options.rewriteImports, importPath)) { let module = pathWithoutExt(absolutePath); let { pos, end } = (<any>node).moduleSpecifier; rewrites.push({ pos, end, module }); } if (!isIgnoreDependency(absolutePath)) { result.push(absolutePath); } })); } else if (isSourceFileDeclaration(node)) { result = result.concat((<ts.SourceFile>node).referencedFiles.map(function (f) { return path.resolve(path.dirname((<ts.SourceFile>node).fileName), f.fileName); })); } this.state.ts.forEachChild(node, visit); }; visit(sourceFile); return Promise.all(resolves).then(() => { let orderedRewrites = (<any>_).sortByAll(rewrites, 'pos', 'end').reverse(); orderedRewrites.forEach(({ pos, end, module }) => { scriptSnapshot = updateText(scriptSnapshot, pos, end, module) }); this.state.updateFile(fileName, scriptSnapshot); return result; }); } resolve(resolver: IResolver, fileName: string, defPath: string): Promise<string> { let result; if (!path.extname(defPath).length) { result = resolver(path.dirname(fileName), defPath + ".ts") .error(function (error) { return resolver(path.dirname(fileName), defPath + ".d.ts") }) .error(function (error) { return resolver(path.dirname(fileName), defPath) }) .error(function (error) { // Node builtin modules try { if (require.resolve(defPath) == defPath) { return defPath; } else { throw error; } } catch (e) { throw error; } }) } else { // We don't need to resolve .d.ts here because they are already // absolute at this step. if (/\.d\.ts$/.test(defPath)) { result = Promise.resolve(defPath) } else { result = resolver(path.dirname(fileName), defPath) } } return result .error(function (error) { let detailedError: any = new ResolutionError(); detailedError.message = error.message + "\n Required in " + fileName; detailedError.cause = error; detailedError.fileName = fileName; throw detailedError; }) } } export interface IDependencyGraphItem { fileName: string; dependencies: IDependencyGraphItem[] } export class DependencyManager { dependencies: {[fileName: string]: string[]}; knownTypeDeclarations: FileSet; constructor(dependencies: {[fileName: string]: string[]} = {}, knownTypeDeclarations: FileSet = {}) { this.dependencies = dependencies; this.knownTypeDeclarations = knownTypeDeclarations; } clone(): DependencyManager { return new DependencyManager( _.cloneDeep(this.dependencies), _.cloneDeep(this.knownTypeDeclarations) ) } addDependency(fileName: string, depFileName: string): void { if (!this.dependencies.hasOwnProperty(fileName)) { this.clearDependencies(fileName); } this.dependencies[fileName].push(depFileName); } clearDependencies(fileName: string): void { this.dependencies[fileName] = [] } getDependencies(fileName: string): string[] { if (!this.dependencies.hasOwnProperty(fileName)) { this.clearDependencies(fileName); } return this.dependencies[fileName].slice() } addTypeDeclaration(fileName: string) { this.knownTypeDeclarations[fileName] = true } hasTypeDeclaration(fileName: string): boolean { return this.knownTypeDeclarations.hasOwnProperty(fileName) } getTypeDeclarations(): {[fileName: string]: boolean} { return objectAssign({}, this.knownTypeDeclarations); } 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[depFileName]) { appliedDeps[depFileName] = true; walk(depFileName, depContext); } }) }; walk(fileName, result); return result; } formatDependencyGraph(item: IDependencyGraphItem): string { let result = { buf: 'DEPENDENCY GRAPH FOR: ' + path.relative(process.cwd(), item.fileName) }; let walk = (item: IDependencyGraphItem, level: number, buf: typeof result) => { for (let i = 0; i < level; i++) { buf.buf = buf.buf + " " } buf.buf = buf.buf + path.relative(process.cwd(), item.fileName); buf.buf = buf.buf + "\n"; item.dependencies.forEach((dep) => walk(dep, level + 1, buf)) }; walk(item, 0, result); return result.buf += '\n\n'; } 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 { message: string; fileName: string; cause: Error; } util.inherits(ResolutionError, Error);