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;