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;
|
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 Astro’s experimental assets support.
|
* 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:
|
* 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 Astro’s 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 & {});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue