dejarun
Version:
A file system operation cache for Node.js, using a hash of the file or folder contents as the cache key.
106 lines (94 loc) • 3.24 kB
text/typescript
import chalk from "chalk";
import { defaults, hashElement } from "folder-hash";
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import logger from "./logger.ts";
import { each } from "./utils/tagged-templates.ts";
function hashValue(value: any) {
return createHash(defaults.algo ?? "sha1")
.update(JSON.stringify(value))
.digest(defaults.encoding ?? "base64");
}
const hashFileOrDirList = (paths: string[] = []) =>
Promise.all(
paths.map(async (path) => {
try {
return (await hashElement(path)).hash;
} catch {
return null;
}
}),
);
/**
* Caches the result of a callback function based on the hashes of input and output files or directories and additional dependencies.
* If the hashes of the inputs, outputs and other dependencies have not changed since the last run, the callback will not be executed.
*
* @param name - A unique name for the cache entry. Example: "build".
* @param callback - The function to execute if the cache is missed.
* @param options - An object containing the following optional properties:
* - inputs: An array of file or directory paths to hash for cache validation.
* - outputs: An array of file or directory paths to hash for cache validation.
* - dependencies: An array of arbitrary values to hash for cache validation.
* - clean: A boolean indicating whether to clean the cache (default: false).
* - debug: A boolean indicating whether to log debug information (default: false).
* - logo: A string or false to customize the logo in the logs (default: '[déjàrun]').
*/
export default async function dejarun(
name: string,
callback: () => Promise<any>,
options?: {
inputs?: string[];
outputs?: string[];
dependencies?: any[];
clean?: boolean;
debug?: boolean;
logo?: string | false;
},
) {
const cacheName = `${process.env.npm_package_name}/${name}`;
const CACHE_FILE = new URL("../.cache.json", import.meta.url).pathname;
let cache: Record<string, string> = {};
try {
cache = JSON.parse(await fs.readFile(CACHE_FILE, "utf8"));
} catch {}
const inputs = await hashFileOrDirList(options?.inputs);
const totalHash = hashValue({
inputs,
outputs: await hashFileOrDirList(options?.outputs),
dependencies: options?.dependencies,
});
if (options?.debug) {
logger(
"debug",
`
name: ${chalk.bold(name)}
cache name: ${chalk.bold(cacheName)}
cache file: ${chalk.bold(CACHE_FILE)}
inputs:${chalk.bold(each`
${options.inputs}`)}
outputs:${chalk.bold(each`
${options.outputs}`)}
dependencies:${chalk.bold(each`
${options.dependencies}`)}
total hash: ${totalHash}`,
options.logo,
);
}
if (options?.clean) {
logger("clean", name, options.logo);
delete cache[cacheName];
} else {
if (totalHash === cache[cacheName]) {
logger("hit", name, options?.logo);
return;
}
logger("miss", name, options?.logo);
}
await callback();
cache[cacheName] = hashValue({
inputs,
outputs: await hashFileOrDirList(options?.outputs),
dependencies: options?.dependencies,
});
await fs.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2));
}