// @ts-check import fs from "node:fs/promises" import { fileURLToPath } from "node:url" import { posix as path, resolve } from "node:path" import { saveAndCopyAsset } from "./utils/saveAndCopyAsset.js" import vitePluginAstroImageTools, { store } from "../plugin/index.js" import pMap from "p-map" import { sync as mkdir } from '@polymech/fs/dir' import { sync as readFile } from '@polymech/fs/read' import { sync as writeFile } from '@polymech/fs/write' import { sync as exists } from '@polymech/fs/exists' const filename = fileURLToPath(import.meta.url); const astroViteConfigsPath = resolve(filename, "../../astroViteConfigs.js"); export default { name: "imagetools", hooks: { "astro:config:setup": async function ({ config, command, updateConfig }) { const environment = command; const isSsrBuild = command === "build" && !!config.adapter && config.output === "server"; console.log(`[imagetools] config:setup command: ${command}`); let projectBase = path.normalize(config.base); if (projectBase.startsWith("./")) projectBase = projectBase.slice(1); if (!projectBase.startsWith("/")) projectBase = "/" + projectBase; if (projectBase.endsWith("/")) projectBase = projectBase.slice(0, -1); const astroViteConfigs = { environment, isSsrBuild, projectBase, publicDir: fileURLToPath(config.publicDir.href), rootDir: fileURLToPath(config.root.href), }; await fs.writeFile( astroViteConfigsPath, `export default ${JSON.stringify(astroViteConfigs)}` ); updateConfig({ vite: { plugins: [vitePluginAstroImageTools], }, }); }, "astro:build:done": async function closeBundle({ logger }) { console.time('[imagetools] build:done'); const { default: astroViteConfigs } = await import( // @ts-ignore "../astroViteConfigs.js" ); const { mode, outDir, assetsDir, isSsrBuild } = astroViteConfigs; if (mode === "production") { // 1. Define the manifest path in the current working directory. const manifestPath = path.join(process.cwd(), "./imagetools-manifest.json") // 2. Read the manifest from the previous build, if it exists. let previousAssets = new Map(); try { const manifestContent = (await readFile(manifestPath, "string")) || ""; previousAssets = new Map(JSON.parse(/** @type {string} */(manifestContent) || "[]")); } catch (e) { // Manifest doesn't exist or is invalid, start fresh. } // 3. Get assets from the current build's in-memory store. const currentAssets = new Map( [...store.entries()].filter(([, { hash = null } = {}]) => hash) ); logger.info(`[imagetools] Found ${currentAssets.size} new image(s) processed in this build.`); // 4. Merge previous and current assets. Current assets overwrite previous ones. const allAssets = new Map([...previousAssets, ...currentAssets]); logger.info(`[imagetools] Replaying ${allAssets.size} image(s) from the persistent manifest.`); if (allAssets.size === 0) { logger.info('[imagetools] No images to process.'); console.timeEnd('[imagetools] build:done'); return; } // 5. Process the entire merged list of assets. await pMap( [...allAssets.entries()], async ([assetPath, { hash, image, buffer }]) => { try { await saveAndCopyAsset( hash, image, buffer, outDir, assetsDir, assetPath, isSsrBuild ); } catch (error) { logger.error(`Failed to process image ${assetPath}:`, error); } }, // higher concurrency causes sharp/vips errors as well { concurrency: 10 } ); // 6. Write the updated asset list back to the manifest for the next build. try { // Create a serializable version of the manifest data const serializableAssets = [...allAssets.entries()].map( ([key, { hash, buffer }]) => { // We only need the hash and buffer for reconstitution. // The `image` object is a non-serializable Sharp instance. return [key, { hash, buffer }]; } ); const manifestContent = JSON.stringify(serializableAssets, null, 2); logger.info(`[imagetools] Writing manifest (${(manifestContent.length / 1024).toFixed(2)} KB)...`); console.time('[imagetools] Manifest write took'); await writeFile(manifestPath, manifestContent); console.timeEnd('[imagetools] Manifest write took'); } catch (error) { logger.error("Failed to write the asset manifest."); console.error(error); // Log the full error object } } console.timeEnd('[imagetools] build:done'); }, }, };