scripts/sync-docs.js (368 lines of code) (raw):
/* eslint-disable max-len */
const childProcess = require('child_process');
const fs = require('fs/promises');
const path = require('path');
const process = require('process');
const os = require('os');
const Listr = require('listr');
const simpleGit = require('simple-git');
const semver = require('semver');
const replace = require('replace-in-file');
const common = require('./common.js');
const { versions, versionMap } = require('../config/apisix-versions.js');
const { projects, languages, projectPaths } = common;
const tempPath = './temp';
const websitePath = '../doc';
const gitMap = {};
const projectReleases = {};
const tasks = new Listr([
{
title: 'Start documents sync',
task: async () => {
if (!(await isDirExisted(tempPath))) {
await fs.mkdir(tempPath);
}
},
},
{
title: 'Clone git repositories',
task: () => {
const gitTasks = projects.map((project) => ({
title: `Clone ${project.name} repository`,
task: async () => {
const { name } = project;
const dir = `${tempPath}/${name}/`;
const exist = await isDirExisted(dir);
if (exist) {
gitMap[name] = simpleGit(dir);
await gitMap[name]
.cwd(dir)
.fetch(['--prune', '--filter=blob:none', '--recurse-submodules=no']);
} else {
gitMap[name] = simpleGit();
await gitMap[name]
.clone(`https://github.com/apache/${name}.git`, dir, {
'--filter': 'blob:none',
'--sparse': true,
'--recurse-submodules': 'no',
})
.cwd(dir)
.raw(['sparse-checkout', 'set', 'docs']);
if (name === 'apisix') {
await gitMap[name]
.cwd(dir)
.raw(['sparse-checkout', 'add', 'apisix/core', 'autodocs']);
}
}
},
}));
return new Listr(gitTasks, { concurrent: projects.length });
},
},
{
title: 'Find project release',
task: () => {
const findReleaseTasks = projects
.filter((p) => p.name !== 'apisix')
.map((project) => ({
title: `Find ${project.name} releases`,
task: async () => {
const ret = await gitMap[project.name].cwd(`${tempPath}/${project.name}/`).branch();
if (ret.all) {
const isIngressController = project.name === 'apisix-ingress-controller';
projectReleases[project.name] = ret.all
.filter((release) => (isIngressController
? release.includes('remotes/origin/v')
&& semver.gt(release.replace('remotes/origin/v', ''), '0.3.0')
: release.includes('remotes/origin/release/')))
.map((release) => (isIngressController
? release.replace('remotes/origin/v', '')
: release.replace('remotes/origin/release/', '')))
.sort((a, b) => semver.compare(semver.coerce(a).version, semver.coerce(b).version));
}
},
}));
return new Listr(findReleaseTasks, { concurrent: projects.length });
},
},
{
title: 'Extract documents',
task: () => {
// add apisix versions to release
projectReleases.apisix = versions;
const writeVersion2File = async (target, versions) => {
if (versions.length === 0) return Promise.resolve();
if (await isFileExisted(target)) await fs.rm(target);
return fs.writeFile(
target,
JSON.stringify(versions.map((v) => versionMap[v] || v).reverse(), null, 2),
);
};
const extractTasks = projectPaths.map((project) => ({
title: `Extract ${project.name}`,
task: () => new Listr([
{
title: `Create target dir`,
task: async () => {
const projectName = project.name;
const docs = `${websitePath}/docs-${projectName}_versioned_docs`;
const sidebar = `${websitePath}/docs-${projectName}_versioned_sidebars`;
const versions = `${websitePath}/docs-${projectName}_versions.json`;
const i18nDocs = `${websitePath}/i18n/zh/docusaurus-plugin-content-docs-docs-${projectName}`;
await Promise.allSettled([
removeFolder(docs),
removeFolder(sidebar),
removeFolder(i18nDocs),
writeVersion2File(versions, projectReleases[projectName]),
]);
},
},
{
title: `Extract ${project.name} release versions documents`,
task: () => {
const steps = projectReleases[project.name].map((version) => ({
title: `Extract ${project.name} ${version} documents`,
task: () => extractDocsVersionTasks(project, version),
}));
return new Listr(steps);
},
},
{
title: `Extract ${project.name} next version documents`,
task: () => extractDocsNextVersionTasks(project, project.branch),
},
]),
}));
return new Listr(extractTasks, { concurrent: projects.length });
},
},
]);
tasks
.run()
.then(() => {
console.log('[Finish] Documents synchronize success');
})
.catch((err) => {
console.error(err);
process.exit(1);
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function log(text) {
// console.log(text);
}
async function replaceMDElements(project, path, branch = 'master') {
const allMDFilePaths = path.map((p) => `${p}/**/*.md`);
// replace the image urls inside markdown files
const imageOptions = {
files: allMDFilePaths,
// NOTE: just replace the url begin with ../assets/images ,
// then can replace with absolute url path
from: /(\.\.\/)+assets\/images\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g,
to: (match) => {
const imgPath = match.replace(/\(|\)|\.\.\/*/g, '');
const newUrl = `https://raw.githubusercontent.com/apache/${project}/${branch}/docs/${imgPath}`;
log(`${project}: ${match} 👉 ${newUrl}`);
return newUrl;
},
};
// replace the markdown urls inside markdown files
const markdownOptions = {
files: allMDFilePaths,
from: RegExp(`\\[.*\\]\\((\\.\\.\\/)*(${languages.join('|')})\\/.*\\.md\\)`, 'g'),
to: (match) => {
const markdownPath = match.replace(/\(|\)|\.\.\/*|\[.*\]|\.\//g, ''); // "en/latest/discovery/dns.md"
const lang = markdownPath.split('/')[0];
const urlPath = markdownPath.replace(
RegExp(`(${languages.join('|')})\\/latest\\/|\\.md`, 'g'),
'',
); // "discovery/dns"
const projectNameWithoutPrefix = project === 'apisix' ? 'apisix' : project.replace('apisix-', '');
const newUrl = match.replace(
/\]\(.*\)/g,
`](https://apisix.apache.org${
lang === 'en' ? '' : `/${lang}`
}/docs/${projectNameWithoutPrefix}/${urlPath})`,
);
log(`${project}: ${match} 👉 ${newUrl}`);
return newUrl;
},
};
await replace(imageOptions);
await replace(markdownOptions);
}
async function isFileExisted(p) {
return fs
.stat(p)
.then((v) => v.isFile())
.catch(() => false);
}
async function isDirExisted(p) {
return fs
.stat(p)
.then((v) => v.isDirectory())
.catch(() => false);
}
async function removeFolder(tarDir) {
if (await isDirExisted()) return;
await fs.rm(tarDir, { recursive: true, force: true });
}
async function copyFolder(srcDir, tarDir) {
const [files] = await Promise.all([fs.readdir(srcDir), fs.mkdir(tarDir, { recursive: true })]);
return Promise.allSettled(
files.map(async (file) => {
const srcPath = path.join(srcDir, file);
const tarPath = path.join(tarDir, file);
if (await isDirExisted(srcPath)) {
return copyFolder(srcPath, tarPath);
}
if (await isFileExisted(srcPath)) {
return fs.copyFile(srcPath, tarPath);
}
return Promise.resolve();
}),
);
}
async function copyDocs(source, target) {
if (!(await isDirExisted(source))) {
log(`cannot find ${source}, skip.`);
return Promise.reject(new Error(`${source} no exist`));
}
return copyFolder(source, target);
}
function normalizeSidebar(sidebarList, version) {
const arr = sidebarList.map((block) => ({
...block,
...(block?.id ? { id: `version-${version}/${block.id}` } : {}),
...(block?.items?.length > 0
? {
collapsible: true,
collapsed: true,
items: block.items.map((v) => {
if (typeof v === 'string') {
return {
type: 'doc',
id: `version-${version}/${v}`,
};
}
return normalizeSidebar([v], version)[`version-${version}/docs`][0];
}),
}
: {}),
}));
return {
[`version-${version}/docs`]: arr,
};
}
async function handleConfig2Sidebar(source, target, version, versionedTarget) {
log(`load ${source} latest docs config.json`);
const config = JSON.parse(await fs.readFile(`${source}/config.json`));
const sidebar = JSON.stringify({ docs: config.sidebar || [] }, null, 2);
const writeVersionedSidebar = async () => {
if (typeof version === 'undefined') return Promise.resolve();
const versionedSidebar = JSON.stringify(normalizeSidebar(config.sidebar, version), null, 2);
await fs.mkdir(versionedTarget, { recursive: true });
return fs.writeFile(`${versionedTarget}/version-${version}-sidebars.json`, versionedSidebar);
};
await Promise.allSettled([
fs.unlink(`${source}/config.json`),
fs.writeFile(`${target}/sidebars.json`, sidebar),
writeVersionedSidebar(),
]);
}
/**
* Generate APISIX API Docs
* @return {Listr<Listr.ListrContext>}
* @param project
* @param version
*/
function generateAPIDocs(project) {
const dir = `./${tempPath}/${project.name}`;
return new Listr([
{
title: 'Generate markdown files',
task: () => {
childProcess.spawnSync(`autodocs/generate.sh`, ['build'], {
cwd: dir,
});
},
},
{
title: 'Copy API docs',
task: async () => {
if (
await copyDocs(`${dir}/autodocs/output`, `${dir}/pdk-docs`)
.then(() => true)
.catch(() => false)
) {
await fs.rm(`${dir}/autodocs/output`, { recursive: true });
}
},
},
]);
}
function extractDocsVersionTasks(project, version) {
const projectPath = `${tempPath}/${project.name}`;
const isIngressController = project.name === 'apisix-ingress-controller';
return new Listr([
{
title: `Checkout ${project.name} version: ${version}`,
task: () => gitMap[project.name]
.cwd(projectPath)
.checkout(
isIngressController
? `remotes/origin/v${version}`
: `remotes/origin/release/${version}`,
['-f'],
),
},
{
title: 'Generate API docs for APISIX',
enabled: () => project.name === 'apisix'
&& os.platform() === 'linux'
&& isFileExisted(`./${tempPath}/${project.name}/autodocs`),
task: () => generateAPIDocs(project),
},
{
title: `Copy to target path`,
task: async () => {
const branchName = isIngressController ? `v${version}` : `release/${version}`;
const projectName = project.name;
const docsPath = `${projectPath}/docs`;
const enSrcDocs = `${docsPath}/en/latest`;
const zhSrcDocs = `${docsPath}/zh/latest`;
const displayVersionName = (projectName === 'apisix' && versionMap?.[version]) || version;
const enTargetDocs = `${websitePath}/docs-${projectName}_versioned_docs/version-${displayVersionName}`;
const zhTargetDocs = `${websitePath}/i18n/zh/docusaurus-plugin-content-docs-docs-${projectName}/version-${displayVersionName}`;
await Promise.allSettled([
copyDocs(enSrcDocs, enTargetDocs)
.then(() => replaceMDElements(projectName, [enTargetDocs], branchName))
.then(() => handleConfig2Sidebar(
enTargetDocs,
enTargetDocs,
displayVersionName,
`${websitePath}/docs-${project.name}_versioned_sidebars`,
)),
copyDocs(zhSrcDocs, zhTargetDocs).then(() => replaceMDElements(projectName, [zhTargetDocs], branchName)),
]).catch(() => {
/* ignore */
});
},
},
]);
}
function extractDocsNextVersionTasks(project, version) {
const projectPath = `${tempPath}/${project.name}`;
return new Listr([
{
title: `Checkout ${project.name} version: ${version}`,
task: () => gitMap[project.name].cwd(projectPath).checkout(`remotes/origin/${version}`, ['-f']),
},
{
title: 'Generate API docs for APISIX',
enabled: () => project.name === 'apisix'
&& os.platform() === 'linux'
&& isFileExisted(`./${projectPath}/autodocs`),
task: () => generateAPIDocs(project),
},
{
title: `Copy to target path`,
task: async () => {
const branchName = project.branch;
const projectName = project.name;
const docsPath = `${projectPath}/docs`;
const enSrcDocs = `${docsPath}/en/latest`;
const zhSrcDocs = `${docsPath}/zh/latest`;
const enTargetDocs = `${websitePath}/docs/${projectName}`;
const zhTargetDocs = `${websitePath}/i18n/zh/docusaurus-plugin-content-docs-docs-${projectName}/current`;
await Promise.all([
copyDocs(enSrcDocs, enTargetDocs)
.then(() => replaceMDElements(projectName, [enTargetDocs], branchName))
.then(() => handleConfig2Sidebar(enTargetDocs, enTargetDocs)),
copyDocs(zhSrcDocs, zhTargetDocs).then(() => replaceMDElements(projectName, [zhTargetDocs], branchName)),
]).catch(() => {
/* ignore */
});
},
},
]);
}