index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _path = _interopRequireDefault(require("path"));
  7. var _sourceMap = require("source-map");
  8. var _webpackSources = require("webpack-sources");
  9. var _RequestShortener = _interopRequireDefault(require("webpack/lib/RequestShortener"));
  10. var _webpack = require("webpack");
  11. var _schemaUtils = _interopRequireDefault(require("schema-utils"));
  12. var _serializeJavascript = _interopRequireDefault(require("serialize-javascript"));
  13. var _package = _interopRequireDefault(require("terser/package.json"));
  14. var _options = _interopRequireDefault(require("./options.json"));
  15. var _TaskRunner = _interopRequireDefault(require("./TaskRunner"));
  16. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  17. const warningRegex = /\[.+:([0-9]+),([0-9]+)\]/;
  18. class TerserPlugin {
  19. constructor(options = {}) {
  20. (0, _schemaUtils.default)(_options.default, options, {
  21. name: 'Terser Plugin',
  22. baseDataPath: 'options'
  23. });
  24. const {
  25. minify,
  26. terserOptions = {},
  27. test = /\.m?js(\?.*)?$/i,
  28. chunkFilter = () => true,
  29. warningsFilter = () => true,
  30. extractComments = true,
  31. sourceMap,
  32. cache = true,
  33. cacheKeys = defaultCacheKeys => defaultCacheKeys,
  34. parallel = true,
  35. include,
  36. exclude
  37. } = options;
  38. this.options = {
  39. test,
  40. chunkFilter,
  41. warningsFilter,
  42. extractComments,
  43. sourceMap,
  44. cache,
  45. cacheKeys,
  46. parallel,
  47. include,
  48. exclude,
  49. minify,
  50. terserOptions
  51. };
  52. }
  53. static isSourceMap(input) {
  54. // All required options for `new SourceMapConsumer(...options)`
  55. // https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap
  56. return Boolean(input && input.version && input.sources && Array.isArray(input.sources) && typeof input.mappings === 'string');
  57. }
  58. static buildSourceMap(inputSourceMap) {
  59. if (!inputSourceMap || !TerserPlugin.isSourceMap(inputSourceMap)) {
  60. return null;
  61. }
  62. return new _sourceMap.SourceMapConsumer(inputSourceMap);
  63. }
  64. static buildError(error, file, sourceMap, requestShortener) {
  65. // Handling error which should have line, col, filename and message
  66. if (error.line) {
  67. const original = sourceMap && sourceMap.originalPositionFor({
  68. line: error.line,
  69. column: error.col
  70. });
  71. if (original && original.source && requestShortener) {
  72. return new Error(`${file} from Terser\n${error.message} [${requestShortener.shorten(original.source)}:${original.line},${original.column}][${file}:${error.line},${error.col}]${error.stack ? `\n${error.stack.split('\n').slice(1).join('\n')}` : ''}`);
  73. }
  74. return new Error(`${file} from Terser\n${error.message} [${file}:${error.line},${error.col}]${error.stack ? `\n${error.stack.split('\n').slice(1).join('\n')}` : ''}`);
  75. }
  76. if (error.stack) {
  77. return new Error(`${file} from Terser\n${error.stack}`);
  78. }
  79. return new Error(`${file} from Terser\n${error.message}`);
  80. }
  81. static buildWarning(warning, file, sourceMap, requestShortener, warningsFilter) {
  82. let warningMessage = warning;
  83. let locationMessage = '';
  84. let source = null;
  85. if (sourceMap) {
  86. const match = warningRegex.exec(warning);
  87. if (match) {
  88. const line = +match[1];
  89. const column = +match[2];
  90. const original = sourceMap.originalPositionFor({
  91. line,
  92. column
  93. });
  94. if (original && original.source && original.source !== file && requestShortener) {
  95. ({
  96. source
  97. } = original);
  98. warningMessage = `${warningMessage.replace(warningRegex, '')}`;
  99. locationMessage = `[${requestShortener.shorten(original.source)}:${original.line},${original.column}]`;
  100. }
  101. }
  102. } // Todo change order in next major release
  103. if (warningsFilter && !warningsFilter(warning, source, file)) {
  104. return null;
  105. }
  106. return `Terser Plugin: ${warningMessage}${locationMessage}`;
  107. }
  108. static removeQueryString(filename) {
  109. let targetFilename = filename;
  110. const queryStringIdx = targetFilename.indexOf('?');
  111. if (queryStringIdx >= 0) {
  112. targetFilename = targetFilename.substr(0, queryStringIdx);
  113. }
  114. return targetFilename;
  115. }
  116. static hasAsset(commentFilename, assets) {
  117. const assetFilenames = Object.keys(assets).map(assetFilename => TerserPlugin.removeQueryString(assetFilename));
  118. return assetFilenames.includes(TerserPlugin.removeQueryString(commentFilename));
  119. }
  120. static isWebpack4() {
  121. return _webpack.version[0] === '4';
  122. }
  123. *taskGenerator(compiler, compilation, allExtractedComments, file) {
  124. let inputSourceMap;
  125. const asset = compilation.assets[file];
  126. try {
  127. let input;
  128. if (this.options.sourceMap && asset.sourceAndMap) {
  129. const {
  130. source,
  131. map
  132. } = 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. } // Handling comment extraction
  144. let commentsFilename = false;
  145. if (this.options.extractComments) {
  146. commentsFilename = this.options.extractComments.filename || '[file].LICENSE.txt[query]';
  147. if (TerserPlugin.isWebpack4()) {
  148. // Todo remove this in next major release
  149. if (typeof commentsFilename === 'function') {
  150. commentsFilename = commentsFilename.bind(null, file);
  151. }
  152. }
  153. let query = '';
  154. let filename = file;
  155. const querySplit = filename.indexOf('?');
  156. if (querySplit >= 0) {
  157. query = filename.substr(querySplit);
  158. filename = filename.substr(0, querySplit);
  159. }
  160. const lastSlashIndex = filename.lastIndexOf('/');
  161. const basename = lastSlashIndex === -1 ? filename : filename.substr(lastSlashIndex + 1);
  162. const data = {
  163. filename,
  164. basename,
  165. query
  166. };
  167. commentsFilename = compilation.getPath(commentsFilename, data);
  168. }
  169. if (commentsFilename && TerserPlugin.hasAsset(commentsFilename, compilation.assets)) {
  170. // Todo make error and stop uglifing in next major release
  171. compilation.warnings.push(new Error(`The comment file "${TerserPlugin.removeQueryString(commentsFilename)}" conflicts with an existing asset, this may lead to code corruption, please use a different name`));
  172. }
  173. const callback = taskResult => {
  174. let {
  175. code
  176. } = taskResult;
  177. const {
  178. error,
  179. map,
  180. warnings
  181. } = taskResult;
  182. const {
  183. extractedComments
  184. } = taskResult;
  185. let sourceMap = null;
  186. if (error || warnings && warnings.length > 0) {
  187. sourceMap = TerserPlugin.buildSourceMap(inputSourceMap);
  188. } // Handling results
  189. // Error case: add errors, and go to next file
  190. if (error) {
  191. compilation.errors.push(TerserPlugin.buildError(error, file, sourceMap, new _RequestShortener.default(compiler.context)));
  192. return;
  193. }
  194. const hasExtractedComments = commentsFilename && extractedComments && extractedComments.length > 0;
  195. const hasBannerForExtractedComments = hasExtractedComments && this.options.extractComments.banner !== false;
  196. let outputSource;
  197. let shebang;
  198. if (hasExtractedComments && hasBannerForExtractedComments && code.startsWith('#!')) {
  199. const firstNewlinePosition = code.indexOf('\n');
  200. shebang = code.substring(0, firstNewlinePosition);
  201. code = code.substring(firstNewlinePosition + 1);
  202. }
  203. if (map) {
  204. outputSource = new _webpackSources.SourceMapSource(code, file, map, input, inputSourceMap, true);
  205. } else {
  206. outputSource = new _webpackSources.RawSource(code);
  207. } // Write extracted comments to commentsFilename
  208. if (hasExtractedComments) {
  209. if (!allExtractedComments[commentsFilename]) {
  210. // eslint-disable-next-line no-param-reassign
  211. allExtractedComments[commentsFilename] = [];
  212. } // eslint-disable-next-line no-param-reassign
  213. allExtractedComments[commentsFilename] = allExtractedComments[commentsFilename].concat(extractedComments); // Add a banner to the original file
  214. if (hasBannerForExtractedComments) {
  215. let banner = this.options.extractComments.banner || `For license information please see ${_path.default.relative(_path.default.dirname(file), commentsFilename).replace(/\\/g, '/')}`;
  216. if (typeof banner === 'function') {
  217. banner = banner(commentsFilename);
  218. }
  219. if (banner) {
  220. outputSource = new _webpackSources.ConcatSource(shebang ? `${shebang}\n` : '', `/*! ${banner} */\n`, outputSource);
  221. }
  222. }
  223. } // Updating assets
  224. // eslint-disable-next-line no-param-reassign
  225. compilation.assets[file] = outputSource; // Handling warnings
  226. if (warnings && warnings.length > 0) {
  227. warnings.forEach(warning => {
  228. const builtWarning = TerserPlugin.buildWarning(warning, file, sourceMap, new _RequestShortener.default(compiler.context), this.options.warningsFilter);
  229. if (builtWarning) {
  230. compilation.warnings.push(builtWarning);
  231. }
  232. });
  233. }
  234. };
  235. const task = {
  236. asset,
  237. file,
  238. input,
  239. inputSourceMap,
  240. commentsFilename,
  241. extractComments: this.options.extractComments,
  242. terserOptions: this.options.terserOptions,
  243. minify: this.options.minify,
  244. callback
  245. };
  246. if (TerserPlugin.isWebpack4()) {
  247. const {
  248. outputOptions: {
  249. hashSalt,
  250. hashDigest,
  251. hashDigestLength,
  252. hashFunction
  253. }
  254. } = compilation;
  255. const hash = _webpack.util.createHash(hashFunction);
  256. if (hashSalt) {
  257. hash.update(hashSalt);
  258. }
  259. hash.update(input);
  260. const digest = hash.digest(hashDigest);
  261. if (this.options.cache) {
  262. const defaultCacheKeys = {
  263. terser: _package.default.version,
  264. // eslint-disable-next-line global-require
  265. 'terser-webpack-plugin': require('../package.json').version,
  266. 'terser-webpack-plugin-options': this.options,
  267. nodeVersion: process.version,
  268. filename: file,
  269. contentHash: digest.substr(0, hashDigestLength)
  270. };
  271. task.cacheKeys = this.options.cacheKeys(defaultCacheKeys, file);
  272. }
  273. } else {
  274. task.cacheKeys = {
  275. terser: _package.default.version,
  276. // eslint-disable-next-line global-require
  277. 'terser-webpack-plugin': require('../package.json').version,
  278. 'terser-webpack-plugin-options': this.options
  279. };
  280. }
  281. yield task;
  282. } catch (error) {
  283. compilation.errors.push(TerserPlugin.buildError(error, file, TerserPlugin.buildSourceMap(inputSourceMap), new _RequestShortener.default(compiler.context)));
  284. }
  285. }
  286. apply(compiler) {
  287. const {
  288. devtool,
  289. output,
  290. plugins
  291. } = compiler.options;
  292. this.options.sourceMap = typeof this.options.sourceMap === 'undefined' ? devtool && !devtool.includes('eval') && !devtool.includes('cheap') && (devtool.includes('source-map') || // Todo remove when `webpack@5` support will be dropped
  293. devtool.includes('sourcemap')) || plugins && plugins.some(plugin => plugin instanceof _webpack.SourceMapDevToolPlugin && plugin.options && plugin.options.columns) : Boolean(this.options.sourceMap);
  294. if (typeof this.options.terserOptions.module === 'undefined' && typeof output.module !== 'undefined') {
  295. this.options.terserOptions.module = output.module;
  296. }
  297. if (typeof this.options.terserOptions.ecma === 'undefined' && typeof output.ecmaVersion !== 'undefined') {
  298. this.options.terserOptions.ecma = output.ecmaVersion;
  299. }
  300. const optimizeFn = async (compilation, chunks) => {
  301. const matchObject = _webpack.ModuleFilenameHelpers.matchObject.bind( // eslint-disable-next-line no-undefined
  302. undefined, this.options);
  303. const files = [].concat(Array.from(compilation.additionalChunkAssets || [])).concat(Array.from(chunks).filter(chunk => this.options.chunkFilter && this.options.chunkFilter(chunk)).reduce((acc, chunk) => acc.concat(Array.from(chunk.files || [])), [])).filter(file => matchObject(file));
  304. if (files.length === 0) {
  305. return Promise.resolve();
  306. }
  307. const CacheEngine = TerserPlugin.isWebpack4() ? // eslint-disable-next-line global-require
  308. require('./Webpack4Cache').default : // eslint-disable-next-line global-require
  309. require('./Webpack5Cache').default;
  310. const allExtractedComments = {};
  311. const taskGenerator = this.taskGenerator.bind(this, compiler, compilation, allExtractedComments);
  312. const taskRunner = new _TaskRunner.default({
  313. taskGenerator,
  314. files,
  315. cache: new CacheEngine(compiler, compilation, this.options),
  316. parallel: this.options.parallel
  317. });
  318. await taskRunner.run();
  319. await taskRunner.exit();
  320. Object.keys(allExtractedComments).forEach(commentsFilename => {
  321. const extractedComments = new Set([...allExtractedComments[commentsFilename].sort()]); // eslint-disable-next-line no-param-reassign
  322. compilation.assets[commentsFilename] = new _webpackSources.RawSource(`${Array.from(extractedComments).join('\n\n')}\n`);
  323. });
  324. return Promise.resolve();
  325. };
  326. const plugin = {
  327. name: this.constructor.name
  328. };
  329. compiler.hooks.compilation.tap(plugin, compilation => {
  330. if (this.options.sourceMap) {
  331. compilation.hooks.buildModule.tap(plugin, moduleArg => {
  332. // to get detailed location info about errors
  333. // eslint-disable-next-line no-param-reassign
  334. moduleArg.useSourceMap = true;
  335. });
  336. }
  337. if (!TerserPlugin.isWebpack4()) {
  338. const hooks = _webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation);
  339. const data = (0, _serializeJavascript.default)({
  340. terser: _package.default.version,
  341. terserOptions: this.options.terserOptions
  342. });
  343. hooks.chunkHash.tap(plugin, (chunk, hash) => {
  344. hash.update('TerserPlugin');
  345. hash.update(data);
  346. });
  347. } else {
  348. // Todo remove after drop `webpack@4` compatibility
  349. const {
  350. mainTemplate,
  351. chunkTemplate
  352. } = compilation;
  353. const data = (0, _serializeJavascript.default)({
  354. terser: _package.default.version,
  355. terserOptions: this.options.terserOptions
  356. }); // Regenerate `contenthash` for minified assets
  357. for (const template of [mainTemplate, chunkTemplate]) {
  358. template.hooks.hashForChunk.tap(plugin, hash => {
  359. hash.update('TerserPlugin');
  360. hash.update(data);
  361. });
  362. }
  363. }
  364. compilation.hooks.optimizeChunkAssets.tapPromise(plugin, optimizeFn.bind(this, compilation));
  365. });
  366. }
  367. }
  368. var _default = TerserPlugin;
  369. exports.default = _default;