Skip to content

Commit

Permalink
fix: defineCollection type inference
Browse files Browse the repository at this point in the history
close: #39
  • Loading branch information
zce committed Feb 12, 2024
1 parent ebaac11 commit 82bcef8
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 91 deletions.
6 changes: 4 additions & 2 deletions docs/guide/using-collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ export { default as others } from './others.json'
```js [index.d.ts]
import config from '../velite.config'

export type Post = NonNullable<typeof config.collections>['posts']['schema']['_output']
type Collections = typeof config.collections

export type Post = Collections['posts']['schema']['_output']
export declare const posts: Post[]

export type Other = NonNullable<typeof config.collections>['others']['schema']['_output']
export type Other = Collections['others']['schema']['_output']
export declare const others: Other[]
```

Expand Down
158 changes: 81 additions & 77 deletions examples/nextjs/velite.config.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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: {
Expand All @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof config.collections>['${name}']['schema']['_output']`)
dts.push(`export type ${collection.name} = Collections['${name}']['schema']['_output']`)
dts.push(`export declare const ${name}: ${collection.name + (collection.single ? '' : '[]')}\n`)
})

Expand Down
25 changes: 14 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,15 @@ export interface Output {
/**
* Collection options
*/
export interface Collection {
export interface Collection<T extends Schema> {
/**
* 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'
*/
Expand All @@ -160,7 +160,7 @@ export interface Collection {
*/
single?: boolean
/**
* Schema
* Collection schema
* @see {@link https://zod.dev}
* @example
* s.object({
Expand All @@ -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<Schema>
}

/**
* Collection Type
*/
export type CollectionType<T extends Collections, P extends keyof T> = T[P]['single'] extends true ? T[P]['schema']['_output'] : Array<T[P]['schema']['_output']>

/**
* All collections result
*/
export type Result<T extends Collections> = {
[K in keyof T]: T[K]['single'] extends true ? T[K]['schema']['_output'] : Array<T[K]['schema']['_output']>
}
export type Result<T extends Collections> = { [P in keyof T]: CollectionType<T, P> }

/**
* This interface for plugins extra user config
Expand Down Expand Up @@ -279,7 +282,7 @@ export interface Config extends Readonly<UserConfig> {
/**
* Define a collection (identity function for type inference)
*/
export const defineCollection = (collection: Collection) => collection
export const defineCollection = <T extends Schema>(collection: Collection<T>) => collection

/**
* Define a loader (identity function for type inference)
Expand All @@ -289,6 +292,6 @@ export const defineLoader = (loader: Loader) => loader
/**
* Define config (identity function for type inference)
*/
export const defineConfig = <C extends Collections>(config: UserConfig<C>) => config
export const defineConfig = <T extends Collections>(config: UserConfig<T>) => config

// ↑↑↑ helper identity functions for type inference

0 comments on commit 82bcef8

Please sign in to comment.