MDX: Drop support for Zod 3 schemas

This commit is contained in:
Fuma Nama
2025-09-20 16:19:15 +08:00
parent f11f89d1a9
commit effe43d636
10 changed files with 63 additions and 112 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'fumadocs-mdx': major
---
**Drop support for Zod 3 schemas**
Zod 3 schemas are still allowed, but you cannot no longer extend on the Zod 4 schemas provided by Fumadocs.
-5
View File
@@ -18,11 +18,6 @@
"types": "./dist/config/index.d.ts",
"require": "./dist/config/index.cjs"
},
"./config/zod-3": {
"import": "./dist/config/zod-3.js",
"types": "./dist/config/zod-3.d.ts",
"require": "./dist/config/zod-3.cjs"
},
"./next": {
"import": "./dist/next/index.js",
"types": "./dist/next/index.d.ts",
+3 -7
View File
@@ -105,9 +105,9 @@ export function defineDocs<
DocSchema extends StandardSchemaV1 = typeof frontmatterSchema,
MetaSchema extends StandardSchemaV1 = typeof metaSchema,
Async extends boolean = false,
>(options?: {
>(options: {
/**
* The directory to scan files
* The content directory to scan files
*
* @defaultValue 'content/docs'
*/
@@ -116,11 +116,7 @@ export function defineDocs<
docs?: Omit<DocCollection<DocSchema, Async>, 'dir' | 'type'>;
meta?: Omit<MetaCollection<MetaSchema>, 'dir' | 'type'>;
}): DocsCollection<DocSchema, MetaSchema, Async> {
if (!options)
console.warn(
'[`source.config.ts`] Deprecated: please pass options to `defineDocs()` and specify a `dir`.',
);
const dir = options?.dir ?? 'content/docs';
const dir = options.dir ?? 'content/docs';
return {
type: 'docs',
-27
View File
@@ -1,27 +0,0 @@
// for backward compatibility
// while Fumadocs expose Zod v4 schemas, some Zod v3 users use `.extend()` on schemas
// we can expose v3 schema instead
import { z } from 'zod/v3';
export const metaSchema = z.object({
title: z.string().optional(),
pages: z.array(z.string()).optional(),
description: z.string().optional(),
root: z.boolean().optional(),
defaultOpen: z.boolean().optional(),
icon: z.string().optional(),
});
export const frontmatterSchema = z.object({
title: z.string(),
description: z.string().optional(),
icon: z.string().optional(),
full: z.boolean().optional(),
// Fumadocs OpenAPI generated
_openapi: z.object({}).passthrough().optional(),
});
export * from './define';
export { getDefaultMDXOptions, type DefaultMDXOptions } from '../mdx/preset';
export * from '@/mdx/remark-include';
+15 -14
View File
@@ -5,7 +5,7 @@ import type { LoadedConfig } from '@/utils/config';
import type { DocCollection, MetaCollection } from '@/config';
import { validate } from '@/utils/validation';
import { fileCache } from '@/map/file-cache';
import type { AsyncRuntimeFile, FileInfo } from '@/runtime/next/types';
import type { AsyncRuntimeFile } from '@/runtime/next/types';
import { load } from 'js-yaml';
import { getGitTimestamp } from '@/utils/git-timestamp';
import { fumaMatter } from '@/utils/fuma-matter';
@@ -15,6 +15,7 @@ import {
toImportPath,
} from '@/utils/import-formatter';
import { getGlobPatterns, isFileSupported } from '@/utils/collections';
import type { FileInfo } from '@/runtime/shared';
async function readFileWithCache(file: string): Promise<string> {
const cached = fileCache.read<string>('read-file', file);
@@ -57,7 +58,7 @@ export async function generateJS(
getImportCode({
type: 'namespace',
name: importId,
specifier: `${toImportPath(file.absolutePath, importPath)}?${params.join('&')}`,
specifier: `${toImportPath(file.fullPath, importPath)}?${params.join('&')}`,
}),
);
@@ -69,9 +70,9 @@ export async function generateJS(
async function getMetaEntries(collection: MetaCollection, files: FileInfo[]) {
const items = files.map(async (file) => {
const source = await readFileWithCache(file.absolutePath).catch(() => '');
const source = await readFileWithCache(file.fullPath).catch(() => '');
let data =
source.length === 0 ? {} : parseMetaEntry(file.absolutePath, source);
source.length === 0 ? {} : parseMetaEntry(file.fullPath, source);
if (collection?.schema) {
data = await validate(
@@ -79,9 +80,9 @@ export async function generateJS(
data,
{
source,
path: file.absolutePath,
path: file.fullPath,
},
`invalid data in ${file.absolutePath}`,
`invalid data in ${file.fullPath}`,
);
}
@@ -110,7 +111,7 @@ export async function generateJS(
const entries = files.map(async (file) => {
const parsed = fumaMatter(
await readFileWithCache(file.absolutePath).catch(() => ''),
await readFileWithCache(file.fullPath).catch(() => ''),
);
let data = parsed.data;
@@ -118,14 +119,14 @@ export async function generateJS(
data = await validate(
collection.schema,
parsed.data,
{ path: file.absolutePath, source: parsed.content },
`invalid frontmatter in ${file.absolutePath}`,
{ path: file.fullPath, source: parsed.content },
`invalid frontmatter in ${file.fullPath}`,
);
}
let lastModified: Date | undefined;
if (config.global?.lastModifiedTime === 'git') {
lastModified = await getGitTimestamp(file.absolutePath);
lastModified = await getGitTimestamp(file.fullPath);
}
return JSON.stringify({
@@ -191,15 +192,15 @@ async function getCollectionFiles(
dirs.map(async (dir) => {
const result = await glob(patterns, {
cwd: path.resolve(dir),
absolute: true,
});
for (const item of result) {
if (!isFileSupported(item, collection)) continue;
const fullPath = path.join(dir, item);
files.set(item, {
path: path.relative(dir, item),
absolutePath: item,
files.set(fullPath, {
path: item,
fullPath,
});
}
}),
+3 -3
View File
@@ -4,7 +4,7 @@ import {
type PageData,
type Source,
} from 'fumadocs-core/source';
import type { DocOut, Runtime } from './types';
import type { DocOut, MetaOut, Runtime } from './types';
import type { CompiledMDXProperties } from '@/mdx/build-mdx';
import * as fs from 'node:fs/promises';
import { type FileInfo, missingProcessedMarkdown } from '@/runtime/shared';
@@ -38,9 +38,9 @@ export const _runtime: Runtime = {
meta(files) {
return files.map((file) => {
return {
info: file.info,
...file.data,
_file: file.info,
};
} satisfies MetaOut;
}) as any;
},
docs(docs, metas) {
+2 -3
View File
@@ -24,9 +24,8 @@ export interface AsyncRuntimeFile {
export type DocOut<Schema extends StandardSchemaV1 = StandardSchemaV1> =
DocCollectionEntry<StandardSchemaV1.InferOutput<Schema>>;
export type MetaOut<Schema extends StandardSchemaV1> = MetaCollectionEntry<
StandardSchemaV1.InferOutput<Schema>
>;
export type MetaOut<Schema extends StandardSchemaV1 = StandardSchemaV1> =
MetaCollectionEntry<StandardSchemaV1.InferOutput<Schema>>;
export interface Runtime {
doc: <C>(
+33 -17
View File
@@ -12,6 +12,7 @@ import {
DocCollectionEntry,
DocData,
FileInfo,
MetaCollectionEntry,
missingProcessedMarkdown,
} from '@/runtime/shared';
import fs from 'node:fs/promises';
@@ -29,7 +30,7 @@ export interface ServerCreate<Config> extends BaseCreate<Config> {
) => Promise<
Source<{
pageData: DocCollectionEntry<DocOut>;
metaData: MetaOut;
metaData: MetaCollectionEntry<MetaOut>;
}>
>;
@@ -39,13 +40,20 @@ export interface ServerCreate<Config> extends BaseCreate<Config> {
) => Promise<
Source<{
pageData: AsyncDocCollectionEntry<DocOut>;
metaData: MetaOut;
metaData: MetaCollectionEntry<MetaOut>;
}>
>;
}
export function fromConfig<Config>(): ServerCreate<Config> {
const base = fromConfigBase<Config>();
function fileInfo(file: string, base: string): FileInfo {
return {
path: file,
fullPath: path.join(base, file),
};
}
function mapDocData(entry: CompiledMDXFile<any>): DocData {
return {
body: entry.default,
@@ -99,16 +107,23 @@ export function fromConfig<Config>(): ServerCreate<Config> {
};
}
function mapMetaData<Data>(
info: FileInfo,
content: Data,
): MetaCollectionEntry<Data> {
return {
info,
...content,
};
}
return {
...base,
async sourceAsync(doc, meta) {
const virtualFiles: Promise<VirtualFile>[] = [
...Object.entries(doc).map(async ([file, content]) => {
const info: FileInfo = {
path: file,
fullPath: path.join(content.base, file),
};
const info = fileInfo(file, content.base);
return {
type: 'page',
@@ -118,11 +133,13 @@ export function fromConfig<Config>(): ServerCreate<Config> {
} satisfies VirtualFile;
}),
...Object.entries(meta).map(async ([file, content]) => {
const info = fileInfo(file, content.base);
return {
type: 'meta',
path: file,
absolutePath: path.join(content.base, file),
data: await content(),
path: info.path,
absolutePath: info.fullPath,
data: mapMetaData(info, await content()),
} satisfies VirtualFile;
}),
];
@@ -132,24 +149,23 @@ export function fromConfig<Config>(): ServerCreate<Config> {
async sourceLazy(doc, meta) {
const virtualFiles: Promise<VirtualFile>[] = [
...Object.entries(doc.head).map(async ([file, frontmatter]) => {
const info: FileInfo = {
path: file,
fullPath: path.join(doc.base, file),
};
const info = fileInfo(file, doc.base);
return {
type: 'page',
path: file,
path: info.path,
absolutePath: info.fullPath,
data: mapPageDataLazy(info, await frontmatter(), doc.body[file]),
} satisfies VirtualFile;
}),
...Object.entries(meta).map(async ([file, content]) => {
const info = fileInfo(file, content.base);
return {
type: 'meta',
path: file,
absolutePath: path.join(content.base, file),
data: await content(),
path: info.path,
absolutePath: info.fullPath,
data: mapMetaData(info, await content()),
} satisfies VirtualFile;
}),
];
-35
View File
@@ -9,7 +9,6 @@ import type {
import type { ProcessorOptions } from '@mdx-js/mdx';
import { pathToFileURL } from 'node:url';
import { buildConfig } from '@/config/build';
import type { Plugin } from 'esbuild';
export function findConfigFile(): string {
return path.resolve('source.config.ts');
@@ -28,39 +27,6 @@ let cache: {
config: Promise<LoadedConfig>;
} | null = null;
async function isZod3() {
try {
const content = JSON.parse(
(await fs.readFile('node_modules/zod/package.json')).toString(),
);
const version = content.version;
return typeof version === 'string' && version.startsWith('3.');
} catch {
return false;
}
}
function createCompatZodPlugin(): Plugin {
return {
name: 'replace-zod-import',
async setup(build) {
const usingZod3 = await isZod3();
if (!usingZod3) return;
console.warn(
'[Fumadocs MDX] Noticed Zod v3 in your node_modules, we recommend upgrading to Zod v4 for better compatibility.',
);
build.onResolve({ filter: /^fumadocs-mdx\/config$/ }, () => {
return {
path: 'fumadocs-mdx/config/zod-3',
external: true,
};
});
},
};
}
async function compileConfig(configPath: string, outDir: string) {
const { build } = await import('esbuild');
@@ -73,7 +39,6 @@ async function compileConfig(configPath: string, outDir: string) {
platform: 'node',
format: 'esm',
packages: 'external',
plugins: [createCompatZodPlugin()],
outExtension: {
'.js': '.mjs',
},
-1
View File
@@ -5,7 +5,6 @@ export default defineConfig({
'./src/{index,loader-mdx,bin}.ts',
'./src/{config,next,vite,bun}/index.ts',
'./src/node/loader.ts',
'./src/config/zod-3.ts',
'./src/runtime/next/{index,async}.ts',
'./src/runtime/vite/{browser,server}.ts',
],