Skip to content

Commit

Permalink
MDX: Implement collection API types
Browse files Browse the repository at this point in the history
  • Loading branch information
fuma-nama committed Aug 26, 2024
1 parent 7d32c21 commit 192b670
Show file tree
Hide file tree
Showing 16 changed files with 602 additions and 312 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ node_modules
dist
.contentlayer
.eslintcache
.source

# production
/build
Expand Down
5 changes: 5 additions & 0 deletions examples/next-mdx/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Hey there!

## Heading

sfd
asdfaasffsd
fsda
fsd

<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.vercel.app" />
Expand Down
3 changes: 2 additions & 1 deletion examples/next-mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"fumadocs-ui": "workspace:*",
"next": "^14.2.5",
"react": "18.3.1",
"react-dom": "18.3.1"
"react-dom": "18.3.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
Expand Down
12 changes: 12 additions & 0 deletions examples/next-mdx/source.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineDocs, defineCollections } from 'fumadocs-mdx/config';
import { z } from 'zod';

export const { docs, meta } = defineDocs();

export const blog = defineCollections({
type: 'doc',
dir: './content/blog',
schema: z.object({
title: z.string(),
}),
});
1 change: 1 addition & 0 deletions packages/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
},
"dependencies": {
"@mdx-js/mdx": "^3.0.1",
"@swc/core": "^1.7.18",
"cross-spawn": "^7.0.3",
"estree-util-value-to-estree": "^3.1.2",
"fast-glob": "^3.3.1",
Expand Down
212 changes: 2 additions & 210 deletions packages/mdx/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,210 +1,2 @@
import path from 'node:path';
import type { NextConfig } from 'next';
import {
rehypeCode,
remarkGfm,
remarkStructure,
remarkHeading,
type RehypeCodeOptions,
remarkImage,
type RemarkImageOptions,
type RemarkHeadingOptions,
} from 'fumadocs-core/mdx-plugins';
import type { Pluggable } from 'unified';
import type { Configuration } from 'webpack';
import { MapWebpackPlugin } from './webpack-plugins/map-plugin';
import remarkMdxExport from './mdx-plugins/remark-exports';
import type { LoaderOptions } from './loader';
import type { Options as MDXLoaderOptions } from './loader-mdx';
import {
SearchIndexPlugin,
type Options as SearchIndexPluginOptions,
} from './webpack-plugins/search-index-plugin';

type MDXOptions = Omit<
NonNullable<MDXLoaderOptions>,
'rehypePlugins' | 'remarkPlugins'
> & {
rehypePlugins?: ResolvePlugins;
remarkPlugins?: ResolvePlugins;

/**
* Properties to export from `vfile.data`
*/
valueToExport?: string[];

remarkHeadingOptions?: RemarkHeadingOptions;
remarkImageOptions?: RemarkImageOptions | false;
rehypeCodeOptions?: RehypeCodeOptions | false;
};

type ResolvePlugins = Pluggable[] | ((v: Pluggable[]) => Pluggable[]);

export interface CreateMDXOptions {
cwd?: string;

mdxOptions?: MDXOptions;

buildSearchIndex?:
| Omit<SearchIndexPluginOptions, 'rootContentDir' | 'rootMapFile'>
| boolean;

/**
* Where the root map.ts should be, relative to cwd
*
* @defaultValue `'./.map.ts'`
*/
rootMapPath?: string;

/**
* Where the content directory should be, relative to cwd
*
* @defaultValue `'./content'`
*/
rootContentPath?: string;

/**
* {@link LoaderOptions.include}
*/
include?: string | string[];
}

function pluginOption(
def: (v: Pluggable[]) => (Pluggable | false)[],
options: ResolvePlugins = [],
): Pluggable[] {
const list = def(Array.isArray(options) ? options : []).filter(
Boolean,
) as Pluggable[];

if (typeof options === 'function') {
return options(list);
}

return list;
}

function getMDXLoaderOptions({
valueToExport = [],
rehypeCodeOptions,
remarkImageOptions,
remarkHeadingOptions,
...mdxOptions
}: MDXOptions): MDXLoaderOptions {
const mdxExports = [
'structuredData',
'toc',
'frontmatter',
'lastModified',
...valueToExport,
];

const remarkPlugins = pluginOption(
(v) => [
remarkGfm,
[remarkHeading, remarkHeadingOptions],
remarkImageOptions !== false && [remarkImage, remarkImageOptions],
...v,
remarkStructure,
[remarkMdxExport, { values: mdxExports }],
],
mdxOptions.remarkPlugins,
);

const rehypePlugins = pluginOption(
(v) => [
rehypeCodeOptions !== false && [rehypeCode, rehypeCodeOptions],
...v,
],
mdxOptions.rehypePlugins,
);

return {
providerImportSource: 'next-mdx-import-source-file',
...mdxOptions,
remarkPlugins,
rehypePlugins,
};
}

const defaultPageExtensions = ['mdx', 'md', 'jsx', 'js', 'tsx', 'ts'];

function createMDX({
mdxOptions = {},
cwd = process.cwd(),
rootMapPath = './.map.ts',
rootContentPath = './content',
buildSearchIndex = false,
...loadOptions
}: CreateMDXOptions = {}) {
const rootMapFile = path.resolve(cwd, rootMapPath);
const rootContentDir = path.resolve(cwd, rootContentPath);
const mdxLoaderOptions = getMDXLoaderOptions(mdxOptions);

return (nextConfig: NextConfig = {}): NextConfig => {
return {
...nextConfig,
pageExtensions: nextConfig.pageExtensions ?? defaultPageExtensions,
webpack: (config: Configuration, options) => {
config.resolve ||= {};

const alias = config.resolve.alias as Record<string, unknown>;

alias['next-mdx-import-source-file'] = [
'private-next-root-dir/src/mdx-components',
'private-next-root-dir/mdx-components',
'@mdx-js/react',
];

config.module ||= {};
config.module.rules ||= [];

config.module.rules.push(
{
test: /\.mdx?$/,
use: [
options.defaultLoaders.babel,
{
loader: 'fumadocs-mdx/loader-mdx',
options: mdxLoaderOptions,
},
],
},
{
test: rootMapFile,
use: {
loader: 'fumadocs-mdx/loader',
options: {
rootContentDir,
rootMapFile,
...loadOptions,
} satisfies LoaderOptions,
},
},
);

config.plugins ||= [];

config.plugins.push(
new MapWebpackPlugin({
rootMapFile,
}),
);

if (buildSearchIndex !== false)
config.plugins.push(
new SearchIndexPlugin({
rootContentDir,
rootMapFile,
...(typeof buildSearchIndex === 'object' ? buildSearchIndex : {}),
}),
);

// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- not provided
return nextConfig.webpack?.(config, options) ?? config;
},
};
};
}

export { createMDX as default };
export * from './next/create';
export * from './config/index';
36 changes: 36 additions & 0 deletions packages/mdx/src/config/built-in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type AnyZodObject } from 'zod';
import {
type CollectionData,
type Collections,
defineCollections,
} from '@/config/collections';
import { frontmatterSchema, metaSchema } from '@/utils/schema';

