plugins_cleanup_dispose.js
/**
* @file plugin/dispose.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*//** */
const WpwPlugin = require("../base");
const { cleanupBuildDone } = require("../../utils/utils");
const { existsSync, findFiles, deleteFile, resolvePath, extname} = require("@spmhome/cmn-utils");
/**
* @augments WpwPlugin
*/
class WpwDisposePlugin extends WpwPlugin
{
/**
* @param {WpwPluginOptions} options
*/
constructor(options) { super(options); }
/**
* @override
*/
static create = WpwDisposePlugin.wrap.bind(this);
/**
* @override
* @returns {WpwPluginTapOptions<any, any, boolean>}
*/
onApply()
{
return {
cleanupAndDispose: {
async: false,
forceRun: true,
hook: "shutdown",
callback: this.cleanup.bind(this)
},
cleanStaleAssets: {
async: true,
hook: "afterEmit",
callback: this.cleanStaleAssets.bind(this)
}
};
}
cleanup()
{
this.hookstart("build cleanup");
cleanupBuildDone(this.build);
this.hookdone("build cleanup");
this.build.state = "done"
}
/**
* Removes stale hashed assets (un-hashed assets are not included)
*
* @private
* @param {WebpackCompilation} compilation
* @returns {Promise<void>}
*/
async cleanStaleAssets(compilation)
{
const build = this.build;
if (build.isRebuild) { return; }
let deleteCount = 0;
const assets = compilation.getAssets(),
isWin32 = process.platform === "win32",
l = this.hookstart("find and delete stale assets"),
lastAssets = this._arr_.asArray(this.store.get("assets")),
previousAssets = this._arr_.pushUniq(this._arr_.asArray(this.store.get("previousAssets")), ...lastAssets);
const paths = this._arr_.uniq([
build.virtualEntry.dirDist, build.virtualEntry.dirBuild, build.compiler.outputPath, build.getDistPath()
], isWin32);
try
{ await this.store.setAsync({ assets: assets.map((a) => a.name), previousAssets });
for (const p of paths) {
deleteCount += await this.cleanStaleAssetsDir(p, assets, previousAssets);
}
}
catch (e) {
this.addMessage({ exception: e, message: "unable to remove to stale asset" });
}
finally
{ l.write(` removed ${deleteCount} stale ${this._str_.pluralize("asset", deleteCount)}`, 1);
this.hookdone("completed stale asset cleanup");
}
}
/**
* @private
* @param {string} dir
* @param {WebpackAsset[]} assets
* @param {string[]} pAssetNms
* @returns {Promise<number>}
*/
async cleanStaleAssetsDir(dir, assets, pAssetNms)
{
let deleteCount = 0;
this.logger.write(` check for stale assets in directory '${dir}'`, 1);
if (existsSync(dir))
{
const b = this.build,
files = (await findFiles("**/*", { cwd: dir, absolute: false, nodir: true, posix: true, maxDepth: 6 }))
.map((f) => ({ f, s: this.fileNameStrip(f, true), se: this.fileNameStrip(f, false) }))
.filter(({ f, s, se }) =>
!assets.find((a) => a.name === f) &&
(pAssetNms.includes(f) ||
(!!b.wpc.entry &&
(this._types_.isString(b.wpc.entry) ?
(s === b.wpc.entry && (
!b.isAnyAppOrLib || (!b.isModule ? [ ".js", ".cjs" ].includes(extname(f)) : extname(f) === ".mjs")
)) : (this._types_.isArray(b.wpc.entry) ?
b.wpc.entry.includes(s) :
Object.values(b.wpc.entry).find((v) =>
this._arr_.asArray((/** @type {WpwExportConfigEntryDescriptor} */(v).import))
.find((i) => se.endsWith(i.replace(/^\.\//, "")))
)
)
)
)
)
);
if (pAssetNms.length === 0)
{ this._arr_.popBy(files, ({ f }) => !this.isOutputAsset(f, true, false, true, true));
if (this.build.options.resource) {
const resourceOutput = this._path_.fwdSlash(this.build.options.resource.output);
this._arr_.popBy(files, ({ f }) => f.startsWith(resourceOutput));
}
}
for (const { f } of files)
{ try
{ this.logger.value(" delete stale output file", f, 1);
await deleteFile(resolvePath(dir, f));
++deleteCount;
} catch (e) { throw new Error(`[${f}] ${e.message}`); }
}
}
return deleteCount;
}
}
module.exports = WpwDisposePlugin.create;