mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: implement Plugins API
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
---
|
||||
'fumadocs-mdx': major
|
||||
---
|
||||
|
||||
**Vite: move `source.generated.ts` to `.source/index.ts`**
|
||||
|
||||
**Why:**
|
||||
- with Fumadocs MDX Plugins, we want to unify the output directory across Vite & Next.js.
|
||||
- `source.generated.ts` looks ugly compared by `.source`.
|
||||
|
||||
**Migrate:**
|
||||
|
||||
- run dev server/typegen to generate a `.source` folder.
|
||||
- import it over the original `source.generated.ts`.
|
||||
- note that both docs and `create-fumadocs-app` are updated to `.source` folder.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
'fumadocs-mdx': minor
|
||||
---
|
||||
|
||||
**Support Plugins API**
|
||||
|
||||
Fumadocs MDX is mostly a bundler plugin meant to be used with tools like Vite and Turbopack.
|
||||
|
||||
With Fumadocs MDX Plugins, you can extend Fumadocs MDX without worrying the underlying bundler.
|
||||
It is designed for:
|
||||
|
||||
- Generate files from config (e.g. types, index files, JSON schemas)
|
||||
- Modify received config
|
||||
@@ -13,7 +13,7 @@ async function start() {
|
||||
const { postInstall } = await import('./next');
|
||||
await postInstall(configPath);
|
||||
} else {
|
||||
const { postInstall } = await import('./vite/postinstall');
|
||||
const { postInstall } = await import('./vite');
|
||||
await postInstall(configPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { DefaultMDXOptions } from '@/loaders/mdx/preset';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import { frontmatterSchema, metaSchema } from '@/config/zod-4';
|
||||
import type { PostprocessOptions } from '@/loaders/mdx/remark-postprocess';
|
||||
import type { PluginOption } from '@/plugins';
|
||||
|
||||
export type CollectionSchema<Schema extends StandardSchemaV1, Context> =
|
||||
| Schema
|
||||
@@ -68,6 +69,8 @@ type GlobalConfigMDXOptions =
|
||||
} & ProcessorOptions);
|
||||
|
||||
export interface GlobalConfig {
|
||||
plugins?: PluginOption[];
|
||||
|
||||
/**
|
||||
* Configure global MDX options
|
||||
*/
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import type { NextConfig } from 'next';
|
||||
import type { Configuration } from 'webpack';
|
||||
import { findConfigFile } from '@/loaders/config';
|
||||
import { start } from './map';
|
||||
import { type Options as MDXLoaderOptions } from '../loader-mdx';
|
||||
import type { TurbopackOptions } from 'next/dist/server/config-shared';
|
||||
|
||||
export interface CreateMDXOptions {
|
||||
/**
|
||||
* Path to source configuration file
|
||||
*/
|
||||
configPath?: string;
|
||||
|
||||
/**
|
||||
* Directory for output files
|
||||
*
|
||||
* @defaultValue '.source'
|
||||
*/
|
||||
outDir?: string;
|
||||
}
|
||||
|
||||
const defaultPageExtensions = ['mdx', 'md', 'jsx', 'js', 'tsx', 'ts'];
|
||||
export { start };
|
||||
|
||||
export function createMDX({
|
||||
configPath = findConfigFile(),
|
||||
outDir = '.source',
|
||||
}: CreateMDXOptions = {}) {
|
||||
if (process.env._FUMADOCS_MDX !== '1') {
|
||||
process.env._FUMADOCS_MDX = '1';
|
||||
|
||||
void start(process.env.NODE_ENV === 'development', configPath, outDir);
|
||||
}
|
||||
|
||||
return (nextConfig: NextConfig = {}): NextConfig => {
|
||||
const mdxLoaderOptions: MDXLoaderOptions = {
|
||||
configPath,
|
||||
outDir,
|
||||
};
|
||||
const turbopack: TurbopackOptions = {
|
||||
...nextConfig.turbopack,
|
||||
rules: {
|
||||
...nextConfig.turbopack?.rules,
|
||||
'*.{md,mdx}': {
|
||||
loaders: [
|
||||
{
|
||||
loader: 'fumadocs-mdx/loader-mdx',
|
||||
options: mdxLoaderOptions as any,
|
||||
},
|
||||
],
|
||||
as: '*.js',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...nextConfig,
|
||||
turbopack,
|
||||
pageExtensions: nextConfig.pageExtensions ?? defaultPageExtensions,
|
||||
webpack: (config: Configuration, options) => {
|
||||
config.resolve ||= {};
|
||||
|
||||
config.module ||= {};
|
||||
config.module.rules ||= [];
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.mdx?$/,
|
||||
use: [
|
||||
options.defaultLoaders.babel,
|
||||
{
|
||||
loader: 'fumadocs-mdx/loader-mdx',
|
||||
options: mdxLoaderOptions,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
config.plugins ||= [];
|
||||
|
||||
return nextConfig.webpack?.(config, options) ?? config;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
+182
-16
@@ -1,26 +1,192 @@
|
||||
import type { NextConfig } from 'next';
|
||||
import type { Configuration } from 'webpack';
|
||||
import { findConfigFile, type LoadedConfig } from '@/loaders/config';
|
||||
import { type Options as MDXLoaderOptions } from '../loader-mdx';
|
||||
import type {
|
||||
TurbopackLoaderOptions,
|
||||
TurbopackOptions,
|
||||
} from 'next/dist/server/config-shared';
|
||||
import { createPluginHandler } from '@/plugins';
|
||||
import * as path from 'node:path';
|
||||
import { getConfigHash, loadConfig } from '@/loaders/config/load';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { generateJS } from '@/next/map/generate';
|
||||
import { findConfigFile } from '@/loaders/config';
|
||||
import { removeFileCache } from '@/next/file-cache';
|
||||
import { ValidationError } from '@/utils/validation';
|
||||
import next from '@/plugins/next';
|
||||
import type { EventName } from 'chokidar/handler.js';
|
||||
|
||||
export * from './create';
|
||||
export interface CreateMDXOptions {
|
||||
/**
|
||||
* Path to source configuration file
|
||||
*/
|
||||
configPath?: string;
|
||||
|
||||
/**
|
||||
* Directory for output files
|
||||
*
|
||||
* @defaultValue '.source'
|
||||
*/
|
||||
outDir?: string;
|
||||
}
|
||||
|
||||
const defaultPageExtensions = ['mdx', 'md', 'jsx', 'js', 'tsx', 'ts'];
|
||||
|
||||
export function createMDX(createOptions: CreateMDXOptions = {}) {
|
||||
const options = applyDefaults(createOptions);
|
||||
|
||||
if (process.env._FUMADOCS_MDX !== '1') {
|
||||
process.env._FUMADOCS_MDX = '1';
|
||||
|
||||
void init(process.env.NODE_ENV === 'development', options);
|
||||
}
|
||||
|
||||
return (nextConfig: NextConfig = {}): NextConfig => {
|
||||
const mdxLoaderOptions: MDXLoaderOptions = options;
|
||||
const turbopack: TurbopackOptions = {
|
||||
...nextConfig.turbopack,
|
||||
rules: {
|
||||
...nextConfig.turbopack?.rules,
|
||||
'*.{md,mdx}': {
|
||||
loaders: [
|
||||
{
|
||||
loader: 'fumadocs-mdx/loader-mdx',
|
||||
options: mdxLoaderOptions as unknown as TurbopackLoaderOptions,
|
||||
},
|
||||
],
|
||||
as: '*.js',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...nextConfig,
|
||||
turbopack,
|
||||
pageExtensions: nextConfig.pageExtensions ?? defaultPageExtensions,
|
||||
webpack: (config: Configuration, options) => {
|
||||
config.resolve ||= {};
|
||||
|
||||
config.module ||= {};
|
||||
config.module.rules ||= [];
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.mdx?$/,
|
||||
use: [
|
||||
options.defaultLoaders.babel,
|
||||
{
|
||||
loader: 'fumadocs-mdx/loader-mdx',
|
||||
options: mdxLoaderOptions,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
config.plugins ||= [];
|
||||
|
||||
return nextConfig.webpack?.(config, options) ?? config;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
async function init(
|
||||
dev: boolean,
|
||||
options: Required<CreateMDXOptions>,
|
||||
): Promise<void> {
|
||||
const pluginHandler = createNextPluginHandler(options);
|
||||
let configHash: string;
|
||||
let config: LoadedConfig;
|
||||
|
||||
async function updateConfig() {
|
||||
configHash = await getConfigHash(options.configPath);
|
||||
config = await pluginHandler.init(
|
||||
await loadConfig(options.configPath, options.outDir, configHash, true),
|
||||
);
|
||||
}
|
||||
|
||||
async function emitFiles() {
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
await pluginHandler.emitAndWrite();
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
console.error(err.toStringFormatted());
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[MDX] updated map file in ${performance.now() - start}ms`);
|
||||
}
|
||||
|
||||
async function devServer() {
|
||||
const { watcher } = await import('@/next/watcher');
|
||||
const instance = watcher(options.configPath, config, [options.outDir]);
|
||||
|
||||
async function onUpdate(event: EventName, file: string) {
|
||||
const absolutePath = path.resolve(file);
|
||||
if (event === 'change') removeFileCache(absolutePath);
|
||||
|
||||
if (absolutePath === path.resolve(options.configPath)) {
|
||||
await updateConfig();
|
||||
console.log('[MDX] restarting dev server');
|
||||
await instance.close();
|
||||
void devServer();
|
||||
}
|
||||
|
||||
await emitFiles();
|
||||
}
|
||||
|
||||
instance.on('ready', () => {
|
||||
console.log('[MDX] started dev server');
|
||||
});
|
||||
|
||||
instance.on('all', (event, file) => {
|
||||
void onUpdate(event, file);
|
||||
});
|
||||
|
||||
process.on('exit', () => {
|
||||
console.log('[MDX] closing dev server');
|
||||
void instance.close();
|
||||
});
|
||||
}
|
||||
|
||||
await updateConfig();
|
||||
await emitFiles();
|
||||
if (dev) void devServer();
|
||||
}
|
||||
|
||||
export async function postInstall(
|
||||
configPath = findConfigFile(),
|
||||
outDir = '.source',
|
||||
) {
|
||||
const config = await loadConfig(configPath, outDir, undefined, true);
|
||||
const outPath = path.join(outDir, 'index.ts');
|
||||
|
||||
// clean past results
|
||||
await fs.rm(outDir, { recursive: true });
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
|
||||
const hash = await getConfigHash(configPath);
|
||||
await fs.writeFile(
|
||||
outPath,
|
||||
await generateJS(configPath, config, { relativeTo: outDir }, hash),
|
||||
const pluginHandler = createNextPluginHandler({
|
||||
outDir,
|
||||
configPath,
|
||||
});
|
||||
await pluginHandler.init(
|
||||
await loadConfig(configPath, outDir, undefined, true),
|
||||
);
|
||||
|
||||
await pluginHandler.emitAndWrite();
|
||||
console.log('[MDX] types generated');
|
||||
}
|
||||
|
||||
function applyDefaults(options: CreateMDXOptions): Required<CreateMDXOptions> {
|
||||
return {
|
||||
outDir: options.outDir ?? '.source',
|
||||
configPath: options.configPath ?? findConfigFile(),
|
||||
};
|
||||
}
|
||||
|
||||
function createNextPluginHandler({
|
||||
outDir,
|
||||
configPath,
|
||||
}: Required<CreateMDXOptions>) {
|
||||
return createPluginHandler(
|
||||
{
|
||||
environment: 'next',
|
||||
outDir,
|
||||
configPath,
|
||||
},
|
||||
[next()],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { getConfigHash, loadConfig } from '@/loaders/config/load';
|
||||
import { generateJS } from '@/next/map/generate';
|
||||
import { removeFileCache } from '@/next/map/file-cache';
|
||||
import { ValidationError } from '@/utils/validation';
|
||||
|
||||
/**
|
||||
* Start a MDX server that builds index and manifest files.
|
||||
*
|
||||
* In development mode, it starts a file watcher to auto-update output as your input changes.
|
||||
*/
|
||||
export async function start(
|
||||
dev: boolean,
|
||||
configPath: string,
|
||||
outDir: string,
|
||||
): Promise<void> {
|
||||
// init
|
||||
let configHash = await getConfigHash(configPath);
|
||||
let config = await loadConfig(configPath, outDir, configHash, true);
|
||||
const outPath = path.resolve(outDir, `index.ts`);
|
||||
|
||||
async function updateMapFile() {
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
await fs.writeFile(
|
||||
outPath,
|
||||
await generateJS(
|
||||
configPath,
|
||||
config,
|
||||
{ relativeTo: outDir },
|
||||
configHash,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
console.error(err.toStringFormatted());
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[MDX] updated map file in ${performance.now() - start}ms`);
|
||||
}
|
||||
|
||||
await updateMapFile();
|
||||
|
||||
if (dev) {
|
||||
const { watcher } = await import('@/next/map/watcher');
|
||||
const instance = watcher(configPath, config, [outPath]);
|
||||
|
||||
instance.on('ready', () => {
|
||||
console.log('[MDX] started dev server');
|
||||
});
|
||||
|
||||
instance.on('all', (event, file) => {
|
||||
if (typeof file !== 'string') return;
|
||||
const absolutePath = path.resolve(file);
|
||||
|
||||
const onUpdate = async () => {
|
||||
const isConfigFile = absolutePath === configPath;
|
||||
|
||||
if (isConfigFile) {
|
||||
configHash = await getConfigHash(configPath);
|
||||
config = await loadConfig(configPath, outDir, configHash, true);
|
||||
}
|
||||
|
||||
if (event === 'change') removeFileCache(absolutePath);
|
||||
|
||||
await updateMapFile();
|
||||
};
|
||||
|
||||
void onUpdate();
|
||||
});
|
||||
|
||||
process.on('exit', () => {
|
||||
console.log('[MDX] closing dev server');
|
||||
void instance.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { LoadedConfig } from '@/loaders/config';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
export interface EmitEntry {
|
||||
/**
|
||||
* path relative to output directory
|
||||
*/
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface PluginContext {
|
||||
environment: 'next' | 'vite';
|
||||
configPath: string;
|
||||
outDir: string;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
/**
|
||||
* on config loaded
|
||||
*/
|
||||
config?: (
|
||||
this: PluginContext,
|
||||
config: LoadedConfig,
|
||||
) => Awaitable<void | LoadedConfig>;
|
||||
|
||||
/**
|
||||
* Generate files (e.g. types, index file, or JSON schemas)
|
||||
*/
|
||||
emit?: (this: PluginContext) => EmitEntry[] | Promise<EmitEntry[]>;
|
||||
}
|
||||
|
||||
export type PluginOption = Awaitable<Plugin | Plugin[] | false>;
|
||||
|
||||
export function createPluginHandler(
|
||||
context: PluginContext,
|
||||
defaultPlugins: PluginOption[] = [],
|
||||
) {
|
||||
const plugins: Plugin[] = [];
|
||||
|
||||
async function write(entry: EmitEntry) {
|
||||
const file = path.join(context.outDir, entry.path);
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, entry.content);
|
||||
}
|
||||
|
||||
return {
|
||||
async init(config: LoadedConfig): Promise<LoadedConfig> {
|
||||
if (config.global.plugins) {
|
||||
defaultPlugins.push(...config.global.plugins);
|
||||
}
|
||||
|
||||
for await (const option of defaultPlugins) {
|
||||
if (!option) continue;
|
||||
if (Array.isArray(option)) plugins.push(...option);
|
||||
else plugins.push(option);
|
||||
}
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const out = await plugin.config?.call(context, config);
|
||||
if (out) config = out;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
async emit(): Promise<EmitEntry[]> {
|
||||
const out = await Promise.all(
|
||||
plugins.map((plugin) => {
|
||||
return plugin.emit?.call(context) ?? [];
|
||||
}),
|
||||
);
|
||||
|
||||
return out.flat();
|
||||
},
|
||||
async emitAndWrite(): Promise<void> {
|
||||
const entries = await this.emit();
|
||||
|
||||
await Promise.all(entries.map(write));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { glob } from 'tinyglobby';
|
||||
import type { LoadedConfig } from '@/loaders/config';
|
||||
import type { DocCollection, MetaCollection } from '@/config';
|
||||
import { validate } from '@/utils/validation';
|
||||
import { readFileWithCache } from '@/next/map/file-cache';
|
||||
import { readFileWithCache } from '@/next/file-cache';
|
||||
import { load } from 'js-yaml';
|
||||
import { getGitTimestamp } from '@/utils/git-timestamp';
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
@@ -15,8 +15,29 @@ import {
|
||||
} from '@/utils/import-formatter';
|
||||
import { getGlobPatterns, isFileSupported } from '@/utils/collections';
|
||||
import type { FileInfo } from '@/runtime/shared';
|
||||
import type { Plugin } from '@/plugins/index';
|
||||
|
||||
export async function generateJS(
|
||||
export default function next(): Plugin {
|
||||
let config: LoadedConfig;
|
||||
|
||||
return {
|
||||
config(v) {
|
||||
config = v;
|
||||
},
|
||||
async emit() {
|
||||
return [
|
||||
{
|
||||
path: 'index.ts',
|
||||
content: await indexFile(this.configPath, config, {
|
||||
relativeTo: this.outDir,
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function indexFile(
|
||||
configPath: string,
|
||||
config: LoadedConfig,
|
||||
importPath: ImportPathConfig,
|
||||
@@ -7,11 +7,11 @@ import type {
|
||||
import { ident, toImportPath } from '@/utils/import-formatter';
|
||||
import type { LoadedConfig } from '@/loaders/config';
|
||||
import { getGlobPatterns } from '@/utils/collections';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
generateGlobImport,
|
||||
type GlobImportOptions,
|
||||
} from '@/utils/glob-import';
|
||||
import type { Plugin } from '@/plugins';
|
||||
|
||||
export interface IndexFileOptions {
|
||||
/**
|
||||
@@ -22,31 +22,39 @@ export interface IndexFileOptions {
|
||||
* - `false` (default): no fallback.
|
||||
*/
|
||||
runtime?: 'bun' | 'node' | false;
|
||||
|
||||
/**
|
||||
* Output index file path
|
||||
*
|
||||
* @defaultValue 'source.generated.ts'
|
||||
*/
|
||||
out?: string;
|
||||
|
||||
/**
|
||||
* add `.js` extensions to imports, needed for ESM without bundler resolution
|
||||
*/
|
||||
addJsExtension?: boolean;
|
||||
}
|
||||
|
||||
export function entry(
|
||||
export default function vite(options: IndexFileOptions): Plugin {
|
||||
let config: LoadedConfig;
|
||||
|
||||
return {
|
||||
config(v) {
|
||||
config = v;
|
||||
},
|
||||
emit() {
|
||||
console.log('[Fumadocs MDX] Generating index files');
|
||||
|
||||
return [
|
||||
{
|
||||
path: 'index.ts',
|
||||
content: indexFile(this.configPath, this.outDir, config, options),
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function indexFile(
|
||||
configPath: string,
|
||||
outDir: string,
|
||||
config: LoadedConfig,
|
||||
options: IndexFileOptions,
|
||||
) {
|
||||
const {
|
||||
out = 'source.generated.ts',
|
||||
addJsExtension = false,
|
||||
runtime,
|
||||
} = options;
|
||||
const outDir = path.dirname(out);
|
||||
const { addJsExtension = false, runtime } = options;
|
||||
|
||||
const lines = [
|
||||
'/// <reference types="vite/client" />',
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
import { buildConfig } from '@/config/build';
|
||||
import { parse } from 'node:querystring';
|
||||
import { validate, ValidationError } from '@/utils/validation';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { load } from 'js-yaml';
|
||||
import { entry, type IndexFileOptions } from '@/vite/generate';
|
||||
import { createMdxLoader } from '@/loaders/mdx';
|
||||
import { resolvedConfig } from '@/loaders/config';
|
||||
import { findConfigFile, resolvedConfig } from '@/loaders/config';
|
||||
import { toVite } from '@/loaders/adapter';
|
||||
import vite, { type IndexFileOptions } from '@/plugins/vite';
|
||||
import { createPluginHandler } from '@/plugins';
|
||||
|
||||
const FumadocsDeps = ['fumadocs-core', 'fumadocs-ui', 'fumadocs-openapi'];
|
||||
|
||||
@@ -36,21 +36,24 @@ export interface PluginOptions {
|
||||
* @defaultValue true
|
||||
*/
|
||||
updateViteConfig?: boolean;
|
||||
|
||||
/**
|
||||
* Output directory of generated files
|
||||
*
|
||||
* @defaultValue '.source'
|
||||
*/
|
||||
outDir?: string;
|
||||
}
|
||||
|
||||
export * from './postinstall';
|
||||
|
||||
export default function mdx(
|
||||
export default async function mdx(
|
||||
config: Record<string, unknown>,
|
||||
options: PluginOptions = {},
|
||||
): Plugin {
|
||||
const {
|
||||
generateIndexFile = true,
|
||||
updateViteConfig = true,
|
||||
configPath = 'source.config.ts',
|
||||
} = options;
|
||||
const loaded = buildConfig(config);
|
||||
pluginOptions: PluginOptions = {},
|
||||
): Promise<Plugin> {
|
||||
const options = applyDefaults(pluginOptions);
|
||||
const { updateViteConfig } = options;
|
||||
const pluginHandler = createVitePluginHandler(options);
|
||||
|
||||
const loaded = await pluginHandler.init(buildConfig(config));
|
||||
const mdxLoader = toVite(createMdxLoader(resolvedConfig(loaded)));
|
||||
|
||||
async function transformMeta(
|
||||
@@ -118,17 +121,8 @@ export default function mdx(
|
||||
} satisfies UserConfig);
|
||||
},
|
||||
async buildStart() {
|
||||
let indexFileOptions = generateIndexFile;
|
||||
if (!indexFileOptions) return;
|
||||
if (typeof indexFileOptions !== 'object') indexFileOptions = {};
|
||||
|
||||
const { out = 'source.generated.ts' } = indexFileOptions;
|
||||
|
||||
console.log('[Fumadocs MDX] Generating index files');
|
||||
await fs.mkdir(path.dirname(out), { recursive: true });
|
||||
await fs.writeFile(out, entry(configPath, loaded, indexFileOptions));
|
||||
await pluginHandler.emitAndWrite();
|
||||
},
|
||||
|
||||
async transform(value, id) {
|
||||
const [file, query = ''] = id.split('?');
|
||||
const ext = path.extname(file);
|
||||
@@ -149,3 +143,46 @@ export default function mdx(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function postInstall(
|
||||
configPath = findConfigFile(),
|
||||
pluginOptions: PluginOptions = {},
|
||||
) {
|
||||
const { loadConfig } = await import('@/loaders/config/load');
|
||||
const options = applyDefaults(pluginOptions);
|
||||
const pluginHandler = createVitePluginHandler(options);
|
||||
|
||||
await pluginHandler.init(
|
||||
await loadConfig(configPath, options.outDir, undefined, true),
|
||||
);
|
||||
|
||||
await pluginHandler.emitAndWrite();
|
||||
console.log('[MDX] types generated');
|
||||
}
|
||||
|
||||
function createVitePluginHandler({
|
||||
configPath,
|
||||
outDir,
|
||||
generateIndexFile,
|
||||
}: Required<PluginOptions>) {
|
||||
return createPluginHandler(
|
||||
{
|
||||
environment: 'vite',
|
||||
configPath,
|
||||
outDir,
|
||||
},
|
||||
[
|
||||
generateIndexFile !== false &&
|
||||
vite(typeof generateIndexFile === 'object' ? generateIndexFile : {}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function applyDefaults(options: PluginOptions): Required<PluginOptions> {
|
||||
return {
|
||||
updateViteConfig: options.updateViteConfig ?? true,
|
||||
generateIndexFile: options.generateIndexFile ?? true,
|
||||
configPath: options.configPath ?? 'source.config.ts',
|
||||
outDir: options.outDir ?? '.source',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { loadConfig } from '@/loaders/config/load';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { entry, type IndexFileOptions } from '@/vite/generate';
|
||||
import { findConfigFile } from '@/loaders/config';
|
||||
|
||||
export async function postInstall(
|
||||
configPath = findConfigFile(),
|
||||
options: IndexFileOptions = {},
|
||||
) {
|
||||
const { out = 'source.generated.ts' } = options;
|
||||
const config = await loadConfig(configPath, 'node_modules', undefined, true);
|
||||
|
||||
await fs.mkdir(path.dirname(out), { recursive: true });
|
||||
await fs.writeFile(out, entry(configPath, config, options));
|
||||
console.log('[MDX] types generated');
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import * as path from 'node:path';
|
||||
import { expect, test } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
import { ValidationError } from '@/utils/validation';
|
||||
import { generateJS } from '@/next/map/generate';
|
||||
import { defineCollections } from '@/config';
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
import { indexFile } from '@/plugins/next';
|
||||
|
||||
test('format errors', async () => {
|
||||
const schema = z.object({
|
||||
@@ -62,7 +62,7 @@ const cases = [
|
||||
|
||||
for (const { name, collection } of cases) {
|
||||
test(`generate JS index file: ${name}`, async () => {
|
||||
const out = await generateJS(
|
||||
const out = await indexFile(
|
||||
path.join(baseDir, './fixtures/config.ts'),
|
||||
{
|
||||
// @ts-expect-error -- test file
|
||||
|
||||
@@ -255,7 +255,6 @@ export function CodeBlockTabsTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: currently Vite RSC plugin has problem with `asChild` due to children is automatically wrapped in <Fragment />, maybe revisit this in future
|
||||
export function CodeBlockTab(props: ComponentProps<typeof TabsContent>) {
|
||||
return <TabsContent {...props} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user