diff --git a/packages/astro/package.json b/packages/astro/package.json index 56c224604b72..7e0a4bfa2583 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -185,7 +185,8 @@ "which-pm": "^2.2.0", "yargs-parser": "^21.1.1", "zod": "^3.23.8", - "zod-to-json-schema": "^3.23.1" + "zod-to-json-schema": "^3.23.1", + "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" diff --git a/packages/astro/src/content/loaders.ts b/packages/astro/src/content/loaders.ts index 2bbcf8e20629..3cf9e59066cd 100644 --- a/packages/astro/src/content/loaders.ts +++ b/packages/astro/src/content/loaders.ts @@ -32,13 +32,13 @@ export interface LoaderContext { ): T; } -export interface Loader { +export interface 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?: S | Promise | (() => S | Promise); + schema?: ZodSchema | Promise | (() => ZodSchema | Promise); render?: (entry: any) => any; } diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 5aed9f1ecd1c..4532ac1f56e7 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -5,6 +5,7 @@ import glob from 'fast-glob'; import { bold, cyan } from 'kleur/colors'; import { type ViteDevServer, normalizePath } from 'vite'; import { z } from 'zod'; +import { zodToTs, printNode } from 'zod-to-ts'; import { zodToJsonSchema } from 'zod-to-json-schema'; import type { AstroSettings, ContentEntryType } from '../@types/astro.js'; import { AstroError } from '../core/errors/errors.js'; @@ -360,6 +361,27 @@ function normalizeConfigPath(from: string, to: string) { return `"${isRelativePath(configPath) ? '' : './'}${normalizedPath}"` as const; } +async function typeForCollection( + collection: ContentConfig['collections'][T] | undefined, + collectionKey: T +): Promise { + if (collection?.schema) { + return `InferEntrySchema<${collectionKey}>`; + } + + if (collection?.type === 'experimental_data' && collection.loader.schema) { + let schema = collection.loader.schema; + if (typeof schema === 'function') { + schema = await schema(); + } + if (schema) { + const ast = zodToTs(schema); + return printNode(ast.node); + } + } + return 'any'; +} + async function writeContentFiles({ fs, contentPaths, @@ -435,11 +457,7 @@ async function writeContentFiles({ : collection.type; const collectionEntryKeys = Object.keys(collection.entries).sort(); - const dataType = - collectionConfig?.schema || - (collectionConfig?.type === 'experimental_data' && collectionConfig.loader?.schema) - ? `InferEntrySchema<${collectionKey}>` - : 'any'; + const dataType = await typeForCollection(collectionConfig, collectionKey); switch (resolvedType) { case 'content': if (collectionEntryKeys.length === 0) { 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 edd2a8dbf443..38e4e28b5816 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 @@ -1,4 +1,4 @@ -import type { Loader } from 'astro:content'; +import { type Loader, z } from 'astro:content'; export interface PostLoaderConfig { url: string; @@ -30,5 +30,15 @@ export function loader(config:PostLoaderConfig): Loader { } meta.set('lastSynced', String(Date.now())); }, + schema: async () => { + // Simulate a delay + await new Promise((resolve) => setTimeout(resolve, 1000)); + return z.object({ + title: z.string(), + body: z.string(), + userId: z.number(), + id: z.number(), + }); + } }; } diff --git a/packages/astro/types/content.d.ts b/packages/astro/types/content.d.ts index 23699f3b1346..c130245cd335 100644 --- a/packages/astro/types/content.d.ts +++ b/packages/astro/types/content.d.ts @@ -55,13 +55,13 @@ declare module 'astro:content' { props: ParseDataOptions ): T; } - export interface Loader { + export interface 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?: S | Promise | (() => S | Promise); + schema?: BaseSchema | Promise | (() => BaseSchema | Promise); render?: (entry: any) => any; } @@ -82,7 +82,7 @@ declare module 'astro:content' { type ContentCollectionV2Config = { type: 'experimental_data'; schema?: S | ((context: SchemaContext) => S); - loader: Loader; + loader: Loader; }; type DataCollectionConfig = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2951b639e8ad..cea105105e2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -717,6 +717,9 @@ importers: zod-to-json-schema: specifier: ^3.23.1 version: 3.23.1(zod@3.23.8) + zod-to-ts: + specifier: ^1.2.0 + version: 1.2.0(typescript@5.5.2)(zod@3.23.8) optionalDependencies: sharp: specifier: ^0.33.3 @@ -11703,6 +11706,12 @@ packages: peerDependencies: zod: ^3.23.3 + zod-to-ts@1.2.0: + resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} + peerDependencies: + typescript: ^4.9.4 || ^5.0.2 + zod: ^3 + zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} @@ -18379,6 +18388,11 @@ snapshots: dependencies: zod: 3.23.8 + zod-to-ts@1.2.0(typescript@5.5.2)(zod@3.23.8): + dependencies: + typescript: 5.5.2 + zod: 3.23.8 + zod@3.23.8: {} zwitch@2.0.4: {}