MDX: watch the output config path on Webpack

This commit is contained in:
Fuma Nama
2025-11-14 21:45:38 +08:00
parent 7237110201
commit 7b450d60c6
12 changed files with 65 additions and 77 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': major
---
Change `postInstall()` signature to `postInstall({ configPath, outDir, ... })`
+2 -2
View File
@@ -12,10 +12,10 @@ async function start() {
if (isNext) {
const { postInstall } = await import('./next');
await postInstall(configPath);
await postInstall({ configPath });
} else {
const { postInstall } = await import('./vite');
await postInstall(configPath);
await postInstall({ configPath });
}
}
+3 -3
View File
@@ -2,7 +2,7 @@ import type { BunPlugin } from 'bun';
import { createMdxLoader } from '@/loaders/mdx';
import { buildConfig } from '@/config/build';
import { pathToFileURL } from 'node:url';
import { type CoreOptions, createCore, findConfigFile } from '@/core';
import { _Defaults, type CoreOptions, createCore } from '@/core';
import { createIntegratedConfigLoader } from '@/loaders/config';
import { createMetaLoader } from '@/loaders/meta';
import { toBun } from '@/loaders/adapter';
@@ -17,8 +17,8 @@ export interface MdxPluginOptions extends Partial<CoreOptions> {
export function createMdxPlugin(options: MdxPluginOptions = {}): BunPlugin {
const {
environment = 'bun',
outDir = '.source',
configPath = findConfigFile(),
outDir = _Defaults.outDir,
configPath = _Defaults.configPath,
disableMetaFile = false,
} = options;
+8 -11
View File
@@ -1,15 +1,15 @@
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import type { LoadedConfig } from '@/config/build';
import { buildConfig } from '@/config/build';
import type { Core } from '@/core';
async function compileConfig(configPath: string, outDir: string) {
async function compileConfig(core: Core) {
const { build } = await import('esbuild');
const transformed = await build({
entryPoints: [{ in: configPath, out: 'source.config' }],
entryPoints: [{ in: core._options.configPath, out: 'source.config' }],
bundle: true,
outdir: outDir,
outdir: core._options.outDir,
target: 'node20',
write: true,
platform: 'node',
@@ -29,18 +29,15 @@ async function compileConfig(configPath: string, outDir: string) {
/**
* Load config
*
* @param configPath - config path
* @param outDir - output directory
* @param build - By default, it assumes the TypeScript config has been compiled to `.source/source.config.mjs`. Set this `true` to compile the config first.
* @param build - By default, it assumes the config file has been compiled. Set this `true` to compile the config first.
*/
export async function loadConfig(
configPath: string,
outDir: string,
core: Core,
build = false,
): Promise<LoadedConfig> {
if (build) await compileConfig(configPath, outDir);
if (build) await compileConfig(core);
const url = pathToFileURL(path.resolve(outDir, 'source.config.mjs'));
const url = pathToFileURL(core.getCompiledConfigPath());
// always return a new config
url.searchParams.set('hash', Date.now().toString());
+10 -3
View File
@@ -67,9 +67,10 @@ export interface EmitOptions {
filterPlugin?: (plugin: Plugin) => boolean;
}
export function findConfigFile(): string {
return path.resolve('source.config.ts');
}
export const _Defaults = {
configPath: 'source.config.ts',
outDir: '.source',
};
async function getPlugins(pluginOptions: PluginOption[]): Promise<Plugin[]> {
const plugins: Plugin[] = [];
@@ -120,6 +121,12 @@ export function createCore(
getConfig(): LoadedConfig {
return config;
},
/**
* The file path of compiled config file, the file may not exist (e.g. on Vite, or still compiling)
*/
getCompiledConfigPath(): string {
return path.join(options.outDir, 'source.config.mjs');
},
async initServer(server: ServerContext): Promise<void> {
server.watcher?.on('all', async (event, file) => {
if (event === 'change') removeFileCache(file);
+1 -5
View File
@@ -36,11 +36,7 @@ export function createStandaloneConfigLoader({
async function newConfig() {
const { loadConfig } = await import('../config/load-from-file');
await core.init({
config: loadConfig(
core._options.configPath,
core._options.outDir,
buildConfig,
),
config: loadConfig(core, buildConfig),
});
return core.getConfig();
+16 -27
View File
@@ -9,7 +9,7 @@ import * as path from 'node:path';
import { loadConfig } from '@/config/load-from-file';
import { ValidationError } from '@/utils/validation';
import next from '@/plugins/next';
import { type Core, createCore, findConfigFile } from '@/core';
import { _Defaults, type Core, createCore } from '@/core';
import { mdxLoaderGlob, metaLoaderGlob } from '@/loaders';
import type { IndexFilePluginOptions } from '@/plugins/index-file';
@@ -32,18 +32,19 @@ export interface CreateMDXOptions {
const defaultPageExtensions = ['mdx', 'md', 'jsx', 'js', 'tsx', 'ts'];
export function createMDX(createOptions: CreateMDXOptions = {}) {
const options = applyDefaults(createOptions);
const core = createNextCore(applyDefaults(createOptions));
const isDev = process.env.NODE_ENV === 'development';
if (process.env._FUMADOCS_MDX !== '1') {
process.env._FUMADOCS_MDX = '1';
void init(isDev, options);
void init(isDev, core);
}
return (nextConfig: NextConfig = {}): NextConfig => {
const loaderOptions: WebpackLoaderOptions = {
...options,
...core._options,
compiledConfigPath: core.getCompiledConfigPath(),
isDev,
};
@@ -122,15 +123,10 @@ export function createMDX(createOptions: CreateMDXOptions = {}) {
};
}
async function init(
dev: boolean,
options: Required<CreateMDXOptions>,
): Promise<void> {
const core = createNextCore(options);
async function init(dev: boolean, core: Core): Promise<void> {
async function initOrReload() {
await core.init({
config: loadConfig(options.configPath, options.outDir, true),
config: loadConfig(core, true),
});
await core.emitAndWrite();
}
@@ -140,10 +136,10 @@ async function init(
const watcher = new FSWatcher({
ignoreInitial: true,
persistent: true,
ignored: [options.outDir],
ignored: [core._options.outDir],
});
watcher.add(options.configPath);
watcher.add(core._options.configPath);
for (const collection of core.getConfig().collectionList) {
if (collection.type === 'docs') {
watcher.add(collection.docs.dir);
@@ -158,7 +154,7 @@ async function init(
});
watcher.on('all', async (_event, file) => {
if (path.resolve(file) === path.resolve(options.configPath)) {
if (path.resolve(file) === path.resolve(core._options.configPath)) {
// skip plugin listeners
watcher.removeAllListeners();
@@ -185,26 +181,19 @@ async function init(
}
}
export async function postInstall(
configPath = findConfigFile(),
outDir = '.source',
) {
const core = await createNextCore({
index: {},
outDir,
configPath,
}).init({
config: loadConfig(configPath, outDir, true),
export async function postInstall(options: CreateMDXOptions) {
const core = createNextCore(applyDefaults(options));
await core.init({
config: loadConfig(core, true),
});
await core.emitAndWrite();
}
function applyDefaults(options: CreateMDXOptions): Required<CreateMDXOptions> {
return {
index: {},
outDir: options.outDir ?? '.source',
configPath: options.configPath ?? findConfigFile(),
outDir: options.outDir ?? _Defaults.outDir,
configPath: options.configPath ?? _Defaults.configPath,
};
}
+3 -3
View File
@@ -1,4 +1,4 @@
import { createCore, findConfigFile } from '@/core';
import { _Defaults, createCore } from '@/core';
import { createMdxLoader } from '@/loaders/mdx';
import { toNode } from '@/loaders/adapter';
import { createStandaloneConfigLoader } from '@/loaders/config';
@@ -7,8 +7,8 @@ import { createMetaLoader } from '@/loaders/meta';
const core = createCore({
environment: 'node',
configPath: findConfigFile(),
outDir: '.source',
configPath: _Defaults.configPath,
outDir: _Defaults.outDir,
});
const configLoader = createStandaloneConfigLoader({
+8 -11
View File
@@ -5,7 +5,7 @@ import { createMdxLoader } from '@/loaders/mdx';
import { toVite } from '@/loaders/adapter';
import vite from '@/plugins/vite';
import type { FSWatcher } from 'chokidar';
import { createCore, findConfigFile } from '@/core';
import { _Defaults, createCore } from '@/core';
import { createIntegratedConfigLoader } from '@/loaders/config';
import { createMetaLoader } from '@/loaders/meta';
import { IndexFilePluginOptions } from '@/plugins/index-file';
@@ -102,16 +102,13 @@ export default async function mdx(
};
}
export async function postInstall(
configPath = findConfigFile(),
pluginOptions: PluginOptions = {},
) {
export async function postInstall(pluginOptions: PluginOptions = {}) {
const { loadConfig } = await import('@/config/load-from-file');
const options = applyDefaults(pluginOptions);
const core = await createViteCore(options).init({
config: loadConfig(configPath, options.outDir, true),
});
const core = createViteCore(applyDefaults(pluginOptions));
await core.init({
config: loadConfig(core, true),
});
await core.emitAndWrite();
}
@@ -130,7 +127,7 @@ function applyDefaults(options: PluginOptions): Required<PluginOptions> {
return {
updateViteConfig: options.updateViteConfig ?? true,
index: options.index ?? true,
configPath: options.configPath ?? 'source.config.ts',
outDir: options.outDir ?? '.source',
configPath: options.configPath ?? _Defaults.configPath,
outDir: options.outDir ?? _Defaults.outDir,
};
}
+3 -4
View File
@@ -1,8 +1,7 @@
import { Core, createCore } from '@/core';
import { type Core, type CoreOptions, createCore } from '@/core';
export interface WebpackLoaderOptions {
configPath: string;
outDir: string;
export interface WebpackLoaderOptions extends CoreOptions {
compiledConfigPath: string;
isDev: boolean;
}
+2 -3
View File
@@ -12,9 +12,8 @@ export default async function loader(
callback: LoaderContext<WebpackLoaderOptions>['callback'],
): Promise<void> {
const options = this.getOptions();
const { configPath, isDev } = options;
this.cacheable(true);
this.addDependency(configPath);
this.addDependency(options.compiledConfigPath);
if (!instance) {
instance = toWebpack(
@@ -22,7 +21,7 @@ export default async function loader(
createStandaloneConfigLoader({
core: getCore(options),
buildConfig: false,
mode: isDev ? 'dev' : 'production',
mode: options.isDev ? 'dev' : 'production',
}),
),
);
+4 -5
View File
@@ -6,23 +6,22 @@ import { getCore, type WebpackLoaderOptions } from '@/webpack';
let instance: WebpackLoader | undefined;
// TODO: improve the performance, this loader is also used for other json/yaml files that's not in a Fumadocs MDX collection.
export default async function loader(
this: LoaderContext<WebpackLoaderOptions>,
source: string,
callback: LoaderContext<WebpackLoaderOptions>['callback'],
): Promise<void> {
const { isDev, configPath } = this.getOptions();
const options = this.getOptions();
this.cacheable(true);
this.addDependency(configPath);
this.addDependency(options.compiledConfigPath);
if (!instance) {
instance = toWebpack(
createMetaLoader(
createStandaloneConfigLoader({
core: getCore(this.getOptions()),
core: getCore(options),
buildConfig: false,
mode: isDev ? 'dev' : 'production',
mode: options.isDev ? 'dev' : 'production',
}),
{
json: 'json',