feat(image): Update image service config to allow passing settings to the service (#6848)
This commit is contained in:
parent
de5a25f4e9
commit
ebae1eaf87
10 changed files with 104 additions and 33 deletions
5
.changeset/tall-news-hang.md
Normal file
5
.changeset/tall-news-hang.md
Normal 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
|
|
@ -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 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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 }>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue