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;
}
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
* Docs: https://docs.astro.build/reference/configuration-reference/
@ -746,26 +752,26 @@ export interface AstroUserConfig {
/**
* @docs
* @name image.service (Experimental)
* @type {'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | string}
* @default `'astro/assets/services/squoosh'`
* @type {{entrypoint: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | string, config: Record<string, any>}}
* @default `{entrypoint: 'astro/assets/services/squoosh', config?: {}}`
* @version 2.1.0
* @description
* Set which image service is used for Astros experimental assets support.
*
* The value should be a module specifier for the image service to use:
* either one of Astros two built-in services, or a third-party implementation.
* 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.
*
* The service entrypoint can be either one of the included services, or a third-party package.
*
* ```js
* {
* image: {
* // 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: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {});
service: ImageServiceConfig;
};
/**

View file

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

View file

@ -1,5 +1,21 @@
import type { ImageServiceConfig } from '../@types/astro.js';
export { getConfiguredImageService, getImage } from './internal.js';
export { baseService, isLocalService } from './services/service.js';
export { type LocalImageProps, type RemoteImageProps } from './types.js';
export { emitESMImage } from './utils/emitAsset.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.
*/
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') {
throw new AstroError({
...AstroErrorData.ExpectedImageOptions,
@ -54,9 +57,11 @@ export async function getImage(options: ImageTransform): Promise<GetImageResult>
}
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
if (isLocalService(service) && globalThis.astroAsset.addStaticImage) {
@ -68,7 +73,9 @@ export async function getImage(options: ImageTransform): Promise<GetImageResult>
options: validatedOptions,
src: imageURL,
attributes:
service.getHTMLAttributes !== undefined ? service.getHTMLAttributes(validatedOptions) : {},
service.getHTMLAttributes !== undefined
? service.getHTMLAttributes(validatedOptions, serviceConfig)
: {},
};
}
@ -121,7 +128,11 @@ export async function generateImage(
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 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`
*
*/
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.
*
* 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.
*/
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.
*
@ -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.)
*/
validateOptions?: (options: ImageTransform) => ImageTransform;
validateOptions?: (options: ImageTransform, serviceConfig: Record<string, any>) => ImageTransform;
}
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.
*/
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
* final image format of the optimized image.
*/
transform: (
inputBuffer: Buffer,
transform: LocalImageTransform
transform: LocalImageTransform,
serviceConfig: Record<string, any>
) => Promise<{ data: Buffer; format: ImageOutputFormat }>;
}

View file

@ -38,7 +38,7 @@ export default function assets({
const adapterName = settings.config.adapter?.name;
if (
['astro/assets/services/sharp', 'astro/assets/services/squoosh'].includes(
settings.config.image.service
settings.config.image.service.entrypoint
) &&
adapterName &&
UNSUPPORTED_ADAPTERS.has(adapterName)
@ -70,7 +70,7 @@ export default function assets({
},
async resolveId(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) {
return resolvedVirtualModuleId;
@ -79,8 +79,12 @@ export default function assets({
load(id) {
if (id === resolvedVirtualModuleId) {
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 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) {
error(logging, 'image', `Failed to parse transform for ${url}`);
@ -127,7 +134,11 @@ export default function assets({
let format: string = meta.format;
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;
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;
if (globalThis.astroAsset.staticImages.has(hash)) {

View file

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

View file

@ -620,7 +620,7 @@ describe('astro:image', () => {
assets: true,
},
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();
@ -641,5 +641,13 @@ describe('astro:image', () => {
const $ = cheerio.load(html);
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) {
return squoosh.getURL(options);
},
getHTMLAttributes(options) {
getHTMLAttributes(options, serviceConfig) {
options['data-service'] = 'my-custom-service';
options['data-service-config'] = serviceConfig.foo;
return squoosh.getHTMLAttributes(options);
},
parseURL(url) {