diff --git a/.changeset/new-coats-check.md b/.changeset/new-coats-check.md new file mode 100644 index 000000000000..e749f551b2b3 --- /dev/null +++ b/.changeset/new-coats-check.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix multiple Image / getImage calls with the same image causing multiple duplicate images to be generated diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index e89b9d5da097..5c5d82e77a82 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -79,7 +79,9 @@ export async function getImage(options: ImageTransform): Promise }; } -export function getStaticImageList(): Iterable<[ImageTransform, string]> { +export function getStaticImageList(): Iterable< + [string, { path: string; options: ImageTransform }] +> { if (!globalThis?.astroAsset?.staticImages) { return []; } diff --git a/packages/astro/src/assets/types.ts b/packages/astro/src/assets/types.ts index 93f742e20bfd..846e4e4cdc2f 100644 --- a/packages/astro/src/assets/types.ts +++ b/packages/astro/src/assets/types.ts @@ -12,7 +12,7 @@ declare global { var astroAsset: { imageService?: ImageService; addStaticImage?: ((options: ImageTransform) => string) | undefined; - staticImages?: Map; + staticImages?: Map; }; } diff --git a/packages/astro/src/assets/utils/transformToPath.ts b/packages/astro/src/assets/utils/transformToPath.ts index 1574af3146e0..c2537b41485e 100644 --- a/packages/astro/src/assets/utils/transformToPath.ts +++ b/packages/astro/src/assets/utils/transformToPath.ts @@ -4,7 +4,7 @@ import { shorthash } from '../../runtime/server/shorthash.js'; import { isESMImportedImage } from '../internal.js'; import type { ImageTransform } from '../types.js'; -export function propsToFilename(transform: ImageTransform, imageService: string) { +export function propsToFilename(transform: ImageTransform, hash: string) { if (!isESMImportedImage(transform.src)) { return transform.src; } @@ -12,9 +12,13 @@ export function propsToFilename(transform: ImageTransform, imageService: string) let filename = removeQueryString(transform.src.src); const ext = extname(filename); filename = basename(filename, ext); + const outputExt = transform.format ? `.${transform.format}` : ext; + return `/${filename}_${hash}${outputExt}`; +} + +export function hashTransform(transform: ImageTransform, imageService: string) { // take everything from transform except alt, which is not used in the hash const { alt, ...rest } = transform; const hashFields = { ...rest, imageService }; - const outputExt = transform.format ? `.${transform.format}` : ext; - return `/${filename}_${shorthash(JSON.stringify(hashFields))}${outputExt}`; + return shorthash(JSON.stringify(hashFields)); } diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 3d96d08f1012..52efe2487429 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -16,7 +16,7 @@ import { copyWasmFiles } from './services/vendor/squoosh/copy-wasm.js'; import { emitESMImage } from './utils/emitAsset.js'; import { imageMetadata } from './utils/metadata.js'; import { getOrigQueryParams } from './utils/queryParams.js'; -import { propsToFilename } from './utils/transformToPath.js'; +import { hashTransform, propsToFilename } from './utils/transformToPath.js'; const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID; @@ -153,12 +153,17 @@ export default function assets({ globalThis.astroAsset.addStaticImage = (options) => { if (!globalThis.astroAsset.staticImages) { - globalThis.astroAsset.staticImages = new Map(); + globalThis.astroAsset.staticImages = new Map< + string, + { path: string; options: ImageTransform } + >(); } + const hash = hashTransform(options, settings.config.image.service); + let filePath: string; - if (globalThis.astroAsset.staticImages.has(options)) { - filePath = globalThis.astroAsset.staticImages.get(options)!; + if (globalThis.astroAsset.staticImages.has(hash)) { + filePath = globalThis.astroAsset.staticImages.get(hash)!.path; } else { // If the image is not imported, we can return the path as-is, since static references // should only point ot valid paths for builds or remote images @@ -167,12 +172,9 @@ export default function assets({ } filePath = prependForwardSlash( - joinPaths( - settings.config.build.assets, - propsToFilename(options, settings.config.image.service) - ) + joinPaths(settings.config.build.assets, propsToFilename(options, hash)) ); - globalThis.astroAsset.staticImages.set(options, filePath); + globalThis.astroAsset.staticImages.set(hash, { path: filePath, options: options }); } return prependForwardSlash(joinPaths(settings.config.base, filePath)); diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 52e4204a999d..e3b4ff2ed306 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -108,7 +108,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn if (opts.settings.config.experimental.assets) { info(opts.logging, null, `\n${bgGreen(black(` generating optimized images `))}`); for (const imageData of getStaticImageList()) { - await generateImage(opts, imageData[0], imageData[1]); + await generateImage(opts, imageData[1].options, imageData[1].path); } delete globalThis.astroAsset.addStaticImage; }