mirror of
https://github.com/withastro/astro.git
synced 2025-01-22 10:31:53 -05:00
feat(assets): Add property to image services to control which properties to use for hashing (#8984)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
100b61ab5a
commit
26b1484e80
10 changed files with 86 additions and 9 deletions
5
.changeset/new-islands-lick.md
Normal file
5
.changeset/new-islands-lick.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Adds a new property `propertiesToHash` to the Image Services API to allow specifying which properties of `getImage()` / `<Image />` / `<Picture />` should be used for hashing the result files when doing local transformations. For most services, this will include properties such as `src`, `width` or `quality` that directly changes the content of the generated image.
|
|
@ -26,3 +26,4 @@ export const VALID_SUPPORTED_FORMATS = [
|
|||
] as const;
|
||||
export const DEFAULT_OUTPUT_FORMAT = 'webp' as const;
|
||||
export const VALID_OUTPUT_FORMATS = ['avif', 'png', 'webp', 'jpeg', 'jpg', 'svg'] as const;
|
||||
export const DEFAULT_HASH_PROPS = ['src', 'width', 'height', 'format', 'quality'];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { isRemotePath } from '@astrojs/internal-helpers/path';
|
||||
import type { AstroConfig, AstroSettings } from '../@types/astro.js';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import { DEFAULT_HASH_PROPS } from './consts.js';
|
||||
import { isLocalService, type ImageService } from './services/service.js';
|
||||
import type {
|
||||
GetImageResult,
|
||||
|
@ -114,10 +115,11 @@ export async function getImage(
|
|||
globalThis.astroAsset.addStaticImage &&
|
||||
!(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)
|
||||
) {
|
||||
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions);
|
||||
const propsToHash = service.propertiesToHash ?? DEFAULT_HASH_PROPS;
|
||||
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions, propsToHash);
|
||||
srcSets = srcSetTransforms.map((srcSet) => ({
|
||||
transform: srcSet.transform,
|
||||
url: globalThis.astroAsset.addStaticImage!(srcSet.transform),
|
||||
url: globalThis.astroAsset.addStaticImage!(srcSet.transform, propsToHash),
|
||||
descriptor: srcSet.descriptor,
|
||||
attributes: srcSet.attributes,
|
||||
}));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { AstroConfig } from '../../@types/astro.js';
|
||||
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
|
||||
import { isRemotePath, joinPaths } from '../../core/path.js';
|
||||
import { DEFAULT_OUTPUT_FORMAT, VALID_SUPPORTED_FORMATS } from '../consts.js';
|
||||
import { DEFAULT_HASH_PROPS, DEFAULT_OUTPUT_FORMAT, VALID_SUPPORTED_FORMATS } from '../consts.js';
|
||||
import { isESMImportedImage, isRemoteAllowed } from '../internal.js';
|
||||
import type { ImageOutputFormat, ImageTransform, UnresolvedSrcSetValue } from '../types.js';
|
||||
|
||||
|
@ -100,6 +100,13 @@ export interface LocalImageService<T extends Record<string, any> = Record<string
|
|||
transform: LocalImageTransform,
|
||||
imageConfig: ImageConfig<T>
|
||||
) => Promise<{ data: Buffer; format: ImageOutputFormat }>;
|
||||
|
||||
/**
|
||||
* A list of properties that should be used to generate the hash for the image.
|
||||
*
|
||||
* Generally, this should be all the properties that can change the result of the image. By default, this is `src`, `width`, `height`, `quality`, and `format`.
|
||||
*/
|
||||
propertiesToHash?: string[];
|
||||
}
|
||||
|
||||
export type BaseServiceTransform = {
|
||||
|
@ -131,6 +138,7 @@ export type BaseServiceTransform = {
|
|||
*
|
||||
*/
|
||||
export const baseService: Omit<LocalImageService, 'transform'> = {
|
||||
propertiesToHash: DEFAULT_HASH_PROPS,
|
||||
validateOptions(options) {
|
||||
// `src` is missing or is `undefined`.
|
||||
if (!options.src || (typeof options.src !== 'string' && typeof options.src !== 'object')) {
|
||||
|
|
|
@ -17,7 +17,7 @@ declare global {
|
|||
// eslint-disable-next-line no-var
|
||||
var astroAsset: {
|
||||
imageService?: ImageService;
|
||||
addStaticImage?: ((options: ImageTransform) => string) | undefined;
|
||||
addStaticImage?: ((options: ImageTransform, hashProperties: string[]) => string) | undefined;
|
||||
staticImages?: AssetsGlobalStaticImagesList;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,9 +16,20 @@ export function propsToFilename(transform: ImageTransform, hash: string) {
|
|||
return `/${filename}_${hash}${outputExt}`;
|
||||
}
|
||||
|
||||
export function hashTransform(transform: ImageTransform, imageService: string) {
|
||||
export function hashTransform(
|
||||
transform: ImageTransform,
|
||||
imageService: string,
|
||||
propertiesToHash: string[]
|
||||
) {
|
||||
// Extract the fields we want to hash
|
||||
const { alt, class: className, style, widths, densities, ...rest } = transform;
|
||||
const hashFields = { ...rest, imageService };
|
||||
const hashFields = propertiesToHash.reduce(
|
||||
(acc, prop) => {
|
||||
// It's possible for `transform[prop]` here to be undefined, or null, but that's fine because it's still consistent
|
||||
// between different transforms. (ex: every transform without a height will explicitly have a `height: undefined` property)
|
||||
acc[prop] = transform[prop];
|
||||
return acc;
|
||||
},
|
||||
{ imageService } as Record<string, unknown>
|
||||
);
|
||||
return shorthash(deterministicString(hashFields));
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ export default function assets({
|
|||
return;
|
||||
}
|
||||
|
||||
globalThis.astroAsset.addStaticImage = (options) => {
|
||||
globalThis.astroAsset.addStaticImage = (options, hashProperties) => {
|
||||
if (!globalThis.astroAsset.staticImages) {
|
||||
globalThis.astroAsset.staticImages = new Map<
|
||||
string,
|
||||
|
@ -88,7 +88,11 @@ export default function assets({
|
|||
const originalImagePath = (
|
||||
isESMImportedImage(options.src) ? options.src.src : options.src
|
||||
).replace(settings.config.build.assetsPrefix || '', '');
|
||||
const hash = hashTransform(options, settings.config.image.service.entrypoint);
|
||||
const hash = hashTransform(
|
||||
options,
|
||||
settings.config.image.service.entrypoint,
|
||||
hashProperties
|
||||
);
|
||||
|
||||
let finalFilePath: string;
|
||||
let transformsForPath = globalThis.astroAsset.staticImages.get(originalImagePath);
|
||||
|
|
|
@ -934,6 +934,29 @@ describe('astro:image', () => {
|
|||
|
||||
expect(isReusingCache).to.be.true;
|
||||
});
|
||||
|
||||
describe('custom service in build', () => {
|
||||
it('uses configured hashes properties', async () => {
|
||||
await fixture.build();
|
||||
const html = await fixture.readFile('/imageDeduplication/index.html');
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const allTheSamePath = $('#all-the-same img')
|
||||
.map((_, el) => $(el).attr('src'))
|
||||
.get();
|
||||
|
||||
expect(allTheSamePath.every((path) => path === allTheSamePath[0])).to.equal(true);
|
||||
|
||||
const useCustomHashProperty = $('#use-data img')
|
||||
.map((_, el) => $(el).attr('src'))
|
||||
.get();
|
||||
expect(useCustomHashProperty.every((path) => path === useCustomHashProperty[0])).to.equal(
|
||||
false
|
||||
);
|
||||
expect(useCustomHashProperty[1]).to.not.equal(allTheSamePath[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev ssr', () => {
|
||||
|
|
22
packages/astro/test/fixtures/core-image-ssg/src/pages/imageDeduplication.astro
vendored
Normal file
22
packages/astro/test/fixtures/core-image-ssg/src/pages/imageDeduplication.astro
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import myImage from "../assets/penguin1.jpg";
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="all-the-same">
|
||||
<Image src={myImage} alt="a penguin" />
|
||||
<Image src={myImage} alt="a penguin" class="something" />
|
||||
<Image src={myImage} alt="a penguin" id="something-else" class="something" />
|
||||
<Image src={myImage} alt="a penguin" id="something-else" class="something" transition:animate={"none"} transition:name='' transition:persist style="color: red" />
|
||||
</div>
|
||||
|
||||
<div id="use-data">
|
||||
<Image src={myImage} alt="a penguin" />
|
||||
<Image src={myImage} alt="a penguin" data-custom="value" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -15,6 +15,7 @@ export function testImageService(config = {}) {
|
|||
/** @type {import("../dist/@types/astro").LocalImageService} */
|
||||
export default {
|
||||
...baseService,
|
||||
propertiesToHash: [...baseService.propertiesToHash, 'data-custom'],
|
||||
getHTMLAttributes(options, serviceConfig) {
|
||||
options['data-service'] = 'my-custom-service';
|
||||
if (serviceConfig.service.config.foo) {
|
||||
|
|
Loading…
Reference in a new issue