awesome-typescript-loader
Version:
Awesome TS loader for webpack
364 lines (299 loc) • 12.4 kB
text/typescript
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);