diff --git a/.changeset/brown-goats-serve.md b/.changeset/brown-goats-serve.md new file mode 100644 index 000000000000..19574439ca6e --- /dev/null +++ b/.changeset/brown-goats-serve.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix image services not being usable on Edge runtimes diff --git a/packages/astro/src/assets/generate.ts b/packages/astro/src/assets/generate.ts new file mode 100644 index 000000000000..25493753ac13 --- /dev/null +++ b/packages/astro/src/assets/generate.ts @@ -0,0 +1,126 @@ +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 { 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 { + 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 (buildOpts.settings.config.output === 'server') { + 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/internal.ts b/packages/astro/src/assets/internal.ts index 2a7d08ff71d8..2d4d18ea7040 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -1,10 +1,5 @@ -import fs from 'node:fs'; -import { basename, join } from 'node:path/posix'; -import type { StaticBuildOptions } from '../core/build/types.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; -import { warn } from '../core/logger/core.js'; -import { prependForwardSlash } from '../core/path.js'; -import { isLocalService, type ImageService, type LocalImageService } from './services/service.js'; +import { isLocalService, type ImageService } from './services/service.js'; import type { GetImageResult, ImageMetadata, ImageTransform } from './types.js'; export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata { @@ -63,121 +58,3 @@ export async function getImage( : {}, }; } - -export function getStaticImageList(): Iterable< - [string, { path: string; options: ImageTransform }] -> { - if (!globalThis?.astroAsset?.staticImages) { - return []; - } - - return globalThis.astroAsset.staticImages?.entries(); -} - -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 { - 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 (buildOpts.settings.config.output === 'server') { - 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), - }, - }; -} diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 8d195bab4536..8a85232c4253 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -19,7 +19,7 @@ import type { import { generateImage as generateImageInternal, getStaticImageList, -} from '../../assets/internal.js'; +} from '../../assets/generate.js'; import { hasPrerenderedPages, type BuildInternals } from '../../core/build/internal.js'; import { prependForwardSlash,