Support Standard Schema for collection schema

This commit is contained in:
Fuma Nama
2025-02-05 11:53:56 +08:00
parent 83c2dd97e2
commit 233a2d1080
7 changed files with 68 additions and 70 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': minor
---
Support Standard Schema for collection `schema`
+1
View File
@@ -62,6 +62,7 @@
},
"devDependencies": {
"@fumadocs/mdx-remote": "workspace:*",
"@standard-schema/spec": "^1.0.0",
"@types/cross-spawn": "^6.0.6",
"@types/mdast": "^4.0.3",
"@types/mdx": "^2.0.13",
+12 -11
View File
@@ -1,4 +1,3 @@
import { type ZodTypeAny, type z } from 'zod';
import { type ProcessorOptions } from '@mdx-js/mdx';
import { type MDXOptions } from '@/utils/build-mdx';
import {
@@ -7,6 +6,7 @@ import {
type MarkdownProps,
} from '@/config/types';
import { frontmatterSchema, metaSchema } from '@/utils/schema';
import { StandardSchemaV1 } from '@standard-schema/spec';
export interface TransformContext {
path: string;
@@ -34,13 +34,14 @@ export interface BaseCollection<Schema> {
schema?: Schema | ((ctx: TransformContext) => Schema);
}
export interface MetaCollection<Schema extends ZodTypeAny = ZodTypeAny>
extends BaseCollection<Schema> {
export interface MetaCollection<
Schema extends StandardSchemaV1 = StandardSchemaV1,
> extends BaseCollection<Schema> {
type: 'meta';
}
export interface DocCollection<
Schema extends ZodTypeAny = ZodTypeAny,
Schema extends StandardSchemaV1 = StandardSchemaV1,
Async extends boolean = boolean,
> extends BaseCollection<Schema> {
type: 'doc';
@@ -55,7 +56,7 @@ export interface DocCollection<
export function defineCollections<
T extends 'doc' | 'meta',
Schema extends ZodTypeAny = ZodTypeAny,
Schema extends StandardSchemaV1 = StandardSchemaV1,
Async extends boolean = false,
>(
options: { type: T } & (T extends 'doc'
@@ -70,15 +71,15 @@ export function defineCollections<
runtime: T extends 'doc'
? Async extends true
? z.infer<Schema> &
? StandardSchemaV1.InferOutput<Schema> &
BaseCollectionEntry & {
load: () => Promise<MarkdownProps>;
}
: Omit<MarkdownProps, keyof z.infer<Schema>> &
z.infer<Schema> &
: Omit<MarkdownProps, keyof StandardSchemaV1.InferOutput<Schema>> &
StandardSchemaV1.InferOutput<Schema> &
BaseCollectionEntry
: typeof options extends MetaCollection
? z.infer<Schema> & BaseCollectionEntry
? StandardSchemaV1.InferOutput<Schema> & BaseCollectionEntry
: never;
};
} {
@@ -91,8 +92,8 @@ export function defineCollections<
}
export function defineDocs<
DocData extends ZodTypeAny = typeof frontmatterSchema,
MetaData extends ZodTypeAny = typeof metaSchema,
DocData extends StandardSchemaV1 = typeof frontmatterSchema,
MetaData extends StandardSchemaV1 = typeof metaSchema,
DocAsync extends boolean = false,
>(options?: {
/**
+15 -10
View File
@@ -6,6 +6,7 @@ import { getConfigHash, loadConfigCached } from '@/utils/config-cache';
import { buildMDX } from '@/utils/build-mdx';
import { formatError } from '@/utils/format-error';
import { getGitTimestamp } from './utils/git-timestamp';
import type { StandardSchemaV1 } from '@standard-schema/spec';
export interface Options {
/**
@@ -70,7 +71,7 @@ export default async function loader(
if (collection?.schema) {
let schema = collection.schema;
if (typeof schema === 'function') {
if (typeof schema === 'function' && !('~standard' in schema)) {
schema = schema({
async buildMDX(v, options = mdxOptions) {
const res = await buildMDX(
@@ -86,17 +87,21 @@ export default async function loader(
});
}
const result = await schema.safeParseAsync(matter.data);
if (result.error) {
callback(
new Error(
formatError(`invalid frontmatter in ${filePath}:`, result.error),
),
if ('~standard' in schema) {
const result = await (schema as StandardSchemaV1)['~standard'].validate(
matter.data,
);
return;
}
if (result.issues) {
callback(
new Error(
formatError(`invalid frontmatter in ${filePath}:`, result.issues),
),
);
return;
}
matter.data = result.data as Record<string, unknown>;
matter.data = result.value as Record<string, unknown>;
}
}
let timestamp: number | undefined;
+6 -30
View File
@@ -1,32 +1,8 @@
import { type ZodError, type ZodFormattedError } from 'zod';
import { StandardSchemaV1 } from '@standard-schema/spec';
export function formatError(message: string, error: ZodError): string {
const lines: string[] = [];
function walk(
key: string | undefined,
{ _errors, ...rest }: ZodFormattedError<any>,
padStart = 0,
): void {
if (key !== undefined || _errors.length > 0) {
const text = key
? `${key}: ${_errors.join('\n ')}`
: _errors.join('\n');
lines.push(
text
.split('\n')
.map((line) => `${' '.repeat(padStart)}${line}`)
.join('\n'),
);
}
for (const [k, v] of Object.entries(rest)) {
walk(key ? `${key}.${k}` : k, v as ZodFormattedError<any>, padStart + 2);
}
}
walk(undefined, error.format());
return [message, ...lines].join('\n');
export function formatError(
message: string,
issues: readonly StandardSchemaV1.Issue[],
): string {
return `${message}:\n${issues.map((issue) => ` ${issue.path}: ${issue.message}`).join('\n')}`;
}
+11 -8
View File
@@ -6,27 +6,30 @@ import { formatError } from '@/utils/format-error';
import { generateJS } from '@/map/generate';
import { defineCollections } from '@/config';
test('format errors', () => {
test('format errors', async () => {
const schema = z.object({
text: z.string(),
obj: z.object({
key: z.number(),
value: z.number(),
}),
value: z.string().max(4),
});
const result = schema.safeParse({
const result = await schema['~standard'].validate({
text: 4,
obj: {},
obj: {
value: 'string',
},
value: 'asfdfsdfsdfsd',
});
if (result.error)
expect(formatError('in index.mdx:', result.error)).toMatchInlineSnapshot(`
"in index.mdx:
if (result.issues)
expect(formatError('in index.mdx:', result.issues)).toMatchInlineSnapshot(`
"in index.mdx::
text: Expected string, received number
obj:
obj.key: Required
obj,key: Required
obj,value: Expected number, received string
value: String must contain at most 4 character(s)"
`);
});
+18 -11
View File
@@ -40,7 +40,7 @@ importers:
version: 6.0.1
tsup:
specifier: 8.3.6
version: 8.3.6(@swc/[email protected](@swc/[email protected]))([email protected])([email protected])([email protected])([email protected])([email protected])
version: 8.3.6(@swc/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
turbo:
specifier: 2.4.0
version: 2.4.0
@@ -810,6 +810,9 @@ importers:
'@fumadocs/mdx-remote':
specifier: workspace:*
version: link:../mdx-remote
'@standard-schema/spec':
specifier: ^1.0.0
version: 1.0.0
'@types/cross-spawn':
specifier: ^6.0.6
version: 6.0.6
@@ -848,7 +851,7 @@ importers:
version: 6.0.3
webpack:
specifier: ^5.97.1
version: 5.97.1(@swc/[email protected](@swc/[email protected]))([email protected])
version: 5.97.1(@swc/[email protected])([email protected])
packages/mdx-remote:
dependencies:
@@ -3090,6 +3093,9 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@standard-schema/[email protected]':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@swc/[email protected]':
resolution: {integrity: sha512-pOANQegUTAriW7jq3SSMZGM5l89yLVMs48R0F2UG6UZsH04SiViCnDctOGlA/Sa++25C+rL9MGMYM1jDLylBbg==}
engines: {node: '>=10'}
@@ -8947,6 +8953,8 @@ snapshots:
'@sindresorhus/[email protected]': {}
'@standard-schema/[email protected]': {}
'@swc/[email protected]':
optional: true
@@ -8977,7 +8985,7 @@ snapshots:
'@swc/[email protected]':
optional: true
'@swc/[email protected](@swc/[email protected])':
'@swc/[email protected]':
dependencies:
'@swc/counter': 0.1.3
'@swc/types': 0.1.17
@@ -8992,7 +9000,6 @@ snapshots:
'@swc/core-win32-arm64-msvc': 1.10.12
'@swc/core-win32-ia32-msvc': 1.10.12
'@swc/core-win32-x64-msvc': 1.10.12
'@swc/helpers': 0.5.15
optional: true
'@swc/[email protected]': {}
@@ -13140,16 +13147,16 @@ snapshots:
[email protected]: {}
[email protected](@swc/[email protected](@swc/[email protected]))([email protected])([email protected](@swc/[email protected](@swc/[email protected]))([email protected])):
[email protected](@swc/[email protected])([email protected])([email protected](@swc/[email protected])([email protected])):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.2
terser: 5.37.0
webpack: 5.97.1(@swc/[email protected](@swc/[email protected]))([email protected])
webpack: 5.97.1(@swc/[email protected])([email protected])
optionalDependencies:
'@swc/core': 1.10.12(@swc/[email protected])
'@swc/core': 1.10.12
esbuild: 0.24.2
[email protected]:
@@ -13234,7 +13241,7 @@ snapshots:
[email protected]: {}
[email protected](@swc/[email protected](@swc/[email protected]))([email protected])([email protected])([email protected])([email protected])([email protected]):
[email protected](@swc/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]):
dependencies:
bundle-require: 5.1.0([email protected])
cac: 6.7.14
@@ -13253,7 +13260,7 @@ snapshots:
tinyglobby: 0.2.10
tree-kill: 1.2.2
optionalDependencies:
'@swc/core': 1.10.12(@swc/[email protected])
'@swc/core': 1.10.12
postcss: 8.5.1
typescript: 5.7.3
transitivePeerDependencies:
@@ -13610,7 +13617,7 @@ snapshots:
[email protected]: {}
[email protected](@swc/[email protected](@swc/[email protected]))([email protected]):
[email protected](@swc/[email protected])([email protected]):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.6
@@ -13632,7 +13639,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.10(@swc/[email protected](@swc/[email protected]))([email protected])([email protected](@swc/[email protected](@swc/[email protected]))([email protected]))
terser-webpack-plugin: 5.3.10(@swc/[email protected])([email protected])([email protected](@swc/[email protected])([email protected]))
watchpack: 2.4.2
webpack-sources: 3.2.3
transitivePeerDependencies: