import type { AstroConfig, AstroIntegration } from 'astro'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { OUTPUT_DIR, PKG_NAME, ROUTE_PATTERN } from './constants.js'; export * from './get-image.js'; export * from './get-picture.js'; import { IntegrationOptions, TransformOptions } from './types.js'; import { ensureDir, isRemoteImage, loadLocalImage, loadRemoteImage, propsToFilename, } from './utils.js'; import { createPlugin } from './vite-plugin-astro-image.js'; const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => { const resolvedOptions = { serviceEntryPoint: '@astrojs/image/sharp', ...options, }; // During SSG builds, this is used to track all transformed images required. const staticImages = new Map(); let _config: AstroConfig; function getViteConfiguration() { return { plugins: [createPlugin(_config, resolvedOptions)], optimizeDeps: { include: ['image-size', 'sharp'], }, ssr: { noExternal: ['@astrojs/image'], }, }; } return { name: PKG_NAME, hooks: { 'astro:config:setup': ({ command, config, injectRoute, updateConfig }) => { _config = config; // Always treat `astro dev` as SSR mode, even without an adapter const mode = command === 'dev' || config.adapter ? 'ssr' : 'ssg'; updateConfig({ vite: getViteConfiguration() }); // Used to cache all images rendered to HTML // Added to globalThis to share the same map in Node and Vite (globalThis as any).addStaticImage = (transform: TransformOptions) => { staticImages.set(propsToFilename(transform), transform); }; // TODO: Add support for custom, user-provided filename format functions (globalThis as any).filenameFormat = ( transform: TransformOptions, searchParams: URLSearchParams ) => { if (mode === 'ssg') { return isRemoteImage(transform.src) ? path.join(OUTPUT_DIR, path.basename(propsToFilename(transform))) : path.join( OUTPUT_DIR, path.dirname(transform.src), path.basename(propsToFilename(transform)) ); } else { return `${ROUTE_PATTERN}?${searchParams.toString()}`; } }; if (mode === 'ssr') { injectRoute({ pattern: ROUTE_PATTERN, entryPoint: command === 'dev' ? '@astrojs/image/endpoints/dev' : '@astrojs/image/endpoints/prod', }); } }, 'astro:build:done': async ({ dir }) => { for await (const [filename, transform] of staticImages) { const loader = (globalThis as any).loader; let inputBuffer: Buffer | undefined = undefined; let outputFile: string; if (isRemoteImage(transform.src)) { // try to load the remote image inputBuffer = await loadRemoteImage(transform.src); const outputFileURL = new URL( path.join('./', OUTPUT_DIR, path.basename(filename)), dir ); outputFile = fileURLToPath(outputFileURL); } else { const inputFileURL = new URL(`.${transform.src}`, _config.srcDir); const inputFile = fileURLToPath(inputFileURL); inputBuffer = await loadLocalImage(inputFile); const outputFileURL = new URL(path.join('./', OUTPUT_DIR, filename), dir); outputFile = fileURLToPath(outputFileURL); } if (!inputBuffer) { // eslint-disable-next-line no-console console.warn(`"${transform.src}" image could not be fetched`); continue; } const { data } = await loader.transform(inputBuffer, transform); ensureDir(path.dirname(outputFile)); await fs.writeFile(outputFile, data); } }, }, }; }; export default createIntegration;