Image integration refactor and cleanup (#4482)
* WIP: simplifying the use of `fs` vs. the vite plugin * removing a few node deps (etag and node:path) * adding ts defs for sharp * using the same mime package as astro's core App * fixing file URL support in windows * using file URLs when loading local image metadata * fixing a bug in the etag helper * Windows compat * splitting out dev & build tests * why do these suites fail in parallel? * one last windows compat case * Adding tests for treating /public images the same as remote URLs * a couple fixes for Astro's `base` config * adding base path tests for SSR * fixing a bad merge, lost the kleur dependency * adding a test suite for images + MDX * chore: add changeset * simplifying the with-mdx tests * bugfix: don't duplicate the period when using existing file extensions * let Vite cache the image loader service * adding some docs for using /public images * fixing changeset * Update packages/integrations/image/README.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update packages/integrations/image/README.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * nit: minor README syntax tweaks Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
12
.changeset/lucky-mirrors-type.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
'@astrojs/image': minor
|
||||
---
|
||||
|
||||
`<Image />` and `<Picture />` now support using images in the `/public` directory :tada:
|
||||
|
||||
- Moving handling of local image files into the Vite plugin
|
||||
- Optimized image files are now built to `/dist` with hashes provided by Vite, removing the need for a `/dist/_image` directory
|
||||
- Removes three npm dependencies: `etag`, `slash`, and `tiny-glob`
|
||||
- Replaces `mrmime` with the `mime` package already used by Astro's SSR server
|
||||
- Simplifies the injected `_image` route to work for both `dev` and `build`
|
||||
- Adds a new test suite for using images with `@astrojs/mdx` - including optimizing images straight from `/public`
|
|
@ -106,7 +106,11 @@ In addition to the component-specific properties, any valid HTML attribute for t
|
|||
|
||||
Source for the original image file.
|
||||
|
||||
For images in your project's repository, use the `src` relative to the `public` directory. For remote images, provide the full URL.
|
||||
For images located in your project's `src`: use the file path relative to the `src` directory. (e.g. `src="../assets/source-pic.png"`)
|
||||
|
||||
For images located in your `public` directory: use the URL path relative to the `public` directory. (e.g. `src="/images/public-image.jpg"`)
|
||||
|
||||
For remote images, provide the full URL. (e.g. `src="https://astro.build/assets/blog/astro-1-release-update.avif"`)
|
||||
|
||||
#### format
|
||||
|
||||
|
@ -182,7 +186,7 @@ A `number` can also be provided, useful when the aspect ratio is calculated at b
|
|||
|
||||
Source for the original image file.
|
||||
|
||||
For images in your project's repository, use the `src` relative to the `public` directory. For remote images, provide the full URL.
|
||||
For images in your project's repository, use the path relative to the `src` or `public` directory. For remote images, provide the full URL.
|
||||
|
||||
#### alt
|
||||
|
||||
|
@ -341,6 +345,24 @@ import heroImage from '../assets/hero.png';
|
|||
<Image src={import('../assets/hero.png')} />
|
||||
```
|
||||
|
||||
#### Images in `/public`
|
||||
|
||||
Files in the `/public` directory are always served or copied as-is, with no processing. We recommend that local images are always kept in `src/` so that Astro can transform, optimize and bundle them. But if you absolutely must keep an image in `public/`, use its relative URL path as the image's `src=` attribute. It will be treated as a remote image, which requires an `aspectRatio` attribute.
|
||||
|
||||
Alternatively, you can import an image from your `public/` directory in your frontmatter and use a variable in your `src=` attribute. You cannot, however, import this directly inside the component as its `src` value.
|
||||
|
||||
For example, use an image located at `public/social.png` in either static or SSR builds like so:
|
||||
|
||||
```astro title="src/pages/page.astro"
|
||||
---
|
||||
import { Image } from '@astrojs/image/components';
|
||||
import socialImage from '/social.png';
|
||||
---
|
||||
// In static builds: the image will be built and optimized to `/dist`.
|
||||
// In SSR builds: the image will be optimized by the server when requested by a browser.
|
||||
<Image src={socialImage} width={1280} aspectRatio="16:9" />
|
||||
```
|
||||
|
||||
### Remote images
|
||||
|
||||
Remote images can be transformed with the `<Image />` component. The `<Image />` component needs to know the final dimensions for the `<img />` element to avoid content layout shifts. For remote images, this means you must either provide `width` and `height`, or one of the dimensions plus the required `aspectRatio`.
|
||||
|
|
|
@ -21,9 +21,8 @@
|
|||
"homepage": "https://docs.astro.build/en/guides/integrations-guide/image/",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./endpoint": "./dist/endpoint.js",
|
||||
"./sharp": "./dist/loaders/sharp.js",
|
||||
"./endpoints/dev": "./dist/endpoints/dev.js",
|
||||
"./endpoints/prod": "./dist/endpoints/prod.js",
|
||||
"./components": "./components/index.js",
|
||||
"./package.json": "./package.json",
|
||||
"./client": "./client.d.ts",
|
||||
|
@ -41,19 +40,15 @@
|
|||
"test": "mocha --exit --timeout 20000 test"
|
||||
},
|
||||
"dependencies": {
|
||||
"etag": "^1.8.1",
|
||||
"image-size": "^1.0.1",
|
||||
"mrmime": "^1.0.0",
|
||||
"sharp": "^0.30.6",
|
||||
"slash": "^4.0.0",
|
||||
"tiny-glob": "^0.2.9"
|
||||
"image-size": "^1.0.2",
|
||||
"magic-string": "^0.25.9",
|
||||
"mime": "^3.0.0",
|
||||
"sharp": "^0.30.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/etag": "^1.8.1",
|
||||
"@types/sharp": "^0.30.4",
|
||||
"@types/sharp": "^0.30.5",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"kleur": "^4.1.4",
|
||||
"tiny-glob": "^0.2.9"
|
||||
"kleur": "^4.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ import { bgGreen, black, cyan, dim, green } from 'kleur/colors';
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { OUTPUT_DIR } from '../constants.js';
|
||||
import type { AstroConfig } from 'astro';
|
||||
import type { SSRImageService, TransformOptions } from '../loaders/index.js';
|
||||
import { isRemoteImage, loadLocalImage, loadRemoteImage } from '../utils/images.js';
|
||||
import { loadLocalImage, loadRemoteImage } from '../utils/images.js';
|
||||
import { debug, info, LoggerLevel, warn } from '../utils/logger.js';
|
||||
import { ensureDir } from '../utils/paths.js';
|
||||
import { isRemoteImage } from '../utils/paths.js';
|
||||
|
||||
function getTimeStat(timeStart: number, timeEnd: number) {
|
||||
const buildTime = timeEnd - timeStart;
|
||||
|
@ -16,12 +16,12 @@ function getTimeStat(timeStart: number, timeEnd: number) {
|
|||
export interface SSGBuildParams {
|
||||
loader: SSRImageService;
|
||||
staticImages: Map<string, Map<string, TransformOptions>>;
|
||||
srcDir: URL;
|
||||
config: AstroConfig;
|
||||
outDir: URL;
|
||||
logLevel: LoggerLevel;
|
||||
}
|
||||
|
||||
export async function ssgBuild({ loader, staticImages, srcDir, outDir, logLevel }: SSGBuildParams) {
|
||||
export async function ssgBuild({ loader, staticImages, config, outDir, logLevel }: SSGBuildParams) {
|
||||
const timer = performance.now();
|
||||
|
||||
info({
|
||||
|
@ -35,15 +35,21 @@ export async function ssgBuild({ loader, staticImages, srcDir, outDir, logLevel
|
|||
const inputFiles = new Set<string>();
|
||||
|
||||
// process transforms one original image file at a time
|
||||
for (const [src, transformsMap] of staticImages) {
|
||||
for (let [src, transformsMap] of staticImages) {
|
||||
let inputFile: string | undefined = undefined;
|
||||
let inputBuffer: Buffer | undefined = undefined;
|
||||
|
||||
// Vite will prefix a hashed image with the base path, we need to strip this
|
||||
// off to find the actual file relative to /dist
|
||||
if (config.base && src.startsWith(config.base)) {
|
||||
src = src.substring(config.base.length - 1);
|
||||
}
|
||||
|
||||
if (isRemoteImage(src)) {
|
||||
// try to load the remote image
|
||||
inputBuffer = await loadRemoteImage(src);
|
||||
} else {
|
||||
const inputFileURL = new URL(`.${src}`, srcDir);
|
||||
const inputFileURL = new URL(`.${src}`, outDir);
|
||||
inputFile = fileURLToPath(inputFileURL);
|
||||
inputBuffer = await loadLocalImage(inputFile);
|
||||
|
||||
|
@ -62,39 +68,21 @@ export async function ssgBuild({ loader, staticImages, srcDir, outDir, logLevel
|
|||
debug({ level: logLevel, prefix: false, message: `${green('▶')} ${src}` });
|
||||
let timeStart = performance.now();
|
||||
|
||||
if (inputFile) {
|
||||
const to = inputFile.replace(fileURLToPath(srcDir), fileURLToPath(outDir));
|
||||
await ensureDir(path.dirname(to));
|
||||
await fs.copyFile(inputFile, to);
|
||||
|
||||
const timeEnd = performance.now();
|
||||
const timeChange = getTimeStat(timeStart, timeEnd);
|
||||
const timeIncrease = `(+${timeChange})`;
|
||||
const pathRelative = inputFile.replace(fileURLToPath(srcDir), '');
|
||||
debug({
|
||||
level: logLevel,
|
||||
prefix: false,
|
||||
message: ` ${cyan('└─')} ${dim(`(original) ${pathRelative}`)} ${dim(timeIncrease)}`,
|
||||
});
|
||||
}
|
||||
|
||||
// process each transformed versiono of the
|
||||
for (const [filename, transform] of transforms) {
|
||||
timeStart = performance.now();
|
||||
let outputFile: string;
|
||||
|
||||
if (isRemoteImage(src)) {
|
||||
const outputFileURL = new URL(path.join('./', OUTPUT_DIR, path.basename(filename)), outDir);
|
||||
const outputFileURL = new URL(path.join('./', path.basename(filename)), outDir);
|
||||
outputFile = fileURLToPath(outputFileURL);
|
||||
} else {
|
||||
const outputFileURL = new URL(path.join('./', OUTPUT_DIR, filename), outDir);
|
||||
const outputFileURL = new URL(path.join('./', filename), outDir);
|
||||
outputFile = fileURLToPath(outputFileURL);
|
||||
}
|
||||
|
||||
const { data } = await loader.transform(inputBuffer, transform);
|
||||
|
||||
ensureDir(path.dirname(outputFile));
|
||||
|
||||
await fs.writeFile(outputFile, data);
|
||||
|
||||
const timeEnd = performance.now();
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import glob from 'tiny-glob';
|
||||
import { ensureDir } from '../utils/paths.js';
|
||||
|
||||
async function globImages(dir: URL) {
|
||||
const srcPath = fileURLToPath(dir);
|
||||
return await glob('./**/*.{heic,heif,avif,jpeg,jpg,png,tiff,webp,gif}', {
|
||||
cwd: fileURLToPath(dir),
|
||||
});
|
||||
}
|
||||
|
||||
export interface SSRBuildParams {
|
||||
srcDir: URL;
|
||||
outDir: URL;
|
||||
}
|
||||
|
||||
export async function ssrBuild({ srcDir, outDir }: SSRBuildParams) {
|
||||
const images = await globImages(srcDir);
|
||||
|
||||
for (const image of images) {
|
||||
const from = path.join(fileURLToPath(srcDir), image);
|
||||
const to = path.join(fileURLToPath(outDir), image);
|
||||
|
||||
await ensureDir(path.dirname(to));
|
||||
await fs.copyFile(from, to);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export const PKG_NAME = '@astrojs/image';
|
||||
export const ROUTE_PATTERN = '/_image';
|
||||
export const OUTPUT_DIR = '/_image';
|
|
@ -1,31 +1,39 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import etag from 'etag';
|
||||
import { lookup } from 'mrmime';
|
||||
import mime from 'mime';
|
||||
// @ts-ignore
|
||||
import loader from 'virtual:image-loader';
|
||||
import { isRemoteImage, loadLocalImage, loadRemoteImage } from '../utils/images.js';
|
||||
import { etag } from './utils/etag.js';
|
||||
import { isRemoteImage } from './utils/paths.js';
|
||||
|
||||
async function loadRemoteImage(src: URL) {
|
||||
try {
|
||||
const res = await fetch(src);
|
||||
|
||||
if (!res.ok) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const get: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const transform = loader.parseTransform(url.searchParams);
|
||||
|
||||
if (!transform) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
let inputBuffer: Buffer | undefined = undefined;
|
||||
|
||||
if (isRemoteImage(transform.src)) {
|
||||
inputBuffer = await loadRemoteImage(transform.src);
|
||||
} else {
|
||||
const clientRoot = new URL('../client/', import.meta.url);
|
||||
const localPath = new URL('.' + transform.src, clientRoot);
|
||||
inputBuffer = await loadLocalImage(localPath);
|
||||
}
|
||||
// TODO: handle config subpaths?
|
||||
const sourceUrl = isRemoteImage(transform.src)
|
||||
? new URL(transform.src)
|
||||
: new URL(transform.src, url.origin);
|
||||
inputBuffer = await loadRemoteImage(sourceUrl);
|
||||
|
||||
if (!inputBuffer) {
|
||||
return new Response(`"${transform.src} not found`, { status: 404 });
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const { data, format } = await loader.transform(inputBuffer, transform);
|
||||
|
@ -33,13 +41,13 @@ export const get: APIRoute = async ({ request }) => {
|
|||
return new Response(data, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': lookup(format) || '',
|
||||
'Content-Type': mime.getType(format) || '',
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
ETag: etag(inputBuffer),
|
||||
ETag: etag(data.toString()),
|
||||
Date: new Date().toUTCString(),
|
||||
},
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
return new Response(`Server Error: ${err}`, { status: 500 });
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import { lookup } from 'mrmime';
|
||||
import loader from '../loaders/sharp.js';
|
||||
import { loadImage } from '../utils/images.js';
|
||||
|
||||
export const get: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const transform = loader.parseTransform(url.searchParams);
|
||||
|
||||
if (!transform) {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const inputBuffer = await loadImage(transform.src);
|
||||
|
||||
if (!inputBuffer) {
|
||||
return new Response(`"${transform.src} not found`, { status: 404 });
|
||||
}
|
||||
|
||||
const { data, format } = await loader.transform(inputBuffer, transform);
|
||||
|
||||
return new Response(data, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': lookup(format) || '',
|
||||
},
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
return new Response(`Server Error: ${err}`, { status: 500 });
|
||||
}
|
||||
};
|
|
@ -1,21 +1,19 @@
|
|||
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 { ImageService, TransformOptions } from './loaders/index.js';
|
||||
import type { LoggerLevel } from './utils/logger.js';
|
||||
import { filenameFormat, propsToFilename } from './utils/paths.js';
|
||||
import { createPlugin } from './vite-plugin-astro-image.js';
|
||||
import { ssgBuild } from './build/ssg.js';
|
||||
import type { ImageService, TransformOptions } from './loaders/index.js';
|
||||
import type { LoggerLevel } from './utils/logger.js';
|
||||
import { joinPaths, prependForwardSlash, propsToFilename } from './utils/paths.js';
|
||||
|
||||
export { getImage } from './lib/get-image.js';
|
||||
export { getPicture } from './lib/get-picture.js';
|
||||
export * from './loaders/index.js';
|
||||
export type { ImageMetadata } from './vite-plugin-astro-image.js';
|
||||
|
||||
const PKG_NAME = '@astrojs/image';
|
||||
const ROUTE_PATTERN = '/_image';
|
||||
|
||||
interface ImageIntegration {
|
||||
loader?: ImageService;
|
||||
addStaticImage?: (transform: TransformOptions) => void;
|
||||
filenameFormat?: (transform: TransformOptions, searchParams: URLSearchParams) => string;
|
||||
addStaticImage?: (transform: TransformOptions) => string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -38,12 +36,11 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
|||
...options,
|
||||
};
|
||||
|
||||
let _config: AstroConfig;
|
||||
|
||||
// During SSG builds, this is used to track all transformed images required.
|
||||
const staticImages = new Map<string, Map<string, TransformOptions>>();
|
||||
|
||||
let _config: AstroConfig;
|
||||
let output: 'server' | 'static';
|
||||
|
||||
function getViteConfiguration() {
|
||||
return {
|
||||
plugins: [createPlugin(_config, resolvedOptions)],
|
||||
|
@ -59,25 +56,18 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
|||
return {
|
||||
name: PKG_NAME,
|
||||
hooks: {
|
||||
'astro:config:setup': ({ command, config, injectRoute, updateConfig }) => {
|
||||
'astro:config:setup': ({ command, config, updateConfig, injectRoute }) => {
|
||||
_config = config;
|
||||
|
||||
// Always treat `astro dev` as SSR mode, even without an adapter
|
||||
output = command === 'dev' ? 'server' : config.output;
|
||||
|
||||
updateConfig({ vite: getViteConfiguration() });
|
||||
|
||||
if (output === 'server') {
|
||||
if (command === 'dev' || config.output === 'server') {
|
||||
injectRoute({
|
||||
pattern: ROUTE_PATTERN,
|
||||
entryPoint:
|
||||
command === 'dev' ? '@astrojs/image/endpoints/dev' : '@astrojs/image/endpoints/prod',
|
||||
entryPoint: '@astrojs/image/endpoint',
|
||||
});
|
||||
}
|
||||
},
|
||||
'astro:server:setup': async ({ server }) => {
|
||||
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
|
||||
|
@ -86,26 +76,28 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
|||
? staticImages.get(transform.src)!
|
||||
: new Map<string, TransformOptions>();
|
||||
|
||||
srcTranforms.set(propsToFilename(transform), transform);
|
||||
const filename = propsToFilename(transform);
|
||||
|
||||
srcTranforms.set(filename, transform);
|
||||
staticImages.set(transform.src, srcTranforms);
|
||||
|
||||
// Prepend the Astro config's base path, if it was used.
|
||||
// Doing this here makes sure that base is ignored when building
|
||||
// staticImages to /dist, but the rendered HTML will include the
|
||||
// base prefix for `src`.
|
||||
return prependForwardSlash(joinPaths(_config.base, filename));
|
||||
}
|
||||
|
||||
// Helpers for building static images should only be available for SSG
|
||||
globalThis.astroImage =
|
||||
output === 'static'
|
||||
_config.output === 'static'
|
||||
? {
|
||||
addStaticImage,
|
||||
filenameFormat,
|
||||
}
|
||||
: {};
|
||||
},
|
||||
'astro:build:done': async ({ dir }) => {
|
||||
if (output === 'server') {
|
||||
// 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 {
|
||||
if (_config.output === 'static') {
|
||||
// for SSG builds, build all requested image transforms to dist
|
||||
const loader = globalThis?.astroImage?.loader;
|
||||
|
||||
|
@ -113,13 +105,13 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
|||
await ssgBuild({
|
||||
loader,
|
||||
staticImages,
|
||||
srcDir: _config.srcDir,
|
||||
config: _config,
|
||||
outDir: dir,
|
||||
logLevel: resolvedOptions.logLevel,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
/// <reference types="astro/astro-jsx" />
|
||||
import slash from 'slash';
|
||||
import { ROUTE_PATTERN } from '../constants.js';
|
||||
import { ImageService, isSSRService, OutputFormat, TransformOptions } from '../loaders/index.js';
|
||||
import { isSSRService, parseAspectRatio } from '../loaders/index.js';
|
||||
import sharp from '../loaders/sharp.js';
|
||||
import { isRemoteImage, parseAspectRatio } from '../utils/images.js';
|
||||
import { ImageMetadata } from '../vite-plugin-astro-image.js';
|
||||
import type { ImageService, OutputFormat, TransformOptions } from '../loaders/index.js';
|
||||
import { isRemoteImage } from '../utils/paths.js';
|
||||
import type { ImageMetadata } from '../vite-plugin-astro-image.js';
|
||||
|
||||
export interface GetImageTransform extends Omit<TransformOptions, 'src'> {
|
||||
src: string | ImageMetadata | Promise<{ default: ImageMetadata }>;
|
||||
|
@ -96,7 +95,7 @@ async function resolveTransform(input: GetImageTransform): Promise<TransformOpti
|
|||
* @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(
|
||||
export async function getImage(
|
||||
transform: GetImageTransform
|
||||
): Promise<astroHTML.JSX.ImgHTMLAttributes> {
|
||||
if (!transform.src) {
|
||||
|
@ -132,25 +131,26 @@ export async function getImage(
|
|||
throw new Error('@astrojs/image: loader not found!');
|
||||
}
|
||||
|
||||
// For SSR services, build URLs for the injected route
|
||||
if (isSSRService(_loader)) {
|
||||
const { searchParams } = _loader.serializeTransform(resolved);
|
||||
const { searchParams } = isSSRService(_loader)
|
||||
? _loader.serializeTransform(resolved)
|
||||
: sharp.serializeTransform(resolved);
|
||||
|
||||
// cache all images rendered to HTML
|
||||
if (globalThis.astroImage?.addStaticImage) {
|
||||
globalThis.astroImage.addStaticImage(resolved);
|
||||
}
|
||||
let src: string;
|
||||
|
||||
const src = globalThis.astroImage?.filenameFormat
|
||||
? globalThis.astroImage.filenameFormat(resolved, searchParams)
|
||||
: `${ROUTE_PATTERN}?${searchParams.toString()}`;
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
src: slash(src), // Windows compat
|
||||
};
|
||||
if (/^[\/\\]?@astroimage/.test(resolved.src)) {
|
||||
src = `${resolved.src}?${searchParams.toString()}`;
|
||||
} else {
|
||||
searchParams.set('href', resolved.src);
|
||||
src = `/_image?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
// For hosted services, return the `<img />` attributes as-is
|
||||
return attributes;
|
||||
// cache all images rendered to HTML
|
||||
if (globalThis.astroImage?.addStaticImage) {
|
||||
src = globalThis.astroImage.addStaticImage(resolved);
|
||||
}
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
src
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/// <reference types="astro/astro-jsx" />
|
||||
import { lookup } from 'mrmime';
|
||||
import mime from 'mime';
|
||||
import { extname } from 'node:path';
|
||||
import { OutputFormat, TransformOptions } from '../loaders/index.js';
|
||||
import { parseAspectRatio } from '../utils/images.js';
|
||||
import { parseAspectRatio } from '../loaders/index.js';
|
||||
import { ImageMetadata } from '../vite-plugin-astro-image.js';
|
||||
import { getImage } from './get-image.js';
|
||||
|
||||
|
@ -71,7 +71,7 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe
|
|||
);
|
||||
|
||||
return {
|
||||
type: lookup(format) || format,
|
||||
type: mime.getType(format) || format,
|
||||
srcset: imgs.join(','),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,6 +12,28 @@ export type InputFormat =
|
|||
|
||||
export type OutputFormat = 'avif' | 'jpeg' | 'png' | 'webp';
|
||||
|
||||
export function isOutputFormat(value: string): value is OutputFormat {
|
||||
return ['avif', 'jpeg', 'png', 'webp'].includes(value);
|
||||
}
|
||||
|
||||
export function isAspectRatioString(value: string): value is `${number}:${number}` {
|
||||
return /^\d*:\d*$/.test(value);
|
||||
}
|
||||
|
||||
export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) {
|
||||
if (!aspectRatio) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// parse aspect ratio strings, if required (ex: "16:9")
|
||||
if (typeof aspectRatio === 'number') {
|
||||
return aspectRatio;
|
||||
} else {
|
||||
const [width, height] = aspectRatio.split(':');
|
||||
return parseInt(width) / parseInt(height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the original image and transforms that need to be applied to it.
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import sharp from 'sharp';
|
||||
import { isAspectRatioString, isOutputFormat } from '../utils/images.js';
|
||||
import { isAspectRatioString, isOutputFormat } from '../loaders/index.js';
|
||||
import type { OutputFormat, SSRImageService, TransformOptions } from './index.js';
|
||||
|
||||
class SharpService implements SSRImageService {
|
||||
|
@ -37,16 +37,10 @@ class SharpService implements SSRImageService {
|
|||
searchParams.append('ar', transform.aspectRatio.toString());
|
||||
}
|
||||
|
||||
searchParams.append('href', transform.src);
|
||||
|
||||
return { searchParams };
|
||||
}
|
||||
|
||||
parseTransform(searchParams: URLSearchParams) {
|
||||
if (!searchParams.has('href')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let transform: TransformOptions = { src: searchParams.get('href')! };
|
||||
|
||||
if (searchParams.has('q')) {
|
||||
|
|
51
packages/integrations/image/src/utils/etag.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* FNV-1a Hash implementation
|
||||
* @author Travis Webb (tjwebb) <me@traviswebb.com>
|
||||
*
|
||||
* Ported from https://github.com/tjwebb/fnv-plus/blob/master/index.js
|
||||
*
|
||||
* Simplified, optimized and add modified for 52 bit, which provides a larger hash space
|
||||
* and still making use of Javascript's 53-bit integer space.
|
||||
*/
|
||||
export const fnv1a52 = (str: string) => {
|
||||
const len = str.length
|
||||
let i = 0,
|
||||
t0 = 0,
|
||||
v0 = 0x2325,
|
||||
t1 = 0,
|
||||
v1 = 0x8422,
|
||||
t2 = 0,
|
||||
v2 = 0x9ce4,
|
||||
t3 = 0,
|
||||
v3 = 0xcbf2
|
||||
|
||||
while (i < len) {
|
||||
v0 ^= str.charCodeAt(i++)
|
||||
t0 = v0 * 435
|
||||
t1 = v1 * 435
|
||||
t2 = v2 * 435
|
||||
t3 = v3 * 435
|
||||
t2 += v0 << 8
|
||||
t3 += v1 << 8
|
||||
t1 += t0 >>> 16
|
||||
v0 = t0 & 65535
|
||||
t2 += t1 >>> 16
|
||||
v1 = t1 & 65535
|
||||
v3 = (t3 + (t2 >>> 16)) & 65535
|
||||
v2 = t2 & 65535
|
||||
}
|
||||
|
||||
return (
|
||||
(v3 & 15) * 281474976710656 +
|
||||
v2 * 4294967296 +
|
||||
v1 * 65536 +
|
||||
(v0 ^ (v3 >> 4))
|
||||
)
|
||||
}
|
||||
|
||||
export const etag = (payload: string, weak = false) => {
|
||||
const prefix = weak ? 'W/"' : '"'
|
||||
return (
|
||||
prefix + fnv1a52(payload).toString(36) + payload.length.toString(36) + '"'
|
||||
)
|
||||
}
|
|
@ -1,17 +1,4 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import type { OutputFormat, TransformOptions } from '../loaders/index.js';
|
||||
|
||||
export function isOutputFormat(value: string): value is OutputFormat {
|
||||
return ['avif', 'jpeg', 'png', 'webp'].includes(value);
|
||||
}
|
||||
|
||||
export function isAspectRatioString(value: string): value is `${number}:${number}` {
|
||||
return /^\d*:\d*$/.test(value);
|
||||
}
|
||||
|
||||
export function isRemoteImage(src: string) {
|
||||
return /^http(s?):\/\//.test(src);
|
||||
}
|
||||
|
||||
export async function loadLocalImage(src: string | URL) {
|
||||
try {
|
||||
|
@ -34,21 +21,3 @@ export async function loadRemoteImage(src: string) {
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadImage(src: string) {
|
||||
return isRemoteImage(src) ? await loadRemoteImage(src) : await loadLocalImage(src);
|
||||
}
|
||||
|
||||
export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) {
|
||||
if (!aspectRatio) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// parse aspect ratio strings, if required (ex: "16:9")
|
||||
if (typeof aspectRatio === 'number') {
|
||||
return aspectRatio;
|
||||
} else {
|
||||
const [width, height] = aspectRatio.split(':');
|
||||
return parseInt(width) / parseInt(height);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import sizeOf from 'image-size';
|
||||
import fs from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { InputFormat } from '../loaders/index.js';
|
||||
import { ImageMetadata } from '../vite-plugin-astro-image.js';
|
||||
|
||||
export async function metadata(src: string): Promise<ImageMetadata | undefined> {
|
||||
export async function metadata(src: URL): Promise<ImageMetadata | undefined> {
|
||||
const file = await fs.readFile(src);
|
||||
|
||||
const { width, height, type, orientation } = await sizeOf(file);
|
||||
|
@ -14,7 +15,7 @@ export async function metadata(src: string): Promise<ImageMetadata | undefined>
|
|||
}
|
||||
|
||||
return {
|
||||
src,
|
||||
src: fileURLToPath(src),
|
||||
width: isPortrait ? height : width,
|
||||
height: isPortrait ? width : height,
|
||||
format: type as InputFormat,
|
||||
|
|
|
@ -1,54 +1,74 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { OUTPUT_DIR } from '../constants.js';
|
||||
import type { TransformOptions } from '../loaders/index.js';
|
||||
import { isRemoteImage } from './images.js';
|
||||
import { shorthash } from './shorthash.js';
|
||||
import { OutputFormat, TransformOptions } from "../loaders/index.js";
|
||||
import { shorthash } from "./shorthash.js";
|
||||
|
||||
export function isRemoteImage(src: string) {
|
||||
return /^http(s?):\/\//.test(src);
|
||||
}
|
||||
|
||||
function removeQueryString(src: string) {
|
||||
const index = src.lastIndexOf('?');
|
||||
return index > 0 ? src.substring(0, index) : src;
|
||||
}
|
||||
|
||||
function removeExtname(src: string) {
|
||||
const ext = path.extname(src);
|
||||
function extname(src: string, format?: OutputFormat) {
|
||||
const index = src.lastIndexOf('.');
|
||||
|
||||
if (!ext) {
|
||||
if (index <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return src.substring(index);
|
||||
}
|
||||
|
||||
function removeExtname(src: string) {
|
||||
const index = src.lastIndexOf('.');
|
||||
|
||||
if (index <= 0) {
|
||||
return src;
|
||||
}
|
||||
|
||||
const index = src.lastIndexOf(ext);
|
||||
return src.substring(0, index);
|
||||
}
|
||||
|
||||
export function ensureDir(dir: string) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
function basename(src: string) {
|
||||
return src.replace(/^.*[\\\/]/, '');
|
||||
}
|
||||
|
||||
export function propsToFilename({ src, width, height, format }: TransformOptions) {
|
||||
export function propsToFilename(transform: TransformOptions) {
|
||||
// strip off the querystring first, then remove the file extension
|
||||
let filename = removeQueryString(src);
|
||||
const ext = path.extname(filename);
|
||||
let filename = removeQueryString(transform.src);
|
||||
filename = basename(filename);
|
||||
filename = removeExtname(filename);
|
||||
|
||||
// for remote images, add a hash of the full URL to dedupe images with the same filename
|
||||
if (isRemoteImage(src)) {
|
||||
filename += `-${shorthash(src)}`;
|
||||
}
|
||||
const ext = transform.format || extname(transform.src)?.substring(1);
|
||||
|
||||
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;
|
||||
return `/${filename}_${shorthash(JSON.stringify(transform))}.${ext}`;
|
||||
}
|
||||
|
||||
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)));
|
||||
export function appendForwardSlash(path: string) {
|
||||
return path.endsWith('/') ? path : path + '/';
|
||||
}
|
||||
|
||||
export function prependForwardSlash(path: string) {
|
||||
return path[0] === '/' ? path : '/' + path;
|
||||
}
|
||||
|
||||
export function removeTrailingForwardSlash(path: string) {
|
||||
return path.endsWith('/') ? path.slice(0, path.length - 1) : path;
|
||||
}
|
||||
|
||||
export function removeLeadingForwardSlash(path: string) {
|
||||
return path.startsWith('/') ? path.substring(1) : path;
|
||||
}
|
||||
|
||||
export function trimSlashes(path: string) {
|
||||
return path.replace(/^\/|\/$/g, '');
|
||||
}
|
||||
|
||||
function isString(path: unknown): path is string {
|
||||
return typeof path === 'string' || path instanceof String;
|
||||
}
|
||||
|
||||
export function joinPaths(...paths: (string | undefined)[]) {
|
||||
return paths.filter(isString).map(trimSlashes).join('/');
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import { basename, extname, join } from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import type { AstroConfig } from 'astro';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import MagicString from 'magic-string';
|
||||
import type { PluginContext } from 'rollup';
|
||||
import slash from 'slash';
|
||||
import type { Plugin, ResolvedConfig } from 'vite';
|
||||
import type { IntegrationOptions } from './index.js';
|
||||
import type { InputFormat } from './loaders/index.js';
|
||||
import sharp from './loaders/sharp.js';
|
||||
import { metadata } from './utils/metadata.js';
|
||||
|
||||
export interface ImageMetadata {
|
||||
|
@ -21,19 +27,6 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO
|
|||
const virtualModuleId = 'virtual:image-loader';
|
||||
|
||||
let resolvedConfig: ResolvedConfig;
|
||||
let loaderModuleId: string;
|
||||
|
||||
async function resolveLoader(context: PluginContext) {
|
||||
if (!loaderModuleId) {
|
||||
const module = await context.resolve(options.serviceEntryPoint);
|
||||
if (!module) {
|
||||
throw new Error(`"${options.serviceEntryPoint}" could not be found`);
|
||||
}
|
||||
loaderModuleId = module.id;
|
||||
}
|
||||
|
||||
return loaderModuleId;
|
||||
}
|
||||
|
||||
return {
|
||||
name: '@astrojs/image',
|
||||
|
@ -46,7 +39,7 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO
|
|||
// This ensures the module is available in `astro dev` and is included
|
||||
// in the SSR server bundle.
|
||||
if (id === virtualModuleId) {
|
||||
return await resolveLoader(this);
|
||||
return await this.resolve(options.serviceEntryPoint);
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
|
@ -55,19 +48,90 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO
|
|||
return null;
|
||||
}
|
||||
|
||||
const meta = await metadata(id);
|
||||
const url = pathToFileURL(id);
|
||||
|
||||
const fileUrl = pathToFileURL(id);
|
||||
const src = resolvedConfig.isProduction
|
||||
? fileUrl.pathname.replace(config.srcDir.pathname, '/')
|
||||
: id;
|
||||
const meta = await metadata(url);
|
||||
|
||||
const output = {
|
||||
...meta,
|
||||
src: slash(src), // Windows compat
|
||||
};
|
||||
if (!meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(output)}`;
|
||||
if (!this.meta.watchMode) {
|
||||
const filename = basename(url.pathname, extname(url.pathname)) + `.${meta.format}`;
|
||||
|
||||
const handle = this.emitFile({
|
||||
name: filename,
|
||||
source: await fs.readFile(url),
|
||||
type: 'asset',
|
||||
});
|
||||
|
||||
meta.src = `__ASTRO_IMAGE_ASSET__${handle}__`;
|
||||
} else {
|
||||
const relId = path.relative(fileURLToPath(config.srcDir), id);
|
||||
|
||||
meta.src = join('/@astroimage', relId);
|
||||
|
||||
// Windows compat
|
||||
meta.src = slash(meta.src);
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(meta)}`;
|
||||
},
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
if (req.url?.startsWith('/@astroimage/')) {
|
||||
const [, id] = req.url.split('/@astroimage/');
|
||||
|
||||
const url = new URL(id, config.srcDir);
|
||||
const file = await fs.readFile(url);
|
||||
|
||||
const meta = await metadata(url);
|
||||
|
||||
if (!meta) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const transform = await sharp.parseTransform(url.searchParams);
|
||||
|
||||
if (!transform) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const result = await sharp.transform(file, transform);
|
||||
|
||||
res.setHeader('Content-Type', `image/${result.format}`);
|
||||
res.setHeader('Cache-Control', 'max-age=360000');
|
||||
|
||||
const stream = Readable.from(result.data);
|
||||
return stream.pipe(res);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
},
|
||||
async renderChunk(code) {
|
||||
const assetUrlRE = /__ASTRO_IMAGE_ASSET__([a-z\d]{8})__(?:_(.*?)__)?/g;
|
||||
|
||||
let match;
|
||||
let s;
|
||||
while ((match = assetUrlRE.exec(code))) {
|
||||
s = s || (s = new MagicString(code));
|
||||
const [full, hash, postfix = ''] = match;
|
||||
|
||||
const file = this.getFileName(hash);
|
||||
const outputFilepath = resolvedConfig.base + file + postfix;
|
||||
|
||||
s.overwrite(match.index, match.index + full.length, outputFilepath);
|
||||
}
|
||||
|
||||
if (s) {
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: resolvedConfig.build.sourcemap ? s.generateMap({ hires: true }) : null,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
BIN
packages/integrations/image/test/fixtures/basic-image/public/hero.jpg
vendored
Normal file
After Width: | Height: | Size: 270 KiB |
|
@ -8,6 +8,8 @@ import { Image } from '@astrojs/image/components';
|
|||
<!-- Head Stuff -->
|
||||
</head>
|
||||
<body>
|
||||
<Image id="hero" src="/hero.jpg" width={768} height={414} format="webp" />
|
||||
<br />
|
||||
<Image id="social-jpg" src={socialJpg} width={506} height={253} />
|
||||
<br />
|
||||
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" />
|
||||
|
|
BIN
packages/integrations/image/test/fixtures/basic-picture/public/hero.jpg
vendored
Normal file
After Width: | Height: | Size: 270 KiB |
|
@ -8,6 +8,8 @@ import { Picture } from '@astrojs/image/components';
|
|||
<!-- Head Stuff -->
|
||||
</head>
|
||||
<body>
|
||||
<Picture id="hero" src="/hero.jpg" sizes="100vw" widths={[384, 768]} aspectRatio={768/414} alt="Hero image" />
|
||||
<br />
|
||||
<Picture id="social-jpg" src={socialJpg} sizes="(min-width: 640px) 50vw, 100vw" widths={[253, 506]} alt="Social image" />
|
||||
<br />
|
||||
<Picture id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" sizes="(min-width: 640px) 50vw, 100vw" widths={[272, 544]} aspectRatio={544/184} alt="Google logo" />
|
||||
|
|
9
packages/integrations/image/test/fixtures/with-mdx/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import image from '@astrojs/image';
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'http://localhost:3000',
|
||||
integrations: [image({ logLevel: 'silent' }), mdx()]
|
||||
});
|
11
packages/integrations/image/test/fixtures/with-mdx/package.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@test/with-mdx",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/image": "workspace:*",
|
||||
"@astrojs/mdx": "workspace:*",
|
||||
"@astrojs/node": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
BIN
packages/integrations/image/test/fixtures/with-mdx/public/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
packages/integrations/image/test/fixtures/with-mdx/public/hero.jpg
vendored
Normal file
After Width: | Height: | Size: 270 KiB |
44
packages/integrations/image/test/fixtures/with-mdx/server/server.mjs
vendored
Normal file
|
@ -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 = () => {};
|
BIN
packages/integrations/image/test/fixtures/with-mdx/src/assets/blog/introducing-astro.jpg
vendored
Normal file
After Width: | Height: | Size: 270 KiB |
BIN
packages/integrations/image/test/fixtures/with-mdx/src/assets/social.jpg
vendored
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
packages/integrations/image/test/fixtures/with-mdx/src/assets/social.png
vendored
Normal file
After Width: | Height: | Size: 1.4 MiB |
8
packages/integrations/image/test/fixtures/with-mdx/src/layouts/Base.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<!-- Head Stuff -->
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
17
packages/integrations/image/test/fixtures/with-mdx/src/pages/index.mdx
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
layout: '../layouts/Base.astro'
|
||||
---
|
||||
|
||||
import socialJpg from '../assets/social.jpg';
|
||||
import { Image } from '@astrojs/image/components';
|
||||
|
||||
|
||||
<Image id="hero" src="/hero.jpg" width={768} height={414} format="webp" />
|
||||
<br />
|
||||
<Image id="social-jpg" src={socialJpg} width={506} height={253} />
|
||||
<br />
|
||||
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" />
|
||||
<br />
|
||||
<Image id="inline" src={import('../assets/social.jpg')} width={506} />
|
||||
<br />
|
||||
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" />
|
|
@ -4,11 +4,191 @@ import sizeOf from 'image-size';
|
|||
import { fileURLToPath } from 'url';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
let fixture;
|
||||
|
||||
describe('SSG images', function () {
|
||||
describe('SSG images - dev', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/basic-image/' });
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Local images with inline imports', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal('https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSG images with subpath - dev', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/basic-image/', base: '/docs' });
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/docs/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Local images with inline imports', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal('https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSG images - build', function () {
|
||||
let fixture;
|
||||
let $;
|
||||
let html;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/basic-image/' });
|
||||
await fixture.build();
|
||||
|
||||
html = await fixture.readFile('/index.html');
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
function verifyImage(pathname, expected) {
|
||||
|
@ -18,189 +198,174 @@ describe('SSG images', function () {
|
|||
expect(result).to.deep.equal(expected);
|
||||
}
|
||||
|
||||
describe('build', () => {
|
||||
let $;
|
||||
let html;
|
||||
describe('Local images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
before(async () => {
|
||||
await fixture.build();
|
||||
|
||||
html = await fixture.readFile('/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect(image.attr('src')).to.equal('/social.cece8c77_1zwatP.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_1zwatP.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
expect(image.attr('src')).to.equal('/_image/assets/social_506x253.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
describe('Inline imports', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
expect(image.attr('src')).to.equal('/social.cece8c77_Z2tF99.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_Z2tF99.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
// Hard-coding in the test! These should never change since the hash is based
|
||||
// on the static `src` string
|
||||
const HASH = 'Z1RBHqs';
|
||||
const HASH_WITH_QUERY = 'Z17oujH';
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
expect(image.attr('src')).to.equal(
|
||||
`/googlelogo_color_272x92dp_${HASH}.webp`
|
||||
);
|
||||
expect(image.attr('width')).to.equal('544');
|
||||
expect(image.attr('height')).to.equal('184');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage(`/googlelogo_color_272x92dp_${HASH}.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
expect(image.attr('src')).to.equal('/_image/assets/social_506x253.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
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('Remote images', () => {
|
||||
// Hard-coding in the test! These should never change since the hash is based
|
||||
// on the static `src` string
|
||||
const HASH = 'Z1iI4xW';
|
||||
const HASH_WITH_QUERY = '18Aq0m';
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
expect(image.attr('src')).to.equal(
|
||||
`/_image/googlelogo_color_272x92dp-${HASH}_544x184.webp`
|
||||
);
|
||||
expect(image.attr('width')).to.equal('544');
|
||||
expect(image.attr('height')).to.equal('184');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage(`_image/googlelogo_color_272x92dp-${HASH}_544x184.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes query strings', () => {
|
||||
verifyImage(`_image/googlelogo_color_272x92dp-${HASH_WITH_QUERY}_544x184.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
it('removes query strings', () => {
|
||||
verifyImage(`/googlelogo_color_272x92dp_${HASH_WITH_QUERY}.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
let $;
|
||||
describe('/public images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero');
|
||||
|
||||
before(async () => {
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
expect(image.attr('src')).to.equal('/hero_Z2k1JGN.webp');
|
||||
expect(image.attr('width')).to.equal('768');
|
||||
expect(image.attr('height')).to.equal('414');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Local images with inline imports', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#inline');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps remote image query params', () => {
|
||||
const image = $('#query');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc'
|
||||
);
|
||||
});
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('hero_Z2k1JGN.webp', { width: 768, height: 414, type: 'webp' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSG images with subpath - build', function () {
|
||||
let fixture;
|
||||
let $;
|
||||
let html;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/basic-image/', base: '/docs' });
|
||||
await fixture.build();
|
||||
|
||||
html = await fixture.readFile('/index.html');
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
function verifyImage(pathname, expected) {
|
||||
const url = new URL('./fixtures/basic-image/dist/' + pathname, import.meta.url);
|
||||
const dist = fileURLToPath(url);
|
||||
const result = sizeOf(dist);
|
||||
expect(result).to.deep.equal(expected);
|
||||
}
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
expect(image.attr('src')).to.equal('/docs/social.cece8c77_iK4oy.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_iK4oy.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
expect(image.attr('src')).to.equal('/docs/social.cece8c77_1YIUw1.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_1YIUw1.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
// Hard-coding in the test! These should never change since the hash is based
|
||||
// on the static `src` string
|
||||
const HASH = 'Z1RBHqs';
|
||||
const HASH_WITH_QUERY = 'Z17oujH';
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
expect(image.attr('src')).to.equal(
|
||||
`/docs/googlelogo_color_272x92dp_${HASH}.webp`
|
||||
);
|
||||
expect(image.attr('width')).to.equal('544');
|
||||
expect(image.attr('height')).to.equal('184');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage(`/googlelogo_color_272x92dp_${HASH}.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes query strings', () => {
|
||||
verifyImage(`/googlelogo_color_272x92dp_${HASH_WITH_QUERY}.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero');
|
||||
|
||||
expect(image.attr('src')).to.equal('/docs/hero_Z2k1JGN.webp');
|
||||
expect(image.attr('width')).to.equal('768');
|
||||
expect(image.attr('height')).to.equal('414');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('hero_Z2k1JGN.webp', { width: 768, height: 414, type: 'webp' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,25 +1,16 @@
|
|||
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({ streaming: false }),
|
||||
output: 'server',
|
||||
output: 'server'
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
@ -46,32 +37,7 @@ describe('SSR images - build', function () {
|
|||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
});
|
||||
|
||||
it('built the optimized image', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const imgRequest = new Request(`http://example.com${image.attr('src')}`);
|
||||
const imgResponse = await app.render(imgRequest);
|
||||
|
||||
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
|
||||
);
|
||||
expect(searchParams.get('href')).to.equal('/assets/social.cece8c77.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -97,7 +63,7 @@ describe('SSR images - build', function () {
|
|||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
expect(searchParams.get('href')).to.equal('/assets/social.cece8c77.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -122,7 +88,9 @@ describe('SSR images - build', function () {
|
|||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href').endsWith('googlelogo_color_272x92dp.png')).to.equal(true);
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps remote image query params', async () => {
|
||||
|
@ -145,36 +113,61 @@ describe('SSR images - build', function () {
|
|||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href').endsWith('googlelogo_color_272x92dp.png?token=abc')).to.equal(
|
||||
true
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes src, width, and height attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#hero');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSR images - dev', function () {
|
||||
describe('SSR images with subpath - build', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/basic-image/',
|
||||
adapter: testAdapter(),
|
||||
adapter: testAdapter({ streaming: false }),
|
||||
output: 'server',
|
||||
base: '/docs'
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
it('includes src, width, and height attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const src = image.attr('src');
|
||||
|
@ -188,23 +181,19 @@ describe('SSR images - dev', function () {
|
|||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
expect(searchParams.get('href')).to.equal('/docs/assets/social.cece8c77.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
it('includes src, width, and height attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#inline');
|
||||
|
||||
const src = image.attr('src');
|
||||
|
@ -218,12 +207,19 @@ describe('SSR images - dev', function () {
|
|||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
expect(searchParams.get('href')).to.equal('/docs/assets/social.cece8c77.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
it('includes src, width, and height attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#google');
|
||||
|
||||
const src = image.attr('src');
|
||||
|
@ -236,12 +232,17 @@ describe('SSR images - dev', function () {
|
|||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
expect(searchParams.get('href')).to.equal('https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png');
|
||||
});
|
||||
|
||||
it('keeps remote image query params', () => {
|
||||
it('keeps remote image query params', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#query');
|
||||
|
||||
const src = image.attr('src');
|
||||
|
@ -259,4 +260,30 @@ describe('SSR images - dev', function () {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes src, width, and height attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#hero');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
});
|
||||
});
|
||||
});
|
273
packages/integrations/image/test/image-ssr-dev.test.js
Normal file
|
@ -0,0 +1,273 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from '../../../astro/test/test-adapter.js';
|
||||
|
||||
describe('SSR images - dev', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/basic-image/',
|
||||
adapter: testAdapter(),
|
||||
output: 'server',
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps remote image query params', () => {
|
||||
const image = $('#query');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#hero');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#hero');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/webp');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSR images with subpath - dev', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/basic-image/',
|
||||
adapter: testAdapter(),
|
||||
output: 'server',
|
||||
base: '/docs'
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/docs/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps remote image query params', () => {
|
||||
const image = $('#query');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#hero');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('webp');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#hero');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/webp');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,11 +5,261 @@ import sizeOf from 'image-size';
|
|||
import { fileURLToPath } from 'url';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
let fixture;
|
||||
describe('SSG pictures - dev', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
describe('SSG pictures', function () {
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/basic-picture/' });
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#social-jpg source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Local images with inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#inline source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#google source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('png');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal('https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png');
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#hero source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
expect(image.attr('alt')).to.equal('Hero image');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSG pictures with subpath - dev', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/basic-picture/', base: '/docs' });
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/docs/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#social-jpg source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Local images with inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#inline source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#google source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('png');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal('https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png');
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#hero source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
expect(image.attr('alt')).to.equal('Hero image');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSG pictures - build', function () {
|
||||
let fixture;
|
||||
let $;
|
||||
let html;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/basic-picture/' });
|
||||
await fixture.build();
|
||||
|
||||
html = await fixture.readFile('/index.html');
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
function verifyImage(pathname, expected) {
|
||||
|
@ -25,253 +275,310 @@ describe('SSG pictures', function () {
|
|||
}
|
||||
}
|
||||
|
||||
describe('build', () => {
|
||||
let $;
|
||||
let html;
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#social-jpg source');
|
||||
|
||||
before(async () => {
|
||||
await fixture.build();
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
html = await fixture.readFile('/index.html');
|
||||
$ = cheerio.load(html);
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#social-jpg source');
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
expect(image.attr('src')).to.equal('/_image/assets/social_506x253.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('_image/assets/social_253x127.avif', { width: 253, height: 127, type: 'avif' });
|
||||
verifyImage('_image/assets/social_253x127.webp', { width: 253, height: 127, type: 'webp' });
|
||||
verifyImage('_image/assets/social_253x127.jpg', { width: 253, height: 127, type: 'jpg' });
|
||||
verifyImage('_image/assets/social_506x253.avif', { width: 506, height: 253, type: 'avif' });
|
||||
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' });
|
||||
});
|
||||
expect(image.attr('src')).to.equal('/social.cece8c77_isw36.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#inline source');
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_Z1qCkLW.avif', { width: 253, height: 127, type: 'avif' });
|
||||
verifyImage('social.cece8c77_ZHhvOb.webp', { width: 253, height: 127, type: 'webp' });
|
||||
verifyImage('social.cece8c77_ZwfMjf.jpg', { width: 253, height: 127, type: 'jpg' });
|
||||
verifyImage('social.cece8c77_6t5Xo.avif', { width: 506, height: 253, type: 'avif' });
|
||||
verifyImage('social.cece8c77_ONTVa.webp', { width: 506, height: 253, type: 'webp' });
|
||||
verifyImage('social.cece8c77_isw36.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
describe('Inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#inline source');
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
expect(image.attr('src')).to.equal('/_image/assets/social_506x253.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('_image/assets/social_253x127.avif', { width: 253, height: 127, type: 'avif' });
|
||||
verifyImage('_image/assets/social_253x127.webp', { width: 253, height: 127, type: 'webp' });
|
||||
verifyImage('_image/assets/social_253x127.jpg', { width: 253, height: 127, type: 'jpg' });
|
||||
verifyImage('_image/assets/social_506x253.avif', { width: 506, height: 253, type: 'avif' });
|
||||
verifyImage('_image/assets/social_506x253.webp', { width: 506, height: 253, type: 'webp' });
|
||||
verifyImage('_image/assets/social_506x253.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
// Hard-coding in the test! This should never change since the hash is based
|
||||
// on the static `src` string
|
||||
const HASH = 'Z1iI4xW';
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
it('includes sources', () => {
|
||||
const sources = $('#google source');
|
||||
expect(image.attr('src')).to.equal('/social.cece8c77_isw36.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_Z1qCkLW.avif', { width: 253, height: 127, type: 'avif' });
|
||||
verifyImage('social.cece8c77_ZHhvOb.webp', { width: 253, height: 127, type: 'webp' });
|
||||
verifyImage('social.cece8c77_ZwfMjf.jpg', { width: 253, height: 127, type: 'jpg' });
|
||||
verifyImage('social.cece8c77_6t5Xo.avif', { width: 506, height: 253, type: 'avif' });
|
||||
verifyImage('social.cece8c77_ONTVa.webp', { width: 506, height: 253, type: 'webp' });
|
||||
verifyImage('social.cece8c77_isw36.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
describe('Remote images', () => {
|
||||
// Hard-coding in the test! This should never change since the hash is based
|
||||
// on the static `src` string
|
||||
const HASH = 'ZWW1pg';
|
||||
|
||||
it('includes sources', () => {
|
||||
const sources = $('#google source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google img');
|
||||
|
||||
expect(image.attr('src')).to.equal(`/googlelogo_color_272x92dp_${HASH}.png`);
|
||||
expect(image.attr('width')).to.equal('544');
|
||||
expect(image.attr('height')).to.equal('184');
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage(`googlelogo_color_272x92dp_1YsbPJ.avif`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'avif',
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google img');
|
||||
|
||||
expect(image.attr('src')).to.equal(`/_image/googlelogo_color_272x92dp-${HASH}_544x184.png`);
|
||||
expect(image.attr('width')).to.equal('544');
|
||||
expect(image.attr('height')).to.equal('184');
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
verifyImage(`googlelogo_color_272x92dp_1OJIxd.webp`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'webp',
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage(`_image/googlelogo_color_272x92dp-${HASH}_272x92.avif`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'avif',
|
||||
});
|
||||
verifyImage(`_image/googlelogo_color_272x92dp-${HASH}_272x92.webp`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'webp',
|
||||
});
|
||||
verifyImage(`_image/googlelogo_color_272x92dp-${HASH}_272x92.png`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'png',
|
||||
});
|
||||
verifyImage(`_image/googlelogo_color_272x92dp-${HASH}_544x184.avif`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'avif',
|
||||
});
|
||||
verifyImage(`_image/googlelogo_color_272x92dp-${HASH}_544x184.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
verifyImage(`_image/googlelogo_color_272x92dp-${HASH}_544x184.png`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'png',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_ZaELrV.png`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'png',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_I7OBe.avif`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'avif',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_ReA0T.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_ZWW1pg.png`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'png',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
let $;
|
||||
describe('/public images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#hero source');
|
||||
|
||||
before(async () => {
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero img');
|
||||
|
||||
expect(image.attr('src')).to.equal('/hero_1ql1f0.jpg');
|
||||
expect(image.attr('width')).to.equal('768');
|
||||
expect(image.attr('height')).to.equal('414');
|
||||
expect(image.attr('alt')).to.equal('Hero image');
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#social-jpg source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Local images with inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#inline source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#google source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('png');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('hero_ZOXU0F.avif', { width: 384, height: 207, type: 'avif' });
|
||||
verifyImage('hero_ZFR9B0.webp', { width: 384, height: 207, type: 'webp' });
|
||||
verifyImage('hero_Z1rYjFx.jpg', { width: 384, height: 207, type: 'jpg' });
|
||||
verifyImage('hero_Z1kkIMd.avif', { width: 768, height: 414, type: 'avif' });
|
||||
verifyImage('hero_Z1bdXnx.webp', { width: 768, height: 414, type: 'webp' });
|
||||
verifyImage('hero_Z1Wl8s5.jpg', { width: 768, height: 414, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSG pictures with subpath - build', function () {
|
||||
let fixture;
|
||||
let $;
|
||||
let html;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/basic-picture/', base: '/docs' });
|
||||
await fixture.build();
|
||||
|
||||
html = await fixture.readFile('/index.html');
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
function verifyImage(pathname, expected) {
|
||||
const url = new URL('./fixtures/basic-picture/dist/' + pathname, import.meta.url);
|
||||
const dist = fileURLToPath(url);
|
||||
|
||||
// image-size doesn't support AVIF files
|
||||
if (expected.type !== 'avif') {
|
||||
const result = sizeOf(dist);
|
||||
expect(result).to.deep.equal(expected);
|
||||
} else {
|
||||
expect(fs.statSync(dist)).not.to.be.undefined;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#social-jpg source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
expect(image.attr('src')).to.equal('/docs/social.cece8c77_VWX3S.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_2wbTqo.avif', { width: 253, height: 127, type: 'avif' });
|
||||
verifyImage('social.cece8c77_Z1OEppL.webp', { width: 253, height: 127, type: 'webp' });
|
||||
verifyImage('social.cece8c77_Z1xuFVD.jpg', { width: 253, height: 127, type: 'jpg' });
|
||||
verifyImage('social.cece8c77_Z10SMCc.avif', { width: 506, height: 253, type: 'avif' });
|
||||
verifyImage('social.cece8c77_ZhxXEq.webp', { width: 506, height: 253, type: 'webp' });
|
||||
verifyImage('social.cece8c77_Z1ks7l5.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#inline source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
expect(image.attr('src')).to.equal('/docs/social.cece8c77_VWX3S.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_2wbTqo.avif', { width: 253, height: 127, type: 'avif' });
|
||||
verifyImage('social.cece8c77_Z1OEppL.webp', { width: 253, height: 127, type: 'webp' });
|
||||
verifyImage('social.cece8c77_Z1xuFVD.jpg', { width: 253, height: 127, type: 'jpg' });
|
||||
verifyImage('social.cece8c77_Z10SMCc.avif', { width: 506, height: 253, type: 'avif' });
|
||||
verifyImage('social.cece8c77_ZhxXEq.webp', { width: 506, height: 253, type: 'webp' });
|
||||
verifyImage('social.cece8c77_Z1ks7l5.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
// Hard-coding in the test! This should never change since the hash is based
|
||||
// on the static `src` string
|
||||
const HASH = 'ZWW1pg';
|
||||
|
||||
it('includes sources', () => {
|
||||
const sources = $('#google source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google img');
|
||||
|
||||
expect(image.attr('src')).to.equal(`/docs/googlelogo_color_272x92dp_${HASH}.png`);
|
||||
expect(image.attr('width')).to.equal('544');
|
||||
expect(image.attr('height')).to.equal('184');
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage(`googlelogo_color_272x92dp_1YsbPJ.avif`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'avif',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_1OJIxd.webp`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'webp',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_ZaELrV.png`, {
|
||||
width: 272,
|
||||
height: 92,
|
||||
type: 'png',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_I7OBe.avif`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'avif',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_ReA0T.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
verifyImage(`googlelogo_color_272x92dp_ZWW1pg.png`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'png',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#hero source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero img');
|
||||
|
||||
expect(image.attr('src')).to.equal('/docs/hero_1ql1f0.jpg');
|
||||
expect(image.attr('width')).to.equal('768');
|
||||
expect(image.attr('height')).to.equal('414');
|
||||
expect(image.attr('alt')).to.equal('Hero image');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('hero_ZOXU0F.avif', { width: 384, height: 207, type: 'avif' });
|
||||
verifyImage('hero_ZFR9B0.webp', { width: 384, height: 207, type: 'webp' });
|
||||
verifyImage('hero_Z1rYjFx.jpg', { width: 384, height: 207, type: 'jpg' });
|
||||
verifyImage('hero_Z1kkIMd.avif', { width: 768, height: 414, type: 'avif' });
|
||||
verifyImage('hero_Z1bdXnx.webp', { width: 768, height: 414, type: 'webp' });
|
||||
verifyImage('hero_Z1Wl8s5.jpg', { width: 768, height: 414, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
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/',
|
||||
|
@ -60,35 +51,9 @@ describe('SSR pictures - build', function () {
|
|||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
expect(searchParams.get('href')).to.equal('/assets/social.cece8c77.jpg');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
|
||||
it('built the optimized image', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const imgRequest = new Request(`http://example.com${image.attr('src')}`);
|
||||
const imgResponse = await app.render(imgRequest);
|
||||
|
||||
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', () => {
|
||||
|
@ -128,7 +93,7 @@ describe('SSR pictures - build', function () {
|
|||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
expect(searchParams.get('href')).to.equal('/assets/social.cece8c77.jpg');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
});
|
||||
|
@ -170,35 +135,77 @@ describe('SSR pictures - build', function () {
|
|||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('googlelogo_color_272x92dp.png')).to.equal(true);
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes sources', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const sources = $('#hero source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#hero img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
expect(image.attr('alt')).to.equal('Hero image');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSR images - dev', function () {
|
||||
describe('SSR pictures with subpath - build', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/basic-picture/',
|
||||
adapter: testAdapter(),
|
||||
output: 'server',
|
||||
base: '/docs'
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
it('includes sources', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const sources = $('#social-jpg source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
@ -206,7 +213,14 @@ describe('SSR images - dev', function () {
|
|||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
it('includes <img> attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const src = image.attr('src');
|
||||
|
@ -219,25 +233,20 @@ describe('SSR images - dev', function () {
|
|||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
expect(searchParams.get('href')).to.equal('/docs/assets/social.cece8c77.jpg');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
it('includes sources', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const sources = $('#inline source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
@ -245,7 +254,14 @@ describe('SSR images - dev', function () {
|
|||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
it('includes <img> attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#inline img');
|
||||
|
||||
const src = image.attr('src');
|
||||
|
@ -259,13 +275,20 @@ describe('SSR images - dev', function () {
|
|||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href').endsWith('/assets/social.jpg')).to.equal(true);
|
||||
expect(searchParams.get('href')).to.equal('/docs/assets/social.cece8c77.jpg');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes sources', () => {
|
||||
it('includes sources', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const sources = $('#google source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
@ -273,7 +296,14 @@ describe('SSR images - dev', function () {
|
|||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
it('includes <img> attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#google img');
|
||||
|
||||
const src = image.attr('src');
|
||||
|
@ -286,10 +316,52 @@ describe('SSR images - dev', function () {
|
|||
expect(searchParams.get('f')).to.equal('png');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
// TODO: possible to avoid encoding the full image path?
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes sources', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const sources = $('#hero source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes <img> attributes', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
|
||||
const request = new Request('http://example.com/');
|
||||
const response = await app.render(request);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const image = $('#hero img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
expect(image.attr('alt')).to.equal('Hero image');
|
||||
});
|
||||
});
|
||||
});
|
309
packages/integrations/image/test/picture-ssr-dev.test.js
Normal file
|
@ -0,0 +1,309 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from '../../../astro/test/test-adapter.js';
|
||||
|
||||
describe('SSR pictures - dev', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/basic-picture/',
|
||||
adapter: testAdapter(),
|
||||
output: 'server',
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#social-jpg source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#inline source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#google source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#google img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('png');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#hero source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#hero img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
expect(image.attr('alt')).to.equal('Hero image');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#hero img');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSR pictures with subpath - dev', function () {
|
||||
let fixture;
|
||||
let devServer;
|
||||
let $;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/basic-picture/',
|
||||
adapter: testAdapter(),
|
||||
output: 'server',
|
||||
base: '/docs'
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer();
|
||||
const html = await fixture.fetch('/docs/').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#social-jpg source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Social image');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#social-jpg img');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#inline source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#inline img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/@astroimage/assets/social.jpg');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('506');
|
||||
expect(searchParams.get('h')).to.equal('253');
|
||||
expect(image.attr('alt')).to.equal('Inline social image');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#google source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#google img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('png');
|
||||
expect(searchParams.get('w')).to.equal('544');
|
||||
expect(searchParams.get('h')).to.equal('184');
|
||||
expect(searchParams.get('href')).to.equal(
|
||||
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
|
||||
);
|
||||
expect(image.attr('alt')).to.equal('Google logo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes sources', () => {
|
||||
const sources = $('#hero source');
|
||||
|
||||
expect(sources.length).to.equal(3);
|
||||
|
||||
// TODO: better coverage to verify source props
|
||||
});
|
||||
|
||||
it('includes src, width, and height attributes', () => {
|
||||
const image = $('#hero img');
|
||||
|
||||
const src = image.attr('src');
|
||||
const [route, params] = src.split('?');
|
||||
|
||||
expect(route).to.equal('/_image');
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
expect(searchParams.get('f')).to.equal('jpg');
|
||||
expect(searchParams.get('w')).to.equal('768');
|
||||
expect(searchParams.get('h')).to.equal('414');
|
||||
expect(searchParams.get('href')).to.equal('/hero.jpg');
|
||||
expect(image.attr('alt')).to.equal('Hero image');
|
||||
});
|
||||
|
||||
it('returns the optimized image', async () => {
|
||||
const image = $('#hero img');
|
||||
|
||||
const res = await fixture.fetch(image.attr('src'));
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.headers.get('Content-Type')).to.equal('image/jpeg');
|
||||
|
||||
// TODO: verify image file? It looks like sizeOf doesn't support ArrayBuffers
|
||||
});
|
||||
});
|
||||
});
|
|
@ -30,11 +30,23 @@ describe('Image rotation', function () {
|
|||
});
|
||||
|
||||
describe('Landscape images', () => {
|
||||
const hashes = [
|
||||
'/Landscape_0.080ebd7a_ZdTMkT.jpg',
|
||||
'/Landscape_1.c92e81c9_4Eikw.jpg',
|
||||
'/Landscape_2.f54c85e5_1iKxtI.jpg',
|
||||
'/Landscape_3.8e20af03_Z2sFwFL.jpg',
|
||||
'/Landscape_4.15f511b0_1dNJQt.jpg',
|
||||
'/Landscape_5.6d88c17f_ZtLntP.jpg',
|
||||
'/Landscape_6.1a88f6d8_Z1Pl4xy.jpg',
|
||||
'/Landscape_7.cb1008c2_Z1JYr40.jpg',
|
||||
'/Landscape_8.3d2837d2_1xTOBN.jpg'
|
||||
];
|
||||
|
||||
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('src')).to.equal(hashes[i]);
|
||||
expect(image.attr('width')).to.equal('1800');
|
||||
expect(image.attr('height')).to.equal('1200');
|
||||
}
|
||||
|
@ -42,7 +54,7 @@ describe('Image rotation', function () {
|
|||
|
||||
it('built the optimized image', () => {
|
||||
for (let i = 0; i < 9; i++) {
|
||||
verifyImage(`/_image/assets/Landscape_${i}_1800x1200.jpg`, {
|
||||
verifyImage(hashes[i], {
|
||||
width: 1800,
|
||||
height: 1200,
|
||||
type: 'jpg',
|
||||
|
@ -52,11 +64,23 @@ describe('Image rotation', function () {
|
|||
});
|
||||
|
||||
describe('Portait images', () => {
|
||||
const hashes = [
|
||||
'/Portrait_0.e09ae908_5e5uz.jpg',
|
||||
'/Portrait_1.c7b4942e_1RJQep.jpg',
|
||||
'/Portrait_2.8e8be39f_T6sr4.jpg',
|
||||
'/Portrait_3.1dcc58b4_Z1uaoxA.jpg',
|
||||
'/Portrait_4.2f89d418_ZLQlNB.jpg',
|
||||
'/Portrait_5.b3b6cc6f_Z23Ek26.jpg',
|
||||
'/Portrait_6.94e06390_ak2Ek.jpg',
|
||||
'/Portrait_7.9ffdecfe_Z1S4klG.jpg',
|
||||
'/Portrait_8.9d01343d_2dak03.jpg'
|
||||
];
|
||||
|
||||
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('src')).to.equal(hashes[i]);
|
||||
expect(image.attr('width')).to.equal('1200');
|
||||
expect(image.attr('height')).to.equal('1800');
|
||||
}
|
||||
|
@ -64,7 +88,7 @@ describe('Image rotation', function () {
|
|||
|
||||
it('built the optimized image', () => {
|
||||
for (let i = 0; i < 9; i++) {
|
||||
verifyImage(`/_image/assets/Portrait_${i}_1200x1800.jpg`, {
|
||||
verifyImage(hashes[i], {
|
||||
width: 1200,
|
||||
height: 1800,
|
||||
type: 'jpg',
|
||||
|
|
|
@ -26,7 +26,6 @@ describe('Sharp service', () => {
|
|||
}
|
||||
}
|
||||
|
||||
verifyProp(props.src, 'href');
|
||||
verifyProp(props.quality, 'q');
|
||||
verifyProp(props.format, 'f');
|
||||
verifyProp(props.width, 'w');
|
||||
|
|
101
packages/integrations/image/test/with-mdx.test.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import sizeOf from 'image-size';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Images in MDX - build', function () {
|
||||
let fixture;
|
||||
let $;
|
||||
let html;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/with-mdx/' });
|
||||
await fixture.build();
|
||||
|
||||
html = await fixture.readFile('/index.html');
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
function verifyImage(pathname, expected) {
|
||||
const url = new URL('./fixtures/with-mdx/dist/' + pathname, import.meta.url);
|
||||
const dist = fileURLToPath(url);
|
||||
const result = sizeOf(dist);
|
||||
expect(result).to.deep.equal(expected);
|
||||
}
|
||||
|
||||
describe('Local images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#social-jpg');
|
||||
|
||||
expect(image.attr('src')).to.equal('/social.cece8c77_1zwatP.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_1zwatP.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inline imports', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#inline');
|
||||
|
||||
expect(image.attr('src')).to.equal('/social.cece8c77_Z2tF99.jpg');
|
||||
expect(image.attr('width')).to.equal('506');
|
||||
expect(image.attr('height')).to.equal('253');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('social.cece8c77_Z2tF99.jpg', { width: 506, height: 253, type: 'jpg' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remote images', () => {
|
||||
// Hard-coding in the test! These should never change since the hash is based
|
||||
// on the static `src` string
|
||||
const HASH = 'Z1RBHqs';
|
||||
const HASH_WITH_QUERY = 'Z17oujH';
|
||||
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#google');
|
||||
|
||||
expect(image.attr('src')).to.equal(
|
||||
`/googlelogo_color_272x92dp_${HASH}.webp`
|
||||
);
|
||||
expect(image.attr('width')).to.equal('544');
|
||||
expect(image.attr('height')).to.equal('184');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage(`/googlelogo_color_272x92dp_${HASH}.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes query strings', () => {
|
||||
verifyImage(`/googlelogo_color_272x92dp_${HASH_WITH_QUERY}.webp`, {
|
||||
width: 544,
|
||||
height: 184,
|
||||
type: 'webp',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/public images', () => {
|
||||
it('includes <img> attributes', () => {
|
||||
const image = $('#hero');
|
||||
|
||||
expect(image.attr('src')).to.equal('/hero_Z2k1JGN.webp');
|
||||
expect(image.attr('width')).to.equal('768');
|
||||
expect(image.attr('height')).to.equal('414');
|
||||
});
|
||||
|
||||
it('built the optimized image', () => {
|
||||
verifyImage('hero_Z2k1JGN.webp', { width: 768, height: 414, type: 'webp' });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2228,26 +2228,20 @@ importers:
|
|||
|
||||
packages/integrations/image:
|
||||
specifiers:
|
||||
'@types/etag': ^1.8.1
|
||||
'@types/sharp': ^0.30.4
|
||||
'@types/sharp': ^0.30.5
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
etag: ^1.8.1
|
||||
image-size: ^1.0.1
|
||||
image-size: ^1.0.2
|
||||
kleur: ^4.1.4
|
||||
mrmime: ^1.0.0
|
||||
magic-string: ^0.25.9
|
||||
mime: ^3.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
|
||||
mrmime: 1.0.1
|
||||
magic-string: 0.25.9
|
||||
mime: 3.0.0
|
||||
sharp: 0.30.7
|
||||
slash: 4.0.0
|
||||
tiny-glob: 0.2.9
|
||||
devDependencies:
|
||||
'@types/etag': 1.8.1
|
||||
'@types/sharp': 0.30.5
|
||||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
|
@ -2283,6 +2277,18 @@ importers:
|
|||
'@astrojs/node': link:../../../../node
|
||||
astro: link:../../../../../astro
|
||||
|
||||
packages/integrations/image/test/fixtures/with-mdx:
|
||||
specifiers:
|
||||
'@astrojs/image': workspace:*
|
||||
'@astrojs/mdx': workspace:*
|
||||
'@astrojs/node': workspace:*
|
||||
astro: workspace:*
|
||||
dependencies:
|
||||
'@astrojs/image': link:../../..
|
||||
'@astrojs/mdx': link:../../../../mdx
|
||||
'@astrojs/node': link:../../../../node
|
||||
astro: link:../../../../../astro
|
||||
|
||||
packages/integrations/lit:
|
||||
specifiers:
|
||||
'@lit-labs/ssr': ^2.2.0
|
||||
|
@ -11464,11 +11470,6 @@ packages:
|
|||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/etag/1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/event-target-shim/5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -11904,6 +11905,7 @@ packages:
|
|||
|
||||
/globalyzer/0.1.0:
|
||||
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
|
||||
dev: true
|
||||
|
||||
/globby/11.1.0:
|
||||
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
||||
|
@ -11931,6 +11933,7 @@ packages:
|
|||
|
||||
/globrex/0.1.2:
|
||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||
dev: true
|
||||
|
||||
/graceful-fs/4.2.10:
|
||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
|
||||
|
@ -16303,6 +16306,7 @@ packages:
|
|||
dependencies:
|
||||
globalyzer: 0.1.0
|
||||
globrex: 0.1.2
|
||||
dev: true
|
||||
|
||||
/tinypool/0.2.4:
|
||||
resolution: {integrity: sha512-Vs3rhkUH6Qq1t5bqtb816oT+HeJTXfwt2cbPH17sWHIYKTotQIFPk3tf2fgqRrVyMDVOc1EnPgzIxfIulXVzwQ==}
|
||||
|
|