feat(image): Update image service config to allow passing settings to the service (#6848)

This commit is contained in:
Erika 2023-04-27 18:16:40 +02:00 committed by GitHub
parent de5a25f4e9
commit ebae1eaf87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 104 additions and 33 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Update `experimental.assets`'s `image.service` configuration to allow for a config option in addition to an entrypoint

View file

@ -335,6 +335,12 @@ export interface ViteUserConfig extends vite.UserConfig {
ssr?: vite.SSROptions; ssr?: vite.SSROptions;
} }
export interface ImageServiceConfig {
// eslint-disable-next-line @typescript-eslint/ban-types
entrypoint: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {});
config?: Record<string, any>;
}
/** /**
* Astro User Config * Astro User Config
* Docs: https://docs.astro.build/reference/configuration-reference/ * Docs: https://docs.astro.build/reference/configuration-reference/
@ -746,26 +752,26 @@ export interface AstroUserConfig {
/** /**
* @docs * @docs
* @name image.service (Experimental) * @name image.service (Experimental)
* @type {'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | string} * @type {{entrypoint: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | string, config: Record<string, any>}}
* @default `'astro/assets/services/squoosh'` * @default `{entrypoint: 'astro/assets/services/squoosh', config?: {}}`
* @version 2.1.0 * @version 2.1.0
* @description * @description
* Set which image service is used for Astros experimental assets support. * Set which image service is used for Astros experimental assets support.
* *
* The value should be a module specifier for the image service to use: * The value should be an object with an entrypoint for the image service to use and optionally, a config object to pass to the service.
* either one of Astros two built-in services, or a third-party implementation. *
* The service entrypoint can be either one of the included services, or a third-party package.
* *
* ```js * ```js
* { * {
* image: { * image: {
* // Example: Enable the Sharp-based image service * // Example: Enable the Sharp-based image service
* service: 'astro/assets/services/sharp', * service: { entrypoint: 'astro/assets/services/sharp' },
* }, * },
* } * }
* ``` * ```
*/ */
// eslint-disable-next-line @typescript-eslint/ban-types service: ImageServiceConfig;
service: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {});
}; };
/** /**

View file

@ -4,6 +4,8 @@ import { isRemotePath } from '../core/path.js';
import { getConfiguredImageService } from './internal.js'; import { getConfiguredImageService } from './internal.js';
import { isLocalService } from './services/service.js'; import { isLocalService } from './services/service.js';
import { etag } from './utils/etag.js'; import { etag } from './utils/etag.js';
// @ts-expect-error
import { imageServiceConfig } from 'astro:assets';
async function loadRemoteImage(src: URL) { async function loadRemoteImage(src: URL) {
try { try {
@ -31,7 +33,7 @@ export const get: APIRoute = async ({ request }) => {
} }
const url = new URL(request.url); const url = new URL(request.url);
const transform = await imageService.parseURL(url); const transform = await imageService.parseURL(url, imageServiceConfig);
if (!transform || !transform.src) { if (!transform || !transform.src) {
throw new Error('Incorrect transform returned by `parseURL`'); throw new Error('Incorrect transform returned by `parseURL`');
@ -49,7 +51,11 @@ export const get: APIRoute = async ({ request }) => {
return new Response('Not Found', { status: 404 }); return new Response('Not Found', { status: 404 });
} }
const { data, format } = await imageService.transform(inputBuffer, transform); const { data, format } = await imageService.transform(
inputBuffer,
transform,
imageServiceConfig
);
return new Response(data, { return new Response(data, {
status: 200, status: 200,

View file

@ -1,5 +1,21 @@
import type { ImageServiceConfig } from '../@types/astro.js';
export { getConfiguredImageService, getImage } from './internal.js'; export { getConfiguredImageService, getImage } from './internal.js';
export { baseService, isLocalService } from './services/service.js'; export { baseService, isLocalService } from './services/service.js';
export { type LocalImageProps, type RemoteImageProps } from './types.js'; export { type LocalImageProps, type RemoteImageProps } from './types.js';
export { emitESMImage } from './utils/emitAsset.js'; export { emitESMImage } from './utils/emitAsset.js';
export { imageMetadata } from './utils/metadata.js'; export { imageMetadata } from './utils/metadata.js';
export function sharpImageService(): ImageServiceConfig {
return {
entrypoint: 'astro/assets/services/sharp',
config: {},
};
}
export function squooshImageService(): ImageServiceConfig {
return {
entrypoint: 'astro/assets/services/squoosh',
config: {},
};
}

View file

@ -45,7 +45,10 @@ export async function getConfiguredImageService(): Promise<ImageService> {
* *
* This is functionally equivalent to using the `<Image />` component, as the component calls this function internally. * This is functionally equivalent to using the `<Image />` component, as the component calls this function internally.
*/ */
export async function getImage(options: ImageTransform): Promise<GetImageResult> { export async function getImage(
options: ImageTransform,
serviceConfig: Record<string, any>
): Promise<GetImageResult> {
if (!options || typeof options !== 'object') { if (!options || typeof options !== 'object') {
throw new AstroError({ throw new AstroError({
...AstroErrorData.ExpectedImageOptions, ...AstroErrorData.ExpectedImageOptions,
@ -54,9 +57,11 @@ export async function getImage(options: ImageTransform): Promise<GetImageResult>
} }
const service = await getConfiguredImageService(); const service = await getConfiguredImageService();
const validatedOptions = service.validateOptions ? service.validateOptions(options) : options; const validatedOptions = service.validateOptions
? service.validateOptions(options, serviceConfig)
: options;
let imageURL = service.getURL(validatedOptions); let imageURL = service.getURL(validatedOptions, serviceConfig);
// In build and for local services, we need to collect the requested parameters so we can generate the final images // In build and for local services, we need to collect the requested parameters so we can generate the final images
if (isLocalService(service) && globalThis.astroAsset.addStaticImage) { if (isLocalService(service) && globalThis.astroAsset.addStaticImage) {
@ -68,7 +73,9 @@ export async function getImage(options: ImageTransform): Promise<GetImageResult>
options: validatedOptions, options: validatedOptions,
src: imageURL, src: imageURL,
attributes: attributes:
service.getHTMLAttributes !== undefined ? service.getHTMLAttributes(validatedOptions) : {}, service.getHTMLAttributes !== undefined
? service.getHTMLAttributes(validatedOptions, serviceConfig)
: {},
}; };
} }
@ -121,7 +128,11 @@ export async function generateImage(
serverRoot serverRoot
) )
); );
const resultData = await imageService.transform(fileData, { ...options, src: originalImagePath }); const resultData = await imageService.transform(
fileData,
{ ...options, src: originalImagePath },
buildOpts.settings.config.image.service.config
);
const finalFileURL = new URL('.' + filepath, clientRoot); const finalFileURL = new URL('.' + filepath, clientRoot);
const finalFolderURL = new URL('./', finalFileURL); const finalFolderURL = new URL('./', finalFileURL);

View file

@ -31,14 +31,17 @@ interface SharedServiceProps {
* For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image` * For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image`
* *
*/ */
getURL: (options: ImageTransform) => string; getURL: (options: ImageTransform, serviceConfig: Record<string, any>) => string;
/** /**
* Return any additional HTML attributes separate from `src` that your service requires to show the image properly. * Return any additional HTML attributes separate from `src` that your service requires to show the image properly.
* *
* For example, you might want to return the `width` and `height` to avoid CLS, or a particular `class` or `style`. * For example, you might want to return the `width` and `height` to avoid CLS, or a particular `class` or `style`.
* In most cases, you'll want to return directly what your user supplied you, minus the attributes that were used to generate the image. * In most cases, you'll want to return directly what your user supplied you, minus the attributes that were used to generate the image.
*/ */
getHTMLAttributes?: (options: ImageTransform) => Record<string, any>; getHTMLAttributes?: (
options: ImageTransform,
serviceConfig: Record<string, any>
) => Record<string, any>;
/** /**
* Validate and return the options passed by the user. * Validate and return the options passed by the user.
* *
@ -47,7 +50,7 @@ interface SharedServiceProps {
* *
* This method should returns options, and can be used to set defaults (ex: a default output format to be used if the user didn't specify one.) * This method should returns options, and can be used to set defaults (ex: a default output format to be used if the user didn't specify one.)
*/ */
validateOptions?: (options: ImageTransform) => ImageTransform; validateOptions?: (options: ImageTransform, serviceConfig: Record<string, any>) => ImageTransform;
} }
export type ExternalImageService = SharedServiceProps; export type ExternalImageService = SharedServiceProps;
@ -63,14 +66,15 @@ export interface LocalImageService extends SharedServiceProps {
* *
* In most cases, this will get query parameters using, for example, `params.get('width')` and return those. * In most cases, this will get query parameters using, for example, `params.get('width')` and return those.
*/ */
parseURL: (url: URL) => LocalImageTransform | undefined; parseURL: (url: URL, serviceConfig: Record<string, any>) => LocalImageTransform | undefined;
/** /**
* Performs the image transformations on the input image and returns both the binary data and * Performs the image transformations on the input image and returns both the binary data and
* final image format of the optimized image. * final image format of the optimized image.
*/ */
transform: ( transform: (
inputBuffer: Buffer, inputBuffer: Buffer,
transform: LocalImageTransform transform: LocalImageTransform,
serviceConfig: Record<string, any>
) => Promise<{ data: Buffer; format: ImageOutputFormat }>; ) => Promise<{ data: Buffer; format: ImageOutputFormat }>;
} }

