From 6b0abd6aeaaecc8503dd13699b451d21c0c99aad Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Wed, 29 Mar 2023 17:15:53 +0200 Subject: [PATCH] feat(images): Move image() to schema so we can do relative images easily instead of clumsily --- packages/astro/src/assets/utils/emitAsset.ts | 18 ++-- packages/astro/src/content/runtime-assets.ts | 49 +++++----- .../astro/src/content/template/types.d.ts | 4 +- .../content/template/virtual-mod-assets.mjs | 7 -- packages/astro/src/content/utils.ts | 98 +++---------------- .../content/vite-plugin-content-imports.ts | 7 +- .../vite-plugin-content-virtual-mod.ts | 11 +-- .../core-image-base/src/content/config.ts | 4 +- .../core-image-errors/src/content/config.ts | 4 +- .../core-image-ssg/src/content/config.ts | 4 +- .../fixtures/core-image/src/content/config.ts | 4 +- 11 files changed, 63 insertions(+), 147 deletions(-) delete mode 100644 packages/astro/src/content/template/virtual-mod-assets.mjs diff --git a/packages/astro/src/assets/utils/emitAsset.ts b/packages/astro/src/assets/utils/emitAsset.ts index 420264289..79775b96d 100644 --- a/packages/astro/src/assets/utils/emitAsset.ts +++ b/packages/astro/src/assets/utils/emitAsset.ts @@ -3,19 +3,23 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import slash from 'slash'; import type { AstroConfig, AstroSettings } from '../../@types/astro'; -import { imageMetadata } from './metadata.js'; +import { imageMetadata, type Metadata } from './metadata.js'; export async function emitESMImage( - id: string, + id: string | undefined, watchMode: boolean, fileEmitter: any, settings: Pick -) { +): Promise { + if (!id) { + return undefined; + } + const url = pathToFileURL(id); const meta = await imageMetadata(url); if (!meta) { - return; + return undefined; } // Build @@ -48,13 +52,13 @@ export async function emitESMImage( * due to Vite dependencies in core. */ -function rootRelativePath(config: Pick, url: URL) { +function rootRelativePath(config: Pick, url: URL): string { const basePath = fileURLToNormalizedPath(url); const rootPath = fileURLToNormalizedPath(config.root); return prependForwardSlash(basePath.slice(rootPath.length)); } -function prependForwardSlash(filePath: string) { +function prependForwardSlash(filePath: string): string { return filePath[0] === '/' ? filePath : '/' + filePath; } @@ -64,6 +68,6 @@ function fileURLToNormalizedPath(filePath: URL): string { 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; } diff --git a/packages/astro/src/content/runtime-assets.ts b/packages/astro/src/content/runtime-assets.ts index 99a83d143..78e979202 100644 --- a/packages/astro/src/content/runtime-assets.ts +++ b/packages/astro/src/content/runtime-assets.ts @@ -1,20 +1,28 @@ -import { pathToFileURL } from 'url'; +import type { PluginContext } from 'rollup'; import { z } from 'zod'; -import { - imageMetadata as internalGetImageMetadata, - type Metadata, -} from '../assets/utils/metadata.js'; +import type { AstroSettings } from '../@types/astro.js'; +import { emitESMImage } from '../assets/index.js'; + +export function createImage( + settings: AstroSettings, + pluginContext: PluginContext, + entryFilePath: string +) { + if (!settings.config.experimental.assets) { + throw new Error('Enable `experimental.assets` in your Astro config to use image()'); + } -export function createImage(options: { assetsDir: string; relAssetsDir: 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, ctx) => { + 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) => { - const imageMetadata = await getImageMetadata(pathToFileURL(imagePath)); - - if (!imageMetadata) { + if (!metadata) { ctx.addIssue({ code: 'custom', message: `Image ${imagePath} does not exist. Is the path correct?`, @@ -24,20 +32,7 @@ export function createImage(options: { assetsDir: string; relAssetsDir: string } 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 }; -} diff --git a/packages/astro/src/content/template/types.d.ts b/packages/astro/src/content/template/types.d.ts index 2485e2699..8912773d3 100644 --- a/packages/astro/src/content/template/types.d.ts +++ b/packages/astro/src/content/template/types.d.ts @@ -14,7 +14,7 @@ declare module 'astro:content' { (typeof entryMap)[C][keyof (typeof entryMap)[C]]; // 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; width: import('astro/zod').ZodNumber; height: import('astro/zod').ZodNumber; @@ -45,7 +45,7 @@ declare module 'astro:content' { | import('astro/zod').ZodEffects; type BaseCollectionConfig = { - schema?: S; + schema?: S | (({ image }: { image: ImageFunction }) => S); slug?: (entry: { id: CollectionEntry['id']; defaultSlug: string; diff --git a/packages/astro/src/content/template/virtual-mod-assets.mjs b/packages/astro/src/content/template/virtual-mod-assets.mjs deleted file mode 100644 index 5f2a1d54c..000000000 --- a/packages/astro/src/content/template/virtual-mod-assets.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { createImage } from 'astro/content/runtime-assets'; - -const assetsDir = '@@ASSETS_DIR@@'; - -export const image = createImage({ - assetsDir, -}); diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index e9c45c5bb..e5377eb73 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -3,14 +3,14 @@ import matter from 'gray-matter'; import fsMod from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import type { EmitFile, PluginContext } from 'rollup'; -import { normalizePath, type ErrorPayload as ViteErrorPayload, type ViteDevServer } from 'vite'; +import type { PluginContext } from 'rollup'; +import { normalizePath, type ViteDevServer, type ErrorPayload as ViteErrorPayload } from 'vite'; import { z } from 'zod'; import type { AstroConfig, AstroSettings } from '../@types/astro.js'; -import { emitESMImage } from '../assets/utils/emitAsset.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { CONTENT_TYPES_FILE } from './consts.js'; import { errorMap } from './error-map.js'; +import { createImage } from './runtime-assets.js'; export const collectionConfigParser = z.object({ schema: z.any().optional(), @@ -33,7 +33,6 @@ export type CollectionConfig = z.infer; export type ContentConfig = z.infer; type EntryInternal = { rawData: string | undefined; filePath: string }; - export type EntryInfo = { id: string; slug: string; @@ -45,31 +44,6 @@ export const msg = { `${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, - 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({ id, collection, @@ -89,71 +63,31 @@ export function getEntrySlug({ export async function getEntryData( entry: EntryInfo & { unvalidatedData: Record; _internal: EntryInternal }, collectionConfig: CollectionConfig, - resolver: (idToResolve: string) => ReturnType + pluginContext: PluginContext, + settings: AstroSettings ) { // Remove reserved `slug` field before parsing data let { slug, ...data } = entry.unvalidatedData; - if (collectionConfig.schema) { - // TODO: remove for 2.0 stable release - if ( - typeof collectionConfig.schema === 'object' && - !('safeParseAsync' in collectionConfig.schema) - ) { - 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, - }); - } + + const schema = + typeof collectionConfig.schema === 'function' + ? collectionConfig.schema({ + image: createImage(settings, pluginContext, entry._internal.filePath), + }) + : collectionConfig.schema; + + if (schema) { // Catch reserved `slug` field inside schema // Note: will not warn for `z.union` or `z.intersection` schemas - if ( - typeof collectionConfig.schema === 'object' && - 'shape' in collectionConfig.schema && - collectionConfig.schema.shape.slug - ) { + if (typeof schema === 'object' && 'shape' in schema && schema.shape.slug) { throw new AstroError({ ...AstroErrorData.ContentSchemaContainsSlugError, 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) { - if (typeof object !== 'object' || object === null) return; - - for (let [schemaName, schema] of Object.entries(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 - const parsed = await collectionConfig.schema.safeParseAsync(entry.unvalidatedData, { + const parsed = await schema.safeParseAsync(entry.unvalidatedData, { errorMap, }); if (parsed.success) { diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index 4437f4fa0..cd944731f 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -10,6 +10,7 @@ import { AstroError } from '../core/errors/errors.js'; import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js'; import { CONTENT_FLAG } from './consts.js'; import { + NoCollectionError, getContentEntryExts, getContentPaths, getEntryData, @@ -17,8 +18,6 @@ import { getEntrySlug, getEntryType, globalContentConfigObserver, - NoCollectionError, - patchAssets, type ContentConfig, } from './utils.js'; @@ -235,11 +234,11 @@ export const _internal = { ? await getEntryData( { id, collection, slug, _internal, unvalidatedData }, collectionConfig, - (idToResolve: string) => pluginContext.resolve(idToResolve, fileId) + pluginContext, + settings ) : unvalidatedData; - await patchAssets(data, pluginContext.meta.watchMode, pluginContext.emitFile, settings); const contentEntryModule: ContentEntryModule = { id, slug, diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts index faa6cb9be..3a72bf1de 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -24,9 +24,6 @@ export function astroContentVirtualModPlugin({ ); const contentEntryExts = getContentEntryExts(settings); - const assetsDir = settings.config.experimental.assets - ? contentPaths.assetsDir.toString() - : 'undefined'; const extGlob = contentEntryExts.length === 1 ? // Wrapping {...} breaks when there is only one extension @@ -38,14 +35,8 @@ export function astroContentVirtualModPlugin({ .replace('@@CONTENT_DIR@@', relContentDir) .replace('@@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 allContents = settings.config.experimental.assets - ? virtualModContents + virtualAssetsModContents - : virtualModContents; return { name: 'astro-content-virtual-mod-plugin', @@ -58,7 +49,7 @@ export function astroContentVirtualModPlugin({ load(id) { if (id === astroContentVirtualModuleId) { return { - code: allContents, + code: virtualModContents, }; } }, diff --git a/packages/astro/test/fixtures/core-image-base/src/content/config.ts b/packages/astro/test/fixtures/core-image-base/src/content/config.ts index b38ad070e..aa35fb4b6 100644 --- a/packages/astro/test/fixtures/core-image-base/src/content/config.ts +++ b/packages/astro/test/fixtures/core-image-base/src/content/config.ts @@ -1,7 +1,7 @@ -import { defineCollection, image, z } from "astro:content"; +import { defineCollection, z } from "astro:content"; const blogCollection = defineCollection({ - schema: z.object({ + schema: ({image}) => z.object({ title: z.string(), image: image(), cover: z.object({ diff --git a/packages/astro/test/fixtures/core-image-errors/src/content/config.ts b/packages/astro/test/fixtures/core-image-errors/src/content/config.ts index b38ad070e..aa35fb4b6 100644 --- a/packages/astro/test/fixtures/core-image-errors/src/content/config.ts +++ b/packages/astro/test/fixtures/core-image-errors/src/content/config.ts @@ -1,7 +1,7 @@ -import { defineCollection, image, z } from "astro:content"; +import { defineCollection, z } from "astro:content"; const blogCollection = defineCollection({ - schema: z.object({ + schema: ({image}) => z.object({ title: z.string(), image: image(), cover: z.object({ diff --git a/packages/astro/test/fixtures/core-image-ssg/src/content/config.ts b/packages/astro/test/fixtures/core-image-ssg/src/content/config.ts index b38ad070e..aa35fb4b6 100644 --- a/packages/astro/test/fixtures/core-image-ssg/src/content/config.ts +++ b/packages/astro/test/fixtures/core-image-ssg/src/content/config.ts @@ -1,7 +1,7 @@ -import { defineCollection, image, z } from "astro:content"; +import { defineCollection, z } from "astro:content"; const blogCollection = defineCollection({ - schema: z.object({ + schema: ({image}) => z.object({ title: z.string(), image: image(), cover: z.object({ diff --git a/packages/astro/test/fixtures/core-image/src/content/config.ts b/packages/astro/test/fixtures/core-image/src/content/config.ts index b38ad070e..aa35fb4b6 100644 --- a/packages/astro/test/fixtures/core-image/src/content/config.ts +++ b/packages/astro/test/fixtures/core-image/src/content/config.ts @@ -1,7 +1,7 @@ -import { defineCollection, image, z } from "astro:content"; +import { defineCollection, z } from "astro:content"; const blogCollection = defineCollection({ - schema: z.object({ + schema: ({image}) => z.object({ title: z.string(), image: image(), cover: z.object({