astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
321 lines (320 loc) • 9.77 kB
JavaScript
import { promises as fs, existsSync } from "node:fs";
import * as devalue from "devalue";
import { Traverse } from "neotraverse/modern";
import { imageSrcToImportId, importIdToSymbolName } from "../assets/utils/resolveImports.js";
import { AstroError, AstroErrorData } from "../core/errors/index.js";
import { IMAGE_IMPORT_PREFIX } from "./consts.js";
import { ImmutableDataStore } from "./data-store.js";
import { contentModuleToId } from "./utils.js";
const SAVE_DEBOUNCE_MS = 500;
const MAX_DEPTH = 10;
class MutableDataStore extends ImmutableDataStore {
#file;
#assetsFile;
#modulesFile;
#saveTimeout;
#assetsSaveTimeout;
#modulesSaveTimeout;
#dirty = false;
#assetsDirty = false;
#modulesDirty = false;
#assetImports = /* @__PURE__ */ new Set();
#moduleImports = /* @__PURE__ */ new Map();
set(collectionName, key, value) {
const collection = this._collections.get(collectionName) ?? /* @__PURE__ */ new Map();
collection.set(String(key), value);
this._collections.set(collectionName, collection);
this.#saveToDiskDebounced();
}
delete(collectionName, key) {
const collection = this._collections.get(collectionName);
if (collection) {
collection.delete(String(key));
this.#saveToDiskDebounced();
}
}
clear(collectionName) {
this._collections.delete(collectionName);
this.#saveToDiskDebounced();
}
clearAll() {
this._collections.clear();
this.#saveToDiskDebounced();
}
addAssetImport(assetImport, filePath) {
const id = imageSrcToImportId(assetImport, filePath);
if (id) {
this.#assetImports.add(id);
this.#writeAssetsImportsDebounced();
}
}
addAssetImports(assets, filePath) {
assets.forEach((asset) => this.addAssetImport(asset, filePath));
}
addModuleImport(fileName) {
const id = contentModuleToId(fileName);
if (id) {
this.#moduleImports.set(fileName, id);
this.#writeModulesImportsDebounced();
}
}
async writeAssetImports(filePath) {
this.#assetsFile = filePath;
if (this.#assetImports.size === 0) {
try {
await this.#writeFileAtomic(filePath, "export default new Map();");
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
}
if (!this.#assetsDirty && existsSync(filePath)) {
return;
}
const imports = [];
const exports = [];
this.#assetImports.forEach((id) => {
const symbol = importIdToSymbolName(id);
imports.push(`import ${symbol} from ${JSON.stringify(id)};`);
exports.push(`[${JSON.stringify(id)}, ${symbol}]`);
});
const code = (
/* js */
`
${imports.join("\n")}
export default new Map([${exports.join(", ")}]);
`
);
try {
await this.#writeFileAtomic(filePath, code);
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
this.#assetsDirty = false;
}
async writeModuleImports(filePath) {
this.#modulesFile = filePath;
if (this.#moduleImports.size === 0) {
try {
await this.#writeFileAtomic(filePath, "export default new Map();");
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
}
if (!this.#modulesDirty && existsSync(filePath)) {
return;
}
const lines = [];
for (const [fileName, specifier] of this.#moduleImports) {
lines.push(`[${JSON.stringify(fileName)}, () => import(${JSON.stringify(specifier)})]`);
}
const code = `
export default new Map([
${lines.join(",\n")}]);
`;
try {
await this.#writeFileAtomic(filePath, code);
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
this.#modulesDirty = false;
}
#writeAssetsImportsDebounced() {
this.#assetsDirty = true;
if (this.#assetsFile) {
if (this.#assetsSaveTimeout) {
clearTimeout(this.#assetsSaveTimeout);
}
this.#assetsSaveTimeout = setTimeout(() => {
this.#assetsSaveTimeout = void 0;
this.writeAssetImports(this.#assetsFile);
}, SAVE_DEBOUNCE_MS);
}
}
#writeModulesImportsDebounced() {
this.#modulesDirty = true;
if (this.#modulesFile) {
if (this.#modulesSaveTimeout) {
clearTimeout(this.#modulesSaveTimeout);
}
this.#modulesSaveTimeout = setTimeout(() => {
this.#modulesSaveTimeout = void 0;
this.writeModuleImports(this.#modulesFile);
}, SAVE_DEBOUNCE_MS);
}
}
#saveToDiskDebounced() {
this.#dirty = true;
if (this.#file) {
if (this.#saveTimeout) {
clearTimeout(this.#saveTimeout);
}
this.#saveTimeout = setTimeout(() => {
this.#saveTimeout = void 0;
this.writeToDisk(this.#file);
}, SAVE_DEBOUNCE_MS);
}
}
#writing = /* @__PURE__ */ new Set();
#pending = /* @__PURE__ */ new Set();
async #writeFileAtomic(filePath, data, depth = 0) {
if (depth > MAX_DEPTH) {
return;
}
const fileKey = filePath.toString();
if (this.#writing.has(fileKey)) {
this.#pending.add(fileKey);
return;
}
this.#writing.add(fileKey);
const tempFile = filePath instanceof URL ? new URL(`${filePath.href}.tmp`) : `${filePath}.tmp`;
try {
await fs.writeFile(tempFile, data);
await fs.rename(tempFile, filePath);
} finally {
this.#writing.delete(fileKey);
if (this.#pending.has(fileKey)) {
this.#pending.delete(fileKey);
await this.#writeFileAtomic(filePath, data, depth + 1);
}
}
}
scopedStore(collectionName) {
return {
get: (key) => this.get(collectionName, key),
entries: () => this.entries(collectionName),
values: () => this.values(collectionName),
keys: () => this.keys(collectionName),
set: ({
id: key,
data,
body,
filePath,
deferredRender,
digest,
rendered,
assetImports,
legacyId
}) => {
if (!key) {
throw new Error(`ID must be a non-empty string`);
}
const id = String(key);
if (digest) {
const existing = this.get(collectionName, id);
if (existing && existing.digest === digest) {
return false;
}
}
const foundAssets = new Set(assetImports);
new Traverse(data).forEach((_, val) => {
if (typeof val === "string" && val.startsWith(IMAGE_IMPORT_PREFIX)) {
const src = val.replace(IMAGE_IMPORT_PREFIX, "");
foundAssets.add(src);
}
});
const entry = {
id,
data
};
if (body) {
entry.body = body;
}
if (filePath) {
if (filePath.startsWith("/")) {
throw new Error(`File path must be relative to the site root. Got: ${filePath}`);
}
entry.filePath = filePath;
}
if (foundAssets.size) {
entry.assetImports = Array.from(foundAssets);
this.addAssetImports(entry.assetImports, filePath);
}
if (digest) {
entry.digest = digest;
}
if (rendered) {
entry.rendered = rendered;
}
if (legacyId) {
entry.legacyId = legacyId;
}
if (deferredRender) {
entry.deferredRender = deferredRender;
if (filePath) {
this.addModuleImport(filePath);
}
}
this.set(collectionName, id, entry);
return true;
},
delete: (key) => this.delete(collectionName, key),
clear: () => this.clear(collectionName),
has: (key) => this.has(collectionName, key),
addAssetImport: (assetImport, fileName) => this.addAssetImport(assetImport, fileName),
addAssetImports: (assets, fileName) => this.addAssetImports(assets, fileName),
addModuleImport: (fileName) => this.addModuleImport(fileName)
};
}
/**
* Returns a MetaStore for a given collection, or if no collection is provided, the default meta collection.
*/
metaStore(collectionName = ":meta") {
const collectionKey = `meta:${collectionName}`;
return {
get: (key) => this.get(collectionKey, key),
set: (key, data) => this.set(collectionKey, key, data),
delete: (key) => this.delete(collectionKey, key),
has: (key) => this.has(collectionKey, key)
};
}
toString() {
return devalue.stringify(this._collections);
}
async writeToDisk(filePath) {
if (!this.#dirty) {
return;
}
try {
await this.#writeFileAtomic(filePath, this.toString());
this.#file = filePath;
this.#dirty = false;
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
}
/**
* Attempts to load a MutableDataStore from the virtual module.
* This only works in Vite.
*/
static async fromModule() {
try {
const data = await import("astro:data-layer-content");
const map = devalue.unflatten(data.default);
return MutableDataStore.fromMap(map);
} catch {
}
return new MutableDataStore();
}
static async fromMap(data) {
const store = new MutableDataStore();
store._collections = data;
return store;
}
static async fromString(data) {
const map = devalue.parse(data);
return MutableDataStore.fromMap(map);
}
static async fromFile(filePath) {
try {
if (existsSync(filePath)) {
const data = await fs.readFile(filePath, "utf-8");
return MutableDataStore.fromString(data);
}
} catch {
}
return new MutableDataStore();
}
}
export {
MutableDataStore
};