From 6063f5657392a74b6ffc4d5e0de5463c217a8563 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Tue, 2 May 2023 09:42:48 +0200 Subject: [PATCH] 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 * docs(vercel): Add Added In mentions --------- Co-authored-by: Sarah Rainsberger --- .changeset/wise-geckos-applaud.md | 5 + packages/integrations/vercel/README.md | 59 ++++++- packages/integrations/vercel/package.json | 5 +- .../integrations/vercel/src/edge/adapter.ts | 17 +- .../vercel/src/image/build-service.ts | 60 +++++++ .../vercel/src/image/dev-service.ts | 57 +++++++ .../integrations/vercel/src/image/shared.ts | 151 ++++++++++++++++++ .../vercel/src/serverless/adapter.ts | 17 +- .../integrations/vercel/src/static/adapter.ts | 21 ++- .../test/fixtures/image/astro.config.mjs | 9 ++ .../vercel/test/fixtures/image/package.json | 9 ++ .../test/fixtures/image/src/assets/astro.jpeg | Bin 0 -> 3992 bytes .../test/fixtures/image/src/pages/index.astro | 6 + .../integrations/vercel/test/image.test.js | 60 +++++++ .../vercel/test/serverless-prerender.test.js | 4 +- pnpm-lock.yaml | 12 ++ 16 files changed, 484 insertions(+), 8 deletions(-) create mode 100644 .changeset/wise-geckos-applaud.md create mode 100644 packages/integrations/vercel/src/image/build-service.ts create mode 100644 packages/integrations/vercel/src/image/dev-service.ts create mode 100644 packages/integrations/vercel/src/image/shared.ts create mode 100644 packages/integrations/vercel/test/fixtures/image/astro.config.mjs create mode 100644 packages/integrations/vercel/test/fixtures/image/package.json create mode 100644 packages/integrations/vercel/test/fixtures/image/src/assets/astro.jpeg create mode 100644 packages/integrations/vercel/test/fixtures/image/src/pages/index.astro create mode 100644 packages/integrations/vercel/test/image.test.js diff --git a/.changeset/wise-geckos-applaud.md b/.changeset/wise-geckos-applaud.md new file mode 100644 index 000000000000..eae1e3e821ba --- /dev/null +++ b/.changeset/wise-geckos-applaud.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vercel': minor +--- + +Add support for using the Vercel Image Optimization API through `astro:assets` diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index b20131f6719e..0af5632b5f55 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -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`
+**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`
+**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"; +--- + + +My super logo! + + +My super logo! +``` + ### includeFiles **Type:** `string[]`
diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index cee787e41d2e..621ae1da5577 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -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" } } diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index a2e937987e26..3570f5b61d97 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -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 } + : {}), }); }, }, diff --git a/packages/integrations/vercel/src/image/build-service.ts b/packages/integrations/vercel/src/image/build-service.ts new file mode 100644 index 000000000000..23cd664a2cbe --- /dev/null +++ b/packages/integrations/vercel/src/image/build-service.ts @@ -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; diff --git a/packages/integrations/vercel/src/image/dev-service.ts b/packages/integrations/vercel/src/image/dev-service.ts new file mode 100644 index 000000000000..04df9932a504 --- /dev/null +++ b/packages/integrations/vercel/src/image/dev-service.ts @@ -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; diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts new file mode 100644 index 000000000000..0b6db2037805 --- /dev/null +++ b/packages/integrations/vercel/src/image/shared.ts @@ -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 = { + 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, + 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; +} diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 24b9c735b45b..47d164519934 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -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 } + : {}), }); }, }, diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index 5455edbc0a7e..2aa4891333e7 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -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 } + : {}), }); }, }, diff --git a/packages/integrations/vercel/test/fixtures/image/astro.config.mjs b/packages/integrations/vercel/test/fixtures/image/astro.config.mjs new file mode 100644 index 000000000000..a38be5065f8e --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/image/astro.config.mjs @@ -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 + } +}); diff --git a/packages/integrations/vercel/test/fixtures/image/package.json b/packages/integrations/vercel/test/fixtures/image/package.json new file mode 100644 index 000000000000..ea9d554f5d9c --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/image/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-vercel-image", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/vercel": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/vercel/test/fixtures/image/src/assets/astro.jpeg b/packages/integrations/vercel/test/fixtures/image/src/assets/astro.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1443ee4b4f5d658d54e705ce4468b9dc73651c3f GIT binary patch literal 3992 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 diff --git a/packages/integrations/vercel/test/image.test.js b/packages/integrations/vercel/test/image.test.js new file mode 100644 index 000000000000..834b6d69b181 --- /dev/null +++ b/packages/integrations/vercel/test/image.test.js @@ -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'); + }); + }); +}); diff --git a/packages/integrations/vercel/test/serverless-prerender.test.js b/packages/integrations/vercel/test/serverless-prerender.test.js index 4cada43a7b77..491c6d0bdbbd 100644 --- a/packages/integrations/vercel/test/serverless-prerender.test.js +++ b/packages/integrations/vercel/test/serverless-prerender.test.js @@ -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; }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 225ce6c09503..bf43c28474cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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':