Move image()
to be passed as part of schema
(#6703)
* feat(images): Move image() to schema so we can do relative images easily instead of clumsily * chore: changeset * fix: apply feedback * test: add more tests * fix: properly infer type if a function is used * feat(iamge): Add errors when using the old methods * chore: update to minor * feat(image): Move function shape to be experimental.asets only * Update packages/astro/src/content/template/virtual-mod.mjs Co-authored-by: Ben Holmes <hey@bholmes.dev> --------- Co-authored-by: Ben Holmes <hey@bholmes.dev>
This commit is contained in:
parent
9302c0d4d0
commit
a1108e0371
16 changed files with 148 additions and 151 deletions
22
.changeset/many-dancers-fold.md
Normal file
22
.changeset/many-dancers-fold.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Move `image()` to come from `schema` instead to fix it not working with refine and inside complex types
|
||||||
|
|
||||||
|
**Migration**:
|
||||||
|
|
||||||
|
Remove the `image` import from `astro:content`, and instead use a function to generate your schema, like such:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
|
||||||
|
defineCollection({
|
||||||
|
schema: ({ image }) =>
|
||||||
|
z.object({
|
||||||
|
image: image().refine((img) => img.width >= 200, {
|
||||||
|
message: "image too small",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
|
@ -3,19 +3,23 @@ import path from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import slash from 'slash';
|
import slash from 'slash';
|
||||||
import type { AstroConfig, AstroSettings } from '../../@types/astro';
|
import type { AstroConfig, AstroSettings } from '../../@types/astro';
|
||||||
import { imageMetadata } from './metadata.js';
|
import { imageMetadata, type Metadata } from './metadata.js';
|
||||||
|
|
||||||
export async function emitESMImage(
|
export async function emitESMImage(
|
||||||
id: string,
|
id: string | undefined,
|
||||||
watchMode: boolean,
|
watchMode: boolean,
|
||||||
fileEmitter: any,
|
fileEmitter: any,
|
||||||
settings: Pick<AstroSettings, 'config'>
|
settings: Pick<AstroSettings, 'config'>
|
||||||
) {
|
): Promise<Metadata | undefined> {
|
||||||
|
if (!id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const url = pathToFileURL(id);
|
const url = pathToFileURL(id);
|
||||||
const meta = await imageMetadata(url);
|
const meta = await imageMetadata(url);
|
||||||
|
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build
|
// Build
|
||||||
|
@ -48,13 +52,13 @@ export async function emitESMImage(
|
||||||
* due to Vite dependencies in core.
|
* due to Vite dependencies in core.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function rootRelativePath(config: Pick<AstroConfig, 'root'>, url: URL) {
|
function rootRelativePath(config: Pick<AstroConfig, 'root'>, url: URL): string {
|
||||||
const basePath = fileURLToNormalizedPath(url);
|
const basePath = fileURLToNormalizedPath(url);
|
||||||
const rootPath = fileURLToNormalizedPath(config.root);
|
const rootPath = fileURLToNormalizedPath(config.root);
|
||||||
return prependForwardSlash(basePath.slice(rootPath.length));
|
return prependForwardSlash(basePath.slice(rootPath.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
function prependForwardSlash(filePath: string) {
|
function prependForwardSlash(filePath: string): string {
|
||||||
return filePath[0] === '/' ? filePath : '/' + filePath;
|
return filePath[0] === '/' ? filePath : '/' + filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +68,6 @@ function fileURLToNormalizedPath(filePath: URL): string {
|
||||||
return slash(fileURLToPath(filePath) + filePath.search).replace(/\\/g, '/');
|
return slash(fileURLToPath(filePath) + filePath.search).replace(/\\/g, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emoji(char: string, fallback: string) {
|
export function emoji(char: string, fallback: string): string {
|
||||||
return process.platform !== 'win32' ? char : fallback;
|
return process.platform !== 'win32' ? char : fallback;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
import { pathToFileURL } from 'url';
|
import type { PluginContext } from 'rollup';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import {
|
import type { AstroSettings } from '../@types/astro.js';
|
||||||
imageMetadata as internalGetImageMetadata,
|
import { emitESMImage } from '../assets/index.js';
|
||||||
type Metadata,
|
|
||||||
} from '../assets/utils/metadata.js';
|
|
||||||
|
|
||||||
export function createImage(options: { assetsDir: string; relAssetsDir: string }) {
|
export function createImage(
|
||||||
|
settings: AstroSettings,
|
||||||
|
pluginContext: PluginContext,
|
||||||
|
entryFilePath: string
|
||||||
|
) {
|
||||||
return () => {
|
return () => {
|
||||||
if (options.assetsDir === 'undefined') {
|
return z.string().transform(async (imagePath, ctx) => {
|
||||||
throw new Error('Enable `experimental.assets` in your Astro config to use image()');
|
const resolvedFilePath = (await pluginContext.resolve(imagePath, entryFilePath))?.id;
|
||||||
}
|
const metadata = await emitESMImage(
|
||||||
|
resolvedFilePath,
|
||||||
|
pluginContext.meta.watchMode,
|
||||||
|
pluginContext.emitFile,
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
|
||||||
return z.string({ description: '__image' }).transform(async (imagePath, ctx) => {
|
if (!metadata) {
|
||||||
const imageMetadata = await getImageMetadata(pathToFileURL(imagePath));
|
|
||||||
|
|
||||||
if (!imageMetadata) {
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: 'custom',
|
code: 'custom',
|
||||||
message: `Image ${imagePath} does not exist. Is the path correct?`,
|
message: `Image ${imagePath} does not exist. Is the path correct?`,
|
||||||
|
@ -24,20 +28,7 @@ export function createImage(options: { assetsDir: string; relAssetsDir: string }
|
||||||
return z.NEVER;
|
return z.NEVER;
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageMetadata;
|
return metadata;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getImageMetadata(
|
|
||||||
imagePath: URL
|
|
||||||
): Promise<(Metadata & { __astro_asset: true }) | undefined> {
|
|
||||||
const meta = await internalGetImageMetadata(imagePath);
|
|
||||||
|
|
||||||
if (!meta) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete meta.orientation;
|
|
||||||
return { ...meta, __astro_asset: true };
|
|
||||||
}
|
|
||||||
|
|
26
packages/astro/src/content/template/types.d.ts
vendored
26
packages/astro/src/content/template/types.d.ts
vendored
|
@ -13,8 +13,27 @@ declare module 'astro:content' {
|
||||||
export type CollectionEntry<C extends keyof typeof entryMap> =
|
export type CollectionEntry<C extends keyof typeof entryMap> =
|
||||||
(typeof entryMap)[C][keyof (typeof entryMap)[C]];
|
(typeof entryMap)[C][keyof (typeof entryMap)[C]];
|
||||||
|
|
||||||
|
// TODO: Remove this when having this fallback is no longer relevant. 2.3? 3.0? - erika, 2023-04-04
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* `astro:content` no longer provide `image()`.
|
||||||
|
*
|
||||||
|
* Please use it through `schema`, like such:
|
||||||
|
* ```ts
|
||||||
|
* import { defineCollection, z } from "astro:content";
|
||||||
|
*
|
||||||
|
* defineCollection({
|
||||||
|
* schema: ({ image }) =>
|
||||||
|
* z.object({
|
||||||
|
* image: image(),
|
||||||
|
* }),
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const image: never;
|
||||||
|
|
||||||
// This needs to be in sync with ImageMetadata
|
// This needs to be in sync with ImageMetadata
|
||||||
export const image: () => import('astro/zod').ZodObject<{
|
type ImageFunction = () => import('astro/zod').ZodObject<{
|
||||||
src: import('astro/zod').ZodString;
|
src: import('astro/zod').ZodString;
|
||||||
width: import('astro/zod').ZodNumber;
|
width: import('astro/zod').ZodNumber;
|
||||||
height: import('astro/zod').ZodNumber;
|
height: import('astro/zod').ZodNumber;
|
||||||
|
@ -45,7 +64,7 @@ declare module 'astro:content' {
|
||||||
| import('astro/zod').ZodEffects<BaseSchemaWithoutEffects>;
|
| import('astro/zod').ZodEffects<BaseSchemaWithoutEffects>;
|
||||||
|
|
||||||
type BaseCollectionConfig<S extends BaseSchema> = {
|
type BaseCollectionConfig<S extends BaseSchema> = {
|
||||||
schema?: S;
|
schema?: S | (({ image }: { image: ImageFunction }) => S);
|
||||||
slug?: (entry: {
|
slug?: (entry: {
|
||||||
id: CollectionEntry<keyof typeof entryMap>['id'];
|
id: CollectionEntry<keyof typeof entryMap>['id'];
|
||||||
defaultSlug: string;
|
defaultSlug: string;
|
||||||
|
@ -81,8 +100,9 @@ declare module 'astro:content' {
|
||||||
filter?: (entry: CollectionEntry<C>) => unknown
|
filter?: (entry: CollectionEntry<C>) => unknown
|
||||||
): Promise<CollectionEntry<C>[]>;
|
): Promise<CollectionEntry<C>[]>;
|
||||||
|
|
||||||
|
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||||
type InferEntrySchema<C extends keyof typeof entryMap> = import('astro/zod').infer<
|
type InferEntrySchema<C extends keyof typeof entryMap> = import('astro/zod').infer<
|
||||||
Required<ContentConfig['collections'][C]>['schema']
|
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const entryMap: {
|
const entryMap: {
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { createImage } from 'astro/content/runtime-assets';
|
|
||||||
|
|
||||||
const assetsDir = '@@ASSETS_DIR@@';
|
|
||||||
|
|
||||||
export const image = createImage({
|
|
||||||
assetsDir,
|
|
||||||
});
|
|
|
@ -11,6 +11,13 @@ export function defineCollection(config) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this when having this fallback is no longer relevant. 2.3? 3.0? - erika, 2023-04-04
|
||||||
|
export const image = () => {
|
||||||
|
throw new Error(
|
||||||
|
'Importing `image()` from `astro:content` is no longer supported. See https://docs.astro.build/en/guides/assets/#update-content-collections-schemas for our new import instructions.'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const contentDir = '@@CONTENT_DIR@@';
|
const contentDir = '@@CONTENT_DIR@@';
|
||||||
|
|
||||||
const entryGlob = import.meta.glob('@@ENTRY_GLOB_PATH@@', {
|
const entryGlob = import.meta.glob('@@ENTRY_GLOB_PATH@@', {
|
||||||
|
|
|
@ -3,14 +3,14 @@ import matter from 'gray-matter';
|
||||||
import fsMod from 'node:fs';
|
import fsMod from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import type { EmitFile, PluginContext } from 'rollup';
|
import type { PluginContext } from 'rollup';
|
||||||
import { normalizePath, type ErrorPayload as ViteErrorPayload, type ViteDevServer } from 'vite';
|
import { normalizePath, type ViteDevServer, type ErrorPayload as ViteErrorPayload } from 'vite';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { AstroConfig, AstroSettings } from '../@types/astro.js';
|
import type { AstroConfig, AstroSettings } from '../@types/astro.js';
|
||||||
import { emitESMImage } from '../assets/utils/emitAsset.js';
|
|
||||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||||
import { CONTENT_TYPES_FILE } from './consts.js';
|
import { CONTENT_TYPES_FILE } from './consts.js';
|
||||||
import { errorMap } from './error-map.js';
|
import { errorMap } from './error-map.js';
|
||||||
|
import { createImage } from './runtime-assets.js';
|
||||||
|
|
||||||
export const collectionConfigParser = z.object({
|
export const collectionConfigParser = z.object({
|
||||||
schema: z.any().optional(),
|
schema: z.any().optional(),
|
||||||
|
@ -33,7 +33,6 @@ export type CollectionConfig = z.infer<typeof collectionConfigParser>;
|
||||||
export type ContentConfig = z.infer<typeof contentConfigParser>;
|
export type ContentConfig = z.infer<typeof contentConfigParser>;
|
||||||
|
|
||||||
type EntryInternal = { rawData: string | undefined; filePath: string };
|
type EntryInternal = { rawData: string | undefined; filePath: string };
|
||||||
|
|
||||||
export type EntryInfo = {
|
export type EntryInfo = {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
@ -45,31 +44,6 @@ export const msg = {
|
||||||
`${collection} does not have a config. We suggest adding one for type safety!`,
|
`${collection} does not have a config. We suggest adding one for type safety!`,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Mutate (arf) the entryData to reroute assets to their final paths
|
|
||||||
*/
|
|
||||||
export async function patchAssets(
|
|
||||||
frontmatterEntry: Record<string, any>,
|
|
||||||
watchMode: boolean,
|
|
||||||
fileEmitter: EmitFile,
|
|
||||||
astroSettings: AstroSettings
|
|
||||||
) {
|
|
||||||
for (const key of Object.keys(frontmatterEntry)) {
|
|
||||||
if (typeof frontmatterEntry[key] === 'object' && frontmatterEntry[key] !== null) {
|
|
||||||
if (frontmatterEntry[key]['__astro_asset']) {
|
|
||||||
frontmatterEntry[key] = await emitESMImage(
|
|
||||||
frontmatterEntry[key].src,
|
|
||||||
watchMode,
|
|
||||||
fileEmitter,
|
|
||||||
astroSettings
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await patchAssets(frontmatterEntry[key], watchMode, fileEmitter, astroSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEntrySlug({
|
export function getEntrySlug({
|
||||||
id,
|
id,
|
||||||
collection,
|
collection,
|
||||||
|
@ -89,71 +63,37 @@ export function getEntrySlug({
|
||||||
export async function getEntryData(
|
export async function getEntryData(
|
||||||
entry: EntryInfo & { unvalidatedData: Record<string, unknown>; _internal: EntryInternal },
|
entry: EntryInfo & { unvalidatedData: Record<string, unknown>; _internal: EntryInternal },
|
||||||
collectionConfig: CollectionConfig,
|
collectionConfig: CollectionConfig,
|
||||||
resolver: (idToResolve: string) => ReturnType<PluginContext['resolve']>
|
pluginContext: PluginContext,
|
||||||
|
settings: AstroSettings
|
||||||
) {
|
) {
|
||||||
// Remove reserved `slug` field before parsing data
|
// Remove reserved `slug` field before parsing data
|
||||||
let { slug, ...data } = entry.unvalidatedData;
|
let { slug, ...data } = entry.unvalidatedData;
|
||||||
if (collectionConfig.schema) {
|
|
||||||
// TODO: remove for 2.0 stable release
|
let schema = collectionConfig.schema;
|
||||||
if (
|
if (typeof schema === 'function') {
|
||||||
typeof collectionConfig.schema === 'object' &&
|
if (!settings.config.experimental.assets) {
|
||||||
!('safeParseAsync' in collectionConfig.schema)
|
throw new Error(
|
||||||
) {
|
'The function shape for schema can only be used when `experimental.assets` is enabled.'
|
||||||
throw new AstroError({
|
);
|
||||||
title: 'Invalid content collection config',
|
|
||||||
message: `New: Content collection schemas must be Zod objects. Update your collection config to use \`schema: z.object({...})\` instead of \`schema: {...}\`.`,
|
|
||||||
hint: 'See https://docs.astro.build/en/reference/api-reference/#definecollection for an example.',
|
|
||||||
code: 99999,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schema = schema({
|
||||||
|
image: createImage(settings, pluginContext, entry._internal.filePath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
// Catch reserved `slug` field inside schema
|
// Catch reserved `slug` field inside schema
|
||||||
// Note: will not warn for `z.union` or `z.intersection` schemas
|
// Note: will not warn for `z.union` or `z.intersection` schemas
|
||||||
if (
|
if (typeof schema === 'object' && 'shape' in schema && schema.shape.slug) {
|
||||||
typeof collectionConfig.schema === 'object' &&
|
|
||||||
'shape' in collectionConfig.schema &&
|
|
||||||
collectionConfig.schema.shape.slug
|
|
||||||
) {
|
|
||||||
throw new AstroError({
|
throw new AstroError({
|
||||||
...AstroErrorData.ContentSchemaContainsSlugError,
|
...AstroErrorData.ContentSchemaContainsSlugError,
|
||||||
message: AstroErrorData.ContentSchemaContainsSlugError.message(entry.collection),
|
message: AstroErrorData.ContentSchemaContainsSlugError.message(entry.collection),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve all the images referred to in the frontmatter from the file requesting them
|
|
||||||
*/
|
|
||||||
async function preprocessAssetPaths(object: Record<string, any>) {
|
|
||||||
if (typeof object !== 'object' || object === null) return;
|
|
||||||
|
|
||||||
for (let [schemaName, schema] of Object.entries<any>(object)) {
|
|
||||||
if (schema._def.description === '__image') {
|
|
||||||
object[schemaName] = z.preprocess(
|
|
||||||
async (value: unknown) => {
|
|
||||||
if (!value || typeof value !== 'string') return value;
|
|
||||||
return (
|
|
||||||
(await resolver(value))?.id ??
|
|
||||||
path.join(path.dirname(entry._internal.filePath), value)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
schema,
|
|
||||||
{ description: '__image' }
|
|
||||||
);
|
|
||||||
} else if ('shape' in schema) {
|
|
||||||
await preprocessAssetPaths(schema.shape);
|
|
||||||
} else if ('unwrap' in schema) {
|
|
||||||
const unwrapped = schema.unwrap().shape;
|
|
||||||
|
|
||||||
if (unwrapped) {
|
|
||||||
await preprocessAssetPaths(unwrapped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await preprocessAssetPaths(collectionConfig.schema.shape);
|
|
||||||
|
|
||||||
// Use `safeParseAsync` to allow async transforms
|
// Use `safeParseAsync` to allow async transforms
|
||||||
const parsed = await collectionConfig.schema.safeParseAsync(entry.unvalidatedData, {
|
const parsed = await schema.safeParseAsync(entry.unvalidatedData, {
|
||||||
errorMap,
|
errorMap,
|
||||||
});
|
});
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { AstroError } from '../core/errors/errors.js';
|
||||||
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
|
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
|
||||||
import { CONTENT_FLAG } from './consts.js';
|
import { CONTENT_FLAG } from './consts.js';
|
||||||
import {
|
import {
|
||||||
|
NoCollectionError,
|
||||||
getContentEntryExts,
|
getContentEntryExts,
|
||||||
getContentPaths,
|
getContentPaths,
|
||||||
getEntryData,
|
getEntryData,
|
||||||
|
@ -17,8 +18,6 @@ import {
|
||||||
getEntrySlug,
|
getEntrySlug,
|
||||||
getEntryType,
|
getEntryType,
|
||||||
globalContentConfigObserver,
|
globalContentConfigObserver,
|
||||||
NoCollectionError,
|
|
||||||
patchAssets,
|
|
||||||
type ContentConfig,
|
type ContentConfig,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
|
@ -235,11 +234,11 @@ export const _internal = {
|
||||||
? await getEntryData(
|
? await getEntryData(
|
||||||
{ id, collection, slug, _internal, unvalidatedData },
|
{ id, collection, slug, _internal, unvalidatedData },
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
(idToResolve: string) => pluginContext.resolve(idToResolve, fileId)
|
pluginContext,
|
||||||
|
settings
|
||||||
)
|
)
|
||||||
: unvalidatedData;
|
: unvalidatedData;
|
||||||
|
|
||||||
await patchAssets(data, pluginContext.meta.watchMode, pluginContext.emitFile, settings);
|
|
||||||
const contentEntryModule: ContentEntryModule = {
|
const contentEntryModule: ContentEntryModule = {
|
||||||
id,
|
id,
|
||||||
slug,
|
slug,
|
||||||
|
|
|
@ -24,9 +24,6 @@ export function astroContentVirtualModPlugin({
|
||||||
);
|
);
|
||||||
const contentEntryExts = getContentEntryExts(settings);
|
const contentEntryExts = getContentEntryExts(settings);
|
||||||
|
|
||||||
const assetsDir = settings.config.experimental.assets
|
|
||||||
? contentPaths.assetsDir.toString()
|
|
||||||
: 'undefined';
|
|
||||||
const extGlob =
|
const extGlob =
|
||||||
contentEntryExts.length === 1
|
contentEntryExts.length === 1
|
||||||
? // Wrapping {...} breaks when there is only one extension
|
? // Wrapping {...} breaks when there is only one extension
|
||||||
|
@ -38,14 +35,8 @@ export function astroContentVirtualModPlugin({
|
||||||
.replace('@@CONTENT_DIR@@', relContentDir)
|
.replace('@@CONTENT_DIR@@', relContentDir)
|
||||||
.replace('@@ENTRY_GLOB_PATH@@', entryGlob)
|
.replace('@@ENTRY_GLOB_PATH@@', entryGlob)
|
||||||
.replace('@@RENDER_ENTRY_GLOB_PATH@@', entryGlob);
|
.replace('@@RENDER_ENTRY_GLOB_PATH@@', entryGlob);
|
||||||
const virtualAssetsModContents = fsMod
|
|
||||||
.readFileSync(contentPaths.virtualAssetsModTemplate, 'utf-8')
|
|
||||||
.replace('@@ASSETS_DIR@@', assetsDir);
|
|
||||||
|
|
||||||
const astroContentVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
|
const astroContentVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
|
||||||
const allContents = settings.config.experimental.assets
|
|
||||||
? virtualModContents + virtualAssetsModContents
|
|
||||||
: virtualModContents;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'astro-content-virtual-mod-plugin',
|
name: 'astro-content-virtual-mod-plugin',
|
||||||
|
@ -58,7 +49,7 @@ export function astroContentVirtualModPlugin({
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === astroContentVirtualModuleId) {
|
if (id === astroContentVirtualModuleId) {
|
||||||
return {
|
return {
|
||||||
code: allContents,
|
code: virtualModContents,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -263,7 +263,7 @@ describe('astro:image', () => {
|
||||||
|
|
||||||
it('Adds the <img> tags', () => {
|
it('Adds the <img> tags', () => {
|
||||||
let $img = $('img');
|
let $img = $('img');
|
||||||
expect($img).to.have.a.lengthOf(4);
|
expect($img).to.have.a.lengthOf(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has proper source for directly used image', () => {
|
it('has proper source for directly used image', () => {
|
||||||
|
@ -271,6 +271,19 @@ describe('astro:image', () => {
|
||||||
expect($img.attr('src').startsWith('/src/')).to.equal(true);
|
expect($img.attr('src').startsWith('/src/')).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has proper source for refined image', () => {
|
||||||
|
let $img = $('#refined-image img');
|
||||||
|
expect($img.attr('src').startsWith('/src/')).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has proper sources for array of images', () => {
|
||||||
|
let $img = $('#array-of-images img');
|
||||||
|
const imgsSrcs = [];
|
||||||
|
$img.each((i, img) => imgsSrcs.push(img.attribs['src']));
|
||||||
|
expect($img).to.have.a.lengthOf(2);
|
||||||
|
expect(imgsSrcs.every((img) => img.startsWith('/src/'))).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
it('has proper attributes for optimized image through getImage', () => {
|
it('has proper attributes for optimized image through getImage', () => {
|
||||||
let $img = $('#optimized-image-get-image img');
|
let $img = $('#optimized-image-get-image img');
|
||||||
expect($img.attr('src').startsWith('/_image')).to.equal(true);
|
expect($img.attr('src').startsWith('/_image')).to.equal(true);
|
||||||
|
@ -365,7 +378,7 @@ describe('astro:image', () => {
|
||||||
it('properly error image in Markdown frontmatter is not found', async () => {
|
it('properly error image in Markdown frontmatter is not found', async () => {
|
||||||
logs.length = 0;
|
logs.length = 0;
|
||||||
let res = await fixture.fetch('/blog/one');
|
let res = await fixture.fetch('/blog/one');
|
||||||
const text = await res.text();
|
await res.text();
|
||||||
|
|
||||||
expect(logs).to.have.a.lengthOf(1);
|
expect(logs).to.have.a.lengthOf(1);
|
||||||
expect(logs[0].message).to.contain('does not exist. Is the path correct?');
|
expect(logs[0].message).to.contain('does not exist. Is the path correct?');
|
||||||
|
@ -374,7 +387,7 @@ describe('astro:image', () => {
|
||||||
it('properly error image in Markdown content is not found', async () => {
|
it('properly error image in Markdown content is not found', async () => {
|
||||||
logs.length = 0;
|
logs.length = 0;
|
||||||
let res = await fixture.fetch('/post');
|
let res = await fixture.fetch('/post');
|
||||||
const text = await res.text();
|
await res.text();
|
||||||
|
|
||||||
expect(logs).to.have.a.lengthOf(1);
|
expect(logs).to.have.a.lengthOf(1);
|
||||||
expect(logs[0].message).to.contain('Could not find requested image');
|
expect(logs[0].message).to.contain('Could not find requested image');
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineCollection, image, z } from "astro:content";
|
import { defineCollection, z } from "astro:content";
|
||||||
|
|
||||||
const blogCollection = defineCollection({
|
const blogCollection = defineCollection({
|
||||||
schema: z.object({
|
schema: ({image}) => z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
image: image(),
|
image: image(),
|
||||||
cover: z.object({
|
cover: z.object({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineCollection, image, z } from "astro:content";
|
import { defineCollection, z } from "astro:content";
|
||||||
|
|
||||||
const blogCollection = defineCollection({
|
const blogCollection = defineCollection({
|
||||||
schema: z.object({
|
schema: ({image}) => z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
image: image(),
|
image: image(),
|
||||||
cover: z.object({
|
cover: z.object({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineCollection, image, z } from "astro:content";
|
import { defineCollection, z } from "astro:content";
|
||||||
|
|
||||||
const blogCollection = defineCollection({
|
const blogCollection = defineCollection({
|
||||||
schema: z.object({
|
schema: ({image}) => z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
image: image(),
|
image: image(),
|
||||||
cover: z.object({
|
cover: z.object({
|
||||||
|
|
|
@ -3,6 +3,10 @@ title: One
|
||||||
image: ~/assets/penguin2.jpg
|
image: ~/assets/penguin2.jpg
|
||||||
cover:
|
cover:
|
||||||
image: ../../assets/penguin1.jpg
|
image: ../../assets/penguin1.jpg
|
||||||
|
arrayOfImages:
|
||||||
|
- ~/assets/penguin2.jpg
|
||||||
|
- ~/assets/penguin1.jpg
|
||||||
|
refinedImage: ../../assets/penguin1.jpg
|
||||||
---
|
---
|
||||||
|
|
||||||
# A post
|
# A post
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import { defineCollection, image, z } from "astro:content";
|
import { defineCollection, z } from "astro:content";
|
||||||
|
|
||||||
const blogCollection = defineCollection({
|
const blogCollection = defineCollection({
|
||||||
schema: z.object({
|
schema: ({image}) => z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
image: image(),
|
image: image(),
|
||||||
cover: z.object({
|
cover: z.object({
|
||||||
image: image()
|
image: image()
|
||||||
})
|
}),
|
||||||
|
arrayOfImages: z.array(image()),
|
||||||
|
refinedImage: image().refine((img) => img.width > 200)
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
blog: blogCollection
|
blog: blogCollection
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,6 +28,16 @@ const myImage = await getImage({src: entry.data.image});
|
||||||
<img src={entry.data.cover.image.src} width={entry.data.cover.image.width} height={entry.data.cover.image.height} />
|
<img src={entry.data.cover.image.src} width={entry.data.cover.image.width} height={entry.data.cover.image.height} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="array-of-images">
|
||||||
|
{
|
||||||
|
entry.data.arrayOfImages.map((image) => <img src={image.src} />)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="refined-image">
|
||||||
|
<img src={entry.data.refinedImage.src} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="optimized-image-get-image">
|
<div id="optimized-image-get-image">
|
||||||
<img src={myImage.src} {...myImage.attributes} />
|
<img src={myImage.src} {...myImage.attributes} />
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue