0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-31 23:31:30 -05:00

fix(astro): prevent ESM imports being passed directly to getImage (#11279)

* fix(astro): prevent ESM imports being passed directly to getImage

* Apply suggestions from code review

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

---------

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Matt Kane 2024-06-18 14:48:50 +01:00 committed by GitHub
parent bab700d690
commit 9a08d74bc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 79 additions and 12 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Improves type-checking and error handling to catch case where an image import is passed directly to `getImage()`

View file

@ -2,11 +2,12 @@ import type { AstroConfig } from '../@types/astro.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { DEFAULT_HASH_PROPS } from './consts.js';
import { type ImageService, isLocalService } from './services/service.js';
import type {
GetImageResult,
ImageTransform,
SrcSetValue,
UnresolvedImageTransform,
import {
isImageMetadata,
type GetImageResult,
type ImageTransform,
type SrcSetValue,
type UnresolvedImageTransform,
} from './types.js';
import { isESMImportedImage, isRemoteImage, resolveSrc } from './utils/imageKind.js';
import { probe } from './utils/remoteProbe.js';
@ -50,6 +51,10 @@ export async function getImage(
),
});
}
if(isImageMetadata(options)) {
throw new AstroError(AstroErrorData.ExpectedNotESMImage);
}
const service = await getConfiguredImageService();

View file

@ -28,10 +28,14 @@ declare global {
};
}
const isESMImport = Symbol('#isESM');
export type OmitBrand<T> = Omit<T, typeof isESMImport>;
/**
* Type returned by ESM imports of images
*/
export interface ImageMetadata {
export type ImageMetadata = {
src: string;
width: number;
height: number;
@ -39,6 +43,12 @@ export interface ImageMetadata {
orientation?: number;
/** @internal */
fsPath: string;
[isESMImport]?: true;
};
export function isImageMetadata(src: any): src is ImageMetadata {
// For ESM-imported images the fsPath property is set but not enumerable
return src.fsPath && !('fsPath' in src);
}
/**
@ -60,6 +70,8 @@ export type SrcSetValue = UnresolvedSrcSetValue & {
export type UnresolvedImageTransform = Omit<ImageTransform, 'src'> & {
src: ImageMetadata | string | Promise<{ default: ImageMetadata }>;
inferSize?: boolean;
} & {
[isESMImport]?: never;
};
/**

View file

@ -1,6 +1,7 @@
import type { PluginContext } from 'rollup';
import { z } from 'zod';
import { emitESMImage } from '../assets/utils/emitAsset.js';
import type { OmitBrand, ImageMetadata } from '../assets/types.js';
export function createImage(
pluginContext: PluginContext,
@ -10,11 +11,11 @@ export function createImage(
return () => {
return z.string().transform(async (imagePath, ctx) => {
const resolvedFilePath = (await pluginContext.resolve(imagePath, entryFilePath))?.id;
const metadata = await emitESMImage(
const metadata = (await emitESMImage(
resolvedFilePath,
pluginContext.meta.watchMode,
shouldEmitFile ? pluginContext.emitFile : undefined
);
)) as OmitBrand<ImageMetadata>;
if (!metadata) {
ctx.addIssue({

View file

@ -662,6 +662,28 @@ export const ExpectedImageOptions = {
`Expected getImage() parameter to be an object. Received \`${options}\`.`,
} satisfies ErrorData;
/**
* @docs
* @see
* - [Images](https://docs.astro.build/en/guides/images/)
* @description
* An ESM-imported image cannot be passed directly to `getImage()`. Instead, pass an object with the image in the `src` property.
*
* ```diff
* import { getImage } from "astro:assets";
* import myImage from "../assets/my_image.png";
* - const optimizedImage = await getImage( myImage );
* + const optimizedImage = await getImage({ src: myImage });
* ```
*/
export const ExpectedNotESMImage = {
name: 'ExpectedNotESMImage',
title: 'Expected image options, not an ESM-imported image.',
message: 'An ESM-imported image cannot be passed directly to `getImage()`. Instead, pass an object with the image in the `src` property.',
hint: 'Try changing `getImage(myImage)` to `getImage({ src: myImage })`'
} satisfies ErrorData;
/**
* @docs
* @see

View file

@ -725,6 +725,17 @@ describe('astro:image', () => {
assert.equal(logs[0].message.includes('Expected `src` property'), true);
});
it('errors when an ESM imported image is passed directly to getImage', async () => {
logs.length = 0;
let res = await fixture.fetch('/get-image-import-passed');
await res.text();
assert.equal(logs.length >= 1, true);
assert.equal(
logs[0].message.includes('An ESM-imported image cannot be passed directly'),
true
);
});
it('properly error image in Markdown frontmatter is not found', async () => {
logs.length = 0;
let res = await fixture.fetch('/blog/one');

View file

@ -1,9 +1,11 @@
{
"extends": "astro/tsconfigs/base",
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/assets/*": ["src/assets/*"]
"~/assets/*": [
"src/assets/*"
]
},
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 MiB

View file

@ -0,0 +1,7 @@
---
import { getImage } from "astro:assets";
import image from "../images/penguin.png";
const myImage = await getImage(image);
---

View file

@ -1,9 +1,11 @@
{
"extends": "astro/tsconfigs/base",
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/assets/*": ["src/assets/*"]
"~/assets/*": [
"src/assets/*"
]
},
}
}