From 3e40c984bf447db30e4d2477cca7475ef589953f Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 14 Apr 2023 13:35:23 +0200 Subject: [PATCH 01/13] feat(vercel): Add support for image optimization API --- .../integrations/vercel/src/edge/adapter.ts | 8 +++++- packages/integrations/vercel/src/image.ts | 28 +++++++++++++++++++ .../vercel/src/serverless/adapter.ts | 9 +++++- .../integrations/vercel/src/static/adapter.ts | 11 +++++++- 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 packages/integrations/vercel/src/image.ts diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index 411717a4145e..334440b0a460 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -4,6 +4,7 @@ import esbuild from 'esbuild'; import { relative as relativePath } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image.js'; import { copyFilesToFunction, getFilesFromFolder, @@ -26,11 +27,13 @@ function getAdapter(): AstroAdapter { export interface VercelEdgeConfig { includeFiles?: string[]; analytics?: boolean; + images?: VercelImageConfig; } export default function vercelEdge({ includeFiles = [], analytics, + images, }: VercelEdgeConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; @@ -41,6 +44,8 @@ export default function vercelEdge({ name: PACKAGE_NAME, hooks: { 'astro:config:setup': ({ command, config, updateConfig, injectScript }) => { + throwIfAssetsNotEnabled(config, images); + if (command === 'build' && analytics) { injectScript('page', 'import "@astrojs/vercel/analytics"'); } @@ -64,7 +69,7 @@ export default function vercelEdge({ if (config.output === 'static') { throw new Error(` [@astrojs/vercel] \`output: "server"\` is required to use the edge adapter. - + `); } }, @@ -128,6 +133,7 @@ export default function vercelEdge({ { handle: 'filesystem' }, { src: '/.*', dest: 'render' }, ], + ...(images ? { images: images } : {}), }); }, }, diff --git a/packages/integrations/vercel/src/image.ts b/packages/integrations/vercel/src/image.ts new file mode 100644 index 000000000000..39c8f7d60339 --- /dev/null +++ b/packages/integrations/vercel/src/image.ts @@ -0,0 +1,28 @@ +import type { AstroConfig } from 'astro'; + +export interface VercelImageConfig { + sizes: number[]; + domains: string[]; + remotePatterns?: { + protocol?: 'http' | 'https'; + hostname: string; + port?: string; + pathname?: string; + }[]; + minimumCacheTTL?: number; + formats?: ('image/avif' | 'image/webp')[]; + dangerouslyAllowSVG?: boolean; + contentSecurityPolicy?: string; +} + +// TODO: Remove once Astro 3.0 is out and `experimental.assets` is no longer needed +export function throwIfAssetsNotEnabled( + config: AstroConfig, + imageConfig: VercelImageConfig | undefined +) { + if (!config.experimental.assets && imageConfig) { + throw new Error( + `Using the Vercel Image Optimization API requires \`experimental.assets\` to be enabled. See https://docs.astro.build/en/guides/assets/ for more information.` + ); + } +} diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 24b9c735b45b..c5f94f7a1886 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -2,6 +2,7 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; import glob from 'fast-glob'; import { pathToFileURL } from 'url'; +import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image.js'; import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js'; import { copyDependenciesToFunction } from '../lib/nft.js'; import { getRedirects } from '../lib/redirects.js'; @@ -20,12 +21,14 @@ export interface VercelServerlessConfig { includeFiles?: string[]; excludeFiles?: string[]; analytics?: boolean; + images?: VercelImageConfig; } export default function vercelServerless({ includeFiles, excludeFiles, analytics, + images, }: VercelServerlessConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; @@ -36,9 +39,12 @@ export default function vercelServerless({ name: PACKAGE_NAME, hooks: { 'astro:config:setup': ({ command, config, updateConfig, injectScript }) => { + throwIfAssetsNotEnabled(config, images); + if (command === 'build' && analytics) { injectScript('page', 'import "@astrojs/vercel/analytics"'); } + const outDir = getVercelOutput(config.root); updateConfig({ outDir, @@ -59,7 +65,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 +121,7 @@ export default function vercelServerless({ { handle: 'filesystem' }, { src: '/.*', dest: 'render' }, ], + ...(images ? { images: images } : {}), }); }, }, diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index c03c78218b40..42d1830654a5 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -1,5 +1,6 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; +import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image.js'; import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js'; import { getRedirects } from '../lib/redirects.js'; @@ -11,18 +12,25 @@ function getAdapter(): AstroAdapter { export interface VercelStaticConfig { analytics?: boolean; + images?: VercelImageConfig; } -export default function vercelStatic({ analytics }: VercelStaticConfig = {}): AstroIntegration { +export default function vercelStatic({ + analytics, + images, +}: VercelStaticConfig = {}): AstroIntegration { let _config: AstroConfig; return { name: '@astrojs/vercel', hooks: { 'astro:config:setup': ({ command, config, injectScript }) => { + throwIfAssetsNotEnabled(config, images); + if (command === 'build' && analytics) { injectScript('page', 'import "@astrojs/vercel/analytics"'); } + config.outDir = new URL('./static/', getVercelOutput(config.root)); config.build.format = 'directory'; }, @@ -46,6 +54,7 @@ export default function vercelStatic({ analytics }: VercelStaticConfig = {}): As await writeJson(new URL(`./config.json`, getVercelOutput(_config.root)), { version: 3, routes: [...getRedirects(routes, _config), { handle: 'filesystem' }], + ...(images ? { images: images } : {}), }); }, }, From 88fc5d3f90f16d4fb9ec1969925183fb492ac536 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 14 Apr 2023 13:36:15 +0200 Subject: [PATCH 02/13] chore: changeset --- .changeset/wise-geckos-applaud.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wise-geckos-applaud.md 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` From 546c6ce062e75e3b8f9d4b577fd7cb3beb2fd1cc Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 14 Apr 2023 17:22:07 +0200 Subject: [PATCH 03/13] feat: implement image service --- packages/integrations/vercel/package.json | 1 + .../integrations/vercel/src/edge/adapter.ts | 6 +- .../integrations/vercel/src/image-service.ts | 125 ++++++++++++++++++ packages/integrations/vercel/src/image.ts | 28 ---- .../vercel/src/serverless/adapter.ts | 6 +- .../integrations/vercel/src/static/adapter.ts | 16 ++- 6 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 packages/integrations/vercel/src/image-service.ts delete mode 100644 packages/integrations/vercel/src/image.ts diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index 1ea60ef237c6..de65089df51b 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -23,6 +23,7 @@ "./serverless/entrypoint": "./dist/serverless/entrypoint.js", "./static": "./dist/static/adapter.js", "./analytics": "./dist/analytics.js", + "./image-service": "./dist/image-service.js", "./package.json": "./package.json" }, "typesVersions": { diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index 334440b0a460..fc74ce3ad6ee 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -4,7 +4,7 @@ import esbuild from 'esbuild'; import { relative as relativePath } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image.js'; +import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image-service.js'; import { copyFilesToFunction, getFilesFromFolder, @@ -57,6 +57,10 @@ export default function vercelEdge({ client: new URL('./static/', outDir), server: new URL('./dist/', config.root), }, + image: { + service: '@astrojs/vercel/image-service', + serviceConfig: images ?? {}, + }, }); }, 'astro:config:done': ({ setAdapter, config }) => { diff --git a/packages/integrations/vercel/src/image-service.ts b/packages/integrations/vercel/src/image-service.ts new file mode 100644 index 000000000000..e3e60fe4aebe --- /dev/null +++ b/packages/integrations/vercel/src/image-service.ts @@ -0,0 +1,125 @@ +import type { AstroConfig, ExternalImageService, ImageQualityPreset, ImageTransform } from 'astro'; + +// 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; +}; + +const qualityTable: Record = { + low: 25, + mid: 50, + high: 80, + max: 100, +}; + +const mode = (import.meta as any).env.MODE as 'development' | 'production'; + +const service: ExternalImageService = { + validateOptions(options, serviceOptions) { + const vercelImageOptions = serviceOptions as VercelImageConfig; + + if ( + mode === 'development' && + (!vercelImageOptions.sizes || vercelImageOptions.sizes.length === 0) + ) { + console.warn('Vercel Image Optimization requires at least one size to be configured.'); + } + + const configuredWidths = vercelImageOptions.sizes.sort((a, b) => a - b); + const largestWidth = configuredWidths.at(-1); + + if (!options.width) { + options.width = largestWidth; + } 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; + }); + + options.width = nearestWidth; + + if (mode === 'development') { + console.warn( + `Width ${options.width} is currently missing from your Vercel Image Optimization configuration. Falling back to ${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; + }, + getURL: function (options: ImageTransform): string { + 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; + }, +}; + +// TODO: Remove once Astro 3.0 is out and `experimental.assets` is no longer needed +export function throwIfAssetsNotEnabled( + config: AstroConfig, + imageConfig: VercelImageConfig | undefined +) { + if (!config.experimental.assets && imageConfig) { + throw new Error( + `Using the Vercel Image Optimization API requires \`experimental.assets\` to be enabled. See https://docs.astro.build/en/guides/assets/ for more information.` + ); + } +} + +function removeLeadingForwardSlash(path: string) { + return path.startsWith('/') ? path.substring(1) : path; +} + +export default service; diff --git a/packages/integrations/vercel/src/image.ts b/packages/integrations/vercel/src/image.ts deleted file mode 100644 index 39c8f7d60339..000000000000 --- a/packages/integrations/vercel/src/image.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AstroConfig } from 'astro'; - -export interface VercelImageConfig { - sizes: number[]; - domains: string[]; - remotePatterns?: { - protocol?: 'http' | 'https'; - hostname: string; - port?: string; - pathname?: string; - }[]; - minimumCacheTTL?: number; - formats?: ('image/avif' | 'image/webp')[]; - dangerouslyAllowSVG?: boolean; - contentSecurityPolicy?: string; -} - -// TODO: Remove once Astro 3.0 is out and `experimental.assets` is no longer needed -export function throwIfAssetsNotEnabled( - config: AstroConfig, - imageConfig: VercelImageConfig | undefined -) { - if (!config.experimental.assets && imageConfig) { - throw new Error( - `Using the Vercel Image Optimization API requires \`experimental.assets\` to be enabled. See https://docs.astro.build/en/guides/assets/ for more information.` - ); - } -} diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index c5f94f7a1886..62482afa0b3b 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -2,7 +2,7 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; import glob from 'fast-glob'; import { pathToFileURL } from 'url'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image.js'; +import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image-service.js'; import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js'; import { copyDependenciesToFunction } from '../lib/nft.js'; import { getRedirects } from '../lib/redirects.js'; @@ -53,6 +53,10 @@ export default function vercelServerless({ client: new URL('./static/', outDir), server: new URL('./dist/', config.root), }, + image: { + service: '@astrojs/vercel/image-service', + serviceConfig: images ?? {}, + }, }); }, 'astro:config:done': ({ setAdapter, config }) => { diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index 42d1830654a5..60f5128a4b8f 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -1,6 +1,6 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image.js'; +import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image-service.js'; import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js'; import { getRedirects } from '../lib/redirects.js'; @@ -24,15 +24,23 @@ export default function vercelStatic({ return { name: '@astrojs/vercel', hooks: { - 'astro:config:setup': ({ command, config, injectScript }) => { + 'astro:config:setup': ({ command, config, injectScript, updateConfig }) => { throwIfAssetsNotEnabled(config, images); if (command === 'build' && analytics) { injectScript('page', 'import "@astrojs/vercel/analytics"'); } - config.outDir = new URL('./static/', getVercelOutput(config.root)); - config.build.format = 'directory'; + updateConfig({ + outdir: new URL('./static/', getVercelOutput(config.root)), + build: { + format: 'directory', + }, + image: { + service: '@astrojs/vercel/image-service', + serviceConfig: images ?? {}, + }, + }); }, 'astro:config:done': ({ setAdapter, config }) => { setAdapter(getAdapter()); From cacabf3596cb5f7abbb3e27b9a23ea232466d5f7 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Tue, 25 Apr 2023 16:09:38 +0200 Subject: [PATCH 04/13] feat: dev service --- packages/integrations/vercel/package.json | 3 +- .../integrations/vercel/src/edge/adapter.ts | 2 +- .../integrations/vercel/src/image-service.ts | 125 ------------------ .../vercel/src/image/build-service.ts | 24 ++++ .../vercel/src/image/dev-service.ts | 34 +++++ .../integrations/vercel/src/image/shared.ts | 107 +++++++++++++++ .../vercel/src/serverless/adapter.ts | 2 +- .../integrations/vercel/src/static/adapter.ts | 2 +- 8 files changed, 170 insertions(+), 129 deletions(-) delete mode 100644 packages/integrations/vercel/src/image-service.ts 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 diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index de65089df51b..d81770f24b05 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -23,7 +23,8 @@ "./serverless/entrypoint": "./dist/serverless/entrypoint.js", "./static": "./dist/static/adapter.js", "./analytics": "./dist/analytics.js", - "./image-service": "./dist/image-service.js", + "./build-image-service": "./dist/image/build-service.js", + "./dev-image-service": "./dist/image/dev-service.js", "./package.json": "./package.json" }, "typesVersions": { diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index fc74ce3ad6ee..088affb9c4ec 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -4,7 +4,7 @@ import esbuild from 'esbuild'; import { relative as relativePath } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image-service.js'; +import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image/shared.js'; import { copyFilesToFunction, getFilesFromFolder, diff --git a/packages/integrations/vercel/src/image-service.ts b/packages/integrations/vercel/src/image-service.ts deleted file mode 100644 index e3e60fe4aebe..000000000000 --- a/packages/integrations/vercel/src/image-service.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { AstroConfig, ExternalImageService, ImageQualityPreset, ImageTransform } from 'astro'; - -// 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; -}; - -const qualityTable: Record = { - low: 25, - mid: 50, - high: 80, - max: 100, -}; - -const mode = (import.meta as any).env.MODE as 'development' | 'production'; - -const service: ExternalImageService = { - validateOptions(options, serviceOptions) { - const vercelImageOptions = serviceOptions as VercelImageConfig; - - if ( - mode === 'development' && - (!vercelImageOptions.sizes || vercelImageOptions.sizes.length === 0) - ) { - console.warn('Vercel Image Optimization requires at least one size to be configured.'); - } - - const configuredWidths = vercelImageOptions.sizes.sort((a, b) => a - b); - const largestWidth = configuredWidths.at(-1); - - if (!options.width) { - options.width = largestWidth; - } 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; - }); - - options.width = nearestWidth; - - if (mode === 'development') { - console.warn( - `Width ${options.width} is currently missing from your Vercel Image Optimization configuration. Falling back to ${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; - }, - getURL: function (options: ImageTransform): string { - 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; - }, -}; - -// TODO: Remove once Astro 3.0 is out and `experimental.assets` is no longer needed -export function throwIfAssetsNotEnabled( - config: AstroConfig, - imageConfig: VercelImageConfig | undefined -) { - if (!config.experimental.assets && imageConfig) { - throw new Error( - `Using the Vercel Image Optimization API requires \`experimental.assets\` to be enabled. See https://docs.astro.build/en/guides/assets/ for more information.` - ); - } -} - -function removeLeadingForwardSlash(path: string) { - return path.startsWith('/') ? path.substring(1) : path; -} - -export default service; 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..286e47f45181 --- /dev/null +++ b/packages/integrations/vercel/src/image/build-service.ts @@ -0,0 +1,24 @@ +import type { ExternalImageService, ImageTransform } from 'astro'; +import { sharedValidateOptions } from './shared'; + +const service: ExternalImageService = { + validateOptions: (options) => sharedValidateOptions(options, {}, 'production'), + getURL: function (options: ImageTransform): string { + 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..afb6788e2a87 --- /dev/null +++ b/packages/integrations/vercel/src/image/dev-service.ts @@ -0,0 +1,34 @@ +import type { LocalImageService } from 'astro'; +import { sharedValidateOptions } from './shared'; + +const service: LocalImageService = { + validateOptions: (options) => sharedValidateOptions(options, {}, 'development'), + getURL(options) { + const fileSrc = typeof options.src === 'string' ? options.src : 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 '/_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; + }, +}; + +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..9c9e20e252e8 --- /dev/null +++ b/packages/integrations/vercel/src/image/shared.ts @@ -0,0 +1,107 @@ +import type { AstroConfig, ImageQualityPreset, ImageTransform } from 'astro'; + +// 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, + imageConfig: VercelImageConfig | undefined +) { + if (!config.experimental.assets && imageConfig) { + throw new Error( + `Using the Vercel Image Optimization API requires \`experimental.assets\` to be enabled. See https://docs.astro.build/en/guides/assets/ for more information.` + ); + } +} + +export function sharedValidateOptions( + options: ImageTransform, + serviceOptions: Record, + mode: 'development' | 'production' +) { + const vercelImageOptions = serviceOptions as VercelImageConfig; + + if ( + mode === 'development' && + (!vercelImageOptions.sizes || vercelImageOptions.sizes.length === 0) + ) { + console.warn('Vercel Image Optimization requires at least one size to be configured.'); + } + + const configuredWidths = vercelImageOptions.sizes.sort((a, b) => a - b); + const largestWidth = configuredWidths.at(-1); + + if (!options.width) { + options.width = largestWidth; + } 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; + }); + + options.width = nearestWidth; + + if (mode === 'development') { + console.warn( + `Width ${options.width} is currently missing from your Vercel Image Optimization configuration. Falling back to ${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 62482afa0b3b..d35267b805a0 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -2,7 +2,7 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; import glob from 'fast-glob'; import { pathToFileURL } from 'url'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image-service.js'; +import { 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'; diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index 60f5128a4b8f..ebd875de4b2a 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -1,6 +1,6 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image-service.js'; +import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image/shared.js'; import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js'; import { getRedirects } from '../lib/redirects.js'; From b5ce043a0462010a9dd8f3d72c56d2fa37167869 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Thu, 27 Apr 2023 16:05:19 +0200 Subject: [PATCH 05/13] feat: full local service --- .../integrations/vercel/src/edge/adapter.ts | 15 ++-- .../vercel/src/image/build-service.ts | 41 ++++++++++- .../vercel/src/image/dev-service.ts | 33 ++++++++- .../integrations/vercel/src/image/shared.ts | 69 +++++++++++++++---- .../vercel/src/serverless/adapter.ts | 15 ++-- .../integrations/vercel/src/static/adapter.ts | 11 ++- 6 files changed, 145 insertions(+), 39 deletions(-) diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index 088affb9c4ec..5daf0788c909 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -4,7 +4,11 @@ import esbuild from 'esbuild'; import { relative as relativePath } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image/shared.js'; +import { + getImageConfig, + throwIfAssetsNotEnabled, + type VercelImageConfig, +} from '../image/shared.js'; import { copyFilesToFunction, getFilesFromFolder, @@ -27,13 +31,15 @@ function getAdapter(): AstroAdapter { export interface VercelEdgeConfig { includeFiles?: string[]; analytics?: boolean; - images?: VercelImageConfig; + images?: boolean; + imagesConfig?: VercelImageConfig; } export default function vercelEdge({ includeFiles = [], analytics, images, + imagesConfig, }: VercelEdgeConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; @@ -57,10 +63,7 @@ export default function vercelEdge({ client: new URL('./static/', outDir), server: new URL('./dist/', config.root), }, - image: { - service: '@astrojs/vercel/image-service', - serviceConfig: images ?? {}, - }, + ...getImageConfig(images, imagesConfig, command), }); }, 'astro:config:done': ({ setAdapter, config }) => { diff --git a/packages/integrations/vercel/src/image/build-service.ts b/packages/integrations/vercel/src/image/build-service.ts index 286e47f45181..1569cdfea359 100644 --- a/packages/integrations/vercel/src/image/build-service.ts +++ b/packages/integrations/vercel/src/image/build-service.ts @@ -1,9 +1,44 @@ -import type { ExternalImageService, ImageTransform } from 'astro'; -import { sharedValidateOptions } from './shared'; +import type { ExternalImageService } from 'astro'; +import { isESMImportedImage, sharedValidateOptions } from './shared'; const service: ExternalImageService = { validateOptions: (options) => sharedValidateOptions(options, {}, 'production'), - getURL: function (options: ImageTransform): string { + getHTMLAttributes(options) { + 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) { const fileSrc = typeof options.src === 'string' ? options.src : removeLeadingForwardSlash(options.src.src); diff --git a/packages/integrations/vercel/src/image/dev-service.ts b/packages/integrations/vercel/src/image/dev-service.ts index afb6788e2a87..62e0205a6d19 100644 --- a/packages/integrations/vercel/src/image/dev-service.ts +++ b/packages/integrations/vercel/src/image/dev-service.ts @@ -1,18 +1,35 @@ import type { LocalImageService } from 'astro'; +// @ts-expect-error +import squooshService from 'astro/assets/services/squoosh'; import { sharedValidateOptions } from './shared'; const service: LocalImageService = { - validateOptions: (options) => sharedValidateOptions(options, {}, 'development'), + validateOptions: (options) => + sharedValidateOptions( + options, + { sizes: [640, 750, 828, 1080, 1200], domains: [] }, + 'development' + ), + getHTMLAttributes(options) { + 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); + }, getURL(options) { const fileSrc = typeof options.src === 'string' ? options.src : options.src.src; const searchParams = new URLSearchParams(); - searchParams.append('url', fileSrc); + searchParams.append('href', fileSrc); options.width && searchParams.append('w', options.width.toString()); options.quality && searchParams.append('q', options.quality.toString()); - return '/_image' + searchParams; + return '/_image?' + searchParams; }, parseURL(url) { const params = url.searchParams; @@ -29,6 +46,16 @@ const service: LocalImageService = { return transform; }, + transform(inputBuffer, transform) { + // 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); + }, }; export default service; diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts index 9c9e20e252e8..02bbdea92609 100644 --- a/packages/integrations/vercel/src/image/shared.ts +++ b/packages/integrations/vercel/src/image/shared.ts @@ -1,5 +1,8 @@ -import type { AstroConfig, ImageQualityPreset, ImageTransform } from 'astro'; +import type { AstroConfig, ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro'; +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'; @@ -49,17 +52,36 @@ export const qualityTable: Record = { }; // TODO: Remove once Astro 3.0 is out and `experimental.assets` is no longer needed -export function throwIfAssetsNotEnabled( - config: AstroConfig, - imageConfig: VercelImageConfig | undefined -) { - if (!config.experimental.assets && imageConfig) { +export function throwIfAssetsNotEnabled(config: AstroConfig, images: boolean | undefined) { + if (!config.experimental.assets && images) { throw new Error( `Using the Vercel Image Optimization API 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: + command === 'dev' + ? '@astrojs/vercel/dev-image-service' + : '@astrojs/vercel/build-image-service', + serviceConfig: imagesConfig ?? { + sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + }, + }, + }; + } + + return {}; +} + export function sharedValidateOptions( options: ImageTransform, serviceOptions: Record, @@ -71,27 +93,44 @@ export function sharedValidateOptions( mode === 'development' && (!vercelImageOptions.sizes || vercelImageOptions.sizes.length === 0) ) { - console.warn('Vercel Image Optimization requires at least one size to be configured.'); + throw new Error('Vercel Image Optimization requires at least one size to be configured.'); } const configuredWidths = vercelImageOptions.sizes.sort((a, b) => a - b); - const largestWidth = configuredWidths.at(-1); + // 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) { - options.width = largestWidth; + 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 (mode === 'development') { - console.warn( - `Width ${options.width} is currently missing from your Vercel Image Optimization configuration. Falling back to ${nearestWidth}.}` - ); - } } } diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index d35267b805a0..2b473a877d87 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -2,7 +2,11 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; import glob from 'fast-glob'; import { pathToFileURL } from 'url'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image/shared.js'; +import { + 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'; @@ -21,7 +25,8 @@ export interface VercelServerlessConfig { includeFiles?: string[]; excludeFiles?: string[]; analytics?: boolean; - images?: VercelImageConfig; + images?: boolean; + imagesConfig?: VercelImageConfig; } export default function vercelServerless({ @@ -29,6 +34,7 @@ export default function vercelServerless({ excludeFiles, analytics, images, + imagesConfig, }: VercelServerlessConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; @@ -53,10 +59,7 @@ export default function vercelServerless({ client: new URL('./static/', outDir), server: new URL('./dist/', config.root), }, - image: { - service: '@astrojs/vercel/image-service', - serviceConfig: images ?? {}, - }, + ...getImageConfig(images, imagesConfig, command), }); }, 'astro:config:done': ({ setAdapter, config }) => { diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index ebd875de4b2a..d5327dcd3f4f 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -1,6 +1,6 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; -import { throwIfAssetsNotEnabled, type VercelImageConfig } from '../image/shared.js'; +import { getImageConfig, throwIfAssetsNotEnabled, type VercelImageConfig } from '../image/shared.js'; import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js'; import { getRedirects } from '../lib/redirects.js'; @@ -12,12 +12,14 @@ function getAdapter(): AstroAdapter { export interface VercelStaticConfig { analytics?: boolean; - images?: VercelImageConfig; + images?: boolean; + imagesConfig?: VercelImageConfig; } export default function vercelStatic({ analytics, images, + imagesConfig, }: VercelStaticConfig = {}): AstroIntegration { let _config: AstroConfig; @@ -36,10 +38,7 @@ export default function vercelStatic({ build: { format: 'directory', }, - image: { - service: '@astrojs/vercel/image-service', - serviceConfig: images ?? {}, - }, + ...getImageConfig(images, imagesConfig, command), }); }, 'astro:config:done': ({ setAdapter, config }) => { From c168e4447ea4f671a28469acd6746535771c910a Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Thu, 27 Apr 2023 17:15:30 +0200 Subject: [PATCH 06/13] fix: move assets check to astro:config:done --- packages/integrations/vercel/src/edge/adapter.ts | 3 +-- .../integrations/vercel/src/serverless/adapter.ts | 4 +--- packages/integrations/vercel/src/static/adapter.ts | 12 +++++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index c27aca463ab1..bfdd99e60b70 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -50,8 +50,6 @@ export default function vercelEdge({ name: PACKAGE_NAME, hooks: { 'astro:config:setup': ({ command, config, updateConfig, injectScript }) => { - throwIfAssetsNotEnabled(config, images); - if (command === 'build' && analytics) { injectScript('page', 'import "@astrojs/vercel/analytics"'); } @@ -67,6 +65,7 @@ export default function vercelEdge({ }); }, 'astro:config:done': ({ setAdapter, config }) => { + throwIfAssetsNotEnabled(config, images); setAdapter(getAdapter()); _config = config; buildTempFolder = config.build.server; diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 2b473a877d87..cf0a1943c42d 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -45,12 +45,9 @@ export default function vercelServerless({ name: PACKAGE_NAME, hooks: { 'astro:config:setup': ({ command, config, updateConfig, injectScript }) => { - throwIfAssetsNotEnabled(config, images); - if (command === 'build' && analytics) { injectScript('page', 'import "@astrojs/vercel/analytics"'); } - const outDir = getVercelOutput(config.root); updateConfig({ outDir, @@ -63,6 +60,7 @@ export default function vercelServerless({ }); }, 'astro:config:done': ({ setAdapter, config }) => { + throwIfAssetsNotEnabled(config, images); setAdapter(getAdapter()); _config = config; buildTempFolder = config.build.server; diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index b3545bb37505..fa30312455b6 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -1,6 +1,10 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; -import { getImageConfig, throwIfAssetsNotEnabled, type VercelImageConfig } from '../image/shared.js'; +import { + getImageConfig, + throwIfAssetsNotEnabled, + type VercelImageConfig, +} from '../image/shared.js'; import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js'; import { getRedirects } from '../lib/redirects.js'; @@ -27,13 +31,10 @@ export default function vercelStatic({ name: '@astrojs/vercel', hooks: { 'astro:config:setup': ({ command, config, injectScript, updateConfig }) => { - throwIfAssetsNotEnabled(config, images); - if (command === 'build' && analytics) { injectScript('page', 'import "@astrojs/vercel/analytics"'); } - - const outDir = new URL('./static/', getVercelOutput(config.root)); + const outDir = new URL('./static/', getVercelOutput(config.root)); updateConfig({ outDir, build: { @@ -43,6 +44,7 @@ export default function vercelStatic({ }); }, 'astro:config:done': ({ setAdapter, config }) => { + throwIfAssetsNotEnabled(config, images); setAdapter(getAdapter()); _config = config; From 690197291f415ac44ac71a1ab266efe17d31f87c Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 28 Apr 2023 13:16:10 +0200 Subject: [PATCH 07/13] feat: update with new settings --- .../vercel/src/image/build-service.ts | 7 ++++--- .../vercel/src/image/dev-service.ts | 18 +++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/integrations/vercel/src/image/build-service.ts b/packages/integrations/vercel/src/image/build-service.ts index 1569cdfea359..23cd664a2cbe 100644 --- a/packages/integrations/vercel/src/image/build-service.ts +++ b/packages/integrations/vercel/src/image/build-service.ts @@ -2,8 +2,9 @@ import type { ExternalImageService } from 'astro'; import { isESMImportedImage, sharedValidateOptions } from './shared'; const service: ExternalImageService = { - validateOptions: (options) => sharedValidateOptions(options, {}, 'production'), - getHTMLAttributes(options) { + 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 @@ -38,7 +39,7 @@ const service: ExternalImageService = { decoding: attributes.decoding ?? 'async', }; }, - getURL(options) { + getURL(options, serviceOptions) { const fileSrc = typeof options.src === 'string' ? options.src : removeLeadingForwardSlash(options.src.src); diff --git a/packages/integrations/vercel/src/image/dev-service.ts b/packages/integrations/vercel/src/image/dev-service.ts index 62e0205a6d19..ee43096664ba 100644 --- a/packages/integrations/vercel/src/image/dev-service.ts +++ b/packages/integrations/vercel/src/image/dev-service.ts @@ -4,13 +4,9 @@ import squooshService from 'astro/assets/services/squoosh'; import { sharedValidateOptions } from './shared'; const service: LocalImageService = { - validateOptions: (options) => - sharedValidateOptions( - options, - { sizes: [640, 750, 828, 1080, 1200], domains: [] }, - 'development' - ), - getHTMLAttributes(options) { + 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 @@ -18,9 +14,9 @@ const service: LocalImageService = { props.width = inputtedWidth; } - return squooshService.getHTMLAttributes(props); + return squooshService.getHTMLAttributes(props, serviceOptions); }, - getURL(options) { + getURL(options, serviceOptions) { const fileSrc = typeof options.src === 'string' ? options.src : options.src.src; const searchParams = new URLSearchParams(); @@ -46,7 +42,7 @@ const service: LocalImageService = { return transform; }, - transform(inputBuffer, 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 @@ -54,7 +50,7 @@ const service: LocalImageService = { 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); + return squooshService.transform(inputBuffer, transform, serviceOptions); }, }; From 91a5eaad8912b9aef4a404e8a7fa0da1c1080dc2 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 28 Apr 2023 13:16:42 +0200 Subject: [PATCH 08/13] fix: remove unused param --- packages/integrations/vercel/src/image/dev-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/vercel/src/image/dev-service.ts b/packages/integrations/vercel/src/image/dev-service.ts index ee43096664ba..04df9932a504 100644 --- a/packages/integrations/vercel/src/image/dev-service.ts +++ b/packages/integrations/vercel/src/image/dev-service.ts @@ -16,7 +16,7 @@ const service: LocalImageService = { return squooshService.getHTMLAttributes(props, serviceOptions); }, - getURL(options, serviceOptions) { + getURL(options) { const fileSrc = typeof options.src === 'string' ? options.src : options.src.src; const searchParams = new URLSearchParams(); From 2278a863d5a598dc3ad7d1a47cf09dd4b4266f60 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 28 Apr 2023 15:47:19 +0200 Subject: [PATCH 09/13] test: add tsets --- packages/integrations/vercel/package.json | 3 +- .../integrations/vercel/src/edge/adapter.ts | 3 +- .../integrations/vercel/src/image/shared.ts | 17 +++-- .../vercel/src/serverless/adapter.ts | 3 +- .../integrations/vercel/src/static/adapter.ts | 3 +- .../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 | 63 ++++++++++++++++++ .../vercel/test/serverless-prerender.test.js | 4 +- pnpm-lock.yaml | 12 ++++ 12 files changed, 120 insertions(+), 12 deletions(-) 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/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index ec4acc3c993b..fe9ba8106d45 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -62,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 bfdd99e60b70..82866081555a 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -5,6 +5,7 @@ import { relative as relativePath } from 'node:path'; import { fileURLToPath } from 'node:url'; import { + defaultImageConfig, getImageConfig, throwIfAssetsNotEnabled, type VercelImageConfig, @@ -146,7 +147,7 @@ export default function vercelEdge({ { handle: 'filesystem' }, { src: '/.*', dest: 'render' }, ], - ...(images ? { images: images } : {}), + ...(images ? { images: imagesConfig ? imagesConfig : defaultImageConfig } : {}), }); }, }, diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts index 02bbdea92609..d7b3e3de1b58 100644 --- a/packages/integrations/vercel/src/image/shared.ts +++ b/packages/integrations/vercel/src/image/shared.ts @@ -1,5 +1,10 @@ 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'; } @@ -68,12 +73,12 @@ export function getImageConfig( if (images) { return { image: { - service: - command === 'dev' - ? '@astrojs/vercel/dev-image-service' - : '@astrojs/vercel/build-image-service', - serviceConfig: imagesConfig ?? { - sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + service: { + entrypoint: + command === 'dev' + ? '@astrojs/vercel/dev-image-service' + : '@astrojs/vercel/build-image-service', + config: imagesConfig ? imagesConfig : defaultImageConfig, }, }, }; diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index cf0a1943c42d..1916916fbec2 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -3,6 +3,7 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; import glob from 'fast-glob'; import { pathToFileURL } from 'url'; import { + defaultImageConfig, getImageConfig, throwIfAssetsNotEnabled, type VercelImageConfig, @@ -126,7 +127,7 @@ export default function vercelServerless({ { handle: 'filesystem' }, { src: '/.*', dest: 'render' }, ], - ...(images ? { images: images } : {}), + ...(images ? { images: imagesConfig ? imagesConfig : defaultImageConfig } : {}), }); }, }, diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index fa30312455b6..8bf7d4cfac47 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -1,6 +1,7 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; import { + defaultImageConfig, getImageConfig, throwIfAssetsNotEnabled, type VercelImageConfig, @@ -64,7 +65,7 @@ export default function vercelStatic({ await writeJson(new URL(`./config.json`, getVercelOutput(_config.root)), { version: 3, routes: [...getRedirects(routes, _config), { handle: 'filesystem' }], - ...(images ? { images: images } : {}), + ...(images ? { 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..794d568ec0c4 --- /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({images: 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 zcmai12UOF^*8hiILHIN7hAs`T%7&_zy3;Xtb=e+OcoVmZ5duQf%@5$VmKWmWn2@t+$WNZY0 zKmY(@{{R*h2nYCic!mCt9M~NZ1B;7`fyKlm51*8kls+LNCMKtT^n{|aii(N^L_-^< zqID9g0tE>O2nY%77e8=7Oc??>qP#0mD3skOtNa&Mg|bK4W8W;h=v!yki}eNo^8sVP z7$*n}aDYLaU=XVbIK(E%!NJL9xhEiAZXQl9J`T3kK$vX=fj9)YdAK>bzj=T-IJv;w zB0RjJM~|tQ-Sqxf{6Y0!GcwPa``mv@K>mSh==M!`D<%i~?E!nfG&=|XTpR##ad7f* zvu$c%c5*m5IJx=Q9^dz8uMOrp%B}XRh^Se7hBh<*M|`7d)0S)f{?b^e=MD*ZF36t0Br zIRIw?;DYdO_ku8Y(i;QbZl$9sDR!PoMS+FIwq$`F5$~CV;CtJ~S#QSm&eh;OZE+|Z zv#o?$j{EJ-6CNE~B}Eg0C09MFC9PWFPvm5GN^p=E}}JQmOi{}hn4iJWw{Z4ta95gU)dj`s@qWJqWiW@ZScPJK+Y)-0{A z`08h8M1MV=mgJnW^qK0#Xc~8MGV+@Z`&jwpbYj9z z@rK&^t>>2|h=YW|IJ!Z6{ZKCRYTglL*GaM685}hb1 zC3o^Zm;Zskjqkd9#f17pH)d1TlrRla1kGcM+OpCjNrF&#W!oEXZc@2@O_m2p$d6NLM8jp^LojvkuRR)66-21%m$4mBnfq%IVH5fXF>V-O5XA#;*q6eLQ6~YO}ps>3}n@d4jkCaGz+z zu>d!UjmYJzeFJ)WN85MuB>p3UWdxq-G z+)0)8N7Pt=zKx|+MN!V8S{jJwz zR7n?Niv1xE?dK@2ONqTw$qzw_ zn$%lXRL{Dq(QCRfsHb1}B|=}_{XdqghPA}^l&pG8k>k4p%0bFe6 zVZe8hSMx!unVKh<#aP>AuRmpqs>!_n%Jcs1)tWw3OJQn;P>`+NLdVQjr}*{|(Wh{# zm~K?iPN!*51<~{&N-xRCo|KByvk+h7Ke4-b4NC- z{593qbgMMrm|zcVr|~TfQFsrS=hghtl+rgqaC zcUb`Xrk+Ri5*=Sb)fpgpDSmaDtO#-Ocb>4XMtBdXlSGq06nb3v5yr*uS|~b97f!(> zNtfI-dL_#E98`~toLbTy9!V{gqINAdOpGV>dC!CaiaRjax@VSB?|@`~Ly3XRLY@NC zFa%jVCLSamLblC=t6Zr>tC=A6jo$3*;w;~O<~lw&5|8n*!g$$qPt7CpCXi&xf*-S& zEER`kxX1J=w~zO|0W7xM7&|7Kwc)SNX!sG&TyRbAp?&s0B)y7m?}on%R#>s}=p1x& zc}e-f`bTLK4>(ALV{kFKujW#7A@>EX$kEvJ#L2Z`1{sk*Ew#MOv?l2`Y0(Pbn&9nsN`Ksy7fgxQ;O!i1RthV~R*?F_Wa)JcO z80||vz~Uss{S)`<+u;{wi-aYL^D>kin1Dlbrkz0Qy!hiH*q3q zFqbczI#wr&3B|D=Oi5iBlSX6r^~=a`2L}3B3f%%VD}>DxQ(P{w{SnP~1u4=Qm_~B&CT%%_GTkh@2w#uM;hNoxsq$) zt7bLW;;EiB--g;VH%8-P;|0Bl7?SdUo7F`1$d}1gfBR6;!LeF{uMWBNHu+;(qb*sU zuNBTDTSl0cvG10M<}Q8$VTsY)R;Qs&nKF4^s3KxwiBE+dPn+6o`B!s=mX}>LwxE+* zVV~*jE66LM89l(%vP93}y;IkZaAS%xz3pKT@s!9XUT^OovbnSLx=1N?K>VC#Yv?c7 zn6{|NsLcE+nRxBHtExvVs7|oenKJ(mOGXIz;DRe&Iam1!Qp!HEb6)Ov*tcA8T=}M8 zhrkU_j6(FBdkSBDS!u1vlAfK`pISs@*R}=DmU}fL4^6qQb+)_X&KKL4zZ2hYK*$PG zY2MNzUJQ+8uFLywgd7N&WU_#mp!N0O#cuK3yA>q`jU74hg%9K3MGR-^Lv^R1*K;Gv zUBahJmY?)(?u;_4puIxq%_)!S5**{%NSP0gWb?V#@bo*yn0qo1p0m5Wg00W|x^T`s z{Fz1ON5|T3QLtKMOzZ>a+f+A{>#Gn4L58CWa%Q#8N${g8!gp;vm>THw^`_=DcA8Mr z(o}tce(q{{Ae32>SUrv{>bU#dAnX1}RuGaBr-)8N9!ianyvi(IaLx5D!?n@;6zmdb zeN8aN8sxt8Nqq+~dnAg8S37O0rnhyo?BiVI=Ya!=m*0{FaLEE9>;iRGw6v?VmXG6X zm?o6)RxjorT769F8lr}I%{^+s!qf~=aAGBBJIG!LE?n(Hx5{&iOpXhs$5@MMyXUN& z6#@1Sv=5Q#g9R;ktFDrKrXi-4d1Bn16gZq36rdm+nf%17$fzmotmyFKZaF;5e&_(Q z|NhPZ0IB4Ck_|lIbb5(EnenqAKN{DT8QrijIph{qma* zxQ?~Vj^`%|DkXDkTf*ntlnZrUS}&oZMz?Emj{}UYH8zhi^)QUkO0dE052SS7gW9Ee z4p`5UQ0SHD^(q&=z&b3gL!Ysxe$~#NK}q$sb|5!UPse4OE8Ed@QmgU~ZpkfRVVK;m zhzL_XF+WTDE7zTC@>P$Z+FxOa2^4%m%IeFMl*#2C$As5ph(8q-H|7&~c&;WnGdZ2l z_KB(J>98M>{jIo%-aucJVZ3z7d6$BpH{(mReO0f{*TZ7QvCadz_533Sp|~Pz$pUKE zn0(i}@~8Dm`8G?B@TTzS^Q-qdG%IwXD^~I|oxum&zdX7ao#80wGYjcO zjCe8@Dak4#i$xMHh6NuINhLSqczc7NT=x&Chni3fAOT1?O){@#HBHGi1-a?#F`6D! zw~pS(;GVZUKonyEa8Z1(Tgmiih8e`5i~t5pKGJ*n^N$1oTrc=J0CqPF*lm9?IO${l z+*5TV(XC2(yvHYoq!CxpE)V_-85sVE;}drRv56Il)@@0s6`Sraq0)QC)%Hx+`T6uYxwA9F4_biH~=- diff --git a/packages/integrations/vercel/test/image.test.js b/packages/integrations/vercel/test/image.test.js new file mode 100644 index 000000000000..fe92573e8a5a --- /dev/null +++ b/packages/integrations/vercel/test/image.test.js @@ -0,0 +1,63 @@ +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/', + experimental: { + assets: true, + }, + }); + 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 df1b6d1e70a8..afd56be87f16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4624,10 +4624,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': From 856b524af95a94035531358a41d49f1856730cf7 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 28 Apr 2023 16:18:56 +0200 Subject: [PATCH 10/13] fix: rename to imageService --- packages/integrations/vercel/src/edge/adapter.ts | 12 +++++++----- packages/integrations/vercel/src/image/shared.ts | 6 +++--- .../integrations/vercel/src/serverless/adapter.ts | 12 +++++++----- packages/integrations/vercel/src/static/adapter.ts | 12 +++++++----- .../vercel/test/fixtures/image/astro.config.mjs | 2 +- packages/integrations/vercel/test/image.test.js | 3 --- 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index 82866081555a..3570f5b61d97 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -32,14 +32,14 @@ function getAdapter(): AstroAdapter { export interface VercelEdgeConfig { includeFiles?: string[]; analytics?: boolean; - images?: boolean; + imageService?: boolean; imagesConfig?: VercelImageConfig; } export default function vercelEdge({ includeFiles = [], analytics, - images, + imageService, imagesConfig, }: VercelEdgeConfig = {}): AstroIntegration { let _config: AstroConfig; @@ -62,11 +62,11 @@ export default function vercelEdge({ client: new URL('./static/', outDir), server: new URL('./dist/', config.root), }, - ...getImageConfig(images, imagesConfig, command), + ...getImageConfig(imageService, imagesConfig, command), }); }, 'astro:config:done': ({ setAdapter, config }) => { - throwIfAssetsNotEnabled(config, images); + throwIfAssetsNotEnabled(config, imageService); setAdapter(getAdapter()); _config = config; buildTempFolder = config.build.server; @@ -147,7 +147,9 @@ export default function vercelEdge({ { handle: 'filesystem' }, { src: '/.*', dest: 'render' }, ], - ...(images ? { images: imagesConfig ? imagesConfig : defaultImageConfig } : {}), + ...(imageService || imagesConfig + ? { images: imagesConfig ? imagesConfig : defaultImageConfig } + : {}), }); }, }, diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts index d7b3e3de1b58..0b6db2037805 100644 --- a/packages/integrations/vercel/src/image/shared.ts +++ b/packages/integrations/vercel/src/image/shared.ts @@ -57,10 +57,10 @@ export const qualityTable: Record = { }; // TODO: Remove once Astro 3.0 is out and `experimental.assets` is no longer needed -export function throwIfAssetsNotEnabled(config: AstroConfig, images: boolean | undefined) { - if (!config.experimental.assets && images) { +export function throwIfAssetsNotEnabled(config: AstroConfig, imageService: boolean | undefined) { + if (!config.experimental.assets && imageService) { throw new Error( - `Using the Vercel Image Optimization API requires \`experimental.assets\` to be enabled. See https://docs.astro.build/en/guides/assets/ for more information.` + `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.` ); } } diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 1916916fbec2..47d164519934 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -26,7 +26,7 @@ export interface VercelServerlessConfig { includeFiles?: string[]; excludeFiles?: string[]; analytics?: boolean; - images?: boolean; + imageService?: boolean; imagesConfig?: VercelImageConfig; } @@ -34,7 +34,7 @@ export default function vercelServerless({ includeFiles, excludeFiles, analytics, - images, + imageService, imagesConfig, }: VercelServerlessConfig = {}): AstroIntegration { let _config: AstroConfig; @@ -57,11 +57,11 @@ export default function vercelServerless({ client: new URL('./static/', outDir), server: new URL('./dist/', config.root), }, - ...getImageConfig(images, imagesConfig, command), + ...getImageConfig(imageService, imagesConfig, command), }); }, 'astro:config:done': ({ setAdapter, config }) => { - throwIfAssetsNotEnabled(config, images); + throwIfAssetsNotEnabled(config, imageService); setAdapter(getAdapter()); _config = config; buildTempFolder = config.build.server; @@ -127,7 +127,9 @@ export default function vercelServerless({ { handle: 'filesystem' }, { src: '/.*', dest: 'render' }, ], - ...(images ? { images: imagesConfig ? imagesConfig : defaultImageConfig } : {}), + ...(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 8bf7d4cfac47..2aa4891333e7 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -17,13 +17,13 @@ function getAdapter(): AstroAdapter { export interface VercelStaticConfig { analytics?: boolean; - images?: boolean; + imageService?: boolean; imagesConfig?: VercelImageConfig; } export default function vercelStatic({ analytics, - images, + imageService, imagesConfig, }: VercelStaticConfig = {}): AstroIntegration { let _config: AstroConfig; @@ -41,11 +41,11 @@ export default function vercelStatic({ build: { format: 'directory', }, - ...getImageConfig(images, imagesConfig, command), + ...getImageConfig(imageService, imagesConfig, command), }); }, 'astro:config:done': ({ setAdapter, config }) => { - throwIfAssetsNotEnabled(config, images); + throwIfAssetsNotEnabled(config, imageService); setAdapter(getAdapter()); _config = config; @@ -65,7 +65,9 @@ export default function vercelStatic({ await writeJson(new URL(`./config.json`, getVercelOutput(_config.root)), { version: 3, routes: [...getRedirects(routes, _config), { handle: 'filesystem' }], - ...(images ? { images: imagesConfig ? imagesConfig : defaultImageConfig } : {}), + ...(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 index 794d568ec0c4..a38be5065f8e 100644 --- a/packages/integrations/vercel/test/fixtures/image/astro.config.mjs +++ b/packages/integrations/vercel/test/fixtures/image/astro.config.mjs @@ -2,7 +2,7 @@ import vercel from '@astrojs/vercel/static'; import { defineConfig } from 'astro/config'; export default defineConfig({ - adapter: vercel({images: true}), + adapter: vercel({imageService: true}), experimental: { assets: true } diff --git a/packages/integrations/vercel/test/image.test.js b/packages/integrations/vercel/test/image.test.js index fe92573e8a5a..834b6d69b181 100644 --- a/packages/integrations/vercel/test/image.test.js +++ b/packages/integrations/vercel/test/image.test.js @@ -9,9 +9,6 @@ describe('Image', () => { before(async () => { fixture = await loadFixture({ root: './fixtures/image/', - experimental: { - assets: true, - }, }); await fixture.build(); }); From 4516dd25cdebd316299dcae6b0d8e962eae701ad Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 28 Apr 2023 16:45:07 +0200 Subject: [PATCH 11/13] docs: add docs --- packages/integrations/vercel/README.md | 57 +++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index b20131f6719e..f58125374410 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,61 @@ export default defineConfig({ }); ``` +### imageConfig + +**Type:** `VercelImageConfig`
+**Available for:** Edge, Serverless, Static + +Configuration to use for [Vercel Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [this page](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 + +When enabled, a [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[]`
From 97e785b889c68b69fc4d41988db3b4f1db937cf0 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Mon, 1 May 2023 12:30:03 +0200 Subject: [PATCH 12/13] Apply suggestions from code review Co-authored-by: Sarah Rainsberger --- packages/integrations/vercel/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index f58125374410..8866885fb982 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -113,7 +113,7 @@ export default defineConfig({ **Type:** `VercelImageConfig`
**Available for:** Edge, Serverless, Static -Configuration to use for [Vercel Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [this page](https://vercel.com/docs/build-output-api/v3/configuration#images) for a complete list of supported parameters. +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 @@ -135,7 +135,7 @@ export default defineConfig({ **Type:** `boolean`
**Available for:** Edge, Serverless, Static -When enabled, a [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, a built-in Squoosh-based service will be used instead. ```js // astro.config.mjs From f6b4e0a1789fa1b6e9f9c9297371067a3438e0c2 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Mon, 1 May 2023 12:32:52 +0200 Subject: [PATCH 13/13] docs(vercel): Add Added In mentions --- packages/integrations/vercel/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index 8866885fb982..0af5632b5f55 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -112,6 +112,7 @@ export default defineConfig({ **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. @@ -134,6 +135,7 @@ export default defineConfig({ **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.