// @ts-check
/**
* @file plugin/upload.js
* !!! This module uses 'plink' and 'pscp' from the PuTTY package: https://www.putty.org
* !!! For first time build on fresh os install:
* !!! - create the environment variables WPW_APP3_SSH_AUTH_*
* !!! - run a plink command manually to generate and trust the fingerprints:
* !!! plink -ssh -pw <PWD> smeesseman@app3.spmeesseman.com "echo hello"
* !!! plink -ssh -batch -pw <PWD> ubuntu@app3.spmhome.io "echo hello"
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*//** */
const WpwPlugin = require("../base");
const WpwRegex = require("../../utils/regex");
const { rm, readdir, rename } = require("fs/promises");
const { getUniqBuildKey } = require("../../utils/utils");
const { copyFileAsync, createDirAsync, existsSync, joinPath, resolvePath } = require("@spmhome/cmn-utils");
/**
* @augments WpwPlugin
*/
class WpwUploadPlugin extends WpwPlugin
{
/**
* @param {WpwPluginOptions} options Plugin options to be applied
*/
constructor(options)
{
super(options);
this.buildOptions = /** @type {WpwBuildOptionsPluginConfig<"upload">} */(this.buildOptions);
}
/**
* @override
*/
static create = WpwUploadPlugin.wrap.bind(this);
/**
* @override
* @returns {WpwPluginTapOptions<any, any, boolean>}
*/
onApply()
{
return {
uploadAssets: {
async: true,
hook: "afterEmit",
callback: this.uploadAssets.bind(this)
},
cleanupAssetUpload: {
async: true,
hook: "done",
callback: this.cleanup.bind(this)
}
};
}
/**
* @private
* @param {WebpackStats} _stats
*/
async cleanup(_stats)
{
const tmpUploadPath = joinPath(this.build.paths.temp, this.build.mode);
try
{ if (existsSync(tmpUploadPath))
{
const tmpFiles = await readdir(tmpUploadPath);
if (tmpFiles.length > 0)
{
await rm(tmpUploadPath, { recursive: true, force: true });
}
this.build.logger.write("upload plugin cleanup completed");
} }
catch (e)
{ this.addMessage({
exception: e,
capture: this.cleanup,
code: this.MsgCode.ERROR_GENERAL,
message: "exception while cleaning upload-from path"
});
}
};
/**
* @public
* @param {WebpackCompilation} compilation
*/
async uploadAssets(compilation)
{ //
// `The lBasePath` variable is a temp directory that we will create in the in
// the OS/env temp dir. We will move only files that have changed content there,
// and perform only one upload when all builds have completed.
//
const build = this.build,
logger = build.logger,
ep = this.buildOptions.endpoint,
toUploadPath = joinPath(build.paths.temp, getUniqBuildKey(build, true));
logger.write("upload resource files", 1);
// const host = process.env.WPBUILD_APP3_SSH_UPLOAD_HOST,
// user = process.env.WPBUILD_APP3_SSH_UPLOAD_USER,
// bo.path = process.env.WPBUILD_APP3_SSH_UPLOAD_PATH,
// sshAuth = process.env.WPBUILD_APP3_SSH_UPLOAD_AUTH,
// sshAuthFlag = process.env.WPBUILD_APP3_SSH_UPLOAD_FLAG;
if (!existsSync(toUploadPath)) {
await createDirAsync(toUploadPath);
}
if (this.buildOptions.paths)
{
for (const p of this._arr_.asArray(this.buildOptions.paths))
{
const pathAbs = resolvePath(build.getContextPath(), p);
if (this._fs_.isDir(pathAbs))
{
await this._fs_.copyDirAsync(pathAbs, toUploadPath, [], true);
}
else {
await this._fs_.copyFileAsync(pathAbs, joinPath(toUploadPath, this._path_.basename(p)));
}
}
}
else
{
for (const chunk of Array.from(compilation.chunks)
.filter(c => c.canBeInitial() && c.name && !WpwRegex.TestsChunk.test(c.name)))
{
for (const file of Array.from(chunk.files))
{
const asset = compilation.getAsset(file);
if (asset && chunk.name && (build.global.hash.next[chunk.name] !== build.global.hash.current[chunk.name] ||
!build.global.hash.previous[chunk.name]))
{
const distPath = this.compiler.outputPath || this.compilation.outputOptions.path || build.getDistPath();
logger.value(" queue asset for upload", logger.tag(file), 2);
logger.value(" asset info", asset.info, 4);
await copyFileAsync(joinPath(distPath, file), joinPath(toUploadPath, file));
if (asset.info.related?.sourceMap)
{
const sourceMapFile = asset.info.related.sourceMap.toString();
logger.value(" queue sourcemap for upload", logger.tag(sourceMapFile), 2);
if (build.mode === "production") {
logger.value(" remove production sourcemap from distribution", sourceMapFile, 3);
await rename(joinPath(distPath, sourceMapFile), joinPath(toUploadPath, sourceMapFile));
}
else {
await copyFileAsync(joinPath(distPath, sourceMapFile), joinPath(toUploadPath, sourceMapFile));
}
}
}
else /* istanbul ignore else */if (asset) {
logger.value(" unchanged, skip asset upload", logger.tag(file), 2);
}
else {
logger.value(" unknown error, skip asset upload", logger.tag(file), 2);
}
}
}
}
const filesToUpload = await readdir(toUploadPath);
if (filesToUpload.length === 0)
{
logger.write("no assets to upload", 1, "");
return;
}
const name = build.pkgJson.scopedName.name;
const plinkCmds = [
`mkdir -p ${ep.path}/product`,
`mkdir -p ${ep.path}/product/${name}`,
`mkdir -p ${ep.path}/product/${name}/doc`,
`mkdir ${ep.path}/${name}/v${build.pkgJson.version}`
// `mkdir ${ep.path}/${name}/v${build.pkgJson.version}/${build.mode}`,
// `rm -f ${ep.path}/${name}/v${build.pkgJson.version}/${build.mode}/*.*`
];
if (build.mode === "production") { plinkCmds.pop(); }
const plinkArgs = [
"-ssh", // force use of ssh protocol
"-batch", // disable all interactive prompts
"-pw", ep.credential.key, // auth key
`${ep.credential.user}@${ep.host}`,
plinkCmds.join(";")
];
const pscpArgs = [
"-pw", ep.credential.key, // auth key
"-q", // quiet, don't show statistics
"-r", // copy directories recursively
toUploadPath, // dir containing the files to upload, the "dir" itself (prod/dev/test) will be
`${ep.credential.user}@${ep.host}:"${ep.path}/product/${name}"`
];
// await copyFileAsync(
// joinPath(build.getBasePath(),
// "node_modules", "source-map", "lib", "mappings.wasm"),
// joinPath(toUploadPath, "mappings.wasm")
// );
logger.write(` resource files prepared for upload to ${ep.host}`, 1, "");
try
{ logger.write(" plink: create / clear remmote directory", 1);
let rc = await this.exec("plink " + plinkArgs.join(" "), "plink");
if (rc.code === 0)
{ logger.write(" pscp: upload files", 1, "");
rc = await this.exec("pscp " + pscpArgs.join(" "), "pscp");
if (rc.code === 0)
{ filesToUpload.forEach((f) => {
logger.write(` ${logger.icons.tag.success} uploaded ${this._path_.basename(f)}`, 1)
});
logger.write("successfully uploaded resource files", 1);
}
}
}
catch (e)
{ logger.error("error uploading resource files:");
filesToUpload.forEach((f) => {
logger.write(` ${logger.icons.tag.error} upload ${this._path_.basename(f)} failed`, 1)
});
logger.error(e);
}
finally {
try { await rm(toUploadPath, { recursive: true, force: true }); } catch (e) { logger.error(e); }
}
}
}
module.exports = WpwUploadPlugin.create;