diff options
48 files changed, 557 insertions, 238 deletions
diff --git a/.changeset/tiny-glasses-play.md b/.changeset/tiny-glasses-play.md new file mode 100644 index 000000000..1515d63ee --- /dev/null +++ b/.changeset/tiny-glasses-play.md @@ -0,0 +1,6 @@ +--- +'@astrojs/image': minor +--- + +- Fixes two bugs that were blocking SSR support when deployed to a hosting service +- The built-in `sharp` service now automatically rotates images based on EXIF data diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index 326c1bc6c..18e35d1a6 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -1,8 +1,7 @@ --- // @ts-ignore -import loader from 'virtual:image-loader'; -import { getImage } from '../src/index.js'; -import type { ImageAttributes, ImageMetadata, TransformOptions, OutputFormat } from '../src/types.js'; +import { getImage } from '../dist/index.js'; +import type { ImageAttributes, ImageMetadata, TransformOptions, OutputFormat } from '../dist/types'; export interface LocalImageProps extends Omit<TransformOptions, 'src'>, Omit<ImageAttributes, 'src' | 'width' | 'height'> { src: ImageMetadata | Promise<{ default: ImageMetadata }>; @@ -19,7 +18,7 @@ export type Props = LocalImageProps | RemoteImageProps; const { loading = "lazy", decoding = "async", ...props } = Astro.props as Props; -const attrs = await getImage(loader, props); +const attrs = await getImage(props); --- <img {...attrs} {loading} {decoding} /> diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro index bff6aad89..badfc7f46 100644 --- a/packages/integrations/image/components/Picture.astro +++ b/packages/integrations/image/components/Picture.astro @@ -1,8 +1,6 @@ --- -// @ts-ignore -import loader from 'virtual:image-loader'; -import { getPicture } from '../src/get-picture.js'; -import type { ImageAttributes, ImageMetadata, OutputFormat, PictureAttributes, TransformOptions } from '../src/types.js'; +import { getPicture } from '../dist/index.js'; +import type { ImageAttributes, ImageMetadata, OutputFormat, PictureAttributes, TransformOptions } from '../dist/types'; export interface LocalImageProps extends Omit<PictureAttributes, 'src' | 'width' | 'height'>, Omit<TransformOptions, 'src'>, Pick<ImageAttributes, 'loading' | 'decoding'> { src: ImageMetadata | Promise<{ default: ImageMetadata }>; @@ -25,7 +23,7 @@ export type Props = LocalImageProps | RemoteImageProps; const { src, alt, sizes, widths, aspectRatio, formats = ['avif', 'webp'], loading = 'lazy', decoding = 'async', ...attrs } = Astro.props as Props; -const { image, sources } = await getPicture({ loader, src, widths, formats, aspectRatio }); +const { image, sources } = await getPicture({ src, widths, formats, aspectRatio }); --- <picture {...attrs}> diff --git a/packages/integrations/image/package.json b/packages/integrations/image/package.json index e4d1f26f9..816f08141 100644 --- a/packages/integrations/image/package.json +++ b/packages/integrations/image/package.json @@ -54,6 +54,7 @@ "@types/etag": "^1.8.1", "@types/sharp": "^0.30.4", "astro": "workspace:*", - "astro-scripts": "workspace:*" + "astro-scripts": "workspace:*", + "tiny-glob": "^0.2.9" } } diff --git a/packages/integrations/image/src/build/ssg.ts b/packages/integrations/image/src/build/ssg.ts new file mode 100644 index 000000000..a3e410709 --- /dev/null +++ b/packages/integrations/image/src/build/ssg.ts @@ -0,0 +1,79 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { OUTPUT_DIR } from '../constants.js'; +import { ensureDir } from '../utils/paths.js'; +import { isRemoteImage, loadRemoteImage, loadLocalImage } from '../utils/images.js'; +import type { SSRImageService, TransformOptions } from '../types.js'; + +export interface SSGBuildParams { + loader: SSRImageService; + staticImages: Map<string, Map<string, TransformOptions>>; + srcDir: URL; + outDir: URL; +} + +export async function ssgBuild({ + loader, + staticImages, + srcDir, + outDir, +}: SSGBuildParams) { + const inputFiles = new Set<string>(); + + // process transforms one original image file at a time + for await (const [src, transformsMap] of staticImages) { + let inputFile: string | undefined = undefined; + let inputBuffer: Buffer | undefined = undefined; + + if (isRemoteImage(src)) { + // try to load the remote image + inputBuffer = await loadRemoteImage(src); + } else { + const inputFileURL = new URL(`.${src}`, srcDir); + inputFile = fileURLToPath(inputFileURL); + inputBuffer = await loadLocalImage(inputFile); + + // track the local file used so the original can be copied over + inputFiles.add(inputFile); + } + + if (!inputBuffer) { + // eslint-disable-next-line no-console + console.warn(`"${src}" image could not be fetched`); + continue; + } + + const transforms = Array.from(transformsMap.entries()); + + // process each transformed versiono of the + for await (const [filename, transform] of transforms) { + let outputFile: string; + + if (isRemoteImage(src)) { + const outputFileURL = new URL( + path.join('./', OUTPUT_DIR, path.basename(filename)), + outDir + ); + outputFile = fileURLToPath(outputFileURL); + } else { + const outputFileURL = new URL(path.join('./', OUTPUT_DIR, filename), outDir); + outputFile = fileURLToPath(outputFileURL); + } + + const { data } = await loader.transform(inputBuffer, transform); + + ensureDir(path.dirname(outputFile)); + + await fs.writeFile(outputFile, data); + } + } + + // copy all original local images to dist + for await (const original of inputFiles) { + const to = original.replace(fileURLToPath(srcDir), fileURLToPath(outDir)); + + await ensureDir(path.dirname(to)); + await fs.copyFile(original, to); + } +} diff --git a/packages/integrations/image/src/build/ssr.ts b/packages/integrations/image/src/build/ssr.ts new file mode 100644 index 000000000..90a699451 --- /dev/null +++ b/packages/integrations/image/src/build/ssr.ts @@ -0,0 +1,29 @@ +import fs from 'fs/promises'; +import path from 'path'; +import glob from 'tiny-glob'; +import { fileURLToPath } from 'url'; +import { ensureDir } from '../utils/paths.js'; + +async function globImages(dir: URL) { + const srcPath = fileURLToPath(dir); + return await glob( + `${srcPath}/**/*.{heic,heif,avif,jpeg,jpg,png,tiff,webp,gif}`, + { absolute: true } + ); +} + +export interface SSRBuildParams { + srcDir: URL; + outDir: URL; +} + +export async function ssrBuild({ srcDir, outDir }: SSRBuildParams) { + const images = await globImages(srcDir); + + for await (const image of images) { + const to = image.replace(fileURLToPath(srcDir), fileURLToPath(outDir)); + + await ensureDir(path.dirname(to)); + await fs.copyFile(image, to); + } +} diff --git a/packages/integrations/image/src/endpoints/dev.ts b/packages/integrations/image/src/endpoints/dev.ts index 67b37b177..dfa7f4900 100644 --- a/packages/integrations/image/src/endpoints/dev.ts +++ b/packages/integrations/image/src/endpoints/dev.ts @@ -1,10 +1,9 @@ import type { APIRoute } from 'astro'; import { lookup } from 'mrmime'; -import { loadImage } from '../utils.js'; +import loader from '../loaders/sharp.js'; +import { loadImage } from '../utils/images.js'; export const get: APIRoute = async ({ request }) => { - const loader = globalThis.astroImage.ssrLoader; - try { const url = new URL(request.url); const transform = loader.parseTransform(url.searchParams); diff --git a/packages/integrations/image/src/endpoints/prod.ts b/packages/integrations/image/src/endpoints/prod.ts index 921b54853..8a15c2e88 100644 --- a/packages/integrations/image/src/endpoints/prod.ts +++ b/packages/integrations/image/src/endpoints/prod.ts @@ -1,9 +1,10 @@ import type { APIRoute } from 'astro'; import etag from 'etag'; import { lookup } from 'mrmime'; +import { fileURLToPath } from 'url'; // @ts-ignore import loader from 'virtual:image-loader'; -import { isRemoteImage, loadRemoteImage } from '../utils.js'; +import { isRemoteImage, loadLocalImage, loadRemoteImage } from '../utils/images.js'; export const get: APIRoute = async ({ request }) => { try { @@ -14,12 +15,14 @@ export const get: APIRoute = async ({ request }) => { return new Response('Bad Request', { status: 400 }); } - // TODO: Can we lean on fs to load local images in SSR prod builds? - const href = isRemoteImage(transform.src) - ? new URL(transform.src) - : new URL(transform.src, url.origin); + let inputBuffer: Buffer | undefined = undefined; - const inputBuffer = await loadRemoteImage(href.toString()); + if (isRemoteImage(transform.src)) { + inputBuffer = await loadRemoteImage(transform.src); + } else { + const pathname = fileURLToPath(new URL(`../client${transform.src}`, import.meta.url)); + inputBuffer = await loadLocalImage(pathname); + } if (!inputBuffer) { return new Response(`"${transform.src} not found`, { status: 404 }); diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index f857bdc70..81ef8c6b9 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -1,137 +1,5 @@ -import type { AstroConfig, AstroIntegration } from 'astro'; -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { OUTPUT_DIR, PKG_NAME, ROUTE_PATTERN } from './constants.js'; -import sharp from './loaders/sharp.js'; -import { IntegrationOptions, TransformOptions } from './types.js'; -import { - ensureDir, - isRemoteImage, - loadLocalImage, - loadRemoteImage, - propsToFilename, -} from './utils.js'; -import { createPlugin } from './vite-plugin-astro-image.js'; -export * from './get-image.js'; -export * from './get-picture.js'; +import integration from './integration.js'; +export * from './lib/get-image.js'; +export * from './lib/get-picture.js'; -const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => { - const resolvedOptions = { - serviceEntryPoint: '@astrojs/image/sharp', - ...options, - }; - - // During SSG builds, this is used to track all transformed images required. - const staticImages = new Map<string, TransformOptions>(); - - let _config: AstroConfig; - - function getViteConfiguration() { - return { - plugins: [createPlugin(_config, resolvedOptions)], - optimizeDeps: { - include: ['image-size', 'sharp'], - }, - ssr: { - noExternal: ['@astrojs/image', resolvedOptions.serviceEntryPoint], - }, - }; - } - - return { - name: PKG_NAME, - hooks: { - 'astro:config:setup': ({ command, config, injectRoute, updateConfig }) => { - _config = config; - - // Always treat `astro dev` as SSR mode, even without an adapter - const mode = command === 'dev' || config.adapter ? 'ssr' : 'ssg'; - - updateConfig({ vite: getViteConfiguration() }); - - // Used to cache all images rendered to HTML - // Added to globalThis to share the same map in Node and Vite - function addStaticImage(transform: TransformOptions) { - staticImages.set(propsToFilename(transform), transform); - } - - // TODO: Add support for custom, user-provided filename format functions - function filenameFormat(transform: TransformOptions, searchParams: URLSearchParams) { - if (mode === 'ssg') { - return isRemoteImage(transform.src) - ? path.join(OUTPUT_DIR, path.basename(propsToFilename(transform))) - : path.join( - OUTPUT_DIR, - path.dirname(transform.src), - path.basename(propsToFilename(transform)) - ); - } else { - return `${ROUTE_PATTERN}?${searchParams.toString()}`; - } - } - - // Initialize the integration's globalThis namespace - // This is needed to share scope between Node and Vite - globalThis.astroImage = { - loader: undefined, // initialized in first getImage() call - ssrLoader: sharp, - command, - addStaticImage, - filenameFormat, - }; - - if (mode === 'ssr') { - injectRoute({ - pattern: ROUTE_PATTERN, - entryPoint: - command === 'dev' ? '@astrojs/image/endpoints/dev' : '@astrojs/image/endpoints/prod', - }); - } - }, - 'astro:build:done': async ({ dir }) => { - for await (const [filename, transform] of staticImages) { - const loader = globalThis.astroImage.loader; - - if (!loader || !('transform' in loader)) { - // this should never be hit, how was a staticImage added without an SSR service? - return; - } - - let inputBuffer: Buffer | undefined = undefined; - let outputFile: string; - - if (isRemoteImage(transform.src)) { - // try to load the remote image - inputBuffer = await loadRemoteImage(transform.src); - - const outputFileURL = new URL( - path.join('./', OUTPUT_DIR, path.basename(filename)), - dir - ); - outputFile = fileURLToPath(outputFileURL); - } else { - const inputFileURL = new URL(`.${transform.src}`, _config.srcDir); - const inputFile = fileURLToPath(inputFileURL); - inputBuffer = await loadLocalImage(inputFile); - - const outputFileURL = new URL(path.join('./', OUTPUT_DIR, filename), dir); - outputFile = fileURLToPath(outputFileURL); - } - - if (!inputBuffer) { - // eslint-disable-next-line no-console - console.warn(`"${transform.src}" image could not be fetched`); - continue; - } - - const { data } = await loader.transform(inputBuffer, transform); - ensureDir(path.dirname(outputFile)); - await fs.writeFile(outputFile, data); - } - }, - }, - }; -}; - -export default createIntegration; +export default integration; diff --git a/packages/integrations/image/src/integration.ts b/packages/integrations/image/src/integration.ts new file mode 100644 index 000000000..0b9542caa --- /dev/null +++ b/packages/integrations/image/src/integration.ts @@ -0,0 +1,93 @@ +import type { AstroConfig, AstroIntegration } from 'astro'; +import { ssgBuild } from './build/ssg.js'; +import { ssrBuild } from './build/ssr.js'; +import { PKG_NAME, ROUTE_PATTERN } from './constants.js'; +import { filenameFormat, propsToFilename } from './utils/paths.js'; +import { IntegrationOptions, TransformOptions } from './types.js'; +import { createPlugin } from './vite-plugin-astro-image.js'; + +export default function integration(options: IntegrationOptions = {}): AstroIntegration { + const resolvedOptions = { + serviceEntryPoint: '@astrojs/image/sharp', + ...options, + }; + + // During SSG builds, this is used to track all transformed images required. + const staticImages = new Map<string, Map<string, TransformOptions>>(); + + let _config: AstroConfig; + let mode: 'ssr' | 'ssg'; + + function getViteConfiguration() { + return { + plugins: [createPlugin(_config, resolvedOptions)], + optimizeDeps: { + include: ['image-size', 'sharp'], + }, + ssr: { + noExternal: ['@astrojs/image', resolvedOptions.serviceEntryPoint], + }, + }; + } + + return { + name: PKG_NAME, + hooks: { + 'astro:config:setup': ({ command, config, injectRoute, updateConfig }) => { + _config = config; + + // Always treat `astro dev` as SSR mode, even without an adapter + mode = command === 'dev' || config.adapter ? 'ssr' : 'ssg'; + + updateConfig({ vite: getViteConfiguration() }); + + if (mode === 'ssr') { + injectRoute({ + pattern: ROUTE_PATTERN, + entryPoint: + command === 'dev' ? '@astrojs/image/endpoints/dev' : '@astrojs/image/endpoints/prod', + }); + } + }, + 'astro:server:setup': async () => { + globalThis.astroImage = {}; + }, + 'astro:build:setup': () => { + // Used to cache all images rendered to HTML + // Added to globalThis to share the same map in Node and Vite + function addStaticImage(transform: TransformOptions) { + const srcTranforms = staticImages.has(transform.src) + ? staticImages.get(transform.src)! + : new Map<string, TransformOptions>(); + + srcTranforms.set(propsToFilename(transform), transform); + + staticImages.set(transform.src, srcTranforms); + } + + // Helpers for building static images should only be available for SSG + globalThis.astroImage = + mode === 'ssg' + ? { + addStaticImage, + filenameFormat, + } + : {}; + }, + 'astro:build:done': async ({ dir }) => { + if (mode === 'ssr') { + // for SSR builds, copy all image files from src to dist + // to make sure they are available for use in production + await ssrBuild({ srcDir: _config.srcDir, outDir: dir }); + } else { + // for SSG builds, build all requested image transforms to dist + const loader = globalThis?.astroImage?.loader; + + if (loader && 'transform' in loader && staticImages.size > 0) { + await ssgBuild({ loader, staticImages, srcDir: _config.srcDir, outDir: dir }); + } + } + }, + }, + }; +} diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/lib/get-image.ts index 10de5c039..60a6b60da 100644 --- a/packages/integrations/image/src/get-image.ts +++ b/packages/integrations/image/src/lib/get-image.ts @@ -1,5 +1,6 @@ import slash from 'slash'; -import { ROUTE_PATTERN } from './constants.js'; +import { ROUTE_PATTERN } from '../constants.js'; +import sharp from '../loaders/sharp.js'; import { ImageAttributes, ImageMetadata, @@ -7,8 +8,8 @@ import { isSSRService, OutputFormat, TransformOptions, -} from './types.js'; -import { isRemoteImage, parseAspectRatio } from './utils.js'; +} from '../types.js'; +import { isRemoteImage, parseAspectRatio } from '../utils/images.js'; export interface GetImageTransform extends Omit<TransformOptions, 'src'> { src: string | ImageMetadata | Promise<{ default: ImageMetadata }>; @@ -97,24 +98,35 @@ async function resolveTransform(input: GetImageTransform): Promise<TransformOpti /** * Gets the HTML attributes required to build an `<img />` for the transformed image. * - * @param loader @type {ImageService} The image service used for transforming images. * @param transform @type {TransformOptions} The transformations requested for the optimized image. * @returns @type {ImageAttributes} The HTML attributes to be included on the built `<img />` element. */ export async function getImage( - loader: ImageService, transform: GetImageTransform ): Promise<ImageAttributes> { - globalThis.astroImage.loader = loader; + if (!transform.src) { + throw new Error('[@astrojs/image] `src` is required'); + } + + let loader = globalThis.astroImage?.loader; + + if (!loader) { + // @ts-ignore + const { default: mod } = await import('virtual:image-loader'); + loader = mod as ImageService; + globalThis.astroImage = globalThis.astroImage || {}; + globalThis.astroImage.loader = loader; + } const resolved = await resolveTransform(transform); const attributes = await loader.getImageAttributes(resolved); - const isDev = globalThis.astroImage.command === 'dev'; + // @ts-ignore + const isDev = import.meta.env.DEV; const isLocalImage = !isRemoteImage(resolved.src); - const _loader = isDev && isLocalImage ? globalThis.astroImage.ssrLoader : loader; + const _loader = isDev && isLocalImage ? sharp : loader; if (!_loader) { throw new Error('@astrojs/image: loader not found!'); @@ -125,11 +137,11 @@ export async function getImage( const { searchParams } = _loader.serializeTransform(resolved); // cache all images rendered to HTML - if (globalThis?.astroImage) { + if (globalThis.astroImage?.addStaticImage) { globalThis.astroImage.addStaticImage(resolved); } - const src = globalThis?.astroImage + const src = globalThis.astroImage?.filenameFormat ? globalThis.astroImage.filenameFormat(resolved, searchParams) : `${ROUTE_PATTERN}?${searchParams.toString()}`; diff --git a/packages/integrations/image/src/get-picture.ts b/packages/integrations/image/src/lib/get-picture.ts index f8ca694ad..a214e1fe6 100644 --- a/packages/integrations/image/src/get-picture.ts +++ b/packages/integrations/image/src/lib/get-picture.ts @@ -4,14 +4,12 @@ import { getImage } from './get-image.js'; import { ImageAttributes, ImageMetadata, - ImageService, OutputFormat, TransformOptions, -} from './types.js'; -import { parseAspectRatio } from './utils.js'; +} from '../types.js'; +import { parseAspectRatio } from '../utils/images.js'; export interface GetPictureParams { - loader: ImageService; src: string | ImageMetadata | Promise<{ default: ImageMetadata }>; widths: number[]; formats: OutputFormat[]; @@ -46,7 +44,15 @@ async function resolveFormats({ src, formats }: GetPictureParams) { } export async function getPicture(params: GetPictureParams): Promise<GetPictureResult> { - const { loader, src, widths, formats } = params; + const { src, widths } = params; + + if (!src) { + throw new Error('[@astrojs/image] `src` is required'); + } + + if (!widths || !Array.isArray(widths)) { + throw new Error('[@astrojs/image] at least one `width` is required'); + } const aspectRatio = await resolveAspectRatio(params); @@ -57,7 +63,7 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe async function getSource(format: OutputFormat) { const imgs = await Promise.all( widths.map(async (width) => { - const img = await getImage(loader, { + const img = await getImage({ src, format, width, @@ -76,7 +82,7 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe // always include the original image format const allFormats = await resolveFormats(params); - const image = await getImage(loader, { + const image = await getImage({ src, width: Math.max(...widths), aspectRatio, diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts index 86c18839d..b4c5e18fd 100644 --- a/packages/integrations/image/src/loaders/sharp.ts +++ b/packages/integrations/image/src/loaders/sharp.ts @@ -1,6 +1,6 @@ import sharp from 'sharp'; -import type { OutputFormat, SSRImageService, TransformOptions } from '../types'; -import { isAspectRatioString, isOutputFormat } from '../utils.js'; +import type { OutputFormat, SSRImageService, TransformOptions } from '../types.js'; +import { isAspectRatioString, isOutputFormat } from '../utils/images.js'; class SharpService implements SSRImageService { async getImageAttributes(transform: TransformOptions) { @@ -84,6 +84,9 @@ class SharpService implements SSRImageService { async transform(inputBuffer: Buffer, transform: TransformOptions) { const sharpImage = sharp(inputBuffer, { failOnError: false }); + // always call rotate to adjust for EXIF data orientation + sharpImage.rotate(); + if (transform.width || transform.height) { const width = transform.width && Math.round(transform.width); const height = transform.height && Math.round(transform.height); diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index 58a1c59f4..f7e0c0e5f 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -3,15 +3,13 @@ export * from './index.js'; interface ImageIntegration { loader?: ImageService; - ssrLoader: SSRImageService; - command: 'dev' | 'build'; - addStaticImage: (transform: TransformOptions) => void; - filenameFormat: (transform: TransformOptions, searchParams: URLSearchParams) => string; + addStaticImage?: (transform: TransformOptions) => void; + filenameFormat?: (transform: TransformOptions, searchParams: URLSearchParams) => string; } declare global { // eslint-disable-next-line no-var - var astroImage: ImageIntegration; + var astroImage: ImageIntegration | undefined; } export type InputFormat = diff --git a/packages/integrations/image/src/utils.ts b/packages/integrations/image/src/utils/images.ts index 80dff1b6e..55a45d1ce 100644 --- a/packages/integrations/image/src/utils.ts +++ b/packages/integrations/image/src/utils/images.ts @@ -1,7 +1,5 @@ -import fs from 'fs'; -import path from 'path'; -import { shorthash } from './shorthash.js'; -import type { OutputFormat, TransformOptions } from './types'; +import fs from 'fs/promises'; +import type { OutputFormat, TransformOptions } from '../types.js'; export function isOutputFormat(value: string): value is OutputFormat { return ['avif', 'jpeg', 'png', 'webp'].includes(value); @@ -11,17 +9,13 @@ export function isAspectRatioString(value: string): value is `${number}:${number return /^\d*:\d*$/.test(value); } -export function ensureDir(dir: string) { - fs.mkdirSync(dir, { recursive: true }); -} - export function isRemoteImage(src: string) { return /^http(s?):\/\//.test(src); } export async function loadLocalImage(src: string) { try { - return await fs.promises.readFile(src); + return await fs.readFile(src); } catch { return undefined; } @@ -45,26 +39,6 @@ export async function loadImage(src: string) { return isRemoteImage(src) ? await loadRemoteImage(src) : await loadLocalImage(src); } -export function propsToFilename({ src, width, height, format }: TransformOptions) { - const ext = path.extname(src); - let filename = src.replace(ext, ''); - - // for remote images, add a hash of the full URL to dedupe images with the same filename - if (isRemoteImage(src)) { - filename += `-${shorthash(src)}`; - } - - if (width && height) { - return `${filename}_${width}x${height}.${format}`; - } else if (width) { - return `${filename}_${width}w.${format}`; - } else if (height) { - return `${filename}_${height}h.${format}`; - } - - return format ? src.replace(ext, format) : src; -} - export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) { if (!aspectRatio) { return undefined; diff --git a/packages/integrations/image/src/metadata.ts b/packages/integrations/image/src/utils/metadata.ts index 823862ea7..38859b817 100644 --- a/packages/integrations/image/src/metadata.ts +++ b/packages/integrations/image/src/utils/metadata.ts @@ -1,6 +1,6 @@ import fs from 'fs/promises'; import sizeOf from 'image-size'; -import { ImageMetadata, InputFormat } from './types'; +import { ImageMetadata, InputFormat } from '../types.js'; export async function metadata(src: string): Promise<ImageMetadata | undefined> { const file = await fs.readFile(src); diff --git a/packages/integrations/image/src/utils/paths.ts b/packages/integrations/image/src/utils/paths.ts new file mode 100644 index 000000000..90e744252 --- /dev/null +++ b/packages/integrations/image/src/utils/paths.ts @@ -0,0 +1,40 @@ +import fs from 'fs'; +import path from 'path'; +import { OUTPUT_DIR } from '../constants.js'; +import { isRemoteImage } from './images.js'; +import { shorthash } from './shorthash.js'; +import type { TransformOptions } from '../types.js'; + +export function ensureDir(dir: string) { + fs.mkdirSync(dir, { recursive: true }); +} + +export function propsToFilename({ src, width, height, format }: TransformOptions) { + const ext = path.extname(src); + let filename = src.replace(ext, ''); + + // for remote images, add a hash of the full URL to dedupe images with the same filename + if (isRemoteImage(src)) { + filename += `-${shorthash(src)}`; + } + + if (width && height) { + return `${filename}_${width}x${height}.${format}`; + } else if (width) { + return `${filename}_${width}w.${format}`; + } else if (height) { + return `${filename}_${height}h.${format}`; + } + + return format ? src.replace(ext, format) : src; +} + +export function filenameFormat(transform: TransformOptions) { + return isRemoteImage(transform.src) + ? path.join(OUTPUT_DIR, path.basename(propsToFilename(transform))) + : path.join( + OUTPUT_DIR, + path.dirname(transform.src), + path.basename(propsToFilename(transform)) + ); +} diff --git a/packages/integrations/image/src/shorthash.ts b/packages/integrations/image/src/utils/shorthash.ts index 99a691ac4..99a691ac4 100644 --- a/packages/integrations/image/src/shorthash.ts +++ b/packages/integrations/image/src/utils/shorthash.ts diff --git a/packages/integrations/image/src/vite-plugin-astro-image.ts b/packages/integrations/image/src/vite-plugin-astro-image.ts index 2dfda8fa5..5ca9c1571 100644 --- a/packages/integrations/image/src/vite-plugin-astro-image.ts +++ b/packages/integrations/image/src/vite-plugin-astro-image.ts @@ -1,11 +1,10 @@ import type { AstroConfig } from 'astro'; -import fs from 'fs/promises'; import type { PluginContext } from 'rollup'; import slash from 'slash'; import { pathToFileURL } from 'url'; import type { Plugin, ResolvedConfig } from 'vite'; -import { metadata } from './metadata.js'; -import type { IntegrationOptions } from './types'; +import { metadata } from './utils/metadata.js'; +import type { IntegrationOptions } from './types.js'; export function createPlugin(config: AstroConfig, options: Required<IntegrationOptions>): Plugin { const filter = (id: string) => @@ -60,15 +59,7 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO src: slash(src), // Windows compat }; - if (resolvedConfig.isProduction) { - this.emitFile({ - fileName: output.src.replace(/^\//, ''), - source: await fs.readFile(id), - type: 'asset', - }); - } - return `export default ${JSON.stringify(output)}`; - }, + } }; } diff --git a/packages/integrations/image/test/fixtures/rotation/astro.config.mjs b/packages/integrations/image/test/fixtures/rotation/astro.config.mjs new file mode 100644 index 000000000..45a11dc9d --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; +import image from '@astrojs/image'; + +// https://astro.build/config +export default defineConfig({ + site: 'http://localhost:3000', + integrations: [image()] +}); diff --git a/packages/integrations/image/test/fixtures/rotation/package.json b/packages/integrations/image/test/fixtures/rotation/package.json new file mode 100644 index 000000000..502e42c96 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/basic-image", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/image": "workspace:*", + "@astrojs/node": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/image/test/fixtures/rotation/public/favicon.ico b/packages/integrations/image/test/fixtures/rotation/public/favicon.ico Binary files differnew file mode 100644 index 000000000..578ad458b --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/public/favicon.ico diff --git a/packages/integrations/image/test/fixtures/rotation/server/server.mjs b/packages/integrations/image/test/fixtures/rotation/server/server.mjs new file mode 100644 index 000000000..d7a0a7a40 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/server/server.mjs @@ -0,0 +1,44 @@ +import { createServer } from 'http'; +import fs from 'fs'; +import mime from 'mime'; +import { handler as ssrHandler } from '../dist/server/entry.mjs'; + +const clientRoot = new URL('../dist/client/', import.meta.url); + +async function handle(req, res) { + ssrHandler(req, res, async (err) => { + if (err) { + res.writeHead(500); + res.end(err.stack); + return; + } + + let local = new URL('.' + req.url, clientRoot); + try { + const data = await fs.promises.readFile(local); + res.writeHead(200, { + 'Content-Type': mime.getType(req.url), + }); + res.end(data); + } catch { + res.writeHead(404); + res.end(); + } + }); +} + +const server = createServer((req, res) => { + handle(req, res).catch((err) => { + console.error(err); + res.writeHead(500, { + 'Content-Type': 'text/plain', + }); + res.end(err.toString()); + }); +}); + +server.listen(8085); +console.log('Serving at http://localhost:8085'); + +// Silence weird <time> warning +console.error = () => {}; diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_0.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_0.jpg Binary files differnew file mode 100644 index 000000000..8518c82b5 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_0.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_1.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_1.jpg Binary files differnew file mode 100644 index 000000000..fda188236 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_1.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_2.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_2.jpg Binary files differnew file mode 100644 index 000000000..d2605f81b --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_2.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_3.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_3.jpg Binary files differnew file mode 100644 index 000000000..f50805234 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_3.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_4.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_4.jpg Binary files differnew file mode 100644 index 000000000..d73dee8fd --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_4.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_5.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_5.jpg Binary files differnew file mode 100644 index 000000000..975d85883 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_5.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_6.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_6.jpg Binary files differnew file mode 100644 index 000000000..b579b7f9a --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_6.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_7.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_7.jpg Binary files differnew file mode 100644 index 000000000..b1e919cfd --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_7.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_8.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_8.jpg Binary files differnew file mode 100644 index 000000000..c381db10e --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_8.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_0.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_0.jpg Binary files differnew file mode 100644 index 000000000..aa9632e5e --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_0.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_1.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_1.jpg Binary files differnew file mode 100644 index 000000000..dcb57c537 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_1.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_2.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_2.jpg Binary files differnew file mode 100644 index 000000000..8c3adf7af --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_2.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_3.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_3.jpg Binary files differnew file mode 100644 index 000000000..5a5544f23 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_3.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_4.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_4.jpg Binary files differnew file mode 100644 index 000000000..9eb2a6a1e --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_4.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_5.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_5.jpg Binary files differnew file mode 100644 index 000000000..905169aa7 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_5.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_6.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_6.jpg Binary files differnew file mode 100644 index 000000000..8fc576e06 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_6.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_7.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_7.jpg Binary files differnew file mode 100644 index 000000000..cfa04d66e --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_7.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_8.jpg b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_8.jpg Binary files differnew file mode 100644 index 000000000..b2a50d6eb --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_8.jpg diff --git a/packages/integrations/image/test/fixtures/rotation/src/pages/index.astro b/packages/integrations/image/test/fixtures/rotation/src/pages/index.astro new file mode 100644 index 000000000..52124b5c0 --- /dev/null +++ b/packages/integrations/image/test/fixtures/rotation/src/pages/index.astro @@ -0,0 +1,48 @@ +--- +import { Image } from '@astrojs/image/components'; +--- + +<html> + <head> + <!-- Head Stuff --> + </head> + <body> + <Image id='landscape-0' src={import('../assets/Landscape_0.jpg')} /> + <br /> + <Image id='landscape-1' src={import('../assets/Landscape_1.jpg')} /> + <br /> + <Image id='landscape-2' src={import('../assets/Landscape_2.jpg')} /> + <br /> + <Image id='landscape-3' src={import('../assets/Landscape_3.jpg')} /> + <br /> + <Image id='landscape-4' src={import('../assets/Landscape_4.jpg')} /> + <br /> + <Image id='landscape-5' src={import('../assets/Landscape_5.jpg')} /> + <br /> + <Image id='landscape-6' src={import('../assets/Landscape_6.jpg')} /> + <br /> + <Image id='landscape-7' src={import('../assets/Landscape_7.jpg')} /> + <br /> + <Image id='landscape-8' src={import('../assets/Landscape_8.jpg')} /> + <br /> + + <Image id='portrait-0' src={import('../assets/Portrait_0.jpg')} /> + <br /> + <Image id='portrait-1' src={import('../assets/Portrait_1.jpg')} /> + <br /> + <Image id='portrait-2' src={import('../assets/Portrait_2.jpg')} /> + <br /> + <Image id='portrait-3' src={import('../assets/Portrait_3.jpg')} /> + <br /> + <Image id='portrait-4' src={import('../assets/Portrait_4.jpg')} /> + <br /> + <Image id='portrait-5' src={import('../assets/Portrait_5.jpg')} /> + <br /> + <Image id='portrait-6' src={import('../assets/Portrait_6.jpg')} /> + <br /> + <Image id='portrait-7' src={import('../assets/Portrait_7.jpg')} /> + <br /> + <Image id='portrait-8' src={import('../assets/Portrait_8.jpg')} /> + <br /> + </body> +</html> diff --git a/packages/integrations/image/test/image-ssg.test.js b/packages/integrations/image/test/image-ssg.test.js index 8b93dc037..0b1fe192a 100644 --- a/packages/integrations/image/test/image-ssg.test.js +++ b/packages/integrations/image/test/image-ssg.test.js @@ -30,7 +30,7 @@ describe('SSG images', function () { }); describe('Local images', () => { - it('includes src, width, and height attributes', () => { + it('includes <img> attributes', () => { const image = $('#social-jpg'); expect(image.attr('src')).to.equal('/_image/assets/social_506x253.jpg'); @@ -40,7 +40,7 @@ describe('SSG images', function () { }); describe('Inline imports', () => { - it('includes src, width, and height attributes', () => { + it('includes <img> attributes', () => { const image = $('#inline'); expect(image.attr('src')).to.equal('/_image/assets/social_506x253.jpg'); @@ -62,7 +62,7 @@ describe('SSG images', function () { // on the static `src` string const HASH = 'Z1iI4xW'; - it('includes src, width, and height attributes', () => { + it('includes <img> attributes', () => { const image = $('#google'); expect(image.attr('src')).to.equal( @@ -97,7 +97,7 @@ describe('SSG images', function () { }); describe('Local images', () => { - it('includes src, width, and height attributes', () => { + it('includes <img> attributes', () => { const image = $('#social-jpg'); const src = image.attr('src'); @@ -127,7 +127,7 @@ describe('SSG images', function () { }); describe('Local images with inline imports', () => { - it('includes src, width, and height attributes', () => { + it('includes <img> attributes', () => { const image = $('#inline'); const src = image.attr('src'); @@ -157,7 +157,7 @@ describe('SSG images', function () { }); describe('Remote images', () => { - it('includes src, width, and height attributes', () => { + it('includes <img> attributes', () => { const image = $('#google'); const src = image.attr('src'); diff --git a/packages/integrations/image/test/image-ssr.test.js b/packages/integrations/image/test/image-ssr.test.js index 784a92e53..33ef7a5f5 100644 --- a/packages/integrations/image/test/image-ssr.test.js +++ b/packages/integrations/image/test/image-ssr.test.js @@ -1,15 +1,24 @@ import { expect } from 'chai'; import * as cheerio from 'cheerio'; +import sizeOf from 'image-size'; +import { fileURLToPath } from 'url'; import { loadFixture } from './test-utils.js'; import testAdapter from '../../../astro/test/test-adapter.js'; describe('SSR images - build', function () { let fixture; + function verifyImage(pathname) { + const url = new URL('./fixtures/basic-image/dist/client' + pathname, import.meta.url); + const dist = fileURLToPath(url); + const result = sizeOf(dist); + expect(result).not.be.be.undefined; + } + before(async () => { fixture = await loadFixture({ root: './fixtures/basic-image/', - adapter: testAdapter(), + adapter: testAdapter({ streaming: false }), experimental: { ssr: true, }, @@ -42,8 +51,7 @@ describe('SSR images - build', function () { expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true); }); - // TODO: Track down why the fixture.fetch is failing with the test adapter - it.skip('built the optimized image', async () => { + it('built the optimized image', async () => { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com/'); @@ -53,13 +61,18 @@ describe('SSR images - build', function () { const image = $('#social-jpg'); - const res = await fixture.fetch(image.attr('src')); + const imgRequest = new Request(`http://example.com${image.attr('src')}`); + const imgResponse = await app.render(imgRequest); - expect(res.status).to.equal(200); - expect(res.headers.get('Content-Type')).to.equal('image/jpeg'); + expect(imgResponse.status).to.equal(200); + expect(imgResponse.headers.get('Content-Type')).to.equal('image/jpeg'); // TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers }); + + it('includes the original images', () => { + ['/assets/social.jpg', '/assets/social.png', '/assets/blog/introducing-astro.jpg'].map(verifyImage); + }); }); describe('Inline imports', () => { diff --git a/packages/integrations/image/test/picture-ssg.test.js b/packages/integrations/image/test/picture-ssg.test.js index d8851dbfa..d8719af29 100644 --- a/packages/integrations/image/test/picture-ssg.test.js +++ b/packages/integrations/image/test/picture-ssg.test.js @@ -62,6 +62,10 @@ describe('SSG pictures', function () { verifyImage('_image/assets/social_506x253.webp', { width: 506, height: 253, type: 'webp' }); verifyImage('_image/assets/social_506x253.jpg', { width: 506, height: 253, type: 'jpg' }); }); + + it('dist includes original image', () => { + verifyImage('assets/social.jpg', { width: 2024, height: 1012, type: 'jpg' }); + }); }); describe('Inline imports', () => { diff --git a/packages/integrations/image/test/picture-ssr.test.js b/packages/integrations/image/test/picture-ssr.test.js index 4914b7354..8810ec760 100644 --- a/packages/integrations/image/test/picture-ssr.test.js +++ b/packages/integrations/image/test/picture-ssr.test.js @@ -1,11 +1,20 @@ import { expect } from 'chai'; import * as cheerio from 'cheerio'; +import sizeOf from 'image-size'; +import { fileURLToPath } from 'url'; import { loadFixture } from './test-utils.js'; import testAdapter from '../../../astro/test/test-adapter.js'; describe('SSR pictures - build', function () { let fixture; + function verifyImage(pathname) { + const url = new URL('./fixtures/basic-image/dist/client' + pathname, import.meta.url); + const dist = fileURLToPath(url); + const result = sizeOf(dist); + expect(result).not.be.be.undefined; + } + before(async () => { fixture = await loadFixture({ root: './fixtures/basic-picture/', @@ -58,8 +67,7 @@ describe('SSR pictures - build', function () { expect(image.attr('alt')).to.equal('Social image'); }); - // TODO: Track down why the fixture.fetch is failing with the test adapter - it.skip('built the optimized image', async () => { + it('built the optimized image', async () => { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com/'); @@ -69,13 +77,18 @@ describe('SSR pictures - build', function () { const image = $('#social-jpg img'); - const res = await fixture.fetch(image.attr('src')); + const imgRequest = new Request(`http://example.com${image.attr('src')}`); + const imgResponse = await app.render(imgRequest); - expect(res.status).to.equal(200); - expect(res.headers.get('Content-Type')).to.equal('image/jpeg'); + expect(imgResponse.status).to.equal(200); + expect(imgResponse.headers.get('Content-Type')).to.equal('image/jpeg'); // TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers }); + + it('includes the original images', () => { + ['/assets/social.jpg', '/assets/social.png', '/assets/blog/introducing-astro.jpg'].map(verifyImage); + }); }); describe('Inline imports', () => { diff --git a/packages/integrations/image/test/rotation.test.js b/packages/integrations/image/test/rotation.test.js new file mode 100644 index 000000000..9eee72918 --- /dev/null +++ b/packages/integrations/image/test/rotation.test.js @@ -0,0 +1,68 @@ +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; +import sizeOf from 'image-size'; +import { fileURLToPath } from 'url'; +import { loadFixture } from './test-utils.js'; + +let fixture; + +describe('Image rotation', function () { + before(async () => { + fixture = await loadFixture({ root: './fixtures/rotation/' }); + }); + + function verifyImage(pathname, expected) { + const url = new URL('./fixtures/rotation/dist/' + pathname, import.meta.url); + const dist = fileURLToPath(url); + const result = sizeOf(dist); + expect(result).to.deep.equal(expected); + } + + describe('build', () => { + let $; + let html; + + before(async () => { + await fixture.build(); + + html = await fixture.readFile('/index.html'); + $ = cheerio.load(html); + }); + + describe('Landscape images', () => { + it('includes <img> attributes', () => { + for (let i = 0; i < 9; i++) { + const image = $(`#landscape-${i}`); + + expect(image.attr('src')).to.equal(`/_image/assets/Landscape_${i}_1800x1200.jpg`); + expect(image.attr('width')).to.equal('1800'); + expect(image.attr('height')).to.equal('1200'); + } + }); + + it('built the optimized image', () => { + for (let i = 0; i < 9; i++) { + verifyImage(`/_image/assets/Landscape_${i}_1800x1200.jpg`, { width: 1800, height: 1200, type: 'jpg' }); + } + }); + }); + + describe('Portait images', () => { + it('includes <img> attributes', () => { + for (let i = 0; i < 9; i++) { + const image = $(`#portrait-${i}`); + + expect(image.attr('src')).to.equal(`/_image/assets/Portrait_${i}_1200x1800.jpg`); + expect(image.attr('width')).to.equal('1200'); + expect(image.attr('height')).to.equal('1800'); + } + }); + + it('built the optimized image', () => { + for (let i = 0; i < 9; i++) { + verifyImage(`/_image/assets/Portrait_${i}_1200x1800.jpg`, { width: 1200, height: 1800, type: 'jpg' }); + } + }); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2c67e34b..46b4116c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2082,6 +2082,7 @@ importers: mrmime: ^1.0.0 sharp: ^0.30.6 slash: ^4.0.0 + tiny-glob: ^0.2.9 dependencies: etag: 1.8.1 image-size: 1.0.2 @@ -2094,6 +2095,7 @@ importers: '@types/sharp': 0.30.4 astro: link:../../astro astro-scripts: link:../../../scripts + tiny-glob: 0.2.9 packages/integrations/image/test/fixtures/basic-image: specifiers: @@ -2115,6 +2117,16 @@ importers: '@astrojs/node': link:../../../../node astro: link:../../../../../astro + packages/integrations/image/test/fixtures/rotation: + specifiers: + '@astrojs/image': workspace:* + '@astrojs/node': workspace:* + astro: workspace:* + dependencies: + '@astrojs/image': link:../../.. + '@astrojs/node': link:../../../../node + astro: link:../../../../../astro + packages/integrations/lit: specifiers: '@lit-labs/ssr': ^2.2.0 |