astro/packages/integrations/image/src/index.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

140 lines
4.7 KiB
TypeScript
Raw Normal View History

Adds an `@astrojs/image` integration for optimizing images (#3694) * initial commit * WIP: starting to define interfaces for images and transformers * WIP: basic sharp service to test out the API setup * adding a few tests for sharp.toImageSrc * Adding tests for sharp.parseImageSrc * hooking up basic SSR support * updating image services to return width/height * simplifying config setup for v1 * hooking up basic SSR + SSG support (dev & build) * refactor: a bit of code cleanup and commenting * WIP: migrating local files to ESM + vite plugin * WIP: starting to hook up user-provided loaderEntryPoints * chore: update lock file * chore: update merged lockfile * refactor: code cleanup and type docs * pulling over the README template for first-party integrations * moving metadata out to the loader * updating the test for the refactored import * revert: remove unrelated webapi formatting * revert: remove unrelated change * fixing up the existing sharp tests * fix: vite plugin wasn't dynamically loading the image service properly * refactor: minor API renaming, removing last hard-coded use of sharp loader * don't manipulate src for hosted image services * Adding support for automatically calculating dimensions by aspect ratio, if needed * a few bug fixes + renaming the aspect ratio search param to "ar" * Adding ETag support, removing need for loaders to parse file metadata * using the battle tested `etag` package * Adding support for dynamically calculating partial sizes * refactor: moving to the packages/integrations dir, Astro Labs TBD later * refactor: renaming parse/serialize functions * Adding tests for SSG image optimizations * refactor: clean up outdated names related to ImageProps * nit: reusing cached SSG filename * chore: update pnpm lock file * handling file URLs when resolving local image imports * updating image file resolution to use file URLs * increasing test timeout for image build tests * fixing eslint error in sharp test * adding slash for windows compat in src URLs * chore: update lockfile after merge * Adding README content * adding a readme call to action for configuration options * review: A few of the quick updates from the PR review * hack: adds a one-off check to allow query params for the _image route * Adds support for src={import("...")}, and named component exports * adding SSR tests * nit: adding a bit more comments * limiting the query params in SSG dev to the images integration
2022-07-01 15:47:48 +00:00
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import slash from 'slash';
import { ensureDir, isRemoteImage, loadLocalImage, loadRemoteImage, propsToFilename } from './utils.js';
import { createPlugin } from './vite-plugin-astro-image.js';
import type { AstroConfig, AstroIntegration } from 'astro';
import type { ImageAttributes, IntegrationOptions, SSRImageService, TransformOptions } from './types';
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)
]
}
}
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;