index.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  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 _os = _interopRequireDefault(require("os"));
  8. var _sourceMap = require("source-map");
  9. var _webpack = _interopRequireWildcard(require("webpack"));
  10. var _RequestShortener = _interopRequireDefault(require("webpack/lib/RequestShortener"));
  11. var _schemaUtils = require("schema-utils");
  12. var _serializeJavascript = _interopRequireDefault(require("serialize-javascript"));
  13. var _package = _interopRequireDefault(require("terser/package.json"));
  14. var _pLimit = _interopRequireDefault(require("p-limit"));
  15. var _jestWorker = _interopRequireDefault(require("jest-worker"));
  16. var _options = _interopRequireDefault(require("./options.json"));
  17. var _minify = require("./minify");
  18. function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
  19. function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
  20. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  21. // webpack 5 exposes the sources property to ensure the right version of webpack-sources is used
  22. const {
  23. SourceMapSource,
  24. RawSource,
  25. ConcatSource
  26. } = // eslint-disable-next-line global-require
  27. _webpack.default.sources || require('webpack-sources');
  28. class TerserPlugin {
  29. constructor(options = {}) {
  30. (0, _schemaUtils.validate)(_options.default, options, {
  31. name: 'Terser Plugin',
  32. baseDataPath: 'options'
  33. });
  34. const {
  35. minify,
  36. terserOptions = {},
  37. test = /\.[cm]?js(\?.*)?$/i,
  38. extractComments = true,
  39. sourceMap,
  40. cache = true,
  41. cacheKeys = defaultCacheKeys => defaultCacheKeys,
  42. parallel = true,
  43. include,
  44. exclude
  45. } = options;
  46. this.options = {
  47. test,
  48. extractComments,
  49. sourceMap,
  50. cache,
  51. cacheKeys,
  52. parallel,
  53. include,
  54. exclude,
  55. minify,
  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 buildError(error, file, sourceMap, requestShortener) {
  65. if (error.line) {
  66. const original = sourceMap && sourceMap.originalPositionFor({
  67. line: error.line,
  68. column: error.col
  69. });
  70. if (original && original.source && requestShortener) {
  71. 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')}` : ''}`);
  72. }
  73. 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')}` : ''}`);
  74. }
  75. if (error.stack) {
  76. return new Error(`${file} from Terser\n${error.stack}`);
  77. }
  78. return new Error(`${file} from Terser\n${error.message}`);
  79. }
  80. static isWebpack4() {
  81. return _webpack.version[0] === '4';
  82. }
  83. static getAvailableNumberOfCores(parallel) {
  84. // In some cases cpus() returns undefined
  85. // https://github.com/nodejs/node/issues/19022
  86. const cpus = _os.default.cpus() || {
  87. length: 1
  88. };
  89. return parallel === true ? cpus.length - 1 : Math.min(Number(parallel) || 0, cpus.length - 1);
  90. } // eslint-disable-next-line consistent-return
  91. static getAsset(compilation, name) {
  92. // New API
  93. if (compilation.getAsset) {
  94. return compilation.getAsset(name);
  95. }
  96. /* istanbul ignore next */
  97. if (compilation.assets[name]) {
  98. return {
  99. name,
  100. source: compilation.assets[name],
  101. info: {}
  102. };
  103. }
  104. }
  105. static emitAsset(compilation, name, source, assetInfo) {
  106. // New API
  107. if (compilation.emitAsset) {
  108. compilation.emitAsset(name, source, assetInfo);
  109. } // eslint-disable-next-line no-param-reassign
  110. compilation.assets[name] = source;
  111. }
  112. static updateAsset(compilation, name, newSource, assetInfo) {
  113. // New API
  114. if (compilation.updateAsset) {
  115. compilation.updateAsset(name, newSource, assetInfo);
  116. } // eslint-disable-next-line no-param-reassign
  117. compilation.assets[name] = newSource;
  118. }
  119. async optimize(compiler, compilation, assets, CacheEngine, weakCache) {
  120. let assetNames;
  121. if (TerserPlugin.isWebpack4()) {
  122. assetNames = [].concat(Array.from(compilation.additionalChunkAssets || [])).concat( // In webpack@4 it is `chunks`
  123. Array.from(assets).reduce((acc, chunk) => acc.concat(Array.from(chunk.files || [])), [])).concat(Object.keys(compilation.assets)).filter((assetName, index, existingAssets) => existingAssets.indexOf(assetName) === index).filter(assetName => _webpack.ModuleFilenameHelpers.matchObject.bind( // eslint-disable-next-line no-undefined
  124. undefined, this.options)(assetName));
  125. } else {
  126. assetNames = Object.keys(assets).filter(assetName => _webpack.ModuleFilenameHelpers.matchObject.bind( // eslint-disable-next-line no-undefined
  127. undefined, this.options)(assetName));
  128. }
  129. if (assetNames.length === 0) {
  130. return;
  131. }
  132. const availableNumberOfCores = TerserPlugin.getAvailableNumberOfCores(this.options.parallel);
  133. let concurrency = Infinity;
  134. let worker;
  135. if (availableNumberOfCores > 0) {
  136. // Do not create unnecessary workers when the number of files is less than the available cores, it saves memory
  137. const numWorkers = Math.min(assetNames.length, availableNumberOfCores);
  138. concurrency = numWorkers;
  139. worker = new _jestWorker.default(require.resolve('./minify'), {
  140. numWorkers
  141. }); // https://github.com/facebook/jest/issues/8872#issuecomment-524822081
  142. const workerStdout = worker.getStdout();
  143. if (workerStdout) {
  144. workerStdout.on('data', chunk => {
  145. return process.stdout.write(chunk);
  146. });
  147. }
  148. const workerStderr = worker.getStderr();
  149. if (workerStderr) {
  150. workerStderr.on('data', chunk => {
  151. return process.stderr.write(chunk);
  152. });
  153. }
  154. }
  155. const limit = (0, _pLimit.default)(concurrency);
  156. const cache = new CacheEngine(compilation, {
  157. cache: this.options.cache
  158. }, weakCache);
  159. const allExtractedComments = new Map();
  160. const scheduledTasks = [];
  161. for (const name of assetNames) {
  162. scheduledTasks.push(limit(async () => {
  163. const {
  164. info,
  165. source: inputSource
  166. } = TerserPlugin.getAsset(compilation, name); // Skip double minimize assets from child compilation
  167. if (info.minimized) {
  168. return;
  169. }
  170. let input;
  171. let inputSourceMap; // TODO refactor after drop webpack@4, webpack@5 always has `sourceAndMap` on sources
  172. if (this.options.sourceMap && inputSource.sourceAndMap) {
  173. const {
  174. source,
  175. map
  176. } = inputSource.sourceAndMap();
  177. input = source;
  178. if (map) {
  179. if (TerserPlugin.isSourceMap(map)) {
  180. inputSourceMap = map;
  181. } else {
  182. inputSourceMap = map;
  183. compilation.warnings.push(new Error(`${name} contains invalid source map`));
  184. }
  185. }
  186. } else {
  187. input = inputSource.source();
  188. inputSourceMap = null;
  189. }
  190. if (Buffer.isBuffer(input)) {
  191. input = input.toString();
  192. }
  193. const cacheData = {
  194. name,
  195. inputSource
  196. };
  197. if (TerserPlugin.isWebpack4()) {
  198. if (this.options.cache) {
  199. const {
  200. outputOptions: {
  201. hashSalt,
  202. hashDigest,
  203. hashDigestLength,
  204. hashFunction
  205. }
  206. } = compilation;
  207. const hash = _webpack.util.createHash(hashFunction);
  208. if (hashSalt) {
  209. hash.update(hashSalt);
  210. }
  211. hash.update(input);
  212. const digest = hash.digest(hashDigest);
  213. cacheData.input = input;
  214. cacheData.inputSourceMap = inputSourceMap;
  215. cacheData.cacheKeys = this.options.cacheKeys({
  216. terser: _package.default.version,
  217. // eslint-disable-next-line global-require
  218. 'terser-webpack-plugin': require('../package.json').version,
  219. 'terser-webpack-plugin-options': this.options,
  220. name,
  221. contentHash: digest.substr(0, hashDigestLength)
  222. }, name);
  223. }
  224. }
  225. let output = await cache.get(cacheData, {
  226. RawSource,
  227. ConcatSource,
  228. SourceMapSource
  229. });
  230. if (!output) {
  231. const minimizerOptions = {
  232. name,
  233. input,
  234. inputSourceMap,
  235. minify: this.options.minify,
  236. minimizerOptions: this.options.terserOptions,
  237. extractComments: this.options.extractComments
  238. };
  239. if (/\.mjs(\?.*)?$/i.test(name)) {
  240. this.options.terserOptions.module = true;
  241. }
  242. try {
  243. output = await (worker ? worker.transform((0, _serializeJavascript.default)(minimizerOptions)) : (0, _minify.minify)(minimizerOptions));
  244. } catch (error) {
  245. compilation.errors.push(TerserPlugin.buildError(error, name, inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap) ? new _sourceMap.SourceMapConsumer(inputSourceMap) : null, new _RequestShortener.default(compiler.context)));
  246. return;
  247. }
  248. let shebang;
  249. if (this.options.extractComments.banner !== false && output.extractedComments && output.extractedComments.length > 0 && output.code.startsWith('#!')) {
  250. const firstNewlinePosition = output.code.indexOf('\n');
  251. shebang = output.code.substring(0, firstNewlinePosition);
  252. output.code = output.code.substring(firstNewlinePosition + 1);
  253. }
  254. if (output.map) {
  255. output.source = new SourceMapSource(output.code, name, output.map, input, inputSourceMap, true);
  256. } else {
  257. output.source = new RawSource(output.code);
  258. }
  259. let commentsFilename;
  260. if (output.extractedComments && output.extractedComments.length > 0) {
  261. commentsFilename = this.options.extractComments.filename || '[file].LICENSE.txt[query]';
  262. let query = '';
  263. let filename = name;
  264. const querySplit = filename.indexOf('?');
  265. if (querySplit >= 0) {
  266. query = filename.substr(querySplit);
  267. filename = filename.substr(0, querySplit);
  268. }
  269. const lastSlashIndex = filename.lastIndexOf('/');
  270. const basename = lastSlashIndex === -1 ? filename : filename.substr(lastSlashIndex + 1);
  271. const data = {
  272. filename,
  273. basename,
  274. query
  275. };
  276. commentsFilename = compilation.getPath(commentsFilename, data);
  277. output.commentsFilename = commentsFilename;
  278. let banner; // Add a banner to the original file
  279. if (this.options.extractComments.banner !== false) {
  280. banner = this.options.extractComments.banner || `For license information please see ${_path.default.relative(_path.default.dirname(name), commentsFilename).replace(/\\/g, '/')}`;
  281. if (typeof banner === 'function') {
  282. banner = banner(commentsFilename);
  283. }
  284. if (banner) {
  285. output.source = new ConcatSource(shebang ? `${shebang}\n` : '', `/*! ${banner} */\n`, output.source);
  286. output.banner = banner;
  287. output.shebang = shebang;
  288. }
  289. }
  290. const extractedCommentsString = output.extractedComments.sort().join('\n\n');
  291. output.extractedCommentsSource = new RawSource(`${extractedCommentsString}\n`);
  292. }
  293. await cache.store({ ...output,
  294. ...cacheData
  295. });
  296. } // TODO `...` required only for webpack@4
  297. const newInfo = { ...info,
  298. minimized: true
  299. };
  300. const {
  301. source,
  302. extractedCommentsSource
  303. } = output; // Write extracted comments to commentsFilename
  304. if (extractedCommentsSource) {
  305. const {
  306. commentsFilename
  307. } = output; // TODO `...` required only for webpack@4
  308. newInfo.related = {
  309. license: commentsFilename,
  310. ...info.related
  311. };
  312. allExtractedComments.set(name, {
  313. extractedCommentsSource,
  314. commentsFilename
  315. });
  316. }
  317. TerserPlugin.updateAsset(compilation, name, source, newInfo);
  318. }));
  319. }
  320. await Promise.all(scheduledTasks);
  321. if (worker) {
  322. await worker.end();
  323. }
  324. await Array.from(allExtractedComments).sort().reduce(async (previousPromise, [from, value]) => {
  325. const previous = await previousPromise;
  326. const {
  327. commentsFilename,
  328. extractedCommentsSource
  329. } = value;
  330. if (previous && previous.commentsFilename === commentsFilename) {
  331. const {
  332. from: previousFrom,
  333. source: prevSource
  334. } = previous;
  335. const mergedName = `${previousFrom}|${from}`;
  336. const cacheData = {
  337. target: 'comments'
  338. };
  339. if (TerserPlugin.isWebpack4()) {
  340. const {
  341. outputOptions: {
  342. hashSalt,
  343. hashDigest,
  344. hashDigestLength,
  345. hashFunction
  346. }
  347. } = compilation;
  348. const previousHash = _webpack.util.createHash(hashFunction);
  349. const hash = _webpack.util.createHash(hashFunction);
  350. if (hashSalt) {
  351. previousHash.update(hashSalt);
  352. hash.update(hashSalt);
  353. }
  354. previousHash.update(prevSource.source());
  355. hash.update(extractedCommentsSource.source());
  356. const previousDigest = previousHash.digest(hashDigest);
  357. const digest = hash.digest(hashDigest);
  358. cacheData.cacheKeys = {
  359. mergedName,
  360. previousContentHash: previousDigest.substr(0, hashDigestLength),
  361. contentHash: digest.substr(0, hashDigestLength)
  362. };
  363. cacheData.inputSource = extractedCommentsSource;
  364. } else {
  365. const mergedInputSource = [prevSource, extractedCommentsSource];
  366. cacheData.name = `${commentsFilename}|${mergedName}`;
  367. cacheData.inputSource = mergedInputSource;
  368. }
  369. let output = await cache.get(cacheData, {
  370. ConcatSource
  371. });
  372. if (!output) {
  373. output = new ConcatSource(Array.from(new Set([...prevSource.source().split('\n\n'), ...extractedCommentsSource.source().split('\n\n')])).join('\n\n'));
  374. await cache.store({ ...cacheData,
  375. output
  376. });
  377. }
  378. TerserPlugin.updateAsset(compilation, commentsFilename, output);
  379. return {
  380. commentsFilename,
  381. from: mergedName,
  382. source: output
  383. };
  384. }
  385. const existingAsset = TerserPlugin.getAsset(compilation, commentsFilename);
  386. if (existingAsset) {
  387. return {
  388. commentsFilename,
  389. from: commentsFilename,
  390. source: existingAsset.source
  391. };
  392. }
  393. TerserPlugin.emitAsset(compilation, commentsFilename, extractedCommentsSource);
  394. return {
  395. commentsFilename,
  396. from,
  397. source: extractedCommentsSource
  398. };
  399. }, Promise.resolve());
  400. }
  401. static getEcmaVersion(environment) {
  402. // ES 6th
  403. if (environment.arrowFunction || environment.const || environment.destructuring || environment.forOf || environment.module) {
  404. return 2015;
  405. } // ES 11th
  406. if (environment.bigIntLiteral || environment.dynamicImport) {
  407. return 2020;
  408. }
  409. return 5;
  410. }
  411. apply(compiler) {
  412. const {
  413. devtool,
  414. output,
  415. plugins
  416. } = compiler.options;
  417. this.options.sourceMap = typeof this.options.sourceMap === 'undefined' ? devtool && !devtool.includes('eval') && !devtool.includes('cheap') && (devtool.includes('source-map') || // Todo remove when `webpack@4` support will be dropped
  418. devtool.includes('sourcemap')) || plugins && plugins.some(plugin => plugin instanceof _webpack.SourceMapDevToolPlugin && plugin.options && plugin.options.columns) : Boolean(this.options.sourceMap);
  419. if (typeof this.options.terserOptions.module === 'undefined' && typeof output.module !== 'undefined') {
  420. this.options.terserOptions.module = output.module;
  421. }
  422. if (typeof this.options.terserOptions.ecma === 'undefined') {
  423. this.options.terserOptions.ecma = TerserPlugin.getEcmaVersion(output.environment || {});
  424. }
  425. const pluginName = this.constructor.name;
  426. const weakCache = TerserPlugin.isWebpack4() && (this.options.cache === true || typeof this.options.cache === 'string') ? new WeakMap() : // eslint-disable-next-line no-undefined
  427. undefined;
  428. compiler.hooks.compilation.tap(pluginName, compilation => {
  429. if (this.options.sourceMap) {
  430. compilation.hooks.buildModule.tap(pluginName, moduleArg => {
  431. // to get detailed location info about errors
  432. // eslint-disable-next-line no-param-reassign
  433. moduleArg.useSourceMap = true;
  434. });
  435. }
  436. if (TerserPlugin.isWebpack4()) {
  437. // eslint-disable-next-line global-require
  438. const CacheEngine = require('./Webpack4Cache').default;
  439. const {
  440. mainTemplate,
  441. chunkTemplate
  442. } = compilation;
  443. const data = (0, _serializeJavascript.default)({
  444. terser: _package.default.version,
  445. terserOptions: this.options.terserOptions
  446. }); // Regenerate `contenthash` for minified assets
  447. for (const template of [mainTemplate, chunkTemplate]) {
  448. template.hooks.hashForChunk.tap(pluginName, hash => {
  449. hash.update('TerserPlugin');
  450. hash.update(data);
  451. });
  452. }
  453. compilation.hooks.optimizeChunkAssets.tapPromise(pluginName, assets => this.optimize(compiler, compilation, assets, CacheEngine, weakCache));
  454. } else {
  455. // eslint-disable-next-line global-require
  456. const CacheEngine = require('./Webpack5Cache').default; // eslint-disable-next-line global-require
  457. const Compilation = require('webpack/lib/Compilation');
  458. const hooks = _webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation);
  459. const data = (0, _serializeJavascript.default)({
  460. terser: _package.default.version,
  461. terserOptions: this.options.terserOptions
  462. });
  463. hooks.chunkHash.tap(pluginName, (chunk, hash) => {
  464. hash.update('TerserPlugin');
  465. hash.update(data);
  466. });
  467. compilation.hooks.processAssets.tapPromise({
  468. name: pluginName,
  469. stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE
  470. }, assets => this.optimize(compiler, compilation, assets, CacheEngine));
  471. compilation.hooks.statsPrinter.tap(pluginName, stats => {
  472. stats.hooks.print.for('asset.info.minimized').tap('terser-webpack-plugin', (minimized, {
  473. green,
  474. formatFlag
  475. }) => // eslint-disable-next-line no-undefined
  476. minimized ? green(formatFlag('minimized')) : undefined);
  477. });
  478. }
  479. });
  480. }
  481. }
  482. var _default = TerserPlugin;
  483. exports.default = _default;