先把工具掏出来。
你可能用过jsdoc,用代码里面的注释生成文档。但是苦于jsdoc
生成的文档网页太不好看,目录结构不好调整。
你可能也用过VuePress或者VitePress。但是苦于手写Markdown文档,太费时间。
那有没有一种方案,可以直接用代码注释,生成vitepress构建的文档网页呢?
为什么不行?今天,作者封装了一个工具组件,满足你的需求!!
我们大可不必从头撸起,只需要把jsdoc
和VitePress
巧妙结合一下。
JavaScript注释->Markdown文档->VitePress站点
这里面JavaScript注释->Markdown文档
的部分,我们用到jsdoc2md这个组件来完成。
生成Markdown文件到指定目录后,我们希望不用修改VitePress
的配置文件,就可以自动根据目录的结构,生成VitePress
的侧边栏。这里我们用到作者的另一个组件vitepress-plugin-autobar。
根据泰斯勒定律,也叫复杂性守恒定律,每一个过程都有其固有的复杂性,存在一个临界点,到达临界点之后就不能再简化了,你只能将固有的复杂性从一个地方转移到另外一个地方。
为了方便使用,我们把现有的组件封装一下,把上面想法的复杂性留给自己,给使用者留下最简单易用的jsdoc2vitepress
组件。
期望的效果是:
jsdoc2vitepress init
初始化出下面的VitePress文档目录
.
├─ docs
│ ├─ .vitepress
│ │ └─ config.js
│ │ └─ jsdoc2vitepress.config.js
│ └─ index.md
这里就是一个简单的模板脚手架的功能,可以参考我们之前的一篇文章来实现具体的代码。
下面是具体的代码实现。
// init-docs.ts
import ora from 'ora';
import fs from 'fs-extra';
import gitclone from 'git-clone/promise';
import path from 'path';
/**
* @module init-docs
* @description init docs directory
*/
/**
* @exports initDocs "jsdoc2vitepress init" init docs directory
*/
export const initDocs = async () => {
const loading = ora('Init docs directory');
try {
const templateGitUrl = 'https://github.com/luciozhang/jsdoc2vitepress-template.git';
const docsDir = path.resolve(process.cwd(), 'docs');
await gitclone(templateGitUrl, docsDir, { checkout: 'master', shallow: true });
fs.removeSync(path.join(docsDir, '.git'));
loading.succeed('Init docs directory success');
} catch (error) {
loading.fail(`Init docs directory fail: ${error}\nPlease delete local 'docs' directory and retry.`);
}
};
这一步主要是读取源码,用jsdoc2md
对源码注释生成Markdown文档。
我们直接用这个组件的代码作为示例,代码的目录结构是下面这样的
预期效果是生成下面的Markdown文档。
Markdown内容是注释生成的API文档。
jsdoc2md
需要一个配置文件jsdoc2md.config.json
。这里上一步初始化的模板已经生成了一个可用的配置文件,更多的配置内容,参考Configuring JSDoc with a configuration file。
下面是具体代码实现。
// jsdoc-to-md.ts
import fs from 'fs-extra';
import path from 'path';
import jsdoc2md from 'jsdoc-to-markdown';
import ora from 'ora';
const configJ2VPath = path.resolve(process.cwd(), 'docs', '.vitepress', 'jsdoc2vitepress.config.json');
const configJ2MPath = path.resolve(process.cwd(), 'docs', '.vitepress', 'jsdoc2md.config.json');
/**
* @module jsdoc-to-md
* @description Generates Markdown API documentation from jsdoc annotated source code.
*/
/**
* @exports jsdocToMd Generates Markdown
* @returns {Promise} Return promise to check if generate success
*/
export const jsdocToMd = async () => {
const loading = ora('Generates Markdown');
try {
const { markdownDirs } = await fs.readJSON(configJ2VPath);
await Promise.all(markdownDirs.map(async (sourceObject: any) => {
const { root, output, ingoreList } = sourceObject;
await makeMarkDownForFiles(root, output, ingoreList);
}));
loading.succeed('Generates Markdown success');
} catch (error) {
loading.fail(`Generates Markdown fail${error}`);
}
};
const makeMarkDownForFiles = async (root: string, output: string, ingoreList: Array<string>) => {
const fileList = await fs.readdir(root);
await Promise.all(fileList.map(async (fileName) => {
if (ingoreList.indexOf(fileName) === -1) {
await makeMarkDownDoc(fileName, root, output);
}
}));
};
const makeMarkDownDoc = async (sourceName: string, sourceRootPath: string, outputPath: string) => {
let sourcePath = `${sourceRootPath}/${sourceName}`;
const outputName = sourceName.replace('.js', '').replace('.ts', '');
const loading = ora(`Generates Markdown for ${sourcePath}`);
try {
// 处理js文件的路径,需要区分是文件或目录,目录会将目录下所有文件生成为一个md
const stat = fs.lstatSync(sourcePath);
if (stat.isDirectory()) {
sourcePath = `${sourcePath}/*`;
}
const mdStr = await jsdoc2md.render({
'example-lang': 'javascript',
files: path.resolve(process.cwd(), sourcePath),
'name-format': 'backticks',
'heading-depth': 2,
'module-index-format': 'none',
configure: path.resolve(process.cwd(), configJ2MPath),
});
if (mdStr) {
fs.outputFile(path.resolve(process.cwd(), `${outputPath}/${outputName}.md`), mdStr);
loading.succeed('Generates Markdown success in ' + `${outputPath}/${outputName}.md`);
}
} catch (error) {
loading.fail(`Generates Markdown fail for ${sourcePath}`);
}
};
有了Markdown文档,我们接下来就是简单的运行Vitepress框架,生成Vitepress文档站点了。
这里我们额外加一个优化,自动生成Vitepress的侧边栏配置,用到作者的另一个组件vitepress-plugin-autobar。
这一步主要是用shell执行vitepress命令,具体代码如下。
//md-to-vitepress.ts
import shell from 'shelljs';
/**
* @module md-to-vitepress
* @description Generates VitePress API documentation from jsdoc annotated source code.
*/
/**
* @exports startVitePress Run VitePress server
* @returns {Promise} Return promise to check if run success
*/
export const startVitePress = async () => {
shell.exec('node_modules/.bin//vitepress dev docs');
};
/**
* @exports buildVitePress build VitePress server
* @returns {Promise} Return promise to check if build success
*/
export const buildVitePress = async () => {
shell.exec('node_modules/.bin/vitepress build docs');
};
最后,就只需要在入口文件,把上面封装的方法,整合成命令行工具了,具体代码如下
这里用到了commander库。
// index.ts
#!/usr/bin/env node
import { Command } from 'commander';
import { initDocs } from './init-docs';
import { jsdocToMd } from './jsdoc-to-md';
import { startVitePress, buildVitePress } from './md-to-vitepress';
const program = new Command();
program
.name('jsdoc2vitepress')
.description('Generates vitepress API documentation from jsdoc annotated source code.')
.version('1.0.0');
program
.command('init')
.description('init vitepress directory.')
.action(async () => {
await initDocs();
});
program
.command('start')
.description('Generates vitepress API documentation from jsdoc annotated source code and run Vitepress')
.action(async () => {
await jsdocToMd();
await startVitePress();
});
program
.command('build')
.description('Generates vitepress API documentation from jsdoc annotated source code and run Vitepress')
.action(async () => {
await jsdocToMd();
await buildVitePress();
});
program.parse();
jsdoc2vitepress init
jsdoc2vitepress start
控制台输出
文档网站
jsdoc2vitepress build
控制台输出
构建结果
这个工具最适合用于给组件库生成文档,建议配合CI/CD,在提交组件库代码的时候,触发构建文档和发布npm的流程,由代码注释生成文档并构建发布,从而确保组合库和文档的一致性。