-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathjsdocMd.mjs
157 lines (136 loc) · 5.17 KB
/
jsdocMd.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import fs from "fs";
import { globby } from "globby";
import { resolve } from "path";
import CliError from "./CliError.mjs";
import codeToJsdocComments from "./codeToJsdocComments.mjs";
import jsdocCommentToMember from "./jsdocCommentToMember.mjs";
import membersToMdAst from "./membersToMdAst.mjs";
import replaceMdSection from "./replaceMdSection.mjs";
/**
* Analyzes JSDoc from source files to populate a markdown file documentation
* section. Source files are excluded via `.gitignore` files. If the optional
* peer dependency [`prettier`](https://npm.im/prettier) is installed, the new
* markdown file contents is [Prettier](https://prettier.io) formatted.
* @kind function
* @name jsdocMd
* @param {object} [options] Options.
* @param {string} [options.cwd] A directory path to scope the search for source and `.gitignore` files, defaulting to `process.cwd()`.
* @param {string} [options.sourceGlob="**\/*.{mjs,cjs,js}"] JSDoc source file glob pattern.
* @param {string} [options.markdownPath="readme.md"] Path to the markdown file for docs insertion.
* @param {string} [options.targetHeading="API"] Markdown file heading to insert docs under.
* @param {boolean} [options.check=false] Should an error be thrown instead of updating the markdown file if the contents would change; useful for checking docs are up to date in CI.
* @returns {Promise<void>} Resolves once the operation is done.
* @example <caption>Ways to `import`.</caption>
* ```js
* import { jsdocMd } from "jsdoc-md";
* ```
*
* ```js
* import jsdocMd from "jsdoc-md/jsdocMd.mjs";
* ```
* @example <caption>Customizing options.</caption>
* ```js
* jsdocMd({
* cwd: "/path/to/project",
* sourceGlob: "index.mjs",
* markdownPath: "README.md",
* targetHeading: "Docs",
* }).then(() => {
* console.log("Done!");
* });
* ```
*/
export default async function jsdocMd({
cwd = process.cwd(),
sourceGlob = "**/*.{mjs,cjs,js}",
markdownPath = "readme.md",
targetHeading = "API",
check = false,
} = {}) {
if (typeof cwd !== "string")
throw new TypeError("Option `cwd` must be a string.");
if (cwd === "")
throw new TypeError("Option `cwd` must be a populated string.");
if (typeof sourceGlob !== "string")
throw new TypeError("Option `sourceGlob` must be a string.");
if (sourceGlob === "")
throw new TypeError("Option `sourceGlob` must be a populated string.");
if (typeof markdownPath !== "string")
throw new TypeError("Option `markdownPath` must be a string.");
if (markdownPath === "")
throw new TypeError("Option `markdownPath` must be a populated string.");
if (typeof targetHeading !== "string")
throw new TypeError("Option `targetHeading` must be a string.");
if (targetHeading === "")
throw new TypeError("Option `targetHeading` must be a populated string.");
if (typeof check !== "boolean")
throw new TypeError("Option `check` must be a boolean.");
const codeFilePaths = await globby(sourceGlob, { cwd, gitignore: true });
const codeFiles = new Map();
const jsdocMembers = [];
await Promise.all(
codeFilePaths.map(async (codeFilePath) => {
// Update the code files map.
codeFiles.set(
codeFilePath,
await fs.promises.readFile(resolve(cwd, codeFilePath), "utf8")
);
// Get the JSDoc comments from the code.
const jsdocComments = await codeToJsdocComments(
codeFiles.get(codeFilePath),
codeFilePath
);
// Get the JSDoc members from the JSDoc comments.
for (const jsdocComment of jsdocComments) {
const jsdocMember = jsdocCommentToMember(
jsdocComment,
codeFiles,
codeFilePath
);
if (jsdocMember) jsdocMembers.push(jsdocMember);
}
})
);
const mdAbsolutePath = resolve(cwd, markdownPath);
const mdOriginal = await fs.promises.readFile(mdAbsolutePath, "utf8");
let mdUpdated = replaceMdSection(
mdOriginal,
targetHeading,
membersToMdAst(jsdocMembers, codeFiles)
);
try {
var { default: prettier } = await import("prettier");
// It would be great if there was a way to test this without Prettier
// installed.
// coverage ignore next line
} catch (error) {
// Ignore the error, as Prettier is an optional peer dependency.
}
if (prettier) {
// Determine if the the markdown file is Prettier ignored.
const { ignored, inferredParser } = await prettier.getFileInfo(
mdAbsolutePath,
{
resolveConfig: true,
ignorePath:
// Prettier CLI doesn’t resolve the ignore file like Git, npm, etc.
// See: https://github.com/prettier/prettier/issues/4081
resolve(cwd, ".prettierignore"),
}
);
if (!ignored) {
// Get the Prettier config for the file.
const prettierConfig = await prettier.resolveConfig(mdAbsolutePath, {
editorconfig: true,
});
// Prettier format the new file contents.
mdUpdated = prettier.format(mdUpdated, {
...prettierConfig,
parser: inferredParser || "markdown",
});
}
}
if (mdOriginal !== mdUpdated)
if (check) throw new CliError("Checked markdown needs updating.");
else await fs.promises.writeFile(mdAbsolutePath, mdUpdated);
}