mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: make index-file a plugin and optimize re-generation
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': minor
|
||||
---
|
||||
|
||||
Make `index-file` a plugin and optimize re-generations.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': major
|
||||
---
|
||||
|
||||
**[Vite] rename `generateIndexFile` option to `index`**
|
||||
@@ -41,6 +41,7 @@ export interface DocsCollectionItem extends DocsCollection {
|
||||
name: string;
|
||||
meta: MetaCollectionItem;
|
||||
docs: DocCollectionItem;
|
||||
hasFile: (filePath: string) => boolean;
|
||||
}
|
||||
|
||||
const SupportedFormats = {
|
||||
@@ -58,7 +59,10 @@ export function buildCollection(
|
||||
name,
|
||||
meta: buildPrimitiveCollection(name, config.meta),
|
||||
docs: buildPrimitiveCollection(name, config.docs),
|
||||
};
|
||||
hasFile(filePath) {
|
||||
return this.docs.hasFile(filePath) || this.meta.hasFile(filePath);
|
||||
},
|
||||
} as DocsCollectionItem;
|
||||
}
|
||||
|
||||
return buildPrimitiveCollection(name, config) as CollectionItem;
|
||||
|
||||
+18
-14
@@ -111,10 +111,10 @@ export function createCore(
|
||||
|
||||
return this;
|
||||
},
|
||||
getConfig() {
|
||||
getConfig(): LoadedConfig {
|
||||
return config;
|
||||
},
|
||||
async initServer(server: ServerContext) {
|
||||
async initServer(server: ServerContext): Promise<void> {
|
||||
server.watcher?.on('all', async (event, file) => {
|
||||
if (event === 'change') removeFileCache(file);
|
||||
});
|
||||
@@ -123,21 +123,25 @@ export function createCore(
|
||||
await plugin.configureServer?.call(this.getPluginContext(), server);
|
||||
}
|
||||
},
|
||||
async emitAndWrite({
|
||||
filterPlugin = () => true,
|
||||
}: EmitOptions = {}): Promise<void> {
|
||||
async emit({ filterPlugin = () => true }: EmitOptions = {}): Promise<
|
||||
EmitEntry[]
|
||||
> {
|
||||
return (
|
||||
await Promise.all(
|
||||
plugins.map((plugin) => {
|
||||
if (!filterPlugin(plugin) || !plugin.emit) return [];
|
||||
|
||||
return plugin.emit.call(this.getPluginContext());
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
},
|
||||
async emitAndWrite(emitOptions?: EmitOptions): Promise<void> {
|
||||
const start = performance.now();
|
||||
|
||||
const out = await Promise.all(
|
||||
plugins.map((plugin) => {
|
||||
if (!filterPlugin(plugin) || !plugin.emit) return [];
|
||||
|
||||
return plugin.emit.call(this.getPluginContext());
|
||||
}),
|
||||
);
|
||||
const out = await this.emit(emitOptions);
|
||||
|
||||
await Promise.all(
|
||||
out.flat().map(async (entry) => {
|
||||
out.map(async (entry) => {
|
||||
const file = path.join(options.outDir, entry.path);
|
||||
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ValidationError } from '@/utils/validation';
|
||||
import next from '@/plugins/next';
|
||||
import { type Core, createCore, findConfigFile } from '@/core';
|
||||
import { mdxLoaderGlob, metaLoaderGlob } from '@/loaders';
|
||||
import type { IndexFilePluginOptions } from '@/plugins/index-file';
|
||||
|
||||
export interface CreateMDXOptions {
|
||||
/**
|
||||
@@ -24,6 +25,8 @@ export interface CreateMDXOptions {
|
||||
* @defaultValue '.source'
|
||||
*/
|
||||
outDir?: string;
|
||||
|
||||
index?: IndexFilePluginOptions;
|
||||
}
|
||||
|
||||
const defaultPageExtensions = ['mdx', 'md', 'jsx', 'js', 'tsx', 'ts'];
|
||||
@@ -187,6 +190,7 @@ export async function postInstall(
|
||||
outDir = '.source',
|
||||
) {
|
||||
const core = await createNextCore({
|
||||
index: {},
|
||||
outDir,
|
||||
configPath,
|
||||
}).init({
|
||||
@@ -198,22 +202,20 @@ export async function postInstall(
|
||||
|
||||
function applyDefaults(options: CreateMDXOptions): Required<CreateMDXOptions> {
|
||||
return {
|
||||
index: {},
|
||||
outDir: options.outDir ?? '.source',
|
||||
configPath: options.configPath ?? findConfigFile(),
|
||||
};
|
||||
}
|
||||
|
||||
function createNextCore({
|
||||
outDir,
|
||||
configPath,
|
||||
}: Required<CreateMDXOptions>): Core {
|
||||
function createNextCore(options: Required<CreateMDXOptions>): Core {
|
||||
const core = createCore(
|
||||
{
|
||||
environment: 'next',
|
||||
outDir,
|
||||
configPath,
|
||||
outDir: options.outDir,
|
||||
configPath: options.configPath,
|
||||
},
|
||||
[next()],
|
||||
[next(options)],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
import type { Plugin } from '@/core';
|
||||
import type {
|
||||
LoadedConfig,
|
||||
CollectionItem,
|
||||
DocCollectionItem,
|
||||
MetaCollectionItem,
|
||||
} from '@/config/build';
|
||||
import path from 'path';
|
||||
import { type CodeGen, createCodegen, ident } from '@/utils/codegen';
|
||||
import { glob } from 'tinyglobby';
|
||||
import { readFileWithCache } from '@/utils/codegen/cache';
|
||||
import { createHash } from 'crypto';
|
||||
import type { LazyEntry } from '@/runtime/dynamic';
|
||||
import type { EmitEntry } from '@/core';
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
import { getGitTimestamp } from '@/utils/git-timestamp';
|
||||
import { validate } from '@/utils/validation';
|
||||
|
||||
export interface IndexFilePluginOptions {
|
||||
target?: 'default' | 'vite';
|
||||
|
||||
/**
|
||||
* add `.js` extensions to imports, needed for ESM without bundler resolution
|
||||
*/
|
||||
addJsExtension?: boolean;
|
||||
browser?: boolean;
|
||||
}
|
||||
|
||||
export default function indexFile(
|
||||
options: IndexFilePluginOptions = {},
|
||||
): Plugin {
|
||||
const { target = 'default', addJsExtension, browser = true } = options;
|
||||
let config: LoadedConfig;
|
||||
let dynamicCollections: CollectionItem[];
|
||||
|
||||
function isDynamic(collection: CollectionItem) {
|
||||
return (
|
||||
(collection.type === 'docs' && collection.docs.dynamic) ||
|
||||
(collection.type === 'doc' && collection.dynamic)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'index-file',
|
||||
config(v) {
|
||||
config = v;
|
||||
dynamicCollections = config.collectionList.filter(isDynamic);
|
||||
},
|
||||
configureServer(server) {
|
||||
if (!server.watcher) return;
|
||||
|
||||
server.watcher.on('all', async (event, file) => {
|
||||
// dynamic collections always require re-generation on change
|
||||
if (dynamicCollections.length === 0) {
|
||||
// vite uses `import.meta.glob`, no need to re-generate
|
||||
if (target === 'vite') return;
|
||||
// only re-generate when adding/deleting entries
|
||||
if (target === 'default' && event === 'change') return;
|
||||
}
|
||||
|
||||
const updatedCollection = config.collectionList.find((collection) =>
|
||||
collection.hasFile(file),
|
||||
);
|
||||
|
||||
if (!updatedCollection) return;
|
||||
if (!isDynamic(updatedCollection)) {
|
||||
if (target === 'vite') return;
|
||||
if (target === 'default' && event === 'change') return;
|
||||
}
|
||||
|
||||
await this.core.emitAndWrite({
|
||||
filterPlugin: (plugin) => plugin.name === 'index-file',
|
||||
});
|
||||
});
|
||||
},
|
||||
async emit() {
|
||||
const globCache = new Map<string, Promise<string[]>>();
|
||||
|
||||
const makeCodeGen = () =>
|
||||
createCodegen({
|
||||
target,
|
||||
outDir: this.outDir,
|
||||
jsExtension: addJsExtension,
|
||||
globCache,
|
||||
});
|
||||
|
||||
async function toEmitEntry(
|
||||
path: string,
|
||||
content: Promise<string>,
|
||||
): Promise<EmitEntry> {
|
||||
return {
|
||||
path,
|
||||
content: await content,
|
||||
};
|
||||
}
|
||||
|
||||
const out: Promise<EmitEntry>[] = [
|
||||
toEmitEntry(
|
||||
'index.ts',
|
||||
generateServerIndexFile(makeCodeGen(), config, this.configPath),
|
||||
),
|
||||
toEmitEntry(
|
||||
'dynamic.ts',
|
||||
generateDynamicIndexFile(makeCodeGen(), config, this.configPath),
|
||||
),
|
||||
];
|
||||
|
||||
if (browser)
|
||||
out.push(
|
||||
toEmitEntry(
|
||||
'browser.ts',
|
||||
generateBrowserIndexFile(makeCodeGen(), config, this.configPath),
|
||||
),
|
||||
);
|
||||
|
||||
return await Promise.all(out);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateServerIndexFile(
|
||||
codegen: CodeGen,
|
||||
config: LoadedConfig,
|
||||
configPath: string,
|
||||
): Promise<string> {
|
||||
codegen.lines.push(
|
||||
`import { fromConfig } from 'fumadocs-mdx/runtime/server';`,
|
||||
`import type * as Config from '${codegen.formatImportPath(configPath)}';`,
|
||||
'',
|
||||
`const create = fromConfig<typeof Config>();`,
|
||||
);
|
||||
|
||||
async function generateCollectionObject(
|
||||
collection: CollectionItem,
|
||||
): Promise<string | undefined> {
|
||||
switch (collection.type) {
|
||||
case 'docs': {
|
||||
if (collection.docs.dynamic) return;
|
||||
|
||||
if (collection.docs.async) {
|
||||
const [metaGlob, headGlob, bodyGlob] = await Promise.all([
|
||||
generateMetaCollectionGlob(codegen, collection.meta, true),
|
||||
generateDocCollectionFrontmatterGlob(
|
||||
codegen,
|
||||
collection.docs,
|
||||
true,
|
||||
),
|
||||
generateDocCollectionGlob(codegen, collection.docs),
|
||||
]);
|
||||
|
||||
return `await create.docsLazy("${collection.name}", "${collection.dir}", ${metaGlob}, ${headGlob}, ${bodyGlob})`;
|
||||
}
|
||||
|
||||
const [metaGlob, docGlob] = await Promise.all([
|
||||
generateMetaCollectionGlob(codegen, collection.meta, true),
|
||||
generateDocCollectionGlob(codegen, collection.docs, true),
|
||||
]);
|
||||
|
||||
return `await create.docs("${collection.name}", "${collection.dir}", ${metaGlob}, ${docGlob})`;
|
||||
}
|
||||
case 'doc':
|
||||
if (collection.dynamic) return;
|
||||
|
||||
if (collection.async) {
|
||||
const [headGlob, bodyGlob] = await Promise.all([
|
||||
generateDocCollectionFrontmatterGlob(codegen, collection, true),
|
||||
generateDocCollectionGlob(codegen, collection),
|
||||
]);
|
||||
|
||||
return `await create.docLazy("${collection.name}", "${collection.dir}", ${headGlob}, ${bodyGlob})`;
|
||||
}
|
||||
|
||||
return `await create.doc("${collection.name}", "${collection.dir}", ${await generateDocCollectionGlob(
|
||||
codegen,
|
||||
collection,
|
||||
true,
|
||||
)})`;
|
||||
case 'meta':
|
||||
return `await create.meta("${collection.name}", "${collection.dir}", ${await generateMetaCollectionGlob(
|
||||
codegen,
|
||||
collection,
|
||||
true,
|
||||
)})`;
|
||||
}
|
||||
}
|
||||
|
||||
await codegen.pushAsync(
|
||||
config.collectionList.map(async (collection) => {
|
||||
const obj = await generateCollectionObject(collection);
|
||||
if (!obj) return;
|
||||
|
||||
return `\nexport const ${collection.name} = ${obj};`;
|
||||
}),
|
||||
);
|
||||
|
||||
return codegen.toString();
|
||||
}
|
||||
|
||||
export async function generateDynamicIndexFile(
|
||||
codegen: CodeGen,
|
||||
config: LoadedConfig,
|
||||
configPath: string,
|
||||
) {
|
||||
codegen.lines.push(
|
||||
`import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';`,
|
||||
`import * as Config from '${codegen.formatImportPath(configPath)}';`,
|
||||
'',
|
||||
`const create = fromConfigDynamic(Config);`,
|
||||
);
|
||||
|
||||
async function generateCollectionObjectEntry(
|
||||
collection: DocCollectionItem,
|
||||
file: string,
|
||||
) {
|
||||
const fullPath = path.join(collection.dir, file);
|
||||
const content = await readFileWithCache(fullPath).catch(() => '');
|
||||
const parsed = fumaMatter(content);
|
||||
let data = parsed.data;
|
||||
|
||||
if (collection.schema) {
|
||||
data = await validate(
|
||||
collection.schema,
|
||||
parsed.data,
|
||||
{ path: fullPath, source: parsed.content },
|
||||
`invalid frontmatter in ${fullPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
let lastModified: Date | undefined;
|
||||
if (config.global?.lastModifiedTime === 'git') {
|
||||
lastModified = await getGitTimestamp(fullPath);
|
||||
}
|
||||
|
||||
const hash = createHash('md5').update(content).digest('hex');
|
||||
const infoStr: string[] = [
|
||||
// make sure it's included in vercel/nft
|
||||
`absolutePath: path.resolve(${JSON.stringify(fullPath)})`,
|
||||
];
|
||||
for (const [k, v] of Object.entries({
|
||||
info: {
|
||||
fullPath,
|
||||
path: file,
|
||||
},
|
||||
data,
|
||||
lastModified,
|
||||
hash,
|
||||
} satisfies LazyEntry<unknown>)) {
|
||||
infoStr.push(`${k}: ${JSON.stringify(v)}`);
|
||||
}
|
||||
|
||||
return `{ ${infoStr.join(', ')} }`;
|
||||
}
|
||||
|
||||
async function generateCollectionObject(
|
||||
parent: CollectionItem,
|
||||
): Promise<string | undefined> {
|
||||
let collection: DocCollectionItem | undefined;
|
||||
if (parent.type === 'doc') collection = parent;
|
||||
else if (parent.type === 'docs') collection = parent.docs;
|
||||
|
||||
if (!collection || !collection.dynamic) return;
|
||||
|
||||
const files = await glob(collection.patterns, {
|
||||
cwd: collection.dir,
|
||||
});
|
||||
const entries = await Promise.all(
|
||||
files.map((file) => generateCollectionObjectEntry(collection, file)),
|
||||
);
|
||||
|
||||
switch (parent.type) {
|
||||
case 'docs': {
|
||||
const metaGlob = await generateMetaCollectionGlob(
|
||||
codegen,
|
||||
parent.meta,
|
||||
true,
|
||||
);
|
||||
|
||||
return `await create.docs("${parent.name}", "${parent.dir}", ${metaGlob}, ${entries.join(', ')})`;
|
||||
}
|
||||
case 'doc':
|
||||
return `await create.doc("${collection.name}", "${collection.dir}", ${entries.join(', ')})`;
|
||||
}
|
||||
}
|
||||
|
||||
await codegen.pushAsync(
|
||||
config.collectionList.map(async (collection) => {
|
||||
const obj = await generateCollectionObject(collection);
|
||||
if (!obj) return;
|
||||
|
||||
return `\nexport const ${collection.name} = ${obj};`;
|
||||
}),
|
||||
);
|
||||
|
||||
return codegen.toString();
|
||||
}
|
||||
|
||||
export async function generateBrowserIndexFile(
|
||||
codegen: CodeGen,
|
||||
config: LoadedConfig,
|
||||
configPath: string,
|
||||
): Promise<string> {
|
||||
codegen.lines.push(
|
||||
`import { fromConfig } from 'fumadocs-mdx/runtime/browser';`,
|
||||
`import type * as Config from '${codegen.formatImportPath(configPath)}';`,
|
||||
'',
|
||||
`const create = fromConfig<typeof Config>(Config);`,
|
||||
);
|
||||
|
||||
async function generateCollectionObject(
|
||||
collection: CollectionItem,
|
||||
): Promise<string | undefined> {
|
||||
switch (collection.type) {
|
||||
case 'docs': {
|
||||
if (collection.docs.dynamic) return;
|
||||
|
||||
const [docGlob, metaGlob] = await Promise.all([
|
||||
generateCollectionObject(collection.docs),
|
||||
generateCollectionObject(collection.meta),
|
||||
]);
|
||||
|
||||
return `{\n${ident(`doc: ${docGlob},\nmeta: ${metaGlob}`)}\n}`;
|
||||
}
|
||||
case 'doc':
|
||||
if (collection.dynamic) return;
|
||||
|
||||
return `create.doc("${collection.name}", ${await generateDocCollectionGlob(codegen, collection)})`;
|
||||
case 'meta':
|
||||
return `create.meta("${collection.name}", ${await generateMetaCollectionGlob(
|
||||
codegen,
|
||||
collection,
|
||||
)})`;
|
||||
}
|
||||
}
|
||||
|
||||
await codegen.pushAsync(
|
||||
config.collectionList.map(async (collection) => {
|
||||
const obj = await generateCollectionObject(collection);
|
||||
if (!obj) return;
|
||||
|
||||
return `\nexport const ${collection.name} = ${obj};`;
|
||||
}),
|
||||
);
|
||||
|
||||
return codegen.toString();
|
||||
}
|
||||
|
||||
function generateDocCollectionFrontmatterGlob(
|
||||
codegen: CodeGen,
|
||||
collection: DocCollectionItem,
|
||||
eager = false,
|
||||
) {
|
||||
return codegen.generateGlobImport(collection.patterns, {
|
||||
query: {
|
||||
collection: collection.name,
|
||||
only: 'frontmatter',
|
||||
},
|
||||
import: 'frontmatter',
|
||||
base: collection.dir,
|
||||
eager,
|
||||
});
|
||||
}
|
||||
|
||||
function generateDocCollectionGlob(
|
||||
codegen: CodeGen,
|
||||
collection: DocCollectionItem,
|
||||
eager = false,
|
||||
) {
|
||||
return codegen.generateGlobImport(collection.patterns, {
|
||||
query: {
|
||||
collection: collection.name,
|
||||
},
|
||||
base: collection.dir,
|
||||
eager,
|
||||
});
|
||||
}
|
||||
|
||||
function generateMetaCollectionGlob(
|
||||
codegen: CodeGen,
|
||||
collection: MetaCollectionItem,
|
||||
eager = false,
|
||||
) {
|
||||
return codegen.generateGlobImport(collection.patterns, {
|
||||
query: {
|
||||
collection: collection.name,
|
||||
},
|
||||
import: 'default',
|
||||
base: collection.dir,
|
||||
eager,
|
||||
});
|
||||
}
|
||||
@@ -1,43 +1,7 @@
|
||||
import type { LoadedConfig } from '@/config/build';
|
||||
import type { Plugin } from '@/core';
|
||||
import { emitIndexFiles } from '@/utils/codegen';
|
||||
import type { PluginOption } from '@/core';
|
||||
import type { CreateMDXOptions } from '@/next';
|
||||
import indexFile from './index-file';
|
||||
|
||||
export default function next(): Plugin {
|
||||
let config: LoadedConfig;
|
||||
let shouldEmitOnChange = false;
|
||||
|
||||
return {
|
||||
name: 'next',
|
||||
config(v) {
|
||||
config = v;
|
||||
|
||||
// always emit again when async mode enabled
|
||||
shouldEmitOnChange = config.collectionList.some((collection) => {
|
||||
return (
|
||||
(collection.type === 'doc' && collection.async) ||
|
||||
collection.type === 'docs' ||
|
||||
collection.type === 'meta'
|
||||
);
|
||||
});
|
||||
},
|
||||
configureServer(server) {
|
||||
if (!server.watcher) return;
|
||||
|
||||
server.watcher.on('all', async (event) => {
|
||||
if (event === 'change' && !shouldEmitOnChange) return;
|
||||
|
||||
await this.core.emitAndWrite({
|
||||
filterPlugin: (plugin) => plugin.name === 'next',
|
||||
});
|
||||
});
|
||||
},
|
||||
async emit() {
|
||||
// TODO: implement meta entries validation.
|
||||
return emitIndexFiles({
|
||||
config,
|
||||
configPath: this.configPath,
|
||||
outDir: this.outDir,
|
||||
});
|
||||
},
|
||||
};
|
||||
export default function next(config: CreateMDXOptions): PluginOption {
|
||||
return [indexFile(config.index)];
|
||||
}
|
||||
|
||||
@@ -1,90 +1,25 @@
|
||||
import type { LoadedConfig } from '@/config/build';
|
||||
import type { EmitEntry, Plugin } from '@/core';
|
||||
import { emitIndexFiles } from '@/utils/codegen';
|
||||
import type { PluginOption } from '@/core';
|
||||
import indexFile, { type IndexFilePluginOptions } from './index-file';
|
||||
import type { PluginOptions } from '@/vite';
|
||||
|
||||
export interface IndexFileOptions {
|
||||
/**
|
||||
* Runtime compat fallbacks for Vite specific APIs
|
||||
*
|
||||
* - `bun`: use Bun-specific APIs.
|
||||
* - `node`: use Node.js APIs.
|
||||
* - `false` (default): no fallback.
|
||||
*/
|
||||
runtime?: 'bun' | 'node' | false;
|
||||
/**
|
||||
* add `.js` extensions to imports, needed for ESM without bundler resolution
|
||||
*/
|
||||
addJsExtension?: boolean;
|
||||
export default function vite(config: Required<PluginOptions>): PluginOption {
|
||||
const out: PluginOption = [];
|
||||
|
||||
/**
|
||||
* Generate entry point for browser environment
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
browser?: boolean;
|
||||
if (config.index) {
|
||||
const options: IndexFilePluginOptions =
|
||||
config.index === true ? {} : config.index;
|
||||
|
||||
out.push(indexFile(applyDefaults(options)));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export default function vite({
|
||||
index,
|
||||
}: {
|
||||
index: IndexFileOptions | boolean;
|
||||
}): Plugin {
|
||||
let config: LoadedConfig;
|
||||
let indexOptions: Required<IndexFileOptions> | false;
|
||||
if (index === false) indexOptions = false;
|
||||
else indexOptions = applyDefaults(index === true ? {} : index);
|
||||
|
||||
function applyDefaults(
|
||||
options: IndexFilePluginOptions,
|
||||
): IndexFilePluginOptions {
|
||||
return {
|
||||
name: 'vite',
|
||||
config(v) {
|
||||
config = v;
|
||||
},
|
||||
configureServer(server) {
|
||||
if (
|
||||
!server.watcher ||
|
||||
indexOptions === false ||
|
||||
indexOptions.runtime === false
|
||||
)
|
||||
return;
|
||||
|
||||
// for bun/node runtimes, alternative import.meta.glob has to be re-generated on update
|
||||
server.watcher.on('all', (event, file) => {
|
||||
if (event === 'change') return;
|
||||
const isUpdated = config.collectionList.some((collection) => {
|
||||
if (collection.type === 'docs')
|
||||
return (
|
||||
collection.docs.hasFile(file) || collection.meta.hasFile(file)
|
||||
);
|
||||
|
||||
return collection.hasFile(file);
|
||||
});
|
||||
|
||||
if (isUpdated) {
|
||||
this.core.emitAndWrite({
|
||||
filterPlugin: (plugin) => plugin.name === 'vite',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
async emit() {
|
||||
const out: EmitEntry[] = [];
|
||||
if (indexOptions === false) return out;
|
||||
|
||||
return emitIndexFiles({
|
||||
config,
|
||||
configPath: this.configPath,
|
||||
outDir: this.outDir,
|
||||
target: indexOptions.runtime === false ? 'vite' : 'default',
|
||||
browser: indexOptions.browser,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyDefaults(options: IndexFileOptions): Required<IndexFileOptions> {
|
||||
return {
|
||||
addJsExtension: options.addJsExtension ?? false,
|
||||
browser: options.browser ?? false,
|
||||
runtime: options.runtime ?? false,
|
||||
...options,
|
||||
target: options.target ?? 'vite',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,361 +1,182 @@
|
||||
import type {
|
||||
LoadedConfig,
|
||||
CollectionItem,
|
||||
DocCollectionItem,
|
||||
MetaCollectionItem,
|
||||
} from '@/config/build';
|
||||
import path from 'path';
|
||||
import { type CodeGen, createCodegen, ident } from './utils';
|
||||
import path from 'node:path';
|
||||
import { glob } from 'tinyglobby';
|
||||
import { readFileWithCache } from './cache';
|
||||
import { fumaMatter } from '../fuma-matter';
|
||||
import { validate } from '../validation';
|
||||
import { getGitTimestamp } from '../git-timestamp';
|
||||
import { createHash } from 'crypto';
|
||||
import type { LazyEntry } from '@/runtime/dynamic';
|
||||
import type { EmitEntry } from '@/core';
|
||||
|
||||
export interface GenerateIndexFileOptions {
|
||||
target?: 'default' | 'vite';
|
||||
|
||||
/**
|
||||
* add `.js` extensions to imports, needed for ESM without bundler resolution
|
||||
*/
|
||||
addJsExtension?: boolean;
|
||||
export interface GlobImportOptions {
|
||||
base: string;
|
||||
query?: Record<string, string>;
|
||||
import?: string;
|
||||
eager?: boolean;
|
||||
}
|
||||
|
||||
export interface CodeGenOptions {
|
||||
target: 'default' | 'vite';
|
||||
outDir: string;
|
||||
config: LoadedConfig;
|
||||
configPath: string;
|
||||
/**
|
||||
* add .js extenstion to imports
|
||||
*/
|
||||
jsExtension: boolean;
|
||||
globCache: Map<string, Promise<string[]>>;
|
||||
}
|
||||
|
||||
export async function emitIndexFiles({
|
||||
browser,
|
||||
...options
|
||||
}: GenerateIndexFileOptions & { browser?: boolean }): Promise<EmitEntry[]> {
|
||||
const out: Promise<EmitEntry>[] = [
|
||||
generateDynamicIndexFile(options).then((content) => ({
|
||||
path: 'dynamic.ts',
|
||||
content,
|
||||
})),
|
||||
generateServerIndexFile(options).then((content) => ({
|
||||
path: 'index.ts',
|
||||
content,
|
||||
})),
|
||||
];
|
||||
export type CodeGen = ReturnType<typeof createCodegen>;
|
||||
|
||||
if (browser !== false)
|
||||
out.push(
|
||||
generateBrowserIndexFile(options).then((content) => ({
|
||||
path: 'browser.ts',
|
||||
content,
|
||||
})),
|
||||
);
|
||||
/**
|
||||
* Code generator (one instance per file)
|
||||
*/
|
||||
export function createCodegen({
|
||||
target = 'default',
|
||||
outDir = '',
|
||||
jsExtension = false,
|
||||
globCache = new Map(),
|
||||
}: Partial<CodeGenOptions>) {
|
||||
let eagerImportId = 0;
|
||||
const banner: string[] = ['// @ts-nocheck'];
|
||||
|
||||
return await Promise.all(out);
|
||||
}
|
||||
|
||||
export async function generateServerIndexFile(
|
||||
options: GenerateIndexFileOptions,
|
||||
): Promise<string> {
|
||||
const {
|
||||
target = 'default',
|
||||
outDir,
|
||||
addJsExtension = false,
|
||||
config,
|
||||
configPath,
|
||||
} = options;
|
||||
|
||||
const codegen = createCodegen({
|
||||
target,
|
||||
outDir,
|
||||
jsExtension: addJsExtension,
|
||||
});
|
||||
|
||||
codegen.lines.push(
|
||||
`import { fromConfig } from 'fumadocs-mdx/runtime/server';`,
|
||||
`import type * as Config from '${codegen.formatImportPath(configPath)}';`,
|
||||
'',
|
||||
`const create = fromConfig<typeof Config>();`,
|
||||
);
|
||||
|
||||
async function generateCollectionObject(
|
||||
collection: CollectionItem,
|
||||
): Promise<string | undefined> {
|
||||
switch (collection.type) {
|
||||
case 'docs': {
|
||||
if (collection.docs.dynamic) return;
|
||||
|
||||
if (collection.docs.async) {
|
||||
const [metaGlob, headGlob, bodyGlob] = await Promise.all([
|
||||
generateMetaCollectionGlob(codegen, collection.meta, true),
|
||||
generateDocCollectionFrontmatterGlob(
|
||||
codegen,
|
||||
collection.docs,
|
||||
true,
|
||||
),
|
||||
generateDocCollectionGlob(codegen, collection.docs),
|
||||
]);
|
||||
|
||||
return `await create.docsLazy("${collection.name}", "${collection.dir}", ${metaGlob}, ${headGlob}, ${bodyGlob})`;
|
||||
}
|
||||
|
||||
const [metaGlob, docGlob] = await Promise.all([
|
||||
generateMetaCollectionGlob(codegen, collection.meta, true),
|
||||
generateDocCollectionGlob(codegen, collection.docs, true),
|
||||
]);
|
||||
|
||||
return `await create.docs("${collection.name}", "${collection.dir}", ${metaGlob}, ${docGlob})`;
|
||||
}
|
||||
case 'doc':
|
||||
if (collection.dynamic) return;
|
||||
|
||||
if (collection.async) {
|
||||
const [headGlob, bodyGlob] = await Promise.all([
|
||||
generateDocCollectionFrontmatterGlob(codegen, collection, true),
|
||||
generateDocCollectionGlob(codegen, collection),
|
||||
]);
|
||||
|
||||
return `await create.docLazy("${collection.name}", "${collection.dir}", ${headGlob}, ${bodyGlob})`;
|
||||
}
|
||||
|
||||
return `await create.doc("${collection.name}", "${collection.dir}", ${await generateDocCollectionGlob(
|
||||
codegen,
|
||||
collection,
|
||||
true,
|
||||
)})`;
|
||||
case 'meta':
|
||||
return `await create.meta("${collection.name}", "${collection.dir}", ${await generateMetaCollectionGlob(
|
||||
codegen,
|
||||
collection,
|
||||
true,
|
||||
)})`;
|
||||
}
|
||||
if (target === 'vite') {
|
||||
banner.push('/// <reference types="vite/client" />');
|
||||
}
|
||||
|
||||
await codegen.pushAsync(
|
||||
config.collectionList.map(async (collection) => {
|
||||
const obj = await generateCollectionObject(collection);
|
||||
if (!obj) return;
|
||||
return {
|
||||
options: {
|
||||
target,
|
||||
outDir,
|
||||
} as CodeGenOptions,
|
||||
lines: [] as string[],
|
||||
addImport(statement: string) {
|
||||
this.lines.unshift(statement);
|
||||
},
|
||||
async pushAsync(insert: Promise<string | undefined>[]) {
|
||||
for (const line of await Promise.all(insert)) {
|
||||
if (line === undefined) continue;
|
||||
|
||||
return `\nexport const ${collection.name} = ${obj};`;
|
||||
}),
|
||||
);
|
||||
this.lines.push(line);
|
||||
}
|
||||
},
|
||||
|
||||
return codegen.toString();
|
||||
}
|
||||
async generateGlobImport(
|
||||
patterns: string | string[],
|
||||
options: GlobImportOptions,
|
||||
): Promise<string> {
|
||||
if (target === 'vite') {
|
||||
return this.generateViteGlobImport(patterns, options);
|
||||
}
|
||||
|
||||
export async function generateDynamicIndexFile(
|
||||
options: GenerateIndexFileOptions,
|
||||
) {
|
||||
const {
|
||||
target,
|
||||
outDir,
|
||||
addJsExtension = false,
|
||||
config,
|
||||
configPath,
|
||||
} = options;
|
||||
return this.generateNodeGlobImport(patterns, options);
|
||||
},
|
||||
|
||||
const codegen = createCodegen({
|
||||
target,
|
||||
outDir,
|
||||
jsExtension: addJsExtension,
|
||||
});
|
||||
|
||||
codegen.lines.push(
|
||||
`import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';`,
|
||||
`import * as Config from '${codegen.formatImportPath(configPath)}';`,
|
||||
'',
|
||||
`const create = fromConfigDynamic(Config);`,
|
||||
);
|
||||
|
||||
async function generateCollectionObjectEntry(
|
||||
collection: DocCollectionItem,
|
||||
file: string,
|
||||
) {
|
||||
const fullPath = path.join(collection.dir, file);
|
||||
const content = await readFileWithCache(fullPath).catch(() => '');
|
||||
const parsed = fumaMatter(content);
|
||||
let data = parsed.data;
|
||||
|
||||
if (collection.schema) {
|
||||
data = await validate(
|
||||
collection.schema,
|
||||
parsed.data,
|
||||
{ path: fullPath, source: parsed.content },
|
||||
`invalid frontmatter in ${fullPath}`,
|
||||
generateViteGlobImport(
|
||||
patterns: string | string[],
|
||||
{ base, ...rest }: GlobImportOptions,
|
||||
): string {
|
||||
patterns = (typeof patterns === 'string' ? [patterns] : patterns).map(
|
||||
normalizeViteGlobPath,
|
||||
);
|
||||
}
|
||||
|
||||
let lastModified: Date | undefined;
|
||||
if (config.global?.lastModifiedTime === 'git') {
|
||||
lastModified = await getGitTimestamp(fullPath);
|
||||
}
|
||||
|
||||
const hash = createHash('md5').update(content).digest('hex');
|
||||
const infoStr: string[] = [
|
||||
// make sure it's included in vercel/nft
|
||||
`absolutePath: path.resolve(${JSON.stringify(fullPath)})`,
|
||||
];
|
||||
for (const [k, v] of Object.entries({
|
||||
info: {
|
||||
fullPath,
|
||||
path: file,
|
||||
},
|
||||
data,
|
||||
lastModified,
|
||||
hash,
|
||||
} satisfies LazyEntry<unknown>)) {
|
||||
infoStr.push(`${k}: ${JSON.stringify(v)}`);
|
||||
}
|
||||
|
||||
return `{ ${infoStr.join(', ')} }`;
|
||||
}
|
||||
|
||||
async function generateCollectionObject(
|
||||
parent: CollectionItem,
|
||||
): Promise<string | undefined> {
|
||||
let collection: DocCollectionItem | undefined;
|
||||
if (parent.type === 'doc') collection = parent;
|
||||
else if (parent.type === 'docs') collection = parent.docs;
|
||||
|
||||
if (!collection || !collection.dynamic) return;
|
||||
|
||||
const files = await glob(collection.patterns, {
|
||||
cwd: collection.dir,
|
||||
});
|
||||
const entries = await Promise.all(
|
||||
files.map((file) => generateCollectionObjectEntry(collection, file)),
|
||||
);
|
||||
|
||||
switch (parent.type) {
|
||||
case 'docs': {
|
||||
const metaGlob = await generateMetaCollectionGlob(
|
||||
codegen,
|
||||
parent.meta,
|
||||
true,
|
||||
);
|
||||
|
||||
return `await create.docs("${parent.name}", "${parent.dir}", ${metaGlob}, ${entries.join(', ')})`;
|
||||
return `import.meta.glob(${JSON.stringify(patterns)}, ${JSON.stringify(
|
||||
{
|
||||
base: normalizeViteGlobPath(path.relative(outDir, base)),
|
||||
...rest,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)})`;
|
||||
},
|
||||
async generateNodeGlobImport(
|
||||
patterns: string | string[],
|
||||
{
|
||||
base,
|
||||
eager = false,
|
||||
query = {},
|
||||
import: importName,
|
||||
}: GlobImportOptions,
|
||||
): Promise<string> {
|
||||
const cacheKey = JSON.stringify({ patterns, base });
|
||||
let files = globCache.get(cacheKey);
|
||||
if (!files) {
|
||||
files = glob(patterns, {
|
||||
cwd: base,
|
||||
});
|
||||
globCache.set(cacheKey, files);
|
||||
}
|
||||
case 'doc':
|
||||
return `await create.doc("${collection.name}", "${collection.dir}", ${entries.join(', ')})`;
|
||||
}
|
||||
}
|
||||
|
||||
await codegen.pushAsync(
|
||||
config.collectionList.map(async (collection) => {
|
||||
const obj = await generateCollectionObject(collection);
|
||||
if (!obj) return;
|
||||
let code: string = '{';
|
||||
for (const item of await files) {
|
||||
const fullPath = path.join(base, item);
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
return `\nexport const ${collection.name} = ${obj};`;
|
||||
}),
|
||||
);
|
||||
for (const [k, v] of Object.entries(query)) {
|
||||
searchParams.set(k, v);
|
||||
}
|
||||
|
||||
return codegen.toString();
|
||||
}
|
||||
const importPath =
|
||||
this.formatImportPath(fullPath) + '?' + searchParams.toString();
|
||||
if (eager) {
|
||||
const name = `__fd_glob_${eagerImportId++}`;
|
||||
this.lines.unshift(
|
||||
importName
|
||||
? `import { ${importName} as ${name} } from ${JSON.stringify(importPath)}`
|
||||
: `import * as ${name} from ${JSON.stringify(importPath)}`,
|
||||
);
|
||||
|
||||
export async function generateBrowserIndexFile(
|
||||
options: GenerateIndexFileOptions,
|
||||
): Promise<string> {
|
||||
const {
|
||||
target,
|
||||
outDir,
|
||||
addJsExtension = false,
|
||||
config,
|
||||
configPath,
|
||||
} = options;
|
||||
const codegen = createCodegen({
|
||||
target,
|
||||
outDir,
|
||||
jsExtension: addJsExtension,
|
||||
});
|
||||
code += `${JSON.stringify(item)}: ${name}, `;
|
||||
} else {
|
||||
let line = `${JSON.stringify(item)}: () => import(${JSON.stringify(importPath)})`;
|
||||
if (importName) {
|
||||
line += `.then(mod => mod.${importName})`;
|
||||
}
|
||||
|
||||
codegen.lines.push(
|
||||
`import { fromConfig } from 'fumadocs-mdx/runtime/browser';`,
|
||||
`import type * as Config from '${codegen.formatImportPath(configPath)}';`,
|
||||
'',
|
||||
`const create = fromConfig<typeof Config>(Config);`,
|
||||
);
|
||||
|
||||
async function generateCollectionObject(
|
||||
collection: CollectionItem,
|
||||
): Promise<string | undefined> {
|
||||
switch (collection.type) {
|
||||
case 'docs': {
|
||||
if (collection.docs.dynamic) return;
|
||||
|
||||
const [docGlob, metaGlob] = await Promise.all([
|
||||
generateCollectionObject(collection.docs),
|
||||
generateCollectionObject(collection.meta),
|
||||
]);
|
||||
|
||||
return `{\n${ident(`doc: ${docGlob},\nmeta: ${metaGlob}`)}\n}`;
|
||||
code += `${line}, `;
|
||||
}
|
||||
}
|
||||
case 'doc':
|
||||
if (collection.dynamic) return;
|
||||
|
||||
return `create.doc("${collection.name}", ${await generateDocCollectionGlob(codegen, collection)})`;
|
||||
case 'meta':
|
||||
return `create.meta("${collection.name}", ${await generateMetaCollectionGlob(
|
||||
codegen,
|
||||
collection,
|
||||
)})`;
|
||||
}
|
||||
code += '}';
|
||||
return code;
|
||||
},
|
||||
formatImportPath(file: string) {
|
||||
const ext = path.extname(file);
|
||||
let filename: string;
|
||||
|
||||
if (ext === '.ts' && jsExtension) {
|
||||
filename = file.substring(0, file.length - ext.length) + '.js';
|
||||
} else if (ext === '.ts') {
|
||||
filename = file.substring(0, file.length - ext.length);
|
||||
} else {
|
||||
filename = file;
|
||||
}
|
||||
|
||||
const importPath = slash(path.relative(outDir, filename));
|
||||
return importPath.startsWith('.') ? importPath : `./${importPath}`;
|
||||
},
|
||||
toString() {
|
||||
return [...banner, ...this.lines].join('\n');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* convert into POSIX & relative file paths, such that Vite can accept it.
|
||||
*/
|
||||
function normalizeViteGlobPath(file: string) {
|
||||
file = slash(file);
|
||||
if (file.startsWith('./')) return file;
|
||||
if (file.startsWith('/')) return `.${file}`;
|
||||
|
||||
return `./${file}`;
|
||||
}
|
||||
|
||||
export function slash(path: string): string {
|
||||
const isExtendedLengthPath = path.startsWith('\\\\?\\');
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
await codegen.pushAsync(
|
||||
config.collectionList.map(async (collection) => {
|
||||
const obj = await generateCollectionObject(collection);
|
||||
if (!obj) return;
|
||||
|
||||
return `\nexport const ${collection.name} = ${obj};`;
|
||||
}),
|
||||
);
|
||||
|
||||
return codegen.toString();
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
function generateDocCollectionFrontmatterGlob(
|
||||
codegen: CodeGen,
|
||||
collection: DocCollectionItem,
|
||||
eager = false,
|
||||
) {
|
||||
return codegen.generateGlobImport(collection.patterns, {
|
||||
query: {
|
||||
collection: collection.name,
|
||||
only: 'frontmatter',
|
||||
},
|
||||
import: 'frontmatter',
|
||||
base: collection.dir,
|
||||
eager,
|
||||
});
|
||||
}
|
||||
|
||||
function generateDocCollectionGlob(
|
||||
codegen: CodeGen,
|
||||
collection: DocCollectionItem,
|
||||
eager = false,
|
||||
) {
|
||||
return codegen.generateGlobImport(collection.patterns, {
|
||||
query: {
|
||||
collection: collection.name,
|
||||
},
|
||||
base: collection.dir,
|
||||
eager,
|
||||
});
|
||||
}
|
||||
|
||||
function generateMetaCollectionGlob(
|
||||
codegen: CodeGen,
|
||||
collection: MetaCollectionItem,
|
||||
eager = false,
|
||||
) {
|
||||
return codegen.generateGlobImport(collection.patterns, {
|
||||
query: {
|
||||
collection: collection.name,
|
||||
},
|
||||
import: 'default',
|
||||
base: collection.dir,
|
||||
eager,
|
||||
});
|
||||
export function ident(code: string, tab: number = 1) {
|
||||
return code
|
||||
.split('\n')
|
||||
.map((v) => ' '.repeat(tab) + v)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import { glob } from 'tinyglobby';
|
||||
|
||||
export interface GlobImportOptions {
|
||||
base: string;
|
||||
query?: Record<string, string>;
|
||||
import?: string;
|
||||
eager?: boolean;
|
||||
}
|
||||
|
||||
export interface CodeGenOptions {
|
||||
target: 'default' | 'vite';
|
||||
outDir: string;
|
||||
/**
|
||||
* add .js extenstion to imports
|
||||
*/
|
||||
jsExtension: boolean;
|
||||
}
|
||||
|
||||
export type CodeGen = ReturnType<typeof createCodegen>;
|
||||
|
||||
export function createCodegen({
|
||||
target = 'default',
|
||||
outDir = '',
|
||||
jsExtension = false,
|
||||
}: Partial<CodeGenOptions>) {
|
||||
let eagerImportId = 0;
|
||||
const globCache = new Map<string, Promise<string[]>>();
|
||||
const banner: string[] = ['// @ts-nocheck'];
|
||||
|
||||
if (target === 'vite') {
|
||||
banner.push('/// <reference types="vite/client" />');
|
||||
}
|
||||
|
||||
return {
|
||||
options: {
|
||||
target,
|
||||
outDir,
|
||||
} as CodeGenOptions,
|
||||
lines: [] as string[],
|
||||
addImport(statement: string) {
|
||||
this.lines.unshift(statement);
|
||||
},
|
||||
async pushAsync(insert: Promise<string | undefined>[]) {
|
||||
for (const line of await Promise.all(insert)) {
|
||||
if (line === undefined) continue;
|
||||
|
||||
this.lines.push(line);
|
||||
}
|
||||
},
|
||||
|
||||
async generateGlobImport(
|
||||
patterns: string | string[],
|
||||
options: GlobImportOptions,
|
||||
): Promise<string> {
|
||||
if (target === 'vite') {
|
||||
return this.generateViteGlobImport(patterns, options);
|
||||
}
|
||||
|
||||
return this.generateNodeGlobImport(patterns, options);
|
||||
},
|
||||
|
||||
generateViteGlobImport(
|
||||
patterns: string | string[],
|
||||
{ base, ...rest }: GlobImportOptions,
|
||||
): string {
|
||||
patterns = (typeof patterns === 'string' ? [patterns] : patterns).map(
|
||||
normalizeViteGlobPath,
|
||||
);
|
||||
|
||||
return `import.meta.glob(${JSON.stringify(patterns)}, ${JSON.stringify(
|
||||
{
|
||||
base: normalizeViteGlobPath(path.relative(outDir, base)),
|
||||
...rest,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)})`;
|
||||
},
|
||||
async generateNodeGlobImport(
|
||||
patterns: string | string[],
|
||||
{
|
||||
base,
|
||||
eager = false,
|
||||
query = {},
|
||||
import: importName,
|
||||
}: GlobImportOptions,
|
||||
): Promise<string> {
|
||||
const cacheKey = JSON.stringify({ patterns, base });
|
||||
let files = globCache.get(cacheKey);
|
||||
if (!files) {
|
||||
files = glob(patterns, {
|
||||
cwd: base,
|
||||
});
|
||||
globCache.set(cacheKey, files);
|
||||
}
|
||||
|
||||
let code: string = '{';
|
||||
for (const item of await files) {
|
||||
const fullPath = path.join(base, item);
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
for (const [k, v] of Object.entries(query)) {
|
||||
searchParams.set(k, v);
|
||||
}
|
||||
|
||||
const importPath =
|
||||
this.formatImportPath(fullPath) + '?' + searchParams.toString();
|
||||
if (eager) {
|
||||
const name = `__fd_glob_${eagerImportId++}`;
|
||||
this.lines.unshift(
|
||||
importName
|
||||
? `import { ${importName} as ${name} } from ${JSON.stringify(importPath)}`
|
||||
: `import * as ${name} from ${JSON.stringify(importPath)}`,
|
||||
);
|
||||
|
||||
code += `${JSON.stringify(item)}: ${name}, `;
|
||||
} else {
|
||||
let line = `${JSON.stringify(item)}: () => import(${JSON.stringify(importPath)})`;
|
||||
if (importName) {
|
||||
line += `.then(mod => mod.${importName})`;
|
||||
}
|
||||
|
||||
code += `${line}, `;
|
||||
}
|
||||
}
|
||||
|
||||
code += '}';
|
||||
return code;
|
||||
},
|
||||
formatImportPath(file: string) {
|
||||
const ext = path.extname(file);
|
||||
let filename: string;
|
||||
|
||||
if (ext === '.ts' && jsExtension) {
|
||||
filename = file.substring(0, file.length - ext.length) + '.js';
|
||||
} else if (ext === '.ts') {
|
||||
filename = file.substring(0, file.length - ext.length);
|
||||
} else {
|
||||
filename = file;
|
||||
}
|
||||
|
||||
const importPath = slash(path.relative(outDir, filename));
|
||||
return importPath.startsWith('.') ? importPath : `./${importPath}`;
|
||||
},
|
||||
toString() {
|
||||
return [...banner, ...this.lines].join('\n');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* convert into POSIX & relative file paths, such that Vite can accept it.
|
||||
*/
|
||||
function normalizeViteGlobPath(file: string) {
|
||||
file = slash(file);
|
||||
if (file.startsWith('./')) return file;
|
||||
if (file.startsWith('/')) return `.${file}`;
|
||||
|
||||
return `./${file}`;
|
||||
}
|
||||
|
||||
export function slash(path: string): string {
|
||||
const isExtendedLengthPath = path.startsWith('\\\\?\\');
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
export function ident(code: string, tab: number = 1) {
|
||||
return code
|
||||
.split('\n')
|
||||
.map((v) => ' '.repeat(tab) + v)
|
||||
.join('\n');
|
||||
}
|
||||
@@ -3,21 +3,22 @@ import { buildConfig } from '@/config/build';
|
||||
import { ValidationError } from '@/utils/validation';
|
||||
import { createMdxLoader } from '@/loaders/mdx';
|
||||
import { toVite } from '@/loaders/adapter';
|
||||
import vite, { type IndexFileOptions } from '@/plugins/vite';
|
||||
import vite from '@/plugins/vite';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
import { createCore, findConfigFile } from '@/core';
|
||||
import { createIntegratedConfigLoader } from '@/loaders/config';
|
||||
import { createMetaLoader } from '@/loaders/meta';
|
||||
import { IndexFilePluginOptions } from '@/plugins/index-file';
|
||||
|
||||
const FumadocsDeps = ['fumadocs-core', 'fumadocs-ui', 'fumadocs-openapi'];
|
||||
|
||||
export interface PluginOptions {
|
||||
/**
|
||||
* Automatically generate index files for accessing files with `import.meta.glob`.
|
||||
* Generate index files for accessing content.
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
generateIndexFile?: boolean | IndexFileOptions;
|
||||
index?: boolean | IndexFilePluginOptions;
|
||||
|
||||
/**
|
||||
* @defaultValue source.config.ts
|
||||
@@ -114,29 +115,21 @@ export async function postInstall(
|
||||
await core.emitAndWrite();
|
||||
}
|
||||
|
||||
function createViteCore({
|
||||
configPath,
|
||||
outDir,
|
||||
generateIndexFile,
|
||||
}: Required<PluginOptions>) {
|
||||
function createViteCore(options: Required<PluginOptions>) {
|
||||
return createCore(
|
||||
{
|
||||
environment: 'vite',
|
||||
configPath,
|
||||
outDir,
|
||||
configPath: options.configPath,
|
||||
outDir: options.outDir,
|
||||
},
|
||||
[
|
||||
vite({
|
||||
index: generateIndexFile,
|
||||
}),
|
||||
],
|
||||
[vite(options)],
|
||||
);
|
||||
}
|
||||
|
||||
function applyDefaults(options: PluginOptions): Required<PluginOptions> {
|
||||
return {
|
||||
updateViteConfig: options.updateViteConfig ?? true,
|
||||
generateIndexFile: options.generateIndexFile ?? true,
|
||||
index: options.index ?? true,
|
||||
configPath: options.configPath ?? 'source.config.ts',
|
||||
outDir: options.outDir ?? '.source',
|
||||
};
|
||||
|
||||
+8
-8
@@ -1,11 +1,3 @@
|
||||
```ts title="dynamic.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';
|
||||
import * as Config from './config';
|
||||
|
||||
const create = fromConfigDynamic(Config);
|
||||
```
|
||||
|
||||
```ts title="index.ts"
|
||||
// @ts-nocheck
|
||||
import { frontmatter as __fd_glob_1 } from "./generate-index/folder/test.mdx?collection=docs&only=frontmatter"
|
||||
@@ -18,6 +10,14 @@ const create = fromConfig<typeof Config>();
|
||||
export const docs = await create.docLazy("docs", "packages/mdx/test/fixtures/generate-index", {"index.mdx": __fd_glob_0, "folder/test.mdx": __fd_glob_1, }, {"index.mdx": () => import("./generate-index/index.mdx?collection=docs"), "folder/test.mdx": () => import("./generate-index/folder/test.mdx?collection=docs"), });
|
||||
```
|
||||
|
||||
```ts title="dynamic.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';
|
||||
import * as Config from './config';
|
||||
|
||||
const create = fromConfigDynamic(Config);
|
||||
```
|
||||
|
||||
```ts title="browser.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfig } from 'fumadocs-mdx/runtime/browser';
|
||||
|
||||
+8
-8
@@ -1,11 +1,3 @@
|
||||
```ts title="dynamic.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';
|
||||
import * as Config from './config';
|
||||
|
||||
const create = fromConfigDynamic(Config);
|
||||
```
|
||||
|
||||
```ts title="index.ts"
|
||||
// @ts-nocheck
|
||||
import { default as __fd_glob_0 } from "./generate-index/meta.json?collection=docs"
|
||||
@@ -17,6 +9,14 @@ const create = fromConfig<typeof Config>();
|
||||
export const docs = await create.meta("docs", "packages/mdx/test/fixtures/generate-index", {"meta.json": __fd_glob_0, });
|
||||
```
|
||||
|
||||
```ts title="dynamic.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';
|
||||
import * as Config from './config';
|
||||
|
||||
const create = fromConfigDynamic(Config);
|
||||
```
|
||||
|
||||
```ts title="browser.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfig } from 'fumadocs-mdx/runtime/browser';
|
||||
|
||||
+8
-8
@@ -1,11 +1,3 @@
|
||||
```ts title="dynamic.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';
|
||||
import * as Config from './config';
|
||||
|
||||
const create = fromConfigDynamic(Config);
|
||||
```
|
||||
|
||||
```ts title="index.ts"
|
||||
// @ts-nocheck
|
||||
import * as __fd_glob_1 from "./generate-index/folder/test.mdx?collection=docs"
|
||||
@@ -18,6 +10,14 @@ const create = fromConfig<typeof Config>();
|
||||
export const docs = await create.doc("docs", "packages/mdx/test/fixtures/generate-index", {"index.mdx": __fd_glob_0, "folder/test.mdx": __fd_glob_1, });
|
||||
```
|
||||
|
||||
```ts title="dynamic.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';
|
||||
import * as Config from './config';
|
||||
|
||||
const create = fromConfigDynamic(Config);
|
||||
```
|
||||
|
||||
```ts title="browser.ts"
|
||||
// @ts-nocheck
|
||||
import { fromConfig } from 'fumadocs-mdx/runtime/browser';
|
||||
|
||||
@@ -5,8 +5,9 @@ import { z } from 'zod';
|
||||
import { ValidationError } from '@/utils/validation';
|
||||
import { defineCollections } from '@/config';
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
import { emitIndexFiles } from '@/utils/codegen';
|
||||
import { buildConfig } from '@/config/build';
|
||||
import { createCore } from '@/core';
|
||||
import indexFile from '@/plugins/index-file';
|
||||
|
||||
test('format errors', async () => {
|
||||
const schema = z.object({
|
||||
@@ -70,20 +71,24 @@ const cases = [
|
||||
|
||||
for (const { name, collection } of cases) {
|
||||
test(`generate JS index file: ${name}`, async () => {
|
||||
const out = await emitIndexFiles({
|
||||
configPath: path.join(baseDir, './fixtures/config.ts'),
|
||||
const core = createCore(
|
||||
{
|
||||
configPath: path.join(baseDir, './fixtures/config.ts'),
|
||||
environment: 'test',
|
||||
outDir: path.join(baseDir, './fixtures'),
|
||||
},
|
||||
[indexFile()],
|
||||
);
|
||||
|
||||
await core.init({
|
||||
config: buildConfig({
|
||||
docs: collection,
|
||||
}),
|
||||
outDir: path.join(baseDir, './fixtures'),
|
||||
});
|
||||
|
||||
const markdown = out
|
||||
.map((entry) => {
|
||||
const content = entry.content.replaceAll(process.cwd(), '$cwd');
|
||||
|
||||
return `\`\`\`ts title="${entry.path}"\n${content}\n\`\`\``;
|
||||
})
|
||||
const markdown = (await core.emit())
|
||||
.map(
|
||||
(entry) => `\`\`\`ts title="${entry.path}"\n${entry.content}\n\`\`\``,
|
||||
)
|
||||
.join('\n\n');
|
||||
|
||||
await expect(markdown).toMatchFileSnapshot(
|
||||
|
||||
Reference in New Issue
Block a user