-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13147 from withastro/move-vercel
chore: move Vercel adapter to core monorepo
- Loading branch information
Showing
123 changed files
with
4,906 additions
and
7 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,38 @@ | ||
# @astrojs/vercel | ||
|
||
The Vercel adapter package has moved. Please see [the new repository for the Vercel adapter](https://github.com/withastro/adapters/tree/main/packages/vercel). | ||
This adapter allows Astro to deploy your SSR site to [Vercel](https://www.vercel.com/). | ||
|
||
## Documentation | ||
|
||
Read the [`@astrojs/vercel` docs][docs] | ||
|
||
## Support | ||
|
||
- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more! | ||
|
||
- Check our [Astro Integration Documentation][astro-integration] for more on integrations. | ||
|
||
- Submit bug reports and feature requests as [GitHub issues][issues]. | ||
|
||
## Contributing | ||
|
||
This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started: | ||
|
||
- [Contributor Manual][contributing] | ||
- [Code of Conduct][coc] | ||
- [Community Guide][community] | ||
|
||
## License | ||
|
||
MIT | ||
|
||
Copyright (c) 2023–present [Astro][astro] | ||
|
||
[astro]: https://astro.build/ | ||
[docs]: https://docs.astro.build/en/guides/integrations-guide/vercel/ | ||
[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md | ||
[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md | ||
[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md | ||
[discord]: https://astro.build/chat/ | ||
[issues]: https://github.com/withastro/astro/issues | ||
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,58 @@ | ||
{ | ||
"name": "@astrojs/vercel", | ||
"version": "1.0.0", | ||
"private": true, | ||
"description": "Deploy your site to Vercel", | ||
"version": "8.0.6", | ||
"type": "module", | ||
"keywords": [], | ||
"dont_remove": "This is a placeholder for the sake of the docs smoke test" | ||
"author": "withastro", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/withastro/adapters.git", | ||
"directory": "packages/vercel" | ||
}, | ||
"keywords": ["withastro", "astro-adapter"], | ||
"bugs": "https://github.com/withastro/adapters/issues", | ||
"homepage": "https://docs.astro.build/en/guides/integrations-guide/vercel/", | ||
"exports": { | ||
".": "./dist/index.js", | ||
"./entrypoint": "./dist/serverless/entrypoint.js", | ||
"./serverless": "./dist/serverless/adapter.js", | ||
"./serverless/entrypoint": "./dist/serverless/entrypoint.js", | ||
"./static": "./dist/static/adapter.js", | ||
"./build-image-service": "./dist/image/build-service.js", | ||
"./dev-image-service": "./dist/image/dev-service.js", | ||
"./package.json": "./package.json" | ||
}, | ||
"typesVersions": { | ||
"*": { | ||
"serverless": ["dist/serverless/adapter.d.ts"], | ||
"static": ["dist/static/adapter.d.ts"] | ||
} | ||
}, | ||
"files": ["dist", "types.d.ts"], | ||
"scripts": { | ||
"build": "astro-scripts build \"src/**/*.ts\" && tsc", | ||
"test": "astro-scripts test --timeout 50000 \"test/**/!(hosted).test.js\"", | ||
"test:hosted": "astro-scripts test --timeout 30000 \"test/hosted/*.test.js\"" | ||
}, | ||
"dependencies": { | ||
"@astrojs/internal-helpers": "^0.5.1", | ||
"@vercel/analytics": "^1.4.1", | ||
"@vercel/edge": "^1.2.1", | ||
"@vercel/nft": "^0.29.0", | ||
"@vercel/routing-utils": "5.0.2", | ||
"esbuild": "^0.24.0", | ||
"fast-glob": "^3.3.3" | ||
}, | ||
"peerDependencies": { | ||
"astro": "^5.0.0" | ||
}, | ||
"devDependencies": { | ||
"astro": "workspace:*", | ||
"astro-scripts": "workspace:*", | ||
"cheerio": "1.0.0" | ||
}, | ||
"publishConfig": { | ||
"provenance": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import type { ExternalImageService } from 'astro'; | ||
import { baseService } from 'astro/assets'; | ||
import { isESMImportedImage, sharedValidateOptions } from './shared.js'; | ||
|
||
const service: ExternalImageService = { | ||
...baseService, | ||
validateOptions: (options, serviceOptions) => | ||
sharedValidateOptions(options, serviceOptions.service.config, 'production'), | ||
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, densities, widths, formats, ...attributes } = | ||
options; | ||
|
||
return { | ||
...attributes, | ||
width: targetWidth, | ||
height: targetHeight, | ||
loading: attributes.loading ?? 'lazy', | ||
decoding: attributes.decoding ?? 'async', | ||
}; | ||
}, | ||
getURL(options) { | ||
const fileSrc = isESMImportedImage(options.src) | ||
? removeLeadingForwardSlash(options.src.src) | ||
: options.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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import type { LocalImageService } from 'astro'; | ||
import sharpService from 'astro/assets/services/sharp'; | ||
import { baseDevService } from './shared-dev-service.js'; | ||
|
||
const service: LocalImageService = { | ||
...baseDevService, | ||
getHTMLAttributes(options, serviceOptions) { | ||
const { inputtedWidth, ...props } = options; | ||
|
||
// If `validateOptions` returned a different width than the one of the image, use it for attributes | ||
if (inputtedWidth) { | ||
props.width = inputtedWidth; | ||
} | ||
|
||
return sharpService.getHTMLAttributes | ||
? sharpService.getHTMLAttributes(props, serviceOptions) | ||
: {}; | ||
}, | ||
transform(inputBuffer, transform, serviceOptions) { | ||
// NOTE: Hardcoding webp here isn't accurate to how the Vercel Image Optimization API works, normally what we should | ||
// do is setup a custom endpoint that sniff the user's accept-content header and serve the proper format based on the | ||
// user's Vercel config. However, that's: a lot of work for: not much. The dev service is inaccurate to the prod service | ||
// in many more ways, this is one of the less offending cases and is, imo, okay, erika - 2023-04-27 | ||
transform.format = transform.src.endsWith('svg') ? 'svg' : 'webp'; | ||
|
||
// The base sharp service works the same way as the Vercel Image Optimization API, so it's a safe fallback in local | ||
return sharpService.transform(inputBuffer, transform, serviceOptions); | ||
}, | ||
}; | ||
|
||
export default service; |
35 changes: 35 additions & 0 deletions
35
packages/integrations/vercel/src/image/shared-dev-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import type { LocalImageService } from 'astro'; | ||
import { baseService } from 'astro/assets'; | ||
import { sharedValidateOptions } from './shared.js'; | ||
|
||
export const baseDevService: Omit<LocalImageService, 'transform'> = { | ||
...baseService, | ||
validateOptions: (options, serviceOptions) => | ||
sharedValidateOptions(options, serviceOptions.service.config, 'development'), | ||
getURL(options) { | ||
const fileSrc = typeof options.src === 'string' ? options.src : options.src.src; | ||
|
||
const searchParams = new URLSearchParams(); | ||
searchParams.append('href', fileSrc); | ||
|
||
options.width && searchParams.append('w', options.width.toString()); | ||
options.quality && searchParams.append('q', options.quality.toString()); | ||
|
||
return '/_image?' + searchParams; | ||
}, | ||
parseURL(url) { | ||
const params = url.searchParams; | ||
|
||
if (!params.has('href')) { | ||
return undefined; | ||
} | ||
|
||
const transform = { | ||
src: params.get('href')!, | ||
width: params.has('w') ? Number.parseInt(params.get('w')!) : undefined, | ||
quality: params.get('q'), | ||
}; | ||
|
||
return transform; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import type { AstroConfig, ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro'; | ||
|
||
export function getDefaultImageConfig(astroImageConfig: AstroConfig['image']): VercelImageConfig { | ||
return { | ||
sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], | ||
domains: astroImageConfig.domains ?? [], | ||
// Cast is necessary here because Vercel's types are slightly different from ours regarding allowed protocols. Behavior should be the same, however. | ||
remotePatterns: (astroImageConfig.remotePatterns as VercelImageConfig['remotePatterns']) ?? [], | ||
}; | ||
} | ||
|
||
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata { | ||
return typeof src === 'object'; | ||
} | ||
|
||
export type DevImageService = 'sharp' | (string & {}); | ||
|
||
// https://vercel.com/docs/build-output-api/v3/configuration#images | ||
type ImageFormat = 'image/avif' | 'image/webp'; | ||
|
||
export type RemotePattern = { | ||
protocol?: 'http' | 'https'; | ||
hostname: string; | ||
port?: string; | ||
pathname?: string; | ||
}; | ||
|
||
export type VercelImageConfig = { | ||
/** | ||
* Supported image widths. | ||
*/ | ||
sizes: number[]; | ||
/** | ||
* Allowed external domains that can use Image Optimization. Leave empty for only allowing the deployment domain to use Image Optimization. | ||
*/ | ||
domains: string[]; | ||
/** | ||
* Allowed external patterns that can use Image Optimization. Similar to `domains` but provides more control with RegExp. | ||
*/ | ||
remotePatterns?: RemotePattern[]; | ||
/** | ||
* Cache duration (in seconds) for the optimized images. | ||
*/ | ||
minimumCacheTTL?: number; | ||
/** | ||
* Supported output image formats | ||
*/ | ||
formats?: ImageFormat[]; | ||
/** | ||
* Allow SVG input image URLs. This is disabled by default for security purposes. | ||
*/ | ||
dangerouslyAllowSVG?: boolean; | ||
/** | ||
* Change the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) of the optimized images. | ||
*/ | ||
contentSecurityPolicy?: string; | ||
}; | ||
|
||
export const qualityTable: Record<ImageQualityPreset, number> = { | ||
low: 25, | ||
mid: 50, | ||
high: 80, | ||
max: 100, | ||
}; | ||
|
||
export function getAstroImageConfig( | ||
images: boolean | undefined, | ||
imagesConfig: VercelImageConfig | undefined, | ||
command: string, | ||
devImageService: DevImageService, | ||
astroImageConfig: AstroConfig['image'] | ||
) { | ||
let devService = '@astrojs/vercel/dev-image-service'; | ||
|
||
switch (devImageService) { | ||
case 'sharp': | ||
devService = '@astrojs/vercel/dev-image-service'; | ||
break; | ||
default: | ||
if (typeof devImageService === 'string') { | ||
devService = devImageService; | ||
} else { | ||
devService = '@astrojs/vercel/dev-image-service'; | ||
} | ||
break; | ||
} | ||
|
||
if (images) { | ||
return { | ||
image: { | ||
service: { | ||
entrypoint: command === 'dev' ? devService : '@astrojs/vercel/build-image-service', | ||
config: imagesConfig ? imagesConfig : getDefaultImageConfig(astroImageConfig), | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
return {}; | ||
} | ||
|
||
export function sharedValidateOptions( | ||
options: ImageTransform, | ||
serviceConfig: Record<string, any>, | ||
mode: 'development' | 'production' | ||
) { | ||
const vercelImageOptions = serviceConfig 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; | ||
} |
Oops, something went wrong.