MDX: Support lazy load MDX files

This commit is contained in:
fuma-nama
2024-10-17 22:12:21 +08:00
parent 58b6b4c922
commit df9e0e1672
19 changed files with 333 additions and 309 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': minor
---
Support `async` output mode
+14 -6
View File
@@ -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,
-1
View File
@@ -8,7 +8,6 @@ const withAnalyzer = createBundleAnalyzer({
/** @type {import('next').NextConfig} */
const config = {
output: 'export',
reactStrictMode: true,
eslint: {
// Replaced by root workspace command
+2 -1
View File
@@ -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": {
+82 -29
View File
@@ -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,
}),
};
+5 -2
View File
@@ -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');
+25 -32
View File
@@ -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>[];
+9 -3
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
export * from './runtime/create';
export * from './runtime';
export type { Manifest } from './map/manifest';
+6 -4
View File
@@ -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 ?? {});
+72 -132
View File
@@ -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('.')) {
+37 -12
View File
@@ -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[],
+13 -37
View File
@@ -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;
-36
View File
@@ -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;
}
}
+2
View File
@@ -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
View File
@@ -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"})];
+31
View File
@@ -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',
);
});
+3
View File
@@ -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