exports_cache.js

/**
 * @file src/exports/cache.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 *
 * @see {@link https://webpack.js.org/configuration/cache webpack.js.org/cache}
 *
 *//** */

const WpwWebpackExport = require("./base");
const { isWpwExportConfigCacheType } = require("../utils");
const enableNodeCache = require("node:module").enableCompileCache;
const { nodejsModulesGlobalPath, nodejsRtMajor } = require("@spmhome/cmn-utils");
const { wpwNodeModulesPath, getScriptBuildDependencies } = require("../utils/utils");


/**
 * @augments WpwWebpackExport
 */
class WpwCacheExport extends WpwWebpackExport
{
    /**
     * @private
     */
    buildDeps;
    /**
     * @type {WebpackSnapshotOptions}
     */
    snapshot;


	/**
     * @param {WpwExportOptions} options
     */
	constructor(options)
	{
		super(options);
        this.buildDeps = { config: [] };
        this.snapshot = { immutablePaths: [], managedPaths: [], unmanagedPaths: [] };
        this.buildOptions = /** @type {WpwBuildOptionsPluginConfig<"cache">} */(this.buildOptions); // reset for typings
	}

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


    /**
     * Return the first file that exists on fs, checking in order of 'files' array
     * @private
     * @param {string[]} files
     * @param {string} [group]
     * @returns {string|void} `true` if a dependency was added, otherwise `false`
     */
    addBuildDep(files, group = "config")
    {
        const build = this.build,
              basePath = build.getBasePath(),
              ctxPath = build.getContextPath();
        let depPath = this._fs_.findExPathSync(files, basePath, true);
        if (depPath && !depPath.endsWith(".tmp"))
        {
            build.logger.value("      add dependency path", depPath, 3);
            if (!this.buildDeps[group]) { this.buildDeps[group] = []; }
            this.buildDeps[group].push(depPath);
            return depPath;
        }
        else if (basePath !== ctxPath)
        {
            depPath = this._fs_.findExPathSync(files, ctxPath, true);
            if (depPath && !depPath.endsWith(".tmp"))
            {
                build.logger.value("      add dependency path", depPath, 3);
                if (!this.buildDeps[group]) { this.buildDeps[group] = []; }
                this.buildDeps[group].push(depPath);
                return depPath;
            }
        }
    }


    /**
     * @private
     * @param {string} baseName
     * @param {string[]} [exts]
     * @param {string} [group]
     * @returns {string | undefined}
     */
    addBuildDepEx(baseName, exts = [ "json" ], group = "config")
    {
        const build = this.build,
              targets = this._arr_.asArray(build.target);
        for (const ext of exts)
        {   const files = [
                `${baseName}.${build.name}`, `${baseName}.${build.name}.${ext}`,
                `${baseName}.${targets.join("-")}`, `${baseName}.${targets.join("-")}.${ext}`,
                `${baseName}.${build.type}`, `${baseName}.${build.type}.${ext}`,
                `${baseName}.${build.mode}`, `${baseName}.${build.mode}.${ext}`
            ];
            if (build.mode === "production")
            {
                files.push(`${baseName}.prod.${ext}`, `${baseName}.prod.js`);
            }
            else if (build.mode === "development")
            {
                files.push(`${baseName}.dev.${ext}`, `${baseName}.devel.${ext}`);
            }
            else if (build.mode === "none")
            {
                if (build.type === "tests") {
                    files.push(`${baseName}.spec.${ext}`, `${baseName}.test.${ext}`);
                }
                else {
                    files.push(`${baseName}.none.${ext}`);
                }
            }
            files.push(`${baseName}.${ext}`, `${baseName}.js`);
            const depPath = this.addBuildDep(files, group);
            if (depPath) { return depPath; }
        }
    };


    /**
     * @private
     * @param {WpwBuild} build
     */
    addCi(build)
    {
        this.addBuildDep([
            ".spmhrc.json", `.spmhrc.${build.name}.json`,
            "azure-pipelines.yml", "azure-pipelines.yaml", ".gitlab-ci.yml", ".gitlab-ci.yaml", "Jenkinsfile"
        ]);
    }