export function defineDocs<
F extends AnyZodObject = typeof frontmatterSchema,
M extends AnyZodObject = typeof metaSchema,
DocsOut = CollectionData<F, 'doc'>,
MetaOut = CollectionData<M, 'meta'>,
>(options?: {
docs?: Partial<Collections<F, 'doc', DocsOut>>;
meta?: Partial<Collections<M, 'meta', MetaOut>>;
}): {
docs: Collections<F, 'doc', DocsOut>;
meta: Collections<M, 'meta', MetaOut>;
} {
return {
docs: defineCollections({
type: 'doc',
dir: 'content/docs',
schema: frontmatterSchema as unknown as F,
...options?.docs,
}),
meta: defineCollections({
type: 'meta',
dir: 'content/docs',
files: ['**/*/meta.json'],
schema: metaSchema as M,
...options?.meta,
}),
};
}
64 changes: 64 additions & 0 deletions packages/mdx/src/config/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { type AnyZodObject, type z } from 'zod';
import type { TableOfContents } from 'fumadocs-core/server';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { MDXProps } from 'mdx/types';

export interface MarkdownProps {
body: (props: MDXProps) => React.ReactElement;
structuredData: StructuredData;
toc: TableOfContents;
}

export interface SupportedTypes {
meta: Record<string, never>;
doc: MarkdownProps;
}

export type SupportedType = keyof SupportedTypes;

export type CollectionData<
Schema extends AnyZodObject,
Type extends SupportedType,
> = Omit<SupportedTypes[Type], keyof z.output<Schema>> & z.output<Schema>;

export interface Collections<
Schema extends AnyZodObject = AnyZodObject,
Type extends SupportedType = SupportedType,
Output = CollectionData<Schema, Type>,
> {
/**
* Directories to scan
*/
dir: string | string[];

/**
* what files to include/exclude (glob patterns)
*
* Include all files if not specified
*/
files?: string[];

schema: Schema;

/**
* content type
*/
type: Type;

transform?: (entry: CollectionData<Schema, Type>) => Output;
}

export function defineCollections<
Schema extends AnyZodObject,
Type extends SupportedType,
Output = CollectionData<Schema, Type>,
>(
options: Collections<Schema, Type, Output>,
): {
_doc: 'collections';
} & Collections<Schema, Type, Output> {
return {
_doc: 'collections',
...options,
};
}
3 changes: 3 additions & 0 deletions packages/mdx/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './types';
export * from './collections';
export * from './built-in';
Loading

0 comments on commit 192b670

Please sign in to comment.