vue-ts-loader
Version:
TypeScript loader for vue-loader
483 lines (482 loc) • 22.1 kB
JavaScript
///<reference path="typings/loaderUtils/loaderUtils.d.ts" />
var typescript = require('typescript');
var path = require('path');
var fs = require('fs');
var os = require('os');
var loaderUtils = require('loader-utils');
var objectAssign = require('object-assign');
var makeResolver = require('./resolver');
require('colors');
var Console = require('console').Console;
var semver = require('semver');
var console = new Console(process.stderr);
var pushArray = function (arr, toPush) {
Array.prototype.splice.apply(arr, [0, 0].concat(toPush));
};
function arrify(val) {
if (val === null || val === undefined) {
return [];
}
return Array.isArray(val) ? val : [val];
}
;
function hasOwnProperty(obj, property) {
return Object.prototype.hasOwnProperty.call(obj, property);
}
var instances = {};
var webpackInstances = [];
var scriptRegex = /\.tsx?$/i;
// Take TypeScript errors, parse them and format to webpack errors
// Optionally adds a file name
function formatErrors(diagnostics, instance, merge) {
return diagnostics
.filter(function (diagnostic) { return instance.loaderOptions.ignoreDiagnostics.indexOf(diagnostic.code) == -1; })
.map(function (diagnostic) {
var errorCategory = instance.compiler.DiagnosticCategory[diagnostic.category].toLowerCase();
var errorCategoryAndCode = errorCategory + ' TS' + diagnostic.code + ': ';
var messageText = errorCategoryAndCode + instance.compiler.flattenDiagnosticMessageText(diagnostic.messageText, os.EOL);
if (diagnostic.file) {
var lineChar = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
return {
message: "" + '('.white + (lineChar.line + 1).toString().cyan + "," + (lineChar.character + 1).toString().cyan + "): " + messageText.red,
rawMessage: messageText,
location: { line: lineChar.line + 1, character: lineChar.character + 1 },
loaderSource: 'ts-loader'
};
}
else {
return {
message: "" + messageText.red,
rawMessage: messageText,
loaderSource: 'ts-loader'
};
}
})
.map(function (error) { return objectAssign(error, merge); });
}
// The tsconfig.json is found using the same method as `tsc`, starting in the current directory
// and continuing up the parent directory chain.
function findConfigFile(compiler, searchPath, configFileName) {
while (true) {
var fileName = path.join(searchPath, configFileName);
if (compiler.sys.fileExists(fileName)) {
return fileName;
}
var parentPath = path.dirname(searchPath);
if (parentPath === searchPath) {
break;
}
searchPath = parentPath;
}
return undefined;
}
function appendSuffixToVue(fileName) {
if (/\.vue$/.test(fileName)) {
return fileName + '.ts';
}
return fileName;
}
// The loader is executed once for each file seen by webpack. However, we need to keep
// a persistent instance of TypeScript that contains all of the files in the program
// along with definition files and options. This function either creates an instance
// or returns the existing one. Multiple instances are possible by using the
// `instance` property.
function ensureTypeScriptInstance(loaderOptions, loader) {
function log() {
var messages = [];
for (var _i = 0; _i < arguments.length; _i++) {
messages[_i - 0] = arguments[_i];
}
if (!loaderOptions.silent) {
console.log.apply(console, messages);
}
}
if (hasOwnProperty(instances, loaderOptions.instance)) {
return { instance: instances[loaderOptions.instance] };
}
try {
var compiler = require(loaderOptions.compiler);
}
catch (e) {
var message = loaderOptions.compiler == 'typescript'
? 'Could not load TypeScript. Try installing with `npm install typescript`. If TypeScript is installed globally, try using `npm link typescript`.'
: "Could not load TypeScript compiler with NPM package name `" + loaderOptions.compiler + "`. Are you sure it is correctly installed?";
return { error: {
message: message.red,
rawMessage: message,
loaderSource: 'ts-loader'
} };
}
var motd = "ts-loader: Using " + loaderOptions.compiler + "@" + compiler.version, compilerCompatible = false;
if (loaderOptions.compiler == 'typescript') {
if (compiler.version && semver.gte(compiler.version, '2.0.0')) {
// don't log yet in this case, if a tsconfig.json exists we want to combine the message
compilerCompatible = true;
}
else {
log((motd + ". This version is incompatible with ts-loader. Please upgrade to the latest version of TypeScript.").red);
}
}
else {
log((motd + ". This version may or may not be compatible with ts-loader.").yellow);
}
var files = {};
var instance = instances[loaderOptions.instance] = {
compiler: compiler,
compilerOptions: null,
loaderOptions: loaderOptions,
files: files,
languageService: null,
version: 0,
dependencyGraph: {}
};
var compilerOptions = {};
// Load any available tsconfig.json file
var filesToLoad = [];
var configFilePath = findConfigFile(compiler, path.dirname(loader.resourcePath), loaderOptions.configFileName);
var configFile;
if (configFilePath) {
if (compilerCompatible)
log((motd + " and " + configFilePath).green);
else
log(("ts-loader: Using config file at " + configFilePath).green);
// HACK: relies on the fact that passing an extra argument won't break
// the old API that has a single parameter
configFile = compiler.readConfigFile(configFilePath, compiler.sys.readFile);
if (configFile.error) {
var configFileError = formatErrors([configFile.error], instance, { file: configFilePath })[0];
return { error: configFileError };
}
}
else {
if (compilerCompatible)
log(motd.green);
configFile = {
config: {
compilerOptions: {},
files: []
}
};
}
configFile.config.compilerOptions = objectAssign({}, configFile.config.compilerOptions, loaderOptions.compilerOptions);
// do any necessary config massaging
if (loaderOptions.transpileOnly) {
configFile.config.compilerOptions.isolatedModules = true;
}
var configParseResult;
configParseResult = compiler.parseJsonConfigFileContent(configFile.config, compiler.sys, path.dirname(configFilePath || ''));
if (configParseResult.errors.length) {
pushArray(loader._module.errors, formatErrors(configParseResult.errors, instance, { file: configFilePath }));
return { error: {
file: configFilePath,
message: 'error while parsing tsconfig.json'.red,
rawMessage: 'error while parsing tsconfig.json',
loaderSource: 'ts-loader'
} };
}
instance.compilerOptions = objectAssign(compilerOptions, configParseResult.options);
filesToLoad = configParseResult.fileNames;
// if `module` is not specified and not using ES6 target, default to CJS module output
if (compilerOptions.module == null && compilerOptions.target !== 2 /* ES6 */) {
compilerOptions.module = 1; /* CommonJS */
}
if (loaderOptions.transpileOnly) {
// quick return for transpiling
// we do need to check for any issues with TS options though
var program = compiler.createProgram([], compilerOptions), diagnostics = program.getOptionsDiagnostics();
pushArray(loader._module.errors, formatErrors(diagnostics, instance, { file: configFilePath || 'tsconfig.json' }));
return { instance: instances[loaderOptions.instance] = { compiler: compiler, compilerOptions: compilerOptions, loaderOptions: loaderOptions, files: files, dependencyGraph: {} } };
}
// Load initial files (core lib files, any files specified in tsconfig.json)
var filePath;
try {
filesToLoad.forEach(function (fp) {
filePath = path.normalize(fp);
files[filePath] = {
text: fs.readFileSync(filePath, 'utf-8'),
version: 0
};
});
}
catch (exc) {
var filePathError = "A file specified in tsconfig.json could not be found: " + filePath;
return { error: {
message: filePathError.red,
rawMessage: filePathError,
loaderSource: 'ts-loader'
} };
}
var newLine = compilerOptions.newLine === 0 /* CarriageReturnLineFeed */ ? '\r\n' :
compilerOptions.newLine === 1 /* LineFeed */ ? '\n' :
os.EOL;
// make a (sync) resolver that follows webpack's rules
var resolver = makeResolver(loader.options);
var readFile = function (fileName) {
fileName = path.normalize(fileName);
try {
return fs.readFileSync(fileName, { encoding: 'utf8' });
}
catch (e) {
return;
}
};
var moduleResolutionHost = {
fileExists: function (fileName) { return readFile(fileName) !== undefined; },
readFile: function (fileName) { return readFile(fileName); }
};
// Create the TypeScript language service
var servicesHost = {
getProjectVersion: function () { return instance.version + ''; },
getScriptFileNames: function () { return Object.keys(files).filter(function (filePath) { return scriptRegex.test(filePath); }); },
getScriptVersion: function (fileName) {
fileName = path.normalize(fileName);
return files[fileName] && files[fileName].version.toString();
},
getScriptSnapshot: function (fileName) {
// This is called any time TypeScript needs a file's text
// We either load from memory or from disk
fileName = path.normalize(fileName);
var file = files[fileName];
if (!file) {
var text = readFile(fileName);
if (text == null)
return;
file = files[fileName] = { version: 0, text: text };
}
return compiler.ScriptSnapshot.fromString(file.text);
},
getCurrentDirectory: function () { return process.cwd(); },
getDirectories: typescript.sys.getDirectories,
directoryExists: typescript.sys.directoryExists,
getCompilationSettings: function () { return compilerOptions; },
getDefaultLibFileName: function (options) { return compiler.getDefaultLibFilePath(options); },
getNewLine: function () { return newLine; },
log: log,
resolveModuleNames: function (moduleNames, containingFile) {
var resolvedModules = [];
for (var _i = 0, moduleNames_1 = moduleNames; _i < moduleNames_1.length; _i++) {
var moduleName = moduleNames_1[_i];
var resolvedFileName = void 0;
var resolutionResult = void 0;
try {
resolvedFileName = resolver.resolveSync(path.normalize(path.dirname(containingFile)), moduleName);
resolvedFileName = appendSuffixToVue(resolvedFileName);
if (!resolvedFileName.match(/\.tsx?$/))
resolvedFileName = null;
else
resolutionResult = { resolvedFileName: resolvedFileName };
}
catch (e) {
resolvedFileName = null;
}
var tsResolution = compiler.resolveModuleName(moduleName, containingFile, compilerOptions, moduleResolutionHost);
if (tsResolution.resolvedModule) {
if (resolvedFileName) {
if (resolvedFileName == tsResolution.resolvedModule.resolvedFileName) {
resolutionResult.isExternalLibraryImport = tsResolution.resolvedModule.isExternalLibraryImport;
}
}
else
resolutionResult = tsResolution.resolvedModule;
}
resolvedModules.push(resolutionResult);
}
instance.dependencyGraph[containingFile] = resolvedModules.filter(function (m) { return m != null; }).map(function (m) { return m.resolvedFileName; });
return resolvedModules;
}
};
var languageService = instance.languageService = compiler.createLanguageService(servicesHost, compiler.createDocumentRegistry());
var getCompilerOptionDiagnostics = true;
loader._compiler.plugin("after-compile", function (compilation, callback) {
// Don't add errors for child compilations
if (compilation.compiler.isChild()) {
callback();
return;
}
var stats = compilation.stats;
// handle all other errors. The basic approach here to get accurate error
// reporting is to start with a "blank slate" each compilation and gather
// all errors from all files. Since webpack tracks errors in a module from
// compilation-to-compilation, and since not every module always runs through
// the loader, we need to detect and remove any pre-existing errors.
function removeTSLoaderErrors(errors) {
var index = -1, length = errors.length;
while (++index < length) {
if (errors[index].loaderSource == 'ts-loader') {
errors.splice(index--, 1);
length--;
}
}
}
removeTSLoaderErrors(compilation.errors);
// handle compiler option errors after the first compile
if (getCompilerOptionDiagnostics) {
getCompilerOptionDiagnostics = false;
pushArray(compilation.errors, formatErrors(languageService.getCompilerOptionsDiagnostics(), instance, { file: configFilePath || 'tsconfig.json' }));
}
// build map of all modules based on normalized filename
// this is used for quick-lookup when trying to find modules
// based on filepath
var modules = {};
compilation.modules.forEach(function (module) {
if (module.resource) {
var modulePath = path.normalize(module.resource);
if (hasOwnProperty(modules, modulePath)) {
var existingModules = modules[modulePath];
if (existingModules.indexOf(module) == -1) {
existingModules.push(module);
}
}
else {
modules[modulePath] = [module];
}
}
});
// gather all errors from TypeScript and output them to webpack
Object.keys(instance.files)
.filter(function (filePath) { return !!filePath.match(/(\.d)?\.ts(x?)$/); })
.forEach(function (filePath) {
var errors = languageService.getSyntacticDiagnostics(filePath).concat(languageService.getSemanticDiagnostics(filePath));
// if we have access to a webpack module, use that
if (hasOwnProperty(modules, filePath)) {
var associatedModules = modules[filePath];
associatedModules.forEach(function (module) {
// remove any existing errors
removeTSLoaderErrors(module.errors);
// append errors
var formattedErrors = formatErrors(errors, instance, { module: module });
pushArray(module.errors, formattedErrors);
pushArray(compilation.errors, formattedErrors);
});
}
else {
pushArray(compilation.errors, formatErrors(errors, instance, { file: filePath }));
}
});
// gather all declaration files from TypeScript and output them to webpack
Object.keys(instance.files)
.filter(function (filePath) { return !!filePath.match(/\.ts(x?)$/); })
.forEach(function (filePath) {
var output = languageService.getEmitOutput(filePath);
var declarationFile = output.outputFiles.filter(function (filePath) { return !!filePath.name.match(/\.d.ts$/); }).pop();
if (declarationFile) {
var context = compilation.options.context;
var assetsPath = path.normalize(path.relative(context, declarationFile.name));
compilation.assets[assetsPath] = {
source: function () { return declarationFile.text; },
size: function () { return declarationFile.text.length; }
};
}
});
callback();
});
// manually update changed files
loader._compiler.plugin("watch-run", function (watching, cb) {
var mtimes = watching.compiler.fileTimestamps ||
watching.compiler.watchFileSystem.watcher.mtimes;
Object.keys(mtimes)
.filter(function (filePath) { return !!filePath.match(/\.tsx?$|\.jsx?$/); })
.forEach(function (filePath) {
filePath = path.normalize(filePath);
var file = instance.files[filePath];
if (file) {
file.text = fs.readFileSync(filePath, { encoding: 'utf8' });
file.version++;
instance.version++;
}
});
cb();
});
return { instance: instance };
}
function loader(contents) {
this.cacheable && this.cacheable();
var callback = this.async();
var filePath = path.normalize(this.resourcePath);
filePath = appendSuffixToVue(filePath);
var queryOptions = loaderUtils.parseQuery(this.query);
var configFileOptions = this.options.ts || {};
var options = objectAssign({}, {
silent: false,
instance: 'default',
compiler: 'typescript',
configFileName: 'tsconfig.json',
transpileOnly: false,
compilerOptions: {}
}, configFileOptions, queryOptions);
options.ignoreDiagnostics = arrify(options.ignoreDiagnostics).map(Number);
// differentiate the TypeScript instance based on the webpack instance
var webpackIndex = webpackInstances.indexOf(this._compiler);
if (webpackIndex == -1) {
webpackIndex = webpackInstances.push(this._compiler) - 1;
}
options.instance = webpackIndex + '_' + options.instance;
var _a = ensureTypeScriptInstance(options, this), instance = _a.instance, error = _a.error;
if (error) {
callback(error);
return;
}
// Update file contents
var file = instance.files[filePath];
if (!file) {
file = instance.files[filePath] = { version: 0 };
}
if (file.text !== contents) {
file.version++;
file.text = contents;
instance.version++;
}
var outputText, sourceMapText, diagnostics = [];
if (options.transpileOnly) {
var fileName = path.basename(filePath);
var transpileResult = instance.compiler.transpileModule(contents, {
compilerOptions: instance.compilerOptions,
reportDiagnostics: true,
fileName: fileName
});
(outputText = transpileResult.outputText, sourceMapText = transpileResult.sourceMapText, diagnostics = transpileResult.diagnostics, transpileResult);
pushArray(this._module.errors, formatErrors(diagnostics, instance, { module: this._module }));
}
else {
var langService = instance.languageService;
// Emit Javascript
var output = langService.getEmitOutput(filePath);
// Make this file dependent on *all* definition files in the program
this.clearDependencies();
this.addDependency(filePath);
var allDefinitionFiles = Object.keys(instance.files).filter(function (filePath) { return /\.d\.ts$/.test(filePath); });
allDefinitionFiles.forEach(this.addDependency.bind(this));
// Additionally make this file dependent on all imported files
var additionalDependencies = instance.dependencyGraph[filePath];
if (additionalDependencies) {
additionalDependencies.forEach(this.addDependency.bind(this));
}
this._module.meta.tsLoaderDefinitionFileVersions = allDefinitionFiles
.concat(additionalDependencies)
.map(function (filePath) { return filePath + '@' + (instance.files[filePath] || { version: '?' }).version; });
var outputFile = output.outputFiles.filter(function (file) { return !!file.name.match(/\.js(x?)$/); }).pop();
if (outputFile) {
outputText = outputFile.text;
}
var sourceMapFile = output.outputFiles.filter(function (file) { return !!file.name.match(/\.js(x?)\.map$/); }).pop();
if (sourceMapFile) {
sourceMapText = sourceMapFile.text;
}
}
if (outputText == null)
throw new Error("Typescript emitted no output for " + filePath);
if (sourceMapText) {
var sourceMap = JSON.parse(sourceMapText);
sourceMap.sources = [loaderUtils.getRemainingRequest(this)];
sourceMap.file = filePath;
sourceMap.sourcesContent = [contents];
outputText = outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
}
// 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
this._module.meta.tsLoaderFileVersion = file.version;
callback(null, outputText, sourceMap);
}
module.exports = loader;
;