    /**
     * @private
     * @param {WpwBuild} build
     */
    addDefaultWebpack(build)
    {
        build.logger.write("   configure webpack filesystem caching", 1);
        let defaultWebpack = this._path_.resolve(build.getBasePath(), "node_modules/webpack/lib");
        if (!this._fs_.existsSync(defaultWebpack))
        {   defaultWebpack = this._path_.resolve(nodejsModulesGlobalPath(), "@spmhome/webpack-wrap/node_modules/webpack/lib");
            if (!this._fs_.existsSync(defaultWebpack)) {
                defaultWebpack = this._path_.resolve(wpwNodeModulesPath(), "webpack/lib");
            }
        }
        if (this._fs_.existsSync(defaultWebpack)) {
            this.buildDeps.defaultWebpack = [ `${defaultWebpack}/` ];
        }
    }


    /**
     * @private
     * @param {WpwBuild} build
     */
    addNodeModules(build)
    {
        if (!this.global.isDevExec && !this.global.isDevDistExec)
        {   this.addBuildDep([
                this._path_.resolve(nodejsModulesGlobalPath(), "@spmhome/webpack-wrap/dist/webpack-wrap.js"),
                this._path_.resolve(build.getBasePath(), "node_modules/@spmhome/webpack-wrap/webpack-wrap.js")
            ]);
        }
        if (build.isTranspiled)
        {
            const nodeModulesPathWpw = wpwNodeModulesPath(),
                  nodeModulesPath = this._path_.joinPath(build.getBasePath(), "node_modules");
            this.snapshot.managedPaths.push(nodeModulesPath);
            if (!nodeModulesPathWpw.startsWith(nodeModulesPath)) {
                this.snapshot.managedPaths.push(nodeModulesPathWpw);
            }
            this.snapshot.unmanagedPaths.push(build.getSrcPath());
        }
    }


    /**
     * @override
     * @param {WpwBuild} build
     */
    app(build)
    {
        this.addBuildDep([ "app.json" ]);                    // react-native / expo / extjs
        // this.addBuildDep([ "app.config.js" ]);            // react-native / expo
        // this.addBuildDep([ "android/build.gradle" ]);     // react-native / expo
        // this.addBuildDep([ "android/settings.gradle" ]);  // react-native / expo
        this.addBuildDepEx("babel.config", [ "js", "json" ]);
        // this.addBuildDepEx(".publishrc", [ "json", "js", "yaml", "yml", "xml" ]);
        if (build.isTranspiled && !build.tsc?.configFileRtIsMock) {
            this.addBuildDep([ build.tsc.configFilePath ]);
        }
    }


    /**
     * @override
     * @param {WpwBuild} build
     */
    base(build)
    {
        this.addCi(build);
        this.addNodeModules(build);
        this.addDefaultWebpack(build);
    };

    /**
     * @override
     * @param {WpwBuild} build
     */
    baseDone(build)
    {   //
        // NodeJs Compile Cache (Node v22+ only)
        //
        this.enableNodeJsCache(build, build.virtualEntry.dirWpCache);

        this._obj_.apply(build.wpc.cache,
        /** @type {WebpackFileCacheOptions} */({
            type: "filesystem",
            buildDependencies: this.buildDeps,
            version: build.pkgJson.version,
            name: "webpack",
            cacheDirectory: build.virtualEntry.dirWpCache,
            profile: build.logger.level >= 4 || !!this.buildOptions.verbose
        }));

        this._obj_.apply(build.wpc.snapshot,
        /** @type {WebpackSnapshotOptions} */({
            module: { timestamp: true },
            resolve: { timestamp: true },
            managedPaths: this.snapshot.managedPaths,
            immutablePaths: this.snapshot.immutablePaths,
            unmanagedPaths: this.snapshot.unmanagedPaths,
            buildDependencies: { hash: build.isTranspiled, timestamp: true },
            resolveBuildDependencies: { hash: build.isTranspiled, timestamp: true }
        }));

        if (!build.logger.isDisabled)
        {
            const l = build.logger;
            l.write(`   added ${this.buildDeps.length} tracked build dependencies`, 1);
            if (l.level >= 3)
            {   Object.entries(this.buildDeps).forEach((c) => {
                    l.write(`   ${c[0]} dependency group:`, 3);
                    c[1].forEach((p) => { l.write("      " + p, 3); });
                });
            }
            if (this.snapshot.immutablePaths.length > 0)
            {   l.write(`      added ${this.snapshot.immutablePaths.length } immutable paths`, 1);
                if (l.level >= 3) {
                    this.snapshot.immutablePaths.forEach((p) => { l.write("         " + p, 3); });
                }
            }
            if (this.snapshot.managedPaths.length > 0)
            {   l.write(`      added ${this.snapshot.managedPaths.length } managed paths`, 1);
                if (l.level >= 3) {
                    this.snapshot.managedPaths.forEach((p) => { l.write("         " + p, 3); });
                }
            }
            if (this.snapshot.unmanagedPaths.length > 0)
            {   l.write(`      added ${this.snapshot.unmanagedPaths.length } unmanaged paths`, 1);
                if (l.level >= 3) {
                    this.snapshot.unmanagedPaths.forEach((p) => { l.write("         " + p, 2); });
                }
            }
        }
    }


