mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Support reusing content with include tag
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': minor
|
||||
---
|
||||
|
||||
Support reusing content with `include` tag
|
||||
@@ -31,3 +31,5 @@ Hello World dsasfdafsd
|
||||
| very **important** | Hey |
|
||||
| _Surprisingly_ | Fumadocs |
|
||||
| very long text that looks weird | hello world hello world hello world |
|
||||
|
||||
<include>./test.mdx</include>
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fast-glob": "^3.3.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"micromatch": "^4.0.8",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,6 +61,7 @@
|
||||
"@types/react": "^19.0.1",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"mdast-util-mdx-jsx": "^3.1.3",
|
||||
"next": "^15.1.1",
|
||||
"tsconfig": "workspace:*",
|
||||
"unified": "^11.0.5",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { build } from 'esbuild';
|
||||
import { validateConfig } from '@/config/validate';
|
||||
import type { DocCollection, MetaCollection } from '@/config/define';
|
||||
import { type GlobalConfig } from '@/config/types';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import { getDefaultMDXOptions } from '@/utils/mdx-options';
|
||||
|
||||
export function findConfigFile(): string {
|
||||
return path.resolve('source.config.ts');
|
||||
@@ -11,6 +12,7 @@ export function findConfigFile(): string {
|
||||
|
||||
export interface LoadedConfig {
|
||||
collections: Map<string, InternalDocCollection | InternalMetaCollection>;
|
||||
defaultMdxOptions: ProcessorOptions;
|
||||
global?: GlobalConfig;
|
||||
|
||||
_runtime: {
|
||||
@@ -48,7 +50,7 @@ export async function loadConfig(configPath: string): Promise<LoadedConfig> {
|
||||
}
|
||||
|
||||
const url = pathToFileURL(outputPath);
|
||||
const [err, config] = validateConfig(
|
||||
const [err, config] = buildConfig(
|
||||
// every call to `loadConfig` will cause the previous cache to be ignored
|
||||
(await import(`${url.toString()}?hash=${Date.now().toString()}`)) as Record<
|
||||
string,
|
||||
@@ -59,3 +61,46 @@ export async function loadConfig(configPath: string): Promise<LoadedConfig> {
|
||||
if (err !== null) throw new Error(err);
|
||||
return config;
|
||||
}
|
||||
|
||||
function buildConfig(
|
||||
config: Record<string, unknown>,
|
||||
): [err: string, value: null] | [err: null, value: LoadedConfig] {
|
||||
const collections: LoadedConfig['collections'] = new Map();
|
||||
let globalConfig: LoadedConfig['global'];
|
||||
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (!v) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && '_doc' in v && v._doc === 'collections') {
|
||||
collections.set(
|
||||
k,
|
||||
v as unknown as InternalMetaCollection | InternalDocCollection,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (k === 'default') {
|
||||
globalConfig = v as GlobalConfig;
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
`Unknown export "${k}", you can only export collections from source configuration file.`,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
null,
|
||||
{
|
||||
global: globalConfig,
|
||||
collections,
|
||||
defaultMdxOptions: getDefaultMDXOptions(globalConfig?.mdxOptions ?? {}),
|
||||
_runtime: {
|
||||
files: new Map(),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import {
|
||||
type InternalDocCollection,
|
||||
type InternalMetaCollection,
|
||||
type LoadedConfig,
|
||||
} from '@/config/load';
|
||||
import { type GlobalConfig } from '@/config/types';
|
||||
|
||||
export function validateConfig(
|
||||
config: Record<string, unknown>,
|
||||
): [err: string, value: null] | [err: null, value: LoadedConfig] {
|
||||
const out: LoadedConfig = {
|
||||
collections: new Map(),
|
||||
_runtime: {
|
||||
files: new Map(),
|
||||
},
|
||||
};
|
||||
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (!v) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && '_doc' in v && v._doc === 'collections') {
|
||||
out.collections.set(
|
||||
k,
|
||||
v as unknown as InternalMetaCollection | InternalDocCollection,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (k === 'default') {
|
||||
out.global = v as GlobalConfig;
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
`Unknown export "${k}", you can only export collections from source configuration file.`,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
return [null, out];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { type LoaderContext } from 'webpack';
|
||||
import { type StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import { getConfigHash, loadConfigCached } from '@/config/cached';
|
||||
import { buildMDX } from '@/utils/build-mdx';
|
||||
import { getDefaultMDXOptions, type TransformContext } from '@/config';
|
||||
import { type TransformContext } from '@/config';
|
||||
import { getManifestEntryPath } from '@/map/manifest';
|
||||
import { formatError } from '@/utils/format-error';
|
||||
import { getGitTimestamp } from './utils/git-timestamp';
|
||||
@@ -85,13 +85,11 @@ export default async function loader(
|
||||
collection = undefined;
|
||||
}
|
||||
|
||||
const mdxOptions =
|
||||
collection?.mdxOptions ??
|
||||
getDefaultMDXOptions(config.global?.mdxOptions ?? {});
|
||||
const mdxOptions = collection?.mdxOptions ?? config.defaultMdxOptions;
|
||||
|
||||
function getTransformContext(): TransformContext {
|
||||
return {
|
||||
buildMDX: async (v, options = mdxOptions) => {
|
||||
async buildMDX(v, options = mdxOptions) {
|
||||
const res = await buildMDX(
|
||||
collectionId ?? 'global',
|
||||
configHash,
|
||||
@@ -148,6 +146,7 @@ export default async function loader(
|
||||
data: {
|
||||
lastModified: timestamp,
|
||||
},
|
||||
_compiler: this,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function generateJS(
|
||||
configPath: string,
|
||||
config: LoadedConfig,
|
||||
outputPath: string,
|
||||
hash: string,
|
||||
configHash: string,
|
||||
getFrontmatter: (file: string) => Promise<unknown>,
|
||||
): Promise<string> {
|
||||
const outDir = path.dirname(outputPath);
|
||||
@@ -33,7 +33,7 @@ export async function generateJS(
|
||||
config._runtime.files.set(file.absolutePath, k);
|
||||
|
||||
if (collection.type === 'doc' && collection.async) {
|
||||
const importPath = `${toImportPath(file.absolutePath, outDir)}?hash=${hash}&collection=${k}`;
|
||||
const importPath = `${toImportPath(file.absolutePath, outDir)}?hash=${configHash}&collection=${k}`;
|
||||
const frontmatter = await getFrontmatter(file.absolutePath);
|
||||
|
||||
return `toRuntimeAsync(${JSON.stringify(frontmatter)}, () => import(${JSON.stringify(importPath)}), ${JSON.stringify(file)})`;
|
||||
@@ -43,7 +43,7 @@ export async function generateJS(
|
||||
imports.push({
|
||||
type: 'namespace',
|
||||
name: importName,
|
||||
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${k}&hash=${hash}`,
|
||||
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${k}&hash=${configHash}`,
|
||||
});
|
||||
|
||||
return `toRuntime("${collection.type}", ${importName}, ${JSON.stringify(file)})`;
|
||||
@@ -55,6 +55,7 @@ export async function generateJS(
|
||||
importedCollections.add(k);
|
||||
}
|
||||
|
||||
// TODO: remove `transform` API on next major, migrate to runtime transform (e.g. transform & re-export in `source.ts`)
|
||||
return collection.transform
|
||||
? `export const ${k} = await Promise.all([${resolvedItems.join(', ')}].map(v => c_${k}.transform(v, ${config.global ? 'c_default' : 'undefined'})));`
|
||||
: `export const ${k} = [${resolvedItems.join(', ')}];`;
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { createProcessor, type ProcessorOptions } from '@mdx-js/mdx';
|
||||
import type { VFile } from 'vfile';
|
||||
import type { Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
||||
import type { Literal } from 'mdast';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import matter from 'gray-matter';
|
||||
|
||||
const cache = new Map<
|
||||
string,
|
||||
{ processor: ReturnType<typeof createProcessor>; configHash: string }
|
||||
>();
|
||||
type Processor = ReturnType<typeof createProcessor>;
|
||||
|
||||
const cache = new Map<string, { processor: Processor; configHash: string }>();
|
||||
|
||||
export interface MDXOptions extends ProcessorOptions {
|
||||
/**
|
||||
@@ -23,12 +29,41 @@ export interface MDXOptions extends ProcessorOptions {
|
||||
* Custom Vfile data
|
||||
*/
|
||||
data?: Record<string, unknown>;
|
||||
|
||||
_compiler?: CompilerOptions;
|
||||
}
|
||||
|
||||
interface CompilerOptions {
|
||||
addDependency: (file: string) => void;
|
||||
}
|
||||
|
||||
function cacheKey(group: string, format: string): string {
|
||||
return `${group}:${format}`;
|
||||
}
|
||||
|
||||
function remarkInclude(this: Processor, options: CompilerOptions): Transformer {
|
||||
return (tree, file) => {
|
||||
visit(tree, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
|
||||
if (node.name === 'include') {
|
||||
const child = node.children.at(0) as Literal | undefined;
|
||||
|
||||
if (!child || child.type !== 'text') return;
|
||||
const specifier = child.value;
|
||||
|
||||
const targetPath = path.resolve(path.dirname(file.path), specifier);
|
||||
|
||||
const content = readFileSync(targetPath);
|
||||
const parsed = this.parse(matter(content).content);
|
||||
|
||||
options.addDependency(targetPath);
|
||||
Object.assign(node, parsed);
|
||||
}
|
||||
|
||||
return 'skip';
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param group - The cache group of MDX content, usually the collection name
|
||||
* @param configHash - config hash
|
||||
@@ -58,6 +93,12 @@ export function buildMDX(
|
||||
outputFormat: 'program',
|
||||
development: process.env.NODE_ENV === 'development',
|
||||
...rest,
|
||||
remarkPlugins: [
|
||||
options._compiler
|
||||
? ([remarkInclude, options._compiler] as any)
|
||||
: null,
|
||||
...(rest.remarkPlugins ?? []),
|
||||
],
|
||||
format,
|
||||
}),
|
||||
|
||||
|
||||
Generated
+6
@@ -782,6 +782,9 @@ importers:
|
||||
micromatch:
|
||||
specifier: ^4.0.8
|
||||
version: 4.0.8
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
zod:
|
||||
specifier: ^3.24.1
|
||||
version: 3.24.1
|
||||
@@ -807,6 +810,9 @@ importers:
|
||||
fumadocs-core:
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
mdast-util-mdx-jsx:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3
|
||||
next:
|
||||
specifier: ^15.1.1
|
||||
version: 15.1.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
|
||||
Reference in New Issue
Block a user