MDX: Improve mutability of LoadedConfig for plugins

This commit is contained in:
Fuma Nama
2025-10-31 23:24:44 +08:00
parent 4c3afa1578
commit 27fc4ed9db
19 changed files with 335 additions and 364 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': patch
---
[Internal] improve mutability of `LoadedConfig` for plugins
+3 -3
View File
@@ -1,10 +1,10 @@
import type { BunPlugin } from 'bun';
import { createMdxLoader } from '@/loaders/mdx';
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';
import { type CoreOptions, createCore, findConfigFile } from '@/core';
import { createIntegratedConfigLoader } from '@/loaders/config';
export type MdxPluginOptions = Partial<CoreOptions>;
@@ -25,7 +25,7 @@ export function createMdxPlugin(options: MdxPluginOptions = {}): BunPlugin {
config: buildConfig(await import(importPath)),
});
return createMdxLoader(core.creatConfigLoader());
return createMdxLoader(createIntegratedConfigLoader(core));
}
return {
+106 -9
View File
@@ -1,15 +1,100 @@
import type { LoadedConfig } from '@/loaders/config';
import type { ProcessorOptions } from '@mdx-js/mdx';
import type {
AnyCollection,
DocCollection,
DocsCollection,
GlobalConfig,
MetaCollection,
} from '@/config/define';
import type { ProcessorOptions } from '@mdx-js/mdx';
import picomatch from 'picomatch';
export interface LoadedConfig {
collectionList: CollectionItem[];
getCollection(name: string): CollectionItem | undefined;
global: GlobalConfig;
getDefaultMDXOptions(mode?: 'default' | 'remote'): Promise<ProcessorOptions>;
}
export type CollectionItem =
| MetaCollectionItem
| DocCollectionItem
| DocsCollectionItem;
type PrimitiveCollectionItem<T extends DocCollection | MetaCollection> = Omit<
T,
'files'
> & {
name: string;
hasFile: (filePath: string) => boolean;
isFileSupported: (filePath: string) => boolean;
patterns: string[];
};
export type MetaCollectionItem = PrimitiveCollectionItem<MetaCollection>;
export type DocCollectionItem = PrimitiveCollectionItem<DocCollection>;
export interface DocsCollectionItem extends DocsCollection {
name: string;
meta: MetaCollectionItem;
docs: DocCollectionItem;
}
const SupportedFormats = {
doc: ['mdx', 'md'],
meta: ['json', 'yaml'],
};
export function buildCollection(
name: string,
config: AnyCollection,
): CollectionItem {
if (config.type === 'docs') {
return {
...config,
name,
meta: buildPrimitiveCollection(name, config.meta),
docs: buildPrimitiveCollection(name, config.docs),
};
}
return buildPrimitiveCollection(name, config) as CollectionItem;
}
function buildPrimitiveCollection<T extends DocCollection | MetaCollection>(
name: string,
{ files, ...config }: T,
): PrimitiveCollectionItem<T> {
const supportedFormats = SupportedFormats[config.type];
const patterns = files ?? [`**/*.{${supportedFormats.join(',')}}`];
let matchers: picomatch.Matcher[];
return {
...config,
name,
patterns,
isFileSupported(filePath) {
return supportedFormats.some((format) => filePath.endsWith(`.${format}`));
},
hasFile(filePath) {
matchers ??= (Array.isArray(config.dir) ? config.dir : [config.dir]).map(
(dir) =>
picomatch(patterns, {
cwd: dir,
}),
);
return (
this.isFileSupported(filePath) &&
matchers.some((matcher) => matcher(filePath))
);
},
};
}
export function buildConfig(config: Record<string, unknown>): LoadedConfig {
const collections: LoadedConfig['collections'] = new Map();
let globalConfig: LoadedConfig['global'] = {};
const collections = new Map<string, CollectionItem>();
const loaded: GlobalConfig = {};
for (const [k, v] of Object.entries(config)) {
if (!v) {
@@ -18,18 +103,21 @@ export function buildConfig(config: Record<string, unknown>): LoadedConfig {
if (typeof v === 'object' && 'type' in v) {
if (v.type === 'docs') {
collections.set(k, v as DocsCollection);
collections.set(k, buildCollection(k, v as DocsCollection));
continue;
}
if (v.type === 'doc' || v.type === 'meta') {
collections.set(k, v as unknown as MetaCollection | DocCollection);
collections.set(
k,
buildCollection(k, v as MetaCollection | DocCollection),
);
continue;
}
}
if (k === 'default' && v) {
globalConfig = v as GlobalConfig;
Object.assign(loaded, v);
continue;
}
@@ -38,10 +126,19 @@ export function buildConfig(config: Record<string, unknown>): LoadedConfig {
);
}
if (loaded.collections) {
for (const [k, v] of Object.entries(loaded.collections)) {
collections.set(k, buildCollection(k, v));
}
}
const mdxOptionsCache = new Map<string, Promise<ProcessorOptions>>();
return {
global: globalConfig,
collections,
global: loaded,
collectionList: Array.from(collections.values()),
getCollection(name: string) {
return collections.get(name);
},
async getDefaultMDXOptions(mode = 'default'): Promise<ProcessorOptions> {
const cached = mdxOptionsCache.get(mode);
if (cached) return cached;
+1
View File
@@ -69,6 +69,7 @@ type GlobalConfigMDXOptions =
} & ProcessorOptions);
export interface GlobalConfig {
collections?: Record<string, AnyCollection>;
plugins?: PluginOption[];
/**
@@ -1,7 +1,7 @@
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import type { LoadedConfig } from '@/config/build';
import { buildConfig } from '@/config/build';
import type { LoadedConfig } from '@/loaders/config';
async function compileConfig(configPath: string, outDir: string) {
const { build } = await import('esbuild');
+5 -8
View File
@@ -1,4 +1,4 @@
import type { ConfigLoader, LoadedConfig } from '@/loaders/config';
import type { LoadedConfig } from '@/config/build';
import path from 'node:path';
import fs from 'node:fs/promises';
import type { FSWatcher } from 'chokidar';
@@ -66,6 +66,10 @@ export interface EmitOptions {
filterPlugin?: (plugin: Plugin) => boolean;
}
export function findConfigFile(): string {
return path.resolve('source.config.ts');
}
export function createCore(
options: CoreOptions,
defaultPlugins: PluginOption[] = [],
@@ -109,13 +113,6 @@ 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(this.getPluginContext(), server);
+72
View File
@@ -0,0 +1,72 @@
import type { Core } from '@/core';
import fs from 'node:fs/promises';
import type { LoadedConfig } from '@/config/build';
export interface ConfigLoader {
getConfig: () => LoadedConfig | Promise<LoadedConfig>;
}
export function createStandaloneConfigLoader({
core,
buildConfig,
mode,
}: {
/**
* core (not initialized)
*/
core: Core;
buildConfig: boolean;
/**
* In dev mode, the config file is dynamically re-loaded when it's updated.
*/
mode: 'dev' | 'production';
}): ConfigLoader {
let loaded: { config: Promise<LoadedConfig>; hash: string } | undefined;
async function getConfigHash(): Promise<string> {
if (mode === 'production') return 'static';
const stats = await fs.stat(core._options.configPath).catch(() => {
throw new Error('Cannot find config file');
});
return stats.mtime.getTime().toString();
}
async function newConfig() {
const { loadConfig } = await import('../config/load-from-file');
await core.init({
config: loadConfig(
core._options.configPath,
core._options.outDir,
buildConfig,
),
});
return core.getConfig();
}
return {
async getConfig() {
const hash = await getConfigHash();
if (loaded && loaded.hash === hash) return loaded.config;
loaded = {
hash,
config: newConfig(),
};
return loaded.config;
},
};
}
/**
* create config loader from initialized core
*/
export function createIntegratedConfigLoader(core: Core): ConfigLoader {
return {
getConfig() {
return core.getConfig();
},
};
}
-101
View File
@@ -1,101 +0,0 @@
import path from 'node:path';
import type {
DocCollection,
DocsCollection,
GlobalConfig,
MetaCollection,
} from '@/config';
import type { ProcessorOptions } from '@mdx-js/mdx';
import fs from 'node:fs/promises';
import type { Core } from '@/core';
export interface ConfigLoader {
getConfig: () => LoadedConfig | Promise<LoadedConfig>;
}
export interface LoadedConfig {
collections: Map<string, DocCollection | MetaCollection | DocsCollection>;
global: GlobalConfig;
getDefaultMDXOptions(mode?: 'default' | 'remote'): Promise<ProcessorOptions>;
}
export function findConfigFile(): string {
return path.resolve('source.config.ts');
}
export function staticConfig({
core,
buildConfig,
}: {
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() {
return (cached ??= newConfig());
},
};
}
export function dynamicConfig({
core,
buildConfig,
}: {
core: Core;
buildConfig: boolean;
}): ConfigLoader {
let loaded: { config: Promise<LoadedConfig>; hash: string } | undefined;
async function getConfigHash(): Promise<string> {
const stats = await fs
.stat(core._options.configPath)
.catch(() => undefined);
if (stats) {
return stats.mtime.getTime().toString();
}
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();
if (loaded && loaded.hash === hash) return loaded.config;
loaded = {
hash,
config: newConfig(),
};
return loaded.config;
},
};
}
+4 -4
View File
@@ -5,11 +5,11 @@ import { buildMDX } from '@/loaders/mdx/build-mdx';
import type { SourceMap } from 'rollup';
import type { Loader } from '@/loaders/adapter';
import { z } from 'zod';
import type { ConfigLoader, LoadedConfig } from '@/loaders/config';
import type { DocCollectionItem, LoadedConfig } from '@/config/build';
import fs from 'node:fs/promises';
import path from 'node:path';
import { createHash } from 'node:crypto';
import type { DocCollection } from '@/config';
import type { ConfigLoader } from '@/loaders/config';
const querySchema = z
.object({
@@ -63,10 +63,10 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
}
const collection = parsed.collection
? config.collections.get(parsed.collection)
? config.getCollection(parsed.collection)
: undefined;
let docCollection: DocCollection | undefined;
let docCollection: DocCollectionItem | undefined;
switch (collection?.type) {
case 'doc':
docCollection = collection;
+3 -4
View File
@@ -1,17 +1,16 @@
import type { NextConfig } from 'next';
import type { Configuration } from 'webpack';
import { findConfigFile } from '@/loaders/config';
import { type Options as MDXLoaderOptions } from '@/webpack';
import type {
TurbopackLoaderOptions,
TurbopackOptions,
} from 'next/dist/server/config-shared';
import * as path from 'node:path';
import { loadConfig } from '@/loaders/config/load';
import { loadConfig } from '@/config/load-from-file';
import { removeFileCache } from '@/next/file-cache';
import { ValidationError } from '@/utils/validation';
import next from '@/plugins/next';
import { type Core, createCore } from '@/core';
import { type Core, createCore, findConfigFile } from '@/core';
export interface CreateMDXOptions {
/**
@@ -112,7 +111,7 @@ async function init(
});
watcher.add(options.configPath);
for (const collection of core.getConfig().collections.values()) {
for (const collection of core.getConfig().collectionList) {
if (collection.type === 'docs') {
watcher.add(collection.docs.dir);
watcher.add(collection.meta.dir);
+4 -3
View File
@@ -1,7 +1,7 @@
import { findConfigFile, staticConfig } from '@/loaders/config';
import { createCore, findConfigFile } from '@/core';
import { createMdxLoader } from '@/loaders/mdx';
import { toNode } from '@/loaders/adapter';
import { createCore } from '@/core';
import { createStandaloneConfigLoader } from '@/loaders/config';
const core = createCore({
environment: 'node',
@@ -11,9 +11,10 @@ const core = createCore({
export const load = toNode(
createMdxLoader(
staticConfig({
createStandaloneConfigLoader({
core,
buildConfig: true,
mode: 'production',
}),
),
(filePath) => filePath.endsWith('.md') || filePath.endsWith('.mdx'),
+24 -13
View File
@@ -1,7 +1,10 @@
import type { EmitEntry, Plugin } from '@/core';
import type { LoadedConfig } from '@/loaders/config';
import type {
DocsCollectionItem,
LoadedConfig,
MetaCollectionItem,
} from '@/config/build';
import { z } from 'zod';
import { createCollectionMatcher } from '@/utils/collections';
import fs from 'node:fs/promises';
import path from 'node:path';
@@ -34,15 +37,23 @@ export default function jsonSchema({
},
configureServer(server) {
if (!server.watcher || !insert) return;
const matcher = createCollectionMatcher(this.core);
server.watcher.on('add', async (file) => {
const match = matcher.getFileCollection(file);
if (!match || match.collection.type !== 'meta') return;
const { name } = match;
const parent = config.collections.get(name);
let parent: DocsCollectionItem | undefined;
let match: MetaCollectionItem | undefined;
for (const collection of config.collectionList) {
if (collection.type === 'meta' && collection.hasFile(file)) {
match = collection;
break;
}
if (collection.type === 'docs' && collection.meta.hasFile(file)) {
parent = collection;
match = collection.meta;
break;
}
}
if (!match) return;
let obj: object;
try {
const content = (await fs.readFile(file)).toString();
@@ -54,7 +65,7 @@ export default function jsonSchema({
if ('$schema' in obj) return;
const schemaPath = path.join(
this.outDir,
getSchemaPath(parent?.type === 'docs' ? `${name}.meta` : name),
getSchemaPath(parent ? `${parent.name}.meta` : match.name),
);
const updated = {
$schema: path.relative(path.dirname(file), schemaPath),
@@ -79,17 +90,17 @@ export default function jsonSchema({
});
}
for (const [name, collection] of config.collections) {
for (const collection of config.collectionList) {
if (collection.type === 'docs') {
if (collection.meta.schema instanceof z.ZodType) {
onSchema(`${name}.meta`, collection.meta.schema);
onSchema(`${collection.name}.meta`, collection.meta.schema);
}
if (collection.docs.schema instanceof z.ZodType) {
onSchema(`${name}.docs`, collection.docs.schema);
onSchema(`${collection.name}.docs`, collection.docs.schema);
}
} else if (collection.schema instanceof z.ZodType) {
onSchema(name, collection.schema);
onSchema(collection.name, collection.schema);
}
}
+51 -18
View File
@@ -1,7 +1,11 @@
import * as path from 'node:path';
import { createHash } from 'node:crypto';
import type { LoadedConfig } from '@/loaders/config';
import type { DocCollection, MetaCollection } from '@/config';
import type {
DocCollectionItem,
LoadedConfig,
MetaCollectionItem,
} from '@/config/build';
import type { MetaCollection } from '@/config';
import { validate } from '@/utils/validation';
import { readFileWithCache } from '@/next/file-cache';
import { load } from 'js-yaml';
@@ -12,7 +16,6 @@ import {
type ImportPathConfig,
toImportPath,
} from '@/utils/import-formatter';
import { getCollectionFiles } from '@/utils/collections';
import type { FileInfo } from '@/runtime/shared';
import type { Plugin } from '@/core';
@@ -26,14 +29,12 @@ export default function next(): Plugin {
config = v;
// always emit again when async mode enabled
shouldEmitOnChange = Array.from(config.collections.values()).some(
(collection) => {
return (
(collection.type === 'doc' && collection.async) ||
(collection.type === 'docs' && collection.docs.async)
);
},
);
shouldEmitOnChange = config.collectionList.some((collection) => {
return (
(collection.type === 'doc' && collection.async) ||
(collection.type === 'docs' && collection.docs.async)
);
});
},
configureServer(server) {
if (!server.watcher) return;
@@ -79,8 +80,6 @@ export async function indexFile(
}),
];
const entries = Array.from(config.collections.entries());
async function getDocEntries(collectionName: string, files: FileInfo[]) {
const items = files.map(async (file, i) => {
const importId = `${collectionName}_${i}`;
@@ -130,7 +129,10 @@ export async function indexFile(
return Promise.all(items);
}
async function getAsyncEntries(collection: DocCollection, files: FileInfo[]) {
async function getAsyncEntries(
collection: DocCollectionItem,
files: FileInfo[],
) {
if (!asyncInit) {
lines.unshift(
getImportCode({
@@ -183,10 +185,11 @@ export async function indexFile(
return Promise.all(entries);
}
const declares = entries.map(async ([k, collection]) => {
const declares = config.collectionList.map(async (collection) => {
const k = collection.name;
if (collection.type === 'docs') {
const docs = await getCollectionFiles(collection.docs);
const metas = await getCollectionFiles(collection.meta);
const docs = await globCollectionFiles(collection.docs);
const metas = await globCollectionFiles(collection.meta);
const metaEntries = (await getMetaEntries(collection.meta, metas)).join(
', ',
);
@@ -204,7 +207,7 @@ export async function indexFile(
return `export const ${k} = _runtime.docs<typeof _source.${k}>([${docsEntries}], [${metaEntries}])`;
}
const files = await getCollectionFiles(collection);
const files = await globCollectionFiles(collection);
if (collection.type === 'doc' && collection.async) {
return `export const ${k} = _runtimeAsync.doc<typeof _source.${k}>([${(await getAsyncEntries(collection, files)).join(', ')}], "${k}", _sourceConfig)`;
@@ -235,3 +238,33 @@ function parseMetaEntry(file: string, content: string) {
throw new Error(`Unknown meta file format: ${extname}, in ${file}.`);
}
async function globCollectionFiles(
collection: DocCollectionItem | MetaCollectionItem,
) {
const { glob } = await import('tinyglobby');
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 glob(collection.patterns, {
cwd: path.resolve(dir),
});
for (const item of result) {
if (collection.isFileSupported(item)) continue;
const fullPath = path.join(dir, item);
files.set(fullPath, {
path: item,
fullPath,
});
}
}),
);
return Array.from(files.values());
}
+40 -58
View File
@@ -1,12 +1,5 @@
import type {
AnyCollection,
DocCollection,
DocsCollection,
MetaCollection,
} from '@/config';
import { ident, toImportPath } from '@/utils/import-formatter';
import type { LoadedConfig } from '@/loaders/config';
import { getGlobPatterns } from '@/utils/collections';
import type { CollectionItem, LoadedConfig } from '@/config/build';
import {
generateGlobImport,
type GlobImportOptions,
@@ -98,52 +91,51 @@ function indexFile(
`export const create = fromConfig<typeof Config>();`,
];
function docs(name: string, collection: DocsCollection) {
const obj = [
ident(`doc: ${doc(name, collection.docs)}`),
ident(`meta: ${meta(name, collection.meta)}`),
].join(',\n');
function generateCollectionGlob(collection: CollectionItem): string {
if (collection.type === 'docs') {
const obj = [
ident(`doc: ${generateCollectionGlob(collection.docs)}`),
ident(`meta: ${generateCollectionGlob(collection.meta)}`),
].join(',\n');
return `{\n${obj}\n}`;
}
return `{\n${obj}\n}`;
}
function doc(name: string, collection: DocCollection) {
const patterns = getGlobPatterns(collection);
const dir = getCollectionDir(collection);
const docGlob = generateGlob(patterns, {
query: {
collection: name,
},
base: dir,
});
if (collection.async) {
const headBlob = generateGlob(patterns, {
if (collection.type === 'doc') {
const docGlob = generateGlob(collection.patterns, {
query: {
only: 'frontmatter',
collection: name,
collection: collection.name,
},
import: 'frontmatter',
base: dir,
});
return `create.docLazy("${name}", "${dir}", ${headBlob}, ${docGlob})`;
if (collection.async) {
const headBlob = generateGlob(collection.patterns, {
query: {
only: 'frontmatter',
collection: collection.name,
},
import: 'frontmatter',
base: dir,
});
return `create.docLazy("${collection.name}", "${dir}", ${headBlob}, ${docGlob})`;
}
return `create.doc("${collection.name}", "${dir}", ${docGlob})`;
}
return `create.doc("${name}", "${dir}", ${docGlob})`;
}
function meta(name: string, collection: MetaCollection) {
const patterns = getGlobPatterns(collection);
const dir = getCollectionDir(collection);
return `create.meta("${name}", "${dir}", ${generateGlob(patterns, {
import: 'default',
base: dir,
query: {
collection: name,
return `create.meta("${collection.name}", "${dir}", ${generateGlob(
collection.patterns,
{
import: 'default',
base: dir,
query: {
collection: collection.name,
},
},
})})`;
)})`;
}
function generateGlob(patterns: string[], options: GlobImportOptions) {
@@ -163,19 +155,11 @@ function indexFile(
}
}
for (const [name, collection] of config.collections.entries()) {
let body: string;
if (collection.type === 'docs') {
body = docs(name, collection);
} else if (collection.type === 'meta') {
body = meta(name, collection);
} else {
body = doc(name, collection);
}
for (const collection of config.collectionList) {
lines.push('');
lines.push(`export const ${name} = ${body};`);
lines.push(
`export const ${collection.name} = ${generateCollectionGlob(collection)};`,
);
}
return lines.join('\n');
@@ -192,9 +176,7 @@ function normalizeGlobPath(file: string) {
return `./${file}`;
}
function getCollectionDir(collection: AnyCollection): string {
const dir = collection.dir;
function getCollectionDir({ dir }: CollectionItem): string {
if (Array.isArray(dir)) {
if (dir.length !== 1)
throw new Error(
+4 -14
View File
@@ -1,34 +1,24 @@
import type { LoadedConfig } from '@/loaders/config';
import type { LoadedConfig } from '@/config/build';
import { _runtime, createMDXSource } from './index';
import type { AsyncDocOut, RuntimeAsync } from './types';
import { buildMDX, CompiledMDXProperties } from '@/loaders/mdx/build-mdx';
import type { ProcessorOptions } from '@mdx-js/mdx';
import { executeMdx } from '@fumadocs/mdx-remote/client';
import { pathToFileURL } from 'node:url';
import { createDocMethods, type DocData } from '@/runtime/shared';
import type { DocCollection } from '@/config';
import { fumaMatter } from '@/utils/fuma-matter';
import fs from 'node:fs/promises';
function getDocCollection(config: LoadedConfig, collection: string) {
const col = config.collections.get(collection);
const col = config.getCollection(collection);
if (col?.type === 'doc' && col.mdxOptions) return col;
if (col?.type === 'docs' && col.docs.mdxOptions) return col.docs;
}
async function getOptions(
config: LoadedConfig,
collection?: DocCollection,
): Promise<ProcessorOptions> {
return (
collection?.mdxOptions ?? (await config.getDefaultMDXOptions('remote'))
);
}
export const _runtimeAsync: RuntimeAsync = {
doc(files, collectionName, config) {
const collection = getDocCollection(config, collectionName);
const initMdxOptions = getOptions(config, collection);
const initMdxOptions =
collection?.mdxOptions ?? config.getDefaultMDXOptions('remote');
return files.map(({ info, data, lastModified }) => {
let cachedResult: CompiledMDXProperties | undefined;
+1 -1
View File
@@ -1,6 +1,6 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import type { MetaData, PageData, Source } from 'fumadocs-core/source';
import type { LoadedConfig } from '@/loaders/config';
import type { LoadedConfig } from '@/config/build';
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
import type {
AsyncDocCollectionEntry,
-112
View File
@@ -1,112 +0,0 @@
import type { DocCollection, MetaCollection } from '@/config/define';
import picomatch from 'picomatch';
import type { LoadedConfig } from '@/loaders/config';
import type { FileInfo } from '@/runtime/shared';
import { glob } from 'tinyglobby';
import path from 'node:path';
import type { Core } from '@/core';
const SupportedFormats = {
doc: ['mdx', 'md'],
meta: ['json', 'yaml'],
};
export function getGlobPatterns(
collection: MetaCollection | DocCollection,
): string[] {
if (collection.files) return collection.files;
return [`**/*.{${SupportedFormats[collection.type].join(',')}}`];
}
function isFileSupported(
filePath: string,
collection: MetaCollection | DocCollection,
) {
return SupportedFormats[collection.type].some((format) =>
filePath.endsWith(`.${format}`),
);
}
interface ScanIndex {
name: string;
collection: DocCollection | MetaCollection;
matcher: picomatch.Matcher;
}
export function createCollectionMatcher(core: Core) {
const CacheKey = 'collection-matcher';
return {
scan(config: LoadedConfig) {
const scanned: ScanIndex[] = [];
function scan(name: string, collection: DocCollection | MetaCollection) {
const patterns = getGlobPatterns(collection);
for (const dir of Array.isArray(collection.dir)
? collection.dir
: [collection.dir]) {
scanned.push({
name,
collection,
matcher: picomatch(patterns, {
cwd: dir,
}),
});
}
}
for (const [name, collection] of config.collections) {
if (collection.type === 'docs') {
scan(name, collection.meta);
scan(name, collection.docs);
} else {
scan(name, collection);
}
}
return scanned;
},
getFileCollection(file: string) {
const scanned =
(core.cache.get(CacheKey) as ScanIndex[]) ??
this.scan(core.getConfig());
core.cache.set(CacheKey, scanned);
for (const item of scanned) {
if (isFileSupported(file, item.collection) && item.matcher(file))
return { name: item.name, collection: item.collection };
}
},
};
}
export async function getCollectionFiles(
collection: DocCollection | MetaCollection,
) {
const files = new Map<string, FileInfo>();
const dirs = Array.isArray(collection.dir)
? collection.dir
: [collection.dir];
const patterns = getGlobPatterns(collection);
await Promise.all(
dirs.map(async (dir) => {
const result = await glob(patterns, {
cwd: path.resolve(dir),
});
for (const item of result) {
if (!isFileSupported(item, collection)) continue;
const fullPath = path.join(dir, item);
files.set(fullPath, {
path: item,
fullPath,
});
}
}),
);
return Array.from(files.values());
}
+5 -5
View File
@@ -10,11 +10,11 @@ import { validate, ValidationError } from '@/utils/validation';
import * as path from 'node:path';
import { load } from 'js-yaml';
import { createMdxLoader } from '@/loaders/mdx';
import { findConfigFile } from '@/loaders/config';
import { toVite } from '@/loaders/adapter';
import vite, { type IndexFileOptions } from '@/plugins/vite';
import type { FSWatcher } from 'chokidar';
import { createCore } from '@/core';
import { createCore, findConfigFile } from '@/core';
import { createIntegratedConfigLoader } from '@/loaders/config';
const FumadocsDeps = ['fumadocs-core', 'fumadocs-ui', 'fumadocs-openapi'];
@@ -54,7 +54,7 @@ export default async function mdx(
const core = await createViteCore(options).init({
config: buildConfig(config),
});
const mdxLoader = toVite(createMdxLoader(core.creatConfigLoader()));
const mdxLoader = toVite(createMdxLoader(createIntegratedConfigLoader(core)));
async function transformMeta(
path: string,
@@ -67,7 +67,7 @@ export default async function mdx(
};
const collection = parsed.collection
? core.getConfig().collections.get(parsed.collection)
? core.getConfig().getCollection(parsed.collection)
: undefined;
if (!collection) return null;
let schema;
@@ -153,7 +153,7 @@ export async function postInstall(
configPath = findConfigFile(),
pluginOptions: PluginOptions = {},
) {
const { loadConfig } = await import('@/loaders/config/load');
const { loadConfig } = await import('@/config/load-from-file');
const options = applyDefaults(pluginOptions);
const core = await createViteCore(options).init({
config: loadConfig(configPath, options.outDir, true),
+6 -10
View File
@@ -1,8 +1,8 @@
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';
import { createStandaloneConfigLoader } from '@/loaders/config';
export interface Options {
configPath: string;
@@ -35,15 +35,11 @@ export default async function loader(
instance = toWebpack(
createMdxLoader(
isDev
? dynamicConfig({
core,
buildConfig: false,
})
: staticConfig({
core,
buildConfig: false,
}),
createStandaloneConfigLoader({
core,
buildConfig: false,
mode: isDev ? 'dev' : 'production',
}),
),
);
}