From 82bcef8ba0cdf599cd2c780e437ca4ac34d46929 Mon Sep 17 00:00:00 2001 From: zce Date: Mon, 12 Feb 2024 22:16:30 +0800 Subject: [PATCH] fix: defineCollection type inference close: #39 --- docs/guide/using-collections.md | 6 +- examples/nextjs/velite.config.ts | 158 ++++++++++++++++--------------- src/output.ts | 3 +- src/types.ts | 25 ++--- 4 files changed, 101 insertions(+), 91 deletions(-) diff --git a/docs/guide/using-collections.md b/docs/guide/using-collections.md index e78dc94..ebcb419 100644 --- a/docs/guide/using-collections.md +++ b/docs/guide/using-collections.md @@ -36,10 +36,12 @@ export { default as others } from './others.json' ```js [index.d.ts] import config from '../velite.config' -export type Post = NonNullable['posts']['schema']['_output'] +type Collections = typeof config.collections + +export type Post = Collections['posts']['schema']['_output'] export declare const posts: Post[] -export type Other = NonNullable['others']['schema']['_output'] +export type Other = Collections['others']['schema']['_output'] export declare const others: Other[] ``` diff --git a/examples/nextjs/velite.config.ts b/examples/nextjs/velite.config.ts index 1336d1d..905ffb8 100644 --- a/examples/nextjs/velite.config.ts +++ b/examples/nextjs/velite.config.ts @@ -1,5 +1,5 @@ import rehypePrettyCode from 'rehype-pretty-code' -import { defineConfig, s } from 'velite' +import { defineCollection, defineConfig, s } from 'velite' const slugify = (input: string) => input @@ -18,6 +18,85 @@ const meta = s }) .default({}) +const options = defineCollection({ + name: 'Options', + pattern: 'options/index.yml', + single: true, + schema: s.object({ + name: s.string().max(20), + title: s.string().max(99), + description: s.string().max(999).optional(), + keywords: s.array(s.string()), + author: s.object({ name: s.string(), email: s.string().email(), url: s.string().url() }), + links: s.array(s.object({ text: s.string(), link: s.string(), type: s.enum(['navigation', 'footer', 'copyright']) })), + socials: s.array(s.object({ name: s.string(), icon, link: s.string().optional(), image: s.image().optional() })) + }) +}) + +const categories = defineCollection({ + name: 'Category', + pattern: 'categories/*.yml', + schema: s + .object({ + name: s.string().max(20), + slug: s.slug('global', ['admin', 'login']), + cover: s.image().optional(), + description: s.string().max(999).optional(), + count + }) + .transform(data => ({ ...data, permalink: `/${data.slug}` })) +}) + +const tags = defineCollection({ + name: 'Tag', + pattern: 'tags/index.yml', + schema: s + .object({ + name: s.string().max(20), + slug: s.slug('global', ['admin', 'login']), + cover: s.image().optional(), + description: s.string().max(999).optional(), + count + }) + .transform(data => ({ ...data, permalink: `/${data.slug}` })) +}) + +const pages = defineCollection({ + name: 'Page', + pattern: 'pages/**/*.mdx', + schema: s + .object({ + title: s.string().max(99), + slug: s.slug('global', ['admin', 'login']), + body: s.mdx() + }) + .transform(data => ({ ...data, permalink: `/${data.slug}` })) +}) + +const posts = defineCollection({ + name: 'Post', + pattern: 'posts/**/*.md', + schema: s + .object({ + title: s.string().max(99), + slug: s.slug('post'), + date: s.isodate(), + updated: s.isodate().optional(), + cover: s.image().optional(), + video: s.file().optional(), + description: s.string().max(999).optional(), + draft: s.boolean().default(false), + featured: s.boolean().default(false), + categories: s.array(s.string()).default(['Journal']), + tags: s.array(s.string()).default([]), + meta: meta, + metadata: s.metadata(), + excerpt: s.excerpt(), + content: s.markdown() + }) + .transform(data => ({ ...data, permalink: `/blog/${data.slug}` })) +}) + export default defineConfig({ root: 'content', output: { @@ -27,82 +106,7 @@ export default defineConfig({ name: '[name]-[hash:6].[ext]', clean: true }, - collections: { - options: { - name: 'Options', - pattern: 'options/index.yml', - single: true, - schema: s.object({ - name: s.string().max(20), - title: s.string().max(99), - description: s.string().max(999).optional(), - keywords: s.array(s.string()), - author: s.object({ name: s.string(), email: s.string().email(), url: s.string().url() }), - links: s.array(s.object({ text: s.string(), link: s.string(), type: s.enum(['navigation', 'footer', 'copyright']) })), - socials: s.array(s.object({ name: s.string(), icon, link: s.string().optional(), image: s.image().optional() })) - }) - }, - categories: { - name: 'Category', - pattern: 'categories/*.yml', - schema: s - .object({ - name: s.string().max(20), - slug: s.slug('global', ['admin', 'login']), - cover: s.image().optional(), - description: s.string().max(999).optional(), - count - }) - .transform(data => ({ ...data, permalink: `/${data.slug}` })) - }, - tags: { - name: 'Tag', - pattern: 'tags/index.yml', - schema: s - .object({ - name: s.string().max(20), - slug: s.slug('global', ['admin', 'login']), - cover: s.image().optional(), - description: s.string().max(999).optional(), - count - }) - .transform(data => ({ ...data, permalink: `/${data.slug}` })) - }, - pages: { - name: 'Page', - pattern: 'pages/**/*.mdx', - schema: s - .object({ - title: s.string().max(99), - slug: s.slug('global', ['admin', 'login']), - body: s.mdx() - }) - .transform(data => ({ ...data, permalink: `/${data.slug}` })) - }, - posts: { - name: 'Post', - pattern: 'posts/**/*.md', - schema: s - .object({ - title: s.string().max(99), - slug: s.slug('post'), - date: s.isodate(), - updated: s.isodate().optional(), - cover: s.image().optional(), - video: s.file().optional(), - description: s.string().max(999).optional(), - draft: s.boolean().default(false), - featured: s.boolean().default(false), - categories: s.array(s.string()).default(['Journal']), - tags: s.array(s.string()).default([]), - meta: meta, - metadata: s.metadata(), - excerpt: s.excerpt(), - content: s.markdown() - }) - .transform(data => ({ ...data, permalink: `/blog/${data.slug}` })) - } - }, + collections: { options, categories, tags, pages, posts }, markdown: { // https://rehype-pretty-code.netlify.app/ rehypePlugins: [rehypePrettyCode] diff --git a/src/output.ts b/src/output.ts index be4ee7f..d6cf952 100644 --- a/src/output.ts +++ b/src/output.ts @@ -39,10 +39,11 @@ export const outputEntry = async (dest: string, configPath: string, collections: const entry: string[] = [] const dts: string[] = [`import config from '${configModPath}'\n`] + dts.push('type Collections = typeof config.collections\n') Object.entries(collections).map(([name, collection]) => { entry.push(`export { default as ${name} } from './${name}.json'`) - dts.push(`export type ${collection.name} = NonNullable['${name}']['schema']['_output']`) + dts.push(`export type ${collection.name} = Collections['${name}']['schema']['_output']`) dts.push(`export declare const ${name}: ${collection.name + (collection.single ? '' : '[]')}\n`) }) diff --git a/src/types.ts b/src/types.ts index e6c5bd9..cc862a9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -141,15 +141,15 @@ export interface Output { /** * Collection options */ -export interface Collection { +export interface Collection { /** - * Schema name (singular), for types generation + * Collection name (singular), for types generation * @example * 'Post' */ name: string /** - * Schema glob pattern, based on `root` + * Collection glob pattern, based on `root` * @example * 'posts/*.md' */ @@ -160,7 +160,7 @@ export interface Collection { */ single?: boolean /** - * Schema + * Collection schema * @see {@link https://zod.dev} * @example * s.object({ @@ -170,22 +170,25 @@ export interface Collection { * content: s.string() // from markdown body * }) */ - schema: Schema + schema: T } /** * All collections */ export interface Collections { - [name: string]: Collection + [name: string]: Collection } +/** + * Collection Type + */ +export type CollectionType = T[P]['single'] extends true ? T[P]['schema']['_output'] : Array + /** * All collections result */ -export type Result = { - [K in keyof T]: T[K]['single'] extends true ? T[K]['schema']['_output'] : Array -} +export type Result = { [P in keyof T]: CollectionType } /** * This interface for plugins extra user config @@ -279,7 +282,7 @@ export interface Config extends Readonly { /** * Define a collection (identity function for type inference) */ -export const defineCollection = (collection: Collection) => collection +export const defineCollection = (collection: Collection) => collection /** * Define a loader (identity function for type inference) @@ -289,6 +292,6 @@ export const defineLoader = (loader: Loader) => loader /** * Define config (identity function for type inference) */ -export const defineConfig = (config: UserConfig) => config +export const defineConfig = (config: UserConfig) => config // ↑↑↑ helper identity functions for type inference