/* eslint-disable no-template-curly-in-string */
/**
* @file plugins//doc/jsdoc.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*//** */
const WpwDocPlugin = require("./base");
const { wpwPath } = require("../../utils/utils");
const { isWpwPluginConfigDocJsdocTemplate } = require("../../types/constants");
/**
* @augments WpwDocPlugin
*/
class WpwJsDocDocPlugin extends WpwDocPlugin
{
/**
* @param {WpwPluginOptions} options
*/
constructor(options)
{
super("jsdoc", { ...options, taskHandler: "executeJsdocDocumentationBuild" });
this.buildOptions = /** @type {WpwJsdocDocPluginOptions} */(this.buildOptions);
}
/**
* @override
*/
static create = WpwJsDocDocPlugin.wrap.bind(this);
// /**
// * @override
// * @param {WpwJsdocDocPluginOptions} _
// * @param {WpwBuild} build
// */
// static validate = (_, build) => existsSync(resolvePath(build.getBasePath(), "node_modules/jsdoc"));
/**
* @param {WebpackCompilationAssets} _assets
* @returns {Promise<WpwPluginTaskResult | SpmhError>}
*/
async executeJsdocDocumentationBuild(_assets)
{
let isTemplateCfg = false;
const build = this.build,
logger = build.logger,
bo = this.buildOptions,
bdo = build.options.doc,
wpwGlobalDir = wpwPath(),
srcDir = build.getSrcPath(),
baseDir = build.getBasePath(),
ctxDir = build.getContextPath(),
outDir = !bdo.output ? build.virtualEntry.dirBuild :
this._path_.resolvePath(build.virtualEntry.dirBuild, bdo.output),
// * @type {WpwPluginConfigJsDoc} */
/** @type {Record<string, any>} */
config = { destination: outDir };
logger.write("create jsdoc documentation", 1);
logger.value(" mode", bo.mode, 1);
logger.value(" base directory", baseDir, 2);
logger.value(" context directory", ctxDir, 2);
logger.value(" input directory", srcDir, 2);
logger.value(" output directory", outDir, 2);
logger.value(" wpw directory", wpwGlobalDir, 2);
//
// jsdoc.json config file (use the template config file if not found)
//
if (bo.configFile)
{ const cfgFileAbs = this._path_.resolvePathEx({ psx: true, stat: true }, baseDir, bo.configFile);
if (cfgFileAbs) {
config.configure = cfgFileAbs;
}
}
if (!config.configure)
{ config.configure = await this._fs_.findExPath([
this._path_.joinPath(ctxDir, ".jsdoc.json"),
this._path_.joinPath(ctxDir, "jsdoc.json"),
this._path_.joinPath(baseDir, ".jsdoc.json"),
this._path_.joinPath(baseDir, "jsdoc.json"),
this._path_.joinPath(srcDir, ".jsdoc.json"),
this._path_.joinPath(srcDir, "jsdoc.json"),
this._path_.resolve(wpwGlobalDir, "schema/template/jsdoc/.jsdoc.json"),
this._path_.resolve(wpwGlobalDir, "doc/examples/jsdoc/.jsdoc.json")
]);
if (!config.configure)
{ return this.addMessage({
code: this.MsgCode.ERROR_RESOURCE_MISSING,
message: "could not find a valid jsdoc configuration file"
});
}
isTemplateCfg = config.configure.startsWith(wpwGlobalDir);
}
config.configure = this._path_.fwdSlash(config.configure);
logger.value(" jsdoc configuration file", config.configure, 1);
if (bo.mode !== "config")
{
this._obj_.apply(config, {
verbose: build.logger.level >= 4 || bo.verbose,
debug: build.logger.level >= 5 || bo.debug,
package: this._path_.relativePathEx(baseDir, build.pkgJsonFilePath, { psx: true })
});
//
// Examples directory
//
let path = bo.examplesDir;
if (!path) {
path = await this._fs_.findFiles("**/examples/", { cwd: baseDir, maxDepth: 2, nodir: false })[0];
}
else if (!(await this._fs_.existsAsync(this._path_.resolvePath(baseDir, path))))
{ return this.addMessage({
code: this.MsgCode.ERROR_RESOURCE_MISSING,
message: "specified jsdoc 'examples' directory path does not exist"
});
}
if (path) {
config.examples = this._path_.relativePathEx(baseDir, path, { psx: true });
}
logger.value(" examples directory", config.examples, 1);
//
// Tutorials directory
//
path = bo.tutorialsDir;
if (!path)
{
path = await this._fs_.findFiles("**/tutorials/", { cwd: baseDir, maxDepth: 2, nodir: false })[0];
}
else if (!(await this._fs_.existsAsync(this._path_.resolvePath(baseDir, path))))
{
return this.addMessage({
code: this.MsgCode.ERROR_RESOURCE_MISSING,
message: "specified jsdoc 'tutorials' directory path does not exist"
});
}
if (path) {
config.tutorials = this._path_.relativePathEx(baseDir, path, { psx: true });
}
logger.value(" tutorials directory", config.tutorials, 1);
//
// README file
//
path = bo.readmeFile || await this._fs_.findExPath([
this._path_.joinPath(ctxDir, "README.txt"),
this._path_.joinPath(ctxDir, "README.md"),
this._path_.joinPath(ctxDir, "README"),
this._path_.joinPath(baseDir, "README.txt"),
this._path_.joinPath(baseDir, ".README.md"),
this._path_.joinPath(baseDir, "README"),
this._path_.joinPath(baseDir, ".README")
]);
if (path) {
config.readme = this._path_.relativePathEx(baseDir, path, { psx: true });
}
logger.value(" readme file", config.readme, 1);
}
//
// Read jsdoc.json config file, defaults to internal 'schema/template/.jsdoc.json' if one
// does not exist within the project
//
let rcContent = (await this._fs_.readFileAsync(config.configure));
if (isTemplateCfg)
{
const spmhRc = await this._fs_.readJsonAsync(this._path_.resolvePath(baseDir, ".spmhrc.json")),
pkgJson = (await this._fs_.findDotJsonFileUpAsync("package.json", ctxDir)).data,
tsConfig = (await this._fs_.findDotJsonFileUpAsync("tsconfig.json", ctxDir)).data,
wpwRootPath = this._path_.fwdSlash(wpwGlobalDir),
packageNameParts = this._arr_.asArray(pkgJson.name?.split("/")),
packageName = packageNameParts[1] || packageNameParts[0] || "package",
packageTitle = this._str_.toTitleCase(packageName.replaceAll("-", " ")).replaceAll(" ", "-"),
packageAuthor = this._str_.toTitleCase(pkgJson.author?.name || pkgJson.author ||
pkgJson.author?.email || pkgJson.bugs?.email || "n/a"),
packageType = pkgJson.type === "module" || pkgJson.module ? "module" : "commonjs",
packageDescription = pkgJson.description || packageName,
packageRepoUrl = pkgJson.repository?.url || pkgJson.repository || "n/a",
packageDonateUrl = pkgJson.funding?.url || pkgJson.funding || "n/a",
packageBaseUrl = (pkgJson.bugs?.url || pkgJson.bugs)?.split("/").slice(0, 3).join("") || "",
excludesArray = this._types_.isArray(tsConfig.exclude, false) ? `"${tsConfig.exclude.join("\", \"")}"` : "",
includesArray = this._types_.isArray(tsConfig.include, false) ? `"${tsConfig.include.join("\", \"")}"` :
(this._types_.isArray(tsConfig.files, false) ? `"${tsConfig.files.join("\", \"")}"` : ""),
resourcePath = await this._fs_.findExPath(
[ "res", "resource", "resources", "public", "static" ], [ baseDir, ctxDir ], true, "res"
),
resourcePathRel = this._path_.relativeEx(baseDir, resourcePath, { psx: true }),
distPath = spmhRc.wpw?.paths?.dist || this._fs_.findExPathSync(
[ "dist", "out", "build", "release" ], [ baseDir, ctxDir ], true
) || "dist",
distPathRel = this._path_.relativeEx(baseDir, distPath, { psx: true }),
wwwBaseDocPath = spmhRc.ap?.httpReleaseZipPath?.replace("${SPMHOME_SSH_UPLOAD_PATH}/", resourcePathRel)
.replace("${AP_projectSlug}", packageName) ||
`${resourcePathRel}/product/${packageName}/doc/api`,
wwwBaseDocUrl = `${packageBaseUrl}/${wwwBaseDocPath}`;
rcContent = rcContent
.replaceAll("$(RC_PATH_DIST)", distPathRel)
.replaceAll("$(PACKAGE_NAME)", packageName)
.replaceAll("$(PACKAGE_TYPE)", packageType)
.replaceAll("$(WPW_ROOT_PATH)", wpwRootPath)
.replaceAll("$(PACKAGE_TITLE)", packageTitle)
.replaceAll("$(DONATE_URL)", packageDonateUrl)
.replaceAll("$(WWW_BASE_URL)", packageBaseUrl)
.replaceAll("$(PACKAGE_AUTHOR)", packageAuthor)
.replaceAll("$(REPOSITORY_URL)", packageRepoUrl)
.replaceAll("$(WWW_BASE_DOC_URL)", wwwBaseDocUrl)
.replaceAll("$(RC_PATH_RESOURCES)", resourcePathRel)
.replaceAll("$(RC_DOT_PATH_DIST)", `./${distPathRel}`)
.replaceAll("$(PACKAGE_DESCRIPTION)", packageDescription)
.replaceAll("\"$(RC_ARRAY_PATH_EXCLUDE)\"", excludesArray)
.replaceAll("\"$(RC_ARRAY_PATH_INCLUDE)\"", includesArray)
.replaceAll("$(RC_DOT_PATH_RESOURCES)", `./${resourcePathRel}`)
.replaceAll("\"$(RC_ARRAY_PATH_RESOURCES)\"", `"./${resourcePathRel}"`);
// const tmpDir = build.getTempPath({ path: build.name });
// this._fs_.createDirSync(tmpDir);
config.configure = this._path_.joinPath(baseDir, ".jsdoc.json");
this._fs_.writeFileSync(config.configure, rcContent);
config.configure = ".jsdoc.json";
}
/** @type {Record<string, any>} */
const fileConfig = this._json_.safeParse(rcContent);
//
// Get jsdoc cli arguments
//
const jsdocArgs = this._cmn_.toCliArgs(config);
//
// Recurse flag
//
if (fileConfig.source.include)
{ for (const include of fileConfig.source.include)
{ if (this._fs_.isDir(this._path_.resolvePath(baseDir, include))) {
jsdocArgs.push("--recurse");
break;
} } }
else
{ const srcDir = build.getSrcPath({ rel: true, psx: true, dot: true, fallback: true });
jsdocArgs.push("--recurse", srcDir.includes(" ") && srcDir[0] !== "\"" ? `"${srcDir}"` : srcDir);
}
//
// Template / Theme
//
if (bo.template)
{
config.template = (isWpwPluginConfigDocJsdocTemplate(bo.template) ?
`node_modules/${bo.template}` :
"jsdoc/default").replace("node_modules/jsdoc", "jsdoc");
jsdocArgs.push("--template", config.template);
logger.value(" set template by options", config.template, 2);
}
else if (!fileConfig.opts.template)
{
config.template = "node_modules/clean-jsdoc-theme";
jsdocArgs.push("--template", config.template);
logger.value(" set template by config file", config.template, 2);
}
const result = this.executeJsDoc(jsdocArgs, " ");
if (isTemplateCfg) {
// try { await this._fs_.deleteFile(config.configure); } catch {}
}
return result;
};
/**
* @private
* @param {string[]} args
* @param {string} lPad
* @returns {Promise<WpwPluginTaskResult | SpmhError>}
*/
async executeJsDoc(args, lPad)
{
const build = this.build,
logger = build.logger,
cmdLineArgs = args.join(" "),
jsdocCmd = `npx jsdoc ${cmdLineArgs}`;
logger.write("execute jsdoc command", 1, lPad);
const ignore = [ "**/test/**", "**/tests/**", "**/types/**", "**/typings/**" ],
result = await this.exec(jsdocCmd, "jsdoc", true, { logPad: lPad + " " });
if (result.code !== 0 || this.build.errorCount > 0)
{ return this.addMessage({
code: this.MsgCode.ERROR_JSDOC_FAILED,
message: "error encountered while attempting to execute jsdoc command"
}, true);
}
const srcpaths = await this._fs_.findFiles("**/*", { cwd: this.build.getSrcPath(), maxDepth: 1, nodir: false, ignore });
return { tag: "jsdoc", glob: "**/*", result, paths: [ build.virtualEntry.dirBuild ], srcpaths };
}
}
module.exports = WpwJsDocDocPlugin.create;