mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Improve mutability of LoadedConfig for plugins
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': patch
|
||||
---
|
||||
|
||||
[Internal] improve mutability of `LoadedConfig` for plugins
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user