plugins_copy.js

/**
 * @file plugin/copy.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 *//** */

const WpwPlugin = require("./base");
const {
	copyDirAsync, copyFileAsync, existsSync, isDirectory, findFiles, readFileAsync, relativePathEx, resolvePath, normalizePath
} = require("@spmhome/cmn-utils");


/**
 * @augments WpwPlugin
 */
class WpwCopyPlugin extends WpwPlugin
{
	/**
	 * @param {WpwPluginOptions} options Plugin options to be applied
	 */
	constructor(options)
	{
		super(options);
        this.buildOptions = /** @type {WpwBuildOptionsPluginConfig<"copy">} */(this.buildOptions);
	}


	/**
     * @override
     */
	static create = WpwCopyPlugin.wrap.bind(this);


    /**
     * @override
     * @param {WebpackCompiler} _compiler
     * @returns {WpwPluginTapOptions<any, any, boolean> | undefined}
     */
    onApply(_compiler)
    {
		return {
			copyFileResourcesToOutput: {
				async: true,
				hook: "compilation",
				stage: "ADDITIONAL",
				statsProperty: "copy",
				hookCompilation: "processAssets",
				callback: this.copyFileResourcesToOutput.bind(this)
			}
		};
    }


	/**
	 * @private
	 * @param {WebpackCompilation} _compilation
	 * @returns {Promise<void | Error>}
	 */
	async copyFileResourcesToOutput(_compilation)
	{
		const b = this.build,
			  basePath = b.getBasePath(),
		      items = this.buildOptions.items || [];
		this.hookstart();
		try
		{   for (const i of items) {
				await this.execCopyTask(resolvePath(basePath, i.input),  resolvePath(basePath, i.output), i, "   ");
			}
		}
		catch (e) {
			return e;
		}
		finally {
			this.hookdone();
		}
	}


	/**
	 * @private
	 * @param {string} from
	 * @param {string} to
	 * @param {WpwPluginInputOutputPath} io
	 * @param {string | number} lPad
	 * @returns {Promise<void | Error>}
	 */
	async execCopyTask(from, to, io, lPad)
	{
		const l = this.build.logger, compilation = this.compilation, isDir = isDirectory(from),
			  outputPath = normalizePath(compilation.outputOptions.path || this.compiler.outputPath),
			  outputPathIsVDir = outputPath.startsWith(this.build.virtualEntry.dir);

		l.write("execute copy task", 1, lPad);
		l.value("   input path", from, 1, lPad);
		l.value("   output path", to, 1, lPad);

		if (!existsSync(from)) {
			return l.warn("   copy source 'from' does not exist", lPad);
		}

		/**
		 * @param {string[]} files
		 * @returns {Promise<number>}
		 */
		const _emit = async (files) =>
		{   for (const a of files.map((p) =>
				({ path: p, name: relativePathEx(this.build.getBasePath(), p, { psx: true }), asset: compilation.getAsset(p) })
			))
			{   let source = a.asset?.source;
				if (!source)
				{   const content = await readFileAsync(a.path);
					source = new this.build.wp.sources.RawSource(content);
				}
				l.write(`      emit file asset italic(${a.name})`, 3, lPad);
				compilation.emitAsset(a.name, source, { copy: true, immutable: !!io.assets?.immutable });
			}
			return files.length;
		};

		let tct = 0, fct = 0, act = 0, dct = 0;
		try
		{   let files;
			if (io.assets)
			{
				l.write("   add input files as compilation file dependencies", 1, lPad);
				files = isDir ? await findFiles(io.assets?.glob || "**/*", { cwd: from }) : [ from ];
				dct = tct = files.length;
				compilation.fileDependencies.addAll(files);
				l.write("   emit destination files as compilation assets", 1, lPad);
				act = await _emit(files);
			}
			if (!io.assets || outputPathIsVDir)
			{
				l.write("   static copy input directory to destination", 1, lPad);
				if (isDir) {
					fct = tct = await copyDirAsync(from, to);
				}
				else {
					await copyFileAsync(from, to);
					fct = tct = 1;
				}
			}

			l.write(`   ${tct} total files processed`, 1, lPad);
			l.write(`   ${fct} total files copied (static/raw)`, 1, lPad);
			l.write(`   ${act} total file assets emitted`, 1, lPad);
			l.write(`   ${dct} total file dependencies added`, 1, lPad);
			l.write("copy task completed", 1, lPad);
		}
		catch (e) {
			return this.addMessage({ exception: e, code: this.MsgCode.ERROR_COPY_FAILED, message: "copy task failed" });
		}
	}
}


module.exports = WpwCopyPlugin.create;