blob: 995b09d8d7c26ec50069dedfa79cfecac237bcbc [file] [log] [blame]
"use strict";
const loaderUtils = require("loader-utils");
const path = require("path");
const constants = require("./constants");
const instances_1 = require("./instances");
const utils_1 = require("./utils");
const webpackInstances = [];
const loaderOptionsCache = {};
/**
* The entry point for ts-loader
*/
function loader(contents) {
// tslint:disable-next-line:no-unused-expression strict-boolean-expressions
this.cacheable && this.cacheable();
const callback = this.async();
const options = getLoaderOptions(this);
const instanceOrError = instances_1.getTypeScriptInstance(options, this);
if (instanceOrError.error !== undefined) {
callback(new Error(instanceOrError.error.message));
return;
}
return successLoader(this, contents, callback, options, instanceOrError.instance);
}
function successLoader(loaderContext, contents, callback, options, instance) {
const rawFilePath = path.normalize(loaderContext.resourcePath);
const filePath = options.appendTsSuffixTo.length > 0 || options.appendTsxSuffixTo.length > 0
? utils_1.appendSuffixesIfMatch({
'.ts': options.appendTsSuffixTo,
'.tsx': options.appendTsxSuffixTo
}, rawFilePath)
: rawFilePath;
const fileVersion = updateFileInCache(options, filePath, contents, instance);
const referencedProject = utils_1.getAndCacheProjectReference(filePath, instance);
if (referencedProject !== undefined) {
const [relativeProjectConfigPath, relativeFilePath] = [
path.relative(loaderContext.rootContext, referencedProject.sourceFile.fileName),
path.relative(loaderContext.rootContext, filePath)
];
if (referencedProject.commandLine.options.outFile !== undefined) {
throw new Error(`The referenced project at ${relativeProjectConfigPath} is using ` +
`the outFile' option, which is not supported with ts-loader.`);
}
const jsFileName = utils_1.getAndCacheOutputJSFileName(filePath, referencedProject, instance);
const relativeJSFileName = path.relative(loaderContext.rootContext, jsFileName);
if (!instance.compiler.sys.fileExists(jsFileName)) {
throw new Error(`Could not find output JavaScript file for input ` +
`${relativeFilePath} (looked at ${relativeJSFileName}).\n` +
`The input file is part of a project reference located at ` +
`${relativeProjectConfigPath}, so ts-loader is looking for the ` +
'project’s pre-built output on disk. Try running `tsc --build` ' +
'to build project references.');
}
// Since the output JS file is being read from disk instead of using the
// input TS file, we need to tell the loader that the compilation doesn’t
// actually depend on the current file, but depends on the JS file instead.
loaderContext.clearDependencies();
loaderContext.addDependency(jsFileName);
utils_1.validateSourceMapOncePerProject(instance, loaderContext, jsFileName, referencedProject);
const mapFileName = jsFileName + '.map';
const outputText = instance.compiler.sys.readFile(jsFileName);
const sourceMapText = instance.compiler.sys.readFile(mapFileName);
makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, options, fileVersion, callback);
}
else {
const { outputText, sourceMapText } = options.transpileOnly
? getTranspilationEmit(filePath, contents, instance, loaderContext)
: getEmit(rawFilePath, filePath, instance, loaderContext);
makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, options, fileVersion, callback);
}
}
function makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, options, fileVersion, callback) {
if (outputText === null || outputText === undefined) {
const additionalGuidance = !options.allowTsInNodeModules && filePath.indexOf('node_modules') !== -1
? ' By default, ts-loader will not compile .ts files in node_modules.\n' +
'You should not need to recompile .ts files there, but if you really want to, use the allowTsInNodeModules option.\n' +
'See: https://github.com/Microsoft/TypeScript/issues/12358'
: '';
throw new Error(`TypeScript emitted no output for ${filePath}.${additionalGuidance}`);
}
const { sourceMap, output } = makeSourceMap(sourceMapText, outputText, filePath, contents, loaderContext);
// _module.meta is not available inside happypack
if (!options.happyPackMode && loaderContext._module.buildMeta !== undefined) {
// Make sure webpack is aware that even though the emitted JavaScript may be the same as
// a previously cached version the TypeScript may be different and therefore should be
// treated as new
loaderContext._module.buildMeta.tsLoaderFileVersion = fileVersion;
}
callback(null, output, sourceMap);
}
/**
* either retrieves loader options from the cache
* or creates them, adds them to the cache and returns
*/
function getLoaderOptions(loaderContext) {
// differentiate the TypeScript instance based on the webpack instance
let webpackIndex = webpackInstances.indexOf(loaderContext._compiler);
if (webpackIndex === -1) {
webpackIndex = webpackInstances.push(loaderContext._compiler) - 1;
}
const loaderOptions = loaderUtils.getOptions(loaderContext) ||
{};
const instanceName = webpackIndex + '_' + (loaderOptions.instance || 'default');
if (!loaderOptionsCache.hasOwnProperty(instanceName)) {
loaderOptionsCache[instanceName] = new WeakMap();
}
const cache = loaderOptionsCache[instanceName];
if (cache.has(loaderOptions)) {
return cache.get(loaderOptions);
}
validateLoaderOptions(loaderOptions);
const options = makeLoaderOptions(instanceName, loaderOptions);
cache.set(loaderOptions, options);
return options;
}
const validLoaderOptions = [
'silent',
'logLevel',
'logInfoToStdOut',
'instance',
'compiler',
'context',
'configFile',
'transpileOnly',
'ignoreDiagnostics',
'errorFormatter',
'colors',
'compilerOptions',
'appendTsSuffixTo',
'appendTsxSuffixTo',
'onlyCompileBundledFiles',
'happyPackMode',
'getCustomTransformers',
'reportFiles',
'experimentalWatchApi',
'allowTsInNodeModules',
'experimentalFileCaching',
'projectReferences',
'resolveModuleName',
'resolveTypeReferenceDirective'
];
/**
* Validate the supplied loader options.
* At present this validates the option names only; in future we may look at validating the values too
* @param loaderOptions
*/
function validateLoaderOptions(loaderOptions) {
const loaderOptionKeys = Object.keys(loaderOptions);
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < loaderOptionKeys.length; i++) {
const option = loaderOptionKeys[i];
const isUnexpectedOption = validLoaderOptions.indexOf(option) === -1;
if (isUnexpectedOption) {
throw new Error(`ts-loader was supplied with an unexpected loader option: ${option}
Please take a look at the options you are supplying; the following are valid options:
${validLoaderOptions.join(' / ')}
`);
}
}
if (loaderOptions.context !== undefined &&
!path.isAbsolute(loaderOptions.context)) {
throw new Error(`Option 'context' has to be an absolute path. Given '${loaderOptions.context}'.`);
}
}
function makeLoaderOptions(instanceName, loaderOptions) {
const options = Object.assign({}, {
silent: false,
logLevel: 'WARN',
logInfoToStdOut: false,
compiler: 'typescript',
configFile: 'tsconfig.json',
context: undefined,
transpileOnly: false,
compilerOptions: {},
appendTsSuffixTo: [],
appendTsxSuffixTo: [],
transformers: {},
happyPackMode: false,
colors: true,
onlyCompileBundledFiles: false,
reportFiles: [],
// When the watch API usage stabilises look to remove this option and make watch usage the default behaviour when available
experimentalWatchApi: false,
allowTsInNodeModules: false,
experimentalFileCaching: true
}, loaderOptions);
options.ignoreDiagnostics = utils_1.arrify(options.ignoreDiagnostics).map(Number);
options.logLevel = options.logLevel.toUpperCase();
options.instance = instanceName;
// happypack can be used only together with transpileOnly mode
options.transpileOnly = options.happyPackMode ? true : options.transpileOnly;
return options;
}
/**
* Either add file to the overall files cache or update it in the cache when the file contents have changed
* Also add the file to the modified files
*/
function updateFileInCache(options, filePath, contents, instance) {
let fileWatcherEventKind;
// Update file contents
let file = instance.files.get(filePath);
if (file === undefined) {
file = instance.otherFiles.get(filePath);
if (file !== undefined) {
instance.otherFiles.delete(filePath);
instance.files.set(filePath, file);
}
else {
if (instance.watchHost !== undefined) {
fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Created;
}
file = { version: 0 };
instance.files.set(filePath, file);
}
instance.changedFilesList = true;
}
if (instance.watchHost !== undefined && contents === undefined) {
fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Deleted;
}
// filePath is a root file as it was passed to the loader. But it
// could have been found earlier as a dependency of another file. If
// that is the case, compiling this file changes the structure of
// the program and we need to increase the instance version.
//
// See https://github.com/TypeStrong/ts-loader/issues/943
if (!instance.rootFileNames.has(filePath) &&
// however, be careful not to add files from node_modules unless
// it is allowed by the options.
(options.allowTsInNodeModules || filePath.indexOf('node_modules') === -1)) {
instance.version++;
instance.rootFileNames.add(filePath);
}
if (file.text !== contents) {
file.version++;
file.text = contents;
instance.version++;
if (instance.watchHost !== undefined &&
fileWatcherEventKind === undefined) {
fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Changed;
}
}
if (instance.watchHost !== undefined && fileWatcherEventKind !== undefined) {
instance.hasUnaccountedModifiedFiles = true;
instance.watchHost.invokeFileWatcher(filePath, fileWatcherEventKind);
instance.watchHost.invokeDirectoryWatcher(path.dirname(filePath), filePath);
}
// push this file to modified files hash.
if (instance.modifiedFiles === null || instance.modifiedFiles === undefined) {
instance.modifiedFiles = new Map();
}
instance.modifiedFiles.set(filePath, file);
return file.version;
}
function getEmit(rawFilePath, filePath, instance, loaderContext) {
const outputFiles = instances_1.getEmitOutput(instance, filePath);
loaderContext.clearDependencies();
loaderContext.addDependency(rawFilePath);
const allDefinitionFiles = [...instance.files.keys()].filter(defFilePath => defFilePath.match(constants.dtsDtsxOrDtsDtsxMapRegex));
// Make this file dependent on *all* definition files in the program
const addDependency = loaderContext.addDependency.bind(loaderContext);
allDefinitionFiles.forEach(addDependency);
// Additionally make this file dependent on all imported files
const fileDependencies = instance.dependencyGraph[filePath];
const additionalDependencies = fileDependencies === undefined
? []
: fileDependencies.map(({ resolvedFileName, originalFileName }) => {
const projectReference = utils_1.getAndCacheProjectReference(resolvedFileName, instance);
// In the case of dependencies that are part of a project reference,
// the real dependency that webpack should watch is the JS output file.
return projectReference !== undefined
? utils_1.getAndCacheOutputJSFileName(resolvedFileName, projectReference, instance)
: originalFileName;
});
if (additionalDependencies.length > 0) {
additionalDependencies.forEach(addDependency);
}
loaderContext._module.buildMeta.tsLoaderDefinitionFileVersions = allDefinitionFiles
.concat(additionalDependencies)
.map(defFilePath => defFilePath +
'@' +
(instance.files.get(defFilePath) || { version: '?' }).version);
const outputFile = outputFiles
.filter(file => file.name.match(constants.jsJsx))
.pop();
const outputText = outputFile === undefined ? undefined : outputFile.text;
const sourceMapFile = outputFiles
.filter(file => file.name.match(constants.jsJsxMap))
.pop();
const sourceMapText = sourceMapFile === undefined ? undefined : sourceMapFile.text;
return { outputText, sourceMapText };
}
/**
* Transpile file
*/
function getTranspilationEmit(fileName, contents, instance, loaderContext) {
const { outputText, sourceMapText, diagnostics } = instance.compiler.transpileModule(contents, {
compilerOptions: Object.assign({}, instance.compilerOptions, { rootDir: undefined }),
transformers: instance.transformers,
reportDiagnostics: true,
fileName
});
// _module.errors is not available inside happypack - see https://github.com/TypeStrong/ts-loader/issues/336
if (!instance.loaderOptions.happyPackMode) {
const errors = utils_1.formatErrors(diagnostics, instance.loaderOptions, instance.colors, instance.compiler, { module: loaderContext._module }, loaderContext.context);
loaderContext._module.errors.push(...errors);
}
return { outputText, sourceMapText };
}
function makeSourceMap(sourceMapText, outputText, filePath, contents, loaderContext) {
if (sourceMapText === undefined) {
return { output: outputText, sourceMap: undefined };
}
return {
output: outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
sourceMap: Object.assign(JSON.parse(sourceMapText), {
sources: [loaderUtils.getRemainingRequest(loaderContext)],
file: filePath,
sourcesContent: [contents]
})
};
}
module.exports = loader;
//# sourceMappingURL=index.js.map