251 lines
8.7 KiB
JavaScript
251 lines
8.7 KiB
JavaScript
import fs, { readFileSync } from "node:fs";
|
|
import { basename } from "node:path/posix";
|
|
import { dim, green } from "kleur/colors";
|
|
import { getOutDirWithinCwd } from "../../core/build/common.js";
|
|
import { getTimeStat } from "../../core/build/util.js";
|
|
import { AstroError } from "../../core/errors/errors.js";
|
|
import { AstroErrorData } from "../../core/errors/index.js";
|
|
import { isRemotePath, removeLeadingForwardSlash } from "../../core/path.js";
|
|
import { getConfiguredImageService } from "../internal.js";
|
|
import { isESMImportedImage } from "../utils/imageKind.js";
|
|
import { loadRemoteImage, revalidateRemoteImage } from "./remote.js";
|
|
async function prepareAssetsGenerationEnv(pipeline, totalCount) {
|
|
const { config, logger, settings } = pipeline;
|
|
let useCache = true;
|
|
const assetsCacheDir = new URL("assets/", config.cacheDir);
|
|
const count = { total: totalCount, current: 1 };
|
|
try {
|
|
await fs.promises.mkdir(assetsCacheDir, { recursive: true });
|
|
} catch (err) {
|
|
logger.warn(
|
|
null,
|
|
`An error was encountered while creating the cache directory. Proceeding without caching. Error: ${err}`
|
|
);
|
|
useCache = false;
|
|
}
|
|
const isServerOutput = settings.buildOutput === "server";
|
|
let serverRoot, clientRoot;
|
|
if (isServerOutput) {
|
|
serverRoot = config.build.server;
|
|
clientRoot = config.build.client;
|
|
} else {
|
|
serverRoot = getOutDirWithinCwd(config.outDir);
|
|
clientRoot = config.outDir;
|
|
}
|
|
return {
|
|
logger,
|
|
isSSR: isServerOutput,
|
|
count,
|
|
useCache,
|
|
assetsCacheDir,
|
|
serverRoot,
|
|
clientRoot,
|
|
imageConfig: config.image,
|
|
assetsFolder: config.build.assets
|
|
};
|
|
}
|
|
function getFullImagePath(originalFilePath, env) {
|
|
return new URL(removeLeadingForwardSlash(originalFilePath), env.serverRoot);
|
|
}
|
|
async function generateImagesForPath(originalFilePath, transformsAndPath, env) {
|
|
let originalImage;
|
|
for (const [_, transform] of transformsAndPath.transforms) {
|
|
await generateImage(transform.finalPath, transform.transform);
|
|
}
|
|
if (!env.isSSR && transformsAndPath.originalSrcPath && !globalThis.astroAsset.referencedImages?.has(transformsAndPath.originalSrcPath)) {
|
|
try {
|
|
if (transformsAndPath.originalSrcPath) {
|
|
env.logger.debug(
|
|
"assets",
|
|
`Deleting ${originalFilePath} as it's not referenced outside of image processing.`
|
|
);
|
|
await fs.promises.unlink(getFullImagePath(originalFilePath, env));
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
async function generateImage(filepath, options) {
|
|
const timeStart = performance.now();
|
|
const generationData = await generateImageInternal(filepath, options);
|
|
const timeEnd = performance.now();
|
|
const timeChange = getTimeStat(timeStart, timeEnd);
|
|
const timeIncrease = `(+${timeChange})`;
|
|
const statsText = generationData.cached !== "miss" ? generationData.cached === "hit" ? `(reused cache entry)` : `(revalidated cache entry)` : `(before: ${generationData.weight.before}kB, after: ${generationData.weight.after}kB)`;
|
|
const count = `(${env.count.current}/${env.count.total})`;
|
|
env.logger.info(
|
|
null,
|
|
` ${green("\u25B6")} ${filepath} ${dim(statsText)} ${dim(timeIncrease)} ${dim(count)}`
|
|
);
|
|
env.count.current++;
|
|
}
|
|
async function generateImageInternal(filepath, options) {
|
|
const isLocalImage = isESMImportedImage(options.src);
|
|
const finalFileURL = new URL("." + filepath, env.clientRoot);
|
|
const finalFolderURL = new URL("./", finalFileURL);
|
|
await fs.promises.mkdir(finalFolderURL, { recursive: true });
|
|
const cacheFile = basename(filepath);
|
|
const cachedFileURL = new URL(cacheFile, env.assetsCacheDir);
|
|
const cacheMetaFile = cacheFile + ".json";
|
|
const cachedMetaFileURL = new URL(cacheMetaFile, env.assetsCacheDir);
|
|
try {
|
|
if (isLocalImage) {
|
|
await fs.promises.copyFile(cachedFileURL, finalFileURL, fs.constants.COPYFILE_FICLONE);
|
|
return {
|
|
cached: "hit"
|
|
};
|
|
} else {
|
|
const JSONData = JSON.parse(readFileSync(cachedMetaFileURL, "utf-8"));
|
|
if (!JSONData.expires) {
|
|
try {
|
|
await fs.promises.unlink(cachedFileURL);
|
|
} catch {
|
|
}
|
|
await fs.promises.unlink(cachedMetaFileURL);
|
|
throw new Error(
|
|
`Malformed cache entry for ${filepath}, cache will be regenerated for this file.`
|
|
);
|
|
}
|
|
if (JSONData.data) {
|
|
const { data, ...meta } = JSONData;
|
|
await Promise.all([
|
|
fs.promises.writeFile(cachedFileURL, Buffer.from(data, "base64")),
|
|
writeCacheMetaFile(cachedMetaFileURL, meta, env)
|
|
]);
|
|
}
|
|
if (JSONData.expires > Date.now()) {
|
|
await fs.promises.copyFile(cachedFileURL, finalFileURL, fs.constants.COPYFILE_FICLONE);
|
|
return {
|
|
cached: "hit"
|
|
};
|
|
}
|
|
if (JSONData.etag || JSONData.lastModified) {
|
|
try {
|
|
const revalidatedData = await revalidateRemoteImage(options.src, {
|
|
etag: JSONData.etag,
|
|
lastModified: JSONData.lastModified
|
|
});
|
|
if (revalidatedData.data.length) {
|
|
originalImage = revalidatedData;
|
|
} else {
|
|
await writeCacheMetaFile(cachedMetaFileURL, revalidatedData, env);
|
|
await fs.promises.copyFile(
|
|
cachedFileURL,
|
|
finalFileURL,
|
|
fs.constants.COPYFILE_FICLONE
|
|
);
|
|
return { cached: "revalidated" };
|
|
}
|
|
} catch (e) {
|
|
env.logger.warn(
|
|
null,
|
|
`An error was encountered while revalidating a cached remote asset. Proceeding with stale cache. ${e}`
|
|
);
|
|
await fs.promises.copyFile(cachedFileURL, finalFileURL, fs.constants.COPYFILE_FICLONE);
|
|
return { cached: "hit" };
|
|
}
|
|
}
|
|
await fs.promises.unlink(cachedFileURL);
|
|
await fs.promises.unlink(cachedMetaFileURL);
|
|
}
|
|
} catch (e) {
|
|
if (e.code !== "ENOENT") {
|
|
throw new Error(`An error was encountered while reading the cache file. Error: ${e}`);
|
|
}
|
|
}
|
|
const originalImagePath = isLocalImage ? options.src.src : options.src;
|
|
if (!originalImage) {
|
|
originalImage = await loadImage(originalFilePath, env);
|
|
}
|
|
let resultData = {
|
|
data: void 0,
|
|
expires: originalImage.expires,
|
|
etag: originalImage.etag,
|
|
lastModified: originalImage.lastModified
|
|
};
|
|
const imageService = await getConfiguredImageService();
|
|
try {
|
|
resultData.data = (await imageService.transform(
|
|
originalImage.data,
|
|
{ ...options, src: originalImagePath },
|
|
env.imageConfig
|
|
)).data;
|
|
} catch (e) {
|
|
if (AstroError.is(e)) {
|
|
throw e;
|
|
}
|
|
const error = new AstroError(
|
|
{
|
|
...AstroErrorData.CouldNotTransformImage,
|
|
message: AstroErrorData.CouldNotTransformImage.message(originalFilePath)
|
|
},
|
|
{ cause: e }
|
|
);
|
|
throw error;
|
|
}
|
|
try {
|
|
if (env.useCache) {
|
|
if (isLocalImage) {
|
|
await fs.promises.writeFile(cachedFileURL, resultData.data);
|
|
} else {
|
|
await Promise.all([
|
|
fs.promises.writeFile(cachedFileURL, resultData.data),
|
|
writeCacheMetaFile(cachedMetaFileURL, resultData, env)
|
|
]);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
env.logger.warn(
|
|
null,
|
|
`An error was encountered while creating the cache directory. Proceeding without caching. Error: ${e}`
|
|
);
|
|
} finally {
|
|
await fs.promises.writeFile(finalFileURL, resultData.data);
|
|
}
|
|
return {
|
|
cached: "miss",
|
|
weight: {
|
|
// Divide by 1024 to get size in kilobytes
|
|
before: Math.trunc(originalImage.data.byteLength / 1024),
|
|
after: Math.trunc(Buffer.from(resultData.data).byteLength / 1024)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
async function writeCacheMetaFile(cachedMetaFileURL, resultData, env) {
|
|
try {
|
|
return await fs.promises.writeFile(
|
|
cachedMetaFileURL,
|
|
JSON.stringify({
|
|
expires: resultData.expires,
|
|
etag: resultData.etag,
|
|
lastModified: resultData.lastModified
|
|
})
|
|
);
|
|
} catch (e) {
|
|
env.logger.warn(
|
|
null,
|
|
`An error was encountered while writing the cache file for a remote asset. Proceeding without caching this asset. Error: ${e}`
|
|
);
|
|
}
|
|
}
|
|
function getStaticImageList() {
|
|
if (!globalThis?.astroAsset?.staticImages) {
|
|
return /* @__PURE__ */ new Map();
|
|
}
|
|
return globalThis.astroAsset.staticImages;
|
|
}
|
|
async function loadImage(path, env) {
|
|
if (isRemotePath(path)) {
|
|
return await loadRemoteImage(path);
|
|
}
|
|
return {
|
|
data: await fs.promises.readFile(getFullImagePath(path, env)),
|
|
expires: 0
|
|
};
|
|
}
|
|
export {
|
|
generateImagesForPath,
|
|
getStaticImageList,
|
|
prepareAssetsGenerationEnv
|
|
};
|