awesome-typescript-loader
Version:
Awesome TS loader for webpack
467 lines (396 loc) • 14 kB
text/typescript
import { State } from './host';
import * as fs from 'fs';
import * as path from 'path';
import * as _ from 'lodash';
import { formatError, toUnix } from './helpers';
import { ICompilerInfo } from './host';
import { createChecker } from './checker';
let colors = require('colors/safe');
let pkg = require('../package.json');
export interface LoaderPlugin {
processProgram?: (program: ts.Program) => void;
}
export interface LoaderPluginDef {
file: string;
options: any;
}
export interface ICompilerInstance {
id: number;
tsState: State;
babelImpl?: any;
compiledFiles: {[key:string]: boolean};
configFilePath: string;
compilerConfig: TsConfig;
loaderConfig: LoaderConfig;
externalsInvoked: boolean;
checker: any;
cacheIdentifier: any;
plugins: LoaderPluginDef[];
initedPlugins: LoaderPlugin[];
shouldUpdateProgram: boolean;
}
interface ICompiler {
inputFileSystem: typeof fs;
_tsInstances: {[key:string]: ICompilerInstance};
_tsFork: boolean;
options: {
externals: {
[ key: string ]: string
}
};
}
export 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;
emitFile: (fileName: string, text: string) => void;
options: {
atl?: {
plugins: LoaderPluginDef[]
}
};
}
export interface LoaderConfig {
instanceName?: string;
showRecompileReason?: boolean;
compiler?: string;
emitRequireType?: boolean;
reEmitDependentFiles?: boolean;
tsconfig?: string;
tsconfigContent?: string;
useWebpackText?: boolean;
externals?: string[];
doTypeCheck?: boolean;
ignoreDiagnostics?: number[];
forkChecker?: boolean;
forkCheckerSilent?: boolean;
useBabel?: boolean;
babelCore?: string;
babelOptions?: any;
usePrecompiledFiles?: boolean;
skipDeclarationFilesCheck?: boolean;
useCache?: boolean;
cacheDirectory?: string;
disableFastEmit?: boolean;
}
export type QueryOptions = LoaderConfig & ts.CompilerOptions;
export type TsConfig = ts.ParsedCommandLine;
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];
}
const COMPILER_ERROR = colors.red(`\n\nTypescript compiler cannot be found, please add it to your package.json file:
npm install --save-dev typescript
`);
const BABEL_ERROR = colors.red(`\n\nBabel compiler cannot be found, please add it to your package.json file:
npm install --save-dev babel-core
`);
/**
* Creates compiler instance
*/
let id = 0;
export function ensureInstance(webpack: IWebPack, query: QueryOptions, instanceName: string): ICompilerInstance {
ensureInstanceStore(webpack._compiler);
let exInstance = resolveInstance(webpack._compiler, instanceName);
if (exInstance) {
return exInstance;
}
let compilerInfo = setupTs(query.compiler);
let { tsImpl } = compilerInfo;
let { configFilePath, compilerConfig, loaderConfig } = readConfigFile(process.cwd(), query, tsImpl);
applyDefaults(compilerConfig, loaderConfig);
let babelImpl = setupBabel(loaderConfig);
let cacheIdentifier = setupCache(loaderConfig, tsImpl, webpack, babelImpl);
let forkChecker = loaderConfig.forkChecker && getRootCompiler(webpack._compiler)._tsFork;
let tsState = new State(
loaderConfig,
configFilePath,
compilerConfig,
compilerInfo
);
let compiler = (<any>webpack._compiler);
setupWatchRun(compiler, instanceName);
if (loaderConfig.doTypeCheck) {
setupAfterCompile(compiler, instanceName, forkChecker);
}
let webpackOptions = _.pick(webpack._compiler.options, 'resolve');
let atlOptions = webpack.options.atl;
let plugins: LoaderPluginDef[] = [];
if (atlOptions && atlOptions.plugins) {
plugins = atlOptions.plugins;
}
let initedPlugins = [];
if (!forkChecker) {
initedPlugins = plugins.map(plugin => {
return require(plugin.file)(plugin.options);
});
}
return getInstanceStore(webpack._compiler)[instanceName] = {
id: ++id,
tsState,
babelImpl,
compiledFiles: {},
loaderConfig,
configFilePath,
compilerConfig,
externalsInvoked: false,
checker: forkChecker
? createChecker(
compilerInfo,
loaderConfig,
compilerConfig.options,
webpackOptions,
tsState.defaultLib,
plugins)
: null,
cacheIdentifier,
plugins,
initedPlugins,
shouldUpdateProgram: true
};
}
export function setupTs(compiler: string): ICompilerInfo {
let compilerPath = compiler || 'typescript';
let tsImpl: typeof ts;
let tsImplPath: string;
try {
tsImplPath = require.resolve(compilerPath);
tsImpl = require(tsImplPath);
} catch (e) {
console.error(e);
console.error(COMPILER_ERROR);
process.exit(1);
}
let compilerInfo: ICompilerInfo = {
compilerPath,
tsImpl,
};
return compilerInfo;
}
function setupCache(loaderConfig: LoaderConfig, tsImpl: typeof ts, webpack: IWebPack, babelImpl: any) {
let cacheIdentifier = null;
if (loaderConfig.useCache) {
if (!loaderConfig.cacheDirectory) {
loaderConfig.cacheDirectory = path.join(process.cwd(), '.awcache');
}
if (!fs.existsSync(loaderConfig.cacheDirectory)) {
fs.mkdirSync(loaderConfig.cacheDirectory);
}
cacheIdentifier = {
'typescript': tsImpl.version,
'awesome-typescript-loader': pkg.version,
'awesome-typescript-loader-query': webpack.query,
'babel-core': babelImpl
? babelImpl.version
: null
};
}
}
function setupBabel(loaderConfig: LoaderConfig): any {
let babelImpl: any;
if (loaderConfig.useBabel) {
try {
let babelPath = loaderConfig.babelCore || path.join(process.cwd(), 'node_modules', 'babel-core');
babelImpl = require(babelPath);
} catch (e) {
console.error(BABEL_ERROR);
process.exit(1);
}
}
return babelImpl;
}
function applyDefaults(compilerConfig: TsConfig, loaderConfig: LoaderConfig) {
compilerConfig.typingOptions.exclude = compilerConfig.typingOptions.exclude || [];
let initialFiles = compilerConfig.fileNames;
_.defaults(compilerConfig.options, {
sourceMap: true,
verbose: false,
skipDefaultLibCheck: true,
suppressOutputPathCheck: true,
});
_.defaults(compilerConfig.options, {
sourceRoot: compilerConfig.options.sourceMap ? process.cwd() : undefined
});
_.defaults(loaderConfig, {
externals: [],
doTypeCheck: true,
sourceMap: true,
verbose: false,
});
delete compilerConfig.options.outDir;
delete compilerConfig.options.outFile;
delete compilerConfig.options.out;
delete compilerConfig.options.noEmit;
loaderConfig.externals.push.apply(loaderConfig.externals, initialFiles);
}
export interface Configs {
configFilePath: string;
compilerConfig: TsConfig;
loaderConfig: LoaderConfig;
}
export function readConfigFile(baseDir: string, query: QueryOptions, tsImpl: typeof ts): Configs {
let configFilePath: string;
if (query.tsconfig && query.tsconfig.match(/\.json$/)) {
configFilePath = query.tsconfig;
} else {
configFilePath = tsImpl.findConfigFile(process.cwd(), tsImpl.sys.fileExists);
}
let existingOptions = tsImpl.convertCompilerOptionsFromJson(query, process.cwd(), 'atl.query');
if (!configFilePath || query.tsconfigContent) {
return {
configFilePath: configFilePath || process.cwd(),
compilerConfig: tsImpl.parseJsonConfigFileContent(
query.tsconfigContent || {},
tsImpl.sys,
process.cwd(),
_.extend({}, tsImpl.getDefaultCompilerOptions(), existingOptions.options) as ts.CompilerOptions,
process.cwd()
),
loaderConfig: query as LoaderConfig
};
}
let jsonConfigFile = tsImpl.readConfigFile(configFilePath, tsImpl.sys.readFile);
let compilerConfig = tsImpl.parseJsonConfigFileContent(
jsonConfigFile.config,
tsImpl.sys,
process.cwd(),
existingOptions.options,
configFilePath
);
return {
configFilePath,
compilerConfig,
loaderConfig: _.defaults<LoaderConfig, LoaderConfig>(
query,
jsonConfigFile.config.awesomeTypescriptLoaderOptions)
};
}
let EXTENSIONS = /\.tsx?$|\.jsx?$/;
function setupWatchRun(compiler, instanceName: string) {
compiler.plugin('watch-run', function (watching, callback) {
let instance = resolveInstance(watching.compiler, instanceName);
let state = instance.tsState;
let mtimes = watching.compiler.watchFileSystem.watcher.mtimes;
let changedFiles = Object.keys(mtimes).map(toUnix);
changedFiles.forEach((changedFile) => {
state.fileAnalyzer.validFiles.markFileInvalid(changedFile);
});
try {
changedFiles.forEach(changedFile => {
if (EXTENSIONS.test(changedFile)) {
if (state.hasFile(changedFile)) {
state.readFileAndUpdate(changedFile);
state.fileAnalyzer.checkDependencies(changedFile);
}
}
});
if (!state.loaderConfig.forkChecker) {
state.updateProgram();
}
callback();
} catch (err) {
console.error(err.toString());
callback();
}
});
}
let runChecker = (instance, payload) => {
instance.checker.send({
messageType: 'compile',
payload
});
};
runChecker = _.debounce(runChecker, 200);
function setupAfterCompile(compiler, instanceName, forkChecker = false) {
compiler.plugin('after-compile', function(compilation, callback) {
// Don't add errors for child compilations
if (compilation.compiler.isChild()) {
callback();
return;
}
let instance: ICompilerInstance = resolveInstance(compilation.compiler, instanceName);
let state = instance.tsState;
let deps = state.fileAnalyzer.dependencies;
if (forkChecker) {
let payload = {
files: state.allFiles(),
moduleResolutionCache: deps.moduleResolutions,
typeReferenceResolutionCache: deps.typeReferenceResolutions
};
runChecker(instance, payload);
} else {
if (!state.program || instance.shouldUpdateProgram) {
// program may be undefined here, if all files
// will be loaded by tsconfig
state.updateProgram();
instance.shouldUpdateProgram = false;
}
let diagnostics = state.ts.getPreEmitDiagnostics(state.program);
let emitError = (msg) => {
if (compilation.bail) {
console.error('Error in bail mode:', msg);
process.exit(1);
}
compilation.errors.push(new Error(msg));
};
let { loaderConfig: { ignoreDiagnostics } } = instance;
diagnostics
.filter(err => !ignoreDiagnostics || ignoreDiagnostics.indexOf(err.code) == -1)
.map(err => `[${ instanceName }] ` + formatError(err))
.forEach(emitError);
instance.initedPlugins.forEach(plugin => {
plugin.processProgram(state.program);
});
}
let phantomImports = [];
state.allFileNames().forEach((fileName) => {
if (!instance.compiledFiles[fileName]) {
phantomImports.push(fileName);
}
});
if (instance.compilerConfig.options.declaration) {
phantomImports.forEach(imp => {
let output = instance.tsState.services.getEmitOutput(imp);
let declarationFile = output.outputFiles.filter(filePath =>
!!filePath.name.match(/\.d.ts$/))[0];
if (declarationFile) {
let assetPath = path.normalize(path.relative(process.cwd(), declarationFile.name));
compilation.assets[assetPath] = {
source: () => declarationFile.text,
size: () => declarationFile.text.length
};
}
});
}
instance.compiledFiles = {};
let fileDeps = compilation.fileDependencies;
fileDeps.push.apply(fileDeps, phantomImports.map(path.normalize));
compilation.fileDependencies = _.uniq(fileDeps);
callback();
});
}