UNPKG

awesome-typescript-loader

Version:
385 lines (325 loc) 12.1 kB
/// <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;