UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

157 lines (156 loc) • 6.17 kB
import { isRemotePath } from "@astrojs/internal-helpers/path"; import { AstroError, AstroErrorData } from "../core/errors/index.js"; import { DEFAULT_HASH_PROPS } from "./consts.js"; import { DEFAULT_RESOLUTIONS, LIMITED_RESOLUTIONS, getSizesAttribute, getWidths } from "./layout.js"; import { isLocalService } from "./services/service.js"; import { isImageMetadata } from "./types.js"; import { isESMImportedImage, isRemoteImage, resolveSrc } from "./utils/imageKind.js"; import { inferRemoteSize } from "./utils/remoteProbe.js"; async function getConfiguredImageService() { if (!globalThis?.astroAsset?.imageService) { const { default: service } = await import( // @ts-expect-error "virtual:image-service" ).catch((e) => { const error = new AstroError(AstroErrorData.InvalidImageService); error.cause = e; throw error; }); if (!globalThis.astroAsset) globalThis.astroAsset = {}; globalThis.astroAsset.imageService = service; return service; } return globalThis.astroAsset.imageService; } async function getImage(options, imageConfig) { if (!options || typeof options !== "object") { throw new AstroError({ ...AstroErrorData.ExpectedImageOptions, message: AstroErrorData.ExpectedImageOptions.message(JSON.stringify(options)) }); } if (typeof options.src === "undefined") { throw new AstroError({ ...AstroErrorData.ExpectedImage, message: AstroErrorData.ExpectedImage.message( options.src, "undefined", JSON.stringify(options) ) }); } if (isImageMetadata(options)) { throw new AstroError(AstroErrorData.ExpectedNotESMImage); } const service = await getConfiguredImageService(); const resolvedOptions = { ...options, src: await resolveSrc(options.src) }; let originalWidth; let originalHeight; let originalFormat; if (options.inferSize && isRemoteImage(resolvedOptions.src) && isRemotePath(resolvedOptions.src)) { const result = await inferRemoteSize(resolvedOptions.src); resolvedOptions.width ??= result.width; resolvedOptions.height ??= result.height; originalWidth = result.width; originalHeight = result.height; originalFormat = result.format; delete resolvedOptions.inferSize; } const originalFilePath = isESMImportedImage(resolvedOptions.src) ? resolvedOptions.src.fsPath : void 0; const clonedSrc = isESMImportedImage(resolvedOptions.src) ? ( // @ts-expect-error - clone is a private, hidden prop resolvedOptions.src.clone ?? resolvedOptions.src ) : resolvedOptions.src; if (isESMImportedImage(clonedSrc)) { originalWidth = clonedSrc.width; originalHeight = clonedSrc.height; originalFormat = clonedSrc.format; } if (originalWidth && originalHeight) { const aspectRatio = originalWidth / originalHeight; if (resolvedOptions.height && !resolvedOptions.width) { resolvedOptions.width = Math.round(resolvedOptions.height * aspectRatio); } else if (resolvedOptions.width && !resolvedOptions.height) { resolvedOptions.height = Math.round(resolvedOptions.width / aspectRatio); } else if (!resolvedOptions.width && !resolvedOptions.height) { resolvedOptions.width = originalWidth; resolvedOptions.height = originalHeight; } } resolvedOptions.src = clonedSrc; const layout = options.layout ?? imageConfig.experimentalLayout; if (imageConfig.experimentalResponsiveImages && layout) { resolvedOptions.widths ||= getWidths({ width: resolvedOptions.width, layout, originalWidth, breakpoints: imageConfig.experimentalBreakpoints?.length ? imageConfig.experimentalBreakpoints : isLocalService(service) ? LIMITED_RESOLUTIONS : DEFAULT_RESOLUTIONS }); resolvedOptions.sizes ||= getSizesAttribute({ width: resolvedOptions.width, layout }); if (resolvedOptions.priority) { resolvedOptions.loading ??= "eager"; resolvedOptions.decoding ??= "sync"; resolvedOptions.fetchpriority ??= "high"; } else { resolvedOptions.loading ??= "lazy"; resolvedOptions.decoding ??= "async"; resolvedOptions.fetchpriority ??= "auto"; } delete resolvedOptions.priority; delete resolvedOptions.densities; } const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions; const srcSetTransforms = service.getSrcSet ? await service.getSrcSet(validatedOptions, imageConfig) : []; let imageURL = await service.getURL(validatedOptions, imageConfig); const matchesOriginal = (transform) => transform.width === originalWidth && transform.height === originalHeight && transform.format === originalFormat; let srcSets = await Promise.all( srcSetTransforms.map(async (srcSet) => { return { transform: srcSet.transform, url: matchesOriginal(srcSet.transform) ? imageURL : await service.getURL(srcSet.transform, imageConfig), descriptor: srcSet.descriptor, attributes: srcSet.attributes }; }) ); if (isLocalService(service) && globalThis.astroAsset.addStaticImage && !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)) { const propsToHash = service.propertiesToHash ?? DEFAULT_HASH_PROPS; imageURL = globalThis.astroAsset.addStaticImage( validatedOptions, propsToHash, originalFilePath ); srcSets = srcSetTransforms.map((srcSet) => { return { transform: srcSet.transform, url: matchesOriginal(srcSet.transform) ? imageURL : globalThis.astroAsset.addStaticImage(srcSet.transform, propsToHash, originalFilePath), descriptor: srcSet.descriptor, attributes: srcSet.attributes }; }); } return { rawOptions: resolvedOptions, options: validatedOptions, src: imageURL, srcSet: { values: srcSets, attribute: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(", ") }, attributes: service.getHTMLAttributes !== void 0 ? await service.getHTMLAttributes(validatedOptions, imageConfig) : {} }; } export { getConfiguredImageService, getImage };