mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Support lazy load MDX files
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': minor
|
||||
---
|
||||
|
||||
Support `async` output mode
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
DocsCategory,
|
||||
} from 'fumadocs-ui/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { type ComponentProps, type FC, Fragment, type ReactNode } from 'react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
type FC,
|
||||
Fragment,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import defaultComponents from 'fumadocs-ui/mdx';
|
||||
import { Popup, PopupContent, PopupTrigger } from 'fumadocs-twoslash/ui';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
@@ -15,10 +21,11 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import * as Preview from '@/components/preview';
|
||||
import { createMetadata, metadataImage } from '@/utils/metadata';
|
||||
import { createMetadata } from '@/utils/metadata';
|
||||
import { openapi, source } from '@/app/source';
|
||||
import { Wrapper } from '@/components/preview/wrapper';
|
||||
import { AutoTypeTable } from '@/components/type-table';
|
||||
import { metadataImage } from '@/utils/metadata-image';
|
||||
|
||||
function PreviewRenderer({ preview }: { preview: string }): ReactNode {
|
||||
if (preview && preview in Preview) {
|
||||
@@ -31,7 +38,7 @@ function PreviewRenderer({ preview }: { preview: string }): ReactNode {
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ slug: string[] }>;
|
||||
}): Promise<React.ReactElement> {
|
||||
}): Promise<ReactElement> {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
|
||||
@@ -39,11 +46,12 @@ export default async function Page(props: {
|
||||
|
||||
const path = `apps/docs/content/docs/${page.file.path}`;
|
||||
const preview = page.data.preview;
|
||||
const { body: Mdx, toc, lastModified } = await page.data.load();
|
||||
|
||||
return (
|
||||
<DocsPage
|
||||
toc={page.data.toc}
|
||||
lastUpdate={page.data.lastModified}
|
||||
toc={toc}
|
||||
lastUpdate={lastModified}
|
||||
full={page.data.full}
|
||||
tableOfContent={{
|
||||
style: 'clerk',
|
||||
@@ -60,7 +68,7 @@ export default async function Page(props: {
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
{preview ? <PreviewRenderer preview={preview} /> : null}
|
||||
<page.data.body
|
||||
<Mdx
|
||||
components={{
|
||||
...defaultComponents,
|
||||
Popup,
|
||||
|
||||
@@ -8,7 +8,6 @@ const withAnalyzer = createBundleAnalyzer({
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
output: 'export',
|
||||
reactStrictMode: true,
|
||||
eslint: {
|
||||
// Replaced by root workspace command
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"build:registry": "tsx ./scripts/build-registry.mts",
|
||||
"build:shaders": "glslx shaders/rain.glsl --output=shaders/rain.min.js --format=js --renaming=internal-only",
|
||||
"clean": "rimraf .next",
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --turbo",
|
||||
"lint": "fumadocs-mdx && eslint .",
|
||||
"start": "next start"
|
||||
},
|
||||
@@ -49,6 +49,7 @@
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"shiki": "^1.22.0",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"ts-morph": "^24.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,9 +3,9 @@ import { type ProcessorOptions } from '@mdx-js/mdx';
|
||||
import { type MDXOptions } from '@/utils/build-mdx';
|
||||
import {
|
||||
type BaseCollectionEntry,
|
||||
type CollectionEntry,
|
||||
type FileInfo,
|
||||
type GlobalConfig,
|
||||
type SupportedType,
|
||||
type MarkdownProps,
|
||||
} from '@/config/types';
|
||||
import { frontmatterSchema, metaSchema } from '@/utils/schema';
|
||||
|
||||
@@ -19,11 +19,7 @@ export interface TransformContext {
|
||||
buildMDX: (source: string, options?: ProcessorOptions) => Promise<string>;
|
||||
}
|
||||
|
||||
export interface Collections<
|
||||
Schema extends ZodTypeAny = ZodTypeAny,
|
||||
Type extends SupportedType = SupportedType,
|
||||
Output extends BaseCollectionEntry = CollectionEntry<Type, z.output<Schema>>,
|
||||
> {
|
||||
export interface BaseCollection<Schema> {
|
||||
/**
|
||||
* Directories to scan
|
||||
*/
|
||||
@@ -37,11 +33,13 @@ export interface Collections<
|
||||
files?: string[];
|
||||
|
||||
schema?: Schema | ((ctx: TransformContext) => Schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* content type
|
||||
*/
|
||||
type: Type;
|
||||
export interface MetaCollection<
|
||||
Schema extends ZodTypeAny = ZodTypeAny,
|
||||
TransformOutput = unknown,
|
||||
> extends BaseCollection<Schema> {
|
||||
type: 'meta';
|
||||
|
||||
/**
|
||||
* Do transformation in runtime.
|
||||
@@ -49,33 +47,88 @@ export interface Collections<
|
||||
* This cannot be optimized by bundlers/loaders, avoid expensive calculations here.
|
||||
*/
|
||||
transform?: (
|
||||
entry: CollectionEntry<Type, z.output<Schema>>,
|
||||
entry: {
|
||||
data: z.output<Schema>;
|
||||
file: FileInfo;
|
||||
},
|
||||
globalConfig?: GlobalConfig,
|
||||
) => Output | Promise<Output>;
|
||||
) => TransformOutput | Promise<TransformOutput>;
|
||||
}
|
||||
|
||||
mdxOptions?: Type extends 'doc' ? MDXOptions : never;
|
||||
export interface DocCollection<
|
||||
Schema extends ZodTypeAny = ZodTypeAny,
|
||||
Async extends boolean = boolean,
|
||||
TransformOutput = unknown,
|
||||
> extends BaseCollection<Schema> {
|
||||
type: 'doc';
|
||||
|
||||
/**
|
||||
* Do transformation in runtime.
|
||||
*
|
||||
* This cannot be optimized by bundlers/loaders, avoid expensive calculations here.
|
||||
*/
|
||||
transform?: (
|
||||
entry: {
|
||||
data: z.output<Schema>;
|
||||
file: FileInfo;
|
||||
mdx: Async extends true ? MarkdownProps : never;
|
||||
},
|
||||
globalConfig?: GlobalConfig,
|
||||
) => TransformOutput | Promise<TransformOutput>;
|
||||
|
||||
mdxOptions?: MDXOptions;
|
||||
|
||||
/**
|
||||
* Load files with async
|
||||
*/
|
||||
async?: Async;
|
||||
}
|
||||
|
||||
export function defineCollections<
|
||||
Schema extends ZodTypeAny,
|
||||
Type extends SupportedType,
|
||||
Output extends BaseCollectionEntry = CollectionEntry<Type, z.output<Schema>>,
|
||||
T extends 'doc' | 'meta',
|
||||
Schema extends ZodTypeAny = ZodTypeAny,
|
||||
Async extends boolean = false,
|
||||
TransformOutput = unknown,
|
||||
>(
|
||||
options: Collections<Schema, Type, Output>,
|
||||
options: { type: T } & (T extends 'doc'
|
||||
? DocCollection<Schema, Async, TransformOutput>
|
||||
: MetaCollection<Schema, TransformOutput>),
|
||||
): {
|
||||
_doc: 'collections';
|
||||
} & Collections<Schema, Type, Output> {
|
||||
type: T;
|
||||
|
||||
_type: {
|
||||
async: Async;
|
||||
transform: TransformOutput;
|
||||
|
||||
runtime: T extends 'doc'
|
||||
? Async extends true
|
||||
? z.infer<Schema> &
|
||||
BaseCollectionEntry & {
|
||||
load: () => Promise<MarkdownProps>;
|
||||
}
|
||||
: Omit<MarkdownProps, keyof z.infer<Schema>> &
|
||||
z.infer<Schema> &
|
||||
BaseCollectionEntry
|
||||
: typeof options extends MetaCollection
|
||||
? z.infer<Schema> & BaseCollectionEntry
|
||||
: never;
|
||||
};
|
||||
} {
|
||||
return {
|
||||
_doc: 'collections',
|
||||
// @ts-expect-error -- internal type inferring
|
||||
_type: undefined,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export function defineDocs<
|
||||
F extends ZodTypeAny = typeof frontmatterSchema,
|
||||
M extends ZodTypeAny = typeof metaSchema,
|
||||
DocsOut extends BaseCollectionEntry = CollectionEntry<'doc', z.output<F>>,
|
||||
MetaOut extends BaseCollectionEntry = CollectionEntry<'meta', z.output<M>>,
|
||||
DocData extends ZodTypeAny = typeof frontmatterSchema,
|
||||
MetaData extends ZodTypeAny = typeof metaSchema,
|
||||
DocAsync extends boolean = false,
|
||||
DocOut = unknown,
|
||||
MetaOut = unknown,
|
||||
>(options?: {
|
||||
/**
|
||||
* The directory to scan files
|
||||
@@ -84,11 +137,11 @@ export function defineDocs<
|
||||
*/
|
||||
dir?: string | string[];
|
||||
|
||||
docs?: Omit<Partial<Collections<F, 'doc', DocsOut>>, 'dir'>;
|
||||
meta?: Omit<Partial<Collections<M, 'meta', MetaOut>>, 'dir'>;
|
||||
docs?: Partial<DocCollection<DocData, DocAsync, DocOut>>;
|
||||
meta?: Partial<MetaCollection<MetaData, MetaOut>>;
|
||||
}): {
|
||||
docs: Collections<F, 'doc', DocsOut>;
|
||||
meta: Collections<M, 'meta', MetaOut>;
|
||||
docs: ReturnType<typeof defineCollections<'doc', DocData, DocAsync, DocOut>>;
|
||||
meta: ReturnType<typeof defineCollections<'meta', MetaData, false, MetaOut>>;
|
||||
} {
|
||||
const dir = options?.dir ?? 'content/docs';
|
||||
|
||||
@@ -96,13 +149,13 @@ export function defineDocs<
|
||||
docs: defineCollections({
|
||||
type: 'doc',
|
||||
dir,
|
||||
schema: frontmatterSchema as unknown as F,
|
||||
schema: frontmatterSchema as unknown as DocData,
|
||||
...options?.docs,
|
||||
}),
|
||||
meta: defineCollections({
|
||||
type: 'meta',
|
||||
dir,
|
||||
schema: metaSchema as unknown as M,
|
||||
schema: metaSchema as unknown as MetaData,
|
||||
...options?.meta,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { build } from 'esbuild';
|
||||
import { validateConfig } from '@/config/validate';
|
||||
import { type Collections } from '@/config/define';
|
||||
import type { DocCollection, MetaCollection } from '@/config/define';
|
||||
import { type GlobalConfig } from '@/config/types';
|
||||
|
||||
export function findConfigFile(): string {
|
||||
@@ -10,7 +10,7 @@ export function findConfigFile(): string {
|
||||
}
|
||||
|
||||
export interface LoadedConfig {
|
||||
collections: Map<string, Collections>;
|
||||
collections: Map<string, InternalDocCollection | InternalMetaCollection>;
|
||||
global?: GlobalConfig;
|
||||
|
||||
_runtime: {
|
||||
@@ -21,6 +21,9 @@ export interface LoadedConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export type InternalDocCollection = DocCollection;
|
||||
export type InternalMetaCollection = MetaCollection;
|
||||
|
||||
export async function loadConfig(configPath: string): Promise<LoadedConfig> {
|
||||
const outputPath = path.resolve('.source/source.config.mjs');
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { type z } from 'zod';
|
||||
import { type AnyZodObject, type z } from 'zod';
|
||||
import type { MDXProps } from 'mdx/types';
|
||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import type { TableOfContents } from 'fumadocs-core/server';
|
||||
import { type Collections } from '@/config/define';
|
||||
import { type DefaultMDXOptions } from '@/utils/mdx-options';
|
||||
|
||||
export interface GlobalConfig {
|
||||
@@ -25,25 +24,23 @@ export interface GlobalConfig {
|
||||
generateManifest?: boolean;
|
||||
}
|
||||
|
||||
export type InferSchema<C> =
|
||||
C extends Collections<infer Schema, any, any> ? Schema : never;
|
||||
export type InferSchema<CollectionOut> = CollectionOut extends {
|
||||
_type: {
|
||||
schema: infer T;
|
||||
};
|
||||
}
|
||||
? T
|
||||
: never;
|
||||
|
||||
export type InferSchemaType<C> = z.output<InferSchema<C>>;
|
||||
|
||||
export type InferCollectionsProps<C> = SupportedTypes[C extends Collections<
|
||||
any,
|
||||
infer Type,
|
||||
any
|
||||
>
|
||||
? Type
|
||||
: never];
|
||||
export type InferSchemaType<C> =
|
||||
InferSchema<C> extends AnyZodObject ? z.output<InferSchema<C>> : never;
|
||||
|
||||
export interface FileInfo {
|
||||
path: string;
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
interface MarkdownProps {
|
||||
export interface MarkdownProps {
|
||||
body: (props: MDXProps) => React.ReactElement;
|
||||
structuredData: StructuredData;
|
||||
toc: TableOfContents;
|
||||
@@ -55,29 +52,25 @@ interface MarkdownProps {
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
export interface SupportedTypes {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- empty object
|
||||
meta: {};
|
||||
doc: MarkdownProps;
|
||||
}
|
||||
|
||||
export type SupportedType = keyof SupportedTypes;
|
||||
|
||||
export type CollectionEntry<Type extends SupportedType, Output> = Omit<
|
||||
SupportedTypes[Type],
|
||||
keyof Output
|
||||
> &
|
||||
Output &
|
||||
BaseCollectionEntry;
|
||||
export type CollectionEntry<
|
||||
CollectionOut extends {
|
||||
_type: {
|
||||
runtime: unknown;
|
||||
};
|
||||
},
|
||||
> = CollectionOut['_type']['runtime'];
|
||||
|
||||
export interface BaseCollectionEntry {
|
||||
_file: FileInfo;
|
||||
}
|
||||
|
||||
export type EntryFromCollection<C> =
|
||||
C extends Collections<any, any, infer Output> ? Output : never;
|
||||
|
||||
/**
|
||||
* Get output type of collections
|
||||
*/
|
||||
export type GetOutput<C> = EntryFromCollection<C>[];
|
||||
export type GetOutput<
|
||||
C extends {
|
||||
_type: {
|
||||
runtime: unknown;
|
||||
};
|
||||
},
|
||||
> = CollectionEntry<C>[];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { type LoadedConfig } from '@/config/load';
|
||||
import { type Collections } from '@/config/define';
|
||||
import {
|
||||
type InternalDocCollection,
|
||||
type InternalMetaCollection,
|
||||
type LoadedConfig,
|
||||
} from '@/config/load';
|
||||
import { type GlobalConfig } from '@/config/types';
|
||||
|
||||
export function validateConfig(
|
||||
@@ -18,7 +21,10 @@ export function validateConfig(
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && '_doc' in v && v._doc === 'collections') {
|
||||
out.collections.set(k, v as unknown as Collections);
|
||||
out.collections.set(
|
||||
k,
|
||||
v as unknown as InternalMetaCollection | InternalDocCollection,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './runtime/create';
|
||||
export * from './runtime';
|
||||
export type { Manifest } from './map/manifest';
|
||||
|
||||
@@ -4,7 +4,6 @@ import { parse } from 'node:querystring';
|
||||
import grayMatter from 'gray-matter';
|
||||
import { type LoaderContext } from 'webpack';
|
||||
import { type StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import { findCollectionId } from '@/utils/find-collection';
|
||||
import { getConfigHash, loadConfigCached } from '@/config/cached';
|
||||
import { buildMDX } from '@/utils/build-mdx';
|
||||
import { getDefaultMDXOptions, type TransformContext } from '@/config';
|
||||
@@ -75,14 +74,17 @@ export default async function loader(
|
||||
const query = getQuery(this.resourceQuery);
|
||||
const configHash = query.hash ?? (await getConfigHash(_ctx.configPath));
|
||||
const config = await loadConfigCached(_ctx.configPath, configHash);
|
||||
const collectionId =
|
||||
query.collection ?? findCollectionId(config, filePath, 'doc');
|
||||
const collectionId = query.collection;
|
||||
|
||||
const collection =
|
||||
let collection =
|
||||
collectionId !== undefined
|
||||
? config.collections.get(collectionId)
|
||||
: undefined;
|
||||
|
||||
if (collection && collection.type !== 'doc') {
|
||||
collection = undefined;
|
||||
}
|
||||
|
||||
const mdxOptions =
|
||||
collection?.mdxOptions ??
|
||||
getDefaultMDXOptions(config.global?.mdxOptions ?? {});
|
||||
|
||||
@@ -1,51 +1,67 @@
|
||||
import * as path from 'node:path';
|
||||
import fg from 'fast-glob';
|
||||
import { getTypeFromPath } from '@/utils/get-type-from-path';
|
||||
import type { Collections, FileInfo } from '@/config';
|
||||
import { type LoadedConfig } from '@/config/load';
|
||||
|
||||
interface VariableInfo {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
import type { FileInfo } from '@/config/types';
|
||||
import {
|
||||
type InternalDocCollection,
|
||||
type InternalMetaCollection,
|
||||
type LoadedConfig,
|
||||
} from '@/config/load';
|
||||
|
||||
export async function generateJS(
|
||||
configPath: string,
|
||||
config: LoadedConfig,
|
||||
outputPath: string,
|
||||
hash: string,
|
||||
getFrontmatter: (file: string) => Promise<unknown>,
|
||||
): Promise<string> {
|
||||
const outDir = path.dirname(outputPath);
|
||||
|
||||
const imports: ImportInfo[] = [
|
||||
{
|
||||
type: 'named',
|
||||
names: ['toRuntime'],
|
||||
names: ['toRuntime', 'toRuntimeAsync'],
|
||||
specifier: 'fumadocs-mdx',
|
||||
},
|
||||
];
|
||||
const importedCollections = new Set<string>();
|
||||
const variables: VariableInfo[] = [];
|
||||
|
||||
config._runtime.files.clear();
|
||||
const collected = await Promise.all(
|
||||
Array.from(config.collections.entries()).map(async ([name, collection]) =>
|
||||
processCollection(
|
||||
name,
|
||||
collection,
|
||||
config,
|
||||
hash,
|
||||
outDir,
|
||||
importedCollections,
|
||||
),
|
||||
),
|
||||
);
|
||||
const entries = Array.from(config.collections.entries());
|
||||
const declares = entries.map(async ([k, collection]) => {
|
||||
const files = await getCollectionFiles(collection);
|
||||
const items = files.map(async (file, i) => {
|
||||
config._runtime.files.set(file.absolutePath, k);
|
||||
|
||||
collected.forEach((v) => {
|
||||
imports.push(...v.imports);
|
||||
variables.push(v.variable);
|
||||
if (collection.type === 'doc' && collection.async) {
|
||||
const importPath = `${toImportPath(file.absolutePath, outDir)}?hash=${hash}&collection=${k}`;
|
||||
const frontmatter = await getFrontmatter(file.absolutePath);
|
||||
|
||||
return `toRuntimeAsync(${JSON.stringify(frontmatter)}, async () => await import(${JSON.stringify(importPath)}), ${JSON.stringify(file)})`;
|
||||
}
|
||||
|
||||
const importName = `${k}_${i.toString()}`;
|
||||
imports.push({
|
||||
type: 'namespace',
|
||||
name: importName,
|
||||
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${k}&hash=${hash}`,
|
||||
});
|
||||
|
||||
return `toRuntime("${collection.type}", ${importName}, ${JSON.stringify(file)})`;
|
||||
});
|
||||
const resolvedItems = await Promise.all(items);
|
||||
|
||||
if (collection.transform) {
|
||||
if (config.global) importedCollections.add('default'); // import global config
|
||||
importedCollections.add(k);
|
||||
}
|
||||
|
||||
return collection.transform
|
||||
? `export const ${k} = await Promise.all([${resolvedItems.join(', ')}].map(v => c_${k}.transform(v, c_default)));`
|
||||
: `export const ${k} = [${resolvedItems.join(', ')}];`;
|
||||
});
|
||||
|
||||
const resolvedDeclares = await Promise.all(declares);
|
||||
|
||||
if (importedCollections.size > 0) {
|
||||
imports.push({
|
||||
type: 'named',
|
||||
@@ -56,10 +72,36 @@ export async function generateJS(
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
...imports.map(getImportCode),
|
||||
...variables.map((v) => `export const ${v.name} = ${v.content};`),
|
||||
].join('\n');
|
||||
return [...imports.map(getImportCode), ...resolvedDeclares].join('\n');
|
||||
}
|
||||
|
||||
async function getCollectionFiles(
|
||||
collection: InternalDocCollection | InternalMetaCollection,
|
||||
): Promise<FileInfo[]> {
|
||||
const files = new Map<string, FileInfo>();
|
||||
const dirs = Array.isArray(collection.dir)
|
||||
? collection.dir
|
||||
: [collection.dir];
|
||||
|
||||
await Promise.all(
|
||||
dirs.map(async (dir) => {
|
||||
const result = await fg(collection.files ?? '**/*', {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
result.forEach((item) => {
|
||||
if (getTypeFromPath(item) !== collection.type) return;
|
||||
|
||||
files.set(item, {
|
||||
path: path.relative(dir, item),
|
||||
absolutePath: item,
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.from(files.values());
|
||||
}
|
||||
|
||||
type ImportInfo =
|
||||
@@ -79,108 +121,6 @@ type ImportInfo =
|
||||
specifier: string;
|
||||
};
|
||||
|
||||
async function processCollection(
|
||||
name: string,
|
||||
collection: Collections,
|
||||
config: LoadedConfig,
|
||||
configHash: string,
|
||||
importDir: string,
|
||||
requiredCollection: Set<string>,
|
||||
): Promise<{
|
||||
variable: VariableInfo;
|
||||
imports: ImportInfo[];
|
||||
}> {
|
||||
const dirs = Array.isArray(collection.dir)
|
||||
? collection.dir
|
||||
: [collection.dir];
|
||||
// absolute path to file info
|
||||
const files = new Map<string, FileInfo>();
|
||||
|
||||
for (const dir of dirs) {
|
||||
const result = await fg(collection.files ?? ['**/*'], {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
result.forEach((file) => {
|
||||
files.set(file, {
|
||||
path: path.relative(dir, file),
|
||||
absolutePath: file,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const output = processDirectory(
|
||||
Array.from(files.values()),
|
||||
config,
|
||||
configHash,
|
||||
name,
|
||||
collection,
|
||||
importDir,
|
||||
);
|
||||
|
||||
if (collection.transform) {
|
||||
if (config.global) requiredCollection.add('default'); // import global config
|
||||
requiredCollection.add(name);
|
||||
}
|
||||
|
||||
return {
|
||||
imports: output.imports,
|
||||
variable: {
|
||||
name,
|
||||
content: collection.transform
|
||||
? `await Promise.all([${output.entries.join(',')}].map(v => c_${name}.transform(v, c_default)))`
|
||||
: `[${output.entries.join(',')}]`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function processDirectory(
|
||||
files: FileInfo[],
|
||||
config: LoadedConfig,
|
||||
configHash: string,
|
||||
name: string,
|
||||
collection: Collections,
|
||||
importFrom: string,
|
||||
): {
|
||||
imports: ImportInfo[];
|
||||
entries: string[];
|
||||
} {
|
||||
const imports: ImportInfo[] = [];
|
||||
const entries: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (getTypeFromPath(file.absolutePath) !== collection.type) continue;
|
||||
if (config._runtime.files.has(file.absolutePath)) {
|
||||
const belongs = config._runtime.files.get(file.absolutePath);
|
||||
|
||||
console.warn(
|
||||
`[MDX] Files cannot exist in multiple collections: ${file.path} (${belongs ?? ''} and ${name})`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
config._runtime.files.set(file.absolutePath, name);
|
||||
|
||||
const importName = `${name}_${entries.length.toString()}`;
|
||||
imports.push({
|
||||
type: 'namespace',
|
||||
name: importName,
|
||||
specifier: `${toImportPath(file.absolutePath, importFrom)}?collection=${name}&hash=${configHash}`,
|
||||
});
|
||||
|
||||
entries.push(
|
||||
`toRuntime("${collection.type}", ${importName}, ${JSON.stringify(file)})`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
entries,
|
||||
imports,
|
||||
};
|
||||
}
|
||||
|
||||
function getImportCode(info: ImportInfo): string {
|
||||
const specifier = JSON.stringify(info.specifier);
|
||||
|
||||
@@ -198,7 +138,7 @@ function getImportCode(info: ImportInfo): string {
|
||||
return `import ${specifier}`;
|
||||
}
|
||||
|
||||
function toImportPath(file: string, dir: string): string {
|
||||
export function toImportPath(file: string, dir: string): string {
|
||||
let importPath = path.relative(dir, file);
|
||||
|
||||
if (!path.isAbsolute(importPath) && !importPath.startsWith('.')) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { type EventName } from 'chokidar/handler.js';
|
||||
import grayMatter from 'gray-matter';
|
||||
import { getConfigHash, loadConfigCached } from '@/config/cached';
|
||||
import { generateJS, generateTypes } from '@/map/generate';
|
||||
import { writeManifest } from '@/map/manifest';
|
||||
@@ -21,6 +23,28 @@ export async function start(
|
||||
const jsOut = path.resolve(outDir, `index.js`);
|
||||
const typeOut = path.resolve(outDir, `index.d.ts`);
|
||||
|
||||
const frontmatterCache = new Map<string, unknown>();
|
||||
let hookUpdate = false;
|
||||
|
||||
// TODO: Stream and read only the header
|
||||
const readFrontmatter = async (file: string): Promise<unknown> => {
|
||||
const cached = frontmatterCache.get(file);
|
||||
if (cached) return cached;
|
||||
hookUpdate = true;
|
||||
|
||||
return grayMatter({
|
||||
content: await readFile(file).then((res) => res.toString()),
|
||||
}).data;
|
||||
};
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
jsOut,
|
||||
await generateJS(configPath, config, jsOut, configHash, readFrontmatter),
|
||||
);
|
||||
fs.writeFileSync(typeOut, generateTypes(configPath, config, typeOut));
|
||||
console.log('[MDX] initialized map file');
|
||||
|
||||
if (dev) {
|
||||
const { watcher } = await import('@/map/watcher');
|
||||
const instance = watcher(configPath, config);
|
||||
@@ -36,15 +60,24 @@ export async function start(
|
||||
if (isConfigFile) {
|
||||
configHash = await getConfigHash(configPath);
|
||||
config = await loadConfigCached(configPath, configHash);
|
||||
fs.writeFileSync(typeOut, generateTypes(configPath, config, typeOut));
|
||||
await writeFile(typeOut, generateTypes(configPath, config, typeOut));
|
||||
console.log('[MDX] Updated map types');
|
||||
}
|
||||
|
||||
if (isConfigFile || event !== 'change') {
|
||||
fs.writeFileSync(
|
||||
if (isConfigFile || event !== 'change' || hookUpdate) {
|
||||
if (event === 'change') frontmatterCache.delete(file);
|
||||
|
||||
await writeFile(
|
||||
jsOut,
|
||||
await generateJS(configPath, config, jsOut, configHash),
|
||||
await generateJS(
|
||||
configPath,
|
||||
config,
|
||||
jsOut,
|
||||
configHash,
|
||||
readFrontmatter,
|
||||
),
|
||||
);
|
||||
|
||||
console.log('[MDX] Updated map file');
|
||||
}
|
||||
};
|
||||
@@ -58,14 +91,6 @@ export async function start(
|
||||
});
|
||||
}
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
jsOut,
|
||||
await generateJS(configPath, config, jsOut, configHash),
|
||||
);
|
||||
fs.writeFileSync(typeOut, generateTypes(configPath, config, typeOut));
|
||||
console.log('[MDX] initialized map file');
|
||||
|
||||
if (config.global?.generateManifest && !dev) {
|
||||
process.on('exit', () => {
|
||||
console.log('[MDX] writing manifest');
|
||||
|
||||
@@ -3,20 +3,14 @@ import {
|
||||
type PageData,
|
||||
type Source,
|
||||
} from 'fumadocs-core/source';
|
||||
import {
|
||||
type EntryFromCollection,
|
||||
type Collections,
|
||||
type FileInfo,
|
||||
type CollectionEntry,
|
||||
type SupportedType,
|
||||
} from '@/config';
|
||||
import { type BaseCollectionEntry, type FileInfo } from '@/config';
|
||||
import { resolveFiles } from '@/runtime/resolve-files';
|
||||
|
||||
export function toRuntime(
|
||||
type: SupportedType,
|
||||
type: 'doc' | 'meta',
|
||||
file: Record<string, unknown>,
|
||||
info: FileInfo,
|
||||
): EntryFromCollection<Collections> {
|
||||
): unknown {
|
||||
if (type === 'doc') {
|
||||
const { default: body, frontmatter, ...exports } = file;
|
||||
|
||||
@@ -35,9 +29,28 @@ export function toRuntime(
|
||||
};
|
||||
}
|
||||
|
||||
export function toRuntimeAsync(
|
||||
frontmatter: Record<string, unknown>,
|
||||
load: () => Promise<Record<string, unknown>>,
|
||||
info: FileInfo,
|
||||
): unknown {
|
||||
return {
|
||||
async load() {
|
||||
const { default: body, ...res } = await load();
|
||||
|
||||
return {
|
||||
body,
|
||||
...res,
|
||||
};
|
||||
},
|
||||
...frontmatter,
|
||||
_file: info,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMDXSource<
|
||||
Doc extends CollectionEntry<'doc', PageData>,
|
||||
Meta extends CollectionEntry<'meta', MetaData>,
|
||||
Doc extends PageData & BaseCollectionEntry,
|
||||
Meta extends MetaData & BaseCollectionEntry,
|
||||
>(
|
||||
docs: Doc[],
|
||||
meta: Meta[],
|
||||
@@ -1,13 +1,9 @@
|
||||
import path from 'node:path';
|
||||
import type { VirtualFile } from 'fumadocs-core/source';
|
||||
import { type CollectionEntry } from '@/config';
|
||||
|
||||
const pageTypes = ['.md', '.mdx'];
|
||||
const metaTypes = ['.json', '.yaml'];
|
||||
import type { BaseCollectionEntry } from '@/config';
|
||||
|
||||
interface ResolveOptions {
|
||||
docs: CollectionEntry<'doc', unknown>[];
|
||||
meta: CollectionEntry<'meta', unknown>[];
|
||||
docs: BaseCollectionEntry[];
|
||||
meta: BaseCollectionEntry[];
|
||||
|
||||
rootDir?: string;
|
||||
}
|
||||
@@ -21,40 +17,20 @@ export function resolveFiles({
|
||||
|
||||
for (const entry of docs) {
|
||||
if (!entry._file.path.startsWith(rootDir)) continue;
|
||||
const ext = path.extname(entry._file.path);
|
||||
|
||||
if (pageTypes.includes(ext)) {
|
||||
outputs.push({
|
||||
type: 'page',
|
||||
path: entry._file.path,
|
||||
data: entry,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Unknown Type: ${ext} on ${entry._file.path}, expected: ${pageTypes.toString()}`,
|
||||
);
|
||||
outputs.push({
|
||||
type: 'page',
|
||||
path: entry._file.path,
|
||||
data: entry,
|
||||
});
|
||||
}
|
||||
|
||||
for (const entry of meta) {
|
||||
if (!entry._file.path.startsWith(rootDir)) continue;
|
||||
const ext = path.extname(entry._file.path);
|
||||
|
||||
if (metaTypes.includes(ext)) {
|
||||
outputs.push({
|
||||
type: 'meta',
|
||||
path: entry._file.path,
|
||||
data: entry,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Unknown Type: ${ext} on ${entry._file.path}, expected: ${metaTypes.toString()}`,
|
||||
);
|
||||
outputs.push({
|
||||
type: 'meta',
|
||||
path: entry._file.path,
|
||||
data: entry,
|
||||
});
|
||||
}
|
||||
|
||||
return outputs;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import micromatch from 'micromatch';
|
||||
import { type LoadedConfig } from '@/config/load';
|
||||
import { type SupportedType } from '@/config/types';
|
||||
|
||||
export function findCollectionId(
|
||||
config: LoadedConfig,
|
||||
file: string,
|
||||
type: SupportedType,
|
||||
): string | undefined {
|
||||
const cached = config._runtime.files.get(file);
|
||||
|
||||
if (cached) return cached;
|
||||
|
||||
for (const [name, collection] of config.collections.entries()) {
|
||||
if (collection.type !== type) continue;
|
||||
const dirs = Array.isArray(collection.dir)
|
||||
? collection.dir
|
||||
: [collection.dir];
|
||||
|
||||
const isInDir = dirs.some((dir) => {
|
||||
const relative = path.relative(dir, path.dirname(file));
|
||||
|
||||
return !relative.startsWith('..') && !path.isAbsolute(relative);
|
||||
});
|
||||
if (!isInDir) continue;
|
||||
|
||||
const isIncluded = collection.files
|
||||
? micromatch.isMatch(file, collection.files)
|
||||
: true;
|
||||
if (!isIncluded) continue;
|
||||
|
||||
config._runtime.files.set(file, name);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { toRuntime, toRuntimeAsync } from "fumadocs-mdx"
|
||||
export const docs = [toRuntimeAsync({}, () => import("./index.mdx?hash=hash&collection=docs"), {"path":"index.mdx","absolutePath":"$cwd/packages/mdx/test/fixtures/index.mdx"}), toRuntimeAsync({}, () => import("./folder/test.mdx?hash=hash&collection=docs"), {"path":"folder/test.mdx","absolutePath":"$cwd/packages/mdx/test/fixtures/folder/test.mdx"})];
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { toRuntime } from "fumadocs-mdx"
|
||||
import { toRuntime, toRuntimeAsync } from "fumadocs-mdx"
|
||||
import * as docs_0 from "./index.mdx?collection=docs&hash=hash"
|
||||
import * as docs_1 from "./folder/test.mdx?collection=docs&hash=hash"
|
||||
export const docs = [toRuntime("doc", docs_0, {"path":"index.mdx","absolutePath":"$cwd/packages/mdx/test/fixtures/index.mdx"}),toRuntime("doc", docs_1, {"path":"folder/test.mdx","absolutePath":"$cwd/packages/mdx/test/fixtures/folder/test.mdx"})];
|
||||
export const docs = [toRuntime("doc", docs_0, {"path":"index.mdx","absolutePath":"$cwd/packages/mdx/test/fixtures/index.mdx"}), toRuntime("doc", docs_1, {"path":"folder/test.mdx","absolutePath":"$cwd/packages/mdx/test/fixtures/folder/test.mdx"})];
|
||||
@@ -39,6 +39,7 @@ test('generate JS index file', async () => {
|
||||
_runtime: {
|
||||
files: new Map(),
|
||||
},
|
||||
// @ts-expect-error -- test file
|
||||
collections: new Map([
|
||||
[
|
||||
'docs',
|
||||
@@ -51,9 +52,39 @@ test('generate JS index file', async () => {
|
||||
},
|
||||
path.join(file, './fixtures/index.output.js'),
|
||||
'hash',
|
||||
() => ({}),
|
||||
);
|
||||
|
||||
await expect(out.replaceAll(process.cwd(), '$cwd')).toMatchFileSnapshot(
|
||||
'./fixtures/index.output.js',
|
||||
);
|
||||
});
|
||||
|
||||
test('generate JS index file: async', async () => {
|
||||
const out = await generateJS(
|
||||
path.join(file, './fixtures/config.ts'),
|
||||
{
|
||||
_runtime: {
|
||||
files: new Map(),
|
||||
},
|
||||
// @ts-expect-error -- test file
|
||||
collections: new Map([
|
||||
[
|
||||
'docs',
|
||||
defineCollections({
|
||||
type: 'doc',
|
||||
dir: path.join(file, './fixtures'),
|
||||
async: true,
|
||||
}),
|
||||
],
|
||||
]),
|
||||
},
|
||||
path.join(file, './fixtures/index-async.output.js'),
|
||||
'hash',
|
||||
() => ({}),
|
||||
);
|
||||
|
||||
await expect(out.replaceAll(process.cwd(), '$cwd')).toMatchFileSnapshot(
|
||||
'./fixtures/index-async.output.js',
|
||||
);
|
||||
});
|
||||
|
||||
Generated
+3
@@ -155,6 +155,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: ^2.5.4
|
||||
version: 2.5.4
|
||||
ts-morph:
|
||||
specifier: ^24.0.0
|
||||
version: 24.0.0
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.23.8
|
||||
|
||||
Reference in New Issue
Block a user