You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. "use strict";
  2. const loaderUtils = require("loader-utils");
  3. const path = require("path");
  4. const constants = require("./constants");
  5. const instances_1 = require("./instances");
  6. const utils_1 = require("./utils");
  7. const webpackInstances = [];
  8. const loaderOptionsCache = {};
  9. /**
  10. * The entry point for ts-loader
  11. */
  12. function loader(contents) {
  13. // tslint:disable-next-line:no-unused-expression strict-boolean-expressions
  14. this.cacheable && this.cacheable();
  15. const callback = this.async();
  16. const options = getLoaderOptions(this);
  17. const instanceOrError = instances_1.getTypeScriptInstance(options, this);
  18. if (instanceOrError.error !== undefined) {
  19. callback(new Error(instanceOrError.error.message));
  20. return;
  21. }
  22. return successLoader(this, contents, callback, options, instanceOrError.instance);
  23. }
  24. function successLoader(loaderContext, contents, callback, options, instance) {
  25. const rawFilePath = path.normalize(loaderContext.resourcePath);
  26. const filePath = options.appendTsSuffixTo.length > 0 || options.appendTsxSuffixTo.length > 0
  27. ? utils_1.appendSuffixesIfMatch({
  28. '.ts': options.appendTsSuffixTo,
  29. '.tsx': options.appendTsxSuffixTo
  30. }, rawFilePath)
  31. : rawFilePath;
  32. const fileVersion = updateFileInCache(filePath, contents, instance);
  33. const referencedProject = utils_1.getAndCacheProjectReference(filePath, instance);
  34. if (referencedProject !== undefined) {
  35. const [relativeProjectConfigPath, relativeFilePath] = [
  36. path.relative(loaderContext.rootContext, referencedProject.sourceFile.fileName),
  37. path.relative(loaderContext.rootContext, filePath)
  38. ];
  39. if (referencedProject.commandLine.options.outFile !== undefined) {
  40. throw new Error(`The referenced project at ${relativeProjectConfigPath} is using ` +
  41. `the outFile' option, which is not supported with ts-loader.`);
  42. }
  43. const jsFileName = utils_1.getAndCacheOutputJSFileName(filePath, referencedProject, instance);
  44. const relativeJSFileName = path.relative(loaderContext.rootContext, jsFileName);
  45. if (!instance.compiler.sys.fileExists(jsFileName)) {
  46. throw new Error(`Could not find output JavaScript file for input ` +
  47. `${relativeFilePath} (looked at ${relativeJSFileName}).\n` +
  48. `The input file is part of a project reference located at ` +
  49. `${relativeProjectConfigPath}, so ts-loader is looking for the ` +
  50. 'project’s pre-built output on disk. Try running `tsc --build` ' +
  51. 'to build project references.');
  52. }
  53. // Since the output JS file is being read from disk instead of using the
  54. // input TS file, we need to tell the loader that the compilation doesn’t
  55. // actually depend on the current file, but depends on the JS file instead.
  56. loaderContext.clearDependencies();
  57. loaderContext.addDependency(jsFileName);
  58. utils_1.validateSourceMapOncePerProject(instance, loaderContext, jsFileName, referencedProject);
  59. const mapFileName = jsFileName + '.map';
  60. const outputText = instance.compiler.sys.readFile(jsFileName);
  61. const sourceMapText = instance.compiler.sys.readFile(mapFileName);
  62. makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, options, fileVersion, callback);
  63. }
  64. else {
  65. const { outputText, sourceMapText } = options.transpileOnly
  66. ? getTranspilationEmit(filePath, contents, instance, loaderContext)
  67. : getEmit(rawFilePath, filePath, instance, loaderContext);
  68. makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, options, fileVersion, callback);
  69. }
  70. }
  71. function makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, options, fileVersion, callback) {
  72. if (outputText === null || outputText === undefined) {
  73. const additionalGuidance = !options.allowTsInNodeModules && filePath.indexOf('node_modules') !== -1
  74. ? ' By default, ts-loader will not compile .ts files in node_modules.\n' +
  75. 'You should not need to recompile .ts files there, but if you really want to, use the allowTsInNodeModules option.\n' +
  76. 'See: https://github.com/Microsoft/TypeScript/issues/12358'
  77. : '';
  78. throw new Error(`TypeScript emitted no output for ${filePath}.${additionalGuidance}`);
  79. }
  80. const { sourceMap, output } = makeSourceMap(sourceMapText, outputText, filePath, contents, loaderContext);
  81. // _module.meta is not available inside happypack
  82. if (!options.happyPackMode && loaderContext._module.buildMeta !== undefined) {
  83. // Make sure webpack is aware that even though the emitted JavaScript may be the same as
  84. // a previously cached version the TypeScript may be different and therefore should be
  85. // treated as new
  86. loaderContext._module.buildMeta.tsLoaderFileVersion = fileVersion;
  87. }
  88. callback(null, output, sourceMap);
  89. }
  90. /**
  91. * either retrieves loader options from the cache
  92. * or creates them, adds them to the cache and returns
  93. */
  94. function getLoaderOptions(loaderContext) {
  95. // differentiate the TypeScript instance based on the webpack instance
  96. let webpackIndex = webpackInstances.indexOf(loaderContext._compiler);
  97. if (webpackIndex === -1) {
  98. webpackIndex = webpackInstances.push(loaderContext._compiler) - 1;
  99. }
  100. const loaderOptions = loaderUtils.getOptions(loaderContext) ||
  101. {};
  102. const instanceName = webpackIndex + '_' + (loaderOptions.instance || 'default');
  103. if (!loaderOptionsCache.hasOwnProperty(instanceName)) {
  104. loaderOptionsCache[instanceName] = new WeakMap();
  105. }
  106. const cache = loaderOptionsCache[instanceName];
  107. if (cache.has(loaderOptions)) {
  108. return cache.get(loaderOptions);
  109. }
  110. validateLoaderOptions(loaderOptions);
  111. const options = makeLoaderOptions(instanceName, loaderOptions);
  112. cache.set(loaderOptions, options);
  113. return options;
  114. }
  115. const validLoaderOptions = [
  116. 'silent',
  117. 'logLevel',
  118. 'logInfoToStdOut',
  119. 'instance',
  120. 'compiler',
  121. 'context',
  122. 'configFile',
  123. 'transpileOnly',
  124. 'ignoreDiagnostics',
  125. 'errorFormatter',
  126. 'colors',
  127. 'compilerOptions',
  128. 'appendTsSuffixTo',
  129. 'appendTsxSuffixTo',
  130. 'onlyCompileBundledFiles',
  131. 'happyPackMode',
  132. 'getCustomTransformers',
  133. 'reportFiles',
  134. 'experimentalWatchApi',
  135. 'allowTsInNodeModules',
  136. 'experimentalFileCaching',
  137. 'projectReferences',
  138. 'resolveModuleName',
  139. 'resolveTypeReferenceDirective'
  140. ];
  141. /**
  142. * Validate the supplied loader options.
  143. * At present this validates the option names only; in future we may look at validating the values too
  144. * @param loaderOptions
  145. */
  146. function validateLoaderOptions(loaderOptions) {
  147. const loaderOptionKeys = Object.keys(loaderOptions);
  148. // tslint:disable-next-line:prefer-for-of
  149. for (let i = 0; i < loaderOptionKeys.length; i++) {
  150. const option = loaderOptionKeys[i];
  151. const isUnexpectedOption = validLoaderOptions.indexOf(option) === -1;
  152. if (isUnexpectedOption) {
  153. throw new Error(`ts-loader was supplied with an unexpected loader option: ${option}
  154. Please take a look at the options you are supplying; the following are valid options:
  155. ${validLoaderOptions.join(' / ')}
  156. `);
  157. }
  158. }
  159. if (loaderOptions.context !== undefined &&
  160. !path.isAbsolute(loaderOptions.context)) {
  161. throw new Error(`Option 'context' has to be an absolute path. Given '${loaderOptions.context}'.`);
  162. }
  163. }
  164. function makeLoaderOptions(instanceName, loaderOptions) {
  165. const options = Object.assign({}, {
  166. silent: false,
  167. logLevel: 'WARN',
  168. logInfoToStdOut: false,
  169. compiler: 'typescript',
  170. configFile: 'tsconfig.json',
  171. context: undefined,
  172. transpileOnly: false,
  173. compilerOptions: {},
  174. appendTsSuffixTo: [],
  175. appendTsxSuffixTo: [],
  176. transformers: {},
  177. happyPackMode: false,
  178. colors: true,
  179. onlyCompileBundledFiles: false,
  180. reportFiles: [],
  181. // When the watch API usage stabilises look to remove this option and make watch usage the default behaviour when available
  182. experimentalWatchApi: false,
  183. allowTsInNodeModules: false,
  184. experimentalFileCaching: true
  185. }, loaderOptions);
  186. options.ignoreDiagnostics = utils_1.arrify(options.ignoreDiagnostics).map(Number);
  187. options.logLevel = options.logLevel.toUpperCase();
  188. options.instance = instanceName;
  189. // happypack can be used only together with transpileOnly mode
  190. options.transpileOnly = options.happyPackMode ? true : options.transpileOnly;
  191. return options;
  192. }
  193. /**
  194. * Either add file to the overall files cache or update it in the cache when the file contents have changed
  195. * Also add the file to the modified files
  196. */
  197. function updateFileInCache(filePath, contents, instance) {
  198. let fileWatcherEventKind;
  199. // Update file contents
  200. let file = instance.files.get(filePath);
  201. if (file === undefined) {
  202. file = instance.otherFiles.get(filePath);
  203. if (file !== undefined) {
  204. instance.otherFiles.delete(filePath);
  205. instance.files.set(filePath, file);
  206. }
  207. else {
  208. if (instance.watchHost !== undefined) {
  209. fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Created;
  210. }
  211. file = { version: 0 };
  212. instance.files.set(filePath, file);
  213. }
  214. instance.changedFilesList = true;
  215. }
  216. if (instance.watchHost !== undefined && contents === undefined) {
  217. fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Deleted;
  218. }
  219. if (file.text !== contents) {
  220. file.version++;
  221. file.text = contents;
  222. instance.version++;
  223. if (instance.watchHost !== undefined &&
  224. fileWatcherEventKind === undefined) {
  225. fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Changed;
  226. }
  227. }
  228. if (instance.watchHost !== undefined && fileWatcherEventKind !== undefined) {
  229. instance.hasUnaccountedModifiedFiles = true;
  230. instance.watchHost.invokeFileWatcher(filePath, fileWatcherEventKind);
  231. instance.watchHost.invokeDirectoryWatcher(path.dirname(filePath), filePath);
  232. }
  233. // push this file to modified files hash.
  234. if (instance.modifiedFiles === null || instance.modifiedFiles === undefined) {
  235. instance.modifiedFiles = new Map();
  236. }
  237. instance.modifiedFiles.set(filePath, file);
  238. return file.version;
  239. }
  240. function getEmit(rawFilePath, filePath, instance, loaderContext) {
  241. const outputFiles = instances_1.getEmitOutput(instance, filePath);
  242. loaderContext.clearDependencies();
  243. loaderContext.addDependency(rawFilePath);
  244. const allDefinitionFiles = [...instance.files.keys()].filter(defFilePath => defFilePath.match(constants.dtsDtsxOrDtsDtsxMapRegex));
  245. // Make this file dependent on *all* definition files in the program
  246. const addDependency = loaderContext.addDependency.bind(loaderContext);
  247. allDefinitionFiles.forEach(addDependency);
  248. // Additionally make this file dependent on all imported files
  249. const fileDependencies = instance.dependencyGraph[filePath];
  250. const additionalDependencies = fileDependencies === undefined
  251. ? []
  252. : fileDependencies.map(({ resolvedFileName, originalFileName }) => {
  253. const projectReference = utils_1.getAndCacheProjectReference(resolvedFileName, instance);
  254. // In the case of dependencies that are part of a project reference,
  255. // the real dependency that webpack should watch is the JS output file.
  256. return projectReference !== undefined
  257. ? utils_1.getAndCacheOutputJSFileName(resolvedFileName, projectReference, instance)
  258. : originalFileName;
  259. });
  260. if (additionalDependencies.length > 0) {
  261. additionalDependencies.forEach(addDependency);
  262. }
  263. loaderContext._module.buildMeta.tsLoaderDefinitionFileVersions = allDefinitionFiles
  264. .concat(additionalDependencies)
  265. .map(defFilePath => defFilePath +
  266. '@' +
  267. (instance.files.get(defFilePath) || { version: '?' }).version);
  268. const outputFile = outputFiles
  269. .filter(file => file.name.match(constants.jsJsx))
  270. .pop();
  271. const outputText = outputFile === undefined ? undefined : outputFile.text;
  272. const sourceMapFile = outputFiles
  273. .filter(file => file.name.match(constants.jsJsxMap))
  274. .pop();
  275. const sourceMapText = sourceMapFile === undefined ? undefined : sourceMapFile.text;
  276. return { outputText, sourceMapText };
  277. }
  278. /**
  279. * Transpile file
  280. */
  281. function getTranspilationEmit(fileName, contents, instance, loaderContext) {
  282. const { outputText, sourceMapText, diagnostics } = instance.compiler.transpileModule(contents, {
  283. compilerOptions: Object.assign({}, instance.compilerOptions, { rootDir: undefined }),
  284. transformers: instance.transformers,
  285. reportDiagnostics: true,
  286. fileName
  287. });
  288. // _module.errors is not available inside happypack - see https://github.com/TypeStrong/ts-loader/issues/336
  289. if (!instance.loaderOptions.happyPackMode) {
  290. const errors = utils_1.formatErrors(diagnostics, instance.loaderOptions, instance.colors, instance.compiler, { module: loaderContext._module }, loaderContext.context);
  291. loaderContext._module.errors.push(...errors);
  292. }
  293. return { outputText, sourceMapText };
  294. }
  295. function makeSourceMap(sourceMapText, outputText, filePath, contents, loaderContext) {
  296. if (sourceMapText === undefined) {
  297. return { output: outputText, sourceMap: undefined };
  298. }
  299. return {
  300. output: outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
  301. sourceMap: Object.assign(JSON.parse(sourceMapText), {
  302. sources: [loaderUtils.getRemainingRequest(loaderContext)],
  303. file: filePath,
  304. sourcesContent: [contents]
  305. })
  306. };
  307. }
  308. module.exports = loader;