feat(vercel): Add support for image optimization API (#6845)
* feat(vercel): Add support for image optimization API * chore: changeset * feat: implement image service * feat: dev service * feat: full local service * fix: move assets check to astro:config:done * feat: update with new settings * fix: remove unused param * test: add tsets * fix: rename to imageService * docs: add docs * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * docs(vercel): Add Added In mentions --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
980246f148
commit
6063f56573
16 changed files with 484 additions and 8 deletions
5
.changeset/wise-geckos-applaud.md
Normal file
5
.changeset/wise-geckos-applaud.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/vercel': minor
|
||||
---
|
||||
|
||||
Add support for using the Vercel Image Optimization API through `astro:assets`
|
|
@ -14,7 +14,7 @@ Learn how to deploy your Astro site in our [Vercel deployment guide](https://doc
|
|||
|
||||
## Why Astro Vercel
|
||||
|
||||
If you're using Astro as a static site builder — its behavior out of the box — you don't need an adapter.
|
||||
If you're using Astro as a static site builder — its behavior out of the box — you don't need an adapter.
|
||||
|
||||
If you wish to [use server-side rendering (SSR)](https://docs.astro.build/en/guides/server-side-rendering/), Astro requires an adapter that matches your deployment runtime.
|
||||
|
||||
|
@ -108,6 +108,63 @@ export default defineConfig({
|
|||
});
|
||||
```
|
||||
|
||||
### imageConfig
|
||||
|
||||
**Type:** `VercelImageConfig`<br>
|
||||
**Available for:** Edge, Serverless, Static
|
||||
**Added in:** `@astrojs/vercel@3.3.0`
|
||||
|
||||
Configuration options for [Vercel's Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [Vercel's image configuration documentation](https://vercel.com/docs/build-output-api/v3/configuration#images) for a complete list of supported parameters.
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import vercel from '@astrojs/vercel/static';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: vercel({
|
||||
imageConfig: {
|
||||
sizes: [320, 640, 1280]
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### imageService
|
||||
|
||||
**Type:** `boolean`<br>
|
||||
**Available for:** Edge, 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.
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
import vercel from '@astrojs/vercel/static';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: vercel({
|
||||
imageService: true
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```astro
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
import astroLogo from "../assets/logo.png";
|
||||
---
|
||||
|
||||
<!-- This component -->
|
||||
<Image src={astroLogo} alt="My super logo!" />
|
||||
|
||||
<!-- will become the following HTML -->
|
||||
<img src="/_vercel/image?url=_astro/logo.hash.png&w=...&q=..." alt="My super logo!" loading="lazy" decoding="async" width="..." height="..." />
|
||||
```
|
||||
|
||||
### includeFiles
|
||||
|
||||
**Type:** `string[]`<br>
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
"./serverless/entrypoint": "./dist/serverless/entrypoint.js",
|
||||
"./static": "./dist/static/adapter.js",
|
||||
"./analytics": "./dist/analytics.js",
|
||||
"./build-image-service": "./dist/image/build-service.js",
|
||||
"./dev-image-service": "./dist/image/dev-service.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
|
@ -60,6 +62,7 @@
|
|||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"chai": "^4.3.6",
|
||||
"mocha": "^9.2.2"
|
||||
"mocha": "^9.2.2",
|
||||
"cheerio": "^1.0.0-rc.11"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,12 @@ import esbuild from 'esbuild';
|
|||
import { relative as relativePath } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
defaultImageConfig,
|
||||
getImageConfig,
|
||||
throwIfAssetsNotEnabled,
|
||||
type VercelImageConfig,
|
||||
} from '../image/shared.js';
|
||||
import {
|
||||
copyFilesToFunction,
|
||||
getFilesFromFolder,
|
||||
|
@ -26,11 +32,15 @@ function getAdapter(): AstroAdapter {
|
|||
export interface VercelEdgeConfig {
|
||||
includeFiles?: string[];
|
||||
analytics?: boolean;
|
||||
imageService?: boolean;
|
||||
imagesConfig?: VercelImageConfig;
|
||||
}
|
||||
|
||||
export default function vercelEdge({
|
||||
includeFiles = [],
|
||||
analytics,
|
||||
imageService,
|
||||
imagesConfig,
|
||||
}: VercelEdgeConfig = {}): AstroIntegration {
|
||||
let _config: AstroConfig;
|
||||
let buildTempFolder: URL;
|
||||
|
@ -52,9 +62,11 @@ export default function vercelEdge({
|
|||
client: new URL('./static/', outDir),
|
||||
server: new URL('./dist/', config.root),
|
||||
},
|
||||
...getImageConfig(imageService, imagesConfig, command),
|
||||
});
|
||||
},
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
throwIfAssetsNotEnabled(config, imageService);
|
||||
setAdapter(getAdapter());
|
||||
_config = config;
|
||||
buildTempFolder = config.build.server;
|
||||
|
@ -64,7 +76,7 @@ export default function vercelEdge({
|
|||
if (config.output === 'static') {
|
||||
throw new Error(`
|
||||
[@astrojs/vercel] \`output: "server"\` is required to use the edge adapter.
|
||||
|
||||
|
||||
`);
|
||||
}
|
||||
},
|
||||
|
@ -135,6 +147,9 @@ export default function vercelEdge({
|
|||
{ handle: 'filesystem' },
|
||||
{ src: '/.*', dest: 'render' },
|
||||
],
|
||||
...(imageService || imagesConfig
|
||||
? { images: imagesConfig ? imagesConfig : defaultImageConfig }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
60
packages/integrations/vercel/src/image/build-service.ts
Normal file
60
packages/integrations/vercel/src/image/build-service.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import type { ExternalImageService } from 'astro';
|
||||
import { isESMImportedImage, sharedValidateOptions } from './shared';
|
||||
|
||||
const service: ExternalImageService = {
|
||||
validateOptions: (options, serviceOptions) =>
|
||||
sharedValidateOptions(options, serviceOptions, 'production'),
|
||||
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;
|
||||
}
|
||||
|
||||
let targetWidth = props.width;
|
||||
let targetHeight = props.height;
|
||||
if (isESMImportedImage(props.src)) {
|
||||
const aspectRatio = props.src.width / props.src.height;
|
||||
if (targetHeight && !targetWidth) {
|
||||
// If we have a height but no width, use height to calculate the width
|
||||
targetWidth = Math.round(targetHeight * aspectRatio);
|
||||
} else if (targetWidth && !targetHeight) {
|
||||
// If we have a width but no height, use width to calculate the height
|
||||
targetHeight = Math.round(targetWidth / aspectRatio);
|
||||
} else if (!targetWidth && !targetHeight) {
|
||||
// If we have neither width or height, use the original image's dimensions
|
||||
targetWidth = props.src.width;
|
||||
targetHeight = props.src.height;
|
||||
}
|
||||
}
|
||||
|
||||
const { src, width, height, format, quality, ...attributes } = props;
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
loading: attributes.loading ?? 'lazy',
|
||||
decoding: attributes.decoding ?? 'async',
|
||||
};
|
||||
},
|
||||
getURL(options, serviceOptions) {
|
||||
const fileSrc =
|
||||
typeof options.src === 'string' ? options.src : removeLeadingForwardSlash(options.src.src);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('url', fileSrc);
|
||||
|
||||
options.width && searchParams.append('w', options.width.toString());
|
||||
options.quality && searchParams.append('q', options.quality.toString());
|
||||
|
||||
return '/_vercel/image?' + searchParams;
|
||||
},
|
||||
};
|
||||
|
||||
function removeLeadingForwardSlash(path: string) {
|
||||
return path.startsWith('/') ? path.substring(1) : path;
|
||||
}
|
||||
|
||||
export default service;
|
57
packages/integrations/vercel/src/image/dev-service.ts
Normal file
57
packages/integrations/vercel/src/image/dev-service.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import type { LocalImageService } from 'astro';
|
||||
// @ts-expect-error
|
||||
import squooshService from 'astro/assets/services/squoosh';
|
||||
import { sharedValidateOptions } from './shared';
|
||||
|
||||
const service: LocalImageService = {
|
||||
validateOptions: (options, serviceOptions) =>
|
||||
sharedValidateOptions(options, serviceOptions, 'development'),
|
||||
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(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';
|
||||
|
||||
// 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;
|
151
packages/integrations/vercel/src/image/shared.ts
Normal file
151
packages/integrations/vercel/src/image/shared.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
import type { AstroConfig, ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro';
|
||||
|
||||
export const defaultImageConfig: VercelImageConfig = {
|
||||
sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||
domains: [],
|
||||
};
|
||||
|
||||
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
|
||||
return typeof src === 'object';
|
||||
}
|
||||
// https://vercel.com/docs/build-output-api/v3/configuration#images
|
||||
type ImageFormat = 'image/avif' | 'image/webp';
|
||||
|
||||
type RemotePattern = {
|
||||
protocol?: 'http' | 'https';
|
||||
hostname: string;
|
||||
port?: string;
|
||||
pathname?: string;
|
||||
};
|
||||
|
||||
export type VercelImageConfig = {
|
||||
/**
|
||||
* Supported image widths.
|
||||
*/
|
||||
sizes: number[];
|
||||
/**
|
||||
* Allowed external domains that can use Image Optimization. Leave empty for only allowing the deployment domain to use Image Optimization.
|
||||
*/
|
||||
domains: string[];
|
||||
/**
|
||||
* Allowed external patterns that can use Image Optimization. Similar to `domains` but provides more control with RegExp.
|
||||
*/
|
||||
remotePatterns?: RemotePattern[];
|
||||
/**
|
||||
* Cache duration (in seconds) for the optimized images.
|
||||
*/
|
||||
minimumCacheTTL?: number;
|
||||
/**
|
||||
* Supported output image formats
|
||||
*/
|
||||
formats?: ImageFormat[];
|
||||
/**
|
||||
* Allow SVG input image URLs. This is disabled by default for security purposes.
|
||||
*/
|
||||
dangerouslyAllowSVG?: boolean;
|
||||
/**
|
||||
* Change the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) of the optimized images.
|
||||
*/
|
||||
contentSecurityPolicy?: string;
|
||||
};
|
||||
|
||||
export const qualityTable: Record<ImageQualityPreset, number> = {
|
||||
low: 25,
|
||||
mid: 50,
|
||||
high: 80,
|
||||
max: 100,
|
||||
};
|
||||
|
||||
// TODO: Remove once Astro 3.0 is out and `experimental.assets` is no longer needed
|
||||
export function throwIfAssetsNotEnabled(config: AstroConfig, imageService: boolean | undefined) {
|
||||
if (!config.experimental.assets && imageService) {
|
||||
throw new Error(
|
||||
`Using the Vercel Image Optimization-powered image service requires \`experimental.assets\` to be enabled. See https://docs.astro.build/en/guides/assets/ for more information.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getImageConfig(
|
||||
images: boolean | undefined,
|
||||
imagesConfig: VercelImageConfig | undefined,
|
||||
command: string
|
||||
) {
|
||||
if (images) {
|
||||
return {
|
||||
image: {
|
||||
service: {
|
||||
entrypoint:
|
||||
command === 'dev'
|
||||
? '@astrojs/vercel/dev-image-service'
|
||||
: '@astrojs/vercel/build-image-service',
|
||||
config: imagesConfig ? imagesConfig : defaultImageConfig,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function sharedValidateOptions(
|
||||
options: ImageTransform,
|
||||
serviceOptions: Record<string, any>,
|
||||
mode: 'development' | 'production'
|
||||
) {
|
||||
const vercelImageOptions = serviceOptions as VercelImageConfig;
|
||||
|
||||
if (
|
||||
mode === 'development' &&
|
||||
(!vercelImageOptions.sizes || vercelImageOptions.sizes.length === 0)
|
||||
) {
|
||||
throw new Error('Vercel Image Optimization requires at least one size to be configured.');
|
||||
}
|
||||
|
||||
const configuredWidths = vercelImageOptions.sizes.sort((a, b) => a - b);
|
||||
|
||||
// The logic for finding the perfect width is a bit confusing, here it goes:
|
||||
// For images where no width has been specified:
|
||||
// - For local, imported images, fallback to nearest width we can find in our configured
|
||||
// - For remote images, that's an error, width is always required.
|
||||
// For images where a width has been specified:
|
||||
// - If the width that the user asked for isn't in `sizes`, then fallback to the nearest one, but save the width
|
||||
// the user asked for so we can put it on the `img` tag later.
|
||||
// - Otherwise, just use as-is.
|
||||
// The end goal is:
|
||||
// - The size on the page is always the one the user asked for or the base image's size
|
||||
// - The actual size of the image file is always one of `sizes`, either the one the user asked for or the nearest to it
|
||||
if (!options.width) {
|
||||
const src = options.src;
|
||||
if (isESMImportedImage(src)) {
|
||||
const nearestWidth = configuredWidths.reduce((prev, curr) => {
|
||||
return Math.abs(curr - src.width) < Math.abs(prev - src.width) ? curr : prev;
|
||||
});
|
||||
|
||||
// Use the image's base width to inform the `width` and `height` on the `img` tag
|
||||
options.inputtedWidth = src.width;
|
||||
options.width = nearestWidth;
|
||||
} else {
|
||||
throw new Error(`Missing \`width\` parameter for remote image ${options.src}`);
|
||||
}
|
||||
} else {
|
||||
if (!configuredWidths.includes(options.width)) {
|
||||
const nearestWidth = configuredWidths.reduce((prev, curr) => {
|
||||
return Math.abs(curr - options.width!) < Math.abs(prev - options.width!) ? curr : prev;
|
||||
});
|
||||
|
||||
// Save the width the user asked for to inform the `width` and `height` on the `img` tag
|
||||
options.inputtedWidth = options.width;
|
||||
options.width = nearestWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.quality && typeof options.quality === 'string') {
|
||||
options.quality = options.quality in qualityTable ? qualityTable[options.quality] : undefined;
|
||||
}
|
||||
|
||||
if (!options.quality) {
|
||||
options.quality = 100;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
|
@ -2,6 +2,12 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
|
|||
|
||||
import glob from 'fast-glob';
|
||||
import { pathToFileURL } from 'url';
|
||||
import {
|
||||
defaultImageConfig,
|
||||
getImageConfig,
|
||||
throwIfAssetsNotEnabled,
|
||||
type VercelImageConfig,
|
||||
} from '../image/shared.js';
|
||||
import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js';
|
||||
import { copyDependenciesToFunction } from '../lib/nft.js';
|
||||
import { getRedirects } from '../lib/redirects.js';
|
||||
|
@ -20,12 +26,16 @@ export interface VercelServerlessConfig {
|
|||
includeFiles?: string[];
|
||||
excludeFiles?: string[];
|
||||
analytics?: boolean;
|
||||
imageService?: boolean;
|
||||
imagesConfig?: VercelImageConfig;
|
||||
}
|
||||
|
||||
export default function vercelServerless({
|
||||
includeFiles,
|
||||
excludeFiles,
|
||||
analytics,
|
||||
imageService,
|
||||
imagesConfig,
|
||||
}: VercelServerlessConfig = {}): AstroIntegration {
|
||||
let _config: AstroConfig;
|
||||
let buildTempFolder: URL;
|
||||
|
@ -47,9 +57,11 @@ export default function vercelServerless({
|
|||
client: new URL('./static/', outDir),
|
||||
server: new URL('./dist/', config.root),
|
||||
},
|
||||
...getImageConfig(imageService, imagesConfig, command),
|
||||
});
|
||||
},
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
throwIfAssetsNotEnabled(config, imageService);
|
||||
setAdapter(getAdapter());
|
||||
_config = config;
|
||||
buildTempFolder = config.build.server;
|
||||
|
@ -59,7 +71,7 @@ export default function vercelServerless({
|
|||
if (config.output === 'static') {
|
||||
throw new Error(`
|
||||
[@astrojs/vercel] \`output: "server"\` is required to use the serverless adapter.
|
||||
|
||||
|
||||
`);
|
||||
}
|
||||
},
|
||||
|
@ -115,6 +127,9 @@ export default function vercelServerless({
|
|||
{ handle: 'filesystem' },
|
||||
{ src: '/.*', dest: 'render' },
|
||||
],
|
||||
...(imageService || imagesConfig
|
||||
? { images: imagesConfig ? imagesConfig : defaultImageConfig }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
|
||||
|
||||
import {
|
||||
defaultImageConfig,
|
||||
getImageConfig,
|
||||
throwIfAssetsNotEnabled,
|
||||
type VercelImageConfig,
|
||||
} from '../image/shared.js';
|
||||
import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js';
|
||||
import { getRedirects } from '../lib/redirects.js';
|
||||
|
||||
|
@ -11,15 +17,21 @@ function getAdapter(): AstroAdapter {
|
|||
|
||||
export interface VercelStaticConfig {
|
||||
analytics?: boolean;
|
||||
imageService?: boolean;
|
||||
imagesConfig?: VercelImageConfig;
|
||||
}
|
||||
|
||||
export default function vercelStatic({ analytics }: VercelStaticConfig = {}): AstroIntegration {
|
||||
export default function vercelStatic({
|
||||
analytics,
|
||||
imageService,
|
||||
imagesConfig,
|
||||
}: VercelStaticConfig = {}): AstroIntegration {
|
||||
let _config: AstroConfig;
|
||||
|
||||
return {
|
||||
name: '@astrojs/vercel',
|
||||
hooks: {
|
||||
'astro:config:setup': ({ command, config, updateConfig, injectScript }) => {
|
||||
'astro:config:setup': ({ command, config, injectScript, updateConfig }) => {
|
||||
if (command === 'build' && analytics) {
|
||||
injectScript('page', 'import "@astrojs/vercel/analytics"');
|
||||
}
|
||||
|
@ -29,9 +41,11 @@ export default function vercelStatic({ analytics }: VercelStaticConfig = {}): As
|
|||
build: {
|
||||
format: 'directory',
|
||||
},
|
||||
...getImageConfig(imageService, imagesConfig, command),
|
||||
});
|
||||
},
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
throwIfAssetsNotEnabled(config, imageService);
|
||||
setAdapter(getAdapter());
|
||||
_config = config;
|
||||
|
||||
|
@ -51,6 +65,9 @@ export default function vercelStatic({ analytics }: VercelStaticConfig = {}): As
|
|||
await writeJson(new URL(`./config.json`, getVercelOutput(_config.root)), {
|
||||
version: 3,
|
||||
routes: [...getRedirects(routes, _config), { handle: 'filesystem' }],
|
||||
...(imageService || imagesConfig
|
||||
? { images: imagesConfig ? imagesConfig : defaultImageConfig }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
9
packages/integrations/vercel/test/fixtures/image/astro.config.mjs
vendored
Normal file
9
packages/integrations/vercel/test/fixtures/image/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
import vercel from '@astrojs/vercel/static';
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
adapter: vercel({imageService: true}),
|
||||
experimental: {
|
||||
assets: true
|
||||
}
|
||||
});
|
9
packages/integrations/vercel/test/fixtures/image/package.json
vendored
Normal file
9
packages/integrations/vercel/test/fixtures/image/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/astro-vercel-image",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/vercel": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
BIN
packages/integrations/vercel/test/fixtures/image/src/assets/astro.jpeg
vendored
Normal file
BIN
packages/integrations/vercel/test/fixtures/image/src/assets/astro.jpeg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
6
packages/integrations/vercel/test/fixtures/image/src/pages/index.astro
vendored
Normal file
6
packages/integrations/vercel/test/fixtures/image/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import astro from "../assets/astro.jpeg";
|
||||
---
|
||||
|
||||
<Image src={astro} alt="Astro" />
|
60
packages/integrations/vercel/test/image.test.js
Normal file
60
packages/integrations/vercel/test/image.test.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Image', () => {
|
||||
/** @type {import('../../../astro/test/test-utils.js').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/image/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('build successful', async () => {
|
||||
expect(await fixture.readFile('../.vercel/output/static/index.html')).to.be.ok;
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
expect(img.attr('src').startsWith('/_vercel/image?url=_astr')).to.be.true;
|
||||
expect(img.attr('loading')).to.equal('lazy');
|
||||
expect(img.attr('width')).to.equal('225');
|
||||
});
|
||||
|
||||
it('has proper vercel config', async () => {
|
||||
const vercelConfig = JSON.parse(await fixture.readFile('../.vercel/output/config.json'));
|
||||
|
||||
expect(vercelConfig.images).to.deep.equal({
|
||||
sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||
domains: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
expect(img.attr('src').startsWith('/_image?href=')).to.be.true;
|
||||
expect(img.attr('loading')).to.equal('lazy');
|
||||
expect(img.attr('width')).to.equal('225');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { loadFixture } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Serverless prerender', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
|
@ -13,6 +13,6 @@ describe('Serverless prerender', () => {
|
|||
|
||||
it('build successful', async () => {
|
||||
await fixture.build();
|
||||
expect(fixture.readFile('/static/index.html')).to.be.ok;
|
||||
expect(await fixture.readFile('../.vercel/output/static/index.html')).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4639,10 +4639,22 @@ importers:
|
|||
chai:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
cheerio:
|
||||
specifier: ^1.0.0-rc.11
|
||||
version: 1.0.0-rc.11
|
||||
mocha:
|
||||
specifier: ^9.2.2
|
||||
version: 9.2.2
|
||||
|
||||
packages/integrations/vercel/test/fixtures/image:
|
||||
dependencies:
|
||||
'@astrojs/vercel':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/vercel/test/fixtures/no-output:
|
||||
dependencies:
|
||||
'@astrojs/vercel':
|
||||
|
|
Loading…
Reference in a new issue