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 */ }); }, }, ]); }