diff --git a/packages/astro/src/content/consts.ts b/packages/astro/src/content/consts.ts index 10734426b471..269b5a27d09d 100644 --- a/packages/astro/src/content/consts.ts +++ b/packages/astro/src/content/consts.ts @@ -27,3 +27,5 @@ export const CONTENT_TYPES_FILE = 'types.d.ts'; export const DATA_STORE_FILE = 'data-store.json'; export const ASSET_IMPORTS_FILE = 'assets.mjs'; + +export const CONTENT_LAYER_TYPE = 'experimental_content'; diff --git a/packages/astro/src/content/data-store.ts b/packages/astro/src/content/data-store.ts index 6c9b5ff69128..720be732b50d 100644 --- a/packages/astro/src/content/data-store.ts +++ b/packages/astro/src/content/data-store.ts @@ -1,8 +1,8 @@ import { promises as fs, type PathLike, existsSync } from 'fs'; -import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js'; -import { AstroError, AstroErrorData } from '../core/errors/index.js'; import type { MarkdownHeading } from '@astrojs/markdown-remark'; import * as devalue from 'devalue'; +import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js'; +import { AstroError, AstroErrorData } from '../core/errors/index.js'; const SAVE_DEBOUNCE_MS = 500; export interface RenderedContent { diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index f609f7bbd59d..abb66fac312b 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -4,7 +4,7 @@ import pLimit from 'p-limit'; import { ZodIssueCode, z } from 'zod'; import type { GetImageResult, ImageMetadata } from '../@types/astro.js'; import { imageSrcToImportId } from '../assets/utils/resolveImports.js'; -import { AstroError, AstroErrorData } from '../core/errors/index.js'; +import { AstroError, AstroErrorData, AstroUserError } from '../core/errors/index.js'; import { prependForwardSlash } from '../core/path.js'; import { type AstroComponentFactory, @@ -17,7 +17,7 @@ import { render as serverRender, unescapeHTML, } from '../runtime/server/index.js'; -import { IMAGE_IMPORT_PREFIX } from './consts.js'; +import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX } from './consts.js'; import { type DataEntry, globalDataStore } from './data-store.js'; import type { ContentLookupMap } from './utils.js'; type LazyImport = () => Promise; @@ -26,6 +26,16 @@ type CollectionToEntryMap = Record; type GetEntryImport = (collection: string, lookupId: string) => Promise; export function defineCollection(config: any) { + if ( + ('loader' in config && config.type !== CONTENT_LAYER_TYPE) || + (config.type === CONTENT_LAYER_TYPE && !('loader' in config)) + ) { + // TODO: when this moves out of experimental, we will set the type automatically + throw new AstroUserError( + 'Collections that use the content layer must have a `loader` defined and `type` set to `experimental_content`', + "Check your collection definitions in `src/content/config.*`.'" + ); + } if (!config.type) config.type = 'content'; return config; } @@ -62,7 +72,7 @@ export function createGetCollection({ }) { return async function getCollection(collection: string, filter?: (entry: any) => unknown) { const store = await globalDataStore.get(); - let type: 'content' | 'data' | 'experimental_data' | 'experimental_content'; + let type: 'content' | 'data'; if (collection in contentCollectionToEntryMap) { type = 'content'; } else if (collection in dataCollectionToEntryMap) { @@ -80,7 +90,6 @@ export function createGetCollection({ ...entry, data, collection, - render: () => renderEntry(entry), }); } return result; @@ -167,7 +176,6 @@ export function createGetEntryBySlug({ return { ...entry, collection, - render: () => renderEntry(entry), }; } if (!collectionNames.has(collection)) { @@ -201,7 +209,10 @@ export function createGetEntryBySlug({ export function createGetDataEntryById({ getEntryImport, collectionNames, -}: { getEntryImport: GetEntryImport; collectionNames: Set }) { +}: { + getEntryImport: GetEntryImport; + collectionNames: Set; +}) { return async function getDataEntryById(collection: string, id: string) { const store = await globalDataStore.get(); @@ -307,7 +318,6 @@ export function createGetEntry({ return { ...entry, collection, - render: () => renderEntry(entry), } as DataEntryResult | ContentEntryResult; } @@ -433,7 +443,14 @@ function updateImageReferencesInData>( }); } -async function renderEntry(entry?: DataEntry) { +export async function renderEntry( + entry: DataEntry | { render: () => Promise<{ Content: AstroComponentFactory }> } +) { + if (entry && 'render' in entry) { + // This is an old content collection entry, so we use its render method + return entry.render(); + } + const html = entry?.rendered?.metadata?.imagePaths?.length && entry.filePath ? await updateImageReferencesInBody(entry.rendered.html, entry.filePath) diff --git a/packages/astro/src/content/sync.ts b/packages/astro/src/content/sync.ts index 5d5c669fce2f..21c5e1b53367 100644 --- a/packages/astro/src/content/sync.ts +++ b/packages/astro/src/content/sync.ts @@ -5,7 +5,7 @@ import type { FSWatcher } from 'vite'; import xxhash from 'xxhash-wasm'; import type { AstroSettings } from '../@types/astro.js'; import type { Logger } from '../core/logger/core.js'; -import { ASSET_IMPORTS_FILE, DATA_STORE_FILE } from './consts.js'; +import { ASSET_IMPORTS_FILE, CONTENT_LAYER_TYPE, DATA_STORE_FILE } from './consts.js'; import type { DataStore } from './data-store.js'; import type { LoaderContext } from './loaders/types.js'; import { getEntryDataAndImages, globalContentConfigObserver, posixRelative } from './utils.js'; @@ -49,7 +49,7 @@ export async function syncContentLayer({ await Promise.all( Object.entries(contentConfig.config.collections).map(async ([name, collection]) => { - if (collection.type !== 'experimental_data' && collection.type !== 'experimental_content') { + if (collection.type !== CONTENT_LAYER_TYPE) { return; } diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 7398290db42e..07df4192efab 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -12,6 +12,7 @@ import { AstroError } from '../core/errors/errors.js'; import { AstroErrorData } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import { isRelativePath } from '../core/path.js'; +import { CONTENT_LAYER_TYPE } from './consts.js'; import { CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from './consts.js'; import { type ContentConfig, @@ -45,7 +46,7 @@ type CollectionEntryMap = { entries: Record; } | { - type: 'data' | 'experimental_content' | 'experimental_data'; + type: 'data' | typeof CONTENT_LAYER_TYPE; entries: Record; }; }; @@ -366,7 +367,7 @@ async function typeForCollection( } if ( - (collection?.type === 'experimental_data' || collection?.type === 'experimental_content') && + collection?.type === CONTENT_LAYER_TYPE && typeof collection.loader === 'object' && collection.loader.schema ) { @@ -426,8 +427,7 @@ async function writeContentFiles({ if ( collectionConfig?.type && collection.type !== 'unknown' && - collectionConfig.type !== 'experimental_data' && - collectionConfig.type !== 'experimental_content' && + collectionConfig.type !== CONTENT_LAYER_TYPE && collection.type !== collectionConfig.type ) { viteServer.hot.send({ @@ -450,7 +450,7 @@ async function writeContentFiles({ }); return; } - const resolvedType: 'content' | 'data' | 'experimental_data' | 'experimental_content' = + const resolvedType = collection.type === 'unknown' ? // Add empty / unknown collections to the data type map by default // This ensures `getCollection('empty-collection')` doesn't raise a type error @@ -477,11 +477,8 @@ async function writeContentFiles({ } contentTypesStr += `};\n`; break; - case 'experimental_data': - dataTypesStr += `${collectionKey}: Record;\n`; - break; - case 'experimental_content': - dataTypesStr += `${collectionKey}: Record;\n rendered?: RenderedContent \n}>;\n`; + case CONTENT_LAYER_TYPE: + dataTypesStr += `${collectionKey}: Record;\n`; break; case 'data': if (collectionEntryKeys.length === 0) { diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 1d153cdc9901..ce83b4e01dd2 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -15,7 +15,12 @@ import type { import { AstroError, AstroErrorData, MarkdownError, errorMap } from '../core/errors/index.js'; import { isYAMLException } from '../core/errors/utils.js'; import type { Logger } from '../core/logger/core.js'; -import { CONTENT_FLAGS, IMAGE_IMPORT_PREFIX, PROPAGATED_ASSET_FLAG } from './consts.js'; +import { + CONTENT_FLAGS, + CONTENT_LAYER_TYPE, + IMAGE_IMPORT_PREFIX, + PROPAGATED_ASSET_FLAG, +} from './consts.js'; import { createImage } from './runtime-assets.js'; /** * Amap from a collection + slug to the local file path. @@ -36,7 +41,7 @@ export const collectionConfigParser = z.union([ schema: z.any().optional(), }), z.object({ - type: z.union([z.literal('experimental_data'), z.literal('experimental_content')]), + type: z.literal(CONTENT_LAYER_TYPE), schema: z.any().optional(), loader: z.union([ z.function().returns( @@ -135,11 +140,7 @@ export async function getEntryDataAndImages< pluginContext?: PluginContext ): Promise<{ data: TOutputData; imageImports: Array }> { let data: TOutputData; - if ( - collectionConfig.type === 'data' || - collectionConfig.type === 'experimental_data' || - collectionConfig.type === 'experimental_content' - ) { + if (collectionConfig.type === 'data' || collectionConfig.type === CONTENT_LAYER_TYPE) { data = entry.unvalidatedData as TOutputData; } else { const { slug, ...unvalidatedData } = entry.unvalidatedData; @@ -155,10 +156,7 @@ export async function getEntryDataAndImages< schema = schema({ image: createImage(pluginContext, shouldEmitFile, entry._internal.filePath), }); - } else if ( - collectionConfig.type === 'experimental_data' || - collectionConfig.type === 'experimental_content' - ) { + } else if (collectionConfig.type === CONTENT_LAYER_TYPE) { schema = schema({ image: () => z.string().transform((val) => { diff --git a/packages/astro/src/core/dev/dev.ts b/packages/astro/src/core/dev/dev.ts index df77de800fe6..453f1cf0c7c9 100644 --- a/packages/astro/src/core/dev/dev.ts +++ b/packages/astro/src/core/dev/dev.ts @@ -6,6 +6,7 @@ import { performance } from 'perf_hooks'; import { gt, major, minor, patch } from 'semver'; import type * as vite from 'vite'; import type { AstroInlineConfig } from '../../@types/astro.js'; +import { DATA_STORE_FILE } from '../../content/consts.js'; import { DataStore, globalDataStore } from '../../content/data-store.js'; import { attachContentServerListeners } from '../../content/index.js'; import { syncContentLayer } from '../../content/sync.js'; @@ -19,7 +20,6 @@ import { fetchLatestAstroVersion, shouldCheckForUpdates, } from './update-check.js'; -import { DATA_STORE_FILE } from '../../content/consts.js'; export interface DevServer { address: AddressInfo; diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index 46fc74bb8fdb..bfd0467ae2c8 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -5,6 +5,7 @@ import { dim } from 'kleur/colors'; import { type HMRPayload, createServer } from 'vite'; import type { AstroConfig, AstroInlineConfig, AstroSettings } from '../../@types/astro.js'; import { getPackage } from '../../cli/install-package.js'; +import { DATA_STORE_FILE } from '../../content/consts.js'; import { DataStore, globalDataStore } from '../../content/data-store.js'; import { createContentTypesGenerator } from '../../content/index.js'; import { syncContentLayer } from '../../content/sync.js'; @@ -30,7 +31,6 @@ import type { Logger } from '../logger/core.js'; import { formatErrorMessage } from '../messages.js'; import { ensureProcessNodeEnv } from '../util.js'; import { setUpEnvTs } from './setup-env-ts.js'; -import { DATA_STORE_FILE } from '../../content/consts.js'; export type SyncOptions = { /** diff --git a/packages/astro/templates/content/module.mjs b/packages/astro/templates/content/module.mjs index b7afc9433978..2d395db49541 100644 --- a/packages/astro/templates/content/module.mjs +++ b/packages/astro/templates/content/module.mjs @@ -9,7 +9,7 @@ import { createReference, } from 'astro/content/runtime'; -export { defineCollection } from 'astro/content/runtime'; +export { defineCollection, renderEntry as render } from 'astro/content/runtime'; export { z } from 'astro/zod'; const contentDir = '@@CONTENT_DIR@@'; diff --git a/packages/astro/templates/content/types.d.ts b/packages/astro/templates/content/types.d.ts index 04e7cc28998c..8fa4d8cf318c 100644 --- a/packages/astro/templates/content/types.d.ts +++ b/packages/astro/templates/content/types.d.ts @@ -6,7 +6,7 @@ declare module 'astro:content' { remarkPluginFrontmatter: Record; }>; } - interface ContentLayerRenderer { + interface ContentLayerRenderResult { Content: import('astro/runtime/server/index.js').AstroComponentFactory; } @@ -109,6 +109,10 @@ declare module 'astro:content' { }[] ): Promise[]>; + export function render( + entry: AnyEntryMap[C][string] + ): Promise; + export function reference( collection: C ): import('astro/zod').ZodEffects< diff --git a/packages/astro/test/content-layer.test.js b/packages/astro/test/content-layer.test.js index 95960cd11921..a98ea7e238bd 100644 --- a/packages/astro/test/content-layer.test.js +++ b/packages/astro/test/content-layer.test.js @@ -3,8 +3,8 @@ import { promises as fs } from 'node:fs'; import { sep } from 'node:path'; import { sep as posixSep } from 'node:path/posix'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; import * as devalue from 'devalue'; +import { loadFixture } from './test-utils.js'; describe('Content Layer', () => { /** @type {import("./test-utils.js").Fixture} */ let fixture; diff --git a/packages/astro/test/fixtures/content-layer/src/content/config.ts b/packages/astro/test/fixtures/content-layer/src/content/config.ts index 1f89603ecdc1..d1c51d3ff82f 100644 --- a/packages/astro/test/fixtures/content-layer/src/content/config.ts +++ b/packages/astro/test/fixtures/content-layer/src/content/config.ts @@ -4,12 +4,12 @@ import { loader } from '../loaders/post-loader.js'; import { fileURLToPath } from 'node:url'; const blog = defineCollection({ - type: 'experimental_data', + type: 'experimental_content', loader: loader({ url: 'https://jsonplaceholder.typicode.com/posts' }), }); const dogs = defineCollection({ - type: 'experimental_data', + type: 'experimental_content', loader: file('src/data/dogs.json'), schema: z.object({ breed: z.string(), @@ -22,7 +22,7 @@ const dogs = defineCollection({ }); const cats = defineCollection({ - type: 'experimental_data', + type: 'experimental_content', loader: async function () { return [ { @@ -92,7 +92,7 @@ const numbers = defineCollection({ }); const increment = defineCollection({ - type: 'experimental_data', + type: 'experimental_content', loader: { name: 'increment-loader', load: async ({ store }) => { diff --git a/packages/astro/test/fixtures/content-layer/src/pages/spacecraft/[slug].astro b/packages/astro/test/fixtures/content-layer/src/pages/spacecraft/[slug].astro index 4df64a395f45..b380a03c37ea 100644 --- a/packages/astro/test/fixtures/content-layer/src/pages/spacecraft/[slug].astro +++ b/packages/astro/test/fixtures/content-layer/src/pages/spacecraft/[slug].astro @@ -1,6 +1,6 @@ --- import type { GetStaticPaths } from "astro"; -import { getCollection, getEntry } from "astro:content" +import { getCollection, getEntry, render } from "astro:content" import { Image } from "astro:assets" export const getStaticPaths = (async () => { @@ -22,7 +22,7 @@ export const getStaticPaths = (async () => { const { craft } = Astro.props as any let cat = craft.data.cat ? await getEntry(craft.data.cat) : undefined -const { Content } = await craft.render() +const { Content } = await render(craft) --- diff --git a/packages/astro/types/content.d.ts b/packages/astro/types/content.d.ts index b927e93fd03b..80ce804db0ab 100644 --- a/packages/astro/types/content.d.ts +++ b/packages/astro/types/content.d.ts @@ -58,7 +58,7 @@ declare module 'astro:content' { export type SchemaContext = { image: ImageFunction }; type ContentLayerConfig = { - type: 'experimental_data' | 'experimental_content'; + type: 'experimental_content'; schema?: S | ((context: SchemaContext) => S); loader: import('astro/loaders').Loader | (() => Array | Promise>); };