plugins_doc_jsdoc.js

/* 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;