diff --git a/.changeset/cold-flies-clean.md b/.changeset/cold-flies-clean.md
new file mode 100644
index 000000000000..6c6a345e65ea
--- /dev/null
+++ b/.changeset/cold-flies-clean.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/vercel': major
+---
+
+Adds a configuration option `devImageService` to choose which of the built-in image services to use in development. Defaults to `sharp`.
diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md
index 00c2a18cfe0b..db3a52a03f0e 100644
--- a/packages/integrations/vercel/README.md
+++ b/packages/integrations/vercel/README.md
@@ -137,7 +137,7 @@ export default defineConfig({
**Available for:** Serverless, Static
**Added in:** `@astrojs/vercel@3.3.0`
-When enabled, an [Image Service](https://docs.astro.build/en/reference/image-service-reference/) powered by the Vercel Image Optimization API will be automatically configured and used in production. In development, a built-in Squoosh-based service will be used instead.
+When enabled, an [Image Service](https://docs.astro.build/en/reference/image-service-reference/) powered by the Vercel Image Optimization API will be automatically configured and used in production. In development, the image service specified by [`devImageService`](#devimageservice) will be used instead.
```js
// astro.config.mjs
@@ -172,6 +172,30 @@ import astroLogo from '../assets/logo.png';
/>
```
+### devImageService
+
+**Type:** `'sharp' | 'squoosh' | string`
+**Available for:** Serverless, Static
+**Added in:** `@astrojs/vercel@3.3.0`
+**Default**: 'sharp'
+
+Allows you to configure which image service to use in development when [imageService](#imageservice) is enabled. This can be useful if you cannot install Sharp's dependencies on your development machine, but using another image service like Squoosh would allow you to preview images in your dev environment. Build is unaffected and will always use Vercel's Image Optimization.
+
+It can also be set to any arbitrary value in order to use a custom image service instead of Astro's built-in ones.
+
+```js
+import { defineConfig } from 'astro/config';
+import vercel from '@astrojs/vercel/serverless';
+
+export default defineConfig({
+ output: 'server',
+ adapter: vercel({
+ imageService: true,
+ devImageService: 'squoosh',
+ }),
+});
+```
+
### includeFiles
**Type:** `string[]`
diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json
index b9ac5aaa0b89..34bbb269bed0 100644
--- a/packages/integrations/vercel/package.json
+++ b/packages/integrations/vercel/package.json
@@ -25,6 +25,7 @@
"./analytics": "./dist/analytics.js",
"./build-image-service": "./dist/image/build-service.js",
"./dev-image-service": "./dist/image/dev-service.js",
+ "./squoosh-dev-service": "./dist/image/squoosh-dev-service.js",
"./package.json": "./package.json"
},
"typesVersions": {
diff --git a/packages/integrations/vercel/src/image/build-service.ts b/packages/integrations/vercel/src/image/build-service.ts
index 0e45167d4780..bd58d3af61ce 100644
--- a/packages/integrations/vercel/src/image/build-service.ts
+++ b/packages/integrations/vercel/src/image/build-service.ts
@@ -40,8 +40,9 @@ const service: ExternalImageService = {
};
},
getURL(options) {
- const fileSrc =
- typeof options.src === 'string' ? options.src : removeLeadingForwardSlash(options.src.src);
+ const fileSrc = isESMImportedImage(options.src)
+ ? removeLeadingForwardSlash(options.src.src)
+ : options.src;
const searchParams = new URLSearchParams();
searchParams.append('url', fileSrc);
diff --git a/packages/integrations/vercel/src/image/dev-service.ts b/packages/integrations/vercel/src/image/dev-service.ts
index a335c8d23295..c9702cff9754 100644
--- a/packages/integrations/vercel/src/image/dev-service.ts
+++ b/packages/integrations/vercel/src/image/dev-service.ts
@@ -1,10 +1,9 @@
import type { LocalImageService } from 'astro';
-import squooshService from 'astro/assets/services/squoosh';
-import { sharedValidateOptions } from './shared.js';
+import sharpService from 'astro/assets/services/sharp';
+import { baseDevService } from './shared-dev-service.js';
const service: LocalImageService = {
- validateOptions: (options, serviceOptions) =>
- sharedValidateOptions(options, serviceOptions.service.config, 'development'),
+ ...baseDevService,
getHTMLAttributes(options, serviceOptions) {
const { inputtedWidth, ...props } = options;
@@ -13,45 +12,19 @@ const service: LocalImageService = {
props.width = inputtedWidth;
}
- return squooshService.getHTMLAttributes
- ? squooshService.getHTMLAttributes(props, serviceOptions)
+ return sharpService.getHTMLAttributes
+ ? sharpService.getHTMLAttributes(props, serviceOptions)
: {};
},
- getURL(options) {
- const fileSrc = typeof options.src === 'string' ? options.src : options.src.src;
-
- const searchParams = new URLSearchParams();
- searchParams.append('href', fileSrc);
-
- options.width && searchParams.append('w', options.width.toString());
- options.quality && searchParams.append('q', options.quality.toString());
-
- return '/_image?' + searchParams;
- },
- parseURL(url) {
- const params = url.searchParams;
-
- if (!params.has('href')) {
- return undefined;
- }
-
- const transform = {
- src: params.get('href')!,
- width: params.has('w') ? parseInt(params.get('w')!) : undefined,
- quality: params.get('q'),
- };
-
- return transform;
- },
transform(inputBuffer, transform, serviceOptions) {
// NOTE: Hardcoding webp here isn't accurate to how the Vercel Image Optimization API works, normally what we should
// do is setup a custom endpoint that sniff the user's accept-content header and serve the proper format based on the
// user's Vercel config. However, that's: a lot of work for: not much. The dev service is inaccurate to the prod service
// in many more ways, this is one of the less offending cases and is, imo, okay, erika - 2023-04-27
- transform.format = 'webp';
+ transform.format = transform.src.endsWith('svg') ? 'svg' : 'webp';
- // The base Squoosh service works the same way as the Vercel Image Optimization API, so it's a safe fallback in local
- return squooshService.transform(inputBuffer, transform, serviceOptions);
+ // The base sharp service works the same way as the Vercel Image Optimization API, so it's a safe fallback in local
+ return sharpService.transform(inputBuffer, transform, serviceOptions);
},
};
diff --git a/packages/integrations/vercel/src/image/shared-dev-service.ts b/packages/integrations/vercel/src/image/shared-dev-service.ts
new file mode 100644
index 000000000000..4251603a704c
--- /dev/null
+++ b/packages/integrations/vercel/src/image/shared-dev-service.ts
@@ -0,0 +1,33 @@
+import type { LocalImageService } from 'astro';
+import { sharedValidateOptions } from './shared.js';
+
+export const baseDevService: Omit = {
+ validateOptions: (options, serviceOptions) =>
+ sharedValidateOptions(options, serviceOptions.service.config, 'development'),
+ getURL(options) {
+ const fileSrc = typeof options.src === 'string' ? options.src : options.src.src;
+
+ const searchParams = new URLSearchParams();
+ searchParams.append('href', fileSrc);
+
+ options.width && searchParams.append('w', options.width.toString());
+ options.quality && searchParams.append('q', options.quality.toString());
+
+ return '/_image?' + searchParams;
+ },
+ parseURL(url) {
+ const params = url.searchParams;
+
+ if (!params.has('href')) {
+ return undefined;
+ }
+
+ const transform = {
+ src: params.get('href')!,
+ width: params.has('w') ? parseInt(params.get('w')!) : undefined,
+ quality: params.get('q'),
+ };
+
+ return transform;
+ },
+};
diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts
index f6cace2a24ee..079186e187c2 100644
--- a/packages/integrations/vercel/src/image/shared.ts
+++ b/packages/integrations/vercel/src/image/shared.ts
@@ -12,6 +12,10 @@ export function getDefaultImageConfig(astroImageConfig: AstroConfig['image']): V
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
return typeof src === 'object';
}
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export type DevImageService = 'sharp' | 'squoosh' | (string & {});
+
// https://vercel.com/docs/build-output-api/v3/configuration#images
type ImageFormat = 'image/avif' | 'image/webp';
@@ -64,16 +68,32 @@ export function getAstroImageConfig(
images: boolean | undefined,
imagesConfig: VercelImageConfig | undefined,
command: string,
+ devImageService: DevImageService,
astroImageConfig: AstroConfig['image']
) {
+ let devService = '@astrojs/vercel/dev-image-service';
+
+ switch (devImageService) {
+ case 'sharp':
+ devService = '@astrojs/vercel/dev-image-service';
+ break;
+ case 'squoosh':
+ devService = '@astrojs/vercel/squoosh-dev-image-service';
+ break;
+ default:
+ if (typeof devImageService === 'string') {
+ devService = devImageService;
+ } else {
+ devService = '@astrojs/vercel/dev-image-service';
+ }
+ break;
+ }
+
if (images) {
return {
image: {
service: {
- entrypoint:
- command === 'dev'
- ? '@astrojs/vercel/dev-image-service'
- : '@astrojs/vercel/build-image-service',
+ entrypoint: command === 'dev' ? devService : '@astrojs/vercel/build-image-service',
config: imagesConfig ? imagesConfig : getDefaultImageConfig(astroImageConfig),
},
},
diff --git a/packages/integrations/vercel/src/image/squoosh-dev-service.ts b/packages/integrations/vercel/src/image/squoosh-dev-service.ts
new file mode 100644
index 000000000000..d3b05bb115f2
--- /dev/null
+++ b/packages/integrations/vercel/src/image/squoosh-dev-service.ts
@@ -0,0 +1,31 @@
+import type { LocalImageService } from 'astro';
+import squooshService from 'astro/assets/services/squoosh';
+import { baseDevService } from './shared-dev-service.js';
+
+const service: LocalImageService = {
+ ...baseDevService,
+ getHTMLAttributes(options, serviceOptions) {
+ const { inputtedWidth, ...props } = options;
+
+ // If `validateOptions` returned a different width than the one of the image, use it for attributes
+ if (inputtedWidth) {
+ props.width = inputtedWidth;
+ }
+
+ return squooshService.getHTMLAttributes
+ ? squooshService.getHTMLAttributes(props, serviceOptions)
+ : {};
+ },
+ transform(inputBuffer, transform, serviceOptions) {
+ // NOTE: Hardcoding webp here isn't accurate to how the Vercel Image Optimization API works, normally what we should
+ // do is setup a custom endpoint that sniff the user's accept-content header and serve the proper format based on the
+ // user's Vercel config. However, that's: a lot of work for: not much. The dev service is inaccurate to the prod service
+ // in many more ways, this is one of the less offending cases and is, imo, okay, erika - 2023-04-27
+ transform.format = transform.src.endsWith('svg') ? 'svg' : 'webp';
+
+ // The base squoosh service works the same way as the Vercel Image Optimization API, so it's a safe fallback in local
+ return squooshService.transform(inputBuffer, transform, serviceOptions);
+ },
+};
+
+export default service;
diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts
index 1c0eb9530d8b..22785abf596a 100644
--- a/packages/integrations/vercel/src/serverless/adapter.ts
+++ b/packages/integrations/vercel/src/serverless/adapter.ts
@@ -12,6 +12,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
import {
getAstroImageConfig,
getDefaultImageConfig,
+ type DevImageService,
type VercelImageConfig,
} from '../image/shared.js';
import { exposeEnv } from '../lib/env.js';
@@ -68,6 +69,7 @@ export interface VercelServerlessConfig {
analytics?: boolean;
imageService?: boolean;
imagesConfig?: VercelImageConfig;
+ devImageService?: DevImageService;
edgeMiddleware?: boolean;
functionPerRoute?: boolean;
}
@@ -78,6 +80,7 @@ export default function vercelServerless({
analytics,
imageService,
imagesConfig,
+ devImageService = 'sharp',
functionPerRoute = true,
edgeMiddleware = false,
}: VercelServerlessConfig = {}): AstroIntegration {
@@ -147,7 +150,13 @@ export default function vercelServerless({
external: ['@vercel/nft'],
},
},
- ...getAstroImageConfig(imageService, imagesConfig, command, config.image),
+ ...getAstroImageConfig(
+ imageService,
+ imagesConfig,
+ command,
+ devImageService,
+ config.image
+ ),
});
},
'astro:config:done': ({ setAdapter, config, logger }) => {
diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts
index 2908dbf58529..27bc2fe2d27d 100644
--- a/packages/integrations/vercel/src/static/adapter.ts
+++ b/packages/integrations/vercel/src/static/adapter.ts
@@ -3,6 +3,7 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
import {
getAstroImageConfig,
getDefaultImageConfig,
+ type DevImageService,
type VercelImageConfig,
} from '../image/shared.js';
import { exposeEnv } from '../lib/env.js';
@@ -36,12 +37,14 @@ export interface VercelStaticConfig {
analytics?: boolean;
imageService?: boolean;
imagesConfig?: VercelImageConfig;
+ devImageService?: DevImageService;
}
export default function vercelStatic({
analytics,
imageService,
imagesConfig,
+ devImageService = 'sharp',
}: VercelStaticConfig = {}): AstroIntegration {
let _config: AstroConfig;
@@ -63,7 +66,13 @@ export default function vercelStatic({
vite: {
define: viteDefine,
},
- ...getAstroImageConfig(imageService, imagesConfig, command, config.image),
+ ...getAstroImageConfig(
+ imageService,
+ imagesConfig,
+ command,
+ devImageService,
+ config.image
+ ),
});
},
'astro:config:done': ({ setAdapter, config }) => {
diff --git a/packages/integrations/vercel/test/fixtures/image/package.json b/packages/integrations/vercel/test/fixtures/image/package.json
index ea9d554f5d9c..87fefe2e019e 100644
--- a/packages/integrations/vercel/test/fixtures/image/package.json
+++ b/packages/integrations/vercel/test/fixtures/image/package.json
@@ -2,6 +2,9 @@
"name": "@test/astro-vercel-image",
"version": "0.0.0",
"private": true,
+ "scripts": {
+ "dev": "astro dev"
+ },
"dependencies": {
"@astrojs/vercel": "workspace:*",
"astro": "workspace:*"
diff --git a/packages/integrations/vercel/test/fixtures/image/src/assets/penguin.svg b/packages/integrations/vercel/test/fixtures/image/src/assets/penguin.svg
new file mode 100644
index 000000000000..341a0522f2bc
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/image/src/assets/penguin.svg
@@ -0,0 +1,183 @@
+
+
+
+
diff --git a/packages/integrations/vercel/test/fixtures/image/src/pages/index.astro b/packages/integrations/vercel/test/fixtures/image/src/pages/index.astro
index 0a154874fa07..db7c22eebc64 100644
--- a/packages/integrations/vercel/test/fixtures/image/src/pages/index.astro
+++ b/packages/integrations/vercel/test/fixtures/image/src/pages/index.astro
@@ -1,6 +1,13 @@
---
import { Image } from "astro:assets";
import astro from "../assets/astro.jpeg";
+import penguin from "../assets/penguin.svg";
---
-
+
+
+
+
+
+
+
diff --git a/packages/integrations/vercel/test/image.test.js b/packages/integrations/vercel/test/image.test.js
index c5153cc6e622..b8bc3af95bd8 100644
--- a/packages/integrations/vercel/test/image.test.js
+++ b/packages/integrations/vercel/test/image.test.js
@@ -20,7 +20,7 @@ describe('Image', () => {
it('has link to vercel in build with proper attributes', async () => {
const html = await fixture.readFile('../.vercel/output/static/index.html');
const $ = cheerio.load(html);
- const img = $('img');
+ const img = $('#basic-image img');
expect(img.attr('src').startsWith('/_vercel/image?url=_astr')).to.be.true;
expect(img.attr('loading')).to.equal('lazy');
@@ -56,11 +56,22 @@ describe('Image', () => {
it('has link to local image in dev with proper attributes', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
- const img = $('img');
+ const img = $('#basic-image img');
expect(img.attr('src').startsWith('/_image?href=')).to.be.true;
expect(img.attr('loading')).to.equal('lazy');
expect(img.attr('width')).to.equal('225');
});
+
+ it('supports SVGs', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
+ const $ = cheerio.load(html);
+ const img = $('#svg img');
+ const src = img.attr('src');
+
+ const res = await fixture.fetch(src);
+ expect(res.status).to.equal(200);
+ expect(res.headers.get('content-type')).to.equal('image/svg+xml');
+ });
});
});