From 082fc52cf359a8b74985dd5c4605a84aa6b5547d Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 12 Nov 2025 15:53:49 +0100 Subject: [PATCH 01/29] feat: loader.getSchemaContext() --- packages/astro/src/content/content-layer.ts | 4 +- packages/astro/src/content/loaders/types.ts | 19 ++++-- packages/astro/src/content/types-generator.ts | 67 +++++++++++-------- packages/astro/src/content/utils.ts | 23 ++++--- 4 files changed, 70 insertions(+), 43 deletions(-) diff --git a/packages/astro/src/content/content-layer.ts b/packages/astro/src/content/content-layer.ts index 17335f54f1e5..f55fbbe46f92 100644 --- a/packages/astro/src/content/content-layer.ts +++ b/packages/astro/src/content/content-layer.ts @@ -253,8 +253,8 @@ class ContentLayer { if (!schema && typeof collection.loader === 'object') { schema = collection.loader.schema; - if (typeof schema === 'function') { - schema = await schema(); + if (!schema && collection.loader.getSchemaContext) { + ({ schema } = await collection.loader.getSchemaContext()); } } diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts index 6939357a5620..c48b35c56a98 100644 --- a/packages/astro/src/content/loaders/types.ts +++ b/packages/astro/src/content/loaders/types.ts @@ -49,14 +49,25 @@ export interface LoaderContext { entryTypes: Map; } -export interface Loader { +export type Loader = { /** Unique name of the loader, e.g. the npm package name */ name: string; /** Do the actual loading of the data */ load: (context: LoaderContext) => Promise; - /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ - schema?: ZodSchema | Promise | (() => ZodSchema | Promise); -} +} & ( + | { + /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ + schema?: ZodSchema; + getSchemaContext?: never; + } + | { + schema?: never; + getSchemaContext: () => Promise<{ + schema: ZodSchema; + types: string; + }>; + } +); export interface LoadEntryContext { filter: TEntryFilter extends never ? { id: string } : TEntryFilter; diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index de246950a7f8..456c54b6f88a 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -342,13 +342,13 @@ function normalizeConfigPath(from: string, to: string) { return `"${isRelativePath(configPath) ? '' : './'}${normalizedPath}"` as const; } -const schemaCache = new Map(); +const schemaContextResultCache = new Map(); -async function getContentLayerSchema( +async function getSchemaContextResult( collection: ContentConfig['collections'][T], collectionKey: T, -): Promise { - const cached = schemaCache.get(collectionKey); +) { + const cached = schemaContextResultCache.get(collectionKey); if (cached) { return cached; } @@ -356,19 +356,29 @@ async function getContentLayerSchema( + collection: ContentConfig['collections'][T], + collectionKey: T, +): Promise { + if (collection?.type !== CONTENT_LAYER_TYPE || typeof collection.loader === 'function') { + return; + } + if (collection.loader.schema) { + return collection.loader.schema; + } + const result = await getSchemaContextResult(collection, collectionKey); + return result?.schema; +} + async function typeForCollection( collection: ContentConfig['collections'][T] | undefined, collectionKey: T, @@ -376,24 +386,20 @@ async function typeForCollection( if (collection?.schema) { return `InferEntrySchema<${collectionKey}>`; } - if (!collection?.type) { + if (!collection?.type || typeof collection.loader === 'function') { return 'any'; } - const schema = await getContentLayerSchema(collection, collectionKey); - if (!schema) { - return 'any'; + if (collection.loader.schema) { + // TODO: this only works if a loader has info about its schema + return `InferLoaderSchema<${collectionKey}>`; } - try { - const zodToTs = await import('zod-to-ts'); - const ast = zodToTs.zodToTs(schema); - return zodToTs.printNode(ast.node); - } catch (err: any) { - // zod-to-ts is sad if we don't have TypeScript installed, but that's fine as we won't be needing types in that case - if (err.message.includes("Cannot find package 'typescript'")) { - return 'any'; - } - throw err; + const result = await getSchemaContextResult(collection, collectionKey); + if (!result) { + return 'any'; } + // TODO: inject result.types + const relativePath = 'TODO:'; + return `import(${relativePath}).Collection`; } async function writeContentFiles({ @@ -487,7 +493,12 @@ async function writeContentFiles({ const key = JSON.parse(collectionKey); contentCollectionManifest.collections.push({ - hasSchema: Boolean(collectionConfig?.schema || schemaCache.has(collectionKey)), + hasSchema: Boolean( + collectionConfig && + (collectionConfig.schema || + (typeof collectionConfig.loader !== 'function' && + (collectionConfig.loader.schema || schemaContextResultCache.has(collectionKey)))), + ), name: key, }); diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index cbd43317f140..11e582ff6356 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -7,7 +7,7 @@ import colors from 'picocolors'; import type { PluginContext } from 'rollup'; import type { ViteDevServer } from 'vite'; import xxhash from 'xxhash-wasm'; -import { z } from 'zod'; +import { type ZodSchema, z } from 'zod'; import { AstroError, AstroErrorData, errorMap, MarkdownError } from '../core/errors/index.js'; import { isYAMLException } from '../core/errors/utils.js'; import type { Logger } from '../core/logger/core.js'; @@ -60,14 +60,19 @@ const collectionConfigParser = z.union([ z.function(), z.object({ name: z.string(), - load: z.function().args(z.custom()).returns( - z.custom<{ - schema?: any; - types?: string; - } | void>(), - ), - schema: z.any().optional(), - render: z.function(z.tuple([z.any()], z.unknown())).optional(), + load: z.function().args(z.custom()).returns(z.promise(z.void())), + schema: z.custom((v) => '_def' in v).optional(), + getSchemaContext: z + .function() + .returns( + z.promise( + z.object({ + schema: z.custom((v) => '_def' in v), + types: z.string(), + }), + ), + ) + .optional(), }), ]), }), From fc7a78547194d30848ccaf93c9a2f787e173d749 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 12 Nov 2025 16:29:25 +0100 Subject: [PATCH 02/29] wip --- packages/astro/src/content/types-generator.ts | 38 ++++++++++++++----- packages/astro/templates/content/types.d.ts | 3 ++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 456c54b6f88a..f57e4f4f8da1 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -11,6 +11,7 @@ import type { Logger } from '../core/logger/core.js'; import { isRelativePath } from '../core/path.js'; import type { AstroSettings } from '../types/astro.js'; import type { ContentEntryType } from '../types/public/content.js'; +import type { InjectedType } from '../types/public/integrations.js'; import { COLLECTIONS_DIR, CONTENT_LAYER_TYPE, @@ -382,24 +383,29 @@ async function getContentLayerSchema( collection: ContentConfig['collections'][T] | undefined, collectionKey: T, -): Promise { +): Promise<{ type: string; injectedType?: InjectedType }> { if (collection?.schema) { - return `InferEntrySchema<${collectionKey}>`; + return { type: `InferEntrySchema<${collectionKey}>` }; } if (!collection?.type || typeof collection.loader === 'function') { - return 'any'; + return { type: 'any' }; } if (collection.loader.schema) { // TODO: this only works if a loader has info about its schema - return `InferLoaderSchema<${collectionKey}>`; + return { type: `InferLoaderSchema<${collectionKey}>` }; } const result = await getSchemaContextResult(collection, collectionKey); if (!result) { - return 'any'; + return { type: 'any' }; } - // TODO: inject result.types - const relativePath = 'TODO:'; - return `import(${relativePath}).Collection`; + const filename = `loaders/${collectionKey}.ts`; + return { + type: `import(${filename}).Collection`, + injectedType: { + filename, + content: result.types, + }, + }; } async function writeContentFiles({ @@ -466,7 +472,21 @@ async function writeContentFiles({ return; } - const dataType = await typeForCollection(collectionConfig, collectionKey); + const { type: dataType, injectedType } = await typeForCollection( + collectionConfig, + collectionKey, + ); + + if (injectedType) { + if (settings.injectedTypes.some((t) => t.filename === CONTENT_TYPES_FILE)) { + // If it's the first time, we inject types the usual way. sync() will handle creating files and references. If it's not the first time, we just override the dts content + const url = new URL(injectedType.filename, settings.dotAstroDir); + await fs.promises.mkdir(path.dirname(fileURLToPath(url)), { recursive: true }); + await fs.promises.writeFile(url, injectedType.content, 'utf-8'); + } else { + settings.injectedTypes.push(injectedType); + } + } dataTypesStr += `${collectionKey}: Record;\n`; diff --git a/packages/astro/templates/content/types.d.ts b/packages/astro/templates/content/types.d.ts index 12d008e3d6e3..425a2017a277 100644 --- a/packages/astro/templates/content/types.d.ts +++ b/packages/astro/templates/content/types.d.ts @@ -104,6 +104,9 @@ declare module 'astro:content' { type InferEntrySchema = import('astro/zod').infer< ReturnTypeOrOriginal['schema']> >; + type InferLoaderSchema = import('astro/zod').infer< + ReturnTypeOrOriginal['loader']['schema']> + >; type DataEntryMap = { // @@DATA_ENTRY_MAP@@ From 865e64ba008d3b1cc8f73871447168ee0cbd0220 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 12 Nov 2025 16:31:18 +0100 Subject: [PATCH 03/29] wip --- packages/astro/src/content/loaders/config.ts | 6 ++++++ packages/astro/src/content/loaders/index.ts | 1 + packages/astro/src/content/loaders/types.ts | 4 ++-- packages/astro/src/content/types-generator.ts | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 packages/astro/src/content/loaders/config.ts diff --git a/packages/astro/src/content/loaders/config.ts b/packages/astro/src/content/loaders/config.ts new file mode 100644 index 000000000000..f2e9f5d54e47 --- /dev/null +++ b/packages/astro/src/content/loaders/config.ts @@ -0,0 +1,6 @@ +import type { ZodSchema } from 'zod'; +import type { Loader } from './types.js'; + +export function defineContentLoader(loader: Loader) { + return loader; +} diff --git a/packages/astro/src/content/loaders/index.ts b/packages/astro/src/content/loaders/index.ts index 30b4bfbe5334..51a2de9fb748 100644 --- a/packages/astro/src/content/loaders/index.ts +++ b/packages/astro/src/content/loaders/index.ts @@ -1,3 +1,4 @@ +export { defineContentLoader } from './config.js'; export { file } from './file.js'; export { glob } from './glob.js'; export * from './types.js'; diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts index c48b35c56a98..14beae56b666 100644 --- a/packages/astro/src/content/loaders/types.ts +++ b/packages/astro/src/content/loaders/types.ts @@ -49,7 +49,7 @@ export interface LoaderContext { entryTypes: Map; } -export type Loader = { +export type Loader = { /** Unique name of the loader, e.g. the npm package name */ name: string; /** Do the actual loading of the data */ @@ -57,7 +57,7 @@ export type Loader = { } & ( | { /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ - schema?: ZodSchema; + schema?: T; getSchemaContext?: never; } | { diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index f57e4f4f8da1..3d8786532842 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -391,7 +391,6 @@ async function typeForCollection( return { type: 'any' }; } if (collection.loader.schema) { - // TODO: this only works if a loader has info about its schema return { type: `InferLoaderSchema<${collectionKey}>` }; } const result = await getSchemaContextResult(collection, collectionKey); From 144c7916e8e98dd74e5288fa494a4657ac928ff2 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 12 Nov 2025 16:33:09 +0100 Subject: [PATCH 04/29] wip --- packages/astro/src/content/loaders/file.ts | 9 +++++---- packages/astro/src/content/loaders/glob.ts | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/astro/src/content/loaders/file.ts b/packages/astro/src/content/loaders/file.ts index c1b4b2a075a6..09b69f148884 100644 --- a/packages/astro/src/content/loaders/file.ts +++ b/packages/astro/src/content/loaders/file.ts @@ -5,7 +5,8 @@ import toml from 'smol-toml'; import { FileGlobNotSupported, FileParserNotFound } from '../../core/errors/errors-data.js'; import { AstroError } from '../../core/errors/index.js'; import { posixRelative } from '../utils.js'; -import type { Loader, LoaderContext } from './types.js'; +import { defineContentLoader } from './config.js'; +import type { LoaderContext } from './types.js'; interface FileOptions { /** @@ -22,7 +23,7 @@ interface FileOptions { * @param fileName The path to the JSON file to load, relative to the content directory. * @param options Additional options for the file loader */ -export function file(fileName: string, options?: FileOptions): Loader { +export function file(fileName: string, options?: FileOptions) { if (fileName.includes('*')) { throw new AstroError(FileGlobNotSupported); } @@ -102,7 +103,7 @@ export function file(fileName: string, options?: FileOptions): Loader { } } - return { + return defineContentLoader({ name: 'file-loader', load: async (context) => { const { config, logger, watcher } = context; @@ -125,5 +126,5 @@ export function file(fileName: string, options?: FileOptions): Loader { } }); }, - }; + }); } diff --git a/packages/astro/src/content/loaders/glob.ts b/packages/astro/src/content/loaders/glob.ts index 99d0dbaca84f..a94715104c50 100644 --- a/packages/astro/src/content/loaders/glob.ts +++ b/packages/astro/src/content/loaders/glob.ts @@ -8,7 +8,7 @@ import { glob as tinyglobby } from 'tinyglobby'; import type { ContentEntryRenderFunction, ContentEntryType } from '../../types/public/content.js'; import type { RenderedContent } from '../data-store.js'; import { getContentEntryIdAndSlug, posixRelative } from '../utils.js'; -import type { Loader } from './types.js'; +import { defineContentLoader } from './config.js'; interface GenerateIdOptions { /** The path to the entry file, relative to the base directory. */ @@ -57,7 +57,7 @@ function checkPrefix(pattern: string | Array, prefix: string) { * @param pattern A glob pattern to match files, relative to the content directory. */ -export function glob(globOptions: GlobOptions): Loader { +export function glob(globOptions: GlobOptions) { if (checkPrefix(globOptions.pattern, '../')) { throw new Error( 'Glob patterns cannot start with `../`. Set the `base` option to a parent directory instead.', @@ -73,7 +73,7 @@ export function glob(globOptions: GlobOptions): Loader { const fileToIdMap = new Map(); - return { + return defineContentLoader({ name: 'glob-loader', load: async ({ config, logger, watcher, parseData, store, generateDigest, entryTypes }) => { const renderFunctionByContentType = new WeakMap< @@ -315,5 +315,5 @@ export function glob(globOptions: GlobOptions): Loader { } }); }, - }; + }); } From 0ecc8d5775d342a79f4b67a359e7f41b2df8ab4a Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 11:34:20 +0100 Subject: [PATCH 05/29] fix --- examples/blog/src/content.config.ts | 57 ++++++-- packages/astro/src/content/config.ts | 124 ++++++++---------- packages/astro/src/content/loaders/config.ts | 2 +- packages/astro/src/content/loaders/types.ts | 40 +++--- packages/astro/src/content/types-generator.ts | 4 +- packages/astro/templates/content/types.d.ts | 2 +- packages/astro/types/content.d.ts | 12 +- 7 files changed, 129 insertions(+), 112 deletions(-) diff --git a/examples/blog/src/content.config.ts b/examples/blog/src/content.config.ts index ce37c7f6ff34..64c9967b0781 100644 --- a/examples/blog/src/content.config.ts +++ b/examples/blog/src/content.config.ts @@ -1,19 +1,54 @@ import { defineCollection, z } from 'astro:content'; -import { glob } from 'astro/loaders'; +import { defineContentLoader, glob } from 'astro/loaders'; + +const test = (options: Parameters[0]) => + defineContentLoader({ + name: 'test', + load: glob(options).load, + // schema: z.object({ + // title: z.string(), + // description: z.string(), + // // Transform string to Date object + // pubDate: z.coerce.date(), + // updatedDate: z.coerce.date().optional(), + // heroImage: z.string().optional(), + // }), + getSchemaContext: async () => { + return { + schema: z.object({ + title: z.string(), + description: z.string(), + // Transform string to Date object + pubDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + heroImage: z.string().optional(), + }), + types: `import type { ImageMetadata } from "astro"; + +export interface Collection { + title: string; + description: string; + pubDate: Date; + updateDate: Date | undefined; + heroImage: ImageMetadata | undefined; +}`, + }; + }, + }); const blog = defineCollection({ // Load Markdown and MDX files in the `src/content/blog/` directory. - loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }), + loader: test({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }), // Type-check frontmatter using a schema - schema: ({ image }) => - z.object({ - title: z.string(), - description: z.string(), - // Transform string to Date object - pubDate: z.coerce.date(), - updatedDate: z.coerce.date().optional(), - heroImage: image().optional(), - }), + // schema: ({ image }) => + // z.object({ + // title: z.string(), + // description: z.string(), + // // Transform string to Date object + // pubDate: z.coerce.date(), + // updatedDate: z.coerce.date().optional(), + // heroImage: image().optional(), + // }), }); export const collections = { blog }; diff --git a/packages/astro/src/content/config.ts b/packages/astro/src/content/config.ts index e6424b5b7b10..373fa35326c8 100644 --- a/packages/astro/src/content/config.ts +++ b/packages/astro/src/content/config.ts @@ -71,29 +71,6 @@ export type BaseSchema = ZodType; export type SchemaContext = { image: ImageFunction }; -type ContentLayerConfig = { - type?: 'content_layer'; - schema?: S | ((context: SchemaContext) => S); - loader: - | Loader - | (() => - | Array - | Promise> - | Record & { id?: string }> - | Promise & { id?: string }>>); -}; - -type DataCollectionConfig = { - type: 'data'; - schema?: S | ((context: SchemaContext) => S); -}; - -type ContentCollectionConfig = { - type?: 'content'; - schema?: S | ((context: SchemaContext) => S); - loader?: never; -}; - export type LiveCollectionConfig< L extends LiveLoader, S extends BaseSchema | undefined = undefined, @@ -103,10 +80,22 @@ export type LiveCollectionConfig< loader: L; }; -export type CollectionConfig = - | ContentCollectionConfig - | DataCollectionConfig - | ContentLayerConfig; +type LoaderConstraint = + | Loader + | (() => + | Array + | Promise> + | Record & { id?: string }> + | Promise & { id?: string }>>); + +export type CollectionConfig< + TSchema extends BaseSchema, + TLoader extends LoaderConstraint<{ id: string }>, +> = { + type?: 'content_layer'; + schema?: TSchema | ((context: SchemaContext) => TSchema); + loader: TLoader; +}; export function defineLiveCollection< L extends LiveLoader, @@ -167,44 +156,45 @@ export function defineLiveCollection< return config; } -export function defineCollection( - config: CollectionConfig, -): CollectionConfig { - const importerFilename = getImporterFilename(); - - if (importerFilename?.includes('live.config')) { - throw new AstroError({ - ...AstroErrorData.LiveContentConfigError, - message: AstroErrorData.LiveContentConfigError.message( - 'Collections in a live config file must use `defineLiveCollection`.', - importerFilename, - ), - }); - } - - if (!('loader' in config)) { - throw new AstroError({ - ...AstroErrorData.ContentCollectionMissingLoader, - message: AstroErrorData.ContentCollectionMissingLoader.message(importerFilename), - }); +export function defineCollection< + TSchema extends BaseSchema, + TLoader extends LoaderConstraint<{ id: string }>, + >(config: CollectionConfig): CollectionConfig { + const importerFilename = getImporterFilename(); + + if (importerFilename?.includes('live.config')) { + throw new AstroError({ + ...AstroErrorData.LiveContentConfigError, + message: AstroErrorData.LiveContentConfigError.message( + 'Collections in a live config file must use `defineLiveCollection`.', + importerFilename, + ), + }); + } + + if (!('loader' in config)) { + throw new AstroError({ + ...AstroErrorData.ContentCollectionMissingLoader, + message: AstroErrorData.ContentCollectionMissingLoader.message(importerFilename), + }); + } + + if (config.type && config.type !== CONTENT_LAYER_TYPE) { + throw new AstroError({ + ...AstroErrorData.ContentCollectionInvalidType, + message: AstroErrorData.ContentCollectionInvalidType.message(config.type, importerFilename), + }); + } + + if ( + typeof config.loader === 'object' && + typeof config.loader.load !== 'function' && + ('loadEntry' in config.loader || 'loadCollection' in config.loader) + ) { + throw new AstroUserError( + `Live content collections must be defined in "src/live.config.ts" file. Check your collection definitions in "${importerFilename ?? 'your content config file'}" to ensure you are not using a live loader.`, + ); + } + config.type = CONTENT_LAYER_TYPE; + return config; } - - if (config.type && config.type !== CONTENT_LAYER_TYPE) { - throw new AstroError({ - ...AstroErrorData.ContentCollectionInvalidType, - message: AstroErrorData.ContentCollectionInvalidType.message(config.type, importerFilename), - }); - } - - if ( - typeof config.loader === 'object' && - typeof config.loader.load !== 'function' && - ('loadEntry' in config.loader || 'loadCollection' in config.loader) - ) { - throw new AstroUserError( - `Live content collections must be defined in "src/live.config.ts" file. Check your collection definitions in "${importerFilename ?? 'your content config file'}" to ensure you are not using a live loader.`, - ); - } - config.type = CONTENT_LAYER_TYPE; - return config; -} diff --git a/packages/astro/src/content/loaders/config.ts b/packages/astro/src/content/loaders/config.ts index f2e9f5d54e47..0616d7beb9ca 100644 --- a/packages/astro/src/content/loaders/config.ts +++ b/packages/astro/src/content/loaders/config.ts @@ -1,6 +1,6 @@ import type { ZodSchema } from 'zod'; import type { Loader } from './types.js'; -export function defineContentLoader(loader: Loader) { +export function defineContentLoader(loader: Loader) { return loader; } diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts index 14beae56b666..8e37f9e5f470 100644 --- a/packages/astro/src/content/loaders/types.ts +++ b/packages/astro/src/content/loaders/types.ts @@ -49,25 +49,27 @@ export interface LoaderContext { entryTypes: Map; } -export type Loader = { - /** Unique name of the loader, e.g. the npm package name */ - name: string; - /** Do the actual loading of the data */ - load: (context: LoaderContext) => Promise; -} & ( - | { - /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ - schema?: T; - getSchemaContext?: never; - } - | { - schema?: never; - getSchemaContext: () => Promise<{ - schema: ZodSchema; - types: string; - }>; - } -); +export type Loader = { + /** Unique name of the loader, e.g. the npm package name */ + name: string; + /** Do the actual loading of the data */ + load: (context: LoaderContext) => Promise; + } & ([T] extends [never] + ? + | { + /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ + schema?: T; + } + | { + getSchemaContext?: () => Promise<{ + schema: ZodSchema; + types: string; + }>; + } + : { + /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ + schema: T; + }); export interface LoadEntryContext { filter: TEntryFilter extends never ? { id: string } : TEntryFilter; diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 3d8786532842..b77dae648bf0 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -397,9 +397,9 @@ async function typeForCollection( if (!result) { return { type: 'any' }; } - const filename = `loaders/${collectionKey}.ts`; + const filename = `loaders/${collectionKey.slice(1,-1)}.ts`; return { - type: `import(${filename}).Collection`, + type: `import("./${filename}").Collection`, injectedType: { filename, content: result.types, diff --git a/packages/astro/templates/content/types.d.ts b/packages/astro/templates/content/types.d.ts index 425a2017a277..9caa399bc7af 100644 --- a/packages/astro/templates/content/types.d.ts +++ b/packages/astro/templates/content/types.d.ts @@ -105,7 +105,7 @@ declare module 'astro:content' { ReturnTypeOrOriginal['schema']> >; type InferLoaderSchema = import('astro/zod').infer< - ReturnTypeOrOriginal['loader']['schema']> + Required['loader']['schema'] >; type DataEntryMap = { diff --git a/packages/astro/types/content.d.ts b/packages/astro/types/content.d.ts index cb5b6d7360b8..36864a923e63 100644 --- a/packages/astro/types/content.d.ts +++ b/packages/astro/types/content.d.ts @@ -8,17 +8,7 @@ declare module 'astro:content' { BaseSchema, SchemaContext, } from 'astro/content/config'; - - export function defineLiveCollection< - L extends import('astro/loaders').LiveLoader, - S extends import('astro/content/config').BaseSchema | undefined = undefined, - >( - config: import('astro/content/config').LiveCollectionConfig, - ): import('astro/content/config').LiveCollectionConfig; - - export function defineCollection( - config: import('astro/content/config').CollectionConfig, - ): import('astro/content/config').CollectionConfig; + export { defineLiveCollection, defineCollection } from 'astro/content/config'; /** Run `astro dev` or `astro sync` to generate high fidelity types */ export const getEntryBySlug: (...args: any[]) => any; From 85fb14a331a5d6cc0660f1f94a059ce4a6c84bb5 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 11:35:36 +0100 Subject: [PATCH 06/29] chore: format --- packages/astro/src/content/config.ts | 80 +++++++++---------- packages/astro/src/content/loaders/types.ts | 40 +++++----- packages/astro/src/content/types-generator.ts | 2 +- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/packages/astro/src/content/config.ts b/packages/astro/src/content/config.ts index 373fa35326c8..a148d8ced405 100644 --- a/packages/astro/src/content/config.ts +++ b/packages/astro/src/content/config.ts @@ -157,44 +157,44 @@ export function defineLiveCollection< } export function defineCollection< - TSchema extends BaseSchema, - TLoader extends LoaderConstraint<{ id: string }>, - >(config: CollectionConfig): CollectionConfig { - const importerFilename = getImporterFilename(); - - if (importerFilename?.includes('live.config')) { - throw new AstroError({ - ...AstroErrorData.LiveContentConfigError, - message: AstroErrorData.LiveContentConfigError.message( - 'Collections in a live config file must use `defineLiveCollection`.', - importerFilename, - ), - }); - } - - if (!('loader' in config)) { - throw new AstroError({ - ...AstroErrorData.ContentCollectionMissingLoader, - message: AstroErrorData.ContentCollectionMissingLoader.message(importerFilename), - }); - } - - if (config.type && config.type !== CONTENT_LAYER_TYPE) { - throw new AstroError({ - ...AstroErrorData.ContentCollectionInvalidType, - message: AstroErrorData.ContentCollectionInvalidType.message(config.type, importerFilename), - }); - } - - if ( - typeof config.loader === 'object' && - typeof config.loader.load !== 'function' && - ('loadEntry' in config.loader || 'loadCollection' in config.loader) - ) { - throw new AstroUserError( - `Live content collections must be defined in "src/live.config.ts" file. Check your collection definitions in "${importerFilename ?? 'your content config file'}" to ensure you are not using a live loader.`, - ); - } - config.type = CONTENT_LAYER_TYPE; - return config; + TSchema extends BaseSchema, + TLoader extends LoaderConstraint<{ id: string }>, +>(config: CollectionConfig): CollectionConfig { + const importerFilename = getImporterFilename(); + + if (importerFilename?.includes('live.config')) { + throw new AstroError({ + ...AstroErrorData.LiveContentConfigError, + message: AstroErrorData.LiveContentConfigError.message( + 'Collections in a live config file must use `defineLiveCollection`.', + importerFilename, + ), + }); } + + if (!('loader' in config)) { + throw new AstroError({ + ...AstroErrorData.ContentCollectionMissingLoader, + message: AstroErrorData.ContentCollectionMissingLoader.message(importerFilename), + }); + } + + if (config.type && config.type !== CONTENT_LAYER_TYPE) { + throw new AstroError({ + ...AstroErrorData.ContentCollectionInvalidType, + message: AstroErrorData.ContentCollectionInvalidType.message(config.type, importerFilename), + }); + } + + if ( + typeof config.loader === 'object' && + typeof config.loader.load !== 'function' && + ('loadEntry' in config.loader || 'loadCollection' in config.loader) + ) { + throw new AstroUserError( + `Live content collections must be defined in "src/live.config.ts" file. Check your collection definitions in "${importerFilename ?? 'your content config file'}" to ensure you are not using a live loader.`, + ); + } + config.type = CONTENT_LAYER_TYPE; + return config; +} diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts index 8e37f9e5f470..8ad19043607d 100644 --- a/packages/astro/src/content/loaders/types.ts +++ b/packages/astro/src/content/loaders/types.ts @@ -50,26 +50,26 @@ export interface LoaderContext { } export type Loader = { - /** Unique name of the loader, e.g. the npm package name */ - name: string; - /** Do the actual loading of the data */ - load: (context: LoaderContext) => Promise; - } & ([T] extends [never] - ? - | { - /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ - schema?: T; - } - | { - getSchemaContext?: () => Promise<{ - schema: ZodSchema; - types: string; - }>; - } - : { - /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ - schema: T; - }); + /** Unique name of the loader, e.g. the npm package name */ + name: string; + /** Do the actual loading of the data */ + load: (context: LoaderContext) => Promise; +} & ([T] extends [never] + ? + | { + /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ + schema?: T; + } + | { + getSchemaContext?: () => Promise<{ + schema: ZodSchema; + types: string; + }>; + } + : { + /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ + schema: T; + }); export interface LoadEntryContext { filter: TEntryFilter extends never ? { id: string } : TEntryFilter; diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index b77dae648bf0..2204beca3a16 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -397,7 +397,7 @@ async function typeForCollection( if (!result) { return { type: 'any' }; } - const filename = `loaders/${collectionKey.slice(1,-1)}.ts`; + const filename = `loaders/${collectionKey.slice(1, -1)}.ts`; return { type: `import("./${filename}").Collection`, injectedType: { From 91a4d99e6f5248d9e29f7fab7ba60ee28c5065f9 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 11:45:46 +0100 Subject: [PATCH 07/29] feat: error --- packages/astro/src/content/utils.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 11e582ff6356..1413a163baf9 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -61,7 +61,27 @@ const collectionConfigParser = z.union([ z.object({ name: z.string(), load: z.function().args(z.custom()).returns(z.promise(z.void())), - schema: z.custom((v) => '_def' in v).optional(), + schema: z + .any() + .superRefine((v, ctx) => { + if (typeof v === 'function') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + // TODO: should the message say to open an issue on the loader's repo? + message: + 'Since Astro 6, a loader schema cannot be a function. Check the docs: TODO:', + }); + return z.NEVER; + } + if (!('_def' in v)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid Zod schema', + }); + return z.NEVER; + } + }) + .optional(), getSchemaContext: z .function() .returns( From a0e3f43f8142b0b0f9c4f46335f6765477c3668f Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 11:49:11 +0100 Subject: [PATCH 08/29] Discard changes to examples/blog/src/content.config.ts --- examples/blog/src/content.config.ts | 57 ++++++----------------------- 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/examples/blog/src/content.config.ts b/examples/blog/src/content.config.ts index 64c9967b0781..ce37c7f6ff34 100644 --- a/examples/blog/src/content.config.ts +++ b/examples/blog/src/content.config.ts @@ -1,54 +1,19 @@ import { defineCollection, z } from 'astro:content'; -import { defineContentLoader, glob } from 'astro/loaders'; - -const test = (options: Parameters[0]) => - defineContentLoader({ - name: 'test', - load: glob(options).load, - // schema: z.object({ - // title: z.string(), - // description: z.string(), - // // Transform string to Date object - // pubDate: z.coerce.date(), - // updatedDate: z.coerce.date().optional(), - // heroImage: z.string().optional(), - // }), - getSchemaContext: async () => { - return { - schema: z.object({ - title: z.string(), - description: z.string(), - // Transform string to Date object - pubDate: z.coerce.date(), - updatedDate: z.coerce.date().optional(), - heroImage: z.string().optional(), - }), - types: `import type { ImageMetadata } from "astro"; - -export interface Collection { - title: string; - description: string; - pubDate: Date; - updateDate: Date | undefined; - heroImage: ImageMetadata | undefined; -}`, - }; - }, - }); +import { glob } from 'astro/loaders'; const blog = defineCollection({ // Load Markdown and MDX files in the `src/content/blog/` directory. - loader: test({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }), + loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }), // Type-check frontmatter using a schema - // schema: ({ image }) => - // z.object({ - // title: z.string(), - // description: z.string(), - // // Transform string to Date object - // pubDate: z.coerce.date(), - // updatedDate: z.coerce.date().optional(), - // heroImage: image().optional(), - // }), + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string(), + // Transform string to Date object + pubDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + heroImage: image().optional(), + }), }); export const collections = { blog }; From 652b78e8e8f5c4be1b48e7f8590349fea104873e Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 12:02:05 +0100 Subject: [PATCH 09/29] feat: backward compat --- packages/astro/templates/content/types.d.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/astro/templates/content/types.d.ts b/packages/astro/templates/content/types.d.ts index 9caa399bc7af..c89b7748a075 100644 --- a/packages/astro/templates/content/types.d.ts +++ b/packages/astro/templates/content/types.d.ts @@ -104,9 +104,12 @@ declare module 'astro:content' { type InferEntrySchema = import('astro/zod').infer< ReturnTypeOrOriginal['schema']> >; - type InferLoaderSchema = import('astro/zod').infer< - Required['loader']['schema'] - >; + type InferLoaderSchema< + C extends keyof DataEntryMap, + L = Required['loader'], + > = L extends { schema: import('astro/zod').ZodSchema } + ? import('astro/zod').infer['loader']['schema']> + : any; type DataEntryMap = { // @@DATA_ENTRY_MAP@@ From 6e26dcd89c4516b7110d08760d830578ff58964b Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 12:09:19 +0100 Subject: [PATCH 10/29] chore: remove unused dep --- packages/astro/package.json | 3 +-- pnpm-lock.yaml | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index 9d73588eda74..f1ca0143eee8 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -167,8 +167,7 @@ "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", - "zod-to-json-schema": "^3.24.6", - "zod-to-ts": "^1.2.0" + "zod-to-json-schema": "^3.24.6" }, "optionalDependencies": { "sharp": "^0.34.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00cf539bf36a..1d1155bbc7d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -643,9 +643,6 @@ importers: zod-to-json-schema: specifier: ^3.24.6 version: 3.24.6(zod@3.25.76) - zod-to-ts: - specifier: ^1.2.0 - version: 1.2.0(typescript@5.9.3)(zod@3.25.76) devDependencies: '@astrojs/check': specifier: workspace:* From 67cb44f00eaa6ce47eded97586c5b4fb441a16f5 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 14:03:23 +0100 Subject: [PATCH 11/29] fix: tests --- .../fixtures/content-layer/src/content.config.ts | 14 +++++++++++--- .../content-layer/src/loaders/post-loader.ts | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) 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 d3a173ff654d..981b732aca45 100644 --- a/packages/astro/test/fixtures/content-layer/src/content.config.ts +++ b/packages/astro/test/fixtures/content-layer/src/content.config.ts @@ -226,14 +226,22 @@ const increment = defineCollection({ rendered: await renderMarkdown(markdownContent) }); }, - // Example of a loader that returns an async schema function - schema: async () => - z.object({ + getSchemaContext: async () => { + return { + schema: z.object({ lastValue: z.number(), lastUpdated: z.date(), refreshContextData: z.record(z.unknown()).optional(), slug: z.string().optional(), }), + types: `export interface Collection { + lastValue: number; + lastUpdated: Date; + refreshContextData?: Record | undefined; + slug?: string | undefined; +}` + } + } }, }); diff --git a/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts b/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts index b03c1442227e..55cc9d8d6185 100644 --- a/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts +++ b/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts @@ -31,15 +31,23 @@ export function loader(config:PostLoaderConfig): Loader { } meta.set('lastSynced', String(Date.now())); }, - schema: async () => { + getSchemaContext: async () => { // Simulate a delay await new Promise((resolve) => setTimeout(resolve, 1000)); - return z.object({ + return { + schema: z.object({ title: z.string(), body: z.string(), userId: z.number(), id: z.number(), - }); + }), + types: `export intreface Collection { + title: string; + body: string; + userId: number; + id: number; +}` + } } }; } From b198c4c3a75896f5600dcf03ce6c1fe3ccf3dfdb Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 14:33:00 +0100 Subject: [PATCH 12/29] test --- packages/astro/src/content/utils.ts | 1 + packages/astro/test/astro-sync.test.js | 43 ++++++++++++++++--- .../astro.config.mjs | 3 ++ .../package.json | 8 ++++ .../src/content.config.ts | 14 ++++++ pnpm-lock.yaml | 6 +++ 6 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 packages/astro/test/fixtures/content-layer-loader-schema-function/astro.config.mjs create mode 100644 packages/astro/test/fixtures/content-layer-loader-schema-function/package.json create mode 100644 packages/astro/test/fixtures/content-layer-loader-schema-function/src/content.config.ts diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 1413a163baf9..53a483b7b447 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -67,6 +67,7 @@ const collectionConfigParser = z.union([ if (typeof v === 'function') { ctx.addIssue({ code: z.ZodIssueCode.custom, + // TODO: remove in Astro 7 // TODO: should the message say to open an issue on the loader's repo? message: 'Since Astro 6, a loader schema cannot be a function. Check the docs: TODO:', diff --git a/packages/astro/test/astro-sync.test.js b/packages/astro/test/astro-sync.test.js index 43a00b355fa2..e06b29a6ad08 100644 --- a/packages/astro/test/astro-sync.test.js +++ b/packages/astro/test/astro-sync.test.js @@ -11,6 +11,8 @@ const createFixture = () => { let astroFixture; /** @type {Record} */ const writtenFiles = {}; + /** @type {Array} */ + const errorLogs = []; /** * @param {string} path @@ -50,13 +52,23 @@ const createFixture = () => { }, }; - await astroFixture.sync( - { root: fileURLToPath(astroFixture.config.root) }, - { - // @ts-ignore - fs: fsMock, - }, - ); + const originalError = console.error; + console.error = (message) => { + originalError(message); + errorLogs.push(message); + }; + + try { + await astroFixture.sync( + { root: fileURLToPath(astroFixture.config.root) }, + { + // @ts-ignore + fs: fsMock, + }, + ); + } finally { + console.error = originalError; + } }, /** @param {string} path */ thenFileShouldExist(path) { @@ -102,6 +114,16 @@ const createFixture = () => { assert.fail(`${path} is not valid TypeScript. Error: ${error.message}`); } }, + /** + * @param {string} message + */ + thenErrorLogsInclude(message) { + if (errorLogs.length === 0) { + assert.fail('No error log'); + } + const index = errorLogs.findIndex((log) => log.includes(message)); + assert.equal(index !== -1, true, 'No error log found'); + }, }; }; @@ -170,6 +192,13 @@ describe('astro sync', () => { 'Types file does not include empty collection type', ); }); + + it('fails when using a loader schema function', async () => { + await fixture.load('./fixtures/content-layer-loader-schema-function/'); + fixture.clean(); + await fixture.whenSyncing(); + fixture.thenErrorLogsInclude('Since Astro 6, a loader schema cannot be a function.'); + }); }); describe('astro:env', () => { diff --git a/packages/astro/test/fixtures/content-layer-loader-schema-function/astro.config.mjs b/packages/astro/test/fixtures/content-layer-loader-schema-function/astro.config.mjs new file mode 100644 index 000000000000..86dbfb924824 --- /dev/null +++ b/packages/astro/test/fixtures/content-layer-loader-schema-function/astro.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({}); diff --git a/packages/astro/test/fixtures/content-layer-loader-schema-function/package.json b/packages/astro/test/fixtures/content-layer-loader-schema-function/package.json new file mode 100644 index 000000000000..4467e4797a5b --- /dev/null +++ b/packages/astro/test/fixtures/content-layer-loader-schema-function/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/content-layer-loader-schema-function", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/content-layer-loader-schema-function/src/content.config.ts b/packages/astro/test/fixtures/content-layer-loader-schema-function/src/content.config.ts new file mode 100644 index 000000000000..f94f7cd93f53 --- /dev/null +++ b/packages/astro/test/fixtures/content-layer-loader-schema-function/src/content.config.ts @@ -0,0 +1,14 @@ +import { defineCollection, z } from 'astro:content'; +import { glob } from 'astro/loaders'; + +const blog = defineCollection({ + loader: { + name: 'test', + load: async () => {}, + schema: () => z.object() + } +}); + +export const collections = { + blog, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d1155bbc7d6..1063f83e133f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2727,6 +2727,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/content-layer-loader-schema-function: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/content-layer-markdoc: dependencies: '@astrojs/markdoc': From 6f3daaf8932656e1346075b8d8d4255fb6b86def Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 15:25:45 +0100 Subject: [PATCH 13/29] chore: todo --- packages/astro/src/content/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 53a483b7b447..e189a4d38656 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -68,9 +68,8 @@ const collectionConfigParser = z.union([ ctx.addIssue({ code: z.ZodIssueCode.custom, // TODO: remove in Astro 7 - // TODO: should the message say to open an issue on the loader's repo? message: - 'Since Astro 6, a loader schema cannot be a function. Check the docs: TODO:', + 'Since Astro 6, a loader schema cannot be a function. Report it to the loader author or check the docs: TODO:', }); return z.NEVER; } From 77b47e0d85b1c54b4fb09734a2e1f4c547351d6a Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 13 Nov 2025 15:29:16 +0100 Subject: [PATCH 14/29] chore: changesets --- .changeset/deep-states-talk.md | 5 +++++ .changeset/fresh-rocks-sing.md | 5 +++++ .changeset/rare-apes-shout.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/deep-states-talk.md create mode 100644 .changeset/fresh-rocks-sing.md create mode 100644 .changeset/rare-apes-shout.md diff --git a/.changeset/deep-states-talk.md b/.changeset/deep-states-talk.md new file mode 100644 index 000000000000..e93537e1b484 --- /dev/null +++ b/.changeset/deep-states-talk.md @@ -0,0 +1,5 @@ +--- +'astro': major +--- + +Adds a new `defineContentLoader()` type helper (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) diff --git a/.changeset/fresh-rocks-sing.md b/.changeset/fresh-rocks-sing.md new file mode 100644 index 000000000000..337b34b35b29 --- /dev/null +++ b/.changeset/fresh-rocks-sing.md @@ -0,0 +1,5 @@ +--- +'astro': major +--- + +Removes the ability for content loaders schemas to be functions (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) diff --git a/.changeset/rare-apes-shout.md b/.changeset/rare-apes-shout.md new file mode 100644 index 000000000000..02540d414273 --- /dev/null +++ b/.changeset/rare-apes-shout.md @@ -0,0 +1,5 @@ +--- +'astro': major +--- + +Adds a new optional `getSchemaContext()` property (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) From b375c59107f644800a7e9516a1974e6af0908252 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 19 Nov 2025 08:44:56 +0100 Subject: [PATCH 15/29] Discard changes to packages/astro/src/content/loaders/glob.ts --- packages/astro/src/content/loaders/glob.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/astro/src/content/loaders/glob.ts b/packages/astro/src/content/loaders/glob.ts index a94715104c50..99d0dbaca84f 100644 --- a/packages/astro/src/content/loaders/glob.ts +++ b/packages/astro/src/content/loaders/glob.ts @@ -8,7 +8,7 @@ import { glob as tinyglobby } from 'tinyglobby'; import type { ContentEntryRenderFunction, ContentEntryType } from '../../types/public/content.js'; import type { RenderedContent } from '../data-store.js'; import { getContentEntryIdAndSlug, posixRelative } from '../utils.js'; -import { defineContentLoader } from './config.js'; +import type { Loader } from './types.js'; interface GenerateIdOptions { /** The path to the entry file, relative to the base directory. */ @@ -57,7 +57,7 @@ function checkPrefix(pattern: string | Array, prefix: string) { * @param pattern A glob pattern to match files, relative to the content directory. */ -export function glob(globOptions: GlobOptions) { +export function glob(globOptions: GlobOptions): Loader { if (checkPrefix(globOptions.pattern, '../')) { throw new Error( 'Glob patterns cannot start with `../`. Set the `base` option to a parent directory instead.', @@ -73,7 +73,7 @@ export function glob(globOptions: GlobOptions) { const fileToIdMap = new Map(); - return defineContentLoader({ + return { name: 'glob-loader', load: async ({ config, logger, watcher, parseData, store, generateDigest, entryTypes }) => { const renderFunctionByContentType = new WeakMap< @@ -315,5 +315,5 @@ export function glob(globOptions: GlobOptions) { } }); }, - }); + }; } From c7fd7a6aedbb9547518bec8523c170381c803e26 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 19 Nov 2025 08:45:05 +0100 Subject: [PATCH 16/29] Discard changes to packages/astro/src/content/loaders/file.ts --- packages/astro/src/content/loaders/file.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/astro/src/content/loaders/file.ts b/packages/astro/src/content/loaders/file.ts index 09b69f148884..c1b4b2a075a6 100644 --- a/packages/astro/src/content/loaders/file.ts +++ b/packages/astro/src/content/loaders/file.ts @@ -5,8 +5,7 @@ import toml from 'smol-toml'; import { FileGlobNotSupported, FileParserNotFound } from '../../core/errors/errors-data.js'; import { AstroError } from '../../core/errors/index.js'; import { posixRelative } from '../utils.js'; -import { defineContentLoader } from './config.js'; -import type { LoaderContext } from './types.js'; +import type { Loader, LoaderContext } from './types.js'; interface FileOptions { /** @@ -23,7 +22,7 @@ interface FileOptions { * @param fileName The path to the JSON file to load, relative to the content directory. * @param options Additional options for the file loader */ -export function file(fileName: string, options?: FileOptions) { +export function file(fileName: string, options?: FileOptions): Loader { if (fileName.includes('*')) { throw new AstroError(FileGlobNotSupported); } @@ -103,7 +102,7 @@ export function file(fileName: string, options?: FileOptions) { } } - return defineContentLoader({ + return { name: 'file-loader', load: async (context) => { const { config, logger, watcher } = context; @@ -126,5 +125,5 @@ export function file(fileName: string, options?: FileOptions) { } }); }, - }); + }; } From 09c6543a4ccf8ae2eb0a3940bf51c0d8c6672134 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 19 Nov 2025 08:48:29 +0100 Subject: [PATCH 17/29] Apply suggestion from @ascorbic Co-authored-by: Matt Kane --- .../test/fixtures/content-layer/src/loaders/post-loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts b/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts index 55cc9d8d6185..ec8b3fd88b8e 100644 --- a/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts +++ b/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts @@ -41,7 +41,7 @@ export function loader(config:PostLoaderConfig): Loader { userId: z.number(), id: z.number(), }), - types: `export intreface Collection { + types: /* ts */`export interface Collection { title: string; body: string; userId: number; From a5a64cc1bb5aac548661b6bd30bcc97bf2f892b3 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 19 Nov 2025 09:05:20 +0100 Subject: [PATCH 18/29] feedback --- packages/astro/src/content/types-generator.ts | 12 ++++++++---- packages/astro/src/content/utils.ts | 18 +++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 2204beca3a16..f8d9028a44bb 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -513,10 +513,14 @@ async function writeContentFiles({ contentCollectionManifest.collections.push({ hasSchema: Boolean( - collectionConfig && - (collectionConfig.schema || - (typeof collectionConfig.loader !== 'function' && - (collectionConfig.loader.schema || schemaContextResultCache.has(collectionKey)))), + // Is there a user provided schema or + collectionConfig?.schema || + // Is it a loader object and + (typeof collectionConfig?.loader !== 'function' && + // Is it a loader static schema or + (collectionConfig?.loader.schema || + // is it a loader dynamic schema + schemaContextResultCache.has(collectionKey))), ), name: key, }); diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index e189a4d38656..a5c1e6c44a89 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -63,17 +63,17 @@ const collectionConfigParser = z.union([ load: z.function().args(z.custom()).returns(z.promise(z.void())), schema: z .any() - .superRefine((v, ctx) => { + .transform((v) => { if (typeof v === 'function') { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - // TODO: remove in Astro 7 - message: - 'Since Astro 6, a loader schema cannot be a function. Report it to the loader author or check the docs: TODO:', - }); - return z.NEVER; + console.warn( + `Since Astro 6, a loader schema cannot be a function. It is ignored and will break in a future major. Report it to the loader author or check the docs: TODO:`, + ); + return undefined; } - if (!('_def' in v)) { + return v; + }) + .superRefine((v, ctx) => { + if (v !== undefined && !('_def' in v)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid Zod schema', From 5bb7411848b1f099c2197595a166ad98def76f4f Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Fri, 21 Nov 2025 09:10:25 +0100 Subject: [PATCH 19/29] Apply suggestion from @florian-lefebvre --- packages/astro/src/content/types-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index f8d9028a44bb..f9bb3e92adad 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -397,7 +397,7 @@ async function typeForCollection( if (!result) { return { type: 'any' }; } - const filename = `loaders/${collectionKey.slice(1, -1)}.ts`; + const filename = `loaders/${collectionKey.slice(1, -1)}.js`; return { type: `import("./${filename}").Collection`, injectedType: { From 8810bd33db3fefd85a3881af684c297ddd6064fb Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Fri, 21 Nov 2025 14:08:48 +0100 Subject: [PATCH 20/29] fix --- packages/astro/src/content/types-generator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 7fdd9561bf4f..f2c684fabb49 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -397,11 +397,11 @@ async function typeForCollection( if (!result) { return { type: 'any' }; } - const filename = `loaders/${collectionKey.slice(1, -1)}.js`; + const base = `loaders/${collectionKey.slice(1, -1)}`; return { - type: `import("./${filename}").Collection`, + type: `import("./${base}.js").Collection`, injectedType: { - filename, + filename: `${base}.ts`, content: result.types, }, }; From c409b14e638a460848e9807f3bbcc309d28ef999 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 11:17:28 +0100 Subject: [PATCH 21/29] fix: test --- packages/astro/test/astro-sync.test.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/astro/test/astro-sync.test.js b/packages/astro/test/astro-sync.test.js index e06b29a6ad08..45c59f2f7840 100644 --- a/packages/astro/test/astro-sync.test.js +++ b/packages/astro/test/astro-sync.test.js @@ -12,7 +12,7 @@ const createFixture = () => { /** @type {Record} */ const writtenFiles = {}; /** @type {Array} */ - const errorLogs = []; + const warnLogs = []; /** * @param {string} path @@ -52,10 +52,10 @@ const createFixture = () => { }, }; - const originalError = console.error; - console.error = (message) => { - originalError(message); - errorLogs.push(message); + const originalWarn = console.warn; + console.warn = (message) => { + originalWarn(message); + warnLogs.push(message); }; try { @@ -67,7 +67,7 @@ const createFixture = () => { }, ); } finally { - console.error = originalError; + console.error = originalWarn; } }, /** @param {string} path */ @@ -117,11 +117,11 @@ const createFixture = () => { /** * @param {string} message */ - thenErrorLogsInclude(message) { - if (errorLogs.length === 0) { + thenWarnLogsInclude(message) { + if (warnLogs.length === 0) { assert.fail('No error log'); } - const index = errorLogs.findIndex((log) => log.includes(message)); + const index = warnLogs.findIndex((log) => log.includes(message)); assert.equal(index !== -1, true, 'No error log found'); }, }; @@ -197,7 +197,7 @@ describe('astro sync', () => { await fixture.load('./fixtures/content-layer-loader-schema-function/'); fixture.clean(); await fixture.whenSyncing(); - fixture.thenErrorLogsInclude('Since Astro 6, a loader schema cannot be a function.'); + fixture.thenWarnLogsInclude('Since Astro 6, a loader schema cannot be a function.'); }); }); From d2c3fd8c81699b0270b913c69fde05fa4b8dd3f3 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 11:22:52 +0100 Subject: [PATCH 22/29] feedback --- .changeset/deep-states-talk.md | 2 +- .changeset/fresh-rocks-sing.md | 2 +- .changeset/rare-apes-shout.md | 5 ---- packages/astro/src/content/loaders/config.ts | 6 ---- packages/astro/src/content/loaders/index.ts | 1 - packages/astro/src/content/loaders/types.ts | 29 +++++++++----------- 6 files changed, 15 insertions(+), 30 deletions(-) delete mode 100644 .changeset/rare-apes-shout.md delete mode 100644 packages/astro/src/content/loaders/config.ts diff --git a/.changeset/deep-states-talk.md b/.changeset/deep-states-talk.md index e93537e1b484..ee4760061141 100644 --- a/.changeset/deep-states-talk.md +++ b/.changeset/deep-states-talk.md @@ -2,4 +2,4 @@ 'astro': major --- -Adds a new `defineContentLoader()` type helper (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) +Updates how schema types are inferred for content loaders with schemas (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) diff --git a/.changeset/fresh-rocks-sing.md b/.changeset/fresh-rocks-sing.md index 337b34b35b29..347a174c152a 100644 --- a/.changeset/fresh-rocks-sing.md +++ b/.changeset/fresh-rocks-sing.md @@ -2,4 +2,4 @@ 'astro': major --- -Removes the ability for content loaders schemas to be functions (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) +Removes the ability for content loaders schemas to be functions and adds a new equivalent `getSchemaContext()` property (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) diff --git a/.changeset/rare-apes-shout.md b/.changeset/rare-apes-shout.md deleted file mode 100644 index 02540d414273..000000000000 --- a/.changeset/rare-apes-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': major ---- - -Adds a new optional `getSchemaContext()` property (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) diff --git a/packages/astro/src/content/loaders/config.ts b/packages/astro/src/content/loaders/config.ts deleted file mode 100644 index 0616d7beb9ca..000000000000 --- a/packages/astro/src/content/loaders/config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ZodSchema } from 'zod'; -import type { Loader } from './types.js'; - -export function defineContentLoader(loader: Loader) { - return loader; -} diff --git a/packages/astro/src/content/loaders/index.ts b/packages/astro/src/content/loaders/index.ts index 51a2de9fb748..30b4bfbe5334 100644 --- a/packages/astro/src/content/loaders/index.ts +++ b/packages/astro/src/content/loaders/index.ts @@ -1,4 +1,3 @@ -export { defineContentLoader } from './config.js'; export { file } from './file.js'; export { glob } from './glob.js'; export * from './types.js'; diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts index 8ad19043607d..7fbb30f80988 100644 --- a/packages/astro/src/content/loaders/types.ts +++ b/packages/astro/src/content/loaders/types.ts @@ -49,27 +49,24 @@ export interface LoaderContext { entryTypes: Map; } -export type Loader = { +export type Loader = { /** Unique name of the loader, e.g. the npm package name */ name: string; /** Do the actual loading of the data */ load: (context: LoaderContext) => Promise; -} & ([T] extends [never] - ? - | { - /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ - schema?: T; - } - | { - getSchemaContext?: () => Promise<{ - schema: ZodSchema; - types: string; - }>; - } - : { +} & ( + | { /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ - schema: T; - }); + schema?: ZodSchema; + } + | { + /** Optionally, provide a function to dynamically provide a schema. Will be overridden by user-defined schema */ + getSchemaContext?: () => Promise<{ + schema: ZodSchema; + types: string; + }>; + } +); export interface LoadEntryContext { filter: TEntryFilter extends never ? { id: string } : TEntryFilter; From d9b14c4a99128f5847b9110ee8ffa2001c6814d4 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 11:32:41 +0100 Subject: [PATCH 23/29] Apply suggestion from @florian-lefebvre --- .../astro/test/fixtures/content-layer/src/content.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 981b732aca45..fa3c73ec7558 100644 --- a/packages/astro/test/fixtures/content-layer/src/content.config.ts +++ b/packages/astro/test/fixtures/content-layer/src/content.config.ts @@ -234,7 +234,7 @@ const increment = defineCollection({ refreshContextData: z.record(z.unknown()).optional(), slug: z.string().optional(), }), - types: `export interface Collection { + types: /* ts */`export interface Collection { lastValue: number; lastUpdated: Date; refreshContextData?: Record | undefined; From 9057916ec74681cdd1bc514e4509e4437c9e859f Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 11:47:45 +0100 Subject: [PATCH 24/29] feat: rename getSchemaContext to createSchema --- .changeset/fresh-rocks-sing.md | 2 +- packages/astro/src/content/content-layer.ts | 4 ++-- packages/astro/src/content/loaders/types.ts | 2 +- packages/astro/src/content/types-generator.ts | 18 +++++++++--------- packages/astro/src/content/utils.ts | 2 +- .../content-layer/src/content.config.ts | 2 +- .../content-layer/src/loaders/post-loader.ts | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.changeset/fresh-rocks-sing.md b/.changeset/fresh-rocks-sing.md index 347a174c152a..6920ff2a729c 100644 --- a/.changeset/fresh-rocks-sing.md +++ b/.changeset/fresh-rocks-sing.md @@ -2,4 +2,4 @@ 'astro': major --- -Removes the ability for content loaders schemas to be functions and adds a new equivalent `getSchemaContext()` property (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) +Removes the ability for content loaders schemas to be functions and adds a new equivalent `createSchema()` property (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) diff --git a/packages/astro/src/content/content-layer.ts b/packages/astro/src/content/content-layer.ts index f55fbbe46f92..583f104e8e4c 100644 --- a/packages/astro/src/content/content-layer.ts +++ b/packages/astro/src/content/content-layer.ts @@ -253,8 +253,8 @@ class ContentLayer { if (!schema && typeof collection.loader === 'object') { schema = collection.loader.schema; - if (!schema && collection.loader.getSchemaContext) { - ({ schema } = await collection.loader.getSchemaContext()); + if (!schema && collection.loader.createSchema) { + ({ schema } = await collection.loader.createSchema()); } } diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts index 7fbb30f80988..8b7449751f66 100644 --- a/packages/astro/src/content/loaders/types.ts +++ b/packages/astro/src/content/loaders/types.ts @@ -61,7 +61,7 @@ export type Loader = { } | { /** Optionally, provide a function to dynamically provide a schema. Will be overridden by user-defined schema */ - getSchemaContext?: () => Promise<{ + createSchema?: () => Promise<{ schema: ZodSchema; types: string; }>; diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index f2c684fabb49..2b9207804d71 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -343,13 +343,13 @@ function normalizeConfigPath(from: string, to: string) { return `"${isRelativePath(configPath) ? '' : './'}${normalizedPath}"` as const; } -const schemaContextResultCache = new Map(); +const createSchemaResultCache = new Map(); -async function getSchemaContextResult( +async function getCreateSchemaResult( collection: ContentConfig['collections'][T], collectionKey: T, ) { - const cached = schemaContextResultCache.get(collectionKey); + const cached = createSchemaResultCache.get(collectionKey); if (cached) { return cached; } @@ -358,10 +358,10 @@ async function getSchemaContextResult( if (collection.loader.schema) { return { type: `InferLoaderSchema<${collectionKey}>` }; } - const result = await getSchemaContextResult(collection, collectionKey); + const result = await getCreateSchemaResult(collection, collectionKey); if (!result) { return { type: 'any' }; } @@ -520,7 +520,7 @@ async function writeContentFiles({ // Is it a loader static schema or (collectionConfig?.loader.schema || // is it a loader dynamic schema - schemaContextResultCache.has(collectionKey))), + createSchemaResultCache.has(collectionKey))), ), name: key, }); diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 1bc9800c6956..a05fd4b7042c 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -82,7 +82,7 @@ const collectionConfigParser = z.union([ } }) .optional(), - getSchemaContext: z + createSchema: z .function() .returns( z.promise( 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 fa3c73ec7558..ed51a7f07e54 100644 --- a/packages/astro/test/fixtures/content-layer/src/content.config.ts +++ b/packages/astro/test/fixtures/content-layer/src/content.config.ts @@ -226,7 +226,7 @@ const increment = defineCollection({ rendered: await renderMarkdown(markdownContent) }); }, - getSchemaContext: async () => { + createSchema: async () => { return { schema: z.object({ lastValue: z.number(), diff --git a/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts b/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts index ec8b3fd88b8e..f0b7a0a95a40 100644 --- a/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts +++ b/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts @@ -31,7 +31,7 @@ export function loader(config:PostLoaderConfig): Loader { } meta.set('lastSynced', String(Date.now())); }, - getSchemaContext: async () => { + createSchema: async () => { // Simulate a delay await new Promise((resolve) => setTimeout(resolve, 1000)); return { From ad72298654555a84f569fcd2ba436e4c46b64761 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 14:33:39 +0100 Subject: [PATCH 25/29] Apply suggestions from code review --- packages/astro/src/content/types-generator.ts | 2 +- .../astro/test/fixtures/content-layer/src/content.config.ts | 2 +- .../test/fixtures/content-layer/src/loaders/post-loader.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 2b9207804d71..bfe934f20cda 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -399,7 +399,7 @@ async function typeForCollection( } const base = `loaders/${collectionKey.slice(1, -1)}`; return { - type: `import("./${base}.js").Collection`, + type: `import("./${base}.js").Entry`, injectedType: { filename: `${base}.ts`, content: result.types, 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 ed51a7f07e54..121f9b274870 100644 --- a/packages/astro/test/fixtures/content-layer/src/content.config.ts +++ b/packages/astro/test/fixtures/content-layer/src/content.config.ts @@ -234,7 +234,7 @@ const increment = defineCollection({ refreshContextData: z.record(z.unknown()).optional(), slug: z.string().optional(), }), - types: /* ts */`export interface Collection { + types: /* ts */`export interface Entry { lastValue: number; lastUpdated: Date; refreshContextData?: Record | undefined; diff --git a/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts b/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts index f0b7a0a95a40..d68a2a3f9c83 100644 --- a/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts +++ b/packages/astro/test/fixtures/content-layer/src/loaders/post-loader.ts @@ -41,7 +41,7 @@ export function loader(config:PostLoaderConfig): Loader { userId: z.number(), id: z.number(), }), - types: /* ts */`export interface Collection { + types: /* ts */`export interface Entry { title: string; body: string; userId: number; From 1e906b2e28a189bc502085e072e017700e197c73 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 17:14:01 +0100 Subject: [PATCH 26/29] Update packages/astro/src/content/utils.ts Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- packages/astro/src/content/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index a05fd4b7042c..1244f8772d4c 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -66,7 +66,7 @@ const collectionConfigParser = z.union([ .transform((v) => { if (typeof v === 'function') { console.warn( - `Since Astro 6, a loader schema cannot be a function. It is ignored and will break in a future major. Report it to the loader author or check the docs: TODO:`, + `Your loader's schema is defined using a function. This is no longer supported and the schema will be ignored. Please update your loader to use the `createSchema()` utility instead, or report this to the loader author. In a future major version, this will cause the loader to break entirely.`, ); return undefined; } From 591eeb84ea02999e928452a478012c317b38f049 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 17:14:22 +0100 Subject: [PATCH 27/29] Update .changeset/fresh-rocks-sing.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/fresh-rocks-sing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fresh-rocks-sing.md b/.changeset/fresh-rocks-sing.md index 6920ff2a729c..072b42123a22 100644 --- a/.changeset/fresh-rocks-sing.md +++ b/.changeset/fresh-rocks-sing.md @@ -2,4 +2,4 @@ 'astro': major --- -Removes the ability for content loaders schemas to be functions and adds a new equivalent `createSchema()` property (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) +Removes the option to define dynamic schemas in content loaders as functions and adds a new equivalent `createSchema()` property (Loader API) - ([v6 upgrade guidance](https://deploy-preview-12322--astro-docs-2.netlify.app/en/guides/upgrade-to/v6/TODO:)) From 0c1b5589cbbc94c16408e30e81f1b0859ce5559b Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 17:15:15 +0100 Subject: [PATCH 28/29] Apply suggestion from @florian-lefebvre --- packages/astro/test/astro-sync.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/test/astro-sync.test.js b/packages/astro/test/astro-sync.test.js index 45c59f2f7840..56e4a766c2b8 100644 --- a/packages/astro/test/astro-sync.test.js +++ b/packages/astro/test/astro-sync.test.js @@ -197,7 +197,7 @@ describe('astro sync', () => { await fixture.load('./fixtures/content-layer-loader-schema-function/'); fixture.clean(); await fixture.whenSyncing(); - fixture.thenWarnLogsInclude('Since Astro 6, a loader schema cannot be a function.'); + fixture.thenWarnLogsInclude("Your loader's schema is defined using a function."); }); }); From 551dea964bf9c54989ebb19a23a8782e8fa04591 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 3 Dec 2025 17:33:52 +0100 Subject: [PATCH 29/29] Apply suggestion from @florian-lefebvre --- packages/astro/src/content/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 1244f8772d4c..023b0b495e43 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -66,7 +66,7 @@ const collectionConfigParser = z.union([ .transform((v) => { if (typeof v === 'function') { console.warn( - `Your loader's schema is defined using a function. This is no longer supported and the schema will be ignored. Please update your loader to use the `createSchema()` utility instead, or report this to the loader author. In a future major version, this will cause the loader to break entirely.`, + `Your loader's schema is defined using a function. This is no longer supported and the schema will be ignored. Please update your loader to use the \`createSchema()\` utility instead, or report this to the loader author. In a future major version, this will cause the loader to break entirely.`, ); return undefined; }