UNPKG

@alauda/doom

Version:

Doctor Doom making docs.

242 lines (241 loc) 10.5 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import { logger, removeTrailingSlash, withBase, } from '@rspress/core'; import { isExternalUrl, removeLeadingSlash, slash, } from '@rspress/shared'; import { unset } from 'es-toolkit/compat'; import picomatch from 'picomatch'; import { pathExists, readJson } from "../../utils/index.js"; import { detectFilePath, extractInfoFromFrontmatter } from "./utils.js"; const sidebarSorter = (a, b) => { const aWeight = 'weight' in a && a.weight != null ? a.weight : 100; const bWeight = 'weight' in b && b.weight != null ? b.weight : 100; return aWeight - bWeight; }; const isExcluded = (onlyIncludeRoutes, excludeRoutes, fileKey) => { const included = !onlyIncludeRoutes.length || onlyIncludeRoutes.some((glob) => picomatch.isMatch(fileKey, glob)); return (!included || excludeRoutes.some((glob) => picomatch.isMatch(fileKey, glob))); }; /** * 1. Split sideMeta into two parts: `index` and `others` and sort `others` by weight * 2. filter only include routes if `onlyIncludeRoutes` is not empty * 3. filter out `excludeRoutes` */ const processSideMeta = (sideMeta, extensions, onlyIncludeRoutes, excludeRoutes) => { const result = sideMeta.reduce((acc, curr) => { if (!curr) { return acc; } if (!('_fileKey' in curr) || !curr._fileKey || 'items' in curr) { acc.others.push(curr); return acc; } const excluded = isExcluded(onlyIncludeRoutes, excludeRoutes, curr._fileKey); let filePart; if ((filePart = curr._fileKey.split(/[\\/]/).at(-1)) && extensions.some((ext) => filePart === `index${ext}`)) { if (acc.index?._fileKey) { // zh/development/component-quickstart/index.md vs zh/development/index.mdx const relative = path.relative(path.dirname(acc.index._fileKey), path.dirname(curr._fileKey)); if (relative === '..' || /[\\/]\.\.$/.test(relative)) { acc.others.unshift(acc.index); acc.index = curr; } else { acc.others.push(curr); } } else { acc.index = curr; } if (excluded) { if (acc.index === curr) { curr.link = ''; if (acc.others.length === 0) { acc.index = undefined; } } else { const index = acc.others.indexOf(curr); if (index > -1) { acc.others.splice(index, 1); } } } } else if (!excluded) { acc.others.push(curr); } return acc; }, { others: [] }); result.others.sort(sidebarSorter); return result; }; export async function scanSideMeta(workDir, rootDir, docsDir, routePrefix, extensions, ignoredDirs, onlyIncludeRoutes, excludeRoutes) { if (!(await pathExists(workDir))) { logger.error('[plugin-auto-sidebar]', `Generate sidebar meta error: ${workDir} not exists`); } const addRoutePrefix = (link) => `${routePrefix}${removeLeadingSlash(link)}`; // find the `_meta.json` file const metaFile = path.resolve(workDir, '_meta.json'); // Fix the windows path const relativePath = slash(path.relative(rootDir, workDir)); let sideMeta; // Get the sidebar config from the `_meta.json` file try { // Don't use require to avoid require cache, which make hmr not work. sideMeta = await readJson(metaFile); } catch { // If the `_meta.json` file doesn't exist, we will generate the sidebar config from the directory structure. let subItems = await fs.readdir(workDir); // If there exists a file with the same name of the directory folder // we don't need to generate SideMeta for this single file subItems = subItems.filter((item) => { const hasExtension = extensions.some((ext) => item.endsWith(ext)); const hasSameBaseName = subItems.some((elem) => { const baseName = elem.replace(/\.[^/.]+$/, ''); return baseName === item.replace(/\.[^/.]+$/, '') && elem !== item; }); return !(hasExtension && hasSameBaseName); }); sideMeta = (await Promise.all(subItems.map(async (item) => { // Fix https://github.com/web-infra-dev/rspress/issues/346 if (item === '_meta.json') { return null; } const stat = await fs.stat(path.join(workDir, item)); // If the item is a directory, we will transform it to a object with `type` and `name` property. if (stat.isDirectory()) { if (ignoredDirs.includes(item)) { return null; } // set H1 title to sidebar label when have same name md/mdx file const mdFilePath = path.join(workDir, `${item}.md`); const mdxFilePath = path.join(workDir, `${item}.mdx`); let label = item; const setLabelFromFilePath = async (filePath) => { const { title } = await extractInfoFromFrontmatter(filePath, rootDir, extensions); label = title; }; if (await pathExists(mdxFilePath)) { await setLabelFromFilePath(mdxFilePath); } else if (await pathExists(mdFilePath)) { await setLabelFromFilePath(mdFilePath); } return { type: 'dir', name: item, label, }; } return extensions.some((ext) => item.endsWith(ext)) ? item : null; }))).filter(Boolean); } const sidebarFromMeta = await Promise.all(sideMeta.map(async (metaItem) => { if (typeof metaItem === 'string') { const { title, overviewHeaders, context, weight } = await extractInfoFromFrontmatter(path.resolve(workDir, metaItem), rootDir, extensions); const pureLink = `${relativePath}/${metaItem.replace(/\.mdx?$/, '')}`; return { text: title, link: addRoutePrefix(pureLink), overviewHeaders, context, weight, _fileKey: path.relative(docsDir, path.join(workDir, metaItem)), }; } const { // eslint-disable-next-line @typescript-eslint/no-useless-default-assignment type = 'file', name, label = '', collapsible, collapsed = true, link, tag, dashed, overviewHeaders, context, } = metaItem; // when type is divider, name maybe undefined, and link is not used const pureLink = `${relativePath}/${name.replace(/\.mdx?$/, '')}`; if (type === 'file') { const info = await extractInfoFromFrontmatter(path.resolve(workDir, name), rootDir, extensions); const title = label || info.title; const realPath = info.realPath; return { text: title, link: addRoutePrefix(pureLink), tag, overviewHeaders: info.overviewHeaders ? info.overviewHeaders : overviewHeaders, context: info.context ? info.context : context, weight: info.weight, _fileKey: realPath ? path.relative(docsDir, realPath) : '', }; } if (type === 'dir') { const subDir = path.resolve(workDir, name); const { index, others: subSidebar } = await scanSideMeta(subDir, rootDir, docsDir, routePrefix, extensions, ['assets'], onlyIncludeRoutes, excludeRoutes); const realPath = await detectFilePath(subDir, extensions); const group = { text: label, collapsible, collapsed, items: subSidebar, link: realPath ? addRoutePrefix(pureLink) : '', tag, overviewHeaders, context, _fileKey: realPath ? path.relative(docsDir, realPath) : '', }; const sidebarItem = index ? { ...group, ...index } : group; if (!subSidebar.length) { if (index) { unset(sidebarItem, 'items'); return sidebarItem; } return; } return sidebarItem; } if (type === 'divider') { return { dividerType: dashed ? 'dashed' : 'solid', }; } if (type === 'section-header') { return { sectionHeaderText: label, tag, }; } return { text: label, link: isExternalUrl(link) ? link : withBase(link, routePrefix), tag, }; })); return processSideMeta(sidebarFromMeta, extensions, onlyIncludeRoutes, excludeRoutes); } // Start walking from the doc directory, scan the `_meta.json` file in each subdirectory // and generate the nav and sidebar config export async function walk(workDir, routePrefix = '/', docsDir, extensions, onlyIncludeRoutes = [], excludeRoutes = [], collapsed) { const { index, others } = await scanSideMeta(workDir, workDir, docsDir, routePrefix, extensions, ['assets', 'public', 'shared'], onlyIncludeRoutes, excludeRoutes); const isIndexExcluded = !!index?._fileKey && isExcluded(onlyIncludeRoutes, excludeRoutes, index._fileKey); const sidebars = index && !isIndexExcluded ? [index, ...others] : others; if (collapsed != null) { for (const sidebarItem of sidebars) { if ('items' in sidebarItem && sidebarItem.items.length) { sidebarItem.collapsed = collapsed; } } } // Every sub dir will represent a group of sidebar const sidebarConfig = { [routePrefix]: sidebars, }; const simpleRoutePrefix = removeTrailingSlash(routePrefix); if (simpleRoutePrefix) { sidebarConfig[simpleRoutePrefix] = sidebars; } const nav = []; return { nav, sidebar: sidebarConfig, }; }