UNPKG

19.3 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.default = void 0;
7
8var path = _interopRequireWildcard(require("path"));
9
10var os = _interopRequireWildcard(require("os"));
11
12var _sourceMap = require("source-map");
13
14var _schemaUtils = require("schema-utils");
15
16var _serializeJavascript = _interopRequireDefault(require("serialize-javascript"));
17
18var terserPackageJson = _interopRequireWildcard(require("terser/package.json"));
19
20var _pLimit = _interopRequireDefault(require("p-limit"));
21
22var _jestWorker = _interopRequireDefault(require("jest-worker"));
23
24var schema = _interopRequireWildcard(require("./options.json"));
25
26var _minify = require("./minify");
27
28function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
29
30function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
31
32function _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; }
33
34/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
35
36/** @typedef {import("webpack").Compiler} Compiler */
37
38/** @typedef {import("webpack").Compilation} Compilation */
39
40/** @typedef {import("webpack").Rules} Rules */
41
42/** @typedef {import("webpack").Source} Source */
43
44/** @typedef {import("webpack").WebpackError} WebpackError */
45
46/** @typedef {import("webpack").Asset} Asset */
47
48/** @typedef {import("webpack").AssetInfo} AssetInfo */
49
50/** @typedef {import("terser").ECMA} TerserECMA */
51
52/** @typedef {import("terser").MinifyOptions} TerserMinifyOptions */
53
54/** @typedef {import("jest-worker").default} JestWorker */
55
56/** @typedef {import("source-map").RawSourceMap} SourceMapRawSourceMap */
57
58/** @typedef {import("./minify.js").InternalMinifyOptions} InternalMinifyOptions */
59
60/** @typedef {import("./minify.js").InternalMinifyResult} InternalMinifyResult */
61
62/** @typedef {JestWorker & { transform: (options: string) => InternalMinifyResult, minify: (options: InternalMinifyOptions) => InternalMinifyResult }} MinifyWorker */
63
64/** @typedef {Object.<any, any> | TerserMinifyOptions} MinifyOptions */
65
66/**
67 * @callback ExtractCommentsFunction
68 * @param {any} astNode
69 * @param {{ value: string, type: 'comment1' | 'comment2' | 'comment3' | 'comment4', pos: number, line: number, col: number }} comment
70 * @returns {boolean}
71 */
72
73/**
74 * @typedef {boolean | string | RegExp | ExtractCommentsFunction} ExtractCommentsCondition
75 */
76
77/**
78 * @typedef {string | ((fileData: any) => string)} ExtractCommentsFilename
79 */
80
81/**
82 * @typedef {boolean | string | ((commentsFile: string) => string)} ExtractCommentsBanner
83 */
84
85/**
86 * @typedef {Object} ExtractCommentsObject
87 * @property {ExtractCommentsCondition} condition
88 * @property {ExtractCommentsFilename} filename
89 * @property {ExtractCommentsBanner} banner
90 */
91
92/**
93 * @callback CustomMinifyFunction
94 * @param {Object.<string, string>} file
95 * @param {SourceMapRawSourceMap} sourceMap
96 * @param {MinifyOptions} minifyOptions
97 */
98
99/**
100 * @typedef {ExtractCommentsCondition | ExtractCommentsObject} ExtractCommentsOptions
101 */
102
103/**
104 * @typedef {Object} TerserPluginOptions
105 * @property {Rules} [test]
106 * @property {Rules} [include]
107 * @property {Rules} [exclude]
108 * @property {MinifyOptions} [terserOptions]
109 * @property {ExtractCommentsOptions} [extractComments]
110 * @property {boolean} [parallel]
111 * @property {CustomMinifyFunction} [minify]
112 */
113class TerserPlugin {
114 /**
115 * @param {TerserPluginOptions} options
116 */
117 constructor(options = {}) {
118 (0, _schemaUtils.validate)(
119 /** @type {Schema} */
120 schema, options, {
121 name: 'Terser Plugin',
122 baseDataPath: 'options'
123 });
124 const {
125 minify,
126 terserOptions = {},
127 test = /\.[cm]?js(\?.*)?$/i,
128 extractComments = true,
129 parallel = true,
130 include,
131 exclude
132 } = options;
133 this.options = {
134 test,
135 extractComments,
136 parallel,
137 include,
138 exclude,
139 minify,
140 terserOptions
141 };
142 }
143 /**
144 * @private
145 * @param {any} input
146 * @returns {boolean}
147 */
148
149
150 static isSourceMap(input) {
151 // All required options for `new SourceMapConsumer(...options)`
152 // https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap
153 return Boolean(input && input.version && input.sources && Array.isArray(input.sources) && typeof input.mappings === 'string');
154 }
155 /**
156 * @private
157 * @param {Error & { line: number, col: number}} error
158 * @param {string} file
159 * @param {Compilation["requestShortener"]} [requestShortener]
160 * @param {SourceMapConsumer} [sourceMap]
161 * @returns {WebpackError}
162 */
163
164
165 static buildError(error, file, requestShortener, sourceMap) {
166 if (error.line) {
167 const original = sourceMap && sourceMap.originalPositionFor({
168 line: error.line,
169 column: error.col
170 });
171
172 if (original && original.source && requestShortener) {
173 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')}` : ''}`);
174 }
175
176 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')}` : ''}`);
177 }
178
179 if (error.stack) {
180 return new Error(`${file} from Terser\n${error.stack}`);
181 }
182
183 return new Error(`${file} from Terser\n${error.message}`);
184 }
185 /**
186 * @private
187 * @param {boolean} parallel
188 * @returns {number}
189 */
190
191
192 static getAvailableNumberOfCores(parallel) {
193 // In some cases cpus() returns undefined
194 // https://github.com/nodejs/node/issues/19022
195 const cpus = os.cpus() || {
196 length: 1
197 };
198 return parallel === true ? cpus.length - 1 : Math.min(Number(parallel) || 0, cpus.length - 1);
199 }
200 /**
201 * @param {Compiler} compiler
202 * @param {Compilation} compilation
203 * @param {Record<string, Source>} assets
204 * @param {{availableNumberOfCores: number}} optimizeOptions
205 * @returns {Promise<void>}
206 */
207
208
209 async optimize(compiler, compilation, assets, optimizeOptions) {
210 const cache = compilation.getCache('TerserWebpackPlugin');
211 let numberOfAssetsForMinify = 0;
212 const assetsForMinify = await Promise.all(Object.keys(assets).filter(name => {
213 const {
214 info
215 } = compilation.getAsset(name); // Skip double minimize assets from child compilation
216
217 if (info.minimized) {
218 return false;
219 }
220
221 if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind( // eslint-disable-next-line no-undefined
222 undefined, this.options)(name)) {
223 return false;
224 }
225
226 return true;
227 }).map(async name => {
228 const {
229 info,
230 source
231 } = compilation.getAsset(name);
232 const eTag = cache.getLazyHashedEtag(source);
233 const cacheItem = cache.getItemCache(name, eTag);
234 const output = await cacheItem.getPromise();
235
236 if (!output) {
237 numberOfAssetsForMinify += 1;
238 }
239
240 return {
241 name,
242 info,
243 inputSource: source,
244 output,
245 cacheItem
246 };
247 }));
248 /** @type {undefined | (() => MinifyWorker)} */
249
250 let getWorker;
251 /** @type {undefined | MinifyWorker} */
252
253 let initializedWorker;
254 /** @type {undefined | number} */
255
256 let numberOfWorkers;
257
258 if (optimizeOptions.availableNumberOfCores > 0) {
259 // Do not create unnecessary workers when the number of files is less than the available cores, it saves memory
260 numberOfWorkers = Math.min(numberOfAssetsForMinify, optimizeOptions.availableNumberOfCores); // eslint-disable-next-line consistent-return
261
262 getWorker = () => {
263 if (initializedWorker) {
264 return initializedWorker;
265 }
266
267 initializedWorker =
268 /** @type {MinifyWorker} */
269 new _jestWorker.default(require.resolve('./minify'), {
270 numWorkers: numberOfWorkers,
271 enableWorkerThreads: true
272 }); // https://github.com/facebook/jest/issues/8872#issuecomment-524822081
273
274 const workerStdout = initializedWorker.getStdout();
275
276 if (workerStdout) {
277 workerStdout.on('data', chunk => {
278 return process.stdout.write(chunk);
279 });
280 }
281
282 const workerStderr = initializedWorker.getStderr();
283
284 if (workerStderr) {
285 workerStderr.on('data', chunk => {
286 return process.stderr.write(chunk);
287 });
288 }
289
290 return initializedWorker;
291 };
292 }
293
294 const limit = (0, _pLimit.default)(getWorker && numberOfAssetsForMinify > 0 ?
295 /** @type {number} */
296 numberOfWorkers : Infinity);
297 const {
298 SourceMapSource,
299 ConcatSource,
300 RawSource
301 } = compiler.webpack.sources;
302 const allExtractedComments = new Map();
303 const scheduledTasks = [];
304
305 for (const asset of assetsForMinify) {
306 scheduledTasks.push(limit(async () => {
307 const {
308 name,
309 inputSource,
310 info,
311 cacheItem
312 } = asset;
313 let {
314 output
315 } = asset;
316
317 if (!output) {
318 let input;
319 let inputSourceMap;
320 const {
321 source: sourceFromInputSource,
322 map
323 } = inputSource.sourceAndMap();
324 input = sourceFromInputSource;
325
326 if (map) {
327 if (TerserPlugin.isSourceMap(map)) {
328 inputSourceMap = map;
329 } else {
330 inputSourceMap = map;
331 compilation.warnings.push(
332 /** @type {WebpackError} */
333 new Error(`${name} contains invalid source map`));
334 }
335 }
336
337 if (Buffer.isBuffer(input)) {
338 input = input.toString();
339 }
340
341 const options = {
342 name,
343 input,
344 inputSourceMap,
345 minify: this.options.minify,
346 minifyOptions: { ...this.options.terserOptions
347 },
348 extractComments: this.options.extractComments
349 };
350
351 if (typeof options.minifyOptions.module === 'undefined') {
352 if (typeof info.javascriptModule !== 'undefined') {
353 options.minifyOptions.module = info.javascriptModule;
354 } else if (/\.mjs(\?.*)?$/i.test(name)) {
355 options.minifyOptions.module = true;
356 } else if (/\.cjs(\?.*)?$/i.test(name)) {
357 options.minifyOptions.module = false;
358 }
359 }
360
361 try {
362 output = await (getWorker ? getWorker().transform((0, _serializeJavascript.default)(options)) : (0, _minify.minify)(options));
363 } catch (error) {
364 const hasSourceMap = inputSourceMap && TerserPlugin.isSourceMap(inputSourceMap);
365 compilation.errors.push(TerserPlugin.buildError(error, name, // eslint-disable-next-line no-undefined
366 hasSourceMap ? compilation.requestShortener : undefined, hasSourceMap ? new _sourceMap.SourceMapConsumer(
367 /** @type {SourceMapRawSourceMap} */
368 inputSourceMap) : // eslint-disable-next-line no-undefined
369 undefined));
370 return;
371 }
372
373 let shebang;
374
375 if (
376 /** @type {ExtractCommentsObject} */
377 this.options.extractComments.banner !== false && output.extractedComments && output.extractedComments.length > 0 && output.code.startsWith('#!')) {
378 const firstNewlinePosition = output.code.indexOf('\n');
379 shebang = output.code.substring(0, firstNewlinePosition);
380 output.code = output.code.substring(firstNewlinePosition + 1);
381 }
382
383 if (output.map) {
384 output.source = new SourceMapSource(output.code, name, output.map, input,
385 /** @type {SourceMapRawSourceMap} */
386 inputSourceMap, true);
387 } else {
388 output.source = new RawSource(output.code);
389 }
390
391 if (output.extractedComments && output.extractedComments.length > 0) {
392 const commentsFilename =
393 /** @type {ExtractCommentsObject} */
394 this.options.extractComments.filename || '[file].LICENSE.txt[query]';
395 let query = '';
396 let filename = name;
397 const querySplit = filename.indexOf('?');
398
399 if (querySplit >= 0) {
400 query = filename.substr(querySplit);
401 filename = filename.substr(0, querySplit);
402 }
403
404 const lastSlashIndex = filename.lastIndexOf('/');
405 const basename = lastSlashIndex === -1 ? filename : filename.substr(lastSlashIndex + 1);
406 const data = {
407 filename,
408 basename,
409 query
410 };
411 output.commentsFilename = compilation.getPath(commentsFilename, data);
412 let banner; // Add a banner to the original file
413
414 if (
415 /** @type {ExtractCommentsObject} */
416 this.options.extractComments.banner !== false) {
417 banner =
418 /** @type {ExtractCommentsObject} */
419 this.options.extractComments.banner || `For license information please see ${path.relative(path.dirname(name), output.commentsFilename).replace(/\\/g, '/')}`;
420
421 if (typeof banner === 'function') {
422 banner = banner(output.commentsFilename);
423 }
424
425 if (banner) {
426 output.source = new ConcatSource(shebang ? `${shebang}\n` : '', `/*! ${banner} */\n`, output.source);
427 }
428 }
429
430 const extractedCommentsString = output.extractedComments.sort().join('\n\n');
431 output.extractedCommentsSource = new RawSource(`${extractedCommentsString}\n`);
432 }
433
434 await cacheItem.storePromise({
435 source: output.source,
436 commentsFilename: output.commentsFilename,
437 extractedCommentsSource: output.extractedCommentsSource
438 });
439 }
440 /** @type {AssetInfo} */
441
442
443 const newInfo = {
444 minimized: true
445 };
446 const {
447 source,
448 extractedCommentsSource
449 } = output; // Write extracted comments to commentsFilename
450
451 if (extractedCommentsSource) {
452 const {
453 commentsFilename
454 } = output;
455 newInfo.related = {
456 license: commentsFilename
457 };
458 allExtractedComments.set(name, {
459 extractedCommentsSource,
460 commentsFilename
461 });
462 }
463
464 compilation.updateAsset(name, source, newInfo);
465 }));
466 }
467
468 await Promise.all(scheduledTasks);
469
470 if (initializedWorker) {
471 await initializedWorker.end();
472 }
473
474 await Array.from(allExtractedComments).sort().reduce(
475 /**
476 * @param {Promise<any>} previousPromise
477 * @param {any} extractedComments
478 * @returns {Promise<any>}
479 */
480 async (previousPromise, [from, value]) => {
481 const previous = await previousPromise;
482 const {
483 commentsFilename,
484 extractedCommentsSource
485 } = value;
486
487 if (previous && previous.commentsFilename === commentsFilename) {
488 const {
489 from: previousFrom,
490 source: prevSource
491 } = previous;
492 const mergedName = `${previousFrom}|${from}`;
493 const name = `${commentsFilename}|${mergedName}`;
494 const eTag = [prevSource, extractedCommentsSource].map(item => cache.getLazyHashedEtag(item)).reduce((previousValue, currentValue) => cache.mergeEtags(previousValue, currentValue));
495 let source = await cache.getPromise(name, eTag);
496
497 if (!source) {
498 source = new ConcatSource(Array.from(new Set([...prevSource.source().split('\n\n'), ...extractedCommentsSource.source().split('\n\n')])).join('\n\n'));
499 await cache.storePromise(name, eTag, source);
500 }
501
502 compilation.updateAsset(commentsFilename, source);
503 return {
504 commentsFilename,
505 from: mergedName,
506 source
507 };
508 }
509
510 const existingAsset = compilation.getAsset(commentsFilename);
511
512 if (existingAsset) {
513 return {
514 commentsFilename,
515 from: commentsFilename,
516 source: existingAsset.source
517 };
518 }
519
520 compilation.emitAsset(commentsFilename, extractedCommentsSource);
521 return {
522 commentsFilename,
523 from,
524 source: extractedCommentsSource
525 };
526 }, Promise.resolve());
527 }
528 /**
529 * @private
530 * @param {any} environment
531 * @returns {TerserECMA}
532 */
533
534
535 static getEcmaVersion(environment) {
536 // ES 6th
537 if (environment.arrowFunction || environment.const || environment.destructuring || environment.forOf || environment.module) {
538 return 2015;
539 } // ES 11th
540
541
542 if (environment.bigIntLiteral || environment.dynamicImport) {
543 return 2020;
544 }
545
546 return 5;
547 }
548 /**
549 * @param {Compiler} compiler
550 * @returns {void}
551 */
552
553
554 apply(compiler) {
555 const {
556 output
557 } = compiler.options;
558
559 if (typeof this.options.terserOptions.ecma === 'undefined') {
560 this.options.terserOptions.ecma = TerserPlugin.getEcmaVersion(output.environment || {});
561 }
562
563 const pluginName = this.constructor.name;
564 const availableNumberOfCores = TerserPlugin.getAvailableNumberOfCores(this.options.parallel);
565 compiler.hooks.compilation.tap(pluginName, compilation => {
566 const hooks = compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation);
567 const data = (0, _serializeJavascript.default)({
568 terser: terserPackageJson.version,
569 terserOptions: this.options.terserOptions
570 });
571 hooks.chunkHash.tap(pluginName, (chunk, hash) => {
572 hash.update('TerserPlugin');
573 hash.update(data);
574 });
575 compilation.hooks.processAssets.tapPromise({
576 name: pluginName,
577 stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE
578 }, assets => this.optimize(compiler, compilation, assets, {
579 availableNumberOfCores
580 }));
581 compilation.hooks.statsPrinter.tap(pluginName, stats => {
582 stats.hooks.print.for('asset.info.minimized').tap('terser-webpack-plugin', (minimized, {
583 green,
584 formatFlag
585 }) => // eslint-disable-next-line no-undefined
586 minimized ? green(formatFlag('minimized')) : undefined);
587 });
588 });
589 }
590
591}
592
593var _default = TerserPlugin;
594exports.default = _default;
\No newline at end of file