Files
beall-11ty/node_modules/@11ty/eleventy-fetch/src/FileCache.js

238 lines
5.4 KiB
JavaScript
Raw Normal View History

2026-03-31 16:38:22 -07:00
const fs = require("node:fs");
const path = require("node:path");
const debugUtil = require("debug");
const { parse } = require("flatted");
const debug = debugUtil("Eleventy:Fetch");
const debugAssets = debugUtil("Eleventy:Assets");
const DirectoryManager = require("./DirectoryManager.js");
const ExistsCache = require("./ExistsCache.js");
let existsCache = new ExistsCache();
class FileCache {
#source;
#directoryManager;
#metadata;
#defaultType;
#contents;
#dryRun = false;
#cacheDirectory = ".cache";
#savePending = false;
#counts = {
read: 0,
write: 0,
};
constructor(cacheFilename, options = {}) {
this.cacheFilename = cacheFilename;
if(options.dir) {
this.#cacheDirectory = options.dir;
}
if(options.source) {
this.#source = options.source;
}
}
setDefaultType(type) {
if(type) {
this.#defaultType = type;
}
}
setDryRun(val) {
this.#dryRun = Boolean(val);
}
setDirectoryManager(manager) {
this.#directoryManager = manager;
}
ensureDir() {
if (this.#dryRun || existsCache.exists(this.#cacheDirectory)) {
return;
}
if(!this.#directoryManager) {
// standalone fallback (for tests)
this.#directoryManager = new DirectoryManager();
}
this.#directoryManager.create(this.#cacheDirectory);
}
set(type, contents, extraMetadata = {}) {
this.#savePending = true;
this.#metadata = {
cachedAt: Date.now(),
type,
// source: this.#source,
metadata: extraMetadata,
};
this.#contents = contents;
}
get fsPath() {
return path.join(this.#cacheDirectory, this.cacheFilename);
}
getContentsPath(type) {
if(!type) {
throw new Error("Missing cache type for " + this.fsPath);
}
// normalize to storage type
if(type === "xml") {
type = "text";
} else if(type === "parsed-xml") {
type = "json";
}
return `${this.fsPath}.${type}`;
}
// only when side loaded (buffer content)
get contentsPath() {
return this.getContentsPath(this.#metadata?.type);
}
get() {
if(this.#metadata) {
return this.#metadata;
}
if(!existsCache.exists(this.fsPath)) {
return;
}
debug(`Fetching from cache ${this.fsPath}`);
if(this.#source) {
debugAssets("[11ty/eleventy-fetch] Reading via %o", this.#source);
} else {
debugAssets("[11ty/eleventy-fetch] Reading %o", this.fsPath);
}
this.#counts.read++;
let data = fs.readFileSync(this.fsPath, "utf8");
let json;
// Backwards compatibility with previous caches usingn flat-cache and `flatted`
if(data.startsWith(`[["1"],`)) {
let flattedParsed = parse(data);
if(flattedParsed?.[0]?.value) {
json = flattedParsed?.[0]?.value
}
} else {
json = JSON.parse(data);
}
this.#metadata = json;
return json;
}
_backwardsCompatGetContents(rawData, type) {
if (type === "json") {
return rawData.contents;
} else if (type === "text") {
return rawData.contents.toString();
}
// buffer
return Buffer.from(rawData.contents);
}
hasContents(type) {
if(this.#contents) {
return true;
}
if(this.get()?.contents) { // backwards compat with very old caches
return true;
}
return existsCache.exists(this.getContentsPath(type));
}
getType() {
return this.#metadata?.type || this.#defaultType;
}
getContents() {
if(this.#contents) {
return this.#contents;
}
let metadata = this.get();
// backwards compat with old caches
if(metadata?.contents) {
// already parsed, part of the top level file
let normalizedContent = this._backwardsCompatGetContents(this.get(), this.getType());
this.#contents = normalizedContent;
return normalizedContent;
}
if(!existsCache.exists(this.contentsPath)) {
return;
}
debug(`Fetching from cache ${this.contentsPath}`);
if(this.#source) {
debugAssets("[11ty/eleventy-fetch] Reading (side loaded) via %o", this.#source);
} else {
debugAssets("[11ty/eleventy-fetch] Reading (side loaded) %o", this.contentsPath);
}
// It is intentional to store contents in a separate file from the metadata: we dont want to
// have to read the entire contents via JSON.parse (or otherwise) to check the cache validity.
this.#counts.read++;
let type = metadata?.type || this.getType();
let data = fs.readFileSync(this.contentsPath);
if (type === "json" || type === "parsed-xml") {
data = JSON.parse(data);
}
this.#contents = data;
return data;
}
save() {
if(this.#dryRun || !this.#savePending || this.#metadata && Object.keys(this.#metadata) === 0) {
return;
}
this.ensureDir(); // doesnt add to counts (yet?)
// contents before metadata
debugAssets("[11ty/eleventy-fetch] Writing %o (side loaded) from %o", this.contentsPath, this.#source);
this.#counts.write++;
// the contents must exist before the cache metadata are saved below
let contents = this.#contents;
let type = this.getType();
if (type === "json" || type === "parsed-xml") {
contents = JSON.stringify(contents);
}
fs.writeFileSync(this.contentsPath, contents);
debug(`Writing ${this.contentsPath}`);
this.#counts.write++;
debugAssets("[11ty/eleventy-fetch] Writing %o from %o", this.fsPath, this.#source);
fs.writeFileSync(this.fsPath, JSON.stringify(this.#metadata), "utf8");
debug(`Writing ${this.fsPath}`);
}
// for testing
getAllPossibleFilePaths() {
let types = ["text", "buffer", "json"];
let paths = new Set();
paths.add(this.fsPath);
for(let type of types) {
paths.add(this.getContentsPath(type));
}
return Array.from(paths);
}
}
module.exports = FileCache;