Skip to content

Commit 39050c6

Browse files
authoredJan 15, 2024
fix: image proxy not working correctly (#9659)
* fix: image proxy not working correctly * fix: only take in valid images * test: add tests * Create slimy-mayflies-vanish.md * nit: remove erika-ism
1 parent 1bf0ddd commit 39050c6

File tree

18 files changed

+129
-30
lines changed

18 files changed

+129
-30
lines changed
 

‎.changeset/slimy-mayflies-vanish.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"astro": patch
3+
---
4+
5+
Fix Astro wrongfully deleting certain images imported with `?url` when used in tandem with `astro:assets`

‎packages/astro/src/assets/internal.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export async function getImage(
6161
: options.src,
6262
};
6363

64+
const originalPath = isESMImportedImage(resolvedOptions.src)
65+
? resolvedOptions.src.fsPath
66+
: resolvedOptions.src;
67+
6468
// Clone the `src` object if it's an ESM import so that we don't refer to any properties of the original object
6569
// Causing our generate step to think the image is used outside of the image optimization pipeline
6670
const clonedSrc = isESMImportedImage(resolvedOptions.src)
@@ -95,10 +99,10 @@ export async function getImage(
9599
!(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)
96100
) {
97101
const propsToHash = service.propertiesToHash ?? DEFAULT_HASH_PROPS;
98-
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions, propsToHash);
102+
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions, propsToHash, originalPath);
99103
srcSets = srcSetTransforms.map((srcSet) => ({
100104
transform: srcSet.transform,
101-
url: globalThis.astroAsset.addStaticImage!(srcSet.transform, propsToHash),
105+
url: globalThis.astroAsset.addStaticImage!(srcSet.transform, propsToHash, originalPath),
102106
descriptor: srcSet.descriptor,
103107
attributes: srcSet.attributes,
104108
}));

