aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/vercel/src/image/shared.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/vercel/src/image/shared.ts')
-rw-r--r--packages/integrations/vercel/src/image/shared.ts172
1 files changed, 172 insertions, 0 deletions
diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts
new file mode 100644
index 000000000..e214cfa42
--- /dev/null
+++ b/packages/integrations/vercel/src/image/shared.ts
@@ -0,0 +1,172 @@
+import type { AstroConfig, ImageQualityPreset, ImageTransform } from 'astro';
+import { isESMImportedImage } from 'astro/assets/utils';
+
+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 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;
+};
+
+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'],
+ responsiveImages?: boolean,
+) {
+ 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) {
+ const config = imagesConfig ? imagesConfig : getDefaultImageConfig(astroImageConfig);
+ return {
+ image: {
+ service: {
+ entrypoint: command === 'dev' ? devService : '@astrojs/vercel/build-image-service',
+ config,
+ },
+ experimentalBreakpoints: responsiveImages ? config.sizes : undefined,
+ },
+ };
+ }
+
+ 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.widths) {
+ // Vercel only supports a fixed set of widths, so remove any that aren't in the list
+ options.widths = options.widths.filter((w) => configuredWidths.includes(w));
+ // Oh no, we've removed all the widths! Let's add the nearest one back in
+ if (options.widths.length === 0) {
+ options.widths = [options.width];
+ }
+ }
+
+ 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;
+}