From cefdc03852414d20e7b734245db638dbaeb771f0 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Wed, 6 Jul 2022 18:50:10 -0500 Subject: [PATCH 01/18] moving all normalization logic out of the Image component --- .../integrations/image/components/Image.astro | 102 +----------------- packages/integrations/image/src/index.ts | 99 ++++++++++++++++- packages/integrations/image/src/utils.ts | 14 +++ 3 files changed, 109 insertions(+), 106 deletions(-) diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index 51d4182a2fc9..dc9b15eadd40 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -17,109 +17,9 @@ export interface RemoteImageProps extends TransformOptions, ImageAttributes { export type Props = LocalImageProps | RemoteImageProps; -function isLocalImage(props: Props): props is LocalImageProps { - // vite-plugin-astro-image resolves ESM imported images - // to a metadata object - return typeof props.src !== 'string'; -} - -function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) { - if (!aspectRatio) { - return undefined; - } - - // parse aspect ratio strings, if required (ex: "16:9") - if (typeof aspectRatio === 'number') { - aspectRatio = aspectRatio; - } else { - const [width, height] = aspectRatio.split(':'); - aspectRatio = parseInt(width) / parseInt(height); - } -} - -async function resolveProps(props: Props): Promise { - // For remote images, just check the width/height provided - if (!isLocalImage(props)) { - return calculateSize(props); - } - - let { width, height, aspectRatio, format, ...rest } = props; - - // if a Promise was provided, unwrap it first - const { src, ...metadata } = 'then' in props.src ? (await props.src).default : props.src; - - if (!width && !height) { - // neither dimension was provided, use the file metadata - width = metadata.width; - height = metadata.height; - } else if (width) { - // one dimension was provided, calculate the other - let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; - height = height || width / ratio; - } else if (height) { - // one dimension was provided, calculate the other - let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; - width = width || height * ratio; - } - - return { - ...rest, - width, - height, - aspectRatio, - src, - format: format || metadata.format as OutputFormat, - } -} - -function calculateSize(transform: TransformOptions): TransformOptions { - // keep width & height as provided - if (transform.width && transform.height) { - return transform; - } - - if (!transform.width && !transform.height) { - throw new Error(`"width" and "height" cannot both be undefined`); - } - - if (!transform.aspectRatio) { - throw new Error(`"aspectRatio" must be included if only "${transform.width ? "width": "height"}" is provided`) - } - - let aspectRatio: number; - - // parse aspect ratio strings, if required (ex: "16:9") - if (typeof transform.aspectRatio === 'number') { - aspectRatio = transform.aspectRatio; - } else { - const [width, height] = transform.aspectRatio.split(':'); - aspectRatio = parseInt(width) / parseInt(height); - } - - if (transform.width) { - // only width was provided, calculate height - return { - ...transform, - width: transform.width, - height: transform.width / aspectRatio - }; - } else if (transform.height) { - // only height was provided, calculate width - return { - ...transform, - width: transform.height * aspectRatio, - height: transform.height - } - } - - return transform; -} - const props = Astro.props as Props; -const imageProps = await resolveProps(props); - -const attrs = await getImage(loader, imageProps); +const attrs = await getImage(loader, props); --- diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index e44b7230b9d3..5f63cbfa5bcc 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -5,7 +5,9 @@ import slash from 'slash'; import { fileURLToPath } from 'url'; import type { ImageAttributes, + ImageMetadata, IntegrationOptions, + OutputFormat, SSRImageService, TransformOptions, } from './types'; @@ -14,6 +16,7 @@ import { isRemoteImage, loadLocalImage, loadRemoteImage, + parseAspectRatio, propsToFilename, } from './utils.js'; import { createPlugin } from './vite-plugin-astro-image.js'; @@ -22,6 +25,90 @@ const PKG_NAME = '@astrojs/image'; const ROUTE_PATTERN = '/_image'; const OUTPUT_DIR = '/_image'; +export interface GetImageTransform extends Omit { + src: string | ImageMetadata | Promise<{ default: ImageMetadata }>; +} + +function resolveSize(transform: TransformOptions): TransformOptions { + // keep width & height as provided + if (transform.width && transform.height) { + return transform; + } + + if (!transform.width && !transform.height) { + throw new Error(`"width" and "height" cannot both be undefined`); + } + + if (!transform.aspectRatio) { + throw new Error(`"aspectRatio" must be included if only "${transform.width ? "width": "height"}" is provided`) + } + + let aspectRatio: number; + + // parse aspect ratio strings, if required (ex: "16:9") + if (typeof transform.aspectRatio === 'number') { + aspectRatio = transform.aspectRatio; + } else { + const [width, height] = transform.aspectRatio.split(':'); + aspectRatio = parseInt(width) / parseInt(height); + } + + if (transform.width) { + // only width was provided, calculate height + return { + ...transform, + width: transform.width, + height: transform.width / aspectRatio + } as TransformOptions; + } else if (transform.height) { + // only height was provided, calculate width + return { + ...transform, + width: transform.height * aspectRatio, + height: transform.height + }; + } + + return transform; +} + +async function resolveTransform(input: GetImageTransform): Promise { + // for remote images, only validate the width and height props + if (typeof input.src === 'string') { + return resolveSize(input as TransformOptions); + } + + // resolve the metadata promise, usually when the ESM import is inlined + const metadata = 'then' in input.src + ? (await input.src).default + : input.src; + + let { width, height, aspectRatio, format = metadata.format, ...rest } = input; + + if (!width && !height) { + // neither dimension was provided, use the file metadata + width = metadata.width; + height = metadata.height; + } else if (width) { + // one dimension was provided, calculate the other + let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; + height = height || width / ratio; + } else if (height) { + // one dimension was provided, calculate the other + let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; + width = width || height * ratio; + } + + return { + ...rest, + src: metadata.src, + width, + height, + aspectRatio, + format: format as OutputFormat, + } +} + /** * Gets the HTML attributes required to build an `` for the transformed image. * @@ -31,24 +118,26 @@ const OUTPUT_DIR = '/_image'; */ export async function getImage( loader: SSRImageService, - transform: TransformOptions + transform: GetImageTransform ): Promise { (globalThis as any).loader = loader; - const attributes = await loader.getImageAttributes(transform); + const resolved = await resolveTransform(transform); + + const attributes = await loader.getImageAttributes(resolved); // For SSR services, build URLs for the injected route if (typeof loader.transform === 'function') { - const { searchParams } = loader.serializeTransform(transform); + const { searchParams } = loader.serializeTransform(resolved); // cache all images rendered to HTML if (globalThis && (globalThis as any).addStaticImage) { - (globalThis as any)?.addStaticImage(transform); + (globalThis as any)?.addStaticImage(resolved); } const src = globalThis && (globalThis as any).filenameFormat - ? (globalThis as any).filenameFormat(transform, searchParams) + ? (globalThis as any).filenameFormat(resolved, searchParams) : `${ROUTE_PATTERN}?${searchParams.toString()}`; return { diff --git a/packages/integrations/image/src/utils.ts b/packages/integrations/image/src/utils.ts index 95e0fb2a11e4..4e5b0ebc1208 100644 --- a/packages/integrations/image/src/utils.ts +++ b/packages/integrations/image/src/utils.ts @@ -58,3 +58,17 @@ export function propsToFilename({ src, width, height, format }: TransformOptions return format ? src.replace(ext, format) : src; } + +export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) { + if (!aspectRatio) { + return undefined; + } + + // parse aspect ratio strings, if required (ex: "16:9") + if (typeof aspectRatio === 'number') { + aspectRatio = aspectRatio; + } else { + const [width, height] = aspectRatio.split(':'); + aspectRatio = parseInt(width) / parseInt(height); + } +} From 4ec0b2d9ff68a0290ff79d84e3564be172f1e2b6 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Wed, 6 Jul 2022 23:12:28 -0500 Subject: [PATCH 02/18] refactor: only require loaders to provide the image src --- packages/integrations/image/src/index.ts | 41 +++++++++++-------- .../integrations/image/src/loaders/sharp.ts | 10 ----- packages/integrations/image/src/types.ts | 19 +++++---- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index 5f63cbfa5bcc..f6432208c40f 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -3,14 +3,15 @@ import fs from 'fs/promises'; import path from 'path'; import slash from 'slash'; import { fileURLToPath } from 'url'; -import type { +import { ImageAttributes, ImageMetadata, + ImageService, IntegrationOptions, + isSSRService, OutputFormat, - SSRImageService, TransformOptions, -} from './types'; +} from './types.js'; import { ensureDir, isRemoteImage, @@ -58,13 +59,13 @@ function resolveSize(transform: TransformOptions): TransformOptions { return { ...transform, width: transform.width, - height: transform.width / aspectRatio + height: Math.round(transform.width / aspectRatio) } as TransformOptions; } else if (transform.height) { // only height was provided, calculate width return { ...transform, - width: transform.height * aspectRatio, + width: Math.round(transform.height * aspectRatio), height: transform.height }; } @@ -92,11 +93,11 @@ async function resolveTransform(input: GetImageTransform): Promise` for the transformed image. * @@ -117,17 +128,15 @@ async function resolveTransform(input: GetImageTransform): Promise` element. */ export async function getImage( - loader: SSRImageService, + loader: ImageService, transform: GetImageTransform ): Promise { (globalThis as any).loader = loader; const resolved = await resolveTransform(transform); - const attributes = await loader.getImageAttributes(resolved); - // For SSR services, build URLs for the injected route - if (typeof loader.transform === 'function') { + if (isSSRService(loader)) { const { searchParams } = loader.serializeTransform(resolved); // cache all images rendered to HTML @@ -140,14 +149,12 @@ export async function getImage( ? (globalThis as any).filenameFormat(resolved, searchParams) : `${ROUTE_PATTERN}?${searchParams.toString()}`; - return { - ...attributes, - src: slash(src), // Windows compat - }; + // Windows compat + return getImageAttributes(slash(src), resolved); } - // For hosted services, return the attributes as-is - return attributes; + // For hosted services, return the `src` attribute as-is + return getImageAttributes(await loader.getImageSrc(resolved), resolved); } const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => { diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts index b82a75044d04..ed36d501b7d9 100644 --- a/packages/integrations/image/src/loaders/sharp.ts +++ b/packages/integrations/image/src/loaders/sharp.ts @@ -3,16 +3,6 @@ import type { OutputFormat, SSRImageService, TransformOptions } from '../types'; import { isAspectRatioString, isOutputFormat } from '../utils.js'; class SharpService implements SSRImageService { - async getImageAttributes(transform: TransformOptions) { - const { width, height, src, format, quality, aspectRatio, ...rest } = transform; - - return { - ...rest, - width: width, - height: height, - }; - } - serializeTransform(transform: TransformOptions) { const searchParams = new URLSearchParams(); diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index b55feb7c5b93..a00ea6def949 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -76,17 +76,12 @@ export type ImageAttributes = Partial; export interface HostedImageService { /** - * Gets the HTML attributes needed for the server rendered `` element. + * Gets the `src` attribute needed for the server rendered `` element. */ - getImageAttributes(transform: T): Promise; + getImageSrc(transform: T): Promise; } -export interface SSRImageService - extends HostedImageService { - /** - * Gets the HTML attributes needed for the server rendered `` element. - */ - getImageAttributes(transform: T): Promise>; +export interface SSRImageService { /** * Serializes image transformation properties to URLSearchParams, used to build * the final `src` that points to the self-hosted SSR endpoint. @@ -115,6 +110,14 @@ export type ImageService = | HostedImageService | SSRImageService; +export function isHostedService(service: ImageService): service is ImageService { + return 'getImageSrc' in service; +} + +export function isSSRService(service: ImageService): service is SSRImageService { + return 'transform' in service; +} + export interface ImageMetadata { src: string; width: number; From 621fb8fccefe92e00951e46166bcaf7b52204d1a Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Wed, 6 Jul 2022 23:12:40 -0500 Subject: [PATCH 03/18] Adding a `` component --- .../integrations/image/components/Image.astro | 8 +++- .../image/components/Picture.astro | 46 +++++++++++++++++++ .../integrations/image/components/index.js | 1 + 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 packages/integrations/image/components/Picture.astro diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index dc9b15eadd40..80ab545c1e6b 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -4,7 +4,7 @@ import loader from 'virtual:image-loader'; import { getImage } from '../src/index.js'; import type { ImageAttributes, ImageMetadata, TransformOptions, OutputFormat } from '../src/types.js'; -export interface LocalImageProps extends Omit, Omit { +export interface LocalImageProps extends Omit { src: ImageMetadata | Promise<{ default: ImageMetadata }>; } @@ -23,3 +23,9 @@ const attrs = await getImage(loader, props); --- + + diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro new file mode 100644 index 000000000000..3ae82973f55c --- /dev/null +++ b/packages/integrations/image/components/Picture.astro @@ -0,0 +1,46 @@ +--- +import Image from './Image.astro'; +// @ts-ignore +import loader from 'virtual:image-loader'; +import { lookup } from 'mrmime'; +import { getImage } from '../src/index.js'; +import type { ImageMetadata, OutputFormat } from '../src/types.js'; + +export interface Props extends Partial { + src: ImageMetadata; + sizes: HTMLImageElement['sizes']; + widths: number[]; + formats?: OutputFormat[]; +} + +const { src, sizes, widths, formats = ['avif', 'webp', 'jpeg'] } = Astro.props as Props; + +if (widths.length <= 0) { + throw new Error('At least one width must be provided for the '); +} + +const aspectRatio = src.width / src.height; + +async function getSource(format: OutputFormat) { + const imgs = await Promise.all(widths.map(async (width) => { + const img = await getImage(loader, { src: src.src, format, width, height: Math.round(width / aspectRatio) }); + return `${img.src} ${width}w`; + })) + + return { + type: lookup(format) || format, + srcset: imgs.join(',') + }; +} + +const sources = await Promise.all(formats.map(format => getSource(format))); + +const width = widths.sort().shift()!; +const height = Math.round(width / aspectRatio); +--- + + + {sources.map(attrs => ( + ))} + + diff --git a/packages/integrations/image/components/index.js b/packages/integrations/image/components/index.js index fa9809650db1..be0e10130ae5 100644 --- a/packages/integrations/image/components/index.js +++ b/packages/integrations/image/components/index.js @@ -1 +1,2 @@ export { default as Image } from './Image.astro'; +export { default as Picture } from './Picture.astro'; From 48d47d5db2b77bdf0e553ceb9c57448cdd4137b6 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Thu, 7 Jul 2022 13:52:36 -0500 Subject: [PATCH 04/18] fixing types.ts imports --- packages/integrations/image/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index a00ea6def949..e1425965e775 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -1,5 +1,5 @@ -export type { Image } from '../components/index'; -export * from './index'; +export type { Image, Picture } from '../components/index.js'; +export * from './index.js'; export type InputFormat = | 'heic' From 0ba01e41d6f959113b02768321bc2a0d0a60f825 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Thu, 7 Jul 2022 14:02:05 -0500 Subject: [PATCH 05/18] refactor: moving getImage to it's own file --- packages/integrations/image/src/config.ts | 3 + packages/integrations/image/src/get-image.ts | 135 +++++++++++++++++ packages/integrations/image/src/index.ts | 149 +------------------ 3 files changed, 141 insertions(+), 146 deletions(-) create mode 100644 packages/integrations/image/src/config.ts create mode 100644 packages/integrations/image/src/get-image.ts diff --git a/packages/integrations/image/src/config.ts b/packages/integrations/image/src/config.ts new file mode 100644 index 000000000000..db52614c53f1 --- /dev/null +++ b/packages/integrations/image/src/config.ts @@ -0,0 +1,3 @@ +export const PKG_NAME = '@astrojs/image'; +export const ROUTE_PATTERN = '/_image'; +export const OUTPUT_DIR = '/_image'; diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/get-image.ts new file mode 100644 index 000000000000..dc314e01f1b1 --- /dev/null +++ b/packages/integrations/image/src/get-image.ts @@ -0,0 +1,135 @@ +import slash from 'slash'; +import { ROUTE_PATTERN } from './config.js'; +import { ImageAttributes, ImageMetadata, ImageService, isSSRService, OutputFormat, TransformOptions } from './types.js'; +import { parseAspectRatio } from './utils.js'; + +export interface GetImageTransform extends Omit { + src: string | ImageMetadata | Promise<{ default: ImageMetadata }>; +} + +function resolveSize(transform: TransformOptions): TransformOptions { + // keep width & height as provided + if (transform.width && transform.height) { + return transform; + } + + if (!transform.width && !transform.height) { + throw new Error(`"width" and "height" cannot both be undefined`); + } + + if (!transform.aspectRatio) { + throw new Error(`"aspectRatio" must be included if only "${transform.width ? "width": "height"}" is provided`) + } + + let aspectRatio: number; + + // parse aspect ratio strings, if required (ex: "16:9") + if (typeof transform.aspectRatio === 'number') { + aspectRatio = transform.aspectRatio; + } else { + const [width, height] = transform.aspectRatio.split(':'); + aspectRatio = parseInt(width) / parseInt(height); + } + + if (transform.width) { + // only width was provided, calculate height + return { + ...transform, + width: transform.width, + height: Math.round(transform.width / aspectRatio) + } as TransformOptions; + } else if (transform.height) { + // only height was provided, calculate width + return { + ...transform, + width: Math.round(transform.height * aspectRatio), + height: transform.height + }; + } + + return transform; +} + +async function resolveTransform(input: GetImageTransform): Promise { + // for remote images, only validate the width and height props + if (typeof input.src === 'string') { + return resolveSize(input as TransformOptions); + } + + // resolve the metadata promise, usually when the ESM import is inlined + const metadata = 'then' in input.src + ? (await input.src).default + : input.src; + + let { width, height, aspectRatio, format = metadata.format, ...rest } = input; + + if (!width && !height) { + // neither dimension was provided, use the file metadata + width = metadata.width; + height = metadata.height; + } else if (width) { + // one dimension was provided, calculate the other + let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; + height = height || Math.round(width / ratio); + } else if (height) { + // one dimension was provided, calculate the other + let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; + width = width || Math.round(height * ratio); + } + + return { + ...rest, + src: metadata.src, + width, + height, + aspectRatio, + format: format as OutputFormat, + } +} + +function getImageAttributes(src: string, transform: TransformOptions): ImageAttributes { + return { + loading: 'lazy', + decoding: 'async', + src, + width: transform.width, + height: transform.height + }; +} + +/** + * Gets the HTML attributes required to build an `` for the transformed image. + * + * @param loader @type {ImageService} The image service used for transforming images. + * @param transform @type {TransformOptions} The transformations requested for the optimized image. + * @returns @type {ImageAttributes} The HTML attributes to be included on the built `` element. + */ + export async function getImage( + loader: ImageService, + transform: GetImageTransform +): Promise { + (globalThis as any).loader = loader; + + const resolved = await resolveTransform(transform); + + // For SSR services, build URLs for the injected route + if (isSSRService(loader)) { + const { searchParams } = loader.serializeTransform(resolved); + + // cache all images rendered to HTML + if (globalThis && (globalThis as any).addStaticImage) { + (globalThis as any)?.addStaticImage(resolved); + } + + const src = + globalThis && (globalThis as any).filenameFormat + ? (globalThis as any).filenameFormat(resolved, searchParams) + : `${ROUTE_PATTERN}?${searchParams.toString()}`; + + // Windows compat + return getImageAttributes(slash(src), resolved); + } + + // For hosted services, return the `src` attribute as-is + return getImageAttributes(await loader.getImageSrc(resolved), resolved); +} diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index f6432208c40f..71ba17a35f16 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -1,162 +1,19 @@ import type { AstroConfig, AstroIntegration } from 'astro'; import fs from 'fs/promises'; import path from 'path'; -import slash from 'slash'; import { fileURLToPath } from 'url'; -import { - ImageAttributes, - ImageMetadata, - ImageService, - IntegrationOptions, - isSSRService, - OutputFormat, - TransformOptions, -} from './types.js'; +import { OUTPUT_DIR, PKG_NAME, ROUTE_PATTERN } from './config.js'; +export * from './get-image.js'; +import { IntegrationOptions, TransformOptions } from './types.js'; import { ensureDir, isRemoteImage, loadLocalImage, loadRemoteImage, - parseAspectRatio, propsToFilename, } from './utils.js'; import { createPlugin } from './vite-plugin-astro-image.js'; -const PKG_NAME = '@astrojs/image'; -const ROUTE_PATTERN = '/_image'; -const OUTPUT_DIR = '/_image'; - -export interface GetImageTransform extends Omit { - src: string | ImageMetadata | Promise<{ default: ImageMetadata }>; -} - -function resolveSize(transform: TransformOptions): TransformOptions { - // keep width & height as provided - if (transform.width && transform.height) { - return transform; - } - - if (!transform.width && !transform.height) { - throw new Error(`"width" and "height" cannot both be undefined`); - } - - if (!transform.aspectRatio) { - throw new Error(`"aspectRatio" must be included if only "${transform.width ? "width": "height"}" is provided`) - } - - let aspectRatio: number; - - // parse aspect ratio strings, if required (ex: "16:9") - if (typeof transform.aspectRatio === 'number') { - aspectRatio = transform.aspectRatio; - } else { - const [width, height] = transform.aspectRatio.split(':'); - aspectRatio = parseInt(width) / parseInt(height); - } - - if (transform.width) { - // only width was provided, calculate height - return { - ...transform, - width: transform.width, - height: Math.round(transform.width / aspectRatio) - } as TransformOptions; - } else if (transform.height) { - // only height was provided, calculate width - return { - ...transform, - width: Math.round(transform.height * aspectRatio), - height: transform.height - }; - } - - return transform; -} - -async function resolveTransform(input: GetImageTransform): Promise { - // for remote images, only validate the width and height props - if (typeof input.src === 'string') { - return resolveSize(input as TransformOptions); - } - - // resolve the metadata promise, usually when the ESM import is inlined - const metadata = 'then' in input.src - ? (await input.src).default - : input.src; - - let { width, height, aspectRatio, format = metadata.format, ...rest } = input; - - if (!width && !height) { - // neither dimension was provided, use the file metadata - width = metadata.width; - height = metadata.height; - } else if (width) { - // one dimension was provided, calculate the other - let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; - height = height || Math.round(width / ratio); - } else if (height) { - // one dimension was provided, calculate the other - let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height; - width = width || Math.round(height * ratio); - } - - return { - ...rest, - src: metadata.src, - width, - height, - aspectRatio, - format: format as OutputFormat, - } -} - -function getImageAttributes(src: string, transform: TransformOptions): ImageAttributes { - return { - loading: 'lazy', - decoding: 'async', - src, - width: transform.width, - height: transform.height - }; -} - -/** - * Gets the HTML attributes required to build an `` for the transformed image. - * - * @param loader @type {ImageService} The image service used for transforming images. - * @param transform @type {TransformOptions} The transformations requested for the optimized image. - * @returns @type {ImageAttributes} The HTML attributes to be included on the built `` element. - */ -export async function getImage( - loader: ImageService, - transform: GetImageTransform -): Promise { - (globalThis as any).loader = loader; - - const resolved = await resolveTransform(transform); - - // For SSR services, build URLs for the injected route - if (isSSRService(loader)) { - const { searchParams } = loader.serializeTransform(resolved); - - // cache all images rendered to HTML - if (globalThis && (globalThis as any).addStaticImage) { - (globalThis as any)?.addStaticImage(resolved); - } - - const src = - globalThis && (globalThis as any).filenameFormat - ? (globalThis as any).filenameFormat(resolved, searchParams) - : `${ROUTE_PATTERN}?${searchParams.toString()}`; - - // Windows compat - return getImageAttributes(slash(src), resolved); - } - - // For hosted services, return the `src` attribute as-is - return getImageAttributes(await loader.getImageSrc(resolved), resolved); -} - const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => { const resolvedOptions = { serviceEntryPoint: '@astrojs/image/sharp', From 6e5f578da8d1d3fd262f3cd9add7549f7580af97 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Thu, 7 Jul 2022 14:17:04 -0500 Subject: [PATCH 06/18] updating component types to use astroHTML.JSX --- packages/integrations/image/components/Image.astro | 2 +- packages/integrations/image/components/Picture.astro | 9 +++++---- packages/integrations/image/src/types.ts | 4 +++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index 80ab545c1e6b..4f959f406ba3 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -4,7 +4,7 @@ import loader from 'virtual:image-loader'; import { getImage } from '../src/index.js'; import type { ImageAttributes, ImageMetadata, TransformOptions, OutputFormat } from '../src/types.js'; -export interface LocalImageProps extends Omit { +export interface LocalImageProps extends Omit, Omit { src: ImageMetadata | Promise<{ default: ImageMetadata }>; } diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro index 3ae82973f55c..762d81089105 100644 --- a/packages/integrations/image/components/Picture.astro +++ b/packages/integrations/image/components/Picture.astro @@ -4,16 +4,17 @@ import Image from './Image.astro'; import loader from 'virtual:image-loader'; import { lookup } from 'mrmime'; import { getImage } from '../src/index.js'; -import type { ImageMetadata, OutputFormat } from '../src/types.js'; +import type { ImageAttributes, ImageMetadata, OutputFormat, PictureAttributes } from '../src/types.js'; -export interface Props extends Partial { +export interface Props extends Omit { src: ImageMetadata; sizes: HTMLImageElement['sizes']; widths: number[]; formats?: OutputFormat[]; + img?: Omit; } -const { src, sizes, widths, formats = ['avif', 'webp', 'jpeg'] } = Astro.props as Props; +const { src, sizes, widths, formats = ['avif', 'webp', 'jpeg'], ...attrs } = Astro.props as Props; if (widths.length <= 0) { throw new Error('At least one width must be provided for the '); @@ -39,7 +40,7 @@ const width = widths.sort().shift()!; const height = Math.round(width / aspectRatio); --- - + {sources.map(attrs => ( ))} diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index e1425965e775..30ba12762062 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -1,3 +1,4 @@ +import 'astro/astro-jsx.js'; export type { Image, Picture } from '../components/index.js'; export * from './index.js'; @@ -72,7 +73,8 @@ export interface TransformOptions { aspectRatio?: number | `${number}:${number}`; } -export type ImageAttributes = Partial; +export type ImageAttributes = astroHTML.JSX.HTMLAttributes; +export type PictureAttributes = astroHTML.JSX.HTMLAttributes; export interface HostedImageService { /** From 350c72737026e09454c252784ae2683797768bf9 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Thu, 7 Jul 2022 14:28:20 -0500 Subject: [PATCH 07/18] Revert "updating component types to use astroHTML.JSX" This reverts commit 6e5f578da8d1d3fd262f3cd9add7549f7580af97. --- packages/integrations/image/components/Image.astro | 2 +- packages/integrations/image/components/Picture.astro | 9 ++++----- packages/integrations/image/src/types.ts | 4 +--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index 4f959f406ba3..80ab545c1e6b 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -4,7 +4,7 @@ import loader from 'virtual:image-loader'; import { getImage } from '../src/index.js'; import type { ImageAttributes, ImageMetadata, TransformOptions, OutputFormat } from '../src/types.js'; -export interface LocalImageProps extends Omit, Omit { +export interface LocalImageProps extends Omit { src: ImageMetadata | Promise<{ default: ImageMetadata }>; } diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro index 762d81089105..3ae82973f55c 100644 --- a/packages/integrations/image/components/Picture.astro +++ b/packages/integrations/image/components/Picture.astro @@ -4,17 +4,16 @@ import Image from './Image.astro'; import loader from 'virtual:image-loader'; import { lookup } from 'mrmime'; import { getImage } from '../src/index.js'; -import type { ImageAttributes, ImageMetadata, OutputFormat, PictureAttributes } from '../src/types.js'; +import type { ImageMetadata, OutputFormat } from '../src/types.js'; -export interface Props extends Omit { +export interface Props extends Partial { src: ImageMetadata; sizes: HTMLImageElement['sizes']; widths: number[]; formats?: OutputFormat[]; - img?: Omit; } -const { src, sizes, widths, formats = ['avif', 'webp', 'jpeg'], ...attrs } = Astro.props as Props; +const { src, sizes, widths, formats = ['avif', 'webp', 'jpeg'] } = Astro.props as Props; if (widths.length <= 0) { throw new Error('At least one width must be provided for the '); @@ -40,7 +39,7 @@ const width = widths.sort().shift()!; const height = Math.round(width / aspectRatio); --- - + {sources.map(attrs => ( ))} diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index 30ba12762062..e1425965e775 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -1,4 +1,3 @@ -import 'astro/astro-jsx.js'; export type { Image, Picture } from '../components/index.js'; export * from './index.js'; @@ -73,8 +72,7 @@ export interface TransformOptions { aspectRatio?: number | `${number}:${number}`; } -export type ImageAttributes = astroHTML.JSX.HTMLAttributes; -export type PictureAttributes = astroHTML.JSX.HTMLAttributes; +export type ImageAttributes = Partial; export interface HostedImageService { /** From 2e229707e246f1650644191a7a0fd7ba4389afdf Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Thu, 7 Jul 2022 14:49:34 -0500 Subject: [PATCH 08/18] going back to letting loaders add extra HTML attributes --- packages/integrations/image/components/Image.astro | 2 +- packages/integrations/image/src/get-image.ts | 11 +++++++---- packages/integrations/image/src/loaders/sharp.ts | 11 +++++++++++ packages/integrations/image/src/types.ts | 10 +++++++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index 80ab545c1e6b..4f959f406ba3 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -4,7 +4,7 @@ import loader from 'virtual:image-loader'; import { getImage } from '../src/index.js'; import type { ImageAttributes, ImageMetadata, TransformOptions, OutputFormat } from '../src/types.js'; -export interface LocalImageProps extends Omit { +export interface LocalImageProps extends Omit, Omit { src: ImageMetadata | Promise<{ default: ImageMetadata }>; } diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/get-image.ts index dc314e01f1b1..5d1bc1e806c0 100644 --- a/packages/integrations/image/src/get-image.ts +++ b/packages/integrations/image/src/get-image.ts @@ -111,6 +111,7 @@ function getImageAttributes(src: string, transform: TransformOptions): ImageAttr (globalThis as any).loader = loader; const resolved = await resolveTransform(transform); + const attributes = await loader.getImageAttributes(resolved); // For SSR services, build URLs for the injected route if (isSSRService(loader)) { @@ -126,10 +127,12 @@ function getImageAttributes(src: string, transform: TransformOptions): ImageAttr ? (globalThis as any).filenameFormat(resolved, searchParams) : `${ROUTE_PATTERN}?${searchParams.toString()}`; - // Windows compat - return getImageAttributes(slash(src), resolved); + return { + ...attributes, + src: slash(src), // Windows compat + }; } - // For hosted services, return the `src` attribute as-is - return getImageAttributes(await loader.getImageSrc(resolved), resolved); + // For hosted services, return the `` attributes as-is + return attributes; } diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts index ed36d501b7d9..86c18839d086 100644 --- a/packages/integrations/image/src/loaders/sharp.ts +++ b/packages/integrations/image/src/loaders/sharp.ts @@ -3,6 +3,17 @@ import type { OutputFormat, SSRImageService, TransformOptions } from '../types'; import { isAspectRatioString, isOutputFormat } from '../utils.js'; class SharpService implements SSRImageService { + async getImageAttributes(transform: TransformOptions) { + // strip off the known attributes + const { width, height, src, format, quality, aspectRatio, ...rest } = transform; + + return { + ...rest, + width: width, + height: height, + }; + } + serializeTransform(transform: TransformOptions) { const searchParams = new URLSearchParams(); diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index e1425965e775..415f1c3dff3d 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -76,12 +76,16 @@ export type ImageAttributes = Partial; export interface HostedImageService { /** - * Gets the `src` attribute needed for the server rendered `` element. + * Gets the HTML attributes needed for the server rendered `` element. */ - getImageSrc(transform: T): Promise; + getImageAttributes(transform: T): Promise; } -export interface SSRImageService { +export interface SSRImageService extends HostedImageService { + /** + * Gets tthe HTML attributes needed for the server rendered `` element. + */ + getImageAttributes(transform: T): Promise>; /** * Serializes image transformation properties to URLSearchParams, used to build * the final `src` that points to the self-hosted SSR endpoint. From 04e23797035c0415118530ecf413dd2a8b540a75 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Fri, 8 Jul 2022 13:13:37 -0500 Subject: [PATCH 09/18] Always use lazy loading and async decoding --- packages/integrations/image/components/Image.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index 4f959f406ba3..9aa24a5fa7d6 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -22,7 +22,7 @@ const props = Astro.props as Props; const attrs = await getImage(loader, props); --- - + diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/get-image.ts index 5d1bc1e806c0..fe8d35df6bd5 100644 --- a/packages/integrations/image/src/get-image.ts +++ b/packages/integrations/image/src/get-image.ts @@ -87,16 +87,6 @@ async function resolveTransform(input: GetImageTransform): Promise` for the transformed image. * diff --git a/packages/integrations/image/src/get-picture.ts b/packages/integrations/image/src/get-picture.ts new file mode 100644 index 000000000000..370da0678504 --- /dev/null +++ b/packages/integrations/image/src/get-picture.ts @@ -0,0 +1,79 @@ +import { lookup } from 'mrmime'; +import { extname } from 'path'; +import { getImage } from './get-image.js'; +import { ImageAttributes, ImageMetadata, ImageService, OutputFormat, TransformOptions } from './types.js'; +import { parseAspectRatio } from './utils.js'; + +export interface GetPictureParams { + loader: ImageService; + src: string | ImageMetadata | Promise<{ default: ImageMetadata }>; + widths: number[]; + formats: OutputFormat[]; + aspectRatio?: TransformOptions['aspectRatio']; +} + +export interface GetPictureResult { + image: ImageAttributes; + sources: { type: string; srcset: string; }[]; +} + +async function resolveAspectRatio({ src, aspectRatio }: GetPictureParams) { + if (typeof src === 'string') { + return parseAspectRatio(aspectRatio); + } else { + const metadata = 'then' in src ? (await src).default : src; + return parseAspectRatio(aspectRatio) || metadata.width / metadata.height; + } +} + +async function resolveFormats({ src, formats }: GetPictureParams) { + const unique = new Set(formats); + + if (typeof src === 'string') { + unique.add(extname(src).replace('.', '') as OutputFormat); + } else { + const metadata = 'then' in src ? (await src).default : src; + unique.add(extname(metadata.src).replace('.', '') as OutputFormat); + } + + return [...unique]; +} + +export async function getPicture(params: GetPictureParams): Promise { + const { loader, src, widths, formats } = params; + + const aspectRatio = await resolveAspectRatio(params); + + if (!aspectRatio) { + throw new Error('`aspectRatio` must be provided for remote images'); + } + + async function getSource(format: OutputFormat) { + const imgs = await Promise.all(widths.map(async (width) => { + const img = await getImage(loader, { src, format, width, height: Math.round(width / aspectRatio!) }); + return `${img.src} ${width}w`; + })) + + return { + type: lookup(format) || format, + srcset: imgs.join(',') + }; + } + + // always include the original image format + const allFormats = await resolveFormats(params); + + const image = await getImage(loader, { + src, + width: Math.max(...widths), + aspectRatio, + format: allFormats[allFormats.length - 1] + }); + + const sources = await Promise.all(allFormats.map(format => getSource(format))); + + return { + sources, + image + } +} diff --git a/packages/integrations/image/src/utils.ts b/packages/integrations/image/src/utils.ts index 4e5b0ebc1208..44c338cf4100 100644 --- a/packages/integrations/image/src/utils.ts +++ b/packages/integrations/image/src/utils.ts @@ -66,9 +66,9 @@ export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) { // parse aspect ratio strings, if required (ex: "16:9") if (typeof aspectRatio === 'number') { - aspectRatio = aspectRatio; + return aspectRatio; } else { const [width, height] = aspectRatio.split(':'); - aspectRatio = parseInt(width) / parseInt(height); + return parseInt(width) / parseInt(height); } } From 8779f04f9de1693edce3382a5180a8d24a644f1c Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Fri, 8 Jul 2022 13:54:45 -0500 Subject: [PATCH 11/18] Adding test coverage for --- .../image/components/Picture.astro | 10 +- packages/integrations/image/src/types.ts | 1 + .../test/fixtures/basic-image/package.json | 2 +- .../basic-image/src/pages/index.astro | 2 +- .../fixtures/basic-picture/astro.config.mjs | 8 + .../test/fixtures/basic-picture/package.json | 10 + .../fixtures/basic-picture/public/favicon.ico | Bin 0 -> 4286 bytes .../fixtures/basic-picture/server/server.mjs | 44 +++ .../src/assets/blog/introducing-astro.jpg | Bin 0 -> 276382 bytes .../basic-picture/src/assets/social.jpg | Bin 0 -> 25266 bytes .../basic-picture/src/assets/social.png | Bin 0 -> 1512228 bytes .../basic-picture/src/pages/index.astro | 17 ++ .../integrations/image/test/image-ssg.test.js | 41 ++- .../integrations/image/test/image-ssr.test.js | 45 +++ .../image/test/picture-ssg.test.js | 263 +++++++++++++++++ .../image/test/picture-ssr.test.js | 278 ++++++++++++++++++ pnpm-lock.yaml | 10 + 17 files changed, 723 insertions(+), 8 deletions(-) create mode 100644 packages/integrations/image/test/fixtures/basic-picture/astro.config.mjs create mode 100644 packages/integrations/image/test/fixtures/basic-picture/package.json create mode 100644 packages/integrations/image/test/fixtures/basic-picture/public/favicon.ico create mode 100644 packages/integrations/image/test/fixtures/basic-picture/server/server.mjs create mode 100644 packages/integrations/image/test/fixtures/basic-picture/src/assets/blog/introducing-astro.jpg create mode 100644 packages/integrations/image/test/fixtures/basic-picture/src/assets/social.jpg create mode 100644 packages/integrations/image/test/fixtures/basic-picture/src/assets/social.png create mode 100644 packages/integrations/image/test/fixtures/basic-picture/src/pages/index.astro create mode 100644 packages/integrations/image/test/picture-ssg.test.js create mode 100644 packages/integrations/image/test/picture-ssr.test.js diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro index aed29519fdca..98b2c593872b 100644 --- a/packages/integrations/image/components/Picture.astro +++ b/packages/integrations/image/components/Picture.astro @@ -2,16 +2,16 @@ // @ts-ignore import loader from 'virtual:image-loader'; import { getPicture } from '../src/get-picture.js'; -import type { ImageAttributes, ImageMetadata, OutputFormat, TransformOptions } from '../src/types.js'; +import type { ImageAttributes, ImageMetadata, OutputFormat, PictureAttributes, TransformOptions } from '../src/types.js'; -export interface LocalImageProps extends Omit, Omit { +export interface LocalImageProps extends Omit, Omit, Omit { src: ImageMetadata | Promise<{ default: ImageMetadata }>; sizes: HTMLImageElement['sizes']; widths: number[]; formats?: OutputFormat[]; } -export interface RemoteImageProps extends TransformOptions, ImageAttributes { +export interface RemoteImageProps extends PictureAttributes, TransformOptions, ImageAttributes { src: string; sizes: HTMLImageElement['sizes']; widths: number[]; @@ -21,12 +21,12 @@ export interface RemoteImageProps extends TransformOptions, ImageAttributes { export type Props = LocalImageProps | RemoteImageProps; -const { src, sizes, widths, aspectRatio, formats = ['avif', 'webp'] } = Astro.props as Props; +const { src, sizes, widths, aspectRatio, formats = ['avif', 'webp'], ...attrs } = Astro.props as Props; const { image, sources } = await getPicture({ loader, src, widths, formats, aspectRatio }); --- - + {sources.map(attrs => ( ))} diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index 415f1c3dff3d..662655737b30 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -73,6 +73,7 @@ export interface TransformOptions { } export type ImageAttributes = Partial; +export type PictureAttributes = Partial; export interface HostedImageService { /** diff --git a/packages/integrations/image/test/fixtures/basic-image/package.json b/packages/integrations/image/test/fixtures/basic-image/package.json index 42b4411a4d7d..502e42c96a85 100644 --- a/packages/integrations/image/test/fixtures/basic-image/package.json +++ b/packages/integrations/image/test/fixtures/basic-image/package.json @@ -1,5 +1,5 @@ { - "name": "@test/sharp", + "name": "@test/basic-image", "version": "0.0.0", "private": true, "dependencies": { diff --git a/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro b/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro index 6ee02360b72e..34deda90e38f 100644 --- a/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro +++ b/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro @@ -12,6 +12,6 @@ import { Image } from '@astrojs/image';

- + diff --git a/packages/integrations/image/test/fixtures/basic-picture/astro.config.mjs b/packages/integrations/image/test/fixtures/basic-picture/astro.config.mjs new file mode 100644 index 000000000000..45a11dc9dda8 --- /dev/null +++ b/packages/integrations/image/test/fixtures/basic-picture/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; +import image from '@astrojs/image'; + +// https://astro.build/config +export default defineConfig({ + site: 'http://localhost:3000', + integrations: [image()] +}); diff --git a/packages/integrations/image/test/fixtures/basic-picture/package.json b/packages/integrations/image/test/fixtures/basic-picture/package.json new file mode 100644 index 000000000000..23c91f0095b4 --- /dev/null +++ b/packages/integrations/image/test/fixtures/basic-picture/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/basic-picture", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/image": "workspace:*", + "@astrojs/node": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/image/test/fixtures/basic-picture/public/favicon.ico b/packages/integrations/image/test/fixtures/basic-picture/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..578ad458b8906c08fbed84f42b045fea04db89d1 GIT binary patch literal 4286 zcmchZF=!M)6ox0}Fc8GdTHG!cdIY>nA!3n2f|wxIl0rn}Hl#=uf>?-!2r&jMEF^_k zh**lGut*gwBmoNv7AaB&2~nbzULg{WBhPQ{ZVzvF_HL8Cb&hv$_s#qN|IO^o>?+mA zuTW6tU%k~z<&{z+7$G%*nRsTcEO|90xy<-G5&JTt%CgZZCDT4%R?+{Vd^wh>P8_)} z`+dF$HQb9!>1o`Ivn;GInlCw{9T@Rt%q+d^T3Ke%cxkk;$v`{s^zCB9nHAv6w$Vbn z8fb<+eQTNM`;rf9#obfGnV#3+OQEUv4gU;{oA@zol%keY9-e>4W>p7AHmH~&!P7f7!Uj` zwgFeQ=<3G4O;mwWO`L!=R-=y3_~-DPjH3W^3f&jjCfC$o#|oGaahSL`_=f?$&Aa+W z2h8oZ+@?NUcjGW|aWJfbM*ZzxzmCPY`b~RobNrrj=rd`=)8-j`iSW64@0_b6?;GYk zNB+-fzOxlqZ?`y{OA$WigtZXa8)#p#=DPYxH=VeC_Q5q9Cv`mvW6*zU&Gnp1;oPM6 zaK_B3j(l^FyJgYeE9RrmDyhE7W2}}nW%ic#0v@i1E!yTey$W)U>fyd+!@2hWQ!Wa==NAtKoj`f3tp4y$Al`e;?)76?AjdaRR>|?&r)~3Git> zb1)a?uiv|R0_{m#A9c;7)eZ1y6l@yQ#oE*>(Z2fG-&&smPa2QTW>m*^K65^~`coP$ z8y5Y?iS<4Gz{Zg##$1mk)u-0;X|!xu^FCr;ce~X<&UWE&pBgqfYmEJTzpK9I%vr%b z3Ksd6qlPJLI%HFfeXK_^|BXiKZC>Ocu(Kk6hD3G-8usLzVG^q00Qh gz)s7ge@$ApxGu7=(6IGIk+uG&HTev01^#CH3$(Wk5&!@I literal 0 HcmV?d00001 diff --git a/packages/integrations/image/test/fixtures/basic-picture/server/server.mjs b/packages/integrations/image/test/fixtures/basic-picture/server/server.mjs new file mode 100644 index 000000000000..d7a0a7a40f77 --- /dev/null +++ b/packages/integrations/image/test/fixtures/basic-picture/server/server.mjs @@ -0,0 +1,44 @@ +import { createServer } from 'http'; +import fs from 'fs'; +import mime from 'mime'; +import { handler as ssrHandler } from '../dist/server/entry.mjs'; + +const clientRoot = new URL('../dist/client/', import.meta.url); + +async function handle(req, res) { + ssrHandler(req, res, async (err) => { + if (err) { + res.writeHead(500); + res.end(err.stack); + return; + } + + let local = new URL('.' + req.url, clientRoot); + try { + const data = await fs.promises.readFile(local); + res.writeHead(200, { + 'Content-Type': mime.getType(req.url), + }); + res.end(data); + } catch { + res.writeHead(404); + res.end(); + } + }); +} + +const server = createServer((req, res) => { + handle(req, res).catch((err) => { + console.error(err); + res.writeHead(500, { + 'Content-Type': 'text/plain', + }); + res.end(err.toString()); + }); +}); + +server.listen(8085); +console.log('Serving at http://localhost:8085'); + +// Silence weird