diff --git a/.changeset/tall-news-hang.md b/.changeset/tall-news-hang.md new file mode 100644 index 000000000..0887bbec1 --- /dev/null +++ b/.changeset/tall-news-hang.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Update `experimental.assets`'s `image.service` configuration to allow for a config option in addition to an entrypoint diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 43fc7ceb7..674445cd7 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -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; +} + /** * 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}} + * @default `{entrypoint: 'astro/assets/services/squoosh', config?: {}}` * @version 2.1.0 * @description * Set which image service is used for Astro’s experimental assets support. * - * The value should be a module specifier for the image service to use: - * either one of Astro’s 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; }; /** diff --git a/packages/astro/src/assets/image-endpoint.ts b/packages/astro/src/assets/image-endpoint.ts index e3f90941e..e3553edbc 100644 --- a/packages/astro/src/assets/image-endpoint.ts +++ b/packages/astro/src/assets/image-endpoint.ts @@ -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, diff --git a/packages/astro/src/assets/index.ts b/packages/astro/src/assets/index.ts index 6b792fa97..bab74a815 100644 --- a/packages/astro/src/assets/index.ts +++ b/packages/astro/src/assets/index.ts @@ -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: {}, + }; +} diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index f38b88124..945b5a3e8 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -45,7 +45,10 @@ export async function getConfiguredImageService(): Promise { * * This is functionally equivalent to using the `` component, as the component calls this function internally. */ -export async function getImage(options: ImageTransform): Promise { +export async function getImage( + options: ImageTransform, + serviceConfig: Record +): Promise { if (!options || typeof options !== 'object') { throw new AstroError({ ...AstroErrorData.ExpectedImageOptions, @@ -54,9 +57,11 @@ export async function getImage(options: ImageTransform): Promise } 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 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); diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index aa33d8901..5c19ce3d9 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -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; /** * 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; + getHTMLAttributes?: ( + options: ImageTransform, + serviceConfig: Record + ) => Record; /** * 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) => 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) => 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 ) => Promise<{ data: Buffer; format: ImageOutputFormat }>; } diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index a78def7e8..3c0a94d2a 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -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)) { diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 1ba6fd829..4c55dc5b6 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -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({ diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index 49f6ce6ae..7865094e5 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -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'); + }); }); }); diff --git a/packages/astro/test/fixtures/core-image/service.mjs b/packages/astro/test/fixtures/core-image/service.mjs index dfede13b3..646622f51 100644 --- a/packages/astro/test/fixtures/core-image/service.mjs +++ b/packages/astro/test/fixtures/core-image/service.mjs @@ -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) {