index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. 'use strict';
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. var _crypto = require('crypto');
  6. var _crypto2 = _interopRequireDefault(_crypto);
  7. var _path = require('path');
  8. var _path2 = _interopRequireDefault(_path);
  9. var _sourceMap = require('source-map');
  10. var _webpackSources = require('webpack-sources');
  11. var _RequestShortener = require('webpack/lib/RequestShortener');
  12. var _RequestShortener2 = _interopRequireDefault(_RequestShortener);
  13. var _ModuleFilenameHelpers = require('webpack/lib/ModuleFilenameHelpers');
  14. var _ModuleFilenameHelpers2 = _interopRequireDefault(_ModuleFilenameHelpers);
  15. var _schemaUtils = require('schema-utils');
  16. var _schemaUtils2 = _interopRequireDefault(_schemaUtils);
  17. var _options = require('./options.json');
  18. var _options2 = _interopRequireDefault(_options);
  19. var _TaskRunner = require('./TaskRunner');
  20. var _TaskRunner2 = _interopRequireDefault(_TaskRunner);
  21. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  22. const warningRegex = /\[.+:([0-9]+),([0-9]+)\]/; /* eslint-disable
  23. no-param-reassign
  24. */
  25. class TerserPlugin {
  26. constructor(options = {}) {
  27. (0, _schemaUtils2.default)(_options2.default, options, 'Terser Plugin');
  28. const {
  29. minify,
  30. terserOptions = {},
  31. test = /\.js(\?.*)?$/i,
  32. warningsFilter = () => true,
  33. extractComments = false,
  34. sourceMap = false,
  35. cache = false,
  36. cacheKeys = defaultCacheKeys => defaultCacheKeys,
  37. parallel = false,
  38. include,
  39. exclude
  40. } = options;
  41. this.options = {
  42. test,
  43. warningsFilter,
  44. extractComments,
  45. sourceMap,
  46. cache,
  47. cacheKeys,
  48. parallel,
  49. include,
  50. exclude,
  51. minify,
  52. terserOptions: Object.assign({
  53. output: {
  54. comments: extractComments ? false : /^\**!|@preserve|@license|@cc_on/i
  55. }
  56. }, terserOptions)
  57. };
  58. }
  59. static isSourceMap(input) {
  60. // All required options for `new SourceMapConsumer(...options)`
  61. // https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap
  62. return Boolean(input && input.version && input.sources && Array.isArray(input.sources) && typeof input.mappings === 'string');
  63. }
  64. static buildSourceMap(inputSourceMap) {
  65. if (!inputSourceMap || !TerserPlugin.isSourceMap(inputSourceMap)) {
  66. return null;
  67. }
  68. return new _sourceMap.SourceMapConsumer(inputSourceMap);
  69. }
  70. static buildError(err, file, sourceMap, requestShortener) {
  71. // Handling error which should have line, col, filename and message
  72. if (err.line) {
  73. const original = sourceMap && sourceMap.originalPositionFor({
  74. line: err.line,
  75. column: err.col
  76. });
  77. if (original && original.source && requestShortener) {
  78. return new Error(`${file} from Terser\n${err.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${err.line},${err.col}]`);
  79. }
  80. return new Error(`${file} from Terser\n${err.message} [${file}:${err.line},${err.col}]`);
  81. } else if (err.stack) {
  82. return new Error(`${file} from Terser\n${err.stack}`);
  83. }
  84. return new Error(`${file} from Terser\n${err.message}`);
  85. }
  86. static buildWarning(warning, file, sourceMap, requestShortener, warningsFilter) {
  87. let warningMessage = warning;
  88. let locationMessage = '';
  89. let source = null;
  90. if (sourceMap) {
  91. const match = warningRegex.exec(warning);
  92. if (match) {
  93. const line = +match[1];
  94. const column = +match[2];
  95. const original = sourceMap.originalPositionFor({
  96. line,
  97. column
  98. });
  99. if (original && original.source && original.source !== file && requestShortener) {
  100. ({ source } = original);
  101. warningMessage = `${warningMessage.replace(warningRegex, '')}`;
  102. locationMessage = `[${requestShortener.shorten(original.source)}:${original.line},${original.column}]`;
  103. }
  104. }
  105. }
  106. if (warningsFilter && !warningsFilter(warning, source)) {
  107. return null;
  108. }
  109. return `Terser Plugin: ${warningMessage}${locationMessage}`;
  110. }
  111. apply(compiler) {
  112. const buildModuleFn = moduleArg => {
  113. // to get detailed location info about errors
  114. moduleArg.useSourceMap = true;
  115. };
  116. const optimizeFn = (compilation, chunks, callback) => {
  117. const taskRunner = new _TaskRunner2.default({
  118. cache: this.options.cache,
  119. parallel: this.options.parallel
  120. });
  121. const processedAssets = new WeakSet();
  122. const tasks = [];
  123. chunks.reduce((acc, chunk) => acc.concat(chunk.files || []), []).concat(compilation.additionalChunkAssets || []).filter(_ModuleFilenameHelpers2.default.matchObject.bind(null, this.options)).forEach(file => {
  124. let inputSourceMap;
  125. const asset = compilation.assets[file];
  126. if (processedAssets.has(asset)) {
  127. return;
  128. }
  129. try {
  130. let input;
  131. if (this.options.sourceMap && asset.sourceAndMap) {
  132. const { source, map } = asset.sourceAndMap();
  133. input = source;
  134. if (TerserPlugin.isSourceMap(map)) {
  135. inputSourceMap = map;
  136. } else {
  137. inputSourceMap = map;
  138. compilation.warnings.push(new Error(`${file} contains invalid source map`));
  139. }
  140. } else {
  141. input = asset.source();
  142. inputSourceMap = null;
  143. }
  144. // Handling comment extraction
  145. let commentsFile = false;
  146. if (this.options.extractComments) {
  147. commentsFile = this.options.extractComments.filename || `${file}.LICENSE`;
  148. if (typeof commentsFile === 'function') {
  149. commentsFile = commentsFile(file);
  150. }
  151. }
  152. const task = {
  153. file,
  154. input,
  155. inputSourceMap,
  156. commentsFile,
  157. extractComments: this.options.extractComments,
  158. terserOptions: this.options.terserOptions,
  159. minify: this.options.minify
  160. };
  161. if (this.options.cache) {
  162. const { outputPath } = compiler;
  163. const defaultCacheKeys = {
  164. // eslint-disable-next-line global-require
  165. terser: require('terser/package.json').version,
  166. // eslint-disable-next-line global-require
  167. 'terser-webpack-plugin': require('../package.json').version,
  168. 'terser-webpack-plugin-options': this.options,
  169. path: `${outputPath ? `${outputPath}/` : ''}${file}`,
  170. hash: _crypto2.default.createHash('md4').update(input).digest('hex')
  171. };
  172. task.cacheKeys = this.options.cacheKeys(defaultCacheKeys, file);
  173. }
  174. tasks.push(task);
  175. } catch (error) {
  176. compilation.errors.push(TerserPlugin.buildError(error, file, TerserPlugin.buildSourceMap(inputSourceMap), new _RequestShortener2.default(compiler.context)));
  177. }
  178. });
  179. taskRunner.run(tasks, (tasksError, results) => {
  180. if (tasksError) {
  181. compilation.errors.push(tasksError);
  182. return;
  183. }
  184. results.forEach((data, index) => {
  185. const { file, input, inputSourceMap, commentsFile } = tasks[index];
  186. const { error, map, code, warnings, extractedComments } = data;
  187. let sourceMap = null;
  188. if (error || warnings && warnings.length > 0) {
  189. sourceMap = TerserPlugin.buildSourceMap(inputSourceMap);
  190. }
  191. // Handling results
  192. // Error case: add errors, and go to next file
  193. if (error) {
  194. compilation.errors.push(TerserPlugin.buildError(error, file, sourceMap, new _RequestShortener2.default(compiler.context)));
  195. return;
  196. }
  197. let outputSource;
  198. if (map) {
  199. outputSource = new _webpackSources.SourceMapSource(code, file, JSON.parse(map), input, inputSourceMap);
  200. } else {
  201. outputSource = new _webpackSources.RawSource(code);
  202. }
  203. // Write extracted comments to commentsFile
  204. if (commentsFile && extractedComments.length > 0) {
  205. // Add a banner to the original file
  206. if (this.options.extractComments.banner !== false) {
  207. let banner = this.options.extractComments.banner || `For license information please see ${_path2.default.posix.basename(commentsFile)}`;
  208. if (typeof banner === 'function') {
  209. banner = banner(commentsFile);
  210. }
  211. if (banner) {
  212. outputSource = new _webpackSources.ConcatSource(`/*! ${banner} */\n`, outputSource);
  213. }
  214. }
  215. const commentsSource = new _webpackSources.RawSource(`${extractedComments.join('\n\n')}\n`);
  216. if (commentsFile in compilation.assets) {
  217. // commentsFile already exists, append new comments...
  218. if (compilation.assets[commentsFile] instanceof _webpackSources.ConcatSource) {
  219. compilation.assets[commentsFile].add('\n');
  220. compilation.assets[commentsFile].add(commentsSource);
  221. } else {
  222. compilation.assets[commentsFile] = new _webpackSources.ConcatSource(compilation.assets[commentsFile], '\n', commentsSource);
  223. }
  224. } else {
  225. compilation.assets[commentsFile] = commentsSource;
  226. }
  227. }
  228. // Updating assets
  229. processedAssets.add(compilation.assets[file] = outputSource);
  230. // Handling warnings
  231. if (warnings && warnings.length > 0) {
  232. warnings.forEach(warning => {
  233. const builtWarning = TerserPlugin.buildWarning(warning, file, sourceMap, new _RequestShortener2.default(compiler.context), this.options.warningsFilter);
  234. if (builtWarning) {
  235. compilation.warnings.push(builtWarning);
  236. }
  237. });
  238. }
  239. });
  240. taskRunner.exit();
  241. callback();
  242. });
  243. };
  244. const plugin = { name: this.constructor.name };
  245. compiler.hooks.compilation.tap(plugin, compilation => {
  246. if (this.options.sourceMap) {
  247. compilation.hooks.buildModule.tap(plugin, buildModuleFn);
  248. }
  249. compilation.hooks.optimizeChunkAssets.tapAsync(plugin, optimizeFn.bind(this, compilation));
  250. });
  251. }
  252. }
  253. exports.default = TerserPlugin;