@alauda/doom
Version:
Doctor Doom making docs.
383 lines (382 loc) • 14.7 kB
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginSvgr } from '@rsbuild/plugin-svgr';
import { pluginYaml } from '@rsbuild/plugin-yaml';
import { logger } from '@rspress/core';
import { pluginAlgolia } from '@rspress/plugin-algolia';
import { pluginLlms } from '@rspress/plugin-llms';
import { addLeadingSlash, addTrailingSlash, normalizeSlash, removeLeadingSlash, } from '@rspress/shared';
import { transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, transformerRemoveNotationEscape, } from '@shikijs/transformers';
import { difference } from 'es-toolkit';
import { glob } from 'tinyglobby';
import { cyan } from 'yoctocolors';
import { attributesPlugin } from '../plugins/attributes/index.js';
import { apiPlugin, autoSidebarPlugin, autoTocPlugin, createTransformerCallouts, directivesPlugin, globalPlugin, mermaidPlugin, permissionPlugin, replacePlugin, sitemapPlugin, } from '../plugins/index.js';
import { isExplicitlyUnversioned, UNVERSIONED, } from '../shared/index.js';
import { pathExists, pkgResolve, resolveStaticConfig } from '../utils/index.js';
import { DEFAULT_CONFIG_NAME, DEFAULT_EXTENSIONS, I18N_FILE, SITES_FILE, YAML_EXTENSIONS, } from './constants.js';
import { defaultGitHubUrl, isDoc } from './helpers.js';
const DEFAULT_LOGO = '/logo.svg';
const KNOWN_LOCALE_CONFIGS = {
en: {
label: 'English',
editLink: {
text: '📝 Edit this page on GitHub',
},
},
zh: {
label: '简体中文',
searchPlaceholderText: '搜索文档',
searchNoResultsText: '未搜索到相关结果',
searchSuggestedQueryText: '可更换不同的关键字后重试',
outlineTitle: '本页概览',
prevPageText: '上一页',
nextPageText: '下一页',
editLink: {
text: '📝 在 GitHub 上编辑此页',
},
},
ru: {
label: 'Русский',
searchPlaceholderText: 'Поиск документов',
searchNoResultsText: 'Не найдено соответствующих результатов',
searchSuggestedQueryText: 'Попробуйте изменить ключевые слова и повторить поиск',
outlineTitle: 'Обзор страницы',
prevPageText: 'Предыдущая страница',
nextPageText: 'Следующая страница',
editLink: {
text: '📝 Редактировать эту страницу на GitHub',
},
},
};
const ALAUDA_ALGOLIA_OPTIONS = {
appId: 'XN35Q5JLSV',
apiKey: '487fb969bdcda09dd4950468d7d8b61e',
indexName: 'docs_alauda_io_xn35q5jlsv_pages',
};
const ALAUDA_RU_ALGOLIA_OPTIONS = {
appId: 'GBNIJIGQ50',
apiKey: '786fa6da36fe76777d688387c0520c5b',
indexName: 'docs_alauda_io_gbnijigq50_pages',
};
const getCommonConfig = async ({ config, root, configFilePath, base, version, download, export: export_, ignore, force, open, lazy, include, exclude, redirect, editRepo, algolia, siteUrl, }) => {
const fallbackToZh = 'lang' in config && !config.lang;
root = resolveDocRoot(root, config.root);
const localBasePath = configFilePath ? path.dirname(configFilePath) : root;
const userBase = (base = addLeadingSlash(addTrailingSlash(base || config.base || '/')));
if (version && !isExplicitlyUnversioned(version)) {
base = userBase + `${version}/`;
}
let { editRepoBaseUrl } = config;
const editRepoEnabled = !!editRepo;
if (typeof editRepo === 'string') {
editRepoBaseUrl = editRepo;
}
if (editRepoEnabled) {
if (editRepoBaseUrl) {
editRepoBaseUrl = defaultGitHubUrl(editRepoBaseUrl);
}
else {
logger.error(`The \`${cyan('-R, --edit-repo')}\` flag is enabled specifically, but no \`${cyan('editRepoBaseUrl')}\` found, it will take no effect`);
}
}
const allLanguages = [];
const locales = [];
if (!fallbackToZh) {
const dirents = await fs.readdir(root, { withFileTypes: true });
for (const dirent of dirents) {
const { name } = dirent;
if (!dirent.isDirectory() ||
['dist', 'node_modules', 'public', 'shared'].includes(name)) {
continue;
}
allLanguages.push(name);
if (include?.length) {
if (!include.includes(name)) {
continue;
}
}
else if (exclude?.length) {
if (exclude.includes(name)) {
continue;
}
}
locales.push({
lang: name,
label: name,
...KNOWN_LOCALE_CONFIGS[name],
editLink: editRepoEnabled && editRepoBaseUrl
? {
docRepoBaseUrl: editRepoBaseUrl,
...KNOWN_LOCALE_CONFIGS.en.editLink,
...KNOWN_LOCALE_CONFIGS[name]?.editLink,
}
: undefined,
});
}
}
const unusedLanguages = difference(allLanguages, locales.map(({ lang }) => lang));
const { editLink, ...zhLocale } = KNOWN_LOCALE_CONFIGS.zh;
const algoliaOptions = ((algolia && config.algolia) ??
(algolia === 'alauda'
? ALAUDA_ALGOLIA_OPTIONS
: algolia === 'alauda-ru' && ALAUDA_RU_ALGOLIA_OPTIONS)) ||
null;
return {
userBase,
root,
base: addTrailingSlash(base),
lang: fallbackToZh ? 'zh' : config.lang || 'en',
title: '',
route: {
include: ignore ? config.onlyIncludeRoutes : undefined,
exclude: [
'dist/**/*',
'public/**/*',
'shared/**/*',
'doom.config.*',
'**/assets/**/*',
'**/*.d.ts',
...((ignore && config.internalRoutes) || []),
...unusedLanguages.map((lang) => `${lang}/**/*`),
],
},
markdown: {
shiki: {
transformers: [
// builtin transformers
transformerMetaHighlight(),
transformerMetaWordHighlight(),
transformerNotationDiff(),
transformerNotationErrorLevel(),
transformerNotationFocus(),
transformerNotationHighlight(),
transformerNotationWordHighlight(),
transformerRemoveNotationEscape(),
// custom transformers
createTransformerCallouts(),
],
},
},
themeConfig: {
enableScrollToTop: true,
// https://github.com/web-infra-dev/rspress/issues/2011
outline: true,
localeRedirect: redirect,
...(fallbackToZh
? editRepoEnabled && editRepoBaseUrl
? {
...zhLocale,
editLink: { ...editLink, docRepoBaseUrl: editRepoBaseUrl },
}
: zhLocale
: { locales }),
},
plugins: [
algoliaOptions && pluginAlgolia(),
pluginLlms(),
apiPlugin({
localBasePath,
}),
attributesPlugin(),
autoSidebarPlugin({ export: export_, ignore }),
autoTocPlugin(),
directivesPlugin(),
globalPlugin({ version, download }),
mermaidPlugin(),
permissionPlugin({
localBasePath,
}),
replacePlugin({
lang: fallbackToZh ? null : (config.lang ?? 'en'),
localBasePath,
force,
}),
siteUrl &&
config.siteUrl &&
sitemapPlugin({
domain: config.siteUrl,
}),
].filter(Boolean),
search: algoliaOptions ? false : undefined,
builderConfig: {
dev: {
lazyCompilation: lazy,
},
performance: {
buildCache: false,
},
plugins: [pluginReact(), pluginSass(), pluginSvgr(), pluginYaml()],
resolve: {
alias: {
classnames$: 'clsx',
},
},
server: {
open,
},
tools: {
rspack(rspackConfig, { mergeConfig, rspack }) {
return mergeConfig(rspackConfig, {
resolve: {
extensionAlias: {
'.js': ['.ts', '.tsx', '.js'],
},
},
plugins: [
new rspack.DefinePlugin({
'process.env.ALGOLIA_APP_ID': JSON.stringify(algoliaOptions?.appId),
'process.env.ALGOLIA_API_KEY': JSON.stringify(algoliaOptions?.apiKey),
'process.env.ALGOLIA_INDEX_NAME': JSON.stringify(algoliaOptions?.indexName),
}),
],
});
},
},
},
};
};
const findConfig = async (basePath) => {
for (const configFile of DEFAULT_EXTENSIONS.map((ext) => basePath + ext)) {
if (await pathExists(configFile, 'file')) {
return configFile;
}
}
};
export async function loadConfig(root, { config: configFile, base, prefix, v: version, download, export: export_, ignore, force, open, lazy, include, exclude, outDir, redirect, editRepo, algolia, siteUrl, } = {}) {
let configFilePath;
if (configFile) {
configFilePath = path.resolve(configFile);
}
else {
if (root) {
root = root.toString();
if (root.startsWith('file:')) {
root = fileURLToPath(root);
}
configFilePath = await findConfig(path.resolve(root, DEFAULT_CONFIG_NAME));
}
if (!configFilePath) {
configFilePath = await findConfig(path.resolve(DEFAULT_CONFIG_NAME));
}
// when root is not specified, try to find config in docs folder
if (!root && !configFilePath) {
configFilePath = await findConfig(path.resolve('docs', DEFAULT_CONFIG_NAME));
}
}
let config;
const { loadConfig, mergeRsbuildConfig } = await import('@rsbuild/core');
if (!configFilePath) {
logger.info(`No doom config file found in ${process.cwd()}`);
}
else {
try {
if (YAML_EXTENSIONS.includes(path.extname(configFilePath))) {
config = await resolveStaticConfig(configFilePath);
}
else {
const { content } = await loadConfig({
cwd: path.dirname(configFilePath),
path: configFilePath,
});
config = content;
}
}
catch {
logger.error(`Failed to load config from ${configFilePath}`);
}
}
config ??= {};
if (config.sites) {
logger.error('Use separate `sites.yaml` config in repository root instead');
}
else {
const sitesConfigFilePath = path.resolve(SITES_FILE);
if (await pathExists(sitesConfigFilePath, 'file')) {
config.sites = await resolveStaticConfig(sitesConfigFilePath);
}
}
version ||= '';
const commonConfig = await getCommonConfig({
config,
configFilePath,
root: root,
base,
version,
download,
export: export_,
ignore,
force,
open,
lazy,
include,
exclude,
redirect,
editRepo,
algolia,
siteUrl,
});
base = commonConfig.base;
const mergedConfig = mergeRsbuildConfig(commonConfig, config, {
base,
lang: commonConfig.lang,
root: commonConfig.root,
});
mergedConfig.export =
config.export &&
(await Promise.all(config.export.map(async (item) => ({
...item,
scope: Array.isArray(item.scope) ? item.scope : [item.scope],
flattenScope: (await glob(item.scope, { cwd: commonConfig.root })).filter(isDoc),
}))));
if (base && prefix) {
mergedConfig.base = (mergedConfig.prefix = normalizeSlash(prefix)) + base;
}
let ensureDefaultLogo = false;
if (!mergedConfig.logo) {
mergedConfig.logo = DEFAULT_LOGO;
ensureDefaultLogo = true;
}
if (!mergedConfig.icon) {
mergedConfig.icon = DEFAULT_LOGO;
ensureDefaultLogo = true;
}
if (ensureDefaultLogo) {
const publicPath = path.resolve(mergedConfig.root, `public`);
await fs.mkdir(publicPath, { recursive: true });
const logoPath = path.resolve(publicPath, removeLeadingSlash(DEFAULT_LOGO));
if (!(await pathExists(logoPath))) {
await fs.copyFile(pkgResolve(`assets${DEFAULT_LOGO}`), logoPath);
}
}
if (mergedConfig.i18nSourcePath) {
if (!path.isAbsolute(mergedConfig.i18nSourcePath)) {
mergedConfig.i18nSourcePath = path.resolve(configFilePath ? path.dirname(configFilePath) : mergedConfig.root, mergedConfig.i18nSourcePath);
}
}
else {
mergedConfig.i18nSourcePath = path.resolve(mergedConfig.root, I18N_FILE);
}
outDir ||= mergedConfig.outDir;
mergedConfig.outDir = `dist${(outDir ? addLeadingSlash(addTrailingSlash(outDir)) : base) +
(isExplicitlyUnversioned(version) ? UNVERSIONED : outDir ? version : '')}`;
return {
config: mergedConfig,
configFilePath: configFilePath || '',
};
}
export function resolveDocRoot(cliRoot, configRoot) {
// CLI root has highest priority
if (cliRoot) {
return path.resolve(cliRoot);
}
// Config root is next in priority
if (configRoot) {
return path.isAbsolute(configRoot) ? configRoot : path.resolve(configRoot);
}
// Default to 'docs' if no root is specified
return path.resolve('docs');
}