fix(markdown): Fix Markdown images breaking the build (#8598)

* fix(markdown): Fix Markdown images breaking the build

* chore: changeset

* Update packages/astro/src/core/errors/errors-data.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Fix tla chunking

* One directory up

* Down we go

---------

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
This commit is contained in:
Erika 2023-09-21 19:07:33 +02:00 committed by GitHub
parent 5a988eaf60
commit bdd267d089
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 43 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix relative images in Markdown breaking the build process in certain circumstances

View file

@ -29,8 +29,8 @@ const qualityTable: Record<
// Squoosh's PNG encoder does not support a quality setting, so we can skip that here // Squoosh's PNG encoder does not support a quality setting, so we can skip that here
}; };
async function getRotationForEXIF(inputBuffer: Buffer): Promise<Operation | undefined> { async function getRotationForEXIF(inputBuffer: Buffer, src?: string): Promise<Operation | undefined> {
const meta = await imageMetadata(inputBuffer); const meta = await imageMetadata(inputBuffer, src);
if (!meta) return undefined; 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. // 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 operations: Operation[] = [];
const rotation = await getRotationForEXIF(inputBuffer); const rotation = await getRotationForEXIF(inputBuffer, transform.src);
if (rotation) { if (rotation) {
operations.push(rotation); operations.push(rotation);

View file

@ -22,11 +22,7 @@ export async function emitESMImage(
return undefined; return undefined;
} }
const fileMetadata = await imageMetadata(fileData); const fileMetadata = await imageMetadata(fileData, id);
if (!fileMetadata) {
return undefined;
}
const emittedImage: ImageMetadata = { const emittedImage: ImageMetadata = {
src: '', src: '',

View file

@ -1,19 +1,23 @@
import probe from 'probe-image-size'; import probe from 'probe-image-size';
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import type { ImageInputFormat, ImageMetadata } from '../types.js'; import type { ImageInputFormat, ImageMetadata } from '../types.js';
export async function imageMetadata(data: Buffer): Promise<Omit<ImageMetadata, 'src'> | undefined> { export async function imageMetadata(
data: Buffer,
src?: string
): Promise<Omit<ImageMetadata, 'src'>> {
const result = probe.sync(data); const result = probe.sync(data);
if (result === null) { 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 { width, height, type, orientation } = result;
const isPortrait = (orientation || 0) >= 5; const isPortrait = (orientation || 0) >= 5;
if (!width || !height || !type) {
return undefined;
}
return { return {
width: isPortrait ? height : width, width: isPortrait ? height : width,
height: isPortrait ? width : height, height: isPortrait ? width : height,

View file

@ -2,6 +2,8 @@ import MagicString from 'magic-string';
import type * as vite from 'vite'; import type * as vite from 'vite';
import { normalizePath } from 'vite'; import { normalizePath } from 'vite';
import type { AstroPluginOptions, ImageTransform } from '../@types/astro.js'; 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 { import {
appendForwardSlash, appendForwardSlash,
joinPaths, 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 // Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev
{ {
name: 'astro:assets', 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) { async resolveId(id) {
if (id === VIRTUAL_SERVICE_ID) { if (id === VIRTUAL_SERVICE_ID) {
return await this.resolve(settings.config.image.service.entrypoint); return await this.resolve(settings.config.image.service.entrypoint);
@ -125,6 +139,14 @@ export default function assets({
} }
if (assetRegex.test(id)) { if (assetRegex.test(id)) {
const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile); 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)}`; return `export default ${JSON.stringify(meta)}`;
} }
}, },

View file

@ -620,8 +620,42 @@ export const ExpectedImageOptions = {
message: (options: string) => message: (options: string) =>
`Expected getImage() parameter to be an object. Received \`${options}\`.`, `Expected getImage() parameter to be an object. Received \`${options}\`.`,
} satisfies ErrorData; } satisfies ErrorData;
/** /**
* @docs * @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 * @message
* Could not find requested image `IMAGE_PATH` at `FULL_IMAGE_PATH`. * Could not find requested image `IMAGE_PATH` at `FULL_IMAGE_PATH`.
* @see * @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.', hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.',
} satisfies ErrorData; } satisfies ErrorData;
/** /**
* @docs * @docs
* @description * @description

View file

@ -12,7 +12,8 @@ import { normalizePath } from 'vite';
import type { AstroSettings } from '../@types/astro.js'; import type { AstroSettings } from '../@types/astro.js';
import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js'; import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js';
import type { Logger } from '../core/logger/core.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 type { PluginMetadata } from '../vite-plugin-astro/types.js';
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.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; const { headings, imagePaths: rawImagePaths, frontmatter } = renderResult.metadata;
// Resolve all the extracted images from the content // 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()) { for (const imagePath of rawImagePaths.values()) {
imagePaths.push({ imagePaths.push({
raw: imagePath, raw: imagePath,
resolved: resolved:
(await this.resolve(imagePath, id))?.id ?? path.join(path.dirname(id), imagePath), (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)};` : ''} ${layout ? `import Layout from ${JSON.stringify(layout)};` : ''}
import { getImage } from "astro:assets"; import { getImage } from "astro:assets";
${imagePaths.map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`)}
export const images = { const images = async function() {
${imagePaths.map( return {
(entry) => ${imagePaths
`'${entry.raw}': await getImageSafely((await import("${entry.raw}")).default, "${ .map((entry) => `"${entry.raw}": await getImage({src: Astro__${entry.safeName}})`)
entry.raw .join('\n')}
}", "${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}" },
});
} }
return await getImage({src: imageSrc})
} }
function updateImageReferences(html) { async function updateImageReferences(html) {
return html.replaceAll( return images().then((images) => {
/__ASTRO_IMAGE_="([^"]+)"/gm, return html.replaceAll(/__ASTRO_IMAGE_="([^"]+)"/gm, (full, imagePath) =>
(full, imagePath) => spreadAttributes({src: images[imagePath].src, ...images[imagePath].attributes}) 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 frontmatter = ${JSON.stringify(frontmatter)};
export const file = ${JSON.stringify(fileId)}; export const file = ${JSON.stringify(fileId)};