166 lines
4.8 KiB
TypeScript
166 lines
4.8 KiB
TypeScript
import type { AstroConfig, AstroIntegration } from 'astro';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import slash from 'slash';
|
|
import { fileURLToPath } from 'url';
|
|
import type {
|
|
ImageAttributes,
|
|
IntegrationOptions,
|
|
SSRImageService,
|
|
TransformOptions,
|
|
} from './types';
|
|
import {
|
|
ensureDir,
|
|
isRemoteImage,
|
|
loadLocalImage,
|
|
loadRemoteImage,
|
|
propsToFilename,
|
|
} from './utils.js';
|
|
import { createPlugin } from './vite-plugin-astro-image.js';
|
|
|
|
const PKG_NAME = '@astrojs/image';
|
|
const ROUTE_PATTERN = '/_image';
|
|
const OUTPUT_DIR = '/_image';
|
|
|
|
/**
|
|
* Gets the HTML attributes required to build an `<img />` for the transformed image.
|
|
*
|
|
* @param loader @type {ImageService} The image service used for transforming images.
|
|
* @param transform @type {TransformOptions} The transformations requested for the optimized image.
|
|
* @returns @type {ImageAttributes} The HTML attributes to be included on the built `<img />` element.
|
|
*/
|
|
export async function getImage(
|
|
loader: SSRImageService,
|
|
transform: TransformOptions
|
|
): Promise<ImageAttributes> {
|
|
(globalThis as any).loader = loader;
|
|
|
|
const attributes = await loader.getImageAttributes(transform);
|
|
|
|
// For SSR services, build URLs for the injected route
|
|
if (typeof loader.transform === 'function') {
|
|
const { searchParams } = loader.serializeTransform(transform);
|
|
|
|
// cache all images rendered to HTML
|
|
if (globalThis && (globalThis as any).addStaticImage) {
|
|
(globalThis as any)?.addStaticImage(transform);
|
|
}
|
|
|
|
const src =
|
|
globalThis && (globalThis as any).filenameFormat
|
|
? (globalThis as any).filenameFormat(transform, searchParams)
|
|
: `${ROUTE_PATTERN}?${searchParams.toString()}`;
|
|
|
|
return {
|
|
...attributes,
|
|
src: slash(src), // Windows compat
|
|
};
|
|
}
|
|
|
|
// For hosted services, return the <img /> attributes as-is
|
|
return attributes;
|
|
}
|
|
|
|
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<string, TransformOptions>();
|
|
|
|
let _config: AstroConfig;
|
|
|
|
function getViteConfiguration() {
|
|
return {
|
|
plugins: [createPlugin(_config, resolvedOptions)],
|
|
optimizeDeps: {
|
|
include: ['image-size', 'sharp'],
|
|
},
|
|
};
|
|
}
|
|
|
|
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) {
|
|
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;
|