mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Support plugins in Webpack loader environment
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': patch
|
||||
---
|
||||
|
||||
Support plugins in Webpack loader environment: now plugins can apply changes on MDX options too.
|
||||
@@ -1,21 +1,31 @@
|
||||
import type { BunPlugin } from 'bun';
|
||||
import { createMdxLoader } from '@/loaders/mdx';
|
||||
import { findConfigFile, resolvedConfig } from '@/loaders/config';
|
||||
import { findConfigFile } from '@/loaders/config';
|
||||
import { buildConfig } from '@/config/build';
|
||||
import { parse } from 'node:querystring';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { type CoreOptions, createCore } from '@/core';
|
||||
|
||||
export interface MdxPluginOptions {
|
||||
configPath?: string;
|
||||
}
|
||||
export type MdxPluginOptions = Partial<CoreOptions>;
|
||||
|
||||
export function createMdxPlugin(options: MdxPluginOptions = {}): BunPlugin {
|
||||
const { configPath = findConfigFile() } = options;
|
||||
const {
|
||||
environment = 'bun',
|
||||
outDir = '.source',
|
||||
configPath = findConfigFile(),
|
||||
} = options;
|
||||
|
||||
async function getMdxLoader() {
|
||||
const importPath = pathToFileURL(configPath).href;
|
||||
const out = buildConfig(await import(importPath));
|
||||
return createMdxLoader(resolvedConfig(out));
|
||||
const core = await createCore({
|
||||
environment,
|
||||
outDir,
|
||||
configPath,
|
||||
}).init({
|
||||
config: buildConfig(await import(importPath)),
|
||||
});
|
||||
|
||||
return createMdxLoader(core.creatConfigLoader());
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import type { LoadedConfig } from '@/loaders/config';
|
||||
import { EmitEntry, Plugin, PluginContext, PluginOption } from '@/plugins';
|
||||
import type { ConfigLoader, LoadedConfig } from '@/loaders/config';
|
||||
import type { EmitEntry, Plugin, PluginOption } from '@/plugins';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
|
||||
export interface ServerContext {
|
||||
watcher?: FSWatcher;
|
||||
}
|
||||
|
||||
export interface CoreOptions {
|
||||
environment: string;
|
||||
configPath: string;
|
||||
outDir: string;
|
||||
}
|
||||
|
||||
export function createCore(
|
||||
context: PluginContext,
|
||||
context: CoreOptions,
|
||||
defaultPlugins: PluginOption[] = [],
|
||||
) {
|
||||
let config: LoadedConfig;
|
||||
@@ -22,6 +28,7 @@ export function createCore(
|
||||
}
|
||||
|
||||
return {
|
||||
_options: context,
|
||||
async init(options: { config: LoadedConfig | Promise<LoadedConfig> }) {
|
||||
config = await options.config;
|
||||
plugins = [];
|
||||
@@ -45,6 +52,13 @@ export function createCore(
|
||||
getConfig() {
|
||||
return config;
|
||||
},
|
||||
creatConfigLoader(): ConfigLoader {
|
||||
return {
|
||||
getConfig() {
|
||||
return config;
|
||||
},
|
||||
};
|
||||
},
|
||||
async initServer(server: ServerContext) {
|
||||
for (const plugin of plugins) {
|
||||
await plugin.configureServer?.call(context, server);
|
||||
@@ -66,3 +80,5 @@ export function createCore(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type Core = ReturnType<typeof createCore>;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
} from '@/config';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import fs from 'node:fs/promises';
|
||||
import { Core } from '@/core';
|
||||
|
||||
export interface ConfigLoader {
|
||||
getConfig: () => LoadedConfig | Promise<LoadedConfig>;
|
||||
@@ -23,51 +24,47 @@ export function findConfigFile(): string {
|
||||
return path.resolve('source.config.ts');
|
||||
}
|
||||
|
||||
export function resolvedConfig(loaded: LoadedConfig): ConfigLoader {
|
||||
return {
|
||||
getConfig() {
|
||||
return loaded;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function staticConfig({
|
||||
outDir,
|
||||
configPath,
|
||||
core,
|
||||
buildConfig,
|
||||
}: {
|
||||
configPath: string;
|
||||
outDir: string;
|
||||
core: Core;
|
||||
buildConfig: boolean;
|
||||
}): ConfigLoader {
|
||||
let cached: Promise<LoadedConfig> | undefined;
|
||||
async function newConfig() {
|
||||
const { loadConfig } = await import('./load');
|
||||
await core.init({
|
||||
config: loadConfig(
|
||||
core._options.configPath,
|
||||
core._options.outDir,
|
||||
buildConfig,
|
||||
),
|
||||
});
|
||||
|
||||
return core.getConfig();
|
||||
}
|
||||
|
||||
return {
|
||||
async getConfig() {
|
||||
if (cached) return cached;
|
||||
|
||||
cached = import('./load').then((mod) =>
|
||||
mod.loadConfig(configPath, outDir, buildConfig),
|
||||
);
|
||||
|
||||
return cached;
|
||||
return (cached ??= newConfig());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function dynamicConfig({
|
||||
outDir,
|
||||
configPath,
|
||||
core,
|
||||
buildConfig,
|
||||
}: {
|
||||
configPath: string;
|
||||
outDir: string;
|
||||
core: Core;
|
||||
buildConfig: boolean;
|
||||
}): ConfigLoader {
|
||||
let loaded: { config: Promise<LoadedConfig>; hash: string } | undefined;
|
||||
|
||||
async function getConfigHash(): Promise<string> {
|
||||
const stats = await fs.stat(configPath).catch(() => undefined);
|
||||
const stats = await fs
|
||||
.stat(core._options.configPath)
|
||||
.catch(() => undefined);
|
||||
|
||||
if (stats) {
|
||||
return stats.mtime.getTime().toString();
|
||||
@@ -76,6 +73,19 @@ export function dynamicConfig({
|
||||
throw new Error('Cannot find config file');
|
||||
}
|
||||
|
||||
async function newConfig() {
|
||||
const { loadConfig } = await import('./load');
|
||||
await core.init({
|
||||
config: loadConfig(
|
||||
core._options.configPath,
|
||||
core._options.outDir,
|
||||
buildConfig,
|
||||
),
|
||||
});
|
||||
|
||||
return core.getConfig();
|
||||
}
|
||||
|
||||
return {
|
||||
async getConfig() {
|
||||
const hash = await getConfigHash();
|
||||
@@ -83,11 +93,8 @@ export function dynamicConfig({
|
||||
|
||||
loaded = {
|
||||
hash,
|
||||
config: import('./load').then((mod) =>
|
||||
mod.loadConfig(configPath, outDir, buildConfig),
|
||||
),
|
||||
config: newConfig(),
|
||||
};
|
||||
|
||||
return loaded.config;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,17 +26,6 @@ const cacheEntry = z.object({
|
||||
|
||||
type CacheEntry = z.infer<typeof cacheEntry>;
|
||||
|
||||
const hashes = new WeakMap<LoadedConfig, string>();
|
||||
|
||||
function getConfigHash(config: LoadedConfig) {
|
||||
let hash = hashes.get(config);
|
||||
if (hash) return hash;
|
||||
|
||||
hash = Date.now().toString();
|
||||
hashes.set(config, hash);
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
return async ({
|
||||
source: value,
|
||||
@@ -47,24 +36,34 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
}) => {
|
||||
const matter = fumaMatter(value);
|
||||
const parsed = querySchema.parse(query);
|
||||
const config = await configLoader.getConfig();
|
||||
|
||||
const loaded = await configLoader.getConfig();
|
||||
const cacheDir = isDevelopment
|
||||
? undefined
|
||||
: loaded.global.experimentalBuildCache;
|
||||
const cacheKey = `${parsed.hash}_${parsed.collection ?? 'global'}_${generateCacheHash(filePath)}`;
|
||||
let after: (() => Promise<void>) | undefined;
|
||||
|
||||
if (!isDevelopment && config.global.experimentalBuildCache) {
|
||||
const cacheDir = config.global.experimentalBuildCache;
|
||||
const cacheKey = `${parsed.hash}_${parsed.collection ?? 'global'}_${generateCacheHash(filePath)}`;
|
||||
|
||||
if (cacheDir) {
|
||||
const cached = await fs
|
||||
.readFile(path.join(cacheDir, cacheKey))
|
||||
.then((content) => cacheEntry.parse(JSON.parse(content.toString())))
|
||||
.catch(() => null);
|
||||
|
||||
if (cached && cached.hash === generateCacheHash(value)) return cached;
|
||||
after = async () => {
|
||||
await fs.mkdir(cacheDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(cacheDir, cacheKey),
|
||||
JSON.stringify({
|
||||
...out,
|
||||
hash: generateCacheHash(value),
|
||||
} satisfies CacheEntry),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const collection = parsed.collection
|
||||
? loaded.collections.get(parsed.collection)
|
||||
? config.collections.get(parsed.collection)
|
||||
: undefined;
|
||||
|
||||
let docCollection: DocCollection | undefined;
|
||||
@@ -97,7 +96,7 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
if (loaded.global.lastModifiedTime === 'git') {
|
||||
if (config.global.lastModifiedTime === 'git') {
|
||||
data.lastModified = (await getGitTimestamp(filePath))?.getTime();
|
||||
}
|
||||
|
||||
@@ -105,11 +104,11 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
const lineOffset = isDevelopment ? countLines(matter.matter) : 0;
|
||||
|
||||
const compiled = await buildMDX(
|
||||
`${getConfigHash(loaded)}:${parsed.collection ?? 'global'}`,
|
||||
`${getConfigHash(config)}:${parsed.collection ?? 'global'}`,
|
||||
'\n'.repeat(lineOffset) + matter.content,
|
||||
{
|
||||
development: isDevelopment,
|
||||
...(docCollection?.mdxOptions ?? (await loaded.getDefaultMDXOptions())),
|
||||
...(docCollection?.mdxOptions ?? (await config.getDefaultMDXOptions())),
|
||||
postprocess: docCollection?.postprocess,
|
||||
data,
|
||||
filePath,
|
||||
@@ -123,21 +122,22 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
map: compiled.map as SourceMap,
|
||||
};
|
||||
|
||||
if (cacheDir) {
|
||||
await fs.mkdir(cacheDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(cacheDir, cacheKey),
|
||||
JSON.stringify({
|
||||
...out,
|
||||
hash: generateCacheHash(value),
|
||||
} satisfies CacheEntry),
|
||||
);
|
||||
}
|
||||
|
||||
await after?.();
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
const hashes = new WeakMap<LoadedConfig, string>();
|
||||
|
||||
function getConfigHash(config: LoadedConfig) {
|
||||
let hash = hashes.get(config);
|
||||
if (hash) return hash;
|
||||
|
||||
hash = Date.now().toString();
|
||||
hashes.set(config, hash);
|
||||
return hash;
|
||||
}
|
||||
|
||||
function generateCacheHash(input: string): string {
|
||||
return createHash('md5').update(input).digest('hex');
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { findConfigFile, staticConfig } from '@/loaders/config';
|
||||
import { createMdxLoader } from '@/loaders/mdx';
|
||||
import { toNode } from '@/loaders/adapter';
|
||||
import { createCore } from '@/core';
|
||||
|
||||
const core = createCore({
|
||||
environment: 'node',
|
||||
configPath: findConfigFile(),
|
||||
outDir: '.source',
|
||||
});
|
||||
|
||||
export const load = toNode(
|
||||
createMdxLoader(
|
||||
staticConfig({
|
||||
configPath: findConfigFile(),
|
||||
outDir: '.source',
|
||||
core,
|
||||
buildConfig: true,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LoadedConfig } from '@/loaders/config';
|
||||
import type { ServerContext } from '@/core';
|
||||
import type { CoreOptions, ServerContext } from '@/core';
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
@@ -11,11 +11,7 @@ export interface EmitEntry {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface PluginContext {
|
||||
environment: 'next' | 'vite';
|
||||
configPath: string;
|
||||
outDir: string;
|
||||
}
|
||||
export type PluginContext = CoreOptions;
|
||||
|
||||
export interface Plugin {
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import { validate, ValidationError } from '@/utils/validation';
|
||||
import * as path from 'node:path';
|
||||
import { load } from 'js-yaml';
|
||||
import { createMdxLoader } from '@/loaders/mdx';
|
||||
import { findConfigFile, resolvedConfig } from '@/loaders/config';
|
||||
import { findConfigFile } from '@/loaders/config';
|
||||
import { toVite } from '@/loaders/adapter';
|
||||
import vite, { type IndexFileOptions } from '@/plugins/vite';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
@@ -54,7 +54,7 @@ export default async function mdx(
|
||||
const core = await createViteCore(options).init({
|
||||
config: buildConfig(config),
|
||||
});
|
||||
const mdxLoader = toVite(createMdxLoader(resolvedConfig(core.getConfig())));
|
||||
const mdxLoader = toVite(createMdxLoader(core.creatConfigLoader()));
|
||||
|
||||
async function transformMeta(
|
||||
path: string,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type LoaderContext } from 'webpack';
|
||||
import { createMdxLoader } from '@/loaders/mdx';
|
||||
import { dynamicConfig, staticConfig } from '@/loaders/config';
|
||||
import { toWebpack, type WebpackLoader } from '@/loaders/adapter';
|
||||
import { createCore } from '@/core';
|
||||
|
||||
export interface Options {
|
||||
configPath: string;
|
||||
@@ -25,22 +26,27 @@ export default async function loader(
|
||||
this.cacheable(true);
|
||||
this.addDependency(configPath);
|
||||
|
||||
instance ??= toWebpack(
|
||||
createMdxLoader(
|
||||
// the config is built on dev server
|
||||
isDev
|
||||
? dynamicConfig({
|
||||
outDir,
|
||||
configPath,
|
||||
buildConfig: false,
|
||||
})
|
||||
: staticConfig({
|
||||
outDir,
|
||||
configPath,
|
||||
buildConfig: false,
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (!instance) {
|
||||
const core = createCore({
|
||||
environment: 'webpack',
|
||||
outDir,
|
||||
configPath,
|
||||
});
|
||||
|
||||
instance = toWebpack(
|
||||
createMdxLoader(
|
||||
isDev
|
||||
? dynamicConfig({
|
||||
core,
|
||||
buildConfig: false,
|
||||
})
|
||||
: staticConfig({
|
||||
core,
|
||||
buildConfig: false,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await instance.call(this, source, callback);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user