/**
* @file services/snapshot.js
* @copyright @spmhome @_2025
* @author Scott Meesseman @spmeesseman
*//** */
const WpwService = require("./base");
const { isFunction, dateAddDays, isString, isArray, applyIf } = require("@spmhome/type-utils");
const {
awaitAlways, existsSync, findFiles, isAbsPath, isDirectory, readFileBufAsync, resolvePath, relativePath,
readFileAsync
} = require("@spmhome/cmn-utils");
class WpwSnapshotService extends WpwService
{
static dependenciesLogged = false;
// /**
// * @protected
// * @type {WebpackCacheFacade}
// */
// wpCache;
/**
* @private
* @type {WebpackCacheFacade}
*/
wpCompilationCache;
// /**
// * @private
// * @type {WebpackCacheFacade}
// */
// wpCacheCompiler;
/**
* @private
* @type {WebpackFileSystemInfo}
*/
wpFsInfo;
/**
* @param {WpwServiceOwnerOptions} options
*/
constructor(options)
{
super(applyIf({ slug: "snapshot" }, options));
}
/**
* @param {string | string[]} fsPathAbs
* @param {boolean} [isDir]
* @param {string} [lPad]
* @returns {boolean}
* @throws {Error}
*/
checkRuntimeRequirements = (fsPathAbs, isDir, lPad = " ") =>
{
this.compiler ||= this.build.compiler;
this.compilation ||= this.build.compilation;
if (!this.compiler || !this.compilation) {
throw new Error(
"snapshot cache not fully initialized",
{ cause: this.MsgCode.WARNING_OPTIONS_INVALID}
);
}
// else if (isDir)
// {
// this.build.addMessage({
// code: this.MsgCode.WARNING_OPTIONS_INVALID,
// message: "directory snapshot not implemented, files only are supported"
// });
// return false;
// }
else if (!fsPathAbs || (isString(fsPathAbs) && !existsSync(fsPathAbs)))
{
throw new Error(
`asset-${!isDir ? "file" : "directory"} path '${fsPathAbs}' does not exist`,
{ cause: this.MsgCode.ERROR_RESOURCE_MISSING}
);
}
this.updateRuntimeInstances(this.compiler, this.compilation, lPad);
return true;
};
/**
* @param {string | string[]} fsPath
* @param {WebpackRawSource | null} [source]
* @param {string} [lPad]
* @param {WpwContentTransformFn} [transform]
* @returns {Promise<CacheResultNoSnapshot>}
*/
checkSnapshot = async (fsPath, source, lPad, transform) =>
{
const now = Date.now(),
l = this.build.logger,
vDir = this.build.virtualEntry.dir,
isAbs = isString(fsPath) ? isAbsPath(fsPath) : false,
fsPathAbs = isAbs ? fsPath : resolvePath(vDir, isString(fsPath) ? fsPath : ""),
fsPathRel = isAbs ? relativePath(vDir, isString(fsPath) ? fsPath : ".") : isString(fsPath) ? fsPath :"",
isDir = isString(fsPathAbs) ? isDirectory(fsPathAbs) : false,
/** @type {CacheResult} */
result = { file: fsPathRel, snapshot: null, source: null, up2date: false };
l.write(`check for cached snapshot @ path '${fsPathRel}'`, 1, lPad);
l.value(" asset absolute path", fsPathAbs, 2, lPad);
try{
this.checkRuntimeRequirements(fsPathAbs, isDir, lPad + " ");
}
catch (e) {
throw new Error(e.message);
}
let hash, cachedEntry;
try {
cachedEntry = await this.getEntry(fsPathRel, null, lPad + " ");
}
catch (e) {
throw new Error(e.message);
}
if (cachedEntry && cachedEntry.snapshot)
{
l.write(" snapshot acquired from cache, perform validation", 2, lPad);
try {
result.up2date = await this.isValidSnapshot(cachedEntry.snapshot, lPad + " ");
}
catch (e) {
throw new Error(e.message);
}
if (result.up2date) {
l.write(" cached snapshot validated", 1, lPad);
if (!isDir) { result.source = cachedEntry.source; }
}
else { l.write(" cached snapshot was invalidated, refresh", 1, lPad); }
}
else {
l.write(" no cached snapshot exists for this asset", 1, lPad);
}
if (isDir || !result.up2date)
{ try
{ if (isArray(fsPath))
{
result.snapshot = await this.createSnapshot(now, fsPath);
hash = this.getContentHash(Buffer.from(fsPath.join("|")));
}
else if (!isDir && isString(fsPathAbs))
{ l.write(" read file-asset source content", 3, lPad);
let data = await readFileBufAsync(fsPathAbs);
if (isFunction(transform)) {
data = await awaitAlways(transform(data));
}
l.write(" create new file-asset snapshot", 1, lPad);
result.source = new this.build.wp.sources.RawSource(data);
result.snapshot = await this.createSnapshot(now, [ fsPath ]);
hash = this.getContentHash(data);
}
else
{ l.write(" create new directory-asset snapshot", 1, lPad);
const files = await findFiles("**/*", { cwd: fsPath, nodir: true });
let data = "";
for (const f of files) {
data += (await readFileAsync(f));
}
let buf = Buffer.from(data, "utf8");
if (isFunction(transform)) {
buf = await awaitAlways(transform(buf));
}
result.snapshot = await this.createSnapshot(now, files, [ fsPath ]);
hash = this.getContentHash(buf);
}
}
catch (e) {
throw new Error(e.message);
}
try
{ result.snapshot.setFileTimestamps(new Map([
[ "timestamp", { timestamp: now, safeTime: dateAddDays(now, 1).getTime() }]
]));
result.snapshot.setFileHashes(new Map([[ "hash", hash ]]));
}
catch (e) {
throw new Error(e.message);
}
try
{ l.value(" persist snapshot to store", fsPathRel, 2, lPad);
await this.wpCompilationCache.storePromise(
this.getIdentifier(fsPath), null, { source, snapshot: result.snapshot, hash }
);
}
catch (e) {
throw new Error(e.message);
}
l.write(`completed cached snapshot check for asset '${fsPathRel}`, 1, lPad);
return result;
}
};
/*
async getSnapshot()
{
const newAssetJson = JSON.stringify(assets);
if (isCompilationCached && options.cache && assetJson === newAssetJson)
{
previousEmittedAssets.forEach(({ name, html }) => {
compilation.emitAsset(name, new webpack.sources.RawSource(html, false));
});
return;
}
else {
previousEmittedAssets = [];
assetJson = newAssetJson;
}
const filename = options.filename.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
(match, options) => `[contenthash${options}]`,
'[templatehash] is now [contenthash]')
);
const replacedFilename = replacePlaceholdersInFilename(filename, html, compilation);
// Add the evaluated html code to the webpack assets
compilation.emitAsset(replacedFilename.path, new webpack.sources.RawSource(html, false), replacedFilename.info);
previousEmittedAssets.push({ name: replacedFilename.path, html });
return replacedFilename.path;
logger.write(` finished execution of jsdoc build @ italic(${relative(this.build.paths.base, outDir)})`, 3);
}
*/
// logger.write(" check compilation cache for snapshot", 4);
// try {
// persistedCache = this.cache.data....
// cacheEntry = await this.wpCacheCompilation.getPromise(`${resourcePath}|${identifier}`, null);
// }
// catch (e) {
// throw new WpwError("jsdoc loader failed: " + e.message, "loaders/jsdoc.js");
// }
// if (cacheEntry)
// {
// let isValidSnapshot;
// logger.write(" check snapshot valid", 4);
// try {
// isValidSnapshot = await this.checkSnapshotValid(cacheEntry.snapshot);
// }
// catch (e) {
// throw new WpwError("jsdoc loader failed" + e.message, "loaders/jsdoc.js", "checking snapshot");
// }
// if (isValidSnapshot)
// {
// logger.write(" snapshot is valid", 4);
// ({ hash, source } = cacheEntry);
// newHash = newHash || this.getContentHash(source);
// if (newHash === hash)
// {
// logger.write(" asset is unchanged since last snapshot", 4);
// }
// else {
// logger.write(" asset has changed since last snapshot", 4);
// }
// }
// else {
// logger.write(" snapshot is invalid", 4);
// }
// }
// if (!source)
// {
// let snapshot;
// const startTime = Date.now();
// data = data || await readFile(resourcePath);
// source = new this.build.wp.sources.RawSource(data);
// logger.write(" create snapshot", 4);
// try {
// snapshot = await this.createSnapshot(startTime, resourcePath);
// }
// catch (e) {
// throw new WpwError("jsdoc loader failed" + e.message,
// "loaders/jsdoc.js", "creating snapshot " + resourcePathRel);
// }
// if (snapshot)
// {
// logger.write(" cache snapshot", 4);
// try {
// newHash = newHash || this.getContentHash(source.buffer());
// snapshot.setFileHashes(hash);
// await this.wpCacheCompilation.storePromise(`${resourcePath}|${identifier}`,
// null, { source, snapshot, hash });
// cacheEntry = await this.wpCacheCompilation.getPromise(`${resourcePath}|${identifier}`, null);
// }
// catch (e) {
// throw new WpwError(
// "jsdoc loader failed" + e.message, "loaders/jsdoc.js", "caching snapshot " + resourcePathRe
// );
// }
// }
// }
// newHash = newHash || this.getContentHash(data);
// if (newHash === persistedCache[resourcePathRel])
// {
// logger.write(" asset is unchanged", 4);
// }
// else {
// logger.write(" asset has changed, update hash in persistent cache", 4);
// persistedCache[resourcePathRel] = newHash;
// this.cache.set(persistedCache);
// }
// const info = {
// // contenthash: newHash,
// immutable: true, // newHash === persistedCache[filePathRel],
// javascriptModule: false,
// jsdoc: true
// };
// this.compilation.buildDependencies.add(filePathRel);
// this.compilation.buildDependencies.add(resourcePath);
// this.compilation.compilationDependencies.add();
// this.compilation.contextDependencies.add();
// const cache = this.compiler.getCache(`${this.build.name}_${this.build.type}_${this.build.wpc.target}`.toLowerCase());
// this.compilation.emitAsset(resourcePathRel, source, info);
// this.compilation.additionalChunkAssets.push(filePathRel);
// const existingAsset = this.compilation.getAsset(resourcePathRel);
// if (existingAsset)
// {
// logger.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
// logger.write("!!!!!!!!!!!*********TEST********** update jsdoc asset !!!!!!!!!!!!!!!!!!!!!!!!!");
// logger.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
// }
// if (!existingAsset)
// {
// logger.write(" emit jsdoc asset", 3);
// this.compilation.emitAsset(resourcePathRel, source, info);
// const fileName = loaderUtils.interpolateName(this, "[name]-[contenthash].[ext]", {content: source});
// this.emitFile(fileName, resourcePath);
// this.addDependency(resourcePath);
// }
// else if (this.options.force)
// {
// logger.write(" update asset", 3);
// this.compilation.updateAsset(resourcePathRel, source, info);
// }
// else {
// logger.write(" asset compared for emit", 3);
// this.compilation.buildDependencies.add(resourcePathRel);
// this.compilation.comparedForEmitAssets.add(resourcePathRel);
// this.compilation.compilationDependencies.add(resourcePathRel);
// }
/**
* @private
* @param {WebpackSnapshot} snapshot
* @param {string} [lPad]
* @returns {Promise<boolean | undefined>} Promise<boolean | undefined>
*/
isValidSnapshot = (snapshot, lPad = " ") =>
{
return new Promise((resolve, reject) =>
{
const l = this.build.logger;
l.write("check snapshot validity", 1, lPad);
this.wpFsInfo.checkSnapshotValid(snapshot, (e, isValid) =>
{ if (e) {
l.error("snapshot validity check resulted in an error", lPad);
reject(e);
}
else { resolve(isValid); }
});
});
};
/**
* @private
* @param {number} now
* @param {string[] | null} files
* @param {string[] | null} dirs
* @param {string[] | null} missing
* @param {WebpackSnapshotOptionsFileSystemInfo | null} [options]
* @returns {Promise<WebpackSnapshot | undefined | null>} Promise<WebpackSnapshot | undefined | null>
*/
createSnapshot(now, files, dirs = null, missing = null, options = {})
{
return new Promise((ok, fail) =>
{
this.wpFsInfo.createSnapshot(
now || Date.now(), files, dirs, missing,
{ ...{ hash: true, timestamp: true }, ...options },
(e, ss) => e ? fail(e) : ok(ss)
);
});
};
/**
* @private
* @param {string} fsPath
* @param {WebpackEtag | null} [etag]
* @param {string} [lPad]
* @returns {Promise<CacheResult | undefined>}
*/
async getEntry(fsPath, etag, lPad = " ")
{
const l = this.logger,
isAbs = isAbsPath(fsPath),
fsPathAbs = !isAbs ? resolvePath(this.compiler.outputPath, fsPath) : fsPath,
fsPathRel = isAbs ? relativePath(this.compiler.outputPath, fsPath) : fsPath;
try {
const key = this.getIdentifier(fsPathRel);
l.value("query store for cached snapshot", fsPathRel, 1, lPad);
l.value(" key", key, 2, lPad);
l.value(" etag", etag, 2, lPad);
l.value(" relative path", fsPathRel, 2, lPad);
l.value(" absolute path", fsPathAbs, 3, lPad);
const result = /** @type {CacheResult} */(
await this.wpCompilationCache.getPromise(key, etag || null)
);
// l.write("found cached snapshot entry", 2, lPad);
// const ts = epochToMSMS(result.snapshot.fileTimestamps.get("timestamp").timestamp),
// tsSafe = epochToMSMS(result.snapshot.fileTimestamps.get("timestamp").safeTime);
// l.value(" safe time", tsSafe, 2, lPad);
// l.value(" timestamp", ts, 2, lPad);
return result;
}
catch (e) {
throw new Error(e.message);
}
}
/**
* @param {Buffer} source
* @returns {string} string
*/
getContentHash(source)
{
const {hashDigest, hashDigestLength, hashFunction, hashSalt } = this.compilation.outputOptions,
hash = this.build.wp.util.createHash(/** @type {string} */(hashFunction));
if (hashSalt) { hash.update(hashSalt); }
return hash.update(source).digest(hashDigest).toString().slice(0, hashDigestLength);
}
/**
* @private
* @param {string | string[]} fsPath
* @returns {string} string
*/
getIdentifier(fsPath) { return `${this._arr_.asArray(fsPath).join("|").replace(/[/\\:.\W]/g, "")}_${this.build.name}`; }
/**
* @private
* @param {boolean} [force]
* @param {string} [lPad]
*/
printDependencies(force, lPad = " ")
{
const c = this.compilation;
if (force || !WpwSnapshotService.dependenciesLogged &&
(c.buildDependencies.size || c.contextDependencies.size || c.fileDependencies.size || c.missingDependencies.size))
{
const l = this.build.logger;
l.value("# of build dependencies", c.buildDependencies.size, 1, lPad);
if (c.buildDependencies.size > 0 && l.level >= 4) {
l.write(" build dependencies:", 4, lPad);
c.buildDependencies.forEach((d) => l.write(d, 4, lPad + " "), this);
}
l.value("# of context dependencies", c.contextDependencies.size, 1, lPad);
if (c.contextDependencies.size > 0 && l.level >= 4) {
l.write("context dependencies:", 4, lPad);
c.contextDependencies.forEach((d) => l.write(d, 4, lPad + " "), this);
}
l.value("# of file dependencies", c.fileDependencies.size, 1, lPad);
if (c.fileDependencies.size > 0 && l.level >= 4) {
l.write("file dependencies:", 4, lPad);
c.fileDependencies.forEach((d) => l.write(d, 4, lPad + " "), this);
}
l.value("# of missing dependencies", c.missingDependencies.size, 1, lPad);
if (c.missingDependencies.size > 0 && l.level >= 4) {
l.write("missing dependencies:", 4, lPad);
c.missingDependencies.forEach((d) => l.write(d, 4, lPad + " "), this);
}
WpwSnapshotService.dependenciesLogged = true;
}
}
/**
* @param {WebpackCompiler} compiler
* @param {WebpackCompilation} compilation
* @param {string} [lPad]
* @throws {Error}
*/
updateRuntimeInstances(compiler, compilation, lPad = " ")
{
this.compiler = compiler;
this.compilation = compilation;
const b = this.build, errs = b.errorCount > 0;
if (!errs && b.wpc.cache.type === "filesystem" && !this.wpCompilationCache && this.compiler && this.compilation)
{
b.logger.value("finish initialization for webpack snapshot cache", b.wpc.cache.cacheDirectory, 1, lPad);
b.logger.write(" get compilation cache facade", 2, lPad);
this.wpFsInfo = this.compilation.fileSystemInfo;
this.wpCompilationCache = this.compilation.getCache(b.wpc.cache.name);
this.printDependencies(false, lPad + " ");
b.logger.write("webpack cache initialized", 2, lPad);
}
// this.printDependencies(false, lPad + " ");
}
}
module.exports = WpwSnapshotService;