diff --git a/.changeset/honest-snakes-peel.md b/.changeset/honest-snakes-peel.md new file mode 100644 index 000000000..c9cf064f6 --- /dev/null +++ b/.changeset/honest-snakes-peel.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix relative images in Markdown breaking the build process in certain circumstances diff --git a/packages/astro/src/assets/services/squoosh.ts b/packages/astro/src/assets/services/squoosh.ts index 4ba78f8d3..023e92fe5 100644 --- a/packages/astro/src/assets/services/squoosh.ts +++ b/packages/astro/src/assets/services/squoosh.ts @@ -29,8 +29,8 @@ const qualityTable: Record< // Squoosh's PNG encoder does not support a quality setting, so we can skip that here }; -async function getRotationForEXIF(inputBuffer: Buffer): Promise { - const meta = await imageMetadata(inputBuffer); +async function getRotationForEXIF(inputBuffer: Buffer, src?: string): Promise { + const meta = await imageMetadata(inputBuffer, src); if (!meta) return undefined; // EXIF orientations are a bit hard to read, but the numbers are actually standard. See https://exiftool.org/TagNames/EXIF.html for a list. @@ -63,7 +63,7 @@ const service: LocalImageService = { const operations: Operation[] = []; - const rotation = await getRotationForEXIF(inputBuffer); + const rotation = await getRotationForEXIF(inputBuffer, transform.src); if (rotation) { operations.push(rotation); diff --git a/packages/astro/src/assets/utils/emitAsset.ts b/packages/astro/src/assets/utils/emitAsset.ts index 9b83a020a..b9ca146b7 100644 --- a/packages/astro/src/assets/utils/emitAsset.ts +++ b/packages/astro/src/assets/utils/emitAsset.ts @@ -22,11 +22,7 @@ export async function emitESMImage( return undefined; } - const fileMetadata = await imageMetadata(fileData); - - if (!fileMetadata) { - return undefined; - } + const fileMetadata = await imageMetadata(fileData, id); const emittedImage: ImageMetadata = { src: '', diff --git a/packages/astro/src/assets/utils/metadata.ts b/packages/astro/src/assets/utils/metadata.ts index 7d7ee7457..fc89ca1ca 100644 --- a/packages/astro/src/assets/utils/metadata.ts +++ b/packages/astro/src/assets/utils/metadata.ts @@ -1,19 +1,23 @@ import probe from 'probe-image-size'; +import { AstroError, AstroErrorData } from '../../core/errors/index.js'; import type { ImageInputFormat, ImageMetadata } from '../types.js'; -export async function imageMetadata(data: Buffer): Promise | undefined> { +export async function imageMetadata( + data: Buffer, + src?: string +): Promise> { const result = probe.sync(data); + if (result === null) { - throw new Error('Failed to probe image size.'); + throw new AstroError({ + ...AstroErrorData.NoImageMetadata, + message: AstroErrorData.NoImageMetadata.message(src), + }); } const { width, height, type, orientation } = result; const isPortrait = (orientation || 0) >= 5; - if (!width || !height || !type) { - return undefined; - } - return { width: isPortrait ? height : width, height: isPortrait ? width : height, diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 0fe45d1ab..9c95b6dc4 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -2,6 +2,8 @@ import MagicString from 'magic-string'; import type * as vite from 'vite'; import { normalizePath } from 'vite'; import type { AstroPluginOptions, ImageTransform } from '../@types/astro.js'; +import { extendManualChunks } from '../core/build/plugins/util.js'; +import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { appendForwardSlash, joinPaths, @@ -28,6 +30,18 @@ export default function assets({ // Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev { name: 'astro:assets', + outputOptions(outputOptions) { + // Specifically split out chunk for asset files to prevent TLA deadlock + // caused by `getImage()` for markdown components. + // https://github.com/rollup/rollup/issues/4708 + extendManualChunks(outputOptions, { + after(id) { + if (id.includes('astro/dist/assets/services/')) { + return `astro-assets-services`; + } + }, + }); + }, async resolveId(id) { if (id === VIRTUAL_SERVICE_ID) { return await this.resolve(settings.config.image.service.entrypoint); @@ -125,6 +139,14 @@ export default function assets({ } if (assetRegex.test(id)) { const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile); + + if (!meta) { + throw new AstroError({ + ...AstroErrorData.ImageNotFound, + message: AstroErrorData.ImageNotFound.message(id), + }); + } + return `export default ${JSON.stringify(meta)}`; } }, diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 1f336e5f8..e4fe35540 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -620,8 +620,42 @@ export const ExpectedImageOptions = { message: (options: string) => `Expected getImage() parameter to be an object. Received \`${options}\`.`, } satisfies ErrorData; + /** * @docs + * @see + * - [Images](https://docs.astro.build/en/guides/images/) + * @description + * Astro could not find an image you imported. Often, this is simply caused by a typo in the path. + * + * Images in Markdown are relative to the current file. To refer to an image that is located in the same folder as the `.md` file, the path should start with `./` + */ +export const ImageNotFound = { + name: 'ImageNotFound', + title: 'Image not found.', + message: (imagePath: string) => `Could not find requested image \`${imagePath}\`. Does it exist?`, + hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.', +} satisfies ErrorData; + +/** + * @docs + * @message Could not process image metadata for `IMAGE_PATH`. + * @see + * - [Images](https://docs.astro.build/en/guides/images/) + * @description + * Astro could not process the metadata of an image you imported. This is often caused by a corrupted or malformed image and re-exporting the image from your image editor may fix this issue. + */ +export const NoImageMetadata = { + name: 'NoImageMetadata', + title: 'Could not process image metadata.', + message: (imagePath: string | undefined) => + `Could not process image metadata${imagePath ? ' for `${imagePath}`' : ''}.`, + hint: 'This is often caused by a corrupted or malformed image. Re-exporting the image from your image editor may fix this issue.', +} satisfies ErrorData; + +/** + * @docs + * @deprecated This error is no longer Markdown specific and as such, as been replaced by `ImageNotFound` * @message * Could not find requested image `IMAGE_PATH` at `FULL_IMAGE_PATH`. * @see @@ -640,6 +674,7 @@ export const MarkdownImageNotFound = { }`, hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.', } satisfies ErrorData; + /** * @docs * @description diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 163baab0d..7d4a97392 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -12,7 +12,8 @@ import { normalizePath } from 'vite'; import type { AstroSettings } from '../@types/astro.js'; import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; -import { isMarkdownFile, rootRelativePath } from '../core/util.js'; +import { isMarkdownFile } from '../core/util.js'; +import { shorthash } from '../runtime/server/shorthash.js'; import type { PluginMetadata } from '../vite-plugin-astro/types.js'; import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js'; @@ -92,12 +93,13 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug const { headings, imagePaths: rawImagePaths, frontmatter } = renderResult.metadata; // Resolve all the extracted images from the content - const imagePaths: { raw: string; resolved: string }[] = []; + const imagePaths: { raw: string; resolved: string; safeName: string }[] = []; for (const imagePath of rawImagePaths.values()) { imagePaths.push({ raw: imagePath, resolved: (await this.resolve(imagePath, id))?.id ?? path.join(path.dirname(id), imagePath), + safeName: shorthash(imagePath), }); } @@ -118,39 +120,28 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug ${layout ? `import Layout from ${JSON.stringify(layout)};` : ''} import { getImage } from "astro:assets"; + ${imagePaths.map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`)} - export const images = { - ${imagePaths.map( - (entry) => - `'${entry.raw}': await getImageSafely((await import("${entry.raw}")).default, "${ - entry.raw - }", "${rootRelativePath(settings.config.root, entry.resolved)}")` - )} - } - - async function getImageSafely(imageSrc, imagePath, resolvedImagePath) { - if (!imageSrc) { - throw new AstroError({ - ...AstroErrorData.MarkdownImageNotFound, - message: AstroErrorData.MarkdownImageNotFound.message( - imagePath, - resolvedImagePath - ), - location: { file: "${id}" }, - }); + const images = async function() { + return { + ${imagePaths + .map((entry) => `"${entry.raw}": await getImage({src: Astro__${entry.safeName}})`) + .join('\n')} } - - return await getImage({src: imageSrc}) } - function updateImageReferences(html) { - return html.replaceAll( - /__ASTRO_IMAGE_="([^"]+)"/gm, - (full, imagePath) => spreadAttributes({src: images[imagePath].src, ...images[imagePath].attributes}) - ); + async function updateImageReferences(html) { + return images().then((images) => { + return html.replaceAll(/__ASTRO_IMAGE_="([^"]+)"/gm, (full, imagePath) => + spreadAttributes({ + src: images[imagePath].src, + ...images[imagePath].attributes, + }) + ); + }); } - const html = updateImageReferences(${JSON.stringify(html)}); + const html = await updateImageReferences(${JSON.stringify(html)}); export const frontmatter = ${JSON.stringify(frontmatter)}; export const file = ${JSON.stringify(fileId)};