Skip to content

Commit

Permalink
feat(vercel): Use Sharp in dev instead of Squoosh by default (#8445)
Browse files Browse the repository at this point in the history
* 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 <sarah@rainsberger.ca>

* docs: small change in other part of the README

---------

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
Princesseuh and sarah11918 authored Sep 13, 2023
1 parent 6c6f1ae commit 9138037
Show file tree
Hide file tree
Showing 14 changed files with 357 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-flies-clean.md
Original file line number Diff line number Diff line change
@@ -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`.
26 changes: 25 additions & 1 deletion packages/integrations/vercel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -172,6 +172,30 @@ import astroLogo from '../assets/logo.png';
/>
```

### devImageService

**Type:** `'sharp' | 'squoosh' | string`<br>
**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[]`<br>
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/vercel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 3 additions & 2 deletions packages/integrations/vercel/src/image/build-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
43 changes: 8 additions & 35 deletions packages/integrations/vercel/src/image/dev-service.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
},
};

Expand Down
33 changes: 33 additions & 0 deletions packages/integrations/vercel/src/image/shared-dev-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { LocalImageService } from 'astro';
import { sharedValidateOptions } from './shared.js';

export const baseDevService: Omit<LocalImageService, 'transform'> = {
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;
},
};
28 changes: 24 additions & 4 deletions packages/integrations/vercel/src/image/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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),
},
},
Expand Down
31 changes: 31 additions & 0 deletions packages/integrations/vercel/src/image/squoosh-dev-service.ts
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 10 additions & 1 deletion packages/integrations/vercel/src/serverless/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,6 +69,7 @@ export interface VercelServerlessConfig {
analytics?: boolean;
imageService?: boolean;
imagesConfig?: VercelImageConfig;
devImageService?: DevImageService;
edgeMiddleware?: boolean;
functionPerRoute?: boolean;
}
Expand All @@ -78,6 +80,7 @@ export default function vercelServerless({
analytics,
imageService,
imagesConfig,
devImageService = 'sharp',
functionPerRoute = true,
edgeMiddleware = false,
}: VercelServerlessConfig = {}): AstroIntegration {
Expand Down Expand Up @@ -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 }) => {
Expand Down
11 changes: 10 additions & 1 deletion packages/integrations/vercel/src/static/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand All @@ -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 }) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/integrations/vercel/test/fixtures/image/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*"
Expand Down
Loading

0 comments on commit 9138037

Please sign in to comment.