Implement RFC "A core story for images" (#6344)
* feat(assets): Add Vite plugin
* feat(images): Set up Image component
* fix(types): Attempt to fix type generation
* Revert "fix(types): Attempt to fix type generation"
This reverts commit 063aa276e2
.
* fix(image): Fix image types causing build to fail
* feat(image): Implement client side part
* feat(services): Allow arbitrary transforms parameters
* fix(image): Fix paths and types
* config(types): Update config types to provide completions for available services
* feat(image): Add serving in dev
* feat(image): Improve type error messages
* refactor(image): Move sharp's parseParams to baseService
* refactor(image): Skip work in dev for remote servies
* feat(image): Add support for remote images
* feat(image): Add squoosh service
* chore: update export map
* refactor(image): Abstract attributes handling by services
* config(vercel): Remove test image service
* feat(image): Support for relative images in Markdown (WIP)
* feat(images): Add support for relative images in Markdown
* feat(image): Update with RFC feedback
* fix(image): Fix alt error on getImage
* feat(image): Add support for assets validation through content collections
* feat(image): Remove validateTransform
* feat(image): Move to assets folder
* fix(image): Fix package exports
* feat(image): Add static imports references to virtual moduel
* fix(image): Fix images from content collections not working when embedded
* chore: lockfile
* fix(markdown): Fix type
* fix(images): Flag enhanced images behing an experimental flag
* config(example): Update images example conifg
* fix(image): Fix types
* fix(image): Fix asset type for strict, allow arbritary input and output formats
* chore: fix example check
* feat(image): Emit assets for ESM imported images
* Add initial core image tests (#6381)
* feat(images): Make frontmatter extraction more generic than images for future
* feat(image): Add support for building
* fix(image): Fix types
* fix(images): Fix compatibility with image integration
* feat(images): Cuter generation stats
* fix(images): Globals are unsafe, it turns out
* fix(images): Only generate images if flag is enabled
* fix(images): Only create `addStaticImage` in build
* feat(images): Add SSR endpoint
* fix(images): Only inject route in SSR
* Add tests for SSR
* Remove console.log
* Updated lockfile
* rename to satisfy the link gods
* skip build tests for now
* fix(images): Fix WASM files not being copied in dev
* feat(images): Add quality presets
* fix build tests running
* Remove console.log
* Add tests for getImage
* Test local services
* Test the content collections API
* Add tests for quality
* Skipping content collections test
* feat(image): Add support for `~/assets` alias
* test(image): Add tests for aliases in dev
* Fix windows + content collections
* test(image): Add tests for aliased images and images in Markdown
* Fix markdown images being built
* Should be posix join
* Use the optimized image
* fix test
* Fixes windows smoke
* fix(image): Nits
* feat(images): Add automatic update for `env.d.ts` when experimental images are enabled
* fix(images): Revert env.d.ts change if the user opted-out of the experimental image support
* chore: remove bad image example project
* feat(image): Rename `experimental.images` to `experimental.assets`
* fix(images): Remove unused code in MDX integration
* chore: Remove unrelated change
* fix(images): Remove export from astro/components
* Fix, esm import on Win
* test(images): Add test for format
* fix(images): Add `client-image.d.ts` to export map
* chore: changeset
* fix(images): Adjust with feedback, no more automatic refine, asset() -> image()
* fix(images): Fix types
* fix(images): Remove unnecessary spread
* fix(images): Better types for parseUrl and transform
* fix(images): Fix types
* fix(images): Adjust from feedback
* fix(images): Pass width and height through getHTMLAttributes even if they're not added by the uesr
* fix(images): Recusirsively extract frontmatter assets
* fix(images): Use a reduce instead
* feat(images): Add support for data: URIs
* chore: changeset
* docs(images): Misc docs fixes
* Update .changeset/gold-rocks-cry.md
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update .changeset/gold-rocks-cry.md
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
* Update packages/astro/src/@types/astro.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/services/service.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/services/service.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/services/service.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/types.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/types.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
---------
Co-authored-by: Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Matthew Phillips <matthew@matthewphillips.info>
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
377530a810
commit
694918a56b
113 changed files with 14978 additions and 149 deletions
15
.changeset/gold-rocks-cry.md
Normal file
15
.changeset/gold-rocks-cry.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
'astro': minor
|
||||
'@astrojs/mdx': minor
|
||||
'@astrojs/markdown-remark': minor
|
||||
---
|
||||
|
||||
Add a new experimental flag (`experimental.assets`) to enable our new core Assets story.
|
||||
|
||||
This unlocks a few features:
|
||||
- A new built-in image component and JavaScript API to transform and optimize images.
|
||||
- Relative images with automatic optimization in Markdown.
|
||||
- Support for validating assets using content collections.
|
||||
- and more!
|
||||
|
||||
See [Assets (Experimental)](https://docs.astro.build/en/guides/assets/) on our docs site for more information on how to use this feature!
|
23
packages/astro/client-base.d.ts
vendored
23
packages/astro/client-base.d.ts
vendored
|
@ -1,5 +1,28 @@
|
|||
/// <reference path="./import-meta.d.ts" />
|
||||
|
||||
declare module 'astro:assets' {
|
||||
// Exporting things one by one is a bit cumbersome, not sure if there's a better way - erika, 2023-02-03
|
||||
type AstroAssets = {
|
||||
getImage: typeof import('./dist/assets/index.js').getImage;
|
||||
Image: typeof import('./components/Image.astro').default;
|
||||
};
|
||||
|
||||
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] };
|
||||
type ImgAttributes = WithRequired<
|
||||
Omit<import('./types').HTMLAttributes<'img'>, 'src' | 'width' | 'height'>,
|
||||
'alt'
|
||||
>;
|
||||
|
||||
export type LocalImageProps = Simplify<
|
||||
import('./dist/assets/types.js').LocalImageProps<ImgAttributes>
|
||||
>;
|
||||
export type RemoteImageProps = Simplify<
|
||||
import('./dist/assets/types.js').RemoteImageProps<ImgAttributes>
|
||||
>;
|
||||
export const { getImage, Image }: AstroAssets;
|
||||
}
|
||||
|
||||
type MD = import('./dist/@types/astro').MarkdownInstance<Record<string, any>>;
|
||||
interface ExportedMarkdownModuleEntities {
|
||||
frontmatter: MD['frontmatter'];
|
||||
|
|
48
packages/astro/client-image.d.ts
vendored
Normal file
48
packages/astro/client-image.d.ts
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
/// <reference path="./client-base.d.ts" />
|
||||
|
||||
type InputFormat = 'avif' | 'gif' | 'heic' | 'heif' | 'jpeg' | 'jpg' | 'png' | 'tiff' | 'webp';
|
||||
|
||||
interface ImageMetadata {
|
||||
src: string;
|
||||
width: number;
|
||||
height: number;
|
||||
format: InputFormat;
|
||||
}
|
||||
|
||||
// images
|
||||
declare module '*.avif' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
||||
declare module '*.gif' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
||||
declare module '*.heic' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
||||
declare module '*.heif' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
||||
declare module '*.png' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
||||
declare module '*.tiff' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
||||
declare module '*.webp' {
|
||||
const metadata: ImageMetadata;
|
||||
export default metadata;
|
||||
}
|
28
packages/astro/components/Image.astro
Normal file
28
packages/astro/components/Image.astro
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import { getImage, type LocalImageProps, type RemoteImageProps } from 'astro:assets';
|
||||
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
|
||||
|
||||
// The TypeScript diagnostic for JSX props uses the last member of the union to suggest props, so it would be better for
|
||||
// LocalImageProps to be last. Unfortunately, when we do this the error messages that remote images get are complete nonsense
|
||||
// Not 100% sure how to fix this, seems to be a TypeScript issue. Unfortunate.
|
||||
type Props = LocalImageProps | RemoteImageProps;
|
||||
|
||||
const props = Astro.props;
|
||||
|
||||
if (props.alt === undefined || props.alt === null) {
|
||||
throw new AstroError(AstroErrorData.ImageMissingAlt);
|
||||
}
|
||||
|
||||
// As a convenience, allow width and height to be string with a number in them, to match HTML's native `img`.
|
||||
if (typeof props.width === 'string') {
|
||||
props.width = parseInt(props.width);
|
||||
}
|
||||
|
||||
if (typeof props.height === 'string') {
|
||||
props.height = parseInt(props.height);
|
||||
}
|
||||
|
||||
const image = await getImage(props);
|
||||
---
|
||||
|
||||
<img src={image.src} {...image.attributes} />
|
|
@ -32,6 +32,7 @@
|
|||
"./types": "./types.d.ts",
|
||||
"./client": "./client.d.ts",
|
||||
"./client-base": "./client-base.d.ts",
|
||||
"./client-image": "./client-image.d.ts",
|
||||
"./import-meta": "./import-meta.d.ts",
|
||||
"./astro-jsx": "./astro-jsx.d.ts",
|
||||
"./tsconfigs/*.json": "./tsconfigs/*",
|
||||
|
@ -47,6 +48,10 @@
|
|||
"./client/*": "./dist/runtime/client/*",
|
||||
"./components": "./components/index.ts",
|
||||
"./components/*": "./components/*",
|
||||
"./assets": "./dist/assets/index.js",
|
||||
"./assets/image-endpoint": "./dist/assets/image-endpoint.js",
|
||||
"./assets/services/sharp": "./dist/assets/services/sharp.js",
|
||||
"./assets/services/squoosh": "./dist/assets/services/squoosh.js",
|
||||
"./content/internal": "./dist/content/internal.js",
|
||||
"./debug": "./components/Debug.astro",
|
||||
"./internal/*": "./dist/runtime/server/*",
|
||||
|
@ -77,6 +82,7 @@
|
|||
"env.d.ts",
|
||||
"client.d.ts",
|
||||
"client-base.d.ts",
|
||||
"client-image.d.ts",
|
||||
"import-meta.d.ts",
|
||||
"astro-jsx.d.ts",
|
||||
"types.d.ts",
|
||||
|
@ -86,10 +92,10 @@
|
|||
],
|
||||
"scripts": {
|
||||
"prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"",
|
||||
"build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc",
|
||||
"build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc && pnpm run postbuild",
|
||||
"build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\"",
|
||||
"dev": "astro-scripts dev --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
|
||||
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
|
||||
"dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
|
||||
"postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"",
|
||||
"test:unit": "mocha --exit --timeout 30000 ./test/units/**/*.test.js",
|
||||
"test:unit:match": "mocha --exit --timeout 30000 ./test/units/**/*.test.js -g",
|
||||
"test": "pnpm run test:unit && mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
|
||||
|
@ -128,6 +134,7 @@
|
|||
"github-slugger": "^2.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-escaper": "^3.0.3",
|
||||
"image-size": "^1.0.2",
|
||||
"kleur": "^4.1.4",
|
||||
"magic-string": "^0.27.0",
|
||||
"mime": "^3.0.0",
|
||||
|
@ -173,6 +180,7 @@
|
|||
"@types/rimraf": "^3.0.2",
|
||||
"@types/send": "^0.17.1",
|
||||
"@types/server-destroy": "^1.0.1",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/unist": "^2.0.6",
|
||||
"astro-scripts": "workspace:*",
|
||||
"chai": "^4.3.6",
|
||||
|
@ -187,10 +195,19 @@
|
|||
"remark-code-titles": "^0.1.2",
|
||||
"rollup": "^3.9.0",
|
||||
"sass": "^1.52.2",
|
||||
"sharp": "^0.31.3",
|
||||
"srcset-parse": "^1.1.0",
|
||||
"undici": "^5.20.0",
|
||||
"unified": "^10.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"sharp": "^0.31.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"sharp": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.12.0",
|
||||
"npm": ">=6.14.0"
|
||||
|
|
|
@ -18,6 +18,7 @@ import type { PageBuildData } from '../core/build/types';
|
|||
import type { AstroConfigSchema } from '../core/config';
|
||||
import type { AstroTimer } from '../core/config/timer';
|
||||
import type { AstroCookies } from '../core/cookies';
|
||||
import type { LogOptions } from '../core/logger/core';
|
||||
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
|
||||
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
||||
export type {
|
||||
|
@ -28,6 +29,8 @@ export type {
|
|||
RemarkPlugins,
|
||||
ShikiConfig,
|
||||
} from '@astrojs/markdown-remark';
|
||||
export type { ExternalImageService, LocalImageService } from '../assets/services/service';
|
||||
export type { ImageTransform } from '../assets/types';
|
||||
export type { SSRManifest } from '../core/app/types';
|
||||
export type { AstroCookies } from '../core/cookies';
|
||||
|
||||
|
@ -85,6 +88,7 @@ export interface CLIFlags {
|
|||
port?: number;
|
||||
config?: string;
|
||||
drafts?: boolean;
|
||||
experimentalAssets?: boolean;
|
||||
}
|
||||
|
||||
export interface BuildConfig {
|
||||
|
@ -696,6 +700,16 @@ export interface AstroUserConfig {
|
|||
|
||||
server?: ServerConfig | ((options: { command: 'dev' | 'preview' }) => ServerConfig);
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @kind heading
|
||||
* @name Image options
|
||||
*/
|
||||
image?: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
service: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @kind heading
|
||||
|
@ -918,7 +932,27 @@ export interface AstroUserConfig {
|
|||
* Astro offers experimental flags to give users early access to new features.
|
||||
* These flags are not guaranteed to be stable.
|
||||
*/
|
||||
experimental?: object;
|
||||
experimental?: {
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.assets
|
||||
* @type {boolean}
|
||||
* @default `false`
|
||||
* @version 2.1.0
|
||||
* @description
|
||||
* Enable experimental support for optimizing and resizing images. With this enabled, a new `astro:assets` module will be exposed.
|
||||
*
|
||||
* To enable this feature, set `experimental.assets` to `true` in your Astro config:
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* experimental: {
|
||||
* assets: true,
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
assets?: boolean;
|
||||
};
|
||||
|
||||
// Legacy options to be removed
|
||||
|
||||
|
@ -1432,6 +1466,11 @@ export interface AstroIntegration {
|
|||
};
|
||||
}
|
||||
|
||||
export interface AstroPluginOptions {
|
||||
settings: AstroSettings;
|
||||
logging: LogOptions;
|
||||
}
|
||||
|
||||
export type RouteType = 'page' | 'endpoint';
|
||||
|
||||
export interface RoutePart {
|
||||
|
|
3
packages/astro/src/assets/README.md
Normal file
3
packages/astro/src/assets/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# assets
|
||||
|
||||
This directory powers the Assets story in Astro. Notably, it contains all the code related to optimizing images and serving them in the different modes Astro can run in (SSG, SSR, dev, build etc).
|
14
packages/astro/src/assets/consts.ts
Normal file
14
packages/astro/src/assets/consts.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const VIRTUAL_MODULE_ID = 'astro:assets';
|
||||
export const VIRTUAL_SERVICE_ID = 'virtual:image-service';
|
||||
export const VALID_INPUT_FORMATS = [
|
||||
'heic',
|
||||
'heif',
|
||||
'avif',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'png',
|
||||
'tiff',
|
||||
'webp',
|
||||
'gif',
|
||||
] as const;
|
||||
export const VALID_OUTPUT_FORMATS = ['avif', 'png', 'webp', 'jpeg', 'jpg'] as const;
|
66
packages/astro/src/assets/image-endpoint.ts
Normal file
66
packages/astro/src/assets/image-endpoint.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import mime from 'mime';
|
||||
import type { APIRoute } from '../@types/astro.js';
|
||||
import { isRemotePath } from '../core/path.js';
|
||||
import { getConfiguredImageService } from './internal.js';
|
||||
import { isLocalService } from './services/service.js';
|
||||
import { etag } from './utils/etag.js';
|
||||
|
||||
async function loadRemoteImage(src: URL) {
|
||||
try {
|
||||
const res = await fetch(src);
|
||||
|
||||
if (!res.ok) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
} catch (err: unknown) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint used in SSR to serve optimized images
|
||||
*/
|
||||
export const get: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const imageService = await getConfiguredImageService();
|
||||
|
||||
if (!isLocalService(imageService)) {
|
||||
throw new Error('Configured image service is not a local service');
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const transform = await imageService.parseURL(url);
|
||||
|
||||
if (!transform || !transform.src) {
|
||||
throw new Error('Incorrect transform returned by `parseURL`');
|
||||
}
|
||||
|
||||
let inputBuffer: Buffer | undefined = undefined;
|
||||
|
||||
// TODO: handle config subpaths?
|
||||
const sourceUrl = isRemotePath(transform.src)
|
||||
? new URL(transform.src)
|
||||
: new URL(transform.src, url.origin);
|
||||
inputBuffer = await loadRemoteImage(sourceUrl);
|
||||
|
||||
if (!inputBuffer) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const { data, format } = await imageService.transform(inputBuffer, transform);
|
||||
|
||||
return new Response(data, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': mime.getType(format) || '',
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
ETag: etag(data.toString()),
|
||||
Date: new Date().toUTCString(),
|
||||
},
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
return new Response(`Server Error: ${err}`, { status: 500 });
|
||||
}
|
||||
};
|
4
packages/astro/src/assets/index.ts
Normal file
4
packages/astro/src/assets/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { getConfiguredImageService, getImage } from './internal.js';
|
||||
export { baseService } from './services/service.js';
|
||||
export { type LocalImageProps, type RemoteImageProps } from './types.js';
|
||||
export { imageMetadata } from './utils/metadata.js';
|
117
packages/astro/src/assets/internal.ts
Normal file
117
packages/astro/src/assets/internal.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
import fs from 'node:fs';
|
||||
import { StaticBuildOptions } from '../core/build/types.js';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import { ImageService, isLocalService, LocalImageService } from './services/service.js';
|
||||
import type { ImageMetadata, ImageTransform } from './types.js';
|
||||
|
||||
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
|
||||
return typeof src === 'object';
|
||||
}
|
||||
|
||||
export async function getConfiguredImageService(): Promise<ImageService> {
|
||||
if (!globalThis.astroAsset.imageService) {
|
||||
const { default: service }: { default: ImageService } = await import(
|
||||
// @ts-expect-error
|
||||
'virtual:image-service'
|
||||
).catch((e) => {
|
||||
const error = new AstroError(AstroErrorData.InvalidImageService);
|
||||
(error as any).cause = e;
|
||||
throw error;
|
||||
});
|
||||
|
||||
globalThis.astroAsset.imageService = service;
|
||||
return service;
|
||||
}
|
||||
|
||||
return globalThis.astroAsset.imageService;
|
||||
}
|
||||
|
||||
interface GetImageResult {
|
||||
options: ImageTransform;
|
||||
src: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an optimized image and the necessary attributes to render it.
|
||||
*
|
||||
* **Example**
|
||||
* ```astro
|
||||
* ---
|
||||
* import { getImage } from 'astro:assets';
|
||||
* import originalImage from '../assets/image.png';
|
||||
*
|
||||
* const optimizedImage = await getImage({src: originalImage, width: 1280 })
|
||||
* ---
|
||||
* <img src={optimizedImage.src} {...optimizedImage.attributes} />
|
||||
* ```
|
||||
*
|
||||
* This is functionally equivalent to using the `<Image />` component, as the component calls this function internally.
|
||||
*/
|
||||
export async function getImage(options: ImageTransform): Promise<GetImageResult> {
|
||||
const service = await getConfiguredImageService();
|
||||
let imageURL = service.getURL(options);
|
||||
|
||||
// In build and for local services, we need to collect the requested parameters so we can generate the final images
|
||||
if (isLocalService(service) && globalThis.astroAsset.addStaticImage) {
|
||||
imageURL = globalThis.astroAsset.addStaticImage(options);
|
||||
}
|
||||
|
||||
return {
|
||||
options,
|
||||
src: imageURL,
|
||||
attributes: service.getHTMLAttributes !== undefined ? service.getHTMLAttributes(options) : {},
|
||||
};
|
||||
}
|
||||
|
||||
export function getStaticImageList(): Iterable<[ImageTransform, string]> {
|
||||
if (!globalThis?.astroAsset?.staticImages) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return globalThis.astroAsset.staticImages?.entries();
|
||||
}
|
||||
|
||||
interface GenerationData {
|
||||
weight: {
|
||||
before: number;
|
||||
after: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateImage(
|
||||
buildOpts: StaticBuildOptions,
|
||||
options: ImageTransform,
|
||||
filepath: string
|
||||
): Promise<GenerationData | undefined> {
|
||||
if (!isESMImportedImage(options.src)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const imageService = (await getConfiguredImageService()) as LocalImageService;
|
||||
|
||||
let serverRoot: URL, clientRoot: URL;
|
||||
if (buildOpts.settings.config.output === 'server') {
|
||||
serverRoot = buildOpts.settings.config.build.server;
|
||||
clientRoot = buildOpts.settings.config.build.client;
|
||||
} else {
|
||||
serverRoot = buildOpts.settings.config.outDir;
|
||||
clientRoot = buildOpts.settings.config.outDir;
|
||||
}
|
||||
|
||||
const fileData = await fs.promises.readFile(new URL('.' + options.src.src, serverRoot));
|
||||
const resultData = await imageService.transform(fileData, { ...options, src: options.src.src });
|
||||
|
||||
const finalFileURL = new URL('.' + filepath, clientRoot);
|
||||
const finalFolderURL = new URL('./', finalFileURL);
|
||||
|
||||
await fs.promises.mkdir(finalFolderURL, { recursive: true });
|
||||
await fs.promises.writeFile(finalFileURL, resultData.data);
|
||||
|
||||
return {
|
||||
weight: {
|
||||
before: Math.trunc(fileData.byteLength / 1024),
|
||||
after: Math.trunc(resultData.data.byteLength / 1024),
|
||||
},
|
||||
};
|
||||
}
|
174
packages/astro/src/assets/services/service.ts
Normal file
174
packages/astro/src/assets/services/service.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
|
||||
import { isRemotePath } from '../../core/path.js';
|
||||
import { isESMImportedImage } from '../internal.js';
|
||||
import { ImageTransform, OutputFormat } from '../types.js';
|
||||
|
||||
export type ImageService = LocalImageService | ExternalImageService;
|
||||
|
||||
export function isLocalService(service: ImageService | undefined): service is LocalImageService {
|
||||
if (!service) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'transform' in service;
|
||||
}
|
||||
|
||||
export function parseQuality(quality: string): string | number {
|
||||
let result = parseInt(quality);
|
||||
if (Number.isNaN(result)) {
|
||||
return quality;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
interface SharedServiceProps {
|
||||
/**
|
||||
* Return the URL to the endpoint or URL your images are generated from.
|
||||
*
|
||||
* For a local service, your service should expose an endpoint handling the image requests, or use Astro's at `/_image`.
|
||||
*
|
||||
* For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image`
|
||||
*
|
||||
*/
|
||||
getURL: (options: ImageTransform) => string;
|
||||
/**
|
||||
* Return any additional HTML attributes separate from `src` that your service requires to show the image properly.
|
||||
*
|
||||
* For example, you might want to return the `width` and `height` to avoid CLS, or a particular `class` or `style`.
|
||||
* In most cases, you'll want to return directly what your user supplied you, minus the attributes that were used to generate the image.
|
||||
*/
|
||||
getHTMLAttributes?: (options: ImageTransform) => Record<string, any>;
|
||||
}
|
||||
|
||||
export type ExternalImageService = SharedServiceProps;
|
||||
|
||||
type LocalImageTransform = {
|
||||
src: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export interface LocalImageService extends SharedServiceProps {
|
||||
/**
|
||||
* Parse the requested parameters passed in the URL from `getURL` back into an object to be used later by `transform`
|
||||
*
|
||||
* In most cases, this will get query parameters using, for example, `params.get('width')` and return those.
|
||||
*/
|
||||
parseURL: (url: URL) => LocalImageTransform | undefined;
|
||||
/**
|
||||
* Performs the image transformations on the input image and returns both the binary data and
|
||||
* final image format of the optimized image.
|
||||
*/
|
||||
transform: (
|
||||
inputBuffer: Buffer,
|
||||
transform: LocalImageTransform
|
||||
) => Promise<{ data: Buffer; format: OutputFormat }>;
|
||||
}
|
||||
|
||||
export type BaseServiceTransform = {
|
||||
src: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
format?: string | null;
|
||||
quality?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Basic local service using the included `_image` endpoint.
|
||||
* This service intentionally does not implement `transform`.
|
||||
*
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* const service = {
|
||||
* getURL: baseService.getURL,
|
||||
* parseURL: baseService.parseURL,
|
||||
* getHTMLAttributes: baseService.getHTMLAttributes,
|
||||
* async transform(inputBuffer, transformOptions) {...}
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This service only supports the following properties: `width`, `height`, `format` and `quality`.
|
||||
* Additionally, remote URLs are passed as-is.
|
||||
*
|
||||
*/
|
||||
export const baseService: Omit<LocalImageService, 'transform'> = {
|
||||
getHTMLAttributes(options) {
|
||||
let targetWidth = options.width;
|
||||
let targetHeight = options.height;
|
||||
if (isESMImportedImage(options.src)) {
|
||||
const aspectRatio = options.src.width / options.src.height;
|
||||
|
||||
// If we have a desired height and no width, calculate the target width automatically
|
||||
if (targetHeight && !targetWidth) {
|
||||
targetWidth = Math.round(targetHeight * aspectRatio);
|
||||
} else if (targetWidth && !targetHeight) {
|
||||
targetHeight = Math.round(targetWidth / aspectRatio);
|
||||
} else {
|
||||
targetWidth = options.src.width;
|
||||
targetHeight = options.src.height;
|
||||
}
|
||||
}
|
||||
|
||||
const { src, width, height, format, quality, ...attributes } = options;
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
loading: attributes.loading ?? 'lazy',
|
||||
decoding: attributes.decoding ?? 'async',
|
||||
};
|
||||
},
|
||||
getURL(options: ImageTransform) {
|
||||
if (!isESMImportedImage(options.src)) {
|
||||
// For non-ESM imported images, width and height are required to avoid CLS, as we can't infer them from the file
|
||||
let missingDimension: 'width' | 'height' | 'both' | undefined;
|
||||
if (!options.width && !options.height) {
|
||||
missingDimension = 'both';
|
||||
} else if (!options.width && options.height) {
|
||||
missingDimension = 'width';
|
||||
} else if (options.width && !options.height) {
|
||||
missingDimension = 'height';
|
||||
}
|
||||
|
||||
if (missingDimension) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.MissingImageDimension,
|
||||
message: AstroErrorData.MissingImageDimension.message(missingDimension),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Both our currently available local services don't handle remote images, so for them we can just return as is
|
||||
if (!isESMImportedImage(options.src) && isRemotePath(options.src)) {
|
||||
return options.src;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('href', isESMImportedImage(options.src) ? options.src.src : options.src);
|
||||
|
||||
options.width && searchParams.append('w', options.width.toString());
|
||||
options.height && searchParams.append('h', options.height.toString());
|
||||
options.quality && searchParams.append('q', options.quality.toString());
|
||||
options.format && searchParams.append('f', options.format);
|
||||
|
||||
return '/_image?' + searchParams;
|
||||
},
|
||||
parseURL(url) {
|
||||
const params = url.searchParams;
|
||||
|
||||
if (!params.has('href')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const transform: BaseServiceTransform = {
|
||||
src: params.get('href')!,
|
||||
width: params.has('w') ? parseInt(params.get('w')!) : undefined,
|
||||
height: params.has('h') ? parseInt(params.get('h')!) : undefined,
|
||||
format: params.get('f') as OutputFormat | null,
|
||||
quality: params.get('q'),
|
||||
};
|
||||
|
||||
return transform;
|
||||
},
|
||||
};
|
72
packages/astro/src/assets/services/sharp.ts
Normal file
72
packages/astro/src/assets/services/sharp.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import type { FormatEnum } from 'sharp';
|
||||
import type { ImageQualityPreset, OutputFormat } from '../types.js';
|
||||
import { baseService, BaseServiceTransform, LocalImageService, parseQuality } from './service.js';
|
||||
|
||||
let sharp: typeof import('sharp');
|
||||
|
||||
const qualityTable: Record<ImageQualityPreset, number> = {
|
||||
low: 25,
|
||||
mid: 50,
|
||||
high: 80,
|
||||
max: 100,
|
||||
};
|
||||
|
||||
async function loadSharp() {
|
||||
let sharpImport: typeof import('sharp');
|
||||
try {
|
||||
sharpImport = (await import('sharp')).default;
|
||||
} catch (e) {
|
||||
throw new Error('Could not find Sharp. Please install Sharp manually into your project.');
|
||||
}
|
||||
|
||||
return sharpImport;
|
||||
}
|
||||
|
||||
const sharpService: LocalImageService = {
|
||||
getURL: baseService.getURL,
|
||||
parseURL: baseService.parseURL,
|
||||
getHTMLAttributes: baseService.getHTMLAttributes,
|
||||
async transform(inputBuffer, transformOptions) {
|
||||
if (!sharp) sharp = await loadSharp();
|
||||
|
||||
const transform: BaseServiceTransform = transformOptions;
|
||||
|
||||
// If the user didn't specify a format, we'll default to `webp`. It offers the best ratio of compatibility / quality
|
||||
// In the future, hopefully we can replace this with `avif`, alas, Edge. See https://caniuse.com/avif
|
||||
if (!transform.format) {
|
||||
transform.format = 'webp';
|
||||
}
|
||||
|
||||
let result = sharp(inputBuffer, { failOnError: false, pages: -1 });
|
||||
|
||||
// Never resize using both width and height at the same time, prioritizing width.
|
||||
if (transform.height && !transform.width) {
|
||||
result.resize({ height: transform.height });
|
||||
} else if (transform.width) {
|
||||
result.resize({ width: transform.width });
|
||||
}
|
||||
|
||||
if (transform.format) {
|
||||
let quality: number | string | undefined = undefined;
|
||||
if (transform.quality) {
|
||||
const parsedQuality = parseQuality(transform.quality);
|
||||
if (typeof parsedQuality === 'number') {
|
||||
quality = parsedQuality;
|
||||
} else {
|
||||
quality = transform.quality in qualityTable ? qualityTable[transform.quality] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
result.toFormat(transform.format as keyof FormatEnum, { quality: quality });
|
||||
}
|
||||
|
||||
const { data, info } = await result.toBuffer({ resolveWithObject: true });
|
||||
|
||||
return {
|
||||
data: data,
|
||||
format: info.format as OutputFormat,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default sharpService;
|
72
packages/astro/src/assets/services/squoosh.ts
Normal file
72
packages/astro/src/assets/services/squoosh.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
// TODO: Investigate removing this service once sharp lands WASM support, as libsquoosh is deprecated
|
||||
|
||||
import type { ImageQualityPreset, OutputFormat } from '../types.js';
|
||||
import { baseService, BaseServiceTransform, LocalImageService, parseQuality } from './service.js';
|
||||
import { processBuffer } from './vendor/squoosh/image-pool.js';
|
||||
import type { Operation } from './vendor/squoosh/image.js';
|
||||
|
||||
const baseQuality = { low: 25, mid: 50, high: 80, max: 100 };
|
||||
const qualityTable: Record<Exclude<OutputFormat, 'png'>, Record<ImageQualityPreset, number>> = {
|
||||
avif: {
|
||||
// Squoosh's AVIF encoder has a bit of a weird behavior where `62` is technically the maximum, and anything over is overkill
|
||||
max: 62,
|
||||
high: 45,
|
||||
mid: 35,
|
||||
low: 20,
|
||||
},
|
||||
jpeg: baseQuality,
|
||||
jpg: baseQuality,
|
||||
webp: baseQuality,
|
||||
// Squoosh's PNG encoder does not support a quality setting, so we can skip that here
|
||||
};
|
||||
|
||||
const service: LocalImageService = {
|
||||
getURL: baseService.getURL,
|
||||
parseURL: baseService.parseURL,
|
||||
getHTMLAttributes: baseService.getHTMLAttributes,
|
||||
async transform(inputBuffer, transformOptions) {
|
||||
const transform: BaseServiceTransform = transformOptions as BaseServiceTransform;
|
||||
|
||||
let format = transform.format;
|
||||
if (!format) {
|
||||
format = 'webp';
|
||||
}
|
||||
|
||||
const operations: Operation[] = [];
|
||||
|
||||
// Never resize using both width and height at the same time, prioritizing width.
|
||||
if (transform.height && !transform.width) {
|
||||
operations.push({
|
||||
type: 'resize',
|
||||
height: transform.height,
|
||||
});
|
||||
} else if (transform.width) {
|
||||
operations.push({
|
||||
type: 'resize',
|
||||
width: transform.width,
|
||||
});
|
||||
}
|
||||
|
||||
let quality: number | string | undefined = undefined;
|
||||
if (transform.quality) {
|
||||
const parsedQuality = parseQuality(transform.quality);
|
||||
if (typeof parsedQuality === 'number') {
|
||||
quality = parsedQuality;
|
||||
} else {
|
||||
quality =
|
||||
transform.quality in qualityTable[format]
|
||||
? qualityTable[format][transform.quality]
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const data = await processBuffer(inputBuffer, operations, format, quality);
|
||||
|
||||
return {
|
||||
data: Buffer.from(data),
|
||||
format: format,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default service;
|
202
packages/astro/src/assets/services/vendor/squoosh/LICENSE
vendored
Normal file
202
packages/astro/src/assets/services/vendor/squoosh/LICENSE
vendored
Normal file
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
32
packages/astro/src/assets/services/vendor/squoosh/avif/avif_enc.d.ts
vendored
Normal file
32
packages/astro/src/assets/services/vendor/squoosh/avif/avif_enc.d.ts
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
// eslint-disable-next-line no-shadow
|
||||
export const enum AVIFTune {
|
||||
auto,
|
||||
psnr,
|
||||
ssim,
|
||||
}
|
||||
|
||||
export interface EncodeOptions {
|
||||
cqLevel: number
|
||||
denoiseLevel: number
|
||||
cqAlphaLevel: number
|
||||
tileRowsLog2: number
|
||||
tileColsLog2: number
|
||||
speed: number
|
||||
subsample: number
|
||||
chromaDeltaQ: boolean
|
||||
sharpness: number
|
||||
tune: AVIFTune
|
||||
}
|
||||
|
||||
export interface AVIFModule extends EmscriptenWasm.Module {
|
||||
encode(
|
||||
data: BufferSource,
|
||||
width: number,
|
||||
height: number,
|
||||
options: EncodeOptions
|
||||
): Uint8Array
|
||||
}
|
||||
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<AVIFModule>
|
||||
|
||||
export default moduleFactory
|
1765
packages/astro/src/assets/services/vendor/squoosh/avif/avif_node_dec.ts
vendored
Normal file
1765
packages/astro/src/assets/services/vendor/squoosh/avif/avif_node_dec.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
BIN
packages/astro/src/assets/services/vendor/squoosh/avif/avif_node_dec.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/avif/avif_node_dec.wasm
vendored
Normal file
Binary file not shown.
2032
packages/astro/src/assets/services/vendor/squoosh/avif/avif_node_enc.ts
vendored
Normal file
2032
packages/astro/src/assets/services/vendor/squoosh/avif/avif_node_enc.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
BIN
packages/astro/src/assets/services/vendor/squoosh/avif/avif_node_enc.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/avif/avif_node_enc.wasm
vendored
Normal file
Binary file not shown.
373
packages/astro/src/assets/services/vendor/squoosh/codecs.ts
vendored
Normal file
373
packages/astro/src/assets/services/vendor/squoosh/codecs.ts
vendored
Normal file
|
@ -0,0 +1,373 @@
|
|||
import { promises as fsp } from 'node:fs'
|
||||
import { getModuleURL, instantiateEmscriptenWasm, pathify } from './emscripten-utils.js'
|
||||
|
||||
interface DecodeModule extends EmscriptenWasm.Module {
|
||||
decode: (data: Uint8Array) => ImageData
|
||||
}
|
||||
|
||||
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>
|
||||
|
||||
interface RotateModuleInstance {
|
||||
exports: {
|
||||
memory: WebAssembly.Memory
|
||||
rotate(width: number, height: number, rotate: number): void
|
||||
}
|
||||
}
|
||||
|
||||
interface ResizeWithAspectParams {
|
||||
input_width: number
|
||||
input_height: number
|
||||
target_width?: number
|
||||
target_height?: number
|
||||
}
|
||||
|
||||
export interface ResizeOptions {
|
||||
width?: number
|
||||
height?: number
|
||||
method: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'
|
||||
premultiply: boolean
|
||||
linearRGB: boolean
|
||||
}
|
||||
|
||||
export interface RotateOptions {
|
||||
numRotations: number
|
||||
}
|
||||
|
||||
// MozJPEG
|
||||
import type { MozJPEGModule as MozJPEGEncodeModule } from './mozjpeg/mozjpeg_enc'
|
||||
// @ts-ignore
|
||||
import mozEnc from './mozjpeg/mozjpeg_node_enc.js'
|
||||
const mozEncWasm = new URL('./mozjpeg/mozjpeg_node_enc.wasm', getModuleURL(import.meta.url))
|
||||
// @ts-ignore
|
||||
import mozDec from './mozjpeg/mozjpeg_node_dec.js'
|
||||
const mozDecWasm = new URL('./mozjpeg/mozjpeg_node_dec.wasm', getModuleURL(import.meta.url))
|
||||
|
||||
// WebP
|
||||
import type { WebPModule as WebPEncodeModule } from './webp/webp_enc'
|
||||
// @ts-ignore
|
||||
import webpEnc from './webp/webp_node_enc.js'
|
||||
const webpEncWasm = new URL('./webp/webp_node_enc.wasm', getModuleURL(import.meta.url))
|
||||
// @ts-ignore
|
||||
import webpDec from './webp/webp_node_dec.js'
|
||||
const webpDecWasm = new URL('./webp/webp_node_dec.wasm', getModuleURL(import.meta.url))
|
||||
|
||||
// AVIF
|
||||
import type { AVIFModule as AVIFEncodeModule } from './avif/avif_enc'
|
||||
// @ts-ignore
|
||||
import avifEnc from './avif/avif_node_enc.js'
|
||||
const avifEncWasm = new URL('./avif/avif_node_enc.wasm', getModuleURL(import.meta.url))
|
||||
// @ts-ignore
|
||||
import avifDec from './avif/avif_node_dec.js'
|
||||
const avifDecWasm = new URL('./avif/avif_node_dec.wasm', getModuleURL(import.meta.url))
|
||||
|
||||
// PNG
|
||||
// @ts-ignore
|
||||
import * as pngEncDec from './png/squoosh_png.js'
|
||||
const pngEncDecWasm = new URL('./png/squoosh_png_bg.wasm', getModuleURL(import.meta.url))
|
||||
const pngEncDecInit = () =>
|
||||
pngEncDec.default(fsp.readFile(pathify(pngEncDecWasm.toString())))
|
||||
|
||||
// OxiPNG
|
||||
// @ts-ignore
|
||||
import * as oxipng from './png/squoosh_oxipng.js'
|
||||
const oxipngWasm = new URL('./png/squoosh_oxipng_bg.wasm', getModuleURL(import.meta.url))
|
||||
const oxipngInit = () => oxipng.default(fsp.readFile(pathify(oxipngWasm.toString())))
|
||||
|
||||
// Resize
|
||||
// @ts-ignore
|
||||
import * as resize from './resize/squoosh_resize.js'
|
||||
const resizeWasm = new URL('./resize/squoosh_resize_bg.wasm', getModuleURL(import.meta.url))
|
||||
const resizeInit = () => resize.default(fsp.readFile(pathify(resizeWasm.toString())))
|
||||
|
||||
// rotate
|
||||
const rotateWasm = new URL('./rotate/rotate.wasm', getModuleURL(import.meta.url))
|
||||
|
||||
// Our decoders currently rely on a `ImageData` global.
|
||||
import ImageData from './image_data.js'
|
||||
(global as any).ImageData = ImageData
|
||||
|
||||
function resizeNameToIndex(
|
||||
name: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'
|
||||
) {
|
||||
switch (name) {
|
||||
case 'triangle':
|
||||
return 0
|
||||
case 'catrom':
|
||||
return 1
|
||||
case 'mitchell':
|
||||
return 2
|
||||
case 'lanczos3':
|
||||
return 3
|
||||
default:
|
||||
throw Error(`Unknown resize algorithm "${name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
function resizeWithAspect({
|
||||
input_width,
|
||||
input_height,
|
||||
target_width,
|
||||
target_height,
|
||||
}: ResizeWithAspectParams): { width: number; height: number } {
|
||||
if (!target_width && !target_height) {
|
||||
throw Error('Need to specify at least width or height when resizing')
|
||||
}
|
||||
|
||||
if (target_width && target_height) {
|
||||
return { width: target_width, height: target_height }
|
||||
}
|
||||
|
||||
if (!target_width) {
|
||||
return {
|
||||
width: Math.round((input_width / input_height) * target_height!),
|
||||
height: target_height!,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width: target_width,
|
||||
height: Math.round((input_height / input_width) * target_width),
|
||||
}
|
||||
}
|
||||
|
||||
export const preprocessors = {
|
||||
resize: {
|
||||
name: 'Resize',
|
||||
description: 'Resize the image before compressing',
|
||||
instantiate: async () => {
|
||||
await resizeInit()
|
||||
return (
|
||||
buffer: Uint8Array,
|
||||
input_width: number,
|
||||
input_height: number,
|
||||
{ width, height, method, premultiply, linearRGB }: ResizeOptions
|
||||
) => {
|
||||
;({ width, height } = resizeWithAspect({
|
||||
input_width,
|
||||
input_height,
|
||||
target_width: width,
|
||||
target_height: height,
|
||||
}))
|
||||
const imageData = new ImageData(
|
||||
resize.resize(
|
||||
buffer,
|
||||
input_width,
|
||||
input_height,
|
||||
width,
|
||||
height,
|
||||
resizeNameToIndex(method),
|
||||
premultiply,
|
||||
linearRGB
|
||||
),
|
||||
width,
|
||||
height
|
||||
)
|
||||
resize.cleanup()
|
||||
return imageData
|
||||
}
|
||||
},
|
||||
defaultOptions: {
|
||||
method: 'lanczos3',
|
||||
fitMethod: 'stretch',
|
||||
premultiply: true,
|
||||
linearRGB: true,
|
||||
},
|
||||
},
|
||||
rotate: {
|
||||
name: 'Rotate',
|
||||
description: 'Rotate image',
|
||||
instantiate: async () => {
|
||||
return async (
|
||||
buffer: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
{ numRotations }: RotateOptions
|
||||
) => {
|
||||
const degrees = (numRotations * 90) % 360
|
||||
const sameDimensions = degrees === 0 || degrees === 180
|
||||
const size = width * height * 4
|
||||
const instance = (
|
||||
await WebAssembly.instantiate(await fsp.readFile(pathify(rotateWasm.toString())))
|
||||
).instance as RotateModuleInstance
|
||||
const { memory } = instance.exports
|
||||
const additionalPagesNeeded = Math.ceil(
|
||||
(size * 2 - memory.buffer.byteLength + 8) / (64 * 1024)
|
||||
)
|
||||
if (additionalPagesNeeded > 0) {
|
||||
memory.grow(additionalPagesNeeded)
|
||||
}
|
||||
const view = new Uint8ClampedArray(memory.buffer)
|
||||
view.set(buffer, 8)
|
||||
instance.exports.rotate(width, height, degrees)
|
||||
return new ImageData(
|
||||
view.slice(size + 8, size * 2 + 8),
|
||||
sameDimensions ? width : height,
|
||||
sameDimensions ? height : width
|
||||
)
|
||||
}
|
||||
},
|
||||
defaultOptions: {
|
||||
numRotations: 0,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
export const codecs = {
|
||||
mozjpeg: {
|
||||
name: 'MozJPEG',
|
||||
extension: 'jpg',
|
||||
detectors: [/^\xFF\xD8\xFF/],
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm.toString()),
|
||||
enc: () =>
|
||||
instantiateEmscriptenWasm(
|
||||
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
|
||||
mozEncWasm.toString()
|
||||
),
|
||||
defaultEncoderOptions: {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: 3 /*YCbCr*/,
|
||||
quant_table: 3,
|
||||
trellis_multipass: false,
|
||||
trellis_opt_zero: false,
|
||||
trellis_opt_table: false,
|
||||
trellis_loops: 1,
|
||||
auto_subsample: true,
|
||||
chroma_subsample: 2,
|
||||
separate_chroma_quality: false,
|
||||
chroma_quality: 75,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'quality',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
webp: {
|
||||
name: 'WebP',
|
||||
extension: 'webp',
|
||||
detectors: [/^RIFF....WEBPVP8[LX ]/s],
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm.toString()),
|
||||
enc: () =>
|
||||
instantiateEmscriptenWasm(
|
||||
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
|
||||
webpEncWasm.toString()
|
||||
),
|
||||
defaultEncoderOptions: {
|
||||
quality: 75,
|
||||
target_size: 0,
|
||||
target_PSNR: 0,
|
||||
method: 4,
|
||||
sns_strength: 50,
|
||||
filter_strength: 60,
|
||||
filter_sharpness: 0,
|
||||
filter_type: 1,
|
||||
partitions: 0,
|
||||
segments: 4,
|
||||
pass: 1,
|
||||
show_compressed: 0,
|
||||
preprocessing: 0,
|
||||
autofilter: 0,
|
||||
partition_limit: 0,
|
||||
alpha_compression: 1,
|
||||
alpha_filtering: 1,
|
||||
alpha_quality: 100,
|
||||
lossless: 0,
|
||||
exact: 0,
|
||||
image_hint: 0,
|
||||
emulate_jpeg_size: 0,
|
||||
thread_level: 0,
|
||||
low_memory: 0,
|
||||
near_lossless: 100,
|
||||
use_delta_palette: 0,
|
||||
use_sharp_yuv: 0,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'quality',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
avif: {
|
||||
name: 'AVIF',
|
||||
extension: 'avif',
|
||||
// eslint-disable-next-line no-control-regex
|
||||
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
|
||||
dec: () =>
|
||||
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm.toString()),
|
||||
enc: async () => {
|
||||
return instantiateEmscriptenWasm(
|
||||
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
|
||||
avifEncWasm.toString()
|
||||
)
|
||||
},
|
||||
defaultEncoderOptions: {
|
||||
cqLevel: 33,
|
||||
cqAlphaLevel: -1,
|
||||
denoiseLevel: 0,
|
||||
tileColsLog2: 0,
|
||||
tileRowsLog2: 0,
|
||||
speed: 6,
|
||||
subsample: 1,
|
||||
chromaDeltaQ: false,
|
||||
sharpness: 0,
|
||||
tune: 0 /* AVIFTune.auto */,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'cqLevel',
|
||||
min: 62,
|
||||
max: 0,
|
||||
},
|
||||
},
|
||||
oxipng: {
|
||||
name: 'OxiPNG',
|
||||
extension: 'png',
|
||||
// eslint-disable-next-line no-control-regex
|
||||
detectors: [/^\x89PNG\x0D\x0A\x1A\x0A/],
|
||||
dec: async () => {
|
||||
await pngEncDecInit()
|
||||
return {
|
||||
decode: (buffer: Buffer | Uint8Array) => {
|
||||
const imageData = pngEncDec.decode(buffer)
|
||||
pngEncDec.cleanup()
|
||||
return imageData
|
||||
},
|
||||
}
|
||||
},
|
||||
enc: async () => {
|
||||
await pngEncDecInit()
|
||||
await oxipngInit()
|
||||
return {
|
||||
encode: (
|
||||
buffer: Uint8ClampedArray | ArrayBuffer,
|
||||
width: number,
|
||||
height: number,
|
||||
opts: { level: number }
|
||||
) => {
|
||||
const simplePng = pngEncDec.encode(
|
||||
new Uint8Array(buffer),
|
||||
width,
|
||||
height
|
||||
)
|
||||
const imageData = oxipng.optimise(simplePng, opts.level, false)
|
||||
oxipng.cleanup()
|
||||
return imageData
|
||||
},
|
||||
}
|
||||
},
|
||||
defaultEncoderOptions: {
|
||||
level: 2,
|
||||
},
|
||||
autoOptimize: {
|
||||
option: 'level',
|
||||
min: 6,
|
||||
max: 1,
|
||||
},
|
||||
},
|
||||
} as const
|
24
packages/astro/src/assets/services/vendor/squoosh/copy-wasm.ts
vendored
Normal file
24
packages/astro/src/assets/services/vendor/squoosh/copy-wasm.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export async function copyWasmFiles(dir: URL) {
|
||||
const src = new URL('./', import.meta.url);
|
||||
await copyDir(fileURLToPath(src), fileURLToPath(dir));
|
||||
}
|
||||
|
||||
async function copyDir(src: string, dest: string) {
|
||||
const itemNames = await fs.readdir(src);
|
||||
await Promise.all(itemNames.map(async (srcName) => {
|
||||
const srcPath = path.join(src, srcName);
|
||||
const destPath = path.join(dest, srcName);
|
||||
const s = await fs.stat(srcPath);
|
||||
if (s.isFile() && /.wasm$/.test(srcPath)) {
|
||||
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
}
|
||||
else if (s.isDirectory()) {
|
||||
await copyDir(srcPath, destPath);
|
||||
}
|
||||
}));
|
||||
}
|
121
packages/astro/src/assets/services/vendor/squoosh/emscripten-types.d.ts
vendored
Normal file
121
packages/astro/src/assets/services/vendor/squoosh/emscripten-types.d.ts
vendored
Normal file
|
@ -0,0 +1,121 @@
|
|||
// These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten.
|
||||
declare namespace EmscriptenWasm {
|
||||
type ModuleFactory<T extends Module = Module> = (
|
||||
moduleOverrides?: ModuleOpts
|
||||
) => Promise<T>
|
||||
|
||||
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER'
|
||||
|
||||
// Options object for modularized Emscripten files. Shoe-horned by @surma.
|
||||
// FIXME: This an incomplete definition!
|
||||
interface ModuleOpts {
|
||||
mainScriptUrlOrBlob?: string
|
||||
noInitialRun?: boolean
|
||||
locateFile?: (url: string) => string
|
||||
onRuntimeInitialized?: () => void
|
||||
}
|
||||
|
||||
interface Module {
|
||||
print(str: string): void
|
||||
printErr(str: string): void
|
||||
arguments: string[]
|
||||
environment: EnvironmentType
|
||||
preInit: { (): void }[]
|
||||
preRun: { (): void }[]
|
||||
postRun: { (): void }[]
|
||||
preinitializedWebGLContext: WebGLRenderingContext
|
||||
noInitialRun: boolean
|
||||
noExitRuntime: boolean
|
||||
logReadFiles: boolean
|
||||
filePackagePrefixURL: string
|
||||
wasmBinary: ArrayBuffer
|
||||
|
||||
destroy(object: object): void
|
||||
getPreloadedPackage(
|
||||
remotePackageName: string,
|
||||
remotePackageSize: number
|
||||
): ArrayBuffer
|
||||
instantiateWasm(
|
||||
imports: WebAssembly.Imports,
|
||||
successCallback: (module: WebAssembly.Module) => void
|
||||
): WebAssembly.Exports
|
||||
locateFile(url: string): string
|
||||
onCustomMessage(event: MessageEvent): void
|
||||
|
||||
Runtime: any
|
||||
|
||||
ccall(
|
||||
ident: string,
|
||||
returnType: string | null,
|
||||
argTypes: string[],
|
||||
args: any[]
|
||||
): any
|
||||
cwrap(ident: string, returnType: string | null, argTypes: string[]): any
|
||||
|
||||
setValue(ptr: number, value: any, type: string, noSafe?: boolean): void
|
||||
getValue(ptr: number, type: string, noSafe?: boolean): number
|
||||
|
||||
ALLOC_NORMAL: number
|
||||
ALLOC_STACK: number
|
||||
ALLOC_STATIC: number
|
||||
ALLOC_DYNAMIC: number
|
||||
ALLOC_NONE: number
|
||||
|
||||
allocate(slab: any, types: string, allocator: number, ptr: number): number
|
||||
allocate(slab: any, types: string[], allocator: number, ptr: number): number
|
||||
|
||||
Pointer_stringify(ptr: number, length?: number): string
|
||||
UTF16ToString(ptr: number): string
|
||||
stringToUTF16(str: string, outPtr: number): void
|
||||
UTF32ToString(ptr: number): string
|
||||
stringToUTF32(str: string, outPtr: number): void
|
||||
|
||||
// USE_TYPED_ARRAYS == 1
|
||||
HEAP: Int32Array
|
||||
IHEAP: Int32Array
|
||||
FHEAP: Float64Array
|
||||
|
||||
// USE_TYPED_ARRAYS == 2
|
||||
HEAP8: Int8Array
|
||||
HEAP16: Int16Array
|
||||
HEAP32: Int32Array
|
||||
HEAPU8: Uint8Array
|
||||
HEAPU16: Uint16Array
|
||||
HEAPU32: Uint32Array
|
||||
HEAPF32: Float32Array
|
||||
HEAPF64: Float64Array
|
||||
|
||||
TOTAL_STACK: number
|
||||
TOTAL_MEMORY: number
|
||||
FAST_MEMORY: number
|
||||
|
||||
addOnPreRun(cb: () => any): void
|
||||
addOnInit(cb: () => any): void
|
||||
addOnPreMain(cb: () => any): void
|
||||
addOnExit(cb: () => any): void
|
||||
addOnPostRun(cb: () => any): void
|
||||
|
||||
// Tools
|
||||
intArrayFromString(
|
||||
stringy: string,
|
||||
dontAddNull?: boolean,
|
||||
length?: number
|
||||
): number[]
|
||||
intArrayToString(array: number[]): string
|
||||
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void
|
||||
writeArrayToMemory(array: number[], buffer: number): void
|
||||
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void
|
||||
|
||||
addRunDependency(id: any): void
|
||||
removeRunDependency(id: any): void
|
||||
|
||||
preloadedImages: any
|
||||
preloadedAudios: any
|
||||
|
||||
_malloc(size: number): number
|
||||
_free(ptr: number): void
|
||||
|
||||
// Augmentations below by @surma.
|
||||
onRuntimeInitialized: () => void | null
|
||||
}
|
||||
}
|
44
packages/astro/src/assets/services/vendor/squoosh/emscripten-utils.ts
vendored
Normal file
44
packages/astro/src/assets/services/vendor/squoosh/emscripten-utils.ts
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
export function pathify(path: string): string {
|
||||
if (path.startsWith('file://')) {
|
||||
path = fileURLToPath(path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
|
||||
factory: EmscriptenWasm.ModuleFactory<T>,
|
||||
path: string,
|
||||
workerJS = ''
|
||||
): Promise<T> {
|
||||
return factory({
|
||||
locateFile(requestPath) {
|
||||
// The glue code generated by emscripten uses the original
|
||||
// file names of the worker file and the wasm binary.
|
||||
// These will have changed in the bundling process and
|
||||
// we need to inject them here.
|
||||
if (requestPath.endsWith('.wasm')) return pathify(path)
|
||||
if (requestPath.endsWith('.worker.js')) return pathify(workerJS)
|
||||
return requestPath
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function dirname(url: string) {
|
||||
return url.substring(0, url.lastIndexOf('/'))
|
||||
}
|
||||
|
||||
/**
|
||||
* On certain serverless hosts, our ESM bundle is transpiled to CJS before being run, which means
|
||||
* import.meta.url is undefined, so we'll fall back to __dirname in those cases
|
||||
* We should be able to remove this once https://github.com/netlify/zip-it-and-ship-it/issues/750 is fixed
|
||||
*/
|
||||
export function getModuleURL(url: string | undefined): string {
|
||||
if (!url) {
|
||||
return pathToFileURL(__dirname).toString();
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
150
packages/astro/src/assets/services/vendor/squoosh/image-pool.ts
vendored
Normal file
150
packages/astro/src/assets/services/vendor/squoosh/image-pool.ts
vendored
Normal file
|
@ -0,0 +1,150 @@
|
|||
import { isMainThread } from 'node:worker_threads';
|
||||
import { cpus } from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { OutputFormat } from '../../../types.js';
|
||||
import execOnce from './utils/execOnce.js';
|
||||
import WorkerPool from './utils/workerPool.js';
|
||||
import { getModuleURL } from './emscripten-utils.js';
|
||||
import type { Operation } from './image.js';
|
||||
import * as impl from './impl.js';
|
||||
|
||||
const getWorker = execOnce(() => {
|
||||
return new WorkerPool(
|
||||
// There will be at most 7 workers needed since each worker will take
|
||||
// at least 1 operation type.
|
||||
Math.max(1, Math.min(cpus().length - 1, 7)),
|
||||
fileURLToPath(getModuleURL(import.meta.url))
|
||||
);
|
||||
});
|
||||
|
||||
type DecodeParams = {
|
||||
operation: 'decode';
|
||||
buffer: Buffer;
|
||||
};
|
||||
type ResizeParams = {
|
||||
operation: 'resize';
|
||||
imageData: ImageData;
|
||||
height?: number;
|
||||
width?: number;
|
||||
};
|
||||
type RotateParams = {
|
||||
operation: 'rotate';
|
||||
imageData: ImageData;
|
||||
numRotations: number;
|
||||
};
|
||||
type EncodeAvifParams = {
|
||||
operation: 'encodeavif';
|
||||
imageData: ImageData;
|
||||
quality: number;
|
||||
};
|
||||
type EncodeJpegParams = {
|
||||
operation: 'encodejpeg';
|
||||
imageData: ImageData;
|
||||
quality: number;
|
||||
};
|
||||
type EncodePngParams = {
|
||||
operation: 'encodepng';
|
||||
imageData: ImageData;
|
||||
};
|
||||
type EncodeWebpParams = {
|
||||
operation: 'encodewebp';
|
||||
imageData: ImageData;
|
||||
quality: number;
|
||||
};
|
||||
type JobMessage =
|
||||
| DecodeParams
|
||||
| ResizeParams
|
||||
| RotateParams
|
||||
| EncodeAvifParams
|
||||
| EncodeJpegParams
|
||||
| EncodePngParams
|
||||
| EncodeWebpParams;
|
||||
|
||||
function handleJob(params: JobMessage) {
|
||||
switch (params.operation) {
|
||||
case 'decode':
|
||||
return impl.decodeBuffer(params.buffer);
|
||||
case 'resize':
|
||||
return impl.resize({
|
||||
image: params.imageData as any,
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
});
|
||||
case 'rotate':
|
||||
return impl.rotate(params.imageData as any, params.numRotations);
|
||||
case 'encodeavif':
|
||||
return impl.encodeAvif(params.imageData as any, { quality: params.quality });
|
||||
case 'encodejpeg':
|
||||
return impl.encodeJpeg(params.imageData as any, { quality: params.quality });
|
||||
case 'encodepng':
|
||||
return impl.encodePng(params.imageData as any);
|
||||
case 'encodewebp':
|
||||
return impl.encodeWebp(params.imageData as any, { quality: params.quality });
|
||||
default:
|
||||
throw Error(`Invalid job "${(params as any).operation}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function processBuffer(
|
||||
buffer: Buffer,
|
||||
operations: Operation[],
|
||||
encoding: OutputFormat,
|
||||
quality?: number
|
||||
): Promise<Uint8Array> {
|
||||
// @ts-ignore
|
||||
const worker = await getWorker();
|
||||
|
||||
let imageData = await worker.dispatchJob({
|
||||
operation: 'decode',
|
||||
buffer,
|
||||
});
|
||||
for (const operation of operations) {
|
||||
if (operation.type === 'rotate') {
|
||||
imageData = await worker.dispatchJob({
|
||||
operation: 'rotate',
|
||||
imageData,
|
||||
numRotations: operation.numRotations,
|
||||
});
|
||||
} else if (operation.type === 'resize') {
|
||||
imageData = await worker.dispatchJob({
|
||||
operation: 'resize',
|
||||
imageData,
|
||||
height: operation.height,
|
||||
width: operation.width,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (encoding) {
|
||||
case 'avif':
|
||||
return (await worker.dispatchJob({
|
||||
operation: 'encodeavif',
|
||||
imageData,
|
||||
quality,
|
||||
})) as Uint8Array;
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
return (await worker.dispatchJob({
|
||||
operation: 'encodejpeg',
|
||||
imageData,
|
||||
quality,
|
||||
})) as Uint8Array;
|
||||
case 'png':
|
||||
return (await worker.dispatchJob({
|
||||
operation: 'encodepng',
|
||||
imageData,
|
||||
})) as Uint8Array;
|
||||
case 'webp':
|
||||
return (await worker.dispatchJob({
|
||||
operation: 'encodewebp',
|
||||
imageData,
|
||||
quality,
|
||||
})) as Uint8Array;
|
||||
default:
|
||||
throw Error(`Unsupported encoding format`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMainThread) {
|
||||
WorkerPool.useThisThreadAsWorker(handleJob);
|
||||
}
|
43
packages/astro/src/assets/services/vendor/squoosh/image.ts
vendored
Normal file
43
packages/astro/src/assets/services/vendor/squoosh/image.ts
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { OutputFormat } from '../../../types.js';
|
||||
import * as impl from './impl.js';
|
||||
|
||||
type RotateOperation = {
|
||||
type: 'rotate'
|
||||
numRotations: number
|
||||
}
|
||||
type ResizeOperation = {
|
||||
type: 'resize'
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
export type Operation = RotateOperation | ResizeOperation
|
||||
|
||||
export async function processBuffer(
|
||||
buffer: Buffer,
|
||||
operations: Operation[],
|
||||
encoding: OutputFormat,
|
||||
quality?: number
|
||||
): Promise<Uint8Array> {
|
||||
let imageData = await impl.decodeBuffer(buffer)
|
||||
for (const operation of operations) {
|
||||
if (operation.type === 'rotate') {
|
||||
imageData = await impl.rotate(imageData, operation.numRotations);
|
||||
} else if (operation.type === 'resize') {
|
||||
imageData = await impl.resize({ image: imageData, width: operation.width, height: operation.height })
|
||||
}
|
||||
}
|
||||
|
||||
switch (encoding) {
|
||||
case 'avif':
|
||||
return await impl.encodeAvif(imageData, { quality }) as Uint8Array;
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
return await impl.encodeJpeg(imageData, { quality }) as Uint8Array;
|
||||
case 'png':
|
||||
return await impl.encodePng(imageData) as Uint8Array;
|
||||
case 'webp':
|
||||
return await impl.encodeWebp(imageData, { quality }) as Uint8Array;
|
||||
default:
|
||||
throw Error(`Unsupported encoding format`)
|
||||
}
|
||||
}
|
33
packages/astro/src/assets/services/vendor/squoosh/image_data.ts
vendored
Normal file
33
packages/astro/src/assets/services/vendor/squoosh/image_data.ts
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
export default class ImageData {
|
||||
static from(input: ImageData): ImageData {
|
||||
return new ImageData(input.data || input._data, input.width, input.height)
|
||||
}
|
||||
|
||||
private _data: Buffer | Uint8Array | Uint8ClampedArray
|
||||
width: number
|
||||
height: number
|
||||
|
||||
get data(): Buffer {
|
||||
if (Object.prototype.toString.call(this._data) === '[object Object]') {
|
||||
return Buffer.from(Object.values(this._data))
|
||||
}
|
||||
if (
|
||||
this._data instanceof Buffer ||
|
||||
this._data instanceof Uint8Array ||
|
||||
this._data instanceof Uint8ClampedArray
|
||||
) {
|
||||
return Buffer.from(this._data)
|
||||
}
|
||||
throw new Error('invariant')
|
||||
}
|
||||
|
||||
constructor(
|
||||
data: Buffer | Uint8Array | Uint8ClampedArray,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
this._data = data
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
}
|
143
packages/astro/src/assets/services/vendor/squoosh/impl.ts
vendored
Normal file
143
packages/astro/src/assets/services/vendor/squoosh/impl.ts
vendored
Normal file
|
@ -0,0 +1,143 @@
|
|||
import { codecs as supportedFormats, preprocessors } from './codecs.js'
|
||||
import ImageData from './image_data.js'
|
||||
|
||||
type EncoderKey = keyof typeof supportedFormats
|
||||
|
||||
const DELAY_MS = 1000
|
||||
let _promise: Promise<void> | undefined
|
||||
|
||||
function delayOnce(ms: number): Promise<void> {
|
||||
if (!_promise) {
|
||||
_promise = new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
return _promise
|
||||
}
|
||||
|
||||
function maybeDelay(): Promise<void> {
|
||||
const isAppleM1 = process.arch === 'arm64' && process.platform === 'darwin'
|
||||
if (isAppleM1) {
|
||||
return delayOnce(DELAY_MS)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export async function decodeBuffer(
|
||||
_buffer: Buffer | Uint8Array
|
||||
): Promise<ImageData> {
|
||||
const buffer = Buffer.from(_buffer)
|
||||
const firstChunk = buffer.slice(0, 16)
|
||||
const firstChunkString = Array.from(firstChunk)
|
||||
.map((v) => String.fromCodePoint(v))
|
||||
.join('')
|
||||
// TODO (future PR): support more formats
|
||||
if (firstChunkString.includes('GIF')) {
|
||||
throw Error(`GIF images are not supported, please install the @astrojs/image/sharp plugin`)
|
||||
}
|
||||
const key = Object.entries(supportedFormats).find(([, { detectors }]) =>
|
||||
detectors.some((detector) => detector.exec(firstChunkString))
|
||||
)?.[0] as EncoderKey | undefined
|
||||
if (!key) {
|
||||
throw Error(`Buffer has an unsupported format`)
|
||||
}
|
||||
const encoder = supportedFormats[key]
|
||||
const mod = await encoder.dec()
|
||||
const rgba = mod.decode(new Uint8Array(buffer))
|
||||
// @ts-ignore
|
||||
return rgba
|
||||
}
|
||||
|
||||
export async function rotate(
|
||||
image: ImageData,
|
||||
numRotations: number
|
||||
): Promise<ImageData> {
|
||||
image = ImageData.from(image)
|
||||
|
||||
const m = await preprocessors['rotate'].instantiate()
|
||||
return await m(image.data, image.width, image.height, { numRotations })
|
||||
}
|
||||
|
||||
type ResizeOpts = { image: ImageData } & { width?: number; height?: number }
|
||||
|
||||
export async function resize({ image, width, height }: ResizeOpts) {
|
||||
image = ImageData.from(image)
|
||||
|
||||
const p = preprocessors['resize']
|
||||
const m = await p.instantiate()
|
||||
await maybeDelay()
|
||||
return await m(image.data, image.width, image.height, {
|
||||
...p.defaultOptions,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
export async function encodeJpeg(
|
||||
image: ImageData,
|
||||
opts: { quality?: number }
|
||||
): Promise<Uint8Array> {
|
||||
image = ImageData.from(image)
|
||||
|
||||
const e = supportedFormats['mozjpeg']
|
||||
const m = await e.enc()
|
||||
await maybeDelay()
|
||||
const quality = opts.quality || e.defaultEncoderOptions.quality
|
||||
const r = await m.encode(image.data, image.width, image.height, {
|
||||
...e.defaultEncoderOptions,
|
||||
quality,
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
export async function encodeWebp(
|
||||
image: ImageData,
|
||||
opts: { quality?: number }
|
||||
): Promise<Uint8Array> {
|
||||
image = ImageData.from(image)
|
||||
|
||||
const e = supportedFormats['webp']
|
||||
const m = await e.enc()
|
||||
await maybeDelay()
|
||||
const quality = opts.quality || e.defaultEncoderOptions.quality
|
||||
const r = await m.encode(image.data, image.width, image.height, {
|
||||
...e.defaultEncoderOptions,
|
||||
quality,
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
export async function encodeAvif(
|
||||
image: ImageData,
|
||||
opts: { quality?: number }
|
||||
): Promise<Uint8Array> {
|
||||
image = ImageData.from(image)
|
||||
|
||||
const e = supportedFormats['avif']
|
||||
const m = await e.enc()
|
||||
await maybeDelay()
|
||||
const val = e.autoOptimize.min
|
||||
// AVIF doesn't use a 0-100 quality, default to 75 and convert to cqLevel below
|
||||
const quality = opts.quality || 75
|
||||
const r = await m.encode(image.data, image.width, image.height, {
|
||||
...e.defaultEncoderOptions,
|
||||
// Think of cqLevel as the "amount" of quantization (0 to 62),
|
||||
// so a lower value yields higher quality (0 to 100).
|
||||
cqLevel: quality === 0 ? val : Math.round(val - (quality / 100) * val),
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
export async function encodePng(
|
||||
image: ImageData
|
||||
): Promise<Uint8Array> {
|
||||
image = ImageData.from(image)
|
||||
|
||||
const e = supportedFormats['oxipng']
|
||||
const m = await e.enc()
|
||||
await maybeDelay()
|
||||
const r = await m.encode(image.data, image.width, image.height, {
|
||||
...e.defaultEncoderOptions,
|
||||
})
|
||||
return r
|
||||
}
|
38
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_enc.d.ts
vendored
Normal file
38
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_enc.d.ts
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
// eslint-disable-next-line no-shadow
|
||||
export const enum MozJpegColorSpace {
|
||||
GRAYSCALE = 1,
|
||||
RGB,
|
||||
YCbCr,
|
||||
}
|
||||
|
||||
export interface EncodeOptions {
|
||||
quality: number
|
||||
baseline: boolean
|
||||
arithmetic: boolean
|
||||
progressive: boolean
|
||||
optimize_coding: boolean
|
||||
smoothing: number
|
||||
color_space: MozJpegColorSpace
|
||||
quant_table: number
|
||||
trellis_multipass: boolean
|
||||
trellis_opt_zero: boolean
|
||||
trellis_opt_table: boolean
|
||||
trellis_loops: number
|
||||
auto_subsample: boolean
|
||||
chroma_subsample: number
|
||||
separate_chroma_quality: boolean
|
||||
chroma_quality: number
|
||||
}
|
||||
|
||||
export interface MozJPEGModule extends EmscriptenWasm.Module {
|
||||
encode(
|
||||
data: BufferSource,
|
||||
width: number,
|
||||
height: number,
|
||||
options: EncodeOptions
|
||||
): Uint8Array
|
||||
}
|
||||
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<MozJPEGModule>
|
||||
|
||||
export default moduleFactory
|
1775
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_node_dec.ts
vendored
Normal file
1775
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_node_dec.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
BIN
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_node_dec.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_node_dec.wasm
vendored
Normal file
Binary file not shown.
1901
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_node_enc.ts
vendored
Normal file
1901
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_node_enc.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
BIN
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_node_enc.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/mozjpeg/mozjpeg_node_enc.wasm
vendored
Normal file
Binary file not shown.
120
packages/astro/src/assets/services/vendor/squoosh/png/squoosh_oxipng.ts
vendored
Normal file
120
packages/astro/src/assets/services/vendor/squoosh/png/squoosh_oxipng.ts
vendored
Normal file
|
@ -0,0 +1,120 @@
|
|||
// @ts-nocheck
|
||||
let wasm
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', {
|
||||
ignoreBOM: true,
|
||||
fatal: true,
|
||||
})
|
||||
|
||||
cachedTextDecoder.decode()
|
||||
|
||||
let cachegetUint8Memory0 = null
|
||||
function getUint8Memory0() {
|
||||
if (
|
||||
cachegetUint8Memory0 === null ||
|
||||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
|
||||
) {
|
||||
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
|
||||
}
|
||||
return cachegetUint8Memory0
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len))
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1)
|
||||
getUint8Memory0().set(arg, ptr / 1)
|
||||
WASM_VECTOR_LEN = arg.length
|
||||
return ptr
|
||||
}
|
||||
|
||||
let cachegetInt32Memory0 = null
|
||||
function getInt32Memory0() {
|
||||
if (
|
||||
cachegetInt32Memory0 === null ||
|
||||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
|
||||
) {
|
||||
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
|
||||
}
|
||||
return cachegetInt32Memory0
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len)
|
||||
}
|
||||
/**
|
||||
* @param {Uint8Array} data
|
||||
* @param {number} level
|
||||
* @param {boolean} interlace
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function optimise(data, level, interlace) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
|
||||
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
|
||||
const len0 = WASM_VECTOR_LEN
|
||||
wasm.optimise(retptr, ptr0, len0, level, interlace)
|
||||
const r0 = getInt32Memory0()[retptr / 4 + 0]
|
||||
const r1 = getInt32Memory0()[retptr / 4 + 1]
|
||||
const v1 = getArrayU8FromWasm0(r0, r1).slice()
|
||||
wasm.__wbindgen_free(r0, r1 * 1)
|
||||
return v1
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16)
|
||||
}
|
||||
}
|
||||
|
||||
async function load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
return await WebAssembly.instantiateStreaming(module, imports)
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer()
|
||||
return await WebAssembly.instantiate(bytes, imports)
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports)
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module }
|
||||
} else {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function init(input) {
|
||||
const imports = {}
|
||||
imports.wbg = {}
|
||||
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1))
|
||||
}
|
||||
|
||||
if (
|
||||
typeof input === 'string' ||
|
||||
(typeof Request === 'function' && input instanceof Request) ||
|
||||
(typeof URL === 'function' && input instanceof URL)
|
||||
) {
|
||||
input = fetch(input)
|
||||
}
|
||||
|
||||
const { instance, module } = await load(await input, imports)
|
||||
|
||||
wasm = instance.exports
|
||||
init.__wbindgen_wasm_module = module
|
||||
|
||||
return wasm
|
||||
}
|
||||
|
||||
export default init
|
||||
|
||||
// Manually remove the wasm and memory references to trigger GC
|
||||
export function cleanup() {
|
||||
wasm = null
|
||||
cachegetUint8Memory0 = null
|
||||
cachegetInt32Memory0 = null
|
||||
}
|
BIN
packages/astro/src/assets/services/vendor/squoosh/png/squoosh_oxipng_bg.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/png/squoosh_oxipng_bg.wasm
vendored
Normal file
Binary file not shown.
184
packages/astro/src/assets/services/vendor/squoosh/png/squoosh_png.ts
vendored
Normal file
184
packages/astro/src/assets/services/vendor/squoosh/png/squoosh_png.ts
vendored
Normal file
|
@ -0,0 +1,184 @@
|
|||
// @ts-nocheck
|
||||
let wasm
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', {
|
||||
ignoreBOM: true,
|
||||
fatal: true,
|
||||
})
|
||||
|
||||
cachedTextDecoder.decode()
|
||||
|
||||
let cachegetUint8Memory0 = null
|
||||
function getUint8Memory0() {
|
||||
if (
|
||||
cachegetUint8Memory0 === null ||
|
||||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
|
||||
) {
|
||||
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
|
||||
}
|
||||
return cachegetUint8Memory0
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len))
|
||||
}
|
||||
|
||||
let cachegetUint8ClampedMemory0 = null
|
||||
function getUint8ClampedMemory0() {
|
||||
if (
|
||||
cachegetUint8ClampedMemory0 === null ||
|
||||
cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer
|
||||
) {
|
||||
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer)
|
||||
}
|
||||
return cachegetUint8ClampedMemory0
|
||||
}
|
||||
|
||||
function getClampedArrayU8FromWasm0(ptr, len) {
|
||||
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len)
|
||||
}
|
||||
|
||||
const heap = new Array(32).fill(undefined)
|
||||
|
||||
heap.push(undefined, null, true, false)
|
||||
|
||||
let heap_next = heap.length
|
||||
|
||||
function addHeapObject(obj) {
|
||||
if (heap_next === heap.length) heap.push(heap.length + 1)
|
||||
const idx = heap_next
|
||||
heap_next = heap[idx]
|
||||
|
||||
heap[idx] = obj
|
||||
return idx
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1)
|
||||
getUint8Memory0().set(arg, ptr / 1)
|
||||
WASM_VECTOR_LEN = arg.length
|
||||
return ptr
|
||||
}
|
||||
|
||||
let cachegetInt32Memory0 = null
|
||||
function getInt32Memory0() {
|
||||
if (
|
||||
cachegetInt32Memory0 === null ||
|
||||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
|
||||
) {
|
||||
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
|
||||
}
|
||||
return cachegetInt32Memory0
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len)
|
||||
}
|
||||
/**
|
||||
* @param {Uint8Array} data
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function encode(data, width, height) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
|
||||
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
|
||||
const len0 = WASM_VECTOR_LEN
|
||||
wasm.encode(retptr, ptr0, len0, width, height)
|
||||
const r0 = getInt32Memory0()[retptr / 4 + 0]
|
||||
const r1 = getInt32Memory0()[retptr / 4 + 1]
|
||||
const v1 = getArrayU8FromWasm0(r0, r1).slice()
|
||||
wasm.__wbindgen_free(r0, r1 * 1)
|
||||
return v1
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16)
|
||||
}
|
||||
}
|
||||
|
||||
function getObject(idx) {
|
||||
return heap[idx]
|
||||
}
|
||||
|
||||
function dropObject(idx) {
|
||||
if (idx < 36) return
|
||||
heap[idx] = heap_next
|
||||
heap_next = idx
|
||||
}
|
||||
|
||||
function takeObject(idx) {
|
||||
const ret = getObject(idx)
|
||||
dropObject(idx)
|
||||
return ret
|
||||
}
|
||||
/**
|
||||
* @param {Uint8Array} data
|
||||
* @returns {ImageData}
|
||||
*/
|
||||
export function decode(data) {
|
||||
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
|
||||
const len0 = WASM_VECTOR_LEN
|
||||
const ret = wasm.decode(ptr0, len0)
|
||||
return takeObject(ret)
|
||||
}
|
||||
|
||||
async function load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
return await WebAssembly.instantiateStreaming(module, imports)
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer()
|
||||
return await WebAssembly.instantiate(bytes, imports)
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports)
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module }
|
||||
} else {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function init(input) {
|
||||
const imports = {}
|
||||
imports.wbg = {}
|
||||
imports.wbg.__wbg_newwithownedu8clampedarrayandsh_787b2db8ea6bfd62 =
|
||||
function (arg0, arg1, arg2, arg3) {
|
||||
const v0 = getClampedArrayU8FromWasm0(arg0, arg1).slice()
|
||||
wasm.__wbindgen_free(arg0, arg1 * 1)
|
||||
const ret = new ImageData(v0, arg2 >>> 0, arg3 >>> 0)
|
||||
return addHeapObject(ret)
|
||||
}
|
||||
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1))
|
||||
}
|
||||
|
||||
if (
|
||||
typeof input === 'string' ||
|
||||
(typeof Request === 'function' && input instanceof Request) ||
|
||||
(typeof URL === 'function' && input instanceof URL)
|
||||
) {
|
||||
input = fetch(input)
|
||||
}
|
||||
|
||||
const { instance, module } = await load(await input, imports)
|
||||
|
||||
wasm = instance.exports
|
||||
init.__wbindgen_wasm_module = module
|
||||
|
||||
return wasm
|
||||
}
|
||||
|
||||
export default init
|
||||
|
||||
// Manually remove the wasm and memory references to trigger GC
|
||||
export function cleanup() {
|
||||
wasm = null
|
||||
cachegetUint8ClampedMemory0 = null
|
||||
cachegetUint8Memory0 = null
|
||||
cachegetInt32Memory0 = null
|
||||
}
|
BIN
packages/astro/src/assets/services/vendor/squoosh/png/squoosh_png_bg.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/png/squoosh_png_bg.wasm
vendored
Normal file
Binary file not shown.
141
packages/astro/src/assets/services/vendor/squoosh/resize/squoosh_resize.ts
vendored
Normal file
141
packages/astro/src/assets/services/vendor/squoosh/resize/squoosh_resize.ts
vendored
Normal file
|
@ -0,0 +1,141 @@
|
|||
// @ts-nocheck
|
||||
let wasm
|
||||
|
||||
let cachegetUint8Memory0 = null
|
||||
function getUint8Memory0() {
|
||||
if (
|
||||
cachegetUint8Memory0 === null ||
|
||||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
|
||||
) {
|
||||
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
|
||||
}
|
||||
return cachegetUint8Memory0
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1)
|
||||
getUint8Memory0().set(arg, ptr / 1)
|
||||
WASM_VECTOR_LEN = arg.length
|
||||
return ptr
|
||||
}
|
||||
|
||||
let cachegetInt32Memory0 = null
|
||||
function getInt32Memory0() {
|
||||
if (
|
||||
cachegetInt32Memory0 === null ||
|
||||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
|
||||
) {
|
||||
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
|
||||
}
|
||||
return cachegetInt32Memory0
|
||||
}
|
||||
|
||||
let cachegetUint8ClampedMemory0 = null
|
||||
function getUint8ClampedMemory0() {
|
||||
if (
|
||||
cachegetUint8ClampedMemory0 === null ||
|
||||
cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer
|
||||
) {
|
||||
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer)
|
||||
}
|
||||
return cachegetUint8ClampedMemory0
|
||||
}
|
||||
|
||||
function getClampedArrayU8FromWasm0(ptr, len) {
|
||||
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len)
|
||||
}
|
||||
/**
|
||||
* @param {Uint8Array} input_image
|
||||
* @param {number} input_width
|
||||
* @param {number} input_height
|
||||
* @param {number} output_width
|
||||
* @param {number} output_height
|
||||
* @param {number} typ_idx
|
||||
* @param {boolean} premultiply
|
||||
* @param {boolean} color_space_conversion
|
||||
* @returns {Uint8ClampedArray}
|
||||
*/
|
||||
export function resize(
|
||||
input_image,
|
||||
input_width,
|
||||
input_height,
|
||||
output_width,
|
||||
output_height,
|
||||
typ_idx,
|
||||
premultiply,
|
||||
color_space_conversion
|
||||
) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
|
||||
const ptr0 = passArray8ToWasm0(input_image, wasm.__wbindgen_malloc)
|
||||
const len0 = WASM_VECTOR_LEN
|
||||
wasm.resize(
|
||||
retptr,
|
||||
ptr0,
|
||||
len0,
|
||||
input_width,
|
||||
input_height,
|
||||
output_width,
|
||||
output_height,
|
||||
typ_idx,
|
||||
premultiply,
|
||||
color_space_conversion
|
||||
)
|
||||
const r0 = getInt32Memory0()[retptr / 4 + 0]
|
||||
const r1 = getInt32Memory0()[retptr / 4 + 1]
|
||||
const v1 = getClampedArrayU8FromWasm0(r0, r1).slice()
|
||||
wasm.__wbindgen_free(r0, r1 * 1)
|
||||
return v1
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16)
|
||||
}
|
||||
}
|
||||
|
||||
async function load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
return await WebAssembly.instantiateStreaming(module, imports)
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer()
|
||||
return await WebAssembly.instantiate(bytes, imports)
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports)
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module }
|
||||
} else {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function init(input) {
|
||||
const imports = {}
|
||||
|
||||
if (
|
||||
typeof input === 'string' ||
|
||||
(typeof Request === 'function' && input instanceof Request) ||
|
||||
(typeof URL === 'function' && input instanceof URL)
|
||||
) {
|
||||
input = fetch(input)
|
||||
}
|
||||
|
||||
const { instance, module } = await load(await input, imports)
|
||||
|
||||
wasm = instance.exports
|
||||
init.__wbindgen_wasm_module = module
|
||||
|
||||
return wasm
|
||||
}
|
||||
|
||||
export default init
|
||||
|
||||
// Manually remove the wasm and memory references to trigger GC
|
||||
export function cleanup() {
|
||||
wasm = null
|
||||
cachegetUint8Memory0 = null
|
||||
cachegetInt32Memory0 = null
|
||||
}
|
BIN
packages/astro/src/assets/services/vendor/squoosh/resize/squoosh_resize_bg.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/resize/squoosh_resize_bg.wasm
vendored
Normal file
Binary file not shown.
BIN
packages/astro/src/assets/services/vendor/squoosh/rotate/rotate.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/rotate/rotate.wasm
vendored
Normal file
Binary file not shown.
12
packages/astro/src/assets/services/vendor/squoosh/utils/execOnce.ts
vendored
Normal file
12
packages/astro/src/assets/services/vendor/squoosh/utils/execOnce.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default function execOnce<T extends (...args: any[]) => ReturnType<T>>(fn: T): T {
|
||||
let used = false;
|
||||
let result: ReturnType<T>;
|
||||
|
||||
return ((...args: any[]) => {
|
||||
if (!used) {
|
||||
used = true;
|
||||
result = fn(...args);
|
||||
}
|
||||
return result;
|
||||
}) as T;
|
||||
}
|
122
packages/astro/src/assets/services/vendor/squoosh/utils/workerPool.ts
vendored
Normal file
122
packages/astro/src/assets/services/vendor/squoosh/utils/workerPool.ts
vendored
Normal file
|
@ -0,0 +1,122 @@
|
|||
/* tslint-disable ban-types */
|
||||
import { parentPort, Worker } from 'worker_threads';
|
||||
|
||||
function uuid() {
|
||||
return Array.from({ length: 16 }, () => Math.floor(Math.random() * 256).toString(16)).join('');
|
||||
}
|
||||
|
||||
interface Job<I> {
|
||||
msg: I;
|
||||
resolve: (result: any) => void;
|
||||
reject: (reason: any) => void;
|
||||
}
|
||||
|
||||
export default class WorkerPool<I, O> {
|
||||
public numWorkers: number;
|
||||
public jobQueue: TransformStream<Job<I>, Job<I>>;
|
||||
public workerQueue: TransformStream<Worker, Worker>;
|
||||
public done: Promise<void>;
|
||||
|
||||
constructor(numWorkers: number, workerFile: string) {
|
||||
this.numWorkers = numWorkers;
|
||||
this.jobQueue = new TransformStream();
|
||||
this.workerQueue = new TransformStream();
|
||||
|
||||
const writer = this.workerQueue.writable.getWriter();
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
writer.write(new Worker(workerFile));
|
||||
}
|
||||
writer.releaseLock();
|
||||
|
||||
this.done = this._readLoop();
|
||||
}
|
||||
|
||||
async _readLoop() {
|
||||
const reader = this.jobQueue.readable.getReader();
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
await this._terminateAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
throw new Error('Reader did not return any value');
|
||||
}
|
||||
|
||||
const { msg, resolve, reject } = value;
|
||||
const worker = await this._nextWorker();
|
||||
this.jobPromise(worker, msg)
|
||||
.then((result) => resolve(result))
|
||||
.catch((reason) => reject(reason))
|
||||
.finally(() => {
|
||||
// Return the worker to the pool
|
||||
const writer = this.workerQueue.writable.getWriter();
|
||||
writer.write(worker);
|
||||
writer.releaseLock();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _nextWorker() {
|
||||
const reader = this.workerQueue.readable.getReader();
|
||||
const { value } = await reader.read();
|
||||
reader.releaseLock();
|
||||
if (!value) {
|
||||
throw new Error('No worker left');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async _terminateAll() {
|
||||
for (let n = 0; n < this.numWorkers; n++) {
|
||||
const worker = await this._nextWorker();
|
||||
worker.terminate();
|
||||
}
|
||||
this.workerQueue.writable.close();
|
||||
}
|
||||
|
||||
async join() {
|
||||
this.jobQueue.writable.getWriter().close();
|
||||
await this.done;
|
||||
}
|
||||
|
||||
dispatchJob(msg: I): Promise<O> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const writer = this.jobQueue.writable.getWriter();
|
||||
writer.write({ msg, resolve, reject });
|
||||
writer.releaseLock();
|
||||
});
|
||||
}
|
||||
|
||||
private jobPromise(worker: Worker, msg: I) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = uuid();
|
||||
worker.postMessage({ msg, id });
|
||||
worker.on('message', function f({ error, result, id: rid }) {
|
||||
if (rid !== id) {
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
worker.off('message', f);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static useThisThreadAsWorker<I, O>(cb: (msg: I) => O) {
|
||||
parentPort!.on('message', async (data) => {
|
||||
const { msg, id } = data;
|
||||
try {
|
||||
const result = await cb(msg);
|
||||
parentPort!.postMessage({ result, id });
|
||||
} catch (e: any) {
|
||||
parentPort!.postMessage({ error: e.message, id });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
42
packages/astro/src/assets/services/vendor/squoosh/webp/webp_enc.d.ts
vendored
Normal file
42
packages/astro/src/assets/services/vendor/squoosh/webp/webp_enc.d.ts
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
export interface EncodeOptions {
|
||||
quality: number
|
||||
target_size: number
|
||||
target_PSNR: number
|
||||
method: number
|
||||
sns_strength: number
|
||||
filter_strength: number
|
||||
filter_sharpness: number
|
||||
filter_type: number
|
||||
partitions: number
|
||||
segments: number
|
||||
pass: number
|
||||
show_compressed: number
|
||||
preprocessing: number
|
||||
autofilter: number
|
||||
partition_limit: number
|
||||
alpha_compression: number
|
||||
alpha_filtering: number
|
||||
alpha_quality: number
|
||||
lossless: number
|
||||
exact: number
|
||||
image_hint: number
|
||||
emulate_jpeg_size: number
|
||||
thread_level: number
|
||||
low_memory: number
|
||||
near_lossless: number
|
||||
use_delta_palette: number
|
||||
use_sharp_yuv: number
|
||||
}
|
||||
|
||||
export interface WebPModule extends EmscriptenWasm.Module {
|
||||
encode(
|
||||
data: BufferSource,
|
||||
width: number,
|
||||
height: number,
|
||||
options: EncodeOptions
|
||||
): Uint8Array
|
||||
}
|
||||
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<WebPModule>
|
||||
|
||||
export default moduleFactory
|
1614
packages/astro/src/assets/services/vendor/squoosh/webp/webp_node_dec.ts
vendored
Normal file
1614
packages/astro/src/assets/services/vendor/squoosh/webp/webp_node_dec.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
BIN
packages/astro/src/assets/services/vendor/squoosh/webp/webp_node_dec.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/webp/webp_node_dec.wasm
vendored
Normal file
Binary file not shown.
1799
packages/astro/src/assets/services/vendor/squoosh/webp/webp_node_enc.ts
vendored
Normal file
1799
packages/astro/src/assets/services/vendor/squoosh/webp/webp_node_enc.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
BIN
packages/astro/src/assets/services/vendor/squoosh/webp/webp_node_enc.wasm
vendored
Normal file
BIN
packages/astro/src/assets/services/vendor/squoosh/webp/webp_node_enc.wasm
vendored
Normal file
Binary file not shown.
125
packages/astro/src/assets/types.ts
Normal file
125
packages/astro/src/assets/types.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import { VALID_INPUT_FORMATS, VALID_OUTPUT_FORMATS } from './consts.js';
|
||||
import { ImageService } from './services/service.js';
|
||||
|
||||
export type ImageQualityPreset = 'low' | 'mid' | 'high' | 'max' | (string & {});
|
||||
export type ImageQuality = ImageQualityPreset | number;
|
||||
export type InputFormat = (typeof VALID_INPUT_FORMATS)[number] | (string & {});
|
||||
export type OutputFormat = (typeof VALID_OUTPUT_FORMATS)[number] | (string & {});
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var astroAsset: {
|
||||
imageService?: ImageService;
|
||||
addStaticImage?: ((options: ImageTransform) => string) | undefined;
|
||||
staticImages?: Map<ImageTransform, string>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type returned by ESM imports of images and direct calls to imageMetadata
|
||||
*/
|
||||
export interface ImageMetadata {
|
||||
src: string;
|
||||
width: number;
|
||||
height: number;
|
||||
format: InputFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options accepted by the image transformation service.
|
||||
*/
|
||||
export type ImageTransform = {
|
||||
src: ImageMetadata | string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
quality?: ImageQuality;
|
||||
format?: OutputFormat;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
type ImageSharedProps<T> = T & {
|
||||
/**
|
||||
* Width of the image, the value of this property will be used to assign the `width` property on the final `img` element.
|
||||
*
|
||||
* For local images, this value will additionally be used to resize the image to the desired width, taking into account the original aspect ratio of the image.
|
||||
*
|
||||
* **Example**:
|
||||
* ```astro
|
||||
* <Image src={...} width={300} alt="..." />
|
||||
* ```
|
||||
* **Result**:
|
||||
* ```html
|
||||
* <img src="..." width="300" height="..." alt="..." />
|
||||
* ```
|
||||
*/
|
||||
width?: number | `${number}`;
|
||||
/**
|
||||
* Height of the image, the value of this property will be used to assign the `height` property on the final `img` element.
|
||||
*
|
||||
* For local images, if `width` is not present, this value will additionally be used to resize the image to the desired height, taking into account the original aspect ratio of the image.
|
||||
*
|
||||
* **Example**:
|
||||
* ```astro
|
||||
* <Image src={...} height={300} alt="..." />
|
||||
* ```
|
||||
* **Result**:
|
||||
* ```html
|
||||
* <img src="..." height="300" width="..." alt="..." />
|
||||
* ```
|
||||
*/
|
||||
height?: number | `${number}`;
|
||||
};
|
||||
|
||||
export type LocalImageProps<T> = ImageSharedProps<T> & {
|
||||
/**
|
||||
* A reference to a local image imported through an ESM import.
|
||||
*
|
||||
* **Example**:
|
||||
* ```js
|
||||
* import myImage from "~/assets/my_image.png";
|
||||
* ```
|
||||
* And then refer to the image, like so:
|
||||
* ```astro
|
||||
* <Image src={myImage} alt="..."></Image>
|
||||
* ```
|
||||
*/
|
||||
src: ImageMetadata;
|
||||
/**
|
||||
* Desired output format for the image. Defaults to `webp`.
|
||||
*
|
||||
* **Example**:
|
||||
* ```astro
|
||||
* <Image src={...} format="avif" alt="..." />
|
||||
* ```
|
||||
*/
|
||||
format?: OutputFormat;
|
||||
/**
|
||||
* Desired quality for the image. Value can either be a preset such as `low` or `high`, or a numeric value from 0 to 100.
|
||||
*
|
||||
* The perceptual quality of the output image is loader-specific.
|
||||
* For instance, a certain service might decide that `high` results in a very beautiful image, but another could choose for it to be at best passable.
|
||||
*
|
||||
* **Example**:
|
||||
* ```astro
|
||||
* <Image src={...} quality='high' alt="..." />
|
||||
* <Image src={...} quality={300} alt="..." />
|
||||
* ```
|
||||
*/
|
||||
quality?: ImageQuality;
|
||||
};
|
||||
|
||||
export type RemoteImageProps<T> = WithRequired<ImageSharedProps<T>, 'width' | 'height'> & {
|
||||
/**
|
||||
* URL of a remote image. Can start with a protocol (ex: `https://`) or alternatively `/`, or `Astro.url`, for images in the `public` folder
|
||||
*
|
||||
* Remote images are not optimized, and require both `width` and `height` to be set.
|
||||
*
|
||||
* **Example**:
|
||||
* ```
|
||||
* <Image src="https://example.com/image.png" width={450} height={300} alt="..." />
|
||||
* ```
|
||||
*/
|
||||
src: string;
|
||||
};
|
45
packages/astro/src/assets/utils/etag.ts
Normal file
45
packages/astro/src/assets/utils/etag.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* FNV-1a Hash implementation
|
||||
* @author Travis Webb (tjwebb) <me@traviswebb.com>
|
||||
*
|
||||
* Ported from https://github.com/tjwebb/fnv-plus/blob/master/index.js
|
||||
* License https://github.com/tjwebb/fnv-plus#license
|
||||
*
|
||||
* 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) + '"';
|
||||
};
|
39
packages/astro/src/assets/utils/metadata.ts
Normal file
39
packages/astro/src/assets/utils/metadata.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { createRequire } from 'module';
|
||||
import fs from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { ImageMetadata, InputFormat } from '../types.js';
|
||||
const require = createRequire(import.meta.url);
|
||||
const sizeOf = require('image-size');
|
||||
|
||||
export interface Metadata extends ImageMetadata {
|
||||
orientation?: number;
|
||||
}
|
||||
|
||||
export async function imageMetadata(
|
||||
src: URL | string,
|
||||
data?: Buffer
|
||||
): Promise<Metadata | undefined> {
|
||||
let file = data;
|
||||
if (!file) {
|
||||
try {
|
||||
file = await fs.readFile(src);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const { width, height, type, orientation } = await sizeOf(file);
|
||||
const isPortrait = (orientation || 0) >= 5;
|
||||
|
||||
if (!width || !height || !type) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
src: fileURLToPath(src),
|
||||
width: isPortrait ? height : width,
|
||||
height: isPortrait ? width : height,
|
||||
format: type as InputFormat,
|
||||
orientation,
|
||||
};
|
||||
}
|
19
packages/astro/src/assets/utils/queryParams.ts
Normal file
19
packages/astro/src/assets/utils/queryParams.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { ImageMetadata, InputFormat } from '../types.js';
|
||||
|
||||
export function getOrigQueryParams(
|
||||
params: URLSearchParams
|
||||
): Omit<ImageMetadata, 'src'> | undefined {
|
||||
const width = params.get('origWidth');
|
||||
const height = params.get('origHeight');
|
||||
const format = params.get('origFormat');
|
||||
|
||||
if (!width || !height || !format) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
width: parseInt(width),
|
||||
height: parseInt(height),
|
||||
format: format as InputFormat,
|
||||
};
|
||||
}
|
17
packages/astro/src/assets/utils/transformToPath.ts
Normal file
17
packages/astro/src/assets/utils/transformToPath.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { basename, extname } from 'path';
|
||||
import { removeQueryString } from '../../core/path.js';
|
||||
import { shorthash } from '../../runtime/server/shorthash.js';
|
||||
import { isESMImportedImage } from '../internal.js';
|
||||
import { ImageTransform } from '../types.js';
|
||||
|
||||
export function propsToFilename(transform: ImageTransform) {
|
||||
if (!isESMImportedImage(transform.src)) {
|
||||
return transform.src;
|
||||
}
|
||||
|
||||
let filename = removeQueryString(transform.src.src);
|
||||
const ext = extname(filename);
|
||||
filename = basename(filename, ext);
|
||||
const outputExt = transform.format ? `.${transform.format}` : ext;
|
||||
return `/${filename}_${shorthash(JSON.stringify(transform))}${outputExt}`;
|
||||
}
|
234
packages/astro/src/assets/vite-plugin-assets.ts
Normal file
234
packages/astro/src/assets/vite-plugin-assets.ts
Normal file
|
@ -0,0 +1,234 @@
|
|||
import MagicString from 'magic-string';
|
||||
import mime from 'mime';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import type * as vite from 'vite';
|
||||
import { normalizePath } from 'vite';
|
||||
import { AstroPluginOptions, ImageTransform } from '../@types/astro';
|
||||
import { error } from '../core/logger/core.js';
|
||||
import { joinPaths, prependForwardSlash } from '../core/path.js';
|
||||
import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
|
||||
import { isESMImportedImage } from './internal.js';
|
||||
import { isLocalService } from './services/service.js';
|
||||
import { copyWasmFiles } from './services/vendor/squoosh/copy-wasm.js';
|
||||
import { imageMetadata } from './utils/metadata.js';
|
||||
import { getOrigQueryParams } from './utils/queryParams.js';
|
||||
import { propsToFilename } from './utils/transformToPath.js';
|
||||
|
||||
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
|
||||
|
||||
export default function assets({
|
||||
settings,
|
||||
logging,
|
||||
mode,
|
||||
}: AstroPluginOptions & { mode: string }): vite.Plugin[] {
|
||||
let resolvedConfig: vite.ResolvedConfig;
|
||||
|
||||
globalThis.astroAsset = {};
|
||||
|
||||
return [
|
||||
// Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev
|
||||
{
|
||||
name: 'astro:assets',
|
||||
config() {
|
||||
return {
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^~\/assets\/(.+)$/,
|
||||
replacement: fileURLToPath(new URL('./assets/$1', settings.config.srcDir)),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
async resolveId(id) {
|
||||
if (id === VIRTUAL_SERVICE_ID) {
|
||||
return await this.resolve(settings.config.image.service);
|
||||
}
|
||||
if (id === VIRTUAL_MODULE_ID) {
|
||||
return resolvedVirtualModuleId;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
return `
|
||||
export { getImage, getConfiguredImageService } from "astro/assets";
|
||||
export { default as Image } from "astro/components/Image.astro";
|
||||
`;
|
||||
}
|
||||
},
|
||||
// Handle serving images during development
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
if (req.url?.startsWith('/_image')) {
|
||||
// If the currently configured service isn't a local service, we don't need to do anything here.
|
||||
// TODO: Support setting a specific service through a prop on Image / a parameter in getImage
|
||||
if (!isLocalService(globalThis.astroAsset.imageService)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const url = new URL(req.url, 'file:');
|
||||
const filePath = url.searchParams.get('href');
|
||||
|
||||
if (!filePath) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const filePathURL = new URL(filePath, 'file:');
|
||||
const file = await fs.readFile(filePathURL.pathname);
|
||||
|
||||
// Get the file's metadata from the URL
|
||||
let meta = getOrigQueryParams(filePathURL.searchParams);
|
||||
|
||||
// If we don't have them (ex: the image came from Markdown, let's calculate them again)
|
||||
if (!meta) {
|
||||
meta = await imageMetadata(filePathURL, file);
|
||||
|
||||
if (!meta) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
const transform = await globalThis.astroAsset.imageService.parseURL(url);
|
||||
|
||||
if (transform === undefined) {
|
||||
error(logging, 'image', `Failed to parse transform for ${url}`);
|
||||
}
|
||||
|
||||
// if no transforms were added, the original file will be returned as-is
|
||||
let data = file;
|
||||
let format = meta.format;
|
||||
|
||||
if (transform) {
|
||||
const result = await globalThis.astroAsset.imageService.transform(file, transform);
|
||||
data = result.data;
|
||||
format = result.format;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', mime.getType(fileURLToPath(url)) || `image/${format}`);
|
||||
res.setHeader('Cache-Control', 'max-age=360000');
|
||||
|
||||
const stream = Readable.from(data);
|
||||
return stream.pipe(res);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
},
|
||||
buildStart() {
|
||||
if (mode != 'build') {
|
||||
return;
|
||||
}
|
||||
|
||||
globalThis.astroAsset.addStaticImage = (options) => {
|
||||
if (!globalThis.astroAsset.staticImages) {
|
||||
globalThis.astroAsset.staticImages = new Map<ImageTransform, string>();
|
||||
}
|
||||
|
||||
let filePath: string;
|
||||
if (globalThis.astroAsset.staticImages.has(options)) {
|
||||
filePath = globalThis.astroAsset.staticImages.get(options)!;
|
||||
} else {
|
||||
// If the image is not imported, we can return the path as-is, since static references
|
||||
// should only point ot valid paths for builds or remote images
|
||||
if (!isESMImportedImage(options.src)) {
|
||||
return options.src;
|
||||
}
|
||||
|
||||
filePath = prependForwardSlash(
|
||||
joinPaths(
|
||||
settings.config.base,
|
||||
settings.config.build.assets,
|
||||
propsToFilename(options)
|
||||
)
|
||||
);
|
||||
globalThis.astroAsset.staticImages.set(options, filePath);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
};
|
||||
},
|
||||
async buildEnd() {
|
||||
if (mode != 'build') {
|
||||
return;
|
||||
}
|
||||
|
||||
const dir =
|
||||
settings.config.output === 'server'
|
||||
? settings.config.build.server
|
||||
: settings.config.outDir;
|
||||
|
||||
await copyWasmFiles(new URL('./chunks', dir));
|
||||
},
|
||||
// In build, rewrite paths to ESM imported images in code to their final location
|
||||
async renderChunk(code) {
|
||||
const assetUrlRE = /__ASTRO_ASSET_IMAGE__([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 = normalizePath(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;
|
||||
}
|
||||
},
|
||||
},
|
||||
// Return a more advanced shape for images imported in ESM
|
||||
{
|
||||
name: 'astro:assets:esm',
|
||||
enforce: 'pre',
|
||||
configResolved(viteConfig) {
|
||||
resolvedConfig = viteConfig;
|
||||
},
|
||||
async load(id) {
|
||||
if (/\.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif)$/.test(id)) {
|
||||
const url = pathToFileURL(id);
|
||||
const meta = await imageMetadata(url);
|
||||
|
||||
if (!meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build
|
||||
if (!this.meta.watchMode) {
|
||||
const pathname = decodeURI(url.pathname);
|
||||
const filename = path.basename(pathname, path.extname(pathname) + `.${meta.format}`);
|
||||
|
||||
const handle = this.emitFile({
|
||||
name: filename,
|
||||
source: await fs.readFile(url),
|
||||
type: 'asset',
|
||||
});
|
||||
|
||||
meta.src = `__ASTRO_ASSET_IMAGE__${handle}__`;
|
||||
} else {
|
||||
// Pass the original file information through query params so we don't have to load the file twice
|
||||
url.searchParams.append('origWidth', meta.width.toString());
|
||||
url.searchParams.append('origHeight', meta.height.toString());
|
||||
url.searchParams.append('origFormat', meta.format);
|
||||
|
||||
meta.src = url.toString();
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(meta)}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { z } from 'zod';
|
||||
import { imageMetadata, type Metadata } from '../assets/utils/metadata.js';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import { prependForwardSlash } from '../core/path.js';
|
||||
|
||||
|
@ -188,3 +190,29 @@ async function render({
|
|||
remarkPluginFrontmatter: mod.frontmatter,
|
||||
};
|
||||
}
|
||||
|
||||
export function createImage(options: { assetsDir: string }) {
|
||||
return () => {
|
||||
if (options.assetsDir === 'undefined') {
|
||||
throw new Error('Enable `experimental.assets` in your Astro config to use image()');
|
||||
}
|
||||
|
||||
return z.string().transform(async (imagePath) => {
|
||||
const fullPath = new URL(imagePath, options.assetsDir);
|
||||
return await getImageMetadata(fullPath);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function getImageMetadata(
|
||||
imagePath: URL
|
||||
): Promise<(Metadata & { __astro_asset: true }) | undefined> {
|
||||
const meta = await imageMetadata(imagePath);
|
||||
|
||||
if (!meta) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
delete meta.orientation;
|
||||
return { ...meta, __astro_asset: true };
|
||||
}
|
||||
|
|
|
@ -3,6 +3,13 @@ declare module 'astro:content' {
|
|||
export type CollectionEntry<C extends keyof typeof entryMap> =
|
||||
(typeof entryMap)[C][keyof (typeof entryMap)[C]] & Render;
|
||||
|
||||
export const image: () => import('astro/zod').ZodObject<{
|
||||
src: import('astro/zod').ZodString;
|
||||
width: import('astro/zod').ZodNumber;
|
||||
height: import('astro/zod').ZodNumber;
|
||||
format: import('astro/zod').ZodString;
|
||||
}>;
|
||||
|
||||
type BaseSchemaWithoutEffects =
|
||||
| import('astro/zod').AnyZodObject
|
||||
| import('astro/zod').ZodUnion<import('astro/zod').AnyZodObject[]>
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
createCollectionToGlobResultMap,
|
||||
createGetCollection,
|
||||
createGetEntryBySlug,
|
||||
createImage,
|
||||
} from 'astro/content/internal';
|
||||
|
||||
export { z } from 'astro/zod';
|
||||
|
@ -12,6 +13,7 @@ export function defineCollection(config) {
|
|||
}
|
||||
|
||||
const contentDir = '@@CONTENT_DIR@@';
|
||||
const assetsDir = '@@ASSETS_DIR@@';
|
||||
|
||||
const entryGlob = import.meta.glob('@@ENTRY_GLOB_PATH@@', {
|
||||
query: { astroContent: true },
|
||||
|
@ -38,3 +40,7 @@ export const getEntryBySlug = createGetEntryBySlug({
|
|||
getCollection,
|
||||
collectionToRenderEntryMap,
|
||||
});
|
||||
|
||||
export const image = createImage({
|
||||
assetsDir,
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|||
import { ErrorPayload as ViteErrorPayload, normalizePath, ViteDevServer } from 'vite';
|
||||
import { z } from 'zod';
|
||||
import { AstroConfig, AstroSettings } from '../@types/astro.js';
|
||||
import type { ImageMetadata } from '../assets/types.js';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import { contentFileExts, CONTENT_TYPES_FILE } from './consts.js';
|
||||
|
||||
|
@ -49,6 +50,23 @@ export const msg = {
|
|||
`${collection} does not have a config. We suggest adding one for type safety!`,
|
||||
};
|
||||
|
||||
export function extractFrontmatterAssets(data: Record<string, any>): string[] {
|
||||
function findAssets(potentialAssets: Record<string, any>): ImageMetadata[] {
|
||||
return Object.values(potentialAssets).reduce((acc, curr) => {
|
||||
if (typeof curr === 'object') {
|
||||
if (curr.__astro === true) {
|
||||
acc.push(curr);
|
||||
} else {
|
||||
acc.push(...findAssets(curr));
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return findAssets(data).map((asset) => asset.src);
|
||||
}
|
||||
|
||||
export function getEntrySlug({
|
||||
id,
|
||||
collection,
|
||||
|
@ -313,6 +331,7 @@ export function contentObservable(initialCtx: ContentCtx): ContentObservable {
|
|||
|
||||
export type ContentPaths = {
|
||||
contentDir: URL;
|
||||
assetsDir: URL;
|
||||
cacheDir: URL;
|
||||
typesTemplate: URL;
|
||||
virtualModTemplate: URL;
|
||||
|
@ -331,6 +350,7 @@ export function getContentPaths(
|
|||
return {
|
||||
cacheDir: new URL('.astro/', root),
|
||||
contentDir: new URL('./content/', srcDir),
|
||||
assetsDir: new URL('./assets/', srcDir),
|
||||
typesTemplate: new URL('types.d.ts', templateDir),
|
||||
virtualModTemplate: new URL('virtual-mod.mjs', templateDir),
|
||||
config: configStats,
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as devalue from 'devalue';
|
|||
import type fsMod from 'node:fs';
|
||||
import { pathToFileURL } from 'url';
|
||||
import type { Plugin } from 'vite';
|
||||
import { normalizePath } from 'vite';
|
||||
import { AstroSettings } from '../@types/astro.js';
|
||||
import { AstroErrorData } from '../core/errors/errors-data.js';
|
||||
import { AstroError } from '../core/errors/errors.js';
|
||||
|
@ -9,6 +10,7 @@ import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index
|
|||
import { contentFileExts, CONTENT_FLAG } from './consts.js';
|
||||
import {
|
||||
ContentConfig,
|
||||
extractFrontmatterAssets,
|
||||
getContentPaths,
|
||||
getEntryData,
|
||||
getEntryInfo,
|
||||
|
@ -91,11 +93,18 @@ export function astroContentImportPlugin({
|
|||
? await getEntryData(partialEntry, collectionConfig)
|
||||
: unparsedData;
|
||||
|
||||
const images = extractFrontmatterAssets(data).map(
|
||||
(image) => `'${image}': await import('${normalizePath(image)}'),`
|
||||
);
|
||||
|
||||
const code = escapeViteEnvReferences(`
|
||||
export const id = ${JSON.stringify(entryInfo.id)};
|
||||
export const collection = ${JSON.stringify(entryInfo.collection)};
|
||||
export const slug = ${JSON.stringify(slug)};
|
||||
export const body = ${JSON.stringify(body)};
|
||||
const frontmatterImages = {
|
||||
${images.join('\n')}
|
||||
}
|
||||
export const data = ${devalue.uneval(data) /* TODO: reuse astro props serializer */};
|
||||
export const _internal = {
|
||||
filePath: ${JSON.stringify(fileId)},
|
||||
|
|
|
@ -22,10 +22,15 @@ export function astroContentVirtualModPlugin({
|
|||
)
|
||||
)
|
||||
);
|
||||
|
||||
const assetsDir = settings.config.experimental.assets
|
||||
? contentPaths.assetsDir.toString()
|
||||
: 'undefined';
|
||||
const entryGlob = `${relContentDir}**/*{${contentFileExts.join(',')}}`;
|
||||
const virtualModContents = fsMod
|
||||
.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
|
||||
.replace('@@CONTENT_DIR@@', relContentDir)
|
||||
.replace('@@ASSETS_DIR@@', assetsDir)
|
||||
.replace('@@ENTRY_GLOB_PATH@@', entryGlob)
|
||||
.replace('@@RENDER_ENTRY_GLOB_PATH@@', entryGlob);
|
||||
|
||||
|
|
|
@ -9,11 +9,15 @@ import type {
|
|||
AstroSettings,
|
||||
ComponentInstance,
|
||||
EndpointHandler,
|
||||
ImageTransform,
|
||||
RouteType,
|
||||
SSRError,
|
||||
SSRLoadedRenderer,
|
||||
} from '../../@types/astro';
|
||||
import { getContentPaths } from '../../content/index.js';
|
||||
import {
|
||||
generateImage as generateImageInternal,
|
||||
getStaticImageList,
|
||||
} from '../../assets/internal.js';
|
||||
import { BuildInternals, hasPrerenderedPages } from '../../core/build/internal.js';
|
||||
import {
|
||||
prependForwardSlash,
|
||||
|
@ -101,6 +105,14 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
|
|||
}
|
||||
}
|
||||
|
||||
if (opts.settings.config.experimental.assets) {
|
||||
info(opts.logging, null, `\n${bgGreen(black(` generating optimized images `))}`);
|
||||
for (const imageData of getStaticImageList()) {
|
||||
await generateImage(opts, imageData[0], imageData[1]);
|
||||
}
|
||||
delete globalThis.astroAsset.addStaticImage;
|
||||
}
|
||||
|
||||
await runHookBuildGenerated({
|
||||
config: opts.settings.config,
|
||||
buildConfig: opts.buildConfig,
|
||||
|
@ -110,6 +122,26 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
|
|||
info(opts.logging, null, dim(`Completed in ${getTimeStat(timer, performance.now())}.\n`));
|
||||
}
|
||||
|
||||
async function generateImage(opts: StaticBuildOptions, transform: ImageTransform, path: string) {
|
||||
let timeStart = performance.now();
|
||||
const generationData = await generateImageInternal(opts, transform, path);
|
||||
|
||||
if (!generationData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeEnd = performance.now();
|
||||
const timeChange = getTimeStat(timeStart, timeEnd);
|
||||
const timeIncrease = `(+${timeChange})`;
|
||||
info(
|
||||
opts.logging,
|
||||
null,
|
||||
` ${green('▶')} ${path} ${dim(
|
||||
`(before: ${generationData.weight.before}kb, after: ${generationData.weight.after}kb)`
|
||||
)} ${dim(timeIncrease)}`
|
||||
);
|
||||
}
|
||||
|
||||
async function generatePage(
|
||||
opts: StaticBuildOptions,
|
||||
internals: BuildInternals,
|
||||
|
@ -347,10 +379,7 @@ async function generatePath(
|
|||
const env = createEnvironment({
|
||||
adapterName: undefined,
|
||||
logging,
|
||||
markdown: {
|
||||
...settings.config.markdown,
|
||||
contentDir: getContentPaths(settings.config).contentDir,
|
||||
},
|
||||
markdown: settings.config.markdown,
|
||||
mode: opts.mode,
|
||||
renderers,
|
||||
async resolve(specifier: string) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import type { StaticBuildOptions } from '../types';
|
|||
|
||||
import glob from 'fast-glob';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getContentPaths } from '../../../content/index.js';
|
||||
import { runHookBuildSsr } from '../../../integrations/index.js';
|
||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||
import { pagesVirtualModuleId } from '../../app/index.js';
|
||||
|
@ -206,10 +205,7 @@ function buildManifest(
|
|||
routes,
|
||||
site: settings.config.site,
|
||||
base: settings.config.base,
|
||||
markdown: {
|
||||
...settings.config.markdown,
|
||||
contentDir: getContentPaths(settings.config).contentDir,
|
||||
},
|
||||
markdown: settings.config.markdown,
|
||||
pageMap: null as any,
|
||||
propagation: Array.from(internals.propagation),
|
||||
renderers: [],
|
||||
|
|
|
@ -100,6 +100,8 @@ export function resolveFlags(flags: Partial<Flags>): CLIFlags {
|
|||
host:
|
||||
typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
|
||||
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
|
||||
experimentalAssets:
|
||||
typeof flags.experimentalAssets === 'boolean' ? flags.experimentalAssets : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,9 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
},
|
||||
vite: {},
|
||||
legacy: {},
|
||||
experimental: {
|
||||
assets: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const AstroConfigSchema = z.object({
|
||||
|
@ -115,6 +118,17 @@ export const AstroConfigSchema = z.object({
|
|||
.optional()
|
||||
.default({})
|
||||
),
|
||||
image: z
|
||||
.object({
|
||||
service: z.union([
|
||||
z.literal('astro/assets/services/sharp'),
|
||||
z.literal('astro/assets/services/squoosh'),
|
||||
z.string().and(z.object({})),
|
||||
]),
|
||||
})
|
||||
.default({
|
||||
service: 'astro/assets/services/squoosh',
|
||||
}),
|
||||
markdown: z
|
||||
.object({
|
||||
drafts: z.boolean().default(false),
|
||||
|
@ -160,7 +174,12 @@ export const AstroConfigSchema = z.object({
|
|||
vite: z
|
||||
.custom<ViteUserConfig>((data) => data instanceof Object && !Array.isArray(data))
|
||||
.default(ASTRO_CONFIG_DEFAULTS.vite),
|
||||
experimental: z.object({}).optional().default({}),
|
||||
experimental: z
|
||||
.object({
|
||||
assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
legacy: z.object({}).optional().default({}),
|
||||
});
|
||||
|
||||
|
@ -256,7 +275,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
|
|||
config.base = sitePathname;
|
||||
/* eslint-disable no-console */
|
||||
console.warn(`The site configuration value includes a pathname of ${sitePathname} but there is no base configuration.
|
||||
|
||||
|
||||
A future version of Astro will stop using the site pathname when producing <link> and <script> tags. Set your site's base with the base configuration.`);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,10 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
|
|||
tsConfigPath: undefined,
|
||||
|
||||
adapter: undefined,
|
||||
injectedRoutes: [],
|
||||
injectedRoutes:
|
||||
config.experimental.assets && config.output === 'server'
|
||||
? [{ pattern: '/_image', entryPoint: 'astro/assets/image-endpoint' }]
|
||||
: [],
|
||||
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
|
||||
renderers: [jsxRenderer],
|
||||
scripts: [],
|
||||
|
|
|
@ -5,6 +5,7 @@ import nodeFs from 'fs';
|
|||
import { fileURLToPath } from 'url';
|
||||
import * as vite from 'vite';
|
||||
import { crawlFrameworkPkgs } from 'vitefu';
|
||||
import astroAssetsPlugin from '../assets/vite-plugin-assets.js';
|
||||
import {
|
||||
astroContentAssetPropagationPlugin,
|
||||
astroContentImportPlugin,
|
||||
|
@ -115,6 +116,7 @@ export async function createVite(
|
|||
astroContentVirtualModPlugin({ settings }),
|
||||
astroContentImportPlugin({ fs, settings }),
|
||||
astroContentAssetPropagationPlugin({ mode }),
|
||||
settings.config.experimental.assets ? [astroAssetsPlugin({ settings, logging, mode })] : [],
|
||||
],
|
||||
publicDir: fileURLToPath(settings.config.publicDir),
|
||||
root: fileURLToPath(settings.config.root),
|
||||
|
|
|
@ -454,6 +454,26 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
|
|||
`[paginate()] page number param \`${paramName}\` not found in your filepath.`,
|
||||
hint: 'Rename your file to `[page].astro` or `[...page].astro`.',
|
||||
},
|
||||
ImageMissingAlt: {
|
||||
title: 'Missing alt property',
|
||||
code: 3022,
|
||||
message: 'The alt property is required.',
|
||||
hint: "The `alt` property is important for the purpose of accessibility, without it users using screen readers or other assistive technologies won't be able to understand what your image is supposed to represent. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-alt for more information.",
|
||||
},
|
||||
InvalidImageService: {
|
||||
title: 'Error while loading image service',
|
||||
code: 3023,
|
||||
message:
|
||||
'There was an error loading the configured image service. Please see the stack trace for more information',
|
||||
},
|
||||
MissingImageDimension: {
|
||||
title: 'Missing image dimensions',
|
||||
code: 3024,
|
||||
message: (missingDimension: 'width' | 'height' | 'both') =>
|
||||
`For remote images, ${
|
||||
missingDimension === 'both' ? 'width and height are' : `${missingDimension} is`
|
||||
} required.`,
|
||||
},
|
||||
// No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users.
|
||||
// Vite Errors - 4xxx
|
||||
/**
|
||||
|
|
|
@ -64,3 +64,7 @@ export function removeQueryString(path: string) {
|
|||
const index = path.lastIndexOf('?');
|
||||
return index > 0 ? path.substring(0, index) : path;
|
||||
}
|
||||
|
||||
export function isRemotePath(src: string) {
|
||||
return /^(http|ftp|https):?\/\//.test(src) || src.startsWith('data:');
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { AstroSettings, RuntimeMode } from '../../../@types/astro';
|
||||
import { getContentPaths } from '../../../content/index.js';
|
||||
import type { LogOptions } from '../../logger/core.js';
|
||||
import type { ModuleLoader } from '../../module-loader/index';
|
||||
import type { Environment } from '../index';
|
||||
|
@ -22,10 +21,7 @@ export function createDevelopmentEnvironment(
|
|||
let env = createEnvironment({
|
||||
adapterName: settings.adapter?.name,
|
||||
logging,
|
||||
markdown: {
|
||||
...settings.config.markdown,
|
||||
contentDir: getContentPaths(settings.config).contentDir,
|
||||
},
|
||||
markdown: settings.config.markdown,
|
||||
mode,
|
||||
// This will be overridden in the dev server
|
||||
renderers: [],
|
||||
|
|
|
@ -43,8 +43,6 @@ export function createBasicEnvironment(options: CreateBasicEnvironmentArgs): Env
|
|||
...options,
|
||||
markdown: {
|
||||
...(options.markdown ?? {}),
|
||||
// Stub out, not important for basic rendering
|
||||
contentDir: new URL('file:///src/content/'),
|
||||
},
|
||||
mode,
|
||||
renderers: options.renderers ?? [],
|
||||
|
|
|
@ -48,10 +48,24 @@ export async function setUpEnvTs({
|
|||
);
|
||||
|
||||
if (fs.existsSync(envTsPath)) {
|
||||
// Add `.astro` types reference if none exists
|
||||
if (!fs.existsSync(dotAstroDir)) return;
|
||||
|
||||
let typesEnvContents = await fs.promises.readFile(envTsPath, 'utf-8');
|
||||
if (settings.config.experimental.assets && typesEnvContents.includes('types="astro/client"')) {
|
||||
typesEnvContents = typesEnvContents.replace(
|
||||
'types="astro/client"',
|
||||
'types="astro/client-image"'
|
||||
);
|
||||
await fs.promises.writeFile(envTsPath, typesEnvContents, 'utf-8');
|
||||
} else if (typesEnvContents.includes('types="astro/client-image"')) {
|
||||
typesEnvContents = typesEnvContents.replace(
|
||||
'types="astro/client-image"',
|
||||
'types="astro/client"'
|
||||
);
|
||||
await fs.promises.writeFile(envTsPath, typesEnvContents, 'utf-8');
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dotAstroDir))
|
||||
// Add `.astro` types reference if none exists
|
||||
return;
|
||||
const expectedTypeReference = getDotAstroTypeReference(settings.config);
|
||||
|
||||
if (!typesEnvContents.includes(expectedTypeReference)) {
|
||||
|
@ -62,7 +76,9 @@ export async function setUpEnvTs({
|
|||
} else {
|
||||
// Otherwise, inject the `env.d.ts` file
|
||||
let referenceDefs: string[] = [];
|
||||
if (settings.config.integrations.find((i) => i.name === '@astrojs/image')) {
|
||||
if (settings.config.experimental.assets) {
|
||||
referenceDefs.push('/// <reference types="astro/client-image" />');
|
||||
} else if (settings.config.integrations.find((i) => i.name === '@astrojs/image')) {
|
||||
referenceDefs.push('/// <reference types="@astrojs/image/client" />');
|
||||
} else {
|
||||
referenceDefs.push('/// <reference types="astro/client" />');
|
||||
|
|
|
@ -5,11 +5,14 @@ import {
|
|||
} from '@astrojs/markdown-remark/dist/internal.js';
|
||||
import fs from 'fs';
|
||||
import matter from 'gray-matter';
|
||||
import npath from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { PluginContext } from 'rollup';
|
||||
import { pathToFileURL } from 'url';
|
||||
import type { Plugin } from 'vite';
|
||||
import { normalizePath } from 'vite';
|
||||
import type { AstroSettings } from '../@types/astro';
|
||||
import { getContentPaths } from '../content/index.js';
|
||||
import { imageMetadata } from '../assets/index.js';
|
||||
import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js';
|
||||
import type { LogOptions } from '../core/logger/core.js';
|
||||
import { warn } from '../core/logger/core.js';
|
||||
|
@ -56,6 +59,26 @@ const astroJsxRuntimeModulePath = normalizePath(
|
|||
);
|
||||
|
||||
export default function markdown({ settings, logging }: AstroPluginOptions): Plugin {
|
||||
const markdownAssetMap = new Map<string, string>();
|
||||
|
||||
async function resolveImage(this: PluginContext, fileId: string, path: string) {
|
||||
const resolved = await this.resolve(path, fileId);
|
||||
if (!resolved) return path;
|
||||
const rel = npath.relative(normalizePath(fileURLToPath(settings.config.root)), resolved.id);
|
||||
const buffer = await fs.promises.readFile(resolved.id);
|
||||
// This conditional has to be here, to prevent race conditions on setting the map
|
||||
if (markdownAssetMap.has(resolved.id)) {
|
||||
return `ASTRO_ASSET_MD_${markdownAssetMap.get(resolved.id)!}`;
|
||||
}
|
||||
const file = this.emitFile({
|
||||
type: 'asset',
|
||||
name: rel,
|
||||
source: buffer,
|
||||
});
|
||||
markdownAssetMap.set(resolved.id, file);
|
||||
return `ASTRO_ASSET_MD_${file}`;
|
||||
}
|
||||
|
||||
return {
|
||||
enforce: 'pre',
|
||||
name: 'astro:markdown',
|
||||
|
@ -68,15 +91,35 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
|||
const { fileId, fileUrl } = getFileInfo(id, settings.config);
|
||||
const rawFile = await fs.promises.readFile(fileId, 'utf-8');
|
||||
const raw = safeMatter(rawFile, id);
|
||||
|
||||
let imageService = undefined;
|
||||
if (settings.config.experimental.assets) {
|
||||
imageService = (await import(settings.config.image.service)).default;
|
||||
}
|
||||
const renderResult = await renderMarkdown(raw.content, {
|
||||
...settings.config.markdown,
|
||||
fileURL: new URL(`file://${fileId}`),
|
||||
contentDir: getContentPaths(settings.config).contentDir,
|
||||
frontmatter: raw.data,
|
||||
experimentalAssets: settings.config.experimental.assets,
|
||||
imageService,
|
||||
assetsDir: new URL('./assets/', settings.config.srcDir),
|
||||
resolveImage: this.meta.watchMode ? undefined : resolveImage.bind(this, fileId),
|
||||
});
|
||||
|
||||
const html = renderResult.code;
|
||||
this;
|
||||
|
||||
let html = renderResult.code;
|
||||
const { headings } = renderResult.metadata;
|
||||
let imagePaths: string[] = [];
|
||||
if (settings.config.experimental.assets) {
|
||||
let paths = (renderResult.vfile.data.imagePaths as string[]) ?? [];
|
||||
imagePaths = await Promise.all(
|
||||
paths.map(async (imagePath) => {
|
||||
return (await this.resolve(imagePath))?.id ?? imagePath;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const astroData = safelyGetAstroData(renderResult.vfile.data);
|
||||
if (astroData instanceof InvalidAstroDataError) {
|
||||
throw new AstroError(AstroErrorData.InvalidFrontmatterInjectionError);
|
||||
|
@ -96,6 +139,15 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
|||
const code = escapeViteEnvReferences(`
|
||||
import { Fragment, jsx as h } from ${JSON.stringify(astroJsxRuntimeModulePath)};
|
||||
${layout ? `import Layout from ${JSON.stringify(layout)};` : ''}
|
||||
${
|
||||
settings.config.experimental.assets
|
||||
? 'import { getConfiguredImageService } from "astro:assets";\ngetConfiguredImageService();'
|
||||
: ''
|
||||
}
|
||||
|
||||
const images = {
|
||||
${imagePaths.map((entry) => `'${entry}': await import('${entry}')`)}
|
||||
}
|
||||
|
||||
const html = ${JSON.stringify(html)};
|
||||
|
||||
|
@ -153,5 +205,29 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
|||
};
|
||||
}
|
||||
},
|
||||
async generateBundle(_opts, bundle) {
|
||||
for (const [, output] of Object.entries(bundle)) {
|
||||
if (output.type === 'asset') continue;
|
||||
|
||||
if (markdownAssetMap.size) {
|
||||
const optimizedPaths = new Map<string, string>();
|
||||
|
||||
for (const [filename, hash] of markdownAssetMap) {
|
||||
const image = await imageMetadata(pathToFileURL(filename));
|
||||
if (!image) {
|
||||
continue;
|
||||
}
|
||||
const fileName = this.getFileName(hash);
|
||||
image.src = npath.join(settings.config.base, fileName);
|
||||
const optimized = globalThis.astroAsset.addStaticImage!({ src: image });
|
||||
optimizedPaths.set(hash, optimized);
|
||||
}
|
||||
output.code = output.code.replace(/ASTRO_ASSET_MD_([0-9a-z]{8})/, (_str, hash) => {
|
||||
const optimizedName = optimizedPaths.get(hash);
|
||||
return optimizedName || this.getFileName(hash);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
356
packages/astro/test/core-image.test.js
Normal file
356
packages/astro/test/core-image.test.js
Normal file
|
@ -0,0 +1,356 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { Writable } from 'node:stream';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import testAdapter from './test-adapter.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('astro:image', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
describe('dev', () => {
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
/** @type {Array<{ type: any, level: 'error', message: string; }>} */
|
||||
let logs = [];
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/core-image/',
|
||||
experimental: {
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer({
|
||||
logging: {
|
||||
level: 'error',
|
||||
dest: new Writable({
|
||||
objectMode: true,
|
||||
write(event, _, callback) {
|
||||
logs.push(event);
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('basics', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
it('Adds the <img> tag', () => {
|
||||
let $img = $('#local img');
|
||||
expect($img).to.have.a.lengthOf(1);
|
||||
expect($img.attr('src').startsWith('/_image')).to.equal(true);
|
||||
});
|
||||
|
||||
it('includes loading and decoding attributes', () => {
|
||||
let $img = $('#local img');
|
||||
expect(!!$img.attr('loading')).to.equal(true);
|
||||
expect(!!$img.attr('decoding')).to.equal(true);
|
||||
});
|
||||
|
||||
it('has width and height', () => {
|
||||
let $img = $('#local img');
|
||||
expect($img.attr('width')).to.equal('207');
|
||||
expect($img.attr('height')).to.equal('243');
|
||||
});
|
||||
|
||||
it('includes the provided alt', () => {
|
||||
let $img = $('#local img');
|
||||
expect($img.attr('alt')).to.equal('a penguin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote', () => {
|
||||
describe('working', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
it('includes the provided alt', async () => {
|
||||
let $img = $('#remote img');
|
||||
expect($img.attr('alt')).to.equal('fred');
|
||||
});
|
||||
|
||||
it('includes loading and decoding attributes', () => {
|
||||
let $img = $('#remote img');
|
||||
expect(!!$img.attr('loading')).to.equal(true);
|
||||
expect(!!$img.attr('decoding')).to.equal(true);
|
||||
});
|
||||
|
||||
it('includes width and height attributes', () => {
|
||||
let $img = $('#remote img');
|
||||
expect(!!$img.attr('width')).to.equal(true);
|
||||
expect(!!$img.attr('height')).to.equal(true);
|
||||
});
|
||||
|
||||
it('support data: URI', () => {
|
||||
let $img = $('#data-uri img');
|
||||
expect($img.attr('src')).to.equal(
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAA2gAwAEAAAAAQAAAA0AAAAAWvB1rQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KGV7hBwAAAWJJREFUKBVtUDEsQ1EUve+1/SItKYMIkYpF06GJdGAwNFFGkxBEYupssRm6EpvJbpVoYhRd6FBikDSxYECsBpG25D/nvP/+p+Ik551z73v33feuyA/izq5CL8ET8ALcBolYIP+vd0ibX/yAT7uj2qkVzwWzUBa0nbacbkKJHi5dlYhXmARYeAS+MwCWA5FPqKIP/9IH/wiygMru5y5mcRYkPHYKP7gAPw4SDbCjRXMgRBJctM4t4ROriM2QSpmkeOtub6YfMYrZvelykbD1sxJVg+6AfKqURRKQLfA4JvoVWgIjDMNlGLVKZxNRFsZsoHGAgREZHKPlJEi2t7if3r2KKS9nVOo0rtNZ3yR7M/VGTqTy5Y4o/scWHBbKfIq0/eZ+x3850OZpaTTxlu/4D3ssuA72uxrYS2rFYjh+aRbmb24LpTVu1IqVKG8P/lmUEaNMxeh6fmquOhkMBE8JJ2yPfwPjdVhiDbiX6AAAAABJRU5ErkJggg=='
|
||||
);
|
||||
expect(!!$img.attr('width')).to.equal(true);
|
||||
expect(!!$img.attr('height')).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('error if no width and height', async () => {
|
||||
logs.length = 0;
|
||||
let res = await fixture.fetch('/remote-error-no-dimensions');
|
||||
await res.text();
|
||||
|
||||
expect(logs).to.have.a.lengthOf(1);
|
||||
expect(logs[0].message).to.contain('For remote images, width and height are required.');
|
||||
});
|
||||
|
||||
it('error if no height', async () => {
|
||||
logs.length = 0;
|
||||
let res = await fixture.fetch('/remote-error-no-height');
|
||||
await res.text();
|
||||
|
||||
expect(logs).to.have.a.lengthOf(1);
|
||||
expect(logs[0].message).to.contain('For remote images, height is required.');
|
||||
});
|
||||
|
||||
it('supports aliases', async () => {
|
||||
let res = await fixture.fetch('/alias');
|
||||
let html = await res.text();
|
||||
let $ = cheerio.load(html);
|
||||
|
||||
let $img = $('img');
|
||||
expect($img).to.have.a.lengthOf(1);
|
||||
expect($img.attr('src').includes('penguin1.jpg')).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/post');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
it('Adds the <img> tag', () => {
|
||||
let $img = $('img');
|
||||
expect($img).to.have.a.lengthOf(1);
|
||||
expect($img.attr('src').startsWith('/_image')).to.equal(true);
|
||||
});
|
||||
|
||||
it('has width and height attributes', () => {
|
||||
let $img = $('img');
|
||||
expect(!!$img.attr('width')).to.equal(true);
|
||||
expect(!!$img.attr('height')).to.equal(true);
|
||||
});
|
||||
|
||||
it('Supports aliased paths', async () => {
|
||||
let res = await fixture.fetch('/aliasMarkdown');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
|
||||
let $img = $('img');
|
||||
expect($img.attr('src').startsWith('/_image')).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getImage', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/get-image');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
it('Adds the <img> tag', () => {
|
||||
let $img = $('img');
|
||||
expect($img).to.have.a.lengthOf(1);
|
||||
expect($img.attr('src').startsWith('/_image')).to.equal(true);
|
||||
});
|
||||
|
||||
it('includes the provided alt', () => {
|
||||
let $img = $('img');
|
||||
expect($img.attr('alt')).to.equal('a penguin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('content collections', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/blog/one');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
it('Adds the <img> tag', () => {
|
||||
let $img = $('img');
|
||||
expect($img).to.have.a.lengthOf(1);
|
||||
expect($img.attr('src').startsWith('/_image')).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('build ssg', () => {
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/core-image-ssg/',
|
||||
experimental: {
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('writes out images to dist folder', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const src = $('#local img').attr('src');
|
||||
expect(src.length).to.be.greaterThan(0);
|
||||
const data = await fixture.readFile(src, null);
|
||||
expect(data).to.be.an.instanceOf(Buffer);
|
||||
});
|
||||
|
||||
it('getImage() usage also written', async () => {
|
||||
const html = await fixture.readFile('/get-image/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
let $img = $('img');
|
||||
|
||||
// <img> tag
|
||||
expect($img).to.have.a.lengthOf(1);
|
||||
expect($img.attr('alt')).to.equal('a penguin');
|
||||
|
||||
// image itself
|
||||
const src = $img.attr('src');
|
||||
const data = await fixture.readFile(src, null);
|
||||
expect(data).to.be.an.instanceOf(Buffer);
|
||||
});
|
||||
|
||||
it('aliased images are written', async () => {
|
||||
const html = await fixture.readFile('/alias/index.html');
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
let $img = $('img');
|
||||
|
||||
// <img> tag
|
||||
expect($img).to.have.a.lengthOf(1);
|
||||
expect($img.attr('alt')).to.equal('A penguin!');
|
||||
|
||||
// image itself
|
||||
const src = $img.attr('src');
|
||||
const data = await fixture.readFile(src, null);
|
||||
expect(data).to.be.an.instanceOf(Buffer);
|
||||
});
|
||||
|
||||
it('aliased images in Markdown are written', async () => {
|
||||
const html = await fixture.readFile('/aliasMarkdown/index.html');
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
let $img = $('img');
|
||||
|
||||
// <img> tag
|
||||
expect($img).to.have.a.lengthOf(1);
|
||||
expect($img.attr('alt')).to.equal('A penguin');
|
||||
|
||||
// image itself
|
||||
const src = $img.attr('src');
|
||||
const data = await fixture.readFile(src, null);
|
||||
expect(data).to.be.an.instanceOf(Buffer);
|
||||
});
|
||||
|
||||
it('quality attribute produces a different file', async () => {
|
||||
const html = await fixture.readFile('/quality/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('#no-quality img').attr('src')).to.not.equal($('#quality-low img').attr('src'));
|
||||
});
|
||||
|
||||
it('quality can be a number between 0-100', async () => {
|
||||
const html = await fixture.readFile('/quality/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('#no-quality img').attr('src')).to.not.equal($('#quality-num img').attr('src'));
|
||||
});
|
||||
|
||||
it('format attribute produces a different file', async () => {
|
||||
const html = await fixture.readFile('/format/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('#no-format img').attr('src')).to.not.equal($('#format-avif img').attr('src'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('prod ssr', () => {
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/core-image-ssr/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
experimental: {
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
// TODO
|
||||
// This is not working because the image service does a fetch() on the underlying
|
||||
// image and we do not have an HTTP server in these tests. We either need
|
||||
// to start one, or find another way to tell the image service how to load these files.
|
||||
it.skip('dynamic route images are built at response time', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
let request = new Request('http://example.com/');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
const src = $('#local img').attr('src');
|
||||
request = new Request('http://example.com' + src);
|
||||
response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('prerendered routes images are built', async () => {
|
||||
const html = await fixture.readFile('/client/prerender/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
const src = $('img').attr('src');
|
||||
const imgData = await fixture.readFile('/client' + src, null);
|
||||
expect(imgData).to.be.an.instanceOf(Buffer);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom service', () => {
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/core-image/',
|
||||
experimental: {
|
||||
assets: true,
|
||||
},
|
||||
image: {
|
||||
service: fileURLToPath(new URL('./fixtures/core-image/service.mjs', import.meta.url)),
|
||||
},
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
it('custom service implements getHTMLAttributes', async () => {
|
||||
const response = await fixture.fetch('/');
|
||||
const html = await response.text();
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
expect($('#local img').attr('data-service')).to.equal('my-custom-service');
|
||||
});
|
||||
});
|
||||
});
|
11
packages/astro/test/fixtures/core-image-ssg/package.json
vendored
Normal file
11
packages/astro/test/fixtures/core-image-ssg/package.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@test/core-image-ssg",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
"./service": "./src/service.ts"
|
||||
}
|
||||
}
|
BIN
packages/astro/test/fixtures/core-image-ssg/src/assets/penguin1.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image-ssg/src/assets/penguin1.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
packages/astro/test/fixtures/core-image-ssg/src/assets/penguin2.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image-ssg/src/assets/penguin2.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
5
packages/astro/test/fixtures/core-image-ssg/src/pages/alias.astro
vendored
Normal file
5
packages/astro/test/fixtures/core-image-ssg/src/pages/alias.astro
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import image from "~/assets/penguin1.jpg";
|
||||
---
|
||||
|
||||
<img src={image.src} width={image.width} height={image.height} alt="A penguin!" />
|
3
packages/astro/test/fixtures/core-image-ssg/src/pages/aliasMarkdown.md
vendored
Normal file
3
packages/astro/test/fixtures/core-image-ssg/src/pages/aliasMarkdown.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
![A penguin](~/assets/penguin1.jpg)
|
||||
|
||||
A penguin
|
18
packages/astro/test/fixtures/core-image-ssg/src/pages/format.astro
vendored
Normal file
18
packages/astro/test/fixtures/core-image-ssg/src/pages/format.astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import myImage from "../assets/penguin1.jpg";
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="no-format">
|
||||
<Image src={myImage} alt="a penguin" />
|
||||
</div>
|
||||
|
||||
<div id="format-avif">
|
||||
<Image src={myImage} alt="a penguin" format="avif" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/core-image-ssg/src/pages/get-image.astro
vendored
Normal file
8
packages/astro/test/fixtures/core-image-ssg/src/pages/get-image.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import { getImage } from "astro:assets";
|
||||
import image from "../assets/penguin2.jpg";
|
||||
|
||||
const myImage = await getImage({ src: image, width: 207, height: 243, alt: 'a penguin' });
|
||||
---
|
||||
|
||||
<img src={myImage.src} {...myImage.attributes} />
|
18
packages/astro/test/fixtures/core-image-ssg/src/pages/index.astro
vendored
Normal file
18
packages/astro/test/fixtures/core-image-ssg/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import myImage from "../assets/penguin1.jpg";
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="local">
|
||||
<Image src={myImage} alt="a penguin" />
|
||||
</div>
|
||||
|
||||
<div id="remote">
|
||||
<Image src="https://avatars.githubusercontent.com/u/622227?s=64" alt="fred" width="48" height="48" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
3
packages/astro/test/fixtures/core-image-ssg/src/pages/post.md
vendored
Normal file
3
packages/astro/test/fixtures/core-image-ssg/src/pages/post.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
![My article cover](../assets/penguin1.jpg)
|
||||
|
||||
Image worked
|
22
packages/astro/test/fixtures/core-image-ssg/src/pages/quality.astro
vendored
Normal file
22
packages/astro/test/fixtures/core-image-ssg/src/pages/quality.astro
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import myImage from "../assets/penguin1.jpg";
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="no-quality">
|
||||
<Image src={myImage} alt="a penguin" />
|
||||
</div>
|
||||
|
||||
<div id="quality-low">
|
||||
<Image src={myImage} alt="a penguin" quality="low" />
|
||||
</div>
|
||||
|
||||
<div id="quality-num">
|
||||
<Image src={myImage} alt="a penguin" quality="70" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/core-image-ssr/package.json
vendored
Normal file
8
packages/astro/test/fixtures/core-image-ssr/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/core-image-ssr",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
BIN
packages/astro/test/fixtures/core-image-ssr/src/assets/penguin1.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image-ssr/src/assets/penguin1.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
packages/astro/test/fixtures/core-image-ssr/src/assets/penguin2.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image-ssr/src/assets/penguin2.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
18
packages/astro/test/fixtures/core-image-ssr/src/pages/index.astro
vendored
Normal file
18
packages/astro/test/fixtures/core-image-ssr/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import myImage from "../assets/penguin1.jpg";
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="local">
|
||||
<Image src={myImage} alt="a penguin" />
|
||||
</div>
|
||||
|
||||
<div id="remote">
|
||||
<Image src="https://avatars.githubusercontent.com/u/622227?s=64" alt="fred" width="48" height="48" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
15
packages/astro/test/fixtures/core-image-ssr/src/pages/prerender.astro
vendored
Normal file
15
packages/astro/test/fixtures/core-image-ssr/src/pages/prerender.astro
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import myImage from "../assets/penguin2.jpg";
|
||||
export const prerender = true;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="local">
|
||||
<Image src={myImage} alt="a penguin" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
11
packages/astro/test/fixtures/core-image/package.json
vendored
Normal file
11
packages/astro/test/fixtures/core-image/package.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@test/core-image",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev"
|
||||
}
|
||||
}
|
19
packages/astro/test/fixtures/core-image/service.mjs
vendored
Normal file
19
packages/astro/test/fixtures/core-image/service.mjs
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
import squoosh from 'astro/assets/services/squoosh';
|
||||
|
||||
const service = {
|
||||
getURL(options) {
|
||||
return squoosh.getURL(options);
|
||||
},
|
||||
getHTMLAttributes(options) {
|
||||
options['data-service'] = 'my-custom-service';
|
||||
return squoosh.getHTMLAttributes(options);
|
||||
},
|
||||
parseURL(url) {
|
||||
return squoosh.parseURL(url);
|
||||
},
|
||||
transform(buffer, options) {
|
||||
return squoosh.transform(buffer, options);
|
||||
},
|
||||
};
|
||||
|
||||
export default service;
|
BIN
packages/astro/test/fixtures/core-image/src/assets/penguin1.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image/src/assets/penguin1.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
packages/astro/test/fixtures/core-image/src/assets/penguin2.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image/src/assets/penguin2.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
8
packages/astro/test/fixtures/core-image/src/content/blog/one.md
vendored
Normal file
8
packages/astro/test/fixtures/core-image/src/content/blog/one.md
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: One
|
||||
image: penguin2.jpg
|
||||
---
|
||||
|
||||
# A post
|
||||
|
||||
text here
|
12
packages/astro/test/fixtures/core-image/src/content/config.ts
vendored
Normal file
12
packages/astro/test/fixtures/core-image/src/content/config.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { image, defineCollection, z } from "astro:content";
|
||||
|
||||
const blogCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
image: image(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
blog: blogCollection
|
||||
};
|
5
packages/astro/test/fixtures/core-image/src/pages/alias.astro
vendored
Normal file
5
packages/astro/test/fixtures/core-image/src/pages/alias.astro
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
import image from "~/assets/penguin1.jpg";
|
||||
---
|
||||
|
||||
<img src={image.src} width={image.width} height={image.height} alt="A penguin!" />
|
3
packages/astro/test/fixtures/core-image/src/pages/aliasMarkdown.md
vendored
Normal file
3
packages/astro/test/fixtures/core-image/src/pages/aliasMarkdown.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
![A penguin](~/assets/penguin1.jpg)
|
||||
|
||||
A penguin
|
27
packages/astro/test/fixtures/core-image/src/pages/blog/[...slug].astro
vendored
Normal file
27
packages/astro/test/fixtures/core-image/src/pages/blog/[...slug].astro
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getImage } from 'astro:assets';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('blog');
|
||||
return blogEntries.map(entry => ({
|
||||
params: { slug: entry.slug }, props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
const myImage = await getImage(entry.data.image);
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
|
||||
<img src={myImage.src} {...myImage.attributes} />
|
||||
|
||||
<Content />
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/core-image/src/pages/get-image.astro
vendored
Normal file
8
packages/astro/test/fixtures/core-image/src/pages/get-image.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import { getImage } from "astro:assets";
|
||||
import image from "../assets/penguin2.jpg";
|
||||
|
||||
const myImage = await getImage({ src: image, width: 207, height: 243, alt: 'a penguin' });
|
||||
---
|
||||
|
||||
<img src={myImage.src} {...myImage.attributes} />
|
22
packages/astro/test/fixtures/core-image/src/pages/index.astro
vendored
Normal file
22
packages/astro/test/fixtures/core-image/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import myImage from "../assets/penguin1.jpg";
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="local">
|
||||
<Image src={myImage} alt="a penguin" />
|
||||
</div>
|
||||
|
||||
<div id="remote">
|
||||
<Image src="https://avatars.githubusercontent.com/u/622227?s=64" alt="fred" width="48" height="48" />
|
||||
</div>
|
||||
|
||||
<div id="data-uri">
|
||||
<Image src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAA2gAwAEAAAAAQAAAA0AAAAAWvB1rQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KGV7hBwAAAWJJREFUKBVtUDEsQ1EUve+1/SItKYMIkYpF06GJdGAwNFFGkxBEYupssRm6EpvJbpVoYhRd6FBikDSxYECsBpG25D/nvP/+p+Ik551z73v33feuyA/izq5CL8ET8ALcBolYIP+vd0ibX/yAT7uj2qkVzwWzUBa0nbacbkKJHi5dlYhXmARYeAS+MwCWA5FPqKIP/9IH/wiygMru5y5mcRYkPHYKP7gAPw4SDbCjRXMgRBJctM4t4ROriM2QSpmkeOtub6YfMYrZvelykbD1sxJVg+6AfKqURRKQLfA4JvoVWgIjDMNlGLVKZxNRFsZsoHGAgREZHKPlJEi2t7if3r2KKS9nVOo0rtNZ3yR7M/VGTqTy5Y4o/scWHBbKfIq0/eZ+x3850OZpaTTxlu/4D3ssuA72uxrYS2rFYjh+aRbmb24LpTVu1IqVKG8P/lmUEaNMxeh6fmquOhkMBE8JJ2yPfwPjdVhiDbiX6AAAAABJRU5ErkJggg==" alt="Astro logo" width="16" height="16" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
3
packages/astro/test/fixtures/core-image/src/pages/post.md
vendored
Normal file
3
packages/astro/test/fixtures/core-image/src/pages/post.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
![My article cover](../assets/penguin1.jpg)
|
||||
|
||||
Image worked
|
11
packages/astro/test/fixtures/core-image/src/pages/remote-error-no-dimensions.astro
vendored
Normal file
11
packages/astro/test/fixtures/core-image/src/pages/remote-error-no-dimensions.astro
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
import { Image } from 'astro:assets';
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<Image src="https://avatars.githubusercontent.com/u/622227?s=64" alt="fred" />
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue