mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Replace lastModifiedTime option with lastModified plugin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': major
|
||||
---
|
||||
|
||||
Replace `lastModifiedTime` option with `lastModified` plugin
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { z } from 'zod';
|
||||
import type { ElementContent } from 'hast';
|
||||
import jsonSchema from 'fumadocs-mdx/plugins/json-schema';
|
||||
import lastModified from 'fumadocs-mdx/plugins/last-modified';
|
||||
import type { ShikiTransformer } from 'shiki';
|
||||
|
||||
export const docs = defineDocs({
|
||||
@@ -63,11 +64,11 @@ function transformerEscape(): ShikiTransformer {
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
lastModifiedTime: 'git',
|
||||
plugins: [
|
||||
jsonSchema({
|
||||
insert: true,
|
||||
}),
|
||||
lastModified(),
|
||||
],
|
||||
mdxOptions: async () => {
|
||||
const { rehypeCodeDefaultOptions } = await import(
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-remove-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zod": "^4.1.12"
|
||||
"zod": "^4.1.12",
|
||||
"vfile": "^6.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fumadocs/mdx-remote": "workspace:*",
|
||||
@@ -105,7 +106,6 @@
|
||||
"remark-stringify": "^11.0.0",
|
||||
"rollup": "^4.52.5",
|
||||
"tsconfig": "workspace:*",
|
||||
"vfile": "^6.0.3",
|
||||
"vite": "^7.2.0",
|
||||
"webpack": "^5.102.1"
|
||||
},
|
||||
|
||||
@@ -82,12 +82,6 @@ export interface GlobalConfig {
|
||||
*/
|
||||
mdxOptions?: GlobalConfigMDXOptions | (() => Promise<GlobalConfigMDXOptions>);
|
||||
|
||||
/**
|
||||
* Fetch last modified time with specified version control
|
||||
* @defaultValue 'none'
|
||||
*/
|
||||
lastModifiedTime?: 'git' | 'none';
|
||||
|
||||
/**
|
||||
* specify a directory to access & store cache (disabled during development mode).
|
||||
*
|
||||
|
||||
+96
-11
@@ -1,8 +1,14 @@
|
||||
import type { LoadedConfig } from '@/config/build';
|
||||
import type {
|
||||
DocCollectionItem,
|
||||
LoadedConfig,
|
||||
MetaCollectionItem,
|
||||
} from '@/config/build';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
import { removeFileCache } from './utils/codegen/cache';
|
||||
import { validate } from './utils/validation';
|
||||
import type { VFile } from 'vfile';
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
@@ -41,6 +47,30 @@ export interface Plugin {
|
||||
this: PluginContext,
|
||||
server: ServerContext,
|
||||
) => Awaitable<void>;
|
||||
|
||||
/**
|
||||
* Transform frontmatter/metadata
|
||||
*/
|
||||
metadata?: (
|
||||
this: PluginContext & {
|
||||
collection: DocCollectionItem | MetaCollectionItem;
|
||||
filePath: string;
|
||||
},
|
||||
data: unknown,
|
||||
) => Awaitable<unknown | void>;
|
||||
|
||||
doc?: {
|
||||
/**
|
||||
* Transform `vfile` on compilation stage
|
||||
*/
|
||||
vfile?: (
|
||||
this: PluginContext & {
|
||||
collection: DocCollectionItem;
|
||||
filePath: string;
|
||||
},
|
||||
file: VFile,
|
||||
) => Awaitable<VFile | void>;
|
||||
};
|
||||
}
|
||||
|
||||
export type PluginOption = Awaitable<Plugin | PluginOption[] | false>;
|
||||
@@ -91,14 +121,15 @@ export function createCore(
|
||||
let config: LoadedConfig;
|
||||
let plugins: Plugin[];
|
||||
|
||||
return {
|
||||
function createPluginContext(): PluginContext {
|
||||
return {
|
||||
core,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
const core = {
|
||||
_options: options,
|
||||
getPluginContext(): PluginContext {
|
||||
return {
|
||||
core: this,
|
||||
...options,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Convenient cache store, reset when config changes
|
||||
*/
|
||||
@@ -111,8 +142,9 @@ export function createCore(
|
||||
...(config.global.plugins ?? []),
|
||||
]);
|
||||
|
||||
const ctx = createPluginContext();
|
||||
for (const plugin of plugins) {
|
||||
const out = await plugin.config?.call(this.getPluginContext(), config);
|
||||
const out = await plugin.config?.call(ctx, config);
|
||||
if (out) config = out;
|
||||
}
|
||||
|
||||
@@ -132,19 +164,22 @@ export function createCore(
|
||||
if (event === 'change') removeFileCache(file);
|
||||
});
|
||||
|
||||
const ctx = createPluginContext();
|
||||
for (const plugin of plugins) {
|
||||
await plugin.configureServer?.call(this.getPluginContext(), server);
|
||||
await plugin.configureServer?.call(ctx, server);
|
||||
}
|
||||
},
|
||||
async emit({ filterPlugin = () => true }: EmitOptions = {}): Promise<
|
||||
EmitEntry[]
|
||||
> {
|
||||
const ctx = createPluginContext();
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
plugins.map((plugin) => {
|
||||
if (!filterPlugin(plugin) || !plugin.emit) return [];
|
||||
|
||||
return plugin.emit.call(this.getPluginContext());
|
||||
return plugin.emit.call(ctx);
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
@@ -163,7 +198,57 @@ export function createCore(
|
||||
);
|
||||
console.log(`[MDX] generated files in ${performance.now() - start}ms`);
|
||||
},
|
||||
async metadata(
|
||||
collection: DocCollectionItem | MetaCollectionItem,
|
||||
filePath: string,
|
||||
source: string,
|
||||
data: unknown,
|
||||
) {
|
||||
if (collection.schema) {
|
||||
data = await validate(
|
||||
collection.schema,
|
||||
data,
|
||||
{ path: filePath, source },
|
||||
collection.type === 'doc'
|
||||
? `invalid frontmatter in ${filePath}`
|
||||
: `invalid data in ${filePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
...createPluginContext(),
|
||||
filePath,
|
||||
collection,
|
||||
};
|
||||
|
||||
for (const plugin of plugins) {
|
||||
data = (await plugin.metadata?.call(ctx, data)) ?? data;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
doc: {
|
||||
async vfile(
|
||||
collection: DocCollectionItem,
|
||||
filePath: string,
|
||||
file: VFile,
|
||||
): Promise<VFile> {
|
||||
const ctx = {
|
||||
...createPluginContext(),
|
||||
filePath,
|
||||
collection,
|
||||
};
|
||||
|
||||
for (const plugin of plugins) {
|
||||
file = (await plugin.doc?.vfile?.call(ctx, file)) ?? file;
|
||||
}
|
||||
|
||||
return file;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return core;
|
||||
}
|
||||
|
||||
export type Core = ReturnType<typeof createCore>;
|
||||
|
||||
@@ -153,7 +153,7 @@ export function toWebpack(loader: Loader): WebpackLoader {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ValidationError) {
|
||||
return callback(new Error(error.toStringFormatted()));
|
||||
return callback(new Error(await error.toStringFormatted()));
|
||||
}
|
||||
|
||||
if (!(error instanceof Error)) throw error;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { LoadedConfig } from '@/config/build';
|
||||
|
||||
export interface ConfigLoader {
|
||||
getConfig: () => LoadedConfig | Promise<LoadedConfig>;
|
||||
core: Core;
|
||||
}
|
||||
|
||||
export function createStandaloneConfigLoader({
|
||||
@@ -43,6 +44,7 @@ export function createStandaloneConfigLoader({
|
||||
}
|
||||
|
||||
return {
|
||||
core,
|
||||
async getConfig() {
|
||||
const hash = await getConfigHash();
|
||||
if (loaded && loaded.hash === hash) return loaded.config;
|
||||
@@ -61,6 +63,7 @@ export function createStandaloneConfigLoader({
|
||||
*/
|
||||
export function createIntegratedConfigLoader(core: Core): ConfigLoader {
|
||||
return {
|
||||
core,
|
||||
getConfig() {
|
||||
return core.getConfig();
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createProcessor, type ProcessorOptions } from '@mdx-js/mdx';
|
||||
import type { VFile } from 'vfile';
|
||||
import { createProcessor } from '@mdx-js/mdx';
|
||||
import { VFile } from 'vfile';
|
||||
import { remarkInclude } from '@/loaders/mdx/remark-include';
|
||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import type { TOCItemType } from 'fumadocs-core/toc';
|
||||
@@ -10,26 +10,22 @@ import {
|
||||
type PostprocessOptions,
|
||||
remarkPostprocess,
|
||||
} from '@/loaders/mdx/remark-postprocess';
|
||||
import type { Core } from '@/core';
|
||||
import type { DocCollectionItem } from '@/config/build';
|
||||
|
||||
type Processor = ReturnType<typeof createProcessor>;
|
||||
|
||||
const cache = new Map<string, Processor>();
|
||||
|
||||
interface BuildMDXOptions extends ProcessorOptions {
|
||||
interface BuildMDXOptions {
|
||||
/**
|
||||
* Specify a file path for source
|
||||
*/
|
||||
filePath: string;
|
||||
|
||||
source: string;
|
||||
frontmatter?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Custom Vfile data
|
||||
*/
|
||||
data?: Record<string, unknown>;
|
||||
|
||||
environment: 'bundler' | 'runtime';
|
||||
isDevelopment: boolean;
|
||||
_compiler?: CompilerOptions;
|
||||
postprocess?: Partial<PostprocessOptions>;
|
||||
}
|
||||
|
||||
export interface CompilerOptions {
|
||||
@@ -82,45 +78,50 @@ declare module 'vfile' {
|
||||
interface DataMap extends FumadocsDataMap {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cacheKey - key to cache processor
|
||||
* @param source - mdx content
|
||||
* @param options - MDX options
|
||||
*/
|
||||
export async function buildMDX(
|
||||
cacheKey: string,
|
||||
source: string,
|
||||
options: BuildMDXOptions,
|
||||
core: Core,
|
||||
collection: DocCollectionItem | undefined,
|
||||
{
|
||||
filePath,
|
||||
frontmatter,
|
||||
source,
|
||||
_compiler,
|
||||
environment,
|
||||
isDevelopment,
|
||||
}: BuildMDXOptions,
|
||||
): Promise<VFile> {
|
||||
const { filePath, frontmatter, data, _compiler, ...rest } = options;
|
||||
const mdxOptions = await core
|
||||
.getConfig()
|
||||
.getMDXOptions(collection, environment);
|
||||
|
||||
function getProcessor(format: 'md' | 'mdx') {
|
||||
const key = `${cacheKey}:${format}`;
|
||||
const cache = core.cache as Map<string, Processor>;
|
||||
const key = `build-mdx:${collection?.name ?? 'global'}:${format}`;
|
||||
let processor = cache.get(key);
|
||||
|
||||
if (!processor) {
|
||||
const postprocessOptions: PostprocessOptions = {
|
||||
_format: format,
|
||||
...collection?.postprocess,
|
||||
valueToExport: [
|
||||
...(collection?.postprocess?.valueToExport ?? []),
|
||||
'structuredData',
|
||||
'extractedReferences',
|
||||
'frontmatter',
|
||||
'lastModified',
|
||||
'_markdown',
|
||||
'_mdast',
|
||||
],
|
||||
};
|
||||
|
||||
processor = createProcessor({
|
||||
outputFormat: 'program',
|
||||
...rest,
|
||||
development: isDevelopment,
|
||||
...mdxOptions,
|
||||
remarkPlugins: [
|
||||
remarkInclude,
|
||||
...(rest.remarkPlugins ?? []),
|
||||
[
|
||||
remarkPostprocess,
|
||||
{
|
||||
_format: format,
|
||||
...options.postprocess,
|
||||
valueToExport: [
|
||||
...(options.postprocess?.valueToExport ?? []),
|
||||
'structuredData',
|
||||
'extractedReferences',
|
||||
'frontmatter',
|
||||
'lastModified',
|
||||
'_markdown',
|
||||
'_mdast',
|
||||
],
|
||||
} satisfies PostprocessOptions,
|
||||
],
|
||||
...(mdxOptions.remarkPlugins ?? []),
|
||||
[remarkPostprocess, postprocessOptions],
|
||||
],
|
||||
format,
|
||||
});
|
||||
@@ -131,16 +132,15 @@ export async function buildMDX(
|
||||
return processor;
|
||||
}
|
||||
|
||||
return getProcessor(
|
||||
options.format ?? (filePath.endsWith('.mdx') ? 'mdx' : 'md'),
|
||||
).process({
|
||||
let vfile = new VFile({
|
||||
value: source,
|
||||
path: filePath,
|
||||
data: {
|
||||
...data,
|
||||
frontmatter,
|
||||
_compiler,
|
||||
_getProcessor: getProcessor,
|
||||
},
|
||||
data: { frontmatter, _compiler, _getProcessor: getProcessor },
|
||||
});
|
||||
|
||||
if (collection) {
|
||||
vfile = await core.doc.vfile(collection, filePath, vfile);
|
||||
}
|
||||
|
||||
return getProcessor(filePath.endsWith('.mdx') ? 'mdx' : 'md').process(vfile);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
import { validate } from '@/utils/validation';
|
||||
import { getGitTimestamp } from '@/utils/git-timestamp';
|
||||
import type { SourceMap } from 'rollup';
|
||||
import type { Loader } from '@/loaders/adapter';
|
||||
import { z } from 'zod';
|
||||
import type { DocCollectionItem, LoadedConfig } from '@/config/build';
|
||||
import type { DocCollectionItem } from '@/config/build';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
@@ -79,15 +77,12 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
break;
|
||||
}
|
||||
|
||||
if (docCollection?.schema) {
|
||||
matter.data = await validate(
|
||||
docCollection.schema,
|
||||
if (docCollection) {
|
||||
matter.data = await configLoader.core.metadata(
|
||||
docCollection,
|
||||
filePath,
|
||||
value,
|
||||
matter.data,
|
||||
{
|
||||
source: value,
|
||||
path: filePath,
|
||||
},
|
||||
`invalid frontmatter in ${filePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,28 +93,18 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
};
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
if (config.global.lastModifiedTime === 'git') {
|
||||
data.lastModified = (await getGitTimestamp(filePath))?.getTime();
|
||||
}
|
||||
|
||||
// ensure the line number is correct in dev mode
|
||||
const lineOffset = isDevelopment ? countLines(matter.matter) : 0;
|
||||
|
||||
const { buildMDX } = await import('@/loaders/mdx/build-mdx');
|
||||
const compiled = await buildMDX(
|
||||
`${getConfigHash(config)}:${parsed.collection ?? 'global'}`,
|
||||
'\n'.repeat(lineOffset) + matter.content,
|
||||
{
|
||||
development: isDevelopment,
|
||||
...(await config.getMDXOptions(docCollection, 'bundler')),
|
||||
postprocess: docCollection?.postprocess,
|
||||
data,
|
||||
filePath,
|
||||
frontmatter: matter.data as Record<string, unknown>,
|
||||
_compiler: compiler,
|
||||
},
|
||||
);
|
||||
const compiled = await buildMDX(configLoader.core, docCollection, {
|
||||
isDevelopment,
|
||||
source: '\n'.repeat(lineOffset) + matter.content,
|
||||
filePath,
|
||||
frontmatter: matter.data as Record<string, unknown>,
|
||||
_compiler: compiler,
|
||||
environment: 'bundler',
|
||||
});
|
||||
|
||||
const out = {
|
||||
code: String(compiled.value),
|
||||
@@ -132,17 +117,6 @@ export function createMdxLoader(configLoader: ConfigLoader): Loader {
|
||||
};
|
||||
}
|
||||
|
||||
const hashes = new WeakMap<LoadedConfig, string>();
|
||||
|
||||
function getConfigHash(config: LoadedConfig) {
|
||||
let hash = hashes.get(config);
|
||||
if (hash) return hash;
|
||||
|
||||
hash = Date.now().toString();
|
||||
hashes.set(config, hash);
|
||||
return hash;
|
||||
}
|
||||
|
||||
function generateCacheHash(input: string): string {
|
||||
return createHash('md5').update(input).digest('hex');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Loader, LoaderInput } from '@/loaders/adapter';
|
||||
import type { ConfigLoader } from '@/loaders/config';
|
||||
import { dump, load } from 'js-yaml';
|
||||
import { validate } from '@/utils/validation';
|
||||
import { z } from 'zod';
|
||||
import { metaLoaderGlob } from '.';
|
||||
import type { MetaCollectionItem } from '@/config/build';
|
||||
@@ -55,12 +54,12 @@ export function createMetaLoader(
|
||||
}
|
||||
|
||||
const data = parse(filePath, source);
|
||||
if (metaCollection?.schema) {
|
||||
return validate(
|
||||
metaCollection.schema,
|
||||
if (metaCollection) {
|
||||
return configLoader.core.metadata(
|
||||
metaCollection,
|
||||
filePath,
|
||||
source,
|
||||
data,
|
||||
{ path: filePath, source },
|
||||
`invalid data in ${filePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ function createNextCore(options: Required<CreateMDXOptions>): Core {
|
||||
await core.emitAndWrite(...args);
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
console.error(err.toStringFormatted());
|
||||
console.error(await err.toStringFormatted());
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Plugin } from '@/core';
|
||||
import type { Core, Plugin } from '@/core';
|
||||
import type {
|
||||
LoadedConfig,
|
||||
CollectionItem,
|
||||
@@ -13,8 +13,6 @@ 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';
|
||||
@@ -120,7 +118,7 @@ export default function indexFile(
|
||||
out.push(
|
||||
toEmitEntry(
|
||||
'dynamic.ts',
|
||||
generateDynamicIndexFile(makeCodeGen(), config, this.configPath),
|
||||
generateDynamicIndexFile(this.core, makeCodeGen()),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -215,16 +213,13 @@ async function generateServerIndexFile(
|
||||
return codegen.toString();
|
||||
}
|
||||
|
||||
async function generateDynamicIndexFile(
|
||||
codegen: CodeGen,
|
||||
config: LoadedConfig,
|
||||
configPath: string,
|
||||
) {
|
||||
async function generateDynamicIndexFile(core: Core, codegen: CodeGen) {
|
||||
const { configPath } = core._options;
|
||||
codegen.lines.push(
|
||||
`import { fromConfigDynamic } from 'fumadocs-mdx/runtime/dynamic';`,
|
||||
`import * as Config from '${codegen.formatImportPath(configPath)}';`,
|
||||
'',
|
||||
`const create = fromConfigDynamic(Config);`,
|
||||
`const create = await fromConfigDynamic(Config);`,
|
||||
);
|
||||
|
||||
async function generateCollectionObjectEntry(
|
||||
@@ -234,21 +229,12 @@ async function generateDynamicIndexFile(
|
||||
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 data = await core.metadata(
|
||||
collection,
|
||||
fullPath,
|
||||
content,
|
||||
parsed.data,
|
||||
);
|
||||
|
||||
const hash = createHash('md5').update(content).digest('hex');
|
||||
const infoStr: string[] = [
|
||||
@@ -261,9 +247,8 @@ async function generateDynamicIndexFile(
|
||||
path: file,
|
||||
},
|
||||
data,
|
||||
lastModified,
|
||||
hash,
|
||||
} satisfies LazyEntry<unknown>)) {
|
||||
} satisfies LazyEntry)) {
|
||||
infoStr.push(`${k}: ${JSON.stringify(v)}`);
|
||||
}
|
||||
|
||||
@@ -302,7 +287,7 @@ async function generateDynamicIndexFile(
|
||||
}
|
||||
|
||||
await codegen.pushAsync(
|
||||
config.collectionList.map(async (collection) => {
|
||||
core.getConfig().collectionList.map(async (collection) => {
|
||||
const obj = await generateCollectionObject(collection);
|
||||
if (!obj) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import path from 'node:path';
|
||||
import { x } from 'tinyexec';
|
||||
import type { Plugin } from '@/core';
|
||||
|
||||
const cache = new Map<string, Promise<Date>>();
|
||||
|
||||
export interface LastModifiedPluginOptions {
|
||||
/**
|
||||
* Version control to obtain the last modified time.
|
||||
*
|
||||
* - `git`: Requires `git` to be installed.
|
||||
*
|
||||
* If you are using Vercel, please set `VERCEL_DEEP_CLONE` environment variable to `true`.
|
||||
*
|
||||
* @defaultValue 'git'
|
||||
*/
|
||||
versionControl?: 'git';
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects `lastModified` property to page data.
|
||||
*
|
||||
* By default, it uses Git to obtain the last modified time.
|
||||
*/
|
||||
export default function lastModified(
|
||||
options: LastModifiedPluginOptions = {},
|
||||
): Plugin {
|
||||
const { versionControl = 'git' } = options;
|
||||
|
||||
return {
|
||||
name: 'last-modified',
|
||||
doc: {
|
||||
async vfile(file) {
|
||||
if (versionControl === 'git') {
|
||||
const timestamp = await getGitTimestamp(this.filePath);
|
||||
if (timestamp === undefined) return;
|
||||
|
||||
file.data.lastModified = timestamp.getTime();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getGitTimestamp(file: string): Promise<Date | undefined> {
|
||||
const cached = cache.get(file);
|
||||
if (cached) return cached;
|
||||
|
||||
const timePromise = (async () => {
|
||||
const out = await x(
|
||||
'git',
|
||||
['log', '-1', '--pretty="%ai"', path.relative(process.cwd(), file)],
|
||||
{
|
||||
throwOnError: true,
|
||||
},
|
||||
);
|
||||
|
||||
return new Date(out.stdout);
|
||||
})();
|
||||
|
||||
cache.set(file, timePromise);
|
||||
return timePromise.catch(() => undefined);
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import { pathToFileURL } from 'node:url';
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
import fs from 'node:fs/promises';
|
||||
import { type FileInfo, fromConfig } from './server';
|
||||
import { type CoreOptions, createCore } from '@/core';
|
||||
|
||||
export interface LazyEntry<Data> {
|
||||
export interface LazyEntry<Data = unknown> {
|
||||
info: FileInfo;
|
||||
data: Data;
|
||||
lastModified?: Date;
|
||||
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
@@ -17,12 +18,18 @@ export type CreateDynamic<Config> = ReturnType<
|
||||
typeof fromConfigDynamic<Config>
|
||||
>;
|
||||
|
||||
export function fromConfigDynamic<Config>(configExports: Config) {
|
||||
const config = buildConfig(configExports as Record<string, unknown>);
|
||||
export async function fromConfigDynamic<Config>(
|
||||
configExports: Config,
|
||||
coreOptions: CoreOptions,
|
||||
) {
|
||||
const core = await createCore(coreOptions).init({
|
||||
config: buildConfig(configExports as Record<string, unknown>),
|
||||
});
|
||||
|
||||
const create = fromConfig<Config>();
|
||||
|
||||
function getDocCollection(name: string): DocCollectionItem | undefined {
|
||||
const collection = config.getCollection(name);
|
||||
const collection = core.getConfig().getCollection(name);
|
||||
if (!collection) return;
|
||||
|
||||
if (collection.type === 'docs') return collection.docs;
|
||||
@@ -36,20 +43,15 @@ export function fromConfigDynamic<Config>(configExports: Config) {
|
||||
const head: Record<string, () => unknown> = {};
|
||||
const body: Record<string, () => Promise<unknown>> = {};
|
||||
|
||||
async function compile({ info, lastModified, data }: LazyEntry<unknown>) {
|
||||
const mdxOptions = await config.getMDXOptions(collection, 'runtime');
|
||||
async function compile({ info, data }: LazyEntry<unknown>) {
|
||||
const raw = (await fs.readFile(info.fullPath)).toString();
|
||||
|
||||
const { content } = fumaMatter(raw);
|
||||
const compiled = await buildMDX(collection.name, content, {
|
||||
...mdxOptions,
|
||||
development: false,
|
||||
frontmatter: data as Record<string, unknown>,
|
||||
postprocess: collection!.postprocess,
|
||||
data: {
|
||||
lastModified,
|
||||
},
|
||||
const compiled = await buildMDX(core, collection, {
|
||||
filePath: info.fullPath,
|
||||
source: fumaMatter(raw).content,
|
||||
frontmatter: data as Record<string, unknown>,
|
||||
isDevelopment: false,
|
||||
environment: 'runtime',
|
||||
});
|
||||
|
||||
return (await executeMdx(String(compiled.value), {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import { x } from 'tinyexec';
|
||||
|
||||
const cache = new Map<string, Date>();
|
||||
|
||||
/**
|
||||
* Requires `git` to be installed
|
||||
*
|
||||
* if you are using Vercel, please set `VERCEL_DEEP_CLONE` environment variable to `true`
|
||||
*/
|
||||
export async function getGitTimestamp(file: string): Promise<Date | undefined> {
|
||||
const cached = cache.get(file);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
const out = await x(
|
||||
'git',
|
||||
['log', '-1', '--pretty="%ai"', path.relative(process.cwd(), file)],
|
||||
{
|
||||
throwOnError: true,
|
||||
},
|
||||
);
|
||||
|
||||
const time = new Date(out.stdout);
|
||||
cache.set(file, time);
|
||||
return time;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CollectionSchema } from '@/config';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import picocolors from 'picocolors';
|
||||
|
||||
export class ValidationError extends Error {
|
||||
title: string;
|
||||
@@ -15,7 +14,9 @@ export class ValidationError extends Error {
|
||||
this.issues = issues;
|
||||
}
|
||||
|
||||
toStringFormatted() {
|
||||
async toStringFormatted() {
|
||||
const picocolors = await import('picocolors');
|
||||
|
||||
return [
|
||||
picocolors.bold(`[MDX] ${this.title}:`),
|
||||
...this.issues.map((issue) =>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default async function mdx(
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
throw new Error(e.toStringFormatted());
|
||||
throw new Error(await e.toStringFormatted());
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
||||
Reference in New Issue
Block a user