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