source: frontend/node_modules/webpack/lib/CleanPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 13.7 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Sergey Melyukov @smelukov
4*/
5
6"use strict";
7
8const path = require("path");
9const asyncLib = require("neo-async");
10const { SyncBailHook } = require("tapable");
11const Compilation = require("./Compilation");
12const { join } = require("./util/fs");
13const processAsyncTree = require("./util/processAsyncTree");
14
15/** @typedef {import("../declarations/WebpackOptions").CleanOptions} CleanOptions */
16/** @typedef {import("./Compiler")} Compiler */
17/** @typedef {import("./logging/Logger").Logger} Logger */
18/** @typedef {import("./util/fs").IStats} IStats */
19/** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
20/** @typedef {import("./util/fs").StatsCallback} StatsCallback */
21
22/** @typedef {Map<string, number>} Assets */
23
24/**
25 * Defines the clean plugin compilation hooks type used by this module.
26 * @typedef {object} CleanPluginCompilationHooks
27 * @property {SyncBailHook<[string], boolean | void>} keep when returning true the file/directory will be kept during cleaning, returning false will clean it and ignore the following plugins and config
28 */
29
30/**
31 * Defines the keep fn callback.
32 * @callback KeepFn
33 * @param {string} path path
34 * @returns {boolean | undefined} true, if the path should be kept
35 */
36
37const _10sec = 10 * 1000;
38
39/**
40 * merge assets map 2 into map 1
41 * @param {Assets} as1 assets
42 * @param {Assets} as2 assets
43 * @returns {void}
44 */
45const mergeAssets = (as1, as2) => {
46 for (const [key, value1] of as2) {
47 const value2 = as1.get(key);
48 if (!value2 || value1 > value2) as1.set(key, value1);
49 }
50};
51
52/** @typedef {Map<string, number>} CurrentAssets */
53
54/**
55 * Returns set of directory paths.
56 * @param {CurrentAssets} assets current assets
57 * @returns {Set<string>} Set of directory paths
58 */
59function getDirectories(assets) {
60 /** @type {Set<string>} */
61 const directories = new Set();
62 /**
63 * Adds the provided filename to this object.
64 * @param {string} filename asset filename
65 */
66 const addDirectory = (filename) => {
67 directories.add(path.dirname(filename));
68 };
69
70 // get directories of assets
71 for (const [asset] of assets) {
72 addDirectory(asset);
73 }
74 // and all parent directories
75 for (const directory of directories) {
76 addDirectory(directory);
77 }
78 return directories;
79}
80
81/** @typedef {Set<string>} Diff */
82
83/**
84 * Returns diff to fs.
85 * @param {OutputFileSystem} fs filesystem
86 * @param {string} outputPath output path
87 * @param {CurrentAssets} currentAssets filename of the current assets (must not start with .. or ., must only use / as path separator)
88 * @param {(err?: Error | null, set?: Diff) => void} callback returns the filenames of the assets that shouldn't be there
89 * @returns {void}
90 */
91const getDiffToFs = (fs, outputPath, currentAssets, callback) => {
92 const directories = getDirectories(currentAssets);
93 /** @type {Diff} */
94 const diff = new Set();
95 asyncLib.forEachLimit(
96 directories,
97 10,
98 (directory, callback) => {
99 /** @type {NonNullable<OutputFileSystem["readdir"]>} */
100 (fs.readdir)(join(fs, outputPath, directory), (err, entries) => {
101 if (err) {
102 if (err.code === "ENOENT") return callback();
103 if (err.code === "ENOTDIR") {
104 diff.add(directory);
105 return callback();
106 }
107 return callback(err);
108 }
109 for (const entry of /** @type {string[]} */ (entries)) {
110 const file = entry;
111 // Since path.normalize("./file") === path.normalize("file"),
112 // return file directly when directory === "."
113 const filename =
114 directory && directory !== "." ? `${directory}/${file}` : file;
115 if (!directories.has(filename) && !currentAssets.has(filename)) {
116 diff.add(filename);
117 }
118 }
119 callback();
120 });
121 },
122 (err) => {
123 if (err) return callback(err);
124
125 callback(null, diff);
126 }
127 );
128};
129
130/**
131 * Gets diff to old assets.
132 * @param {Assets} currentAssets assets list
133 * @param {Assets} oldAssets old assets list
134 * @returns {Diff} diff
135 */
136const getDiffToOldAssets = (currentAssets, oldAssets) => {
137 /** @type {Diff} */
138 const diff = new Set();
139 const now = Date.now();
140 for (const [asset, ts] of oldAssets) {
141 if (ts >= now) continue;
142 if (!currentAssets.has(asset)) diff.add(asset);
143 }
144 return diff;
145};
146
147/**
148 * Processes the provided f.
149 * @param {OutputFileSystem} fs filesystem
150 * @param {string} filename path to file
151 * @param {StatsCallback} callback callback for provided filename
152 * @returns {void}
153 */
154const doStat = (fs, filename, callback) => {
155 if ("lstat" in fs) {
156 /** @type {NonNullable<OutputFileSystem["lstat"]>} */
157 (fs.lstat)(filename, callback);
158 } else {
159 fs.stat(filename, callback);
160 }
161};
162
163/**
164 * Processes the provided f.
165 * @param {OutputFileSystem} fs filesystem
166 * @param {string} outputPath output path
167 * @param {boolean} dry only log instead of fs modification
168 * @param {Logger} logger logger
169 * @param {Diff} diff filenames of the assets that shouldn't be there
170 * @param {KeepFn} isKept check if the entry is ignored
171 * @param {(err?: Error, assets?: Assets) => void} callback callback
172 * @returns {void}
173 */
174const applyDiff = (fs, outputPath, dry, logger, diff, isKept, callback) => {
175 /**
176 * Processes the provided msg.
177 * @param {string} msg message
178 */
179 const log = (msg) => {
180 if (dry) {
181 logger.info(msg);
182 } else {
183 logger.log(msg);
184 }
185 };
186 /** @typedef {{ type: "check" | "unlink" | "rmdir", filename: string, parent: { remaining: number, job: Job } | undefined }} Job */
187 /** @type {Job[]} */
188 const jobs = Array.from(diff.keys(), (filename) => ({
189 type: "check",
190 filename,
191 parent: undefined
192 }));
193 /** @type {Assets} */
194 const keptAssets = new Map();
195 processAsyncTree(
196 jobs,
197 10,
198 ({ type, filename, parent }, push, callback) => {
199 const path = join(fs, outputPath, filename);
200 /**
201 * Describes how this handle error operation behaves.
202 * @param {Error & { code?: string }} err error
203 * @returns {void}
204 */
205 const handleError = (err) => {
206 const isAlreadyRemoved = () =>
207 new Promise((resolve) => {
208 if (err.code === "ENOENT") {
209 resolve(true);
210 } else if (err.code === "EPERM") {
211 // https://github.com/isaacs/rimraf/blob/main/src/fix-eperm.ts#L37
212 // fs.existsSync(path) === false https://github.com/webpack/webpack/actions/runs/15493412975/job/43624272783?pr=19586
213 doStat(fs, path, (err) => {
214 if (err) {
215 resolve(err.code === "ENOENT");
216 } else {
217 resolve(false);
218 }
219 });
220 } else {
221 resolve(false);
222 }
223 });
224
225 isAlreadyRemoved().then((isRemoved) => {
226 if (isRemoved) {
227 log(`${filename} was removed during cleaning by something else`);
228 handleParent();
229 return callback();
230 }
231 return callback(err);
232 });
233 };
234 const handleParent = () => {
235 if (parent && --parent.remaining === 0) push(parent.job);
236 };
237 switch (type) {
238 case "check":
239 if (isKept(filename)) {
240 keptAssets.set(filename, 0);
241 // do not decrement parent entry as we don't want to delete the parent
242 log(`${filename} will be kept`);
243 return process.nextTick(callback);
244 }
245 doStat(fs, path, (err, stats) => {
246 if (err) return handleError(err);
247 if (!(/** @type {IStats} */ (stats).isDirectory())) {
248 push({
249 type: "unlink",
250 filename,
251 parent
252 });
253 return callback();
254 }
255
256 /** @type {NonNullable<OutputFileSystem["readdir"]>} */
257 (fs.readdir)(path, (err, _entries) => {
258 if (err) return handleError(err);
259 /** @type {Job} */
260 const deleteJob = {
261 type: "rmdir",
262 filename,
263 parent
264 };
265 const entries = /** @type {string[]} */ (_entries);
266 if (entries.length === 0) {
267 push(deleteJob);
268 } else {
269 const parentToken = {
270 remaining: entries.length,
271 job: deleteJob
272 };
273 for (const entry of entries) {
274 const file = /** @type {string} */ (entry);
275 if (file.startsWith(".")) {
276 log(
277 `${filename} will be kept (dot-files will never be removed)`
278 );
279 continue;
280 }
281 push({
282 type: "check",
283 filename: `${filename}/${file}`,
284 parent: parentToken
285 });
286 }
287 }
288 return callback();
289 });
290 });
291 break;
292 case "rmdir":
293 log(`${filename} will be removed`);
294 if (dry) {
295 handleParent();
296 return process.nextTick(callback);
297 }
298 if (!fs.rmdir) {
299 logger.warn(
300 `${filename} can't be removed because output file system doesn't support removing directories (rmdir)`
301 );
302 return process.nextTick(callback);
303 }
304 fs.rmdir(path, (err) => {
305 if (err) return handleError(err);
306 handleParent();
307 callback();
308 });
309 break;
310 case "unlink":
311 log(`${filename} will be removed`);
312 if (dry) {
313 handleParent();
314 return process.nextTick(callback);
315 }
316 if (!fs.unlink) {
317 logger.warn(
318 `${filename} can't be removed because output file system doesn't support removing files (rmdir)`
319 );
320 return process.nextTick(callback);
321 }
322 fs.unlink(path, (err) => {
323 if (err) return handleError(err);
324 handleParent();
325 callback();
326 });
327 break;
328 }
329 },
330 (err) => {
331 if (err) return callback(err);
332 callback(undefined, keptAssets);
333 }
334 );
335};
336
337/** @type {WeakMap<Compilation, CleanPluginCompilationHooks>} */
338const compilationHooksMap = new WeakMap();
339
340const PLUGIN_NAME = "CleanPlugin";
341
342class CleanPlugin {
343 /**
344 * Returns the attached hooks.
345 * @param {Compilation} compilation the compilation
346 * @returns {CleanPluginCompilationHooks} the attached hooks
347 */
348 static getCompilationHooks(compilation) {
349 if (!(compilation instanceof Compilation)) {
350 throw new TypeError(
351 "The 'compilation' argument must be an instance of Compilation"
352 );
353 }
354 let hooks = compilationHooksMap.get(compilation);
355 if (hooks === undefined) {
356 hooks = {
357 keep: new SyncBailHook(["ignore"])
358 };
359 compilationHooksMap.set(compilation, hooks);
360 }
361 return hooks;
362 }
363
364 /** @param {CleanOptions} options options */
365 constructor(options = {}) {
366 /** @type {CleanOptions} */
367 this.options = options;
368 }
369
370 /**
371 * Applies the plugin by registering its hooks on the compiler.
372 * @param {Compiler} compiler the compiler instance
373 * @returns {void}
374 */
375 apply(compiler) {
376 compiler.hooks.validate.tap(PLUGIN_NAME, () => {
377 compiler.validate(
378 () => {
379 const { definitions } = require("../schemas/WebpackOptions.json");
380
381 return {
382 definitions,
383 oneOf: [{ $ref: "#/definitions/CleanOptions" }]
384 };
385 },
386 this.options,
387 {
388 name: "Clean Plugin",
389 baseDataPath: "options"
390 }
391 );
392 });
393
394 const { keep } = this.options;
395
396 /** @type {boolean} */
397 const dry = this.options.dry || false;
398 /** @type {KeepFn} */
399 const keepFn =
400 typeof keep === "function"
401 ? keep
402 : typeof keep === "string"
403 ? (path) => path.startsWith(keep)
404 : typeof keep === "object" && keep.test
405 ? (path) => keep.test(path)
406 : () => false;
407
408 // We assume that no external modification happens while the compiler is active
409 // So we can store the old assets and only diff to them to avoid fs access on
410 // incremental builds
411 /** @type {undefined | Assets} */
412 let oldAssets;
413
414 compiler.hooks.emit.tapAsync(
415 {
416 name: PLUGIN_NAME,
417 stage: 100
418 },
419 (compilation, callback) => {
420 const hooks = CleanPlugin.getCompilationHooks(compilation);
421 const logger = compilation.getLogger(`webpack.${PLUGIN_NAME}`);
422 const fs = /** @type {OutputFileSystem} */ (compiler.outputFileSystem);
423
424 if (!fs.readdir) {
425 return callback(
426 new Error(
427 `${PLUGIN_NAME}: Output filesystem doesn't support listing directories (readdir)`
428 )
429 );
430 }
431
432 /** @type {Assets} */
433 const currentAssets = new Map();
434 const now = Date.now();
435 for (const asset of Object.keys(compilation.assets)) {
436 if (/^[a-z]:\\|^\/|^\\\\/i.test(asset)) continue;
437 /** @type {string} */
438 let normalizedAsset;
439 let newNormalizedAsset = asset.replace(/\\/g, "/");
440 do {
441 normalizedAsset = newNormalizedAsset;
442 newNormalizedAsset = normalizedAsset.replace(
443 /(^|\/)(?!\.\.)[^/]+\/\.\.\//g,
444 "$1"
445 );
446 } while (newNormalizedAsset !== normalizedAsset);
447 if (normalizedAsset.startsWith("../")) continue;
448 const assetInfo = compilation.assetsInfo.get(asset);
449 if (assetInfo && assetInfo.hotModuleReplacement) {
450 currentAssets.set(normalizedAsset, now + _10sec);
451 } else {
452 currentAssets.set(normalizedAsset, 0);
453 }
454 }
455
456 const outputPath = compilation.getPath(compiler.outputPath, {});
457
458 /**
459 * Checks whether this clean plugin is kept.
460 * @param {string} path path
461 * @returns {boolean | undefined} true, if needs to be kept
462 */
463 const isKept = (path) => {
464 const result = hooks.keep.call(path);
465 if (result !== undefined) return result;
466 return keepFn(path);
467 };
468
469 /**
470 * Processes the provided err.
471 * @param {(Error | null)=} err err
472 * @param {Diff=} diff diff
473 */
474 const diffCallback = (err, diff) => {
475 if (err) {
476 oldAssets = undefined;
477 callback(err);
478 return;
479 }
480 applyDiff(
481 fs,
482 outputPath,
483 dry,
484 logger,
485 /** @type {Diff} */ (diff),
486 isKept,
487 (err, keptAssets) => {
488 if (err) {
489 oldAssets = undefined;
490 } else {
491 if (oldAssets) mergeAssets(currentAssets, oldAssets);
492 oldAssets = currentAssets;
493 if (keptAssets) mergeAssets(oldAssets, keptAssets);
494 }
495 callback(err);
496 }
497 );
498 };
499
500 if (oldAssets) {
501 diffCallback(null, getDiffToOldAssets(currentAssets, oldAssets));
502 } else {
503 getDiffToFs(fs, outputPath, currentAssets, diffCallback);
504 }
505 }
506 );
507 }
508}
509
510module.exports = CleanPlugin;
511module.exports._getDirectories = getDirectories;
Note: See TracBrowser for help on using the repository browser.