From 91380378cef545656d2c085117fc5f38c9ce4589 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Wed, 13 Sep 2023 18:40:02 +0200 Subject: [PATCH] feat(vercel): Use Sharp in dev instead of Squoosh by default (#8445) * feat(vercel): Use Sharp in dev instead of Squoosh by default * fix(build): * nit: adjust with feedback * fix: imports * Update packages/integrations/vercel/README.md Co-authored-by: Sarah Rainsberger * docs: small change in other part of the README --------- Co-authored-by: Sarah Rainsberger --- .changeset/cold-flies-clean.md | 5 + packages/integrations/vercel/README.md | 26 ++- packages/integrations/vercel/package.json | 1 + .../vercel/src/image/build-service.ts | 5 +- .../vercel/src/image/dev-service.ts | 43 +--- .../vercel/src/image/shared-dev-service.ts | 33 ++++ .../integrations/vercel/src/image/shared.ts | 28 ++- .../vercel/src/image/squoosh-dev-service.ts | 31 +++ .../vercel/src/serverless/adapter.ts | 11 +- .../integrations/vercel/src/static/adapter.ts | 11 +- .../vercel/test/fixtures/image/package.json | 3 + .../fixtures/image/src/assets/penguin.svg | 183 ++++++++++++++++++ .../test/fixtures/image/src/pages/index.astro | 9 +- .../integrations/vercel/test/image.test.js | 15 +- 14 files changed, 357 insertions(+), 47 deletions(-) create mode 100644 .changeset/cold-flies-clean.md create mode 100644 packages/integrations/vercel/src/image/shared-dev-service.ts create mode 100644 packages/integrations/vercel/src/image/squoosh-dev-service.ts create mode 100644 packages/integrations/vercel/test/fixtures/image/src/assets/penguin.svg diff --git a/.changeset/cold-flies-clean.md b/.changeset/cold-flies-clean.md new file mode 100644 index 000000000..6c6a345e6 --- /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 00c2a18cf..db3a52a03 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 b9ac5aaa0..34bbb269b 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 0e45167d4..bd58d3af6 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 a335c8d23..c9702cff9 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 000000000..4251603a7 --- /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 f6cace2a2..079186e18 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 000000000..d3b05bb11 --- /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 1c0eb9530..22785abf5 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 2908dbf58..27bc2fe2d 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 ea9d554f5..87fefe2e0 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 000000000..341a0522f --- /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 0a154874f..db7c22eeb 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"; --- -Astro +
+ Astro +
+ +
+ Astro +
diff --git a/packages/integrations/vercel/test/image.test.js b/packages/integrations/vercel/test/image.test.js index c5153cc6e..b8bc3af95 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'); + }); }); });