index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. "use strict";
  2. const crypto = require("crypto");
  3. const path = require("path");
  4. const constants = require("./constants");
  5. const instances_1 = require("./instances");
  6. const utils_1 = require("./utils");
  7. const source_map_1 = require("source-map");
  8. const loaderOptionsCache = {};
  9. /**
  10. * The entry point for ts-loader
  11. */
  12. function loader(contents, inputSourceMap) {
  13. this.cacheable && this.cacheable();
  14. const callback = this.async();
  15. const options = getLoaderOptions(this);
  16. const instanceOrError = (0, instances_1.getTypeScriptInstance)(options, this);
  17. if (instanceOrError.error !== undefined) {
  18. callback(new Error(instanceOrError.error.message));
  19. return;
  20. }
  21. const instance = instanceOrError.instance;
  22. (0, instances_1.buildSolutionReferences)(instance, this);
  23. successLoader(this, contents, callback, instance, inputSourceMap);
  24. }
  25. function successLoader(loaderContext, contents, callback, instance, inputSourceMap) {
  26. (0, instances_1.initializeInstance)(loaderContext, instance);
  27. (0, instances_1.reportTranspileErrors)(instance, loaderContext);
  28. const rawFilePath = path.normalize(loaderContext.resourcePath);
  29. const filePath = instance.loaderOptions.appendTsSuffixTo.length > 0 ||
  30. instance.loaderOptions.appendTsxSuffixTo.length > 0
  31. ? (0, utils_1.appendSuffixesIfMatch)({
  32. '.ts': instance.loaderOptions.appendTsSuffixTo,
  33. '.tsx': instance.loaderOptions.appendTsxSuffixTo,
  34. }, rawFilePath)
  35. : rawFilePath;
  36. const fileVersion = updateFileInCache(instance.loaderOptions, filePath, contents, instance);
  37. const { outputText, sourceMapText } = instance.loaderOptions.transpileOnly
  38. ? getTranspilationEmit(filePath, contents, instance, loaderContext)
  39. : getEmit(rawFilePath, filePath, instance, loaderContext);
  40. // the following function is async, which means it will immediately return and run in the "background"
  41. // Webpack will be notified when it's finished when the function calls the `callback` method
  42. makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, fileVersion, callback, instance, inputSourceMap);
  43. }
  44. function makeSourceMapAndFinish(sourceMapText, outputText, filePath, contents, loaderContext, fileVersion, callback, instance, inputSourceMap) {
  45. if (outputText === null || outputText === undefined) {
  46. setModuleMeta(loaderContext, instance, fileVersion);
  47. const additionalGuidance = (0, utils_1.isReferencedFile)(instance, filePath)
  48. ? ' The most common cause for this is having errors when building referenced projects.'
  49. : !instance.loaderOptions.allowTsInNodeModules &&
  50. filePath.indexOf('node_modules') !== -1
  51. ? ' By default, ts-loader will not compile .ts files in node_modules.\n' +
  52. 'You should not need to recompile .ts files there, but if you really want to, use the allowTsInNodeModules option.\n' +
  53. 'See: https://github.com/Microsoft/TypeScript/issues/12358'
  54. : '';
  55. callback(new Error(`TypeScript emitted no output for ${filePath}.${additionalGuidance}`), outputText, undefined);
  56. return;
  57. }
  58. const { sourceMap, output } = makeSourceMap(sourceMapText, outputText, filePath, contents, loaderContext);
  59. setModuleMeta(loaderContext, instance, fileVersion);
  60. // there are two cases where we don't need to perform input source map mapping:
  61. // - either the ts-compiler did not generate a source map (tsconfig had `sourceMap` set to false)
  62. // - or we did not get an input source map
  63. //
  64. // in the first case, we simply return undefined.
  65. // in the second case we only need to return the newly generated source map
  66. // this avoids that we have to make a possibly expensive call to the source-map lib
  67. if (sourceMap === undefined || !inputSourceMap) {
  68. callback(null, output, sourceMap);
  69. return;
  70. }
  71. // otherwise we have to make a mapping to the input source map which is asynchronous
  72. mapToInputSourceMap(sourceMap, loaderContext, inputSourceMap)
  73. .then(mappedSourceMap => {
  74. callback(null, output, mappedSourceMap);
  75. })
  76. .catch((e) => {
  77. callback(e);
  78. });
  79. }
  80. function setModuleMeta(loaderContext, instance, fileVersion) {
  81. // _module.meta is not available inside happypack
  82. if (!instance.loaderOptions.happyPackMode &&
  83. loaderContext._module.buildMeta !== undefined) {
  84. // Make sure webpack is aware that even though the emitted JavaScript may be the same as
  85. // a previously cached version the TypeScript may be different and therefore should be
  86. // treated as new
  87. loaderContext._module.buildMeta.tsLoaderFileVersion = fileVersion;
  88. }
  89. }
  90. /**
  91. * Get a unique hash based on the contents of the options
  92. * Hash is created from the values converted to strings
  93. * Values which are functions (such as getCustomTransformers) are
  94. * converted to strings by this code, which JSON.stringify would not do.
  95. */
  96. function getOptionsHash(loaderOptions) {
  97. const hash = crypto.createHash('sha256');
  98. Object.keys(loaderOptions).forEach(key => {
  99. const value = loaderOptions[key];
  100. if (value !== undefined) {
  101. const valueString = typeof value === 'function' ? value.toString() : JSON.stringify(value);
  102. hash.update(key + valueString);
  103. }
  104. });
  105. return hash.digest('hex').substring(0, 16);
  106. }
  107. /**
  108. * either retrieves loader options from the cache
  109. * or creates them, adds them to the cache and returns
  110. */
  111. function getLoaderOptions(loaderContext) {
  112. const loaderOptions = loaderContext.getOptions();
  113. // If no instance name is given in the options, use the hash of the loader options
  114. // In this way, if different options are given the instances will be different
  115. const instanceName = loaderOptions.instance || 'default_' + getOptionsHash(loaderOptions);
  116. if (!loaderOptionsCache.hasOwnProperty(instanceName)) {
  117. loaderOptionsCache[instanceName] = new WeakMap();
  118. }
  119. const cache = loaderOptionsCache[instanceName];
  120. if (cache.has(loaderOptions)) {
  121. return cache.get(loaderOptions);
  122. }
  123. validateLoaderOptions(loaderOptions);
  124. const options = makeLoaderOptions(instanceName, loaderOptions, loaderContext);
  125. cache.set(loaderOptions, options);
  126. return options;
  127. }
  128. const validLoaderOptions = [
  129. 'silent',
  130. 'logLevel',
  131. 'logInfoToStdOut',
  132. 'instance',
  133. 'compiler',
  134. 'context',
  135. 'configFile',
  136. 'transpileOnly',
  137. 'ignoreDiagnostics',
  138. 'errorFormatter',
  139. 'colors',
  140. 'compilerOptions',
  141. 'appendTsSuffixTo',
  142. 'appendTsxSuffixTo',
  143. 'onlyCompileBundledFiles',
  144. 'happyPackMode',
  145. 'getCustomTransformers',
  146. 'reportFiles',
  147. 'experimentalWatchApi',
  148. 'allowTsInNodeModules',
  149. 'experimentalFileCaching',
  150. 'projectReferences',
  151. 'resolveModuleName',
  152. 'resolveTypeReferenceDirective',
  153. 'useCaseSensitiveFileNames',
  154. ];
  155. /**
  156. * Validate the supplied loader options.
  157. * At present this validates the option names only; in future we may look at validating the values too
  158. * @param loaderOptions
  159. */
  160. function validateLoaderOptions(loaderOptions) {
  161. const loaderOptionKeys = Object.keys(loaderOptions);
  162. for (let i = 0; i < loaderOptionKeys.length; i++) {
  163. const option = loaderOptionKeys[i];
  164. const isUnexpectedOption = validLoaderOptions.indexOf(option) === -1;
  165. if (isUnexpectedOption) {
  166. throw new Error(`ts-loader was supplied with an unexpected loader option: ${option}
  167. Please take a look at the options you are supplying; the following are valid options:
  168. ${validLoaderOptions.join(' / ')}
  169. `);
  170. }
  171. }
  172. if (loaderOptions.context !== undefined &&
  173. !path.isAbsolute(loaderOptions.context)) {
  174. throw new Error(`Option 'context' has to be an absolute path. Given '${loaderOptions.context}'.`);
  175. }
  176. }
  177. function makeLoaderOptions(instanceName, loaderOptions, loaderContext) {
  178. var _a;
  179. const hasForkTsCheckerWebpackPlugin = (_a = loaderContext._compiler) === null || _a === void 0 ? void 0 : _a.options.plugins.some(plugin => {
  180. var _a;
  181. return typeof plugin === 'object' &&
  182. ((_a = plugin.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'ForkTsCheckerWebpackPlugin';
  183. });
  184. const options = Object.assign({}, {
  185. silent: false,
  186. logLevel: 'WARN',
  187. logInfoToStdOut: false,
  188. compiler: 'typescript',
  189. context: undefined,
  190. // Set default transpileOnly to true if there is an instance of ForkTsCheckerWebpackPlugin
  191. transpileOnly: hasForkTsCheckerWebpackPlugin,
  192. compilerOptions: {},
  193. appendTsSuffixTo: [],
  194. appendTsxSuffixTo: [],
  195. transformers: {},
  196. happyPackMode: false,
  197. colors: true,
  198. onlyCompileBundledFiles: false,
  199. reportFiles: [],
  200. // When the watch API usage stabilises look to remove this option and make watch usage the default behaviour when available
  201. experimentalWatchApi: false,
  202. allowTsInNodeModules: false,
  203. experimentalFileCaching: true,
  204. }, loaderOptions);
  205. options.ignoreDiagnostics = (0, utils_1.arrify)(options.ignoreDiagnostics).map(Number);
  206. options.logLevel = options.logLevel.toUpperCase();
  207. options.instance = instanceName;
  208. options.configFile = options.configFile || 'tsconfig.json';
  209. // happypack can be used only together with transpileOnly mode
  210. options.transpileOnly = options.happyPackMode ? true : options.transpileOnly;
  211. return options;
  212. }
  213. /**
  214. * Either add file to the overall files cache or update it in the cache when the file contents have changed
  215. * Also add the file to the modified files
  216. */
  217. function updateFileInCache(options, filePath, contents, instance) {
  218. let fileWatcherEventKind;
  219. // Update file contents
  220. const key = instance.filePathKeyMapper(filePath);
  221. let file = instance.files.get(key);
  222. if (file === undefined) {
  223. file = instance.otherFiles.get(key);
  224. if (file !== undefined) {
  225. if (!(0, utils_1.isReferencedFile)(instance, filePath)) {
  226. instance.otherFiles.delete(key);
  227. instance.files.set(key, file);
  228. instance.changedFilesList = true;
  229. }
  230. }
  231. else {
  232. if (instance.watchHost !== undefined) {
  233. fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Created;
  234. }
  235. file = { fileName: filePath, version: 0 };
  236. if (!(0, utils_1.isReferencedFile)(instance, filePath)) {
  237. instance.files.set(key, file);
  238. instance.changedFilesList = true;
  239. }
  240. }
  241. }
  242. if (instance.watchHost !== undefined && contents === undefined) {
  243. fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Deleted;
  244. }
  245. // filePath is a root file as it was passed to the loader. But it
  246. // could have been found earlier as a dependency of another file. If
  247. // that is the case, compiling this file changes the structure of
  248. // the program and we need to increase the instance version.
  249. //
  250. // See https://github.com/TypeStrong/ts-loader/issues/943
  251. if (!(0, utils_1.isReferencedFile)(instance, filePath) &&
  252. !instance.rootFileNames.has(filePath) &&
  253. // however, be careful not to add files from node_modules unless
  254. // it is allowed by the options.
  255. (options.allowTsInNodeModules || filePath.indexOf('node_modules') === -1)) {
  256. instance.version++;
  257. instance.rootFileNames.add(filePath);
  258. }
  259. if (file.text !== contents) {
  260. file.version++;
  261. file.text = contents;
  262. file.modifiedTime = new Date();
  263. instance.version++;
  264. if (instance.watchHost !== undefined &&
  265. fileWatcherEventKind === undefined) {
  266. fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Changed;
  267. }
  268. }
  269. // Added in case the files were already updated by the watch API
  270. if (instance.modifiedFiles && instance.modifiedFiles.get(key)) {
  271. fileWatcherEventKind = instance.compiler.FileWatcherEventKind.Changed;
  272. }
  273. if (instance.watchHost !== undefined && fileWatcherEventKind !== undefined) {
  274. instance.hasUnaccountedModifiedFiles =
  275. instance.watchHost.invokeFileWatcher(filePath, fileWatcherEventKind) ||
  276. instance.hasUnaccountedModifiedFiles;
  277. }
  278. // push this file to modified files hash.
  279. if (!instance.modifiedFiles) {
  280. instance.modifiedFiles = new Map();
  281. }
  282. instance.modifiedFiles.set(key, true);
  283. return file.version;
  284. }
  285. function getEmit(rawFilePath, filePath, instance, loaderContext) {
  286. var _a;
  287. const outputFiles = (0, instances_1.getEmitOutput)(instance, filePath);
  288. loaderContext.clearDependencies();
  289. loaderContext.addDependency(rawFilePath);
  290. const dependencies = [];
  291. const addDependency = (file) => {
  292. file = path.resolve(file);
  293. loaderContext.addDependency(file);
  294. dependencies.push(file);
  295. };
  296. // Make this file dependent on *all* definition files in the program
  297. if (!(0, utils_1.isReferencedFile)(instance, filePath)) {
  298. for (const { fileName: defFilePath } of instance.files.values()) {
  299. if (defFilePath.match(constants.dtsDtsxOrDtsDtsxMapRegex) &&
  300. // Remove the project reference d.ts as we are adding dependency for .ts later
  301. // This removed extra build pass (resulting in new stats object in initial build)
  302. !((_a = instance.solutionBuilderHost) === null || _a === void 0 ? void 0 : _a.getOutputFileKeyFromReferencedProject(defFilePath))) {
  303. addDependency(defFilePath);
  304. }
  305. }
  306. }
  307. // Additionally make this file dependent on all imported files
  308. const fileDependencies = instance.dependencyGraph.get(instance.filePathKeyMapper(filePath));
  309. if (fileDependencies) {
  310. for (const { resolvedFileName, originalFileName } of fileDependencies) {
  311. // In the case of dependencies that are part of a project reference,
  312. // the real dependency that webpack should watch is the JS output file.
  313. addDependency((0, instances_1.getInputFileNameFromOutput)(instance, path.resolve(resolvedFileName)) ||
  314. originalFileName);
  315. }
  316. }
  317. addDependenciesFromSolutionBuilder(instance, filePath, addDependency);
  318. loaderContext._module.buildMeta.tsLoaderDefinitionFileVersions =
  319. dependencies.map(defFilePath => path.relative(loaderContext.rootContext, defFilePath) +
  320. '@' +
  321. ((0, utils_1.isReferencedFile)(instance, defFilePath)
  322. ? instance
  323. .solutionBuilderHost.getInputFileStamp(defFilePath)
  324. .toString()
  325. : (instance.files.get(instance.filePathKeyMapper(defFilePath)) ||
  326. instance.otherFiles.get(instance.filePathKeyMapper(defFilePath)) || {
  327. version: '?',
  328. }).version));
  329. return getOutputAndSourceMapFromOutputFiles(outputFiles);
  330. }
  331. function getOutputAndSourceMapFromOutputFiles(outputFiles) {
  332. const outputFile = outputFiles
  333. .filter(file => file.name.match(constants.jsJsx))
  334. .pop();
  335. const outputText = outputFile === undefined ? undefined : outputFile.text;
  336. const sourceMapFile = outputFiles
  337. .filter(file => file.name.match(constants.jsJsxMap))
  338. .pop();
  339. const sourceMapText = sourceMapFile === undefined ? undefined : sourceMapFile.text;
  340. return { outputText, sourceMapText };
  341. }
  342. function addDependenciesFromSolutionBuilder(instance, filePath, addDependency) {
  343. if (!instance.solutionBuilderHost) {
  344. return;
  345. }
  346. // Add all the input files from the references as
  347. const resolvedFilePath = instance.filePathKeyMapper(filePath);
  348. if (!(0, utils_1.isReferencedFile)(instance, filePath)) {
  349. if (instance.configParseResult.fileNames.some(f => instance.filePathKeyMapper(f) === resolvedFilePath)) {
  350. addDependenciesFromProjectReferences(instance, instance.configFilePath, instance.configParseResult.projectReferences, addDependency);
  351. }
  352. return;
  353. }
  354. // Referenced file find the config for it
  355. for (const [configFile, configInfo,] of instance.solutionBuilderHost.configFileInfo.entries()) {
  356. if (!configInfo.config ||
  357. !configInfo.config.projectReferences ||
  358. !configInfo.config.projectReferences.length) {
  359. continue;
  360. }
  361. if (configInfo.outputFileNames) {
  362. if (!configInfo.outputFileNames.has(resolvedFilePath)) {
  363. continue;
  364. }
  365. }
  366. else if (!configInfo.config.fileNames.some(f => instance.filePathKeyMapper(f) === resolvedFilePath)) {
  367. continue;
  368. }
  369. // Depend on all the dts files from the program
  370. if (configInfo.dtsFiles) {
  371. configInfo.dtsFiles.forEach(addDependency);
  372. }
  373. addDependenciesFromProjectReferences(instance, configFile, configInfo.config.projectReferences, addDependency);
  374. break;
  375. }
  376. }
  377. function addDependenciesFromProjectReferences(instance, configFile, projectReferences, addDependency) {
  378. if (!projectReferences || !projectReferences.length) {
  379. return;
  380. }
  381. // This is the config for the input file
  382. const seenMap = new Map();
  383. seenMap.set(instance.filePathKeyMapper(configFile), true);
  384. // Add dependencies to all the input files from the project reference files since building them
  385. const queue = projectReferences.slice();
  386. while (true) {
  387. const currentRef = queue.pop();
  388. if (!currentRef) {
  389. break;
  390. }
  391. const refConfigFile = instance.filePathKeyMapper(instance.compiler.resolveProjectReferencePath(currentRef));
  392. if (seenMap.has(refConfigFile)) {
  393. continue;
  394. }
  395. const refConfigInfo = instance.solutionBuilderHost.configFileInfo.get(refConfigFile);
  396. if (!refConfigInfo) {
  397. continue;
  398. }
  399. seenMap.set(refConfigFile, true);
  400. if (refConfigInfo.config) {
  401. refConfigInfo.config.fileNames.forEach(addDependency);
  402. if (refConfigInfo.config.projectReferences) {
  403. queue.push(...refConfigInfo.config.projectReferences);
  404. }
  405. }
  406. }
  407. }
  408. /**
  409. * Transpile file
  410. */
  411. function getTranspilationEmit(fileName, contents, instance, loaderContext) {
  412. if ((0, utils_1.isReferencedFile)(instance, fileName)) {
  413. const outputFiles = instance.solutionBuilderHost.getOutputFilesFromReferencedProjectInput(fileName);
  414. addDependenciesFromSolutionBuilder(instance, fileName, file => loaderContext.addDependency(path.resolve(file)));
  415. return getOutputAndSourceMapFromOutputFiles(outputFiles);
  416. }
  417. const { outputText, sourceMapText, diagnostics } = instance.compiler.transpileModule(contents, {
  418. compilerOptions: { ...instance.compilerOptions, rootDir: undefined },
  419. transformers: instance.transformers,
  420. reportDiagnostics: true,
  421. fileName,
  422. });
  423. const module = loaderContext._module;
  424. addDependenciesFromSolutionBuilder(instance, fileName, file => loaderContext.addDependency(path.resolve(file)));
  425. // _module.errors is not available inside happypack - see https://github.com/TypeStrong/ts-loader/issues/336
  426. if (!instance.loaderOptions.happyPackMode) {
  427. const errors = (0, utils_1.formatErrors)(diagnostics, instance.loaderOptions, instance.colors, instance.compiler, { module }, loaderContext.context);
  428. errors.forEach(error => module.addError(error));
  429. }
  430. return { outputText, sourceMapText };
  431. }
  432. function makeSourceMap(sourceMapText, outputText, filePath, contents, loaderContext) {
  433. if (sourceMapText === undefined) {
  434. return { output: outputText, sourceMap: undefined };
  435. }
  436. return {
  437. output: outputText.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
  438. sourceMap: Object.assign(JSON.parse(sourceMapText), {
  439. sources: [loaderContext.remainingRequest],
  440. file: filePath,
  441. sourcesContent: [contents],
  442. }),
  443. };
  444. }
  445. /**
  446. * This method maps the newly generated @param{sourceMap} to the input source map.
  447. * This is required when ts-loader is not the first loader in the Webpack loader chain.
  448. */
  449. function mapToInputSourceMap(sourceMap, loaderContext, inputSourceMap) {
  450. return new Promise((resolve, reject) => {
  451. const inMap = {
  452. file: loaderContext.remainingRequest,
  453. mappings: inputSourceMap.mappings,
  454. names: inputSourceMap.names,
  455. sources: inputSourceMap.sources,
  456. sourceRoot: inputSourceMap.sourceRoot,
  457. sourcesContent: inputSourceMap.sourcesContent,
  458. version: inputSourceMap.version,
  459. };
  460. Promise.all([
  461. new source_map_1.SourceMapConsumer(inMap),
  462. new source_map_1.SourceMapConsumer(sourceMap),
  463. ])
  464. .then(sourceMapConsumers => {
  465. try {
  466. const generator = source_map_1.SourceMapGenerator.fromSourceMap(sourceMapConsumers[1]);
  467. generator.applySourceMap(sourceMapConsumers[0]);
  468. const mappedSourceMap = generator.toJSON();
  469. // before resolving, we free memory by calling destroy on the source map consumers
  470. sourceMapConsumers.forEach(sourceMapConsumer => sourceMapConsumer.destroy());
  471. resolve(mappedSourceMap);
  472. }
  473. catch (e) {
  474. //before rejecting, we free memory by calling destroy on the source map consumers
  475. sourceMapConsumers.forEach(sourceMapConsumer => sourceMapConsumer.destroy());
  476. reject(e);
  477. }
  478. })
  479. .catch(reject);
  480. });
  481. }
  482. module.exports = loader;
  483. //# sourceMappingURL=index.js.map