View file

@ -38,7 +38,7 @@ export default function assets({
const adapterName = settings.config.adapter?.name; const adapterName = settings.config.adapter?.name;
if ( if (
['astro/assets/services/sharp', 'astro/assets/services/squoosh'].includes( ['astro/assets/services/sharp', 'astro/assets/services/squoosh'].includes(
settings.config.image.service settings.config.image.service.entrypoint
) && ) &&
adapterName && adapterName &&
UNSUPPORTED_ADAPTERS.has(adapterName) UNSUPPORTED_ADAPTERS.has(adapterName)
@ -70,7 +70,7 @@ export default function assets({
}, },
async resolveId(id) { async resolveId(id) {
if (id === VIRTUAL_SERVICE_ID) { if (id === VIRTUAL_SERVICE_ID) {
return await this.resolve(settings.config.image.service); return await this.resolve(settings.config.image.service.entrypoint);
} }
if (id === VIRTUAL_MODULE_ID) { if (id === VIRTUAL_MODULE_ID) {
return resolvedVirtualModuleId; return resolvedVirtualModuleId;
@ -79,8 +79,12 @@ export default function assets({
load(id) { load(id) {
if (id === resolvedVirtualModuleId) { if (id === resolvedVirtualModuleId) {
return ` return `
export { getImage, getConfiguredImageService, isLocalService } from "astro/assets"; export { getConfiguredImageService, isLocalService } from "astro/assets";
import { getImage as getImageInternal } from "astro/assets";
export { default as Image } from "astro/components/Image.astro"; export { default as Image } from "astro/components/Image.astro";
export const imageServiceConfig = ${JSON.stringify(settings.config.image.service.config)};
export const getImage = async (options) => await getImageInternal(options, imageServiceConfig);
`; `;
} }
}, },
@ -116,7 +120,10 @@ export default function assets({
} }
} }
const transform = await globalThis.astroAsset.imageService.parseURL(url); const transform = await globalThis.astroAsset.imageService.parseURL(
url,
settings.config.image.service.config
);
if (transform === undefined) { if (transform === undefined) {
error(logging, 'image', `Failed to parse transform for ${url}`); error(logging, 'image', `Failed to parse transform for ${url}`);
@ -127,7 +134,11 @@ export default function assets({
let format: string = meta.format; let format: string = meta.format;
if (transform) { if (transform) {
const result = await globalThis.astroAsset.imageService.transform(file, transform); const result = await globalThis.astroAsset.imageService.transform(
file,
transform,
settings.config.image.service.config
);
data = result.data; data = result.data;
format = result.format; format = result.format;
} }
@ -155,7 +166,7 @@ export default function assets({
>(); >();
} }
const hash = hashTransform(options, settings.config.image.service); const hash = hashTransform(options, settings.config.image.service.entrypoint);
let filePath: string; let filePath: string;
if (globalThis.astroAsset.staticImages.has(hash)) { if (globalThis.astroAsset.staticImages.has(hash)) {

View file

@ -123,14 +123,17 @@ export const AstroConfigSchema = z.object({
), ),
image: z image: z
.object({ .object({
service: z.union([ service: z.object({
z.literal('astro/assets/services/sharp'), entrypoint: z.union([
z.literal('astro/assets/services/squoosh'), z.literal('astro/assets/services/sharp'),
z.string(), z.literal('astro/assets/services/squoosh'),
]), z.string(),
]),
config: z.record(z.any()).default({}),
}),
}) })
.default({ .default({
service: 'astro/assets/services/squoosh', service: { entrypoint: 'astro/assets/services/squoosh', config: {} },
}), }),
markdown: z markdown: z
.object({ .object({

View file

@ -620,7 +620,7 @@ describe('astro:image', () => {
assets: true, assets: true,
}, },
image: { image: {
service: fileURLToPath(new URL('./fixtures/core-image/service.mjs', import.meta.url)), service: { entrypoint: fileURLToPath(new URL('./fixtures/core-image/service.mjs', import.meta.url)), config: {foo: 'bar'} }
}, },
}); });
devServer = await fixture.startDevServer(); devServer = await fixture.startDevServer();
@ -641,5 +641,13 @@ describe('astro:image', () => {
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('img').attr('data-service')).to.equal('my-custom-service'); expect($('img').attr('data-service')).to.equal('my-custom-service');
}); });
it('gets service config', async () => {
const response = await fixture.fetch('/');
const html = await response.text();
const $ = cheerio.load(html);
expect($('#local img').attr('data-service-config')).to.equal('bar');
});
}); });
}); });

View file

@ -7,8 +7,9 @@ const service = {
getURL(options) { getURL(options) {
return squoosh.getURL(options); return squoosh.getURL(options);
}, },
getHTMLAttributes(options) { getHTMLAttributes(options, serviceConfig) {
options['data-service'] = 'my-custom-service'; options['data-service'] = 'my-custom-service';
options['data-service-config'] = serviceConfig.foo;
return squoosh.getHTMLAttributes(options); return squoosh.getHTMLAttributes(options);
}, },
parseURL(url) { parseURL(url) {