summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/tiny-glasses-play.md6
-rw-r--r--packages/integrations/image/components/Image.astro7
-rw-r--r--packages/integrations/image/components/Picture.astro8
-rw-r--r--packages/integrations/image/package.json3
-rw-r--r--packages/integrations/image/src/build/ssg.ts79
-rw-r--r--packages/integrations/image/src/build/ssr.ts29
-rw-r--r--packages/integrations/image/src/endpoints/dev.ts5
-rw-r--r--packages/integrations/image/src/endpoints/prod.ts15
-rw-r--r--packages/integrations/image/src/index.ts140
-rw-r--r--packages/integrations/image/src/integration.ts93
-rw-r--r--packages/integrations/image/src/lib/get-image.ts (renamed from packages/integrations/image/src/get-image.ts)32
-rw-r--r--packages/integrations/image/src/lib/get-picture.ts (renamed from packages/integrations/image/src/get-picture.ts)20
-rw-r--r--packages/integrations/image/src/loaders/sharp.ts7
-rw-r--r--packages/integrations/image/src/types.ts8
-rw-r--r--packages/integrations/image/src/utils/images.ts (renamed from packages/integrations/image/src/utils.ts)32
-rw-r--r--packages/integrations/image/src/utils/metadata.ts (renamed from packages/integrations/image/src/metadata.ts)2
-rw-r--r--packages/integrations/image/src/utils/paths.ts40
-rw-r--r--packages/integrations/image/src/utils/shorthash.ts (renamed from packages/integrations/image/src/shorthash.ts)0
-rw-r--r--packages/integrations/image/src/vite-plugin-astro-image.ts15
-rw-r--r--packages/integrations/image/test/fixtures/rotation/astro.config.mjs8
-rw-r--r--packages/integrations/image/test/fixtures/rotation/package.json10
-rw-r--r--packages/integrations/image/test/fixtures/rotation/public/favicon.icobin0 -> 4286 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/server/server.mjs44
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_0.jpgbin0 -> 349915 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_1.jpgbin0 -> 347327 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_2.jpgbin0 -> 349209 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_3.jpgbin0 -> 348796 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_4.jpgbin0 -> 348052 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_5.jpgbin0 -> 351275 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_6.jpgbin0 -> 352727 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_7.jpgbin0 -> 351856 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_8.jpgbin0 -> 352067 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_0.jpgbin0 -> 248531 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_1.jpgbin0 -> 245684 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_2.jpgbin0 -> 246915 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_3.jpgbin0 -> 247276 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_4.jpgbin0 -> 246554 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_5.jpgbin0 -> 251487 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_6.jpgbin0 -> 251800 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_7.jpgbin0 -> 250892 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_8.jpgbin0 -> 251978 bytes
-rw-r--r--packages/integrations/image/test/fixtures/rotation/src/pages/index.astro48
-rw-r--r--packages/integrations/image/test/image-ssg.test.js12
-rw-r--r--packages/integrations/image/test/image-ssr.test.js25
-rw-r--r--packages/integrations/image/test/picture-ssg.test.js4
-rw-r--r--packages/integrations/image/test/picture-ssr.test.js23
-rw-r--r--packages/integrations/image/test/rotation.test.js68
-rw-r--r--pnpm-lock.yaml12
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
new file mode 100644
index 000000000..578ad458b
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/public/favicon.ico
Binary files differ
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
new file mode 100644
index 000000000..8518c82b5
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_0.jpg
Binary files differ
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
new file mode 100644
index 000000000..fda188236
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_1.jpg
Binary files differ
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
new file mode 100644
index 000000000..d2605f81b
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_2.jpg
Binary files differ
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
new file mode 100644
index 000000000..f50805234
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_3.jpg
Binary files differ
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
new file mode 100644
index 000000000..d73dee8fd
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_4.jpg
Binary files differ
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
new file mode 100644
index 000000000..975d85883
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_5.jpg
Binary files differ
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
new file mode 100644
index 000000000..b579b7f9a
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_6.jpg
Binary files differ
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
new file mode 100644
index 000000000..b1e919cfd
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_7.jpg
Binary files differ
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
new file mode 100644
index 000000000..c381db10e
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Landscape_8.jpg
Binary files differ
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
new file mode 100644
index 000000000..aa9632e5e
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_0.jpg
Binary files differ
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
new file mode 100644
index 000000000..dcb57c537
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_1.jpg
Binary files differ
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
new file mode 100644
index 000000000..8c3adf7af
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_2.jpg
Binary files differ
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
new file mode 100644
index 000000000..5a5544f23
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_3.jpg
Binary files differ
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
new file mode 100644
index 000000000..9eb2a6a1e
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_4.jpg
Binary files differ
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
new file mode 100644
index 000000000..905169aa7
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_5.jpg
Binary files differ
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
new file mode 100644
index 000000000..8fc576e06
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_6.jpg
Binary files differ
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
new file mode 100644
index 000000000..cfa04d66e
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_7.jpg
Binary files differ
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
new file mode 100644
index 000000000..b2a50d6eb
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/rotation/src/assets/Portrait_8.jpg
Binary files differ
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