Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-news-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Update `experimental.assets`'s `image.service` configuration to allow for a config option in addition to an entrypoint
20 changes: 13 additions & 7 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious because I don't know: Do we need to worry about updating the version number here since this specific configuration will only be appropriate for 2.3.x? (Even though image.service itself was added in 2.1.0?)

Are we at all worried about this being misleading for someone who happens to be on a slightly older version of Astro before this update to the image service config?

* @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;
};

/**
Expand Down
10 changes: 8 additions & 2 deletions packages/astro/src/assets/image-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`');
Expand All @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions packages/astro/src/assets/index.ts
Original file line number Diff line number Diff line change
@@ -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: {},
};
}
21 changes: 16 additions & 5 deletions packages/astro/src/assets/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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)
: {},
};
}

Expand Down Expand Up @@ -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);
Expand Down
14 changes: 9 additions & 5 deletions packages/astro/src/assets/services/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
Expand All @@ -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 }>;
}

Expand Down
23 changes: 17 additions & 6 deletions packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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);
`;
}
},
Expand Down Expand Up @@ -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}`);
Expand All @@ -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;
}
Expand Down Expand Up @@ -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)) {
Expand Down
15 changes: 9 additions & 6 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
10 changes: 9 additions & 1 deletion packages/astro/test/core-image.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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');
});
});
});
3 changes: 2 additions & 1 deletion packages/astro/test/fixtures/core-image/service.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down