awesome-typescript-loader
Version:
Awesome TS loader for webpack
385 lines (325 loc) • 12.1 kB
text/typescript
/// <reference path='../node_modules/typescript/bin/typescriptServices.d.ts' />
/// <reference path='../typings/tsd.d.ts' />
import * as Promise from 'bluebird';
import * as path from 'path';
import * as fs from 'fs';
import * as _ from 'lodash';
import * as childProcess from 'child_process';
import * as colors from 'colors';
import { ICompilerOptions, TypeScriptCompilationError, State, ICompilerInfo } from './host';
import { IResolver, ResolutionError } from './deps';
import * as helpers from './helpers';
import { loadLib } from './helpers';
let loaderUtils = require('loader-utils');
interface ICompiler {
inputFileSystem: typeof fs;
_tsInstances: {[key:string]: ICompilerInstance};
options: {
externals: {
[ key: string ]: string
}
}
}
interface IWebPack {
_compiler: ICompiler;
cacheable: () => void;
query: string;
async: () => (err: Error, source?: string, map?: string) => void;
resourcePath: string;
resolve: () => void;
addDependency: (dep: string) => void;
clearDependencies: () => void;
}
interface ICompilerInstance {
tsFlow: Promise<any>;
tsState: State;
compiledFiles: {[key:string]: boolean};
options: ICompilerOptions;
externalsInvoked: boolean;
checker: any;
}
function getRootCompiler(compiler) {
if (compiler.parentCompilation) {
return getRootCompiler(compiler.parentCompilation.compiler)
} else {
return compiler;
}
}
function getInstanceStore(compiler): {[key:string]: ICompilerInstance} {
let store = getRootCompiler(compiler)._tsInstances;
if (store) {
return store
} else {
throw new Error('Can not resolve instance store')
}
}
function ensureInstanceStore(compiler) {
let rootCompiler = getRootCompiler(compiler);
if (!rootCompiler._tsInstances) {
rootCompiler._tsInstances = {};
}
}
function resolveInstance(compiler, instanceName) {
return getInstanceStore(compiler)[instanceName];
}
function createResolver(compiler: ICompiler, webpackResolver: any): IResolver {
let externals = compiler.options.externals;
let resolver = <IResolver>Promise.promisify(webpackResolver);
function resolve(base: string, dep: string): Promise<string> {
if (externals && externals.hasOwnProperty(dep)) {
return Promise.resolve<string>('%%ignore')
} else {
return resolver(base, dep)
}
}
return resolve;
}
function createChecker(compilerInfo: ICompilerInfo, compilerOptions: ICompilerOptions) {
let checker = childProcess.fork(path.join(__dirname, 'checker.js'));
checker.send({
messageType: 'init',
payload: {
compilerInfo: _.omit(compilerInfo, 'tsImpl'),
compilerOptions
}
}, null);
return checker;
}
const COMPILER_ERROR = colors.red(`\n\nTypescript compiler cannot be found, please add it to your package.json file:
npm install --save-dev typescript
`);
/**
* Creates compiler instance
*/
function ensureInstance(webpack: IWebPack, options: ICompilerOptions, instanceName: string): ICompilerInstance {
ensureInstanceStore(webpack._compiler);
let exInstance = resolveInstance(webpack._compiler, instanceName);
if (exInstance) {
return exInstance
}
let tsFlow = Promise.resolve();
let compilerName = options.compiler || 'typescript';
let compilerPath = path.dirname(compilerName);
if (compilerPath == '.') {
compilerPath = compilerName
}
let tsImpl: typeof ts;
try {
tsImpl = require(compilerName);
} catch (e) {
console.error(COMPILER_ERROR);
process.exit(1);
}
let compilerInfo: ICompilerInfo = {
compilerName,
compilerPath,
tsImpl,
lib5: loadLib(path.join(compilerPath, 'bin', 'lib.d.ts')),
lib6: loadLib(path.join(compilerPath, 'bin', 'lib.es6.d.ts'))
};
let configFileName = tsImpl.findConfigFile(options.tsconfig || process.cwd());
let configFile = null;
if (configFileName) {
configFile = tsImpl.readConfigFile(configFileName);
if (configFile.error) {
throw configFile.error;
}
if (configFile.config) {
_.extend(options, configFile.config.compilerOptions);
_.extend(options, configFile.config.awesomeTypescriptLoaderOptions);
}
}
if (typeof options.emitRequireType === 'undefined') {
options.emitRequireType = true;
} else {
if (typeof options.emitRequireType === 'string') {
options.emitRequireType = (<any>options.emitRequireType) === 'true'
}
}
if (typeof options.reEmitDependentFiles === 'undefined') {
options.reEmitDependentFiles = false;
} else {
if (typeof options.reEmitDependentFiles === 'string') {
options.reEmitDependentFiles = (<any>options.reEmitDependentFiles) === 'true'
}
}
if (typeof options.doTypeCheck === 'undefined') {
options.doTypeCheck = true;
} else {
if (typeof options.doTypeCheck === 'string') {
options.doTypeCheck = (<any>options.doTypeCheck) === 'true'
}
}
if (typeof options.forkChecker === 'undefined') {
options.forkChecker = false;
} else {
if (typeof options.forkChecker === 'string') {
options.forkChecker = (<any>options.forkChecker) === 'true'
}
}
if (typeof options.useWebpackText === 'undefined') {
options.useWebpackText = false;
} else {
if (typeof options.useWebpackText === 'string') {
options.useWebpackText = (<any>options.useWebpackText) === 'true'
}
}
if (typeof options.rewriteImports == 'undefined') {
options.rewriteImports = '';
}
if (options.target) {
options.target = helpers.parseOptionTarget(<any>options.target, tsImpl);
}
let tsState = new State(options, webpack._compiler.inputFileSystem, compilerInfo);
let compiler = (<any>webpack._compiler);
compiler.plugin('watch-run', (watching, callback) => {
let resolver = createResolver(watching.compiler, watching.compiler.resolvers.normal.resolve);
let instance: ICompilerInstance = resolveInstance(watching.compiler, instanceName);
let state = instance.tsState;
let mtimes = watching.compiler.watchFileSystem.watcher.mtimes;
let changedFiles = Object.keys(mtimes);
changedFiles.forEach((changedFile) => {
state.fileAnalyzer.validFiles.markFileInvalid(changedFile);
});
Promise.all(changedFiles.map((changedFile) => {
if (/\.ts$|\.d\.ts|\.tsx$/.test(changedFile)) {
return state.readFileAndUpdate(changedFile).then(() => {
return state.fileAnalyzer.checkDependencies(resolver, changedFile);
});
} else {
return Promise.resolve()
}
}))
.then(_ => { state.updateProgram(); callback(); })
.catch(ResolutionError, err => {
console.error(err.message);
callback();
})
.catch((err) => { console.log(err); callback() })
});
if (options.doTypeCheck) {
compiler.plugin('after-compile', function(compilation, callback) {
let instance: ICompilerInstance = resolveInstance(compilation.compiler, instanceName);
let state = instance.tsState;
if (options.forkChecker) {
let payload = {
files: state.files
};
console.time('\nSending files to the checker');
instance.checker.send({
messageType: 'compile',
payload
})
console.timeEnd('\nSending files to the checker');
} else {
let diagnostics = state.ts.getPreEmitDiagnostics(state.program);
let emitError = (err) => {
if (compilation.bail) {
console.error('Error in bail mode:', err);
process.exit(1);
}
compilation.errors.push(new Error(err))
};
let errors = helpers.formatErrors(diagnostics);
errors.forEach(emitError);
}
let phantomImports = [];
Object.keys(state.files).forEach((fileName) => {
if (!instance.compiledFiles[fileName]) {
phantomImports.push(fileName)
}
});
instance.compiledFiles = {};
compilation.fileDependencies.push.apply(compilation.fileDependencies, phantomImports);
compilation.fileDependencies = _.uniq(compilation.fileDependencies);
callback();
});
}
return getInstanceStore(webpack._compiler)[instanceName] = {
tsFlow,
tsState,
compiledFiles: {},
options,
externalsInvoked: false,
checker: options.forkChecker
? createChecker(compilerInfo, options)
: null
}
}
function loader(text) {
compiler.call(undefined, this, text)
}
function compiler(webpack: IWebPack, text: string): void {
if (webpack.cacheable) {
webpack.cacheable();
}
let options = <ICompilerOptions>loaderUtils.parseQuery(webpack.query);
let instanceName = options.instanceName || 'default';
let instance = ensureInstance(webpack, options, instanceName);
let state = instance.tsState;
let callback = webpack.async();
let fileName = webpack.resourcePath;
let resolver = createResolver(webpack._compiler, webpack.resolve);
let depsInjector = {
add: (depFileName) => {webpack.addDependency(depFileName)},
clear: webpack.clearDependencies.bind(webpack)
};
let applyDeps = _.once(() => {
depsInjector.clear();
depsInjector.add(fileName);
if (state.options.reEmitDependentFiles) {
state.fileAnalyzer.dependencies.applyChain(fileName, depsInjector);
}
});
if (options.externals && !instance.externalsInvoked) {
instance.externalsInvoked = true;
instance.tsFlow = instance.tsFlow.then(
<any>Promise.all(options.externals.split(',').map(external => {
return state.fileAnalyzer.checkDependencies(resolver, external);
}))
);
}
instance.tsFlow = instance.tsFlow
.then(() => {
instance.compiledFiles[fileName] = true;
let doUpdate = false;
if (instance.options.useWebpackText) {
if(state.updateFile(fileName, text, true)) {
doUpdate = true;
}
}
return state.fileAnalyzer.checkDependencies(resolver, fileName).then((wasChanged) => {
if (doUpdate || wasChanged) {
state.updateProgram();
}
});
})
.then(() => {
return state.emit(fileName)
})
.then(output => {
let result = helpers.findResultFor(output, fileName);
if (result.text === undefined) {
throw new Error('no output found for ' + fileName);
}
let sourceMap = JSON.parse(result.sourceMap);
sourceMap.sources = [ fileName ];
sourceMap.file = fileName;
sourceMap.sourcesContent = [ text ];
applyDeps();
try {
callback(null, result.text, sourceMap);
} catch (e) {
console.error('Error in bail mode:', e);
process.exit(1);
}
})
.finally(() => {
applyDeps();
})
.catch(ResolutionError, err => {
callback(err, helpers.codegenErrorReport([err]));
})
.catch((err) => { callback(err) })
}
export = loader;