UNPKG

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