summaryrefslogtreecommitdiff
path: root/packages/astro/src/assets
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src/assets')
-rw-r--r--packages/astro/src/assets/build/generate.ts174
-rw-r--r--packages/astro/src/assets/build/remote.ts48
-rw-r--r--packages/astro/src/assets/generate.ts132
-rw-r--r--packages/astro/src/assets/image-endpoint.ts17
-rw-r--r--packages/astro/src/assets/internal.ts39
-rw-r--r--packages/astro/src/assets/services/service.ts55
-rw-r--r--packages/astro/src/assets/utils/remotePattern.ts63
-rw-r--r--packages/astro/src/assets/utils/transformToPath.ts11
-rw-r--r--packages/astro/src/assets/vite-plugin-assets.ts12
9 files changed, 372 insertions, 179 deletions
diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts
new file mode 100644
index 000000000..b78800a43
--- /dev/null
+++ b/packages/astro/src/assets/build/generate.ts
@@ -0,0 +1,174 @@
+import fs, { readFileSync } from 'node:fs';
+import { basename, join } from 'node:path/posix';
+import type { StaticBuildOptions } from '../../core/build/types.js';
+import { warn } from '../../core/logger/core.js';
+import { prependForwardSlash } from '../../core/path.js';
+import { isServerLikeOutput } from '../../prerender/utils.js';
+import { getConfiguredImageService, isESMImportedImage } from '../internal.js';
+import type { LocalImageService } from '../services/service.js';
+import type { ImageMetadata, ImageTransform } from '../types.js';
+import { loadRemoteImage, type RemoteCacheEntry } from './remote.js';
+
+interface GenerationDataUncached {
+ cached: false;
+ weight: {
+ before: number;
+ after: number;
+ };
+}
+
+interface GenerationDataCached {
+ cached: true;
+}
+
+type GenerationData = GenerationDataUncached | GenerationDataCached;
+
+export async function generateImage(
+ buildOpts: StaticBuildOptions,
+ options: ImageTransform,
+ filepath: string
+): Promise<GenerationData | undefined> {
+ let useCache = true;
+ const assetsCacheDir = new URL('assets/', buildOpts.settings.config.cacheDir);
+
+ // Ensure that the cache directory exists
+ try {
+ await fs.promises.mkdir(assetsCacheDir, { recursive: true });
+ } catch (err) {
+ warn(
+ buildOpts.logging,
+ 'astro:assets',
+ `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${err}`
+ );
+ useCache = false;
+ }
+
+ let serverRoot: URL, clientRoot: URL;
+ if (isServerLikeOutput(buildOpts.settings.config)) {
+ serverRoot = buildOpts.settings.config.build.server;
+ clientRoot = buildOpts.settings.config.build.client;
+ } else {
+ serverRoot = buildOpts.settings.config.outDir;
+ clientRoot = buildOpts.settings.config.outDir;
+ }
+
+ const isLocalImage = isESMImportedImage(options.src);
+
+ const finalFileURL = new URL('.' + filepath, clientRoot);
+ const finalFolderURL = new URL('./', finalFileURL);
+
+ // For remote images, instead of saving the image directly, we save a JSON file with the image data and expiration date from the server
+ const cacheFile = basename(filepath) + (isLocalImage ? '' : '.json');
+ const cachedFileURL = new URL(cacheFile, assetsCacheDir);
+
+ await fs.promises.mkdir(finalFolderURL, { recursive: true });
+
+ // Check if we have a cached entry first
+ try {
+ if (isLocalImage) {
+ await fs.promises.copyFile(cachedFileURL, finalFileURL);
+
+ return {
+ cached: true,
+ };
+ } else {
+ const JSONData = JSON.parse(readFileSync(cachedFileURL, 'utf-8')) as RemoteCacheEntry;
+
+ // If the cache entry is not expired, use it
+ if (JSONData.expires < Date.now()) {
+ await fs.promises.writeFile(finalFileURL, Buffer.from(JSONData.data, 'base64'));
+
+ return {
+ cached: true,
+ };
+ }
+ }
+ } catch (e: any) {
+ if (e.code !== 'ENOENT') {
+ throw new Error(`An error was encountered while reading the cache file. Error: ${e}`);
+ }
+ // If the cache file doesn't exist, just move on, and we'll generate it
+ }
+
+ // The original filepath or URL from the image transform
+ const originalImagePath = isLocalImage
+ ? (options.src as ImageMetadata).src
+ : (options.src as string);
+
+ let imageData;
+ let resultData: { data: Buffer | undefined; expires: number | undefined } = {
+ data: undefined,
+ expires: undefined,
+ };
+
+ // If the image is local, we can just read it directly, otherwise we need to download it
+ if (isLocalImage) {
+ imageData = await fs.promises.readFile(
+ new URL(
+ '.' +
+ prependForwardSlash(
+ join(buildOpts.settings.config.build.assets, basename(originalImagePath))
+ ),
+ serverRoot
+ )
+ );
+ } else {
+ const remoteImage = await loadRemoteImage(originalImagePath);
+ resultData.expires = remoteImage.expires;
+ imageData = remoteImage.data;
+ }
+
+ const imageService = (await getConfiguredImageService()) as LocalImageService;
+ resultData.data = (
+ await imageService.transform(
+ imageData,
+ { ...options, src: originalImagePath },
+ buildOpts.settings.config.image
+ )
+ ).data;
+
+ try {
+ // Write the cache entry
+ if (useCache) {
+ if (isLocalImage) {
+ await fs.promises.writeFile(cachedFileURL, resultData.data);
+ } else {
+ await fs.promises.writeFile(
+ cachedFileURL,
+ JSON.stringify({
+ data: Buffer.from(resultData.data).toString('base64'),
+ expires: resultData.expires,
+ })
+ );
+ }
+ }
+ } catch (e) {
+ warn(
+ buildOpts.logging,
+ 'astro:assets',
+ `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${e}`
+ );
+ } finally {
+ // Write the final file
+ await fs.promises.writeFile(finalFileURL, resultData.data);
+ }
+
+ return {
+ cached: false,
+ weight: {
+ // Divide by 1024 to get size in kilobytes
+ before: Math.trunc(imageData.byteLength / 1024),
+ after: Math.trunc(Buffer.from(resultData.data).byteLength / 1024),
+ },
+ };
+}
+
+export function getStaticImageList(): Iterable<
+ [string, { path: string; options: ImageTransform }]
+> {
+ if (!globalThis?.astroAsset?.staticImages) {
+ return [];
+ }
+
+ return globalThis.astroAsset.staticImages?.entries();
+}
diff --git a/packages/astro/src/assets/build/remote.ts b/packages/astro/src/assets/build/remote.ts
new file mode 100644
index 000000000..c3d4bb9ba
--- /dev/null
+++ b/packages/astro/src/assets/build/remote.ts
@@ -0,0 +1,48 @@
+import CachePolicy from 'http-cache-semantics';
+
+export type RemoteCacheEntry = { data: string; expires: number };
+
+export async function loadRemoteImage(src: string) {
+ const req = new Request(src);
+ const res = await fetch(req);
+
+ if (!res.ok) {
+ throw new Error(
+ `Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`
+ );
+ }
+
+ // calculate an expiration date based on the response's TTL
+ const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
+ const expires = policy.storable() ? policy.timeToLive() : 0;
+
+ return {
+ data: Buffer.from(await res.arrayBuffer()),
+ expires: Date.now() + expires,
+ };
+}
+
+function webToCachePolicyRequest({ url, method, headers: _headers }: Request): CachePolicy.Request {
+ let headers: CachePolicy.Headers = {};
+ // Be defensive here due to a cookie header bug in node@18.14.1 + undici
+ try {
+ headers = Object.fromEntries(_headers.entries());
+ } catch {}
+ return {
+ method,
+ url,
+ headers,
+ };
+}
+
+function webToCachePolicyResponse({ status, headers: _headers }: Response): CachePolicy.Response {
+ let headers: CachePolicy.Headers = {};
+ // Be defensive here due to a cookie header bug in node@18.14.1 + undici
+ try {
+ headers = Object.fromEntries(_headers.entries());
+ } catch {}
+ return {
+ status,
+ headers,
+ };
+}
diff --git a/packages/astro/src/assets/generate.ts b/packages/astro/src/assets/generate.ts
deleted file mode 100644
index 04488ed8f..000000000
--- a/packages/astro/src/assets/generate.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import fs from 'node:fs';
-import { basename, join } from 'node:path/posix';
-import type { StaticBuildOptions } from '../core/build/types.js';
-import { warn } from '../core/logger/core.js';
-import { prependForwardSlash } from '../core/path.js';
-import { isServerLikeOutput } from '../prerender/utils.js';
-import { getConfiguredImageService, isESMImportedImage } from './internal.js';
-import type { LocalImageService } from './services/service.js';
-import type { ImageTransform } from './types.js';
-
-interface GenerationDataUncached {
- cached: false;
- weight: {
- before: number;
- after: number;
- };
-}
-
-interface GenerationDataCached {
- cached: true;
-}
-
-type GenerationData = GenerationDataUncached | GenerationDataCached;
-
-export async function generateImage(
- buildOpts: StaticBuildOptions,
- options: ImageTransform,
- filepath: string
-): Promise<GenerationData | undefined> {
- if (typeof buildOpts.settings.config.image === 'undefined') {
- throw new Error(
- "Astro hasn't set a default service for `astro:assets`. This is an internal error and you should report it."
- );
- }
- if (!isESMImportedImage(options.src)) {
- return undefined;
- }
-
- let useCache = true;
- const assetsCacheDir = new URL('assets/', buildOpts.settings.config.cacheDir);
-
- // Ensure that the cache directory exists
- try {
- await fs.promises.mkdir(assetsCacheDir, { recursive: true });
- } catch (err) {
- warn(
- buildOpts.logging,
- 'astro:assets',
- `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${err}`
- );
- useCache = false;
- }
-
- let serverRoot: URL, clientRoot: URL;
- if (isServerLikeOutput(buildOpts.settings.config)) {
- serverRoot = buildOpts.settings.config.build.server;
- clientRoot = buildOpts.settings.config.build.client;
- } else {
- serverRoot = buildOpts.settings.config.outDir;
- clientRoot = buildOpts.settings.config.outDir;
- }
-
- const finalFileURL = new URL('.' + filepath, clientRoot);
- const finalFolderURL = new URL('./', finalFileURL);
- const cachedFileURL = new URL(basename(filepath), assetsCacheDir);
-
- try {
- await fs.promises.copyFile(cachedFileURL, finalFileURL);
-
- return {
- cached: true,
- };
- } catch (e) {
- // no-op
- }
-
- // The original file's path (the `src` attribute of the ESM imported image passed by the user)
- const originalImagePath = options.src.src;
-
- const fileData = await fs.promises.readFile(
- new URL(
- '.' +
- prependForwardSlash(
- join(buildOpts.settings.config.build.assets, basename(originalImagePath))
- ),
- serverRoot
- )
- );
-
- const imageService = (await getConfiguredImageService()) as LocalImageService;
- const resultData = await imageService.transform(
- fileData,
- { ...options, src: originalImagePath },
- buildOpts.settings.config.image.service.config
- );
-
- await fs.promises.mkdir(finalFolderURL, { recursive: true });
-
- if (useCache) {
- try {
- await fs.promises.writeFile(cachedFileURL, resultData.data);
- await fs.promises.copyFile(cachedFileURL, finalFileURL);
- } catch (e) {
- warn(
- buildOpts.logging,
- 'astro:assets',
- `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${e}`
- );
- await fs.promises.writeFile(finalFileURL, resultData.data);
- }
- } else {
- await fs.promises.writeFile(finalFileURL, resultData.data);
- }
-
- return {
- cached: false,
- weight: {
- before: Math.trunc(fileData.byteLength / 1024),
- after: Math.trunc(resultData.data.byteLength / 1024),
- },
- };
-}
-
-export function getStaticImageList(): Iterable<
- [string, { path: string; options: ImageTransform }]
-> {
- if (!globalThis?.astroAsset?.staticImages) {
- return [];
- }
-
- return globalThis.astroAsset.staticImages?.entries();
-}
diff --git a/packages/astro/src/assets/image-endpoint.ts b/packages/astro/src/assets/image-endpoint.ts
index d9a101679..d83517379 100644
--- a/packages/astro/src/assets/image-endpoint.ts
+++ b/packages/astro/src/assets/image-endpoint.ts
@@ -1,8 +1,10 @@
import mime from 'mime/lite.js';
import type { APIRoute } from '../@types/astro.js';
import { etag } from './utils/etag.js';
+import { isRemotePath } from '../core/path.js';
+import { getConfiguredImageService, isRemoteAllowed } from './internal.js';
// @ts-expect-error
-import { getConfiguredImageService, imageServiceConfig } from 'astro:assets';
+import { imageConfig } from 'astro:assets';
async function loadRemoteImage(src: URL) {
try {
@@ -30,7 +32,7 @@ export const GET: APIRoute = async ({ request }) => {
}
const url = new URL(request.url);
- const transform = await imageService.parseURL(url, imageServiceConfig);
+ const transform = await imageService.parseURL(url, imageConfig);
if (!transform?.src) {
throw new Error('Incorrect transform returned by `parseURL`');
@@ -42,17 +44,18 @@ export const GET: APIRoute = async ({ request }) => {
const sourceUrl = isRemotePath(transform.src)
? new URL(transform.src)
: new URL(transform.src, url.origin);
+
+ if (isRemotePath(transform.src) && isRemoteAllowed(transform.src, imageConfig) === false) {
+ return new Response('Forbidden', { status: 403 });
+ }
+
inputBuffer = await loadRemoteImage(sourceUrl);
if (!inputBuffer) {
return new Response('Not Found', { status: 404 });
}
- const { data, format } = await imageService.transform(
- inputBuffer,
- transform,
- imageServiceConfig
- );
+ const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig);
return new Response(data, {
status: 200,
diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts
index a49828a46..dd5e427f6 100644
--- a/packages/astro/src/assets/internal.ts
+++ b/packages/astro/src/assets/internal.ts
@@ -1,4 +1,5 @@
-import type { AstroSettings } from '../@types/astro.js';
+import { isRemotePath } from '@astrojs/internal-helpers/path';
+import type { AstroConfig, AstroSettings } from '../@types/astro.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { isLocalService, type ImageService } from './services/service.js';
import type {
@@ -7,6 +8,7 @@ import type {
ImageTransform,
UnresolvedImageTransform,
} from './types.js';
+import { matchHostname, matchPattern } from './utils/remotePattern.js';
export function injectImageEndpoint(settings: AstroSettings) {
// TODO: Add a setting to disable the image endpoint
@@ -23,6 +25,26 @@ export function isESMImportedImage(src: ImageMetadata | string): src is ImageMet
return typeof src === 'object';
}
+export function isRemoteImage(src: ImageMetadata | string): src is string {
+ return typeof src === 'string';
+}
+
+export function isRemoteAllowed(
+ src: string,
+ {
+ domains = [],
+ remotePatterns = [],
+ }: Partial<Pick<AstroConfig['image'], 'domains' | 'remotePatterns'>>
+): boolean {
+ if (!isRemotePath(src)) return false;
+
+ const url = new URL(src);
+ return (
+ domains.some((domain) => matchHostname(url, domain)) ||
+ remotePatterns.some((remotePattern) => matchPattern(url, remotePattern))
+ );
+}
+
export async function getConfiguredImageService(): Promise<ImageService> {
if (!globalThis?.astroAsset?.imageService) {
const { default: service }: { default: ImageService } = await import(
@@ -44,7 +66,7 @@ export async function getConfiguredImageService(): Promise<ImageService> {
export async function getImage(
options: ImageTransform | UnresolvedImageTransform,
- serviceConfig: Record<string, any>
+ imageConfig: AstroConfig['image']
): Promise<GetImageResult> {
if (!options || typeof options !== 'object') {
throw new AstroError({
@@ -65,13 +87,18 @@ export async function getImage(
};
const validatedOptions = service.validateOptions
- ? await service.validateOptions(resolvedOptions, serviceConfig)
+ ? await service.validateOptions(resolvedOptions, imageConfig)
: resolvedOptions;
- let imageURL = await service.getURL(validatedOptions, serviceConfig);
+ let imageURL = await service.getURL(validatedOptions, imageConfig);
// In build and for local services, we need to collect the requested parameters so we can generate the final images
- if (isLocalService(service) && globalThis.astroAsset.addStaticImage) {
+ if (
+ isLocalService(service) &&
+ globalThis.astroAsset.addStaticImage &&
+ // If `getURL` returned the same URL as the user provided, it means the service doesn't need to do anything
+ !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)
+ ) {
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions);
}
@@ -81,7 +108,7 @@ export async function getImage(
src: imageURL,
attributes:
service.getHTMLAttributes !== undefined
- ? service.getHTMLAttributes(validatedOptions, serviceConfig)
+ ? service.getHTMLAttributes(validatedOptions, imageConfig)
: {},
};
}
diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts
index d3479c880..5af4a898b 100644
--- a/packages/astro/src/assets/services/service.ts
+++ b/packages/astro/src/assets/services/service.ts
@@ -1,7 +1,8 @@
+import type { AstroConfig } from '../../@types/astro.js';
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import { joinPaths } from '../../core/path.js';
import { VALID_SUPPORTED_FORMATS } from '../consts.js';
-import { isESMImportedImage } from '../internal.js';
+import { isESMImportedImage, isRemoteAllowed } from '../internal.js';
import type { ImageOutputFormat, ImageTransform } from '../types.js';
export type ImageService = LocalImageService | ExternalImageService;
@@ -23,7 +24,11 @@ export function parseQuality(quality: string): string | number {
return result;
}
-interface SharedServiceProps {
+type ImageConfig<T> = Omit<AstroConfig['image'], 'service'> & {
+ service: { entrypoint: string; config: T };
+};
+
+interface SharedServiceProps<T extends Record<string, any> = Record<string, any>> {
/**
* Return the URL to the endpoint or URL your images are generated from.
*
@@ -32,7 +37,7 @@ interface SharedServiceProps {
* For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image`
*
*/
- getURL: (options: ImageTransform, serviceConfig: Record<string, any>) => string | Promise<string>;
+ getURL: (options: ImageTransform, imageConfig: ImageConfig<T>) => string | Promise<string>;
/**
* Return any additional HTML attributes separate from `src` that your service requires to show the image properly.
*
@@ -41,7 +46,7 @@ interface SharedServiceProps {
*/
getHTMLAttributes?: (
options: ImageTransform,
- serviceConfig: Record<string, any>
+ imageConfig: ImageConfig<T>
) => Record<string, any> | Promise<Record<string, any>>;
/**
* Validate and return the options passed by the user.
@@ -53,18 +58,20 @@ interface SharedServiceProps {
*/
validateOptions?: (
options: ImageTransform,
- serviceConfig: Record<string, any>
+ imageConfig: ImageConfig<T>
) => ImageTransform | Promise<ImageTransform>;
}
-export type ExternalImageService = SharedServiceProps;
+export type ExternalImageService<T extends Record<string, any> = Record<string, any>> =
+ SharedServiceProps<T>;
export type LocalImageTransform = {
src: string;
[key: string]: any;
};
-export interface LocalImageService extends SharedServiceProps {
+export interface LocalImageService<T extends Record<string, any> = Record<string, any>>
+ extends SharedServiceProps<T> {
/**
* Parse the requested parameters passed in the URL from `getURL` back into an object to be used later by `transform`.
*
@@ -72,7 +79,7 @@ export interface LocalImageService extends SharedServiceProps {
*/
parseURL: (
url: URL,
- serviceConfig: Record<string, any>
+ imageConfig: ImageConfig<T>
) => LocalImageTransform | undefined | Promise<LocalImageTransform> | Promise<undefined>;
/**
* Performs the image transformations on the input image and returns both the binary data and
@@ -81,7 +88,7 @@ export interface LocalImageService extends SharedServiceProps {
transform: (
inputBuffer: Buffer,
transform: LocalImageTransform,
- serviceConfig: Record<string, any>
+ imageConfig: ImageConfig<T>
) => Promise<{ data: Buffer; format: ImageOutputFormat }>;
}
@@ -202,21 +209,31 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
decoding: attributes.decoding ?? 'async',
};
},
- getURL(options: ImageTransform) {
- // Both our currently available local services don't handle remote images, so we return the path as is.
- if (!isESMImportedImage(options.src)) {
+ getURL(options, imageConfig) {
+ const searchParams = new URLSearchParams();
+
+ if (isESMImportedImage(options.src)) {
+ searchParams.append('href', options.src.src);
+ } else if (isRemoteAllowed(options.src, imageConfig)) {
+ searchParams.append('href', options.src);
+ } else {
+ // If it's not an imported image, nor is it allowed using the current domains or remote patterns, we'll just return the original URL
return options.src;
}
- const searchParams = new URLSearchParams();
- searchParams.append('href', options.src.src);
+ const params: Record<string, keyof typeof options> = {
+ w: 'width',
+ h: 'height',
+ q: 'quality',
+ f: 'format',
+ };
- options.width && searchParams.append('w', options.width.toString());
- options.height && searchParams.append('h', options.height.toString());
- options.quality && searchParams.append('q', options.quality.toString());
- options.format && searchParams.append('f', options.format);
+ Object.entries(params).forEach(([param, key]) => {
+ options[key] && searchParams.append(param, options[key].toString());
+ });
- return joinPaths(import.meta.env.BASE_URL, '/_image?') + searchParams;
+ const imageEndpoint = joinPaths(import.meta.env.BASE_URL, '/_image');
+ return `${imageEndpoint}?${searchParams}`;
},
parseURL(url) {
const params = url.searchParams;
diff --git a/packages/astro/src/assets/utils/remotePattern.ts b/packages/astro/src/assets/utils/remotePattern.ts
new file mode 100644
index 000000000..7708b42e7
--- /dev/null
+++ b/packages/astro/src/assets/utils/remotePattern.ts
@@ -0,0 +1,63 @@
+export type RemotePattern = {
+ hostname?: string;
+ pathname?: string;
+ protocol?: string;
+ port?: string;
+};
+
+export function matchPattern(url: URL, remotePattern: RemotePattern) {
+ return (
+ matchProtocol(url, remotePattern.protocol) &&
+ matchHostname(url, remotePattern.hostname, true) &&
+ matchPort(url, remotePattern.port) &&
+ matchPathname(url, remotePattern.pathname, true)
+ );
+}
+
+export function matchPort(url: URL, port?: string) {
+ return !port || port === url.port;
+}
+
+export function matchProtocol(url: URL, protocol?: string) {
+ return !protocol || protocol === url.protocol.slice(0, -1);
+}
+
+export function matchHostname(url: URL, hostname?: string, allowWildcard?: boolean) {
+ if (!hostname) {
+ return true;
+ } else if (!allowWildcard || !hostname.startsWith('*')) {
+ return hostname === url.hostname;
+ } else if (hostname.startsWith('**.')) {
+ const slicedHostname = hostname.slice(2); // ** length
+ return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname);
+ } else if (hostname.startsWith('*.')) {
+ const slicedHostname = hostname.slice(1); // * length
+ const additionalSubdomains = url.hostname
+ .replace(slicedHostname, '')
+ .split('.')
+ .filter(Boolean);
+ return additionalSubdomains.length === 1;
+ }
+
+ return false;
+}
+
+export function matchPathname(url: URL, pathname?: string, allowWildcard?: boolean) {
+ if (!pathname) {
+ return true;
+ } else if (!allowWildcard || !pathname.endsWith('*')) {
+ return pathname === url.pathname;
+ } else if (pathname.endsWith('/**')) {
+ const slicedPathname = pathname.slice(0, -2); // ** length
+ return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname);
+ } else if (pathname.endsWith('/*')) {
+ const slicedPathname = pathname.slice(0, -1); // * length
+ const additionalPathChunks = url.pathname
+ .replace(slicedPathname, '')
+ .split('/')
+ .filter(Boolean);
+ return additionalPathChunks.length === 1;
+ }
+
+ return false;
+}
diff --git a/packages/astro/src/assets/utils/transformToPath.ts b/packages/astro/src/assets/utils/transformToPath.ts
index 04ddee0a1..d5535137b 100644
--- a/packages/astro/src/assets/utils/transformToPath.ts
+++ b/packages/astro/src/assets/utils/transformToPath.ts
@@ -5,14 +5,13 @@ import { isESMImportedImage } from '../internal.js';
import type { ImageTransform } from '../types.js';
export function propsToFilename(transform: ImageTransform, hash: string) {
- if (!isESMImportedImage(transform.src)) {
- return transform.src;
- }
-
- let filename = removeQueryString(transform.src.src);
+ let filename = removeQueryString(
+ isESMImportedImage(transform.src) ? transform.src.src : transform.src
+ );
const ext = extname(filename);
filename = basename(filename, ext);
- const outputExt = transform.format ? `.${transform.format}` : ext;
+
+ let outputExt = transform.format ? `.${transform.format}` : ext;
return `/${filename}_${hash}${outputExt}`;
}
diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts
index 24de955ba..f194e5288 100644
--- a/packages/astro/src/assets/vite-plugin-assets.ts
+++ b/packages/astro/src/assets/vite-plugin-assets.ts
@@ -9,7 +9,6 @@ import {
removeQueryString,
} from '../core/path.js';
import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
-import { isESMImportedImage } from './internal.js';
import { emitESMImage } from './utils/emitAsset.js';
import { hashTransform, propsToFilename } from './utils/transformToPath.js';
@@ -45,8 +44,8 @@ export default function assets({
import { getImage as getImageInternal } from "astro/assets";
export { default as Image } from "astro/components/Image.astro";
- export const imageServiceConfig = ${JSON.stringify(settings.config.image.service.config)};
- export const getImage = async (options) => await getImageInternal(options, imageServiceConfig);
+ export const imageConfig = ${JSON.stringify(settings.config.image)};
+ export const getImage = async (options) => await getImageInternal(options, imageConfig);
`;
}
},
@@ -69,15 +68,10 @@ export default function assets({
if (globalThis.astroAsset.staticImages.has(hash)) {
filePath = globalThis.astroAsset.staticImages.get(hash)!.path;
} else {
- // If the image is not imported, we can return the path as-is, since static references
- // should only point ot valid paths for builds or remote images
- if (!isESMImportedImage(options.src)) {
- return options.src;
- }
-
filePath = prependForwardSlash(
joinPaths(settings.config.build.assets, propsToFilename(options, hash))
);
+
globalThis.astroAsset.staticImages.set(hash, { path: filePath, options: options });
}