UNPKG

16.6 kBJavaScriptView Raw
1"use strict";
2
3const path = require("path");
4
5const os = require("os");
6
7const {
8 validate
9} = require("schema-utils");
10
11const serialize = require("serialize-javascript");
12
13const worker = require("./worker");
14
15const schema = require("./plugin-options.json");
16
17const {
18 throttleAll,
19 imageminNormalizeConfig,
20 imageminMinify,
21 imageminGenerate,
22 squooshMinify,
23 squooshGenerate
24} = require("./utils.js");
25/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
26
27/** @typedef {import("webpack").WebpackPluginInstance} WebpackPluginInstance */
28
29/** @typedef {import("webpack").Compiler} Compiler */
30
31/** @typedef {import("webpack").Compilation} Compilation */
32
33/** @typedef {import("webpack").WebpackError} WebpackError */
34
35/** @typedef {import("webpack").Asset} Asset */
36
37/** @typedef {import("webpack").AssetInfo} AssetInfo */
38
39/** @typedef {import("webpack").sources.Source} Source */
40
41/** @typedef {import("./utils.js").imageminMinify} ImageminMinifyFunction */
42
43/** @typedef {import("./utils.js").squooshMinify} SquooshMinifyFunction */
44
45/** @typedef {RegExp | string} Rule */
46
47/** @typedef {Rule[] | Rule} Rules */
48
49/**
50 * @callback FilterFn
51 * @param {Buffer} source `Buffer` of source file.
52 * @param {string} sourcePath Absolute path to source.
53 * @returns {boolean}
54 */
55
56/**
57 * @typedef {Object} ImageminOptions
58 * @property {Array<string | [string, Record<string, any>?] | import("imagemin").Plugin>} plugins
59 */
60
61/**
62 * @typedef {Object.<string, any>} SquooshOptions
63 */
64
65/**
66 * @typedef {Object} WorkerResult
67 * @property {string} filename
68 * @property {Buffer} data
69 * @property {Array<Error>} warnings
70 * @property {Array<Error>} errors
71 * @property {AssetInfo} info
72 */
73
74/**
75 * @template T
76 * @typedef {Object} Task
77 * @property {string} name
78 * @property {AssetInfo} info
79 * @property {Source} inputSource
80 * @property {WorkerResult & { source?: Source } | undefined} output
81 * @property {ReturnType<ReturnType<Compilation["getCache"]>["getItemCache"]>} cacheItem
82 * @property {Transformer<T> | Transformer<T>[]} transformer
83 */
84
85/**
86 * @typedef {{ [key: string]: any }} CustomOptions
87 */
88
89/**
90 * @template T
91 * @typedef {T extends infer U ? U : CustomOptions} InferDefaultType
92 */
93
94/**
95 * @template T
96 * @typedef {InferDefaultType<T> | undefined} BasicTransformerOptions
97 */
98
99/**
100 * @template T
101 * @callback BasicTransformerImplementation
102 * @param {WorkerResult} original
103 * @param {BasicTransformerOptions<T>} [options]
104 * @returns {Promise<WorkerResult>}
105 */
106
107/**
108 * @typedef {object} BasicTransformerHelpers
109 * @property {() => {}} [setup]
110 * @property {() => {}} [teardown]
111 */
112
113/**
114 * @template T
115 * @typedef {BasicTransformerImplementation<T> & BasicTransformerHelpers} TransformerFunction
116 */
117
118/**
119 * @typedef {Object} PathData
120 * @property {string} [filename]
121 */
122
123/**
124 * @callback FilenameFn
125 * @param {PathData} pathData
126 * @param {AssetInfo} [assetInfo]
127 * @returns {string}
128 */
129
130/**
131 * @template T
132 * @typedef {Object} Transformer
133 * @property {TransformerFunction<T>} implementation
134 * @property {BasicTransformerOptions<T>} [options]
135 * @property {FilterFn} [filter]
136 * @property {string | FilenameFn} [filename]
137 * @property {string} [preset]
138 * @property {"import" | "asset"} [type]
139 */
140
141/**
142 * @template T
143 * @typedef {Omit<Transformer<T>, "preset" | "type">} Minimizer
144 */
145
146/**
147 * @template T
148 * @typedef {Transformer<T>} Generator
149 */
150
151/**
152 * @template T
153 * @typedef {Object} InternalWorkerOptions
154 * @property {string} filename
155 * @property {Buffer} input
156 * @property {Transformer<T> | Transformer<T>[]} transformer
157 * @property {string} [severityError]
158 * @property {Function} [generateFilename]
159 */
160
161/**
162 * @template T
163 * @typedef {import("./loader").LoaderOptions<T>} InternalLoaderOptions
164 */
165
166/**
167 * @template T, G
168 * @typedef {Object} PluginOptions
169 * @property {Rules} [test] Test to match files against.
170 * @property {Rules} [include] Files to include.
171 * @property {Rules} [exclude] Files to exclude.
172 * @property {T extends any[] ? { [P in keyof T]: Minimizer<T[P]> } : Minimizer<T> | Minimizer<T>[]} [minimizer] Allows to setup the minimizer.
173 * @property {G extends any[] ? { [P in keyof G]: Generator<G[P]> } : Generator<G>[]} [generator] Allows to set the generator.
174 * @property {boolean} [loader] Automatically adding `imagemin-loader`.
175 * @property {number} [concurrency] Maximum number of concurrency optimization processes in one time.
176 * @property {string} [severityError] Allows to choose how errors are displayed.
177 * @property {boolean} [deleteOriginalAssets] Allows to remove original assets. Useful for converting to a `webp` and remove original assets.
178 */
179
180/**
181 * @template T, [G=T]
182 * @extends {WebpackPluginInstance}
183 */
184
185
186class ImageMinimizerPlugin {
187 /**
188 * @param {PluginOptions<T, G>} [options={}] Plugin options.
189 */
190 constructor(options = {}) {
191 validate(
192 /** @type {Schema} */
193 schema, options, {
194 name: "Image Minimizer Plugin",
195 baseDataPath: "options"
196 });
197 const {
198 minimizer,
199 test = /\.(jpe?g|png|gif|tif|webp|svg|avif|jxl)$/i,
200 include,
201 exclude,
202 severityError,
203 generator,
204 loader = true,
205 concurrency,
206 deleteOriginalAssets = true
207 } = options;
208
209 if (!minimizer && !generator) {
210 throw new Error("Not configured 'minimizer' or 'generator' options, please setup them");
211 }
212 /**
213 * @private
214 */
215
216
217 this.options = {
218 minimizer,
219 generator,
220 severityError,
221 exclude,
222 include,
223 loader,
224 concurrency,
225 test,
226 deleteOriginalAssets
227 };
228 }
229 /**
230 * @private
231 * @param {Compiler} compiler
232 * @param {Compilation} compilation
233 * @param {Record<string, Source>} assets
234 * @returns {Promise<void>}
235 */
236
237
238 async optimize(compiler, compilation, assets) {
239 const minimizers = typeof this.options.minimizer !== "undefined" ? Array.isArray(this.options.minimizer) ? this.options.minimizer : [this.options.minimizer] : [];
240 const generators = Array.isArray(this.options.generator) ? this.options.generator.filter(item => {
241 if (item.type === "asset") {
242 return true;
243 }
244
245 return false;
246 }) : [];
247
248 if (minimizers.length === 0 && generators.length === 0) {
249 return;
250 }
251
252 const cache = compilation.getCache("ImageMinimizerWebpackPlugin");
253 const assetsForTransformers = (await Promise.all(Object.keys(assets).filter(name => {
254 const {
255 info
256 } =
257 /** @type {Asset} */
258 compilation.getAsset(name); // Skip double minimize assets from child compilation
259
260 if (info.minimized || info.generated) {
261 return false;
262 }
263
264 if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind(undefined, this.options)(name)) {
265 return false;
266 }
267
268 return true;
269 }).map(async name => {
270 const {
271 info,
272 source
273 } =
274 /** @type {Asset} */
275 compilation.getAsset(name);
276 /**
277 * @template Z
278 * @param {Transformer<Z> | Array<Transformer<Z>>} transformer
279 * @returns {Promise<Task<Z>>}
280 */
281
282 const getFromCache = async transformer => {
283 const cacheName = serialize({
284 name,
285 transformer
286 });
287 const eTag = cache.getLazyHashedEtag(source);
288 const cacheItem = cache.getItemCache(cacheName, eTag);
289 const output = await cacheItem.getPromise();
290 return {
291 name,
292 info,
293 inputSource: source,
294 output,
295 cacheItem,
296 transformer
297 };
298 };
299 /**
300 * @type {Task<T | G>[]}
301 */
302
303
304 const tasks = [];
305
306 if (generators.length > 0) {
307 tasks.push(...(await Promise.all(generators.map(generator => getFromCache(generator)))));
308 }
309
310 if (minimizers.length > 0) {
311 tasks.push(await getFromCache(
312 /** @type {Minimizer<T>[]} */
313 minimizers));
314 }
315
316 return tasks;
317 }))).flat();
318 const cpus = os.cpus() || {
319 length: 1
320 };
321 const limit = this.options.concurrency || Math.max(1, cpus.length - 1);
322 const {
323 RawSource
324 } = compiler.webpack.sources;
325 const scheduledTasks = [];
326
327 for (const asset of assetsForTransformers) {
328 scheduledTasks.push(async () => {
329 const {
330 name,
331 inputSource,
332 cacheItem,
333 transformer
334 } = asset;
335 let {
336 output
337 } = asset;
338 let input;
339 const sourceFromInputSource = inputSource.source();
340
341 if (!output) {
342 input = sourceFromInputSource;
343
344 if (!Buffer.isBuffer(input)) {
345 input = Buffer.from(input);
346 }
347
348 const minifyOptions =
349 /** @type {InternalWorkerOptions<T>} */
350 {
351 filename: name,
352 input,
353 severityError: this.options.severityError,
354 transformer,
355 generateFilename: compilation.getAssetPath.bind(compilation)
356 };
357 output = await worker(minifyOptions);
358 output.source = new RawSource(output.data);
359 await cacheItem.storePromise({
360 source: output.source,
361 info: output.info,
362 filename: output.filename,
363 warnings: output.warnings,
364 errors: output.errors
365 });
366 }
367
368 if (output.warnings.length > 0) {
369 /** @type {[WebpackError]} */
370 output.warnings.forEach(warning => {
371 compilation.warnings.push(warning);
372 });
373 }
374
375 if (output.errors.length > 0) {
376 /** @type {[WebpackError]} */
377 output.errors.forEach(error => {
378 compilation.errors.push(error);
379 });
380 }
381
382 if (compilation.getAsset(output.filename)) {
383 compilation.updateAsset(output.filename,
384 /** @type {Source} */
385 output.source, output.info);
386 } else {
387 compilation.emitAsset(output.filename,
388 /** @type {Source} */
389 output.source, output.info);
390
391 if (this.options.deleteOriginalAssets) {
392 compilation.deleteAsset(name);
393 }
394 }
395 });
396 }
397
398 await throttleAll(limit, scheduledTasks);
399 }
400 /**
401 * @private
402 */
403
404
405 setupAll() {
406 if (typeof this.options.generator !== "undefined") {
407 const {
408 generator
409 } = this.options; // @ts-ignore
410
411 for (const item of generator) {
412 if (typeof item.implementation.setup !== "undefined") {
413 item.implementation.setup();
414 }
415 }
416 }
417
418 if (typeof this.options.minimizer !== "undefined") {
419 const minimizers = Array.isArray(this.options.minimizer) ? this.options.minimizer : [this.options.minimizer];
420
421 for (const item of minimizers) {
422 if (typeof item.implementation.setup !== "undefined") {
423 item.implementation.setup();
424 }
425 }
426 }
427 }
428 /**
429 * @private
430 */
431
432
433 async teardownAll() {
434 if (typeof this.options.generator !== "undefined") {
435 const {
436 generator
437 } = this.options; // @ts-ignore
438
439 for (const item of generator) {
440 if (typeof item.implementation.teardown !== "undefined") {
441 // eslint-disable-next-line no-await-in-loop
442 await item.implementation.teardown();
443 }
444 }
445 }
446
447 if (typeof this.options.minimizer !== "undefined") {
448 const minimizers = Array.isArray(this.options.minimizer) ? this.options.minimizer : [this.options.minimizer];
449
450 for (const item of minimizers) {
451 if (typeof item.implementation.teardown !== "undefined") {
452 // eslint-disable-next-line no-await-in-loop
453 await item.implementation.teardown();
454 }
455 }
456 }
457 }
458 /**
459 * @param {import("webpack").Compiler} compiler
460 */
461
462
463 apply(compiler) {
464 const pluginName = this.constructor.name;
465 this.setupAll();
466
467 if (this.options.loader) {
468 compiler.hooks.compilation.tap({
469 name: pluginName
470 }, compilation => {
471 // Collect asset and update info from old loaders
472 compilation.hooks.moduleAsset.tap({
473 name: pluginName
474 }, (module, file) => {
475 const newInfo = module && module.buildMeta && module.buildMeta.imageMinimizerPluginInfo;
476
477 if (newInfo) {
478 const asset =
479 /** @type {Asset} */
480 compilation.getAsset(file);
481 compilation.updateAsset(file, asset.source, newInfo);
482 }
483 }); // Collect asset modules and update info for asset modules
484
485 compilation.hooks.assetPath.tap({
486 name: pluginName
487 }, (filename, data, info) => {
488 const newInfo = data && // @ts-ignore
489 data.module && // @ts-ignore
490 data.module.buildMeta && // @ts-ignore
491 data.module.buildMeta.imageMinimizerPluginInfo;
492
493 if (newInfo) {
494 Object.assign(info || {}, newInfo);
495 }
496
497 return filename;
498 });
499 });
500 compiler.hooks.afterPlugins.tap({
501 name: pluginName
502 }, () => {
503 const {
504 minimizer,
505 generator,
506 test,
507 include,
508 exclude,
509 severityError
510 } = this.options;
511 const minimizerForLoader = minimizer;
512 let generatorForLoader = generator;
513
514 if (Array.isArray(generatorForLoader)) {
515 const importGenerators = generatorForLoader.filter(item => {
516 if (typeof item.type === "undefined" || item.type === "import") {
517 return true;
518 }
519
520 return false;
521 });
522 generatorForLoader = importGenerators.length > 0 ?
523 /** @type {G extends any[] ? { [P in keyof G]: Generator<G[P]>; } : Generator<G>[]} */
524 importGenerators : undefined;
525 }
526
527 if (!minimizerForLoader && !generatorForLoader) {
528 return;
529 }
530
531 const loader =
532 /** @type {InternalLoaderOptions<T>} */
533 {
534 test,
535 include,
536 exclude,
537 enforce: "pre",
538 loader: require.resolve(path.join(__dirname, "loader.js")),
539 options:
540 /** @type {import("./loader").LoaderOptions<T>} */
541 {
542 generator: generatorForLoader,
543 minimizer: minimizerForLoader,
544 severityError
545 }
546 };
547 const dataURILoader =
548 /** @type {InternalLoaderOptions<T>} */
549 {
550 scheme: /^data$/,
551 mimetype: /^image\/.+/i,
552 enforce: "pre",
553 loader: require.resolve(path.join(__dirname, "loader.js")),
554 options:
555 /** @type {import("./loader").LoaderOptions<T>} */
556 {
557 generator: generatorForLoader,
558 minimizer: minimizerForLoader,
559 severityError
560 }
561 };
562 compiler.options.module.rules.push(loader);
563 compiler.options.module.rules.push(dataURILoader);
564 });
565 }
566
567 compiler.hooks.thisCompilation.tap(pluginName, compilation => {
568 compilation.hooks.afterSeal.tapPromise({
569 name: pluginName
570 }, async () => {
571 await this.teardownAll();
572 });
573 });
574 compiler.hooks.compilation.tap(pluginName, compilation => {
575 compilation.hooks.processAssets.tapPromise({
576 name: pluginName,
577 stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
578 additionalAssets: true
579 }, async assets => {
580 await this.optimize(compiler, compilation, assets);
581 });
582 compilation.hooks.statsPrinter.tap(pluginName, stats => {
583 stats.hooks.print.for("asset.info.minimized").tap("image-minimizer-webpack-plugin", (minimized, {
584 green,
585 formatFlag
586 }) => minimized ?
587 /** @type {Function} */
588 green(
589 /** @type {Function} */
590 formatFlag("minimized")) : "");
591 stats.hooks.print.for("asset.info.generated").tap("image-minimizer-webpack-plugin", (generated, {
592 green,
593 formatFlag
594 }) => generated ?
595 /** @type {Function} */
596 green(
597 /** @type {Function} */
598 formatFlag("generated")) : "");
599 });
600 });
601 }
602
603}
604
605ImageMinimizerPlugin.loader = require.resolve("./loader");
606ImageMinimizerPlugin.imageminNormalizeConfig = imageminNormalizeConfig;
607ImageMinimizerPlugin.imageminMinify = imageminMinify;
608ImageMinimizerPlugin.imageminGenerate = imageminGenerate;
609ImageMinimizerPlugin.squooshMinify = squooshMinify;
610ImageMinimizerPlugin.squooshGenerate = squooshGenerate;
611module.exports = ImageMinimizerPlugin;
\No newline at end of file