    /**
     * @override
     * @param {WpwBuild} build
     */
	doxygen(build) { this.snapshot.unmanagedPaths.push(build.getSrcPath()); }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {string} cacheDir
     */
    enableNodeJsCache(build, cacheDir)
    {
        if (build.options.cache.type === "filesystem" && build.options.cache.nodejs && build.tsc.versionNodeLtsMajor >= 22)
        {
            const useNodeCache = !!build.options.cache.type && isWpwExportConfigCacheType(build.options.cache.type) &&
                                [ "webpack_fs-nodejs", "nodejs", "webpack_mem-nodejs" ].includes(build.options.cache.type);
            if (useNodeCache && this._types_.isFunction(enableNodeCache) )
            {
                const nodeCache = this._path_.joinPath(cacheDir, "node");
                build.logger.write(`   enable node v${build.tsc.versionNodeRuntime} compile cache`, 1);
                // process.env.NODE_COMPILE_CACHE ||= nodeCache
                enableNodeCache(nodeCache);
            }
        }
    }


    /**
     * @override
     * @param {WpwBuild} build
     */
	extjsdoc(build) { this.snapshot.unmanagedPaths.push(build.getSrcPath())  }


    /**
     * @override
     * @param {WpwBuild} build
     */
	jsdoc(build)
    {
        this.addBuildDep([ "jsdoc.json", ".jsdoc.json" ]);
        this.snapshot.unmanagedPaths.push(build.getSrcPath());
    }


    /**
     * @override
     * @param {WpwBuild} build
     */
    lib(build) { this.app(build); }


    /**
     * @override
     * @param {WpwBuild} _build
     */
    plugin(_build) {}


    /**
     * @override
     * @param {WpwBuild} build
     */
    resource(build)
    {
        const ctxPath = build.getContextPath();
        this._arr_.asArray(build.options.resource.input).filter(this._fs_.existsSync).forEach((p) =>
        {
            this.snapshot.unmanagedPaths.push(this._path_.resolvePath(ctxPath, p));
        });
    }


    /**
     * @override
     * @param {WpwBuild} build
     */
	schema(build)
    {
        this.task(build, build.options.schema.scripts);
        this.addBuildDep(getScriptBuildDependencies(build, build.options.schema.scripts));
    }


    /**
     * @override
     * @param {WpwBuild} build
     */
    script(build)
    {
        this.task(build, build.options.script);
        this.addBuildDep(getScriptBuildDependencies(build, build.options.script));
    }


    /**
     * @private
     * @param {WpwBuild} build
     * @param {WpwPluginConfigScript | WpwPluginConfigRunScripts} cfg
     */
    task(build, cfg)
    {
        const ctxPath = build.getContextPath();
        this._arr_.asArray(cfg.items).forEach((s) =>
        {   this.snapshot.unmanagedPaths.push(
                ...this._arr_.asArray(s.paths?.input).map((p) => this._path_.resolvePath(ctxPath, p)).filter(this._fs_.existsSync)
            );
        });
    }


    /**
     * @override
     * @param {WpwBuild} build
     */
    tests(build) { this.snapshot.unmanagedPaths.push(build.getSrcPath()); }


    /**
     * @override
     * @param {WpwBuild} build
     */
    types(build) { this.snapshot.unmanagedPaths.push(build.getSrcPath()); }


    /**
     * @override
     * @param {WpwBuild} build
     */
    webapp(build) { this.app(build); }
}


module.exports = WpwCacheExport.create;