import { extname } from "node:path"; import MagicString from "magic-string"; import picomatch from "picomatch"; import { AstroError, AstroErrorData } from "../core/errors/index.js"; import { appendForwardSlash, joinPaths, prependForwardSlash, removeBase, removeQueryString } from "../core/path.js"; import { normalizePath } from "../core/viteUtils.js"; import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../core/constants.js"; import { isAstroServerEnvironment } from "../environments.js"; import { RESOLVED_VIRTUAL_GET_IMAGE_ID, RESOLVED_VIRTUAL_IMAGE_STYLES_ID, RESOLVED_VIRTUAL_MODULE_ID, VALID_INPUT_FORMATS, VIRTUAL_GET_IMAGE_ID, VIRTUAL_IMAGE_STYLES_ID, VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from "./consts.js"; import { RUNTIME_VIRTUAL_MODULE_ID } from "./fonts/constants.js"; import { fontsPlugin } from "./fonts/vite-plugin-fonts.js"; import { getAssetsPrefix } from "./utils/getAssetsPrefix.js"; import { isESMImportedImage } from "./utils/index.js"; import { emitClientAsset } from "./utils/assets.js"; import { hashTransform, propsToFilename } from "./utils/hash.js"; import { emitImageMetadata } from "./utils/node.js"; import { CONTENT_IMAGE_FLAG } from "../content/consts.js"; import { getProxyCode } from "./utils/proxy.js"; import { makeSvgComponent, parseSvgComponentData } from "./utils/svg.js"; import { createPlaceholderURL, stringifyPlaceholderURL } from "./utils/url.js"; const assetRegex = new RegExp(`\\.(${VALID_INPUT_FORMATS.join("|")})`, "i"); const assetRegexEnds = new RegExp(`\\.(${VALID_INPUT_FORMATS.join("|")})$`, "i"); const addStaticImageFactory = (settings) => { return (options, hashProperties, originalFSPath) => { if (!globalThis.astroAsset.staticImages) { globalThis.astroAsset.staticImages = /* @__PURE__ */ new Map(); } const ESMImportedImageSrc = isESMImportedImage(options.src) ? options.src.src : options.src; const fileExtension = extname(ESMImportedImageSrc); const assetPrefix = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix); const finalOriginalPath = removeBase( removeBase(ESMImportedImageSrc, settings.config.base), assetPrefix ); const hash = hashTransform(options, settings.config.image.service.entrypoint, hashProperties); let finalFilePath; let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath); const transformForHash = transformsForPath?.transforms.get(hash); if (transformsForPath && transformForHash) { finalFilePath = transformForHash.finalPath; } else { finalFilePath = prependForwardSlash( joinPaths( isESMImportedImage(options.src) ? "" : settings.config.build.assets, prependForwardSlash(propsToFilename(finalOriginalPath, options, hash)) ) ); if (!transformsForPath) { globalThis.astroAsset.staticImages.set(finalOriginalPath, { originalSrcPath: originalFSPath, transforms: /* @__PURE__ */ new Map() }); transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath); } transformsForPath.transforms.set(hash, { finalPath: finalFilePath, transform: options }); } const url = createPlaceholderURL( settings.config.build.assetsPrefix ? encodeURI(joinPaths(assetPrefix, finalFilePath)) : encodeURI(prependForwardSlash(joinPaths(settings.config.base, finalFilePath))) ); const assetQueryParams = settings.adapter?.client?.assetQueryParams; if (assetQueryParams) { assetQueryParams.forEach((value, key) => { url.searchParams.set(key, value); }); } return stringifyPlaceholderURL(url); }; }; function assets({ fs, settings, sync, logger }) { let resolvedConfig; let shouldEmitFile = false; let isBuild = false; globalThis.astroAsset = { referencedImages: /* @__PURE__ */ new Set() }; const imageComponentPrefix = settings.config.image.responsiveStyles ? "Responsive" : ""; return [ // Expose the components and different utilities from `astro:assets` { name: "astro:assets", config(_, env) { isBuild = env.command === "build"; }, resolveId: { filter: { id: new RegExp(`^(${VIRTUAL_SERVICE_ID}|${VIRTUAL_MODULE_ID}|${VIRTUAL_GET_IMAGE_ID})$`) }, async handler(id) { if (id === VIRTUAL_SERVICE_ID) { if (isAstroServerEnvironment(this.environment)) { return await this.resolve(settings.config.image.service.entrypoint); } return await this.resolve("astro/assets/services/noop"); } if (id === VIRTUAL_MODULE_ID) { return RESOLVED_VIRTUAL_MODULE_ID; } if (id === VIRTUAL_GET_IMAGE_ID) { return RESOLVED_VIRTUAL_GET_IMAGE_ID; } } }, load: { filter: { id: new RegExp(`^(${RESOLVED_VIRTUAL_MODULE_ID}|${RESOLVED_VIRTUAL_GET_IMAGE_ID})$`) }, handler(id) { if (id === RESOLVED_VIRTUAL_GET_IMAGE_ID) { const isServerEnvironment2 = isAstroServerEnvironment(this.environment); const getImageExport2 = isServerEnvironment2 ? `import { getImage as getImageInternal } from "astro/assets"; export const getImage = async (options) => await getImageInternal(options, imageConfig);` : `import { AstroError, AstroErrorData } from "astro/errors"; export const getImage = async () => { throw new AstroError( AstroErrorData.GetImageNotUsedOnServer.message, AstroErrorData.GetImageNotUsedOnServer.hint, ); };`; const assetQueryParams = settings.adapter?.client?.assetQueryParams ? `new URLSearchParams(${JSON.stringify( Array.from(settings.adapter.client.assetQueryParams.entries()) )})` : "undefined"; return { code: ` export const imageConfig = ${JSON.stringify(settings.config.image)}; Object.defineProperty(imageConfig, 'assetQueryParams', { value: ${assetQueryParams}, enumerable: false, configurable: true, }); ${getImageExport2} ` }; } const isServerEnvironment = isAstroServerEnvironment(this.environment); const getImageExport = isServerEnvironment ? `import { getImage as getImageInternal } from "astro/assets"; export const getImage = async (options) => await getImageInternal(options, imageConfig);` : `import { AstroError, AstroErrorData } from "astro/errors"; export const getImage = async () => { throw new AstroError( AstroErrorData.GetImageNotUsedOnServer.message, AstroErrorData.GetImageNotUsedOnServer.hint, ); };`; return { code: ` import { getConfiguredImageService as _getConfiguredImageService } from "astro/assets"; export { isLocalService } from "astro/assets"; ${settings.config.image.responsiveStyles ? `import "${VIRTUAL_IMAGE_STYLES_ID}";` : ""} export { default as Image } from "astro/components/${imageComponentPrefix}Image.astro"; export { default as Picture } from "astro/components/${imageComponentPrefix}Picture.astro"; import { inferRemoteSize as inferRemoteSizeInternal } from "astro/assets/utils/inferRemoteSize.js"; export { default as Font } from "astro/components/Font.astro"; export * from "${RUNTIME_VIRTUAL_MODULE_ID}"; export const getConfiguredImageService = _getConfiguredImageService; export const viteFSConfig = ${JSON.stringify(resolvedConfig.server.fs ?? {})}; export const safeModulePaths = new Set(${JSON.stringify( // @ts-expect-error safeModulePaths is internal to Vite Array.from(resolvedConfig.safeModulePaths ?? []) )}); export const fsDenyGlob = ${serializeFsDenyGlob(resolvedConfig.server.fs?.deny ?? [])}; const assetQueryParams = ${settings.adapter?.client?.assetQueryParams ? `new URLSearchParams(${JSON.stringify( Array.from(settings.adapter.client.assetQueryParams.entries()) )})` : "undefined"}; export const imageConfig = ${JSON.stringify(settings.config.image)}; Object.defineProperty(imageConfig, 'assetQueryParams', { value: assetQueryParams, enumerable: false, configurable: true, }); export const inferRemoteSize = async (url) => { const service = await _getConfiguredImageService(); return service.getRemoteSize?.(url, imageConfig) ?? inferRemoteSizeInternal(url, imageConfig); } // This is used by the @astrojs/node integration to locate images. // It's unused on other platforms, but on some platforms like Netlify (and presumably also Vercel) // new URL("dist/...") is interpreted by the bundler as a signal to include that directory // in the Lambda bundle, which would bloat the bundle with images. // To prevent this, we mark the URL construction as pure, // so that it's tree-shaken away for all platforms that don't need it. export const outDir = /* #__PURE__ */ new URL(${JSON.stringify( new URL( settings.buildOutput === "server" ? settings.config.build.client : settings.config.outDir ) )}); export const serverDir = /* #__PURE__ */ new URL(${JSON.stringify( new URL(settings.config.build.server) )}); ${getImageExport} ` }; } }, buildStart() { if (!isBuild) return; globalThis.astroAsset.addStaticImage = addStaticImageFactory(settings); }, // In build, rewrite paths to ESM imported images in code to their final location async renderChunk(code) { const assetUrlRE = /__ASTRO_ASSET_IMAGE__([\w$]+)__(?:_(.*?)__)?/g; let match; let s; while (match = assetUrlRE.exec(code)) { s = s || (s = new MagicString(code)); const [full, hash, postfix = ""] = match; const file = this.getFileName(hash); const fileExtension = extname(file); const pf = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix); const prefix = pf ? appendForwardSlash(pf) : resolvedConfig.base; const outputFilepath = prefix + normalizePath(file + postfix); s.overwrite(match.index, match.index + full.length, outputFilepath); } if (s) { return { code: s.toString(), map: resolvedConfig.build.sourcemap ? s.generateMap({ hires: "boundary" }) : null }; } else { return null; } } }, // Return a more advanced shape for images imported in ESM { name: "astro:assets:esm", enforce: "pre", config(_, env) { shouldEmitFile = env.command === "build"; }, configResolved(viteConfig) { resolvedConfig = viteConfig; }, load: { filter: { id: assetRegex }, async handler(id) { if (!globalThis.astroAsset.referencedImages) globalThis.astroAsset.referencedImages = /* @__PURE__ */ new Set(); const isContentImage = id.includes(CONTENT_IMAGE_FLAG); if (isContentImage) { id = removeQueryString(id); } if (id !== removeQueryString(id)) { globalThis.astroAsset.referencedImages.add(removeQueryString(id)); return; } if (!assetRegexEnds.test(id)) { return; } const fileEmitter = shouldEmitFile ? (opts) => emitClientAsset(this, opts) : void 0; const imageMetadata = await emitImageMetadata(id, fileEmitter); if (!imageMetadata) { throw new AstroError({ ...AstroErrorData.ImageNotFound, message: AstroErrorData.ImageNotFound.message(id) }); } if (isAstroServerEnvironment(this.environment)) { if (id.endsWith(".svg") && !isContentImage) { const contents = await fs.promises.readFile(imageMetadata.fsPath, { encoding: "utf8" }); return { code: makeSvgComponent(imageMetadata, contents, settings.config.experimental.svgo) }; } const isSSROnlyEnvironment = settings.buildOutput === "server" && this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr; if (isSSROnlyEnvironment) { globalThis.astroAsset.referencedImages.add(imageMetadata.fsPath); } if (id.endsWith(".svg") && isContentImage) { const contents = await fs.promises.readFile(imageMetadata.fsPath, { encoding: "utf8" }); const svgData = parseSvgComponentData( imageMetadata, contents, settings.config.experimental.svgo ); const metadataWithSvg = { ...imageMetadata, __svgData: svgData }; return { code: `export default ${getProxyCode(metadataWithSvg, isSSROnlyEnvironment)}` }; } return { code: `export default ${getProxyCode(imageMetadata, isSSROnlyEnvironment)}` }; } else { globalThis.astroAsset.referencedImages.add(imageMetadata.fsPath); return { code: `export default ${JSON.stringify(imageMetadata)}` }; } } } }, fontsPlugin({ settings, sync, logger }), { name: "astro:image-styles", resolveId: { filter: { id: new RegExp(`^${VIRTUAL_IMAGE_STYLES_ID}$`) }, handler(id) { if (id === VIRTUAL_IMAGE_STYLES_ID) { return RESOLVED_VIRTUAL_IMAGE_STYLES_ID; } } }, load: { filter: { id: new RegExp(`^${RESOLVED_VIRTUAL_IMAGE_STYLES_ID}$`) }, async handler(id) { if (id === RESOLVED_VIRTUAL_IMAGE_STYLES_ID) { const { generateImageStylesCSS } = await import("./utils/generateImageStylesCSS.js"); const css = generateImageStylesCSS( settings.config.image.objectFit, settings.config.image.objectPosition ); return { code: css }; } } } } ]; } function serializeFsDenyGlob(denyPatterns) { const expandedPatterns = denyPatterns.map( (pattern) => pattern.includes("/") ? pattern : `**/${pattern}` ); const regexes = expandedPatterns.map( (pattern) => picomatch.makeRe(pattern, { matchBase: false, nocase: true, dot: true }) ); const serializedRegexes = regexes.map((re) => re.toString()).join(", "); return `(function() { const regexes = [${serializedRegexes}]; return function fsDenyGlob(testPath) { return regexes.some(re => re.test(testPath)); }; })()`; } export { assets as default };