‎packages/astro/src/assets/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ declare global {
2020
// eslint-disable-next-line no-var
2121
var astroAsset: {
2222
imageService?: ImageService;
23-
addStaticImage?: ((options: ImageTransform, hashProperties: string[]) => string) | undefined;
23+
addStaticImage?:
24+
| ((options: ImageTransform, hashProperties: string[], fsPath: string) => string)
25+
| undefined;
2426
staticImages?: AssetsGlobalStaticImagesList;
2527
referencedImages?: Set<string>;
2628
};

‎packages/astro/src/assets/utils/emitAsset.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function emitESMImage(
3333
Object.defineProperty(emittedImage, 'fsPath', {
3434
enumerable: false,
3535
writable: false,
36-
value: url,
36+
value: id,
3737
});
3838

3939
// Build

‎packages/astro/src/assets/utils/proxy.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
export function getProxyCode(options: Record<string, any>, isSSR: boolean): string {
1+
import type { ImageMetadata } from '../types.js';
2+
3+
export function getProxyCode(options: ImageMetadata, isSSR: boolean): string {
4+
const stringifiedFSPath = JSON.stringify(options.fsPath);
25
return `
36
new Proxy(${JSON.stringify(options)}, {
47
get(target, name, receiver) {
58
if (name === 'clone') {
69
return structuredClone(target);
710
}
8-
${!isSSR ? 'globalThis.astroAsset.referencedImages.add(target.fsPath);' : ''}
11+
if (name === 'fsPath') {
12+
return ${stringifiedFSPath};
13+
}
14+
${!isSSR ? `globalThis.astroAsset.referencedImages.add(${stringifiedFSPath});` : ''}
915
return target[name];
1016
}
1117
})

‎packages/astro/src/assets/vite-plugin-assets.ts

+27-21
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { hashTransform, propsToFilename } from './utils/transformToPath.js';
1919

2020
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
2121

22-
const assetRegex = new RegExp(`\\.(${VALID_INPUT_FORMATS.join('|')})$`, 'i');
22+
const assetRegex = new RegExp(`\\.(${VALID_INPUT_FORMATS.join('|')})`, 'i');
23+
const assetRegexEnds = new RegExp(`\\.(${VALID_INPUT_FORMATS.join('|')})$`, 'i');
2324

2425
export default function assets({
2526
settings,
@@ -81,7 +82,7 @@ export default function assets({
8182
return;
8283
}
8384

84-
globalThis.astroAsset.addStaticImage = (options, hashProperties) => {
85+
globalThis.astroAsset.addStaticImage = (options, hashProperties, originalPath) => {
8586
if (!globalThis.astroAsset.staticImages) {
8687
globalThis.astroAsset.staticImages = new Map<
8788
string,
@@ -97,11 +98,6 @@ export default function assets({
9798
isESMImportedImage(options.src) ? options.src.src : options.src
9899
).replace(settings.config.build.assetsPrefix || '', '');
99100

100-
// This, however, is the real original path, in `src` and all.
101-
const originalSrcPath = isESMImportedImage(options.src)
102-
? options.src.fsPath
103-
: options.src;
104-
105101
const hash = hashTransform(
106102
options,
107103
settings.config.image.service.entrypoint,
@@ -120,7 +116,7 @@ export default function assets({
120116

121117
if (!transformsForPath) {
122118
globalThis.astroAsset.staticImages.set(finalOriginalImagePath, {
123-
originalSrcPath: originalSrcPath,
119+
originalSrcPath: originalPath,
124120
transforms: new Map(),
125121
});
126122
transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalImagePath)!;
@@ -178,15 +174,25 @@ export default function assets({
178174
resolvedConfig = viteConfig;
179175
},
180176
async load(id, options) {
181-
// If our import has any query params, we'll let Vite handle it
182-
// See https://github.com/withastro/astro/issues/8333
183-
if (id !== removeQueryString(id)) {
184-
return;
185-
}
186177
if (assetRegex.test(id)) {
187-
const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile);
178+
if (!globalThis.astroAsset.referencedImages)
179+
globalThis.astroAsset.referencedImages = new Set();
180+
181+
if (id !== removeQueryString(id)) {
182+
// If our import has any query params, we'll let Vite handle it, nonetheless we'll make sure to not delete it
183+
// See https://github.com/withastro/astro/issues/8333
184+
globalThis.astroAsset.referencedImages.add(removeQueryString(id));
185+
return;
186+
}
188187

189-
if (!meta) {
188+
// If the requested ID doesn't end with a valid image extension, we'll let Vite handle it
189+
if (!assetRegexEnds.test(id)) {
190+
return;
191+
}
192+
193+
const imageMetadata = await emitESMImage(id, this.meta.watchMode, this.emitFile);
194+
195+
if (!imageMetadata) {
190196
throw new AstroError({
191197
...AstroErrorData.ImageNotFound,
192198
message: AstroErrorData.ImageNotFound.message(id),
@@ -197,13 +203,13 @@ export default function assets({
197203
// Since you cannot use image optimization on the client anyway, it's safe to assume that if the user imported
198204
// an image on the client, it should be present in the final build.
199205
if (options?.ssr) {
200-
return `export default ${getProxyCode(meta, isServerLikeOutput(settings.config))}`;
206+
return `export default ${getProxyCode(
207+
imageMetadata,
208+
isServerLikeOutput(settings.config)
209+
)}`;
201210
} else {
202-
if (!globalThis.astroAsset.referencedImages)
203-
globalThis.astroAsset.referencedImages = new Set();
204-
205-
globalThis.astroAsset.referencedImages.add(meta.fsPath);
206-
return `export default ${JSON.stringify(meta)}`;
211+
globalThis.astroAsset.referencedImages.add(imageMetadata.fsPath);
212+
return `export default ${JSON.stringify(imageMetadata)}`;
207213
}
208214
}
209215
},

‎packages/astro/src/content/runtime-assets.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function createImage(pluginContext: PluginContext, entryFilePath: string)
2222
return z.never();
2323
}
2424

25-
return { ...metadata, ASTRO_ASSET: true };
25+
return { ...metadata, ASTRO_ASSET: metadata.fsPath };
2626
});
2727
};
2828
}

‎packages/astro/src/content/vite-plugin-content-imports.ts

+1
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ function stringifyEntryData(data: Record<string, any>, isSSR: boolean): string {
364364
// For Astro assets, add a proxy to track references
365365
if (typeof value === 'object' && 'ASTRO_ASSET' in value) {
366366
const { ASTRO_ASSET, ...asset } = value;
367+
asset.fsPath = ASTRO_ASSET;
367368
return getProxyCode(asset, isSSR);
368369
}
369370
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "@test/core-image-deletion",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"astro": "workspace:*"
7+
},
8+
"scripts": {
9+
"dev": "astro dev"
10+
}
11+
}
Loading
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
import { Image } from "astro:assets";
3+
import onlyOne from "../assets/onlyone.jpg";
4+
import twoOfUs from "../assets/twoofus.jpg";
5+
import twoFromURL from "../assets/url.jpg";
6+
import twoFromURL_URL from "../assets/url.jpg?url";
7+
---
8+
9+
<Image src={onlyOne} alt="Only one of me exists at the end of the build" />
10+
11+
<Image src={twoOfUs} alt="Two of us will exist, because I'm also used as a normal image" /></Image>
12+
<img src={twoOfUs.src} alt="Two of us will exist, because I'm also used as a normal image" />
13+
14+
<Image src={twoFromURL} alt="Two of us will exist, because I'm also imported using ?url" /></Image>
15+
<img src={twoFromURL_URL} alt="Two of us will exist, because I'm also used as a normal image" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "astro/tsconfigs/base",
3+
"compilerOptions": {
4+
"baseUrl": ".",
5+
"paths": {
6+
"~/assets/*": ["src/assets/*"]
7+
},
8+
}
9+
}

‎packages/astro/test/fixtures/core-image-ssg/src/pages/blog/[...slug].astro

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
import { getImage } from 'astro:assets';
32
import { getCollection } from 'astro:content';
43
54
export async function getStaticPaths() {
@@ -11,7 +10,6 @@ export async function getStaticPaths() {
1110
1211
const { entry } = Astro.props;
1312
const { Content } = await entry.render();
14-
const myImage = await getImage(entry.data.image);
1513
---
1614
<html>
1715
<head>
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect } from 'chai';
2+
import { testImageService } from './test-image-service.js';
3+
import { loadFixture } from './test-utils.js';
4+
5+
describe('astro:assets - delete images that are unused', () => {
6+
/** @type {import('./test-utils.js').Fixture} */
7+
let fixture;
8+
9+
describe('build ssg', () => {
10+
before(async () => {
11+
fixture = await loadFixture({
12+
root: './fixtures/core-image-deletion/',
13+
image: {
14+
service: testImageService(),
15+
},
16+
});
17+
18+
await fixture.build();
19+
});
20+
21+
it('should delete images that are only used for optimization', async () => {
22+
const imagesOnlyOptimized = await fixture.glob('_astro/onlyone.*.*');
23+
expect(imagesOnlyOptimized).to.have.lengthOf(1);
24+
});
25+
26+
it('should not delete images that are used in other contexts', async () => {
27+
const imagesUsedElsewhere = await fixture.glob('_astro/twoofus.*.*');
28+
expect(imagesUsedElsewhere).to.have.lengthOf(2);
29+
});
30+
31+
it('should not delete images that are also used through query params', async () => {
32+
const imagesUsedElsewhere = await fixture.glob('_astro/url.*.*');
33+
expect(imagesUsedElsewhere).to.have.lengthOf(2);
34+
});
35+
});
36+
});

‎pnpm-lock.yaml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.