@versatiles/release-tool 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +24 -0
- package/README.md +93 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +59 -0
- package/dist/lib/command.d.ts +1 -0
- package/dist/lib/command.js +42 -0
- package/dist/lib/markdown.d.ts +2 -0
- package/dist/lib/markdown.js +193 -0
- package/dist/lib/typedoc.d.ts +6 -0
- package/dist/lib/typedoc.js +204 -0
- package/package.json +38 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to [unlicense.org](https://unlicense.org/)
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# VersaTiles Release Tools
|
|
2
|
+
|
|
3
|
+
Tools used internally for:
|
|
4
|
+
|
|
5
|
+
* creating Markdown documentation of TypeScript libraries: [`vrt ts2md`](#subcommand-vrt-ts2md)
|
|
6
|
+
* creating Markdown documentation of executables: [`vrt cmd2md`](#subcommand-vrt-cmd2md)
|
|
7
|
+
* inserting Markdown into documents: [`vrt insertmd`](#subcommand-vrt-insertmd)
|
|
8
|
+
* updating "Table of Content" in Markdown files: [`vrt inserttoc`](#subcommand-vrt-inserttoc)
|
|
9
|
+
|
|
10
|
+
# Command `vrt`
|
|
11
|
+
|
|
12
|
+
<!--- This chapter is generated automatically --->
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
$ vrt
|
|
16
|
+
Usage: vrt [options] [command]
|
|
17
|
+
|
|
18
|
+
versatiles release and documentaion tool
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-h, --help display help for command
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
ts2md <typescript> <tsconfig> documents a TypeScript file and outputs it to stdout
|
|
25
|
+
cmd2md <command> documents a runnable command and outputs it to stdout
|
|
26
|
+
insertmd <readme> [heading] [foldable] takes Markdown from stdin and insert it into a Markdown file
|
|
27
|
+
inserttoc <readme> [heading] updates the TOC in a Markdown file
|
|
28
|
+
help [command] display help for command
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Subcommand: `vrt ts2md`
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
$ vrt ts2md
|
|
35
|
+
Usage: vrt ts2md [options] <typescript> <tsconfig>
|
|
36
|
+
|
|
37
|
+
documents a TypeScript file and outputs it to stdout
|
|
38
|
+
|
|
39
|
+
Arguments:
|
|
40
|
+
typescript Filename of the TypeScript file
|
|
41
|
+
tsconfig Filename of tsconfig.json
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
-h, --help display help for command
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Subcommand: `vrt cmd2md`
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ vrt cmd2md
|
|
51
|
+
Usage: vrt cmd2md [options] <command>
|
|
52
|
+
|
|
53
|
+
documents a runnable command and outputs it to stdout
|
|
54
|
+
|
|
55
|
+
Arguments:
|
|
56
|
+
command command to run
|
|
57
|
+
|
|
58
|
+
Options:
|
|
59
|
+
-h, --help display help for command
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Subcommand: `vrt insertmd`
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
$ vrt insertmd
|
|
66
|
+
Usage: vrt insertmd [options] <readme> [heading] [foldable]
|
|
67
|
+
|
|
68
|
+
takes Markdown from stdin and insert it into a Markdown file
|
|
69
|
+
|
|
70
|
+
Arguments:
|
|
71
|
+
readme Markdown file, like a readme.md
|
|
72
|
+
heading Heading in the Markdown file (default: "# API")
|
|
73
|
+
foldable Make content foldable (default: false)
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
-h, --help display help for command
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Subcommand: `vrt inserttoc`
|
|
80
|
+
|
|
81
|
+
```console
|
|
82
|
+
$ vrt inserttoc
|
|
83
|
+
Usage: vrt inserttoc [options] <readme> [heading]
|
|
84
|
+
|
|
85
|
+
updates the TOC in a Markdown file
|
|
86
|
+
|
|
87
|
+
Arguments:
|
|
88
|
+
readme Markdown file, like a readme.md
|
|
89
|
+
heading Heading in the Markdown file (default: "# Table of Content")
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
-h, --help display help for command
|
|
93
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node --enable-source-maps
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { generateMarkdownDocumentation } from './lib/typedoc.js';
|
|
5
|
+
import { injectMarkdown, updateTOC } from './lib/markdown.js';
|
|
6
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
7
|
+
import { cwd } from 'node:process';
|
|
8
|
+
import { generateCommandDocumentation } from './lib/command.js';
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('vrt')
|
|
12
|
+
.description('versatiles release and documentaion tool');
|
|
13
|
+
program.command('ts2md')
|
|
14
|
+
.description('documents a TypeScript file and outputs it to stdout')
|
|
15
|
+
.argument('<typescript>', 'Filename of the TypeScript file', checkFilename)
|
|
16
|
+
.argument('<tsconfig>', 'Filename of tsconfig.json', checkFilename)
|
|
17
|
+
.action(async (tsFilename, tsConfig) => {
|
|
18
|
+
const mdDocumentation = await generateMarkdownDocumentation([tsFilename], tsConfig);
|
|
19
|
+
process.stdout.write(mdDocumentation);
|
|
20
|
+
});
|
|
21
|
+
program.command('cmd2md')
|
|
22
|
+
.description('documents a runnable command and outputs it to stdout')
|
|
23
|
+
.argument('<command>', 'command to run')
|
|
24
|
+
.action((command) => {
|
|
25
|
+
const mdDocumentation = generateCommandDocumentation(command);
|
|
26
|
+
process.stdout.write(mdDocumentation);
|
|
27
|
+
});
|
|
28
|
+
program.command('insertmd')
|
|
29
|
+
.description('takes Markdown from stdin and insert it into a Markdown file')
|
|
30
|
+
.argument('<readme>', 'Markdown file, like a readme.md', checkFilename)
|
|
31
|
+
.argument('[heading]', 'Heading in the Markdown file', '# API')
|
|
32
|
+
.argument('[foldable]', 'Make content foldable', false)
|
|
33
|
+
.action(async (mdFilename, heading, foldable) => {
|
|
34
|
+
const buffers = [];
|
|
35
|
+
for await (const data of process.stdin)
|
|
36
|
+
buffers.push(data);
|
|
37
|
+
const mdContent = '<!--- This chapter is generated automatically --->\n' + Buffer.concat(buffers).toString();
|
|
38
|
+
let mdFile = readFileSync(mdFilename, 'utf8');
|
|
39
|
+
mdFile = injectMarkdown(mdFile, mdContent, heading, foldable);
|
|
40
|
+
writeFileSync(mdFilename, mdFile);
|
|
41
|
+
});
|
|
42
|
+
program.command('inserttoc')
|
|
43
|
+
.description('updates the TOC in a Markdown file')
|
|
44
|
+
.argument('<readme>', 'Markdown file, like a readme.md', checkFilename)
|
|
45
|
+
.argument('[heading]', 'Heading in the Markdown file', '# Table of Content')
|
|
46
|
+
.action((mdFilename, heading) => {
|
|
47
|
+
let mdFile = readFileSync(mdFilename, 'utf8');
|
|
48
|
+
mdFile = updateTOC(mdFile, heading);
|
|
49
|
+
writeFileSync(mdFilename, mdFile);
|
|
50
|
+
});
|
|
51
|
+
program.parse();
|
|
52
|
+
function checkFilename(filename) {
|
|
53
|
+
const fullname = resolve(cwd(), filename);
|
|
54
|
+
if (!existsSync(fullname)) {
|
|
55
|
+
throw new InvalidArgumentError('file not found');
|
|
56
|
+
}
|
|
57
|
+
return fullname;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateCommandDocumentation(command: string): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
export function generateCommandDocumentation(command) {
|
|
3
|
+
const result = getCommandResults(command);
|
|
4
|
+
let { markdown } = result;
|
|
5
|
+
const { subcommands } = result;
|
|
6
|
+
for (const subcommand of subcommands) {
|
|
7
|
+
const subResult = getCommandResults(command + ' ' + subcommand);
|
|
8
|
+
markdown += `\n# Subcommand: \`${command} ${subcommand}\`\n\n${subResult.markdown}`;
|
|
9
|
+
}
|
|
10
|
+
return markdown;
|
|
11
|
+
}
|
|
12
|
+
function getCommandResults(command) {
|
|
13
|
+
const cp = spawnSync('npx', [...command.split(' '), '--help']);
|
|
14
|
+
if (cp.error)
|
|
15
|
+
throw Error(cp.error.toString());
|
|
16
|
+
const result = cp.stdout.toString().trim();
|
|
17
|
+
const subcommands = result
|
|
18
|
+
.replace(/.*\nCommands:/msgi, '')
|
|
19
|
+
.replace(/\n[a-z]+:.*/msi, '')
|
|
20
|
+
.split('\n')
|
|
21
|
+
.flatMap((line) => {
|
|
22
|
+
const extract = /^ ([^ ]{2,})/.exec(line);
|
|
23
|
+
if (!extract)
|
|
24
|
+
return [];
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/prefer-destructuring
|
|
26
|
+
const subcommand = extract[1];
|
|
27
|
+
if (subcommand === 'help')
|
|
28
|
+
return [];
|
|
29
|
+
return [subcommand];
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
markdown: [
|
|
33
|
+
'```console',
|
|
34
|
+
'$ ' + command,
|
|
35
|
+
result,
|
|
36
|
+
'```',
|
|
37
|
+
'',
|
|
38
|
+
].join('\n'),
|
|
39
|
+
subcommands,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { remark } from 'remark';
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
3
|
+
export function injectMarkdown(document, segment, heading, foldable) {
|
|
4
|
+
const documentAst = remark().parse(document);
|
|
5
|
+
const segmentAst = remark().parse(segment);
|
|
6
|
+
const headingAst = remark().parse(heading);
|
|
7
|
+
let startIndex;
|
|
8
|
+
try {
|
|
9
|
+
startIndex = findSegmentStart(documentAst, headingAst);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error(`While searching for segment "${heading}" …`);
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
const depth = getHeadingDepth(documentAst, startIndex);
|
|
16
|
+
const endIndex = findNextHeading(documentAst, startIndex + 1, depth);
|
|
17
|
+
indentChapter(segmentAst, depth);
|
|
18
|
+
if (foldable ?? false)
|
|
19
|
+
makeFoldable(segmentAst);
|
|
20
|
+
spliceAst(documentAst, segmentAst, startIndex + 1, endIndex);
|
|
21
|
+
return remark().stringify(documentAst);
|
|
22
|
+
}
|
|
23
|
+
export function updateTOC(main, heading) {
|
|
24
|
+
const mainAst = remark().parse(main);
|
|
25
|
+
const headingText = getMDText(remark().parse(heading));
|
|
26
|
+
const toc = mainAst.children
|
|
27
|
+
.flatMap(c => {
|
|
28
|
+
if (c.type !== 'heading')
|
|
29
|
+
return [];
|
|
30
|
+
const text = getMDText(c);
|
|
31
|
+
if (text === headingText)
|
|
32
|
+
return [];
|
|
33
|
+
const indention = ' '.repeat((c.depth - 1) * 2);
|
|
34
|
+
const anchor = getMDAnchor(c);
|
|
35
|
+
return `${indention}* [${text}](#${anchor})\n`;
|
|
36
|
+
})
|
|
37
|
+
.join('');
|
|
38
|
+
return injectMarkdown(main, toc, heading);
|
|
39
|
+
}
|
|
40
|
+
function findSegmentStart(mainAst, headingAst) {
|
|
41
|
+
if (headingAst.children.length !== 1)
|
|
42
|
+
throw Error();
|
|
43
|
+
if (headingAst.children[0].type !== 'heading')
|
|
44
|
+
throw Error();
|
|
45
|
+
const sectionDepth = headingAst.children[0].depth;
|
|
46
|
+
const sectionText = getMDText(headingAst);
|
|
47
|
+
const indexes = mainAst.children.flatMap((c, index) => {
|
|
48
|
+
if ((c.type === 'heading') && (c.depth === sectionDepth) && getMDText(c).startsWith(sectionText)) {
|
|
49
|
+
return [index];
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
});
|
|
53
|
+
if (indexes.length < 1)
|
|
54
|
+
throw Error('section not found');
|
|
55
|
+
if (indexes.length > 1)
|
|
56
|
+
throw Error('too many sections found');
|
|
57
|
+
return indexes[0];
|
|
58
|
+
}
|
|
59
|
+
function findNextHeading(mainAst, startIndex, depth) {
|
|
60
|
+
for (let i = startIndex; i < mainAst.children.length; i++) {
|
|
61
|
+
const child = mainAst.children[i];
|
|
62
|
+
if (child.type !== 'heading')
|
|
63
|
+
continue;
|
|
64
|
+
if (child.depth !== depth)
|
|
65
|
+
continue;
|
|
66
|
+
return i;
|
|
67
|
+
}
|
|
68
|
+
return mainAst.children.length;
|
|
69
|
+
}
|
|
70
|
+
function getHeadingDepth(mainAst, index) {
|
|
71
|
+
const node = mainAst.children[index];
|
|
72
|
+
if (node.type !== 'heading')
|
|
73
|
+
throw Error();
|
|
74
|
+
return node.depth;
|
|
75
|
+
}
|
|
76
|
+
function indentChapter(segmentAst, depth) {
|
|
77
|
+
segmentAst.children.forEach(node => {
|
|
78
|
+
switch (node.type) {
|
|
79
|
+
case 'code':
|
|
80
|
+
case 'html':
|
|
81
|
+
case 'list':
|
|
82
|
+
case 'listItem':
|
|
83
|
+
case 'paragraph':
|
|
84
|
+
case 'text':
|
|
85
|
+
return;
|
|
86
|
+
case 'heading':
|
|
87
|
+
return node.depth += depth;
|
|
88
|
+
default:
|
|
89
|
+
console.log(node);
|
|
90
|
+
throw Error('unknown type: ' + node.type);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
95
|
+
function spliceAst(mainAst, segmentAst, startIndex, endIndex) {
|
|
96
|
+
mainAst.children.splice(startIndex, endIndex - startIndex, ...segmentAst.children);
|
|
97
|
+
}
|
|
98
|
+
function getMDText(node) {
|
|
99
|
+
switch (node.type) {
|
|
100
|
+
case 'inlineCode':
|
|
101
|
+
case 'text':
|
|
102
|
+
return node.value;
|
|
103
|
+
case 'heading':
|
|
104
|
+
case 'root':
|
|
105
|
+
return node.children.map(getMDText).join('');
|
|
106
|
+
case 'html':
|
|
107
|
+
return '';
|
|
108
|
+
default:
|
|
109
|
+
console.log(node);
|
|
110
|
+
throw Error('unknown type: ' + node.type);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function getMDAnchor(node) {
|
|
114
|
+
let text = '';
|
|
115
|
+
for (const c of node.children) {
|
|
116
|
+
switch (c.type) {
|
|
117
|
+
case 'html':
|
|
118
|
+
const match = /<a\s.*id\s*=\s*['"]([^'"]+)/i.exec(c.value);
|
|
119
|
+
if (match)
|
|
120
|
+
return match[1];
|
|
121
|
+
break;
|
|
122
|
+
case 'text':
|
|
123
|
+
case 'inlineCode':
|
|
124
|
+
text += c.value;
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
console.log(c);
|
|
128
|
+
throw Error('unknown type: ' + c.type);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
text = text.toLowerCase()
|
|
132
|
+
.replace(/[()]+/g, '')
|
|
133
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
134
|
+
.replace(/^\-+|\-+$/g, '');
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
137
|
+
function makeFoldable(ast) {
|
|
138
|
+
const openDetails = [];
|
|
139
|
+
const children = [];
|
|
140
|
+
ast.children.forEach((c) => {
|
|
141
|
+
switch (c.type) {
|
|
142
|
+
case 'html':
|
|
143
|
+
case 'list':
|
|
144
|
+
case 'paragraph':
|
|
145
|
+
children.push(c);
|
|
146
|
+
break;
|
|
147
|
+
case 'heading':
|
|
148
|
+
closeDetails(c.depth);
|
|
149
|
+
children.push({ type: 'html', value: '<details>' });
|
|
150
|
+
children.push({ type: 'html', value: `<summary>${lineToHtml(c)}</summary>` });
|
|
151
|
+
openDetails.unshift(c.depth);
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
throw Error(`unknown type "${c.type}"`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
closeDetails(0);
|
|
158
|
+
ast.children = children;
|
|
159
|
+
function closeDetails(depth) {
|
|
160
|
+
while ((openDetails.length > 0) && (openDetails[0] >= depth)) {
|
|
161
|
+
children.push({ type: 'html', value: '</details>' });
|
|
162
|
+
openDetails.shift();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function lineToHtml(child) {
|
|
167
|
+
const html = child.children.map(c => {
|
|
168
|
+
switch (c.type) {
|
|
169
|
+
case 'html': return c.value;
|
|
170
|
+
case 'text': return c.value;
|
|
171
|
+
case 'inlineCode': return `<code>${textToHtml(c.value)}</code>`;
|
|
172
|
+
//case "break": { throw new Error('Not implemented yet: "break" case') }
|
|
173
|
+
//case "delete": { throw new Error('Not implemented yet: "delete" case') }
|
|
174
|
+
//case "emphasis": { throw new Error('Not implemented yet: "emphasis" case') }
|
|
175
|
+
//case "footnoteReference": { throw new Error('Not implemented yet: "footnoteReference" case') }
|
|
176
|
+
//case "image": { throw new Error('Not implemented yet: "image" case') }
|
|
177
|
+
//case "imageReference": { throw new Error('Not implemented yet: "imageReference" case') }
|
|
178
|
+
//case "inlineCode": { throw new Error('Not implemented yet: "inlineCode" case') }
|
|
179
|
+
//case "link": { throw new Error('Not implemented yet: "link" case') }
|
|
180
|
+
//case "linkReference": { throw new Error('Not implemented yet: "linkReference" case') }
|
|
181
|
+
//case "strong": { throw new Error('Not implemented yet: "strong" case') }
|
|
182
|
+
//case "text": { throw new Error('Not implemented yet: "text" case') }
|
|
183
|
+
default:
|
|
184
|
+
console.log(c);
|
|
185
|
+
throw Error(`unknown type "${c.type}"`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
return `<h${child.depth}>${html.join('')}</h${child.depth}>`;
|
|
189
|
+
}
|
|
190
|
+
function textToHtml(text) {
|
|
191
|
+
return text.replace(/[^a-z0-9 ,.-:_?@äöüß]/gi, c => `&#${c.charCodeAt(0)};`);
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate markdown documentation from TypeScript files.
|
|
3
|
+
* @param entryPoints - Array of absolute TypeScript file paths.
|
|
4
|
+
* @param tsconfig - Absolute file path of tsconfig.json.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateMarkdownDocumentation(entryPoints: string[], tsconfig: string): Promise<string>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Application, ReflectionKind, } from 'typedoc';
|
|
2
|
+
/**
|
|
3
|
+
* Generate markdown documentation from TypeScript files.
|
|
4
|
+
* @param entryPoints - Array of absolute TypeScript file paths.
|
|
5
|
+
* @param tsconfig - Absolute file path of tsconfig.json.
|
|
6
|
+
*/
|
|
7
|
+
export async function generateMarkdownDocumentation(entryPoints, tsconfig) {
|
|
8
|
+
const app = await Application.bootstrap({ entryPoints, tsconfig });
|
|
9
|
+
const project = await app.convert();
|
|
10
|
+
if (!project) {
|
|
11
|
+
throw new Error('Failed to convert project.');
|
|
12
|
+
}
|
|
13
|
+
return Array.from(renderProjectDocumentation(project)).join('\n');
|
|
14
|
+
}
|
|
15
|
+
function* renderProjectDocumentation(project) {
|
|
16
|
+
if (!project.groups) {
|
|
17
|
+
throw new Error('No code to document found! Is this a lib?');
|
|
18
|
+
}
|
|
19
|
+
for (const group of project.groups) {
|
|
20
|
+
for (const declaration of group.children) {
|
|
21
|
+
yield* renderDeclaration(declaration);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function* renderDeclaration(declaration) {
|
|
26
|
+
const typeName = formatTypeName(declaration.kind);
|
|
27
|
+
yield `# ${typeName}: \`${declaration.name}\`<a id="${generateAnchor(declaration)}"></a>`;
|
|
28
|
+
yield* renderSummaryBlock(declaration);
|
|
29
|
+
for (const group of declaration.groups ?? []) {
|
|
30
|
+
const publicMembers = group.children.filter(member => !member.flags.isPrivate && !member.flags.isProtected);
|
|
31
|
+
if (publicMembers.length === 0)
|
|
32
|
+
continue;
|
|
33
|
+
// Sort by order in code
|
|
34
|
+
publicMembers.sort((a, b) => a.id - b.id);
|
|
35
|
+
switch (group.title) {
|
|
36
|
+
case 'Constructors':
|
|
37
|
+
if (publicMembers.length !== 1)
|
|
38
|
+
throw Error();
|
|
39
|
+
yield* renderMethod(publicMembers[0], true);
|
|
40
|
+
continue;
|
|
41
|
+
case 'Properties':
|
|
42
|
+
//yield '';
|
|
43
|
+
//yield '**Properties**';
|
|
44
|
+
for (const member of publicMembers)
|
|
45
|
+
yield formatParameter(member);
|
|
46
|
+
continue;
|
|
47
|
+
case 'Methods':
|
|
48
|
+
//yield '';
|
|
49
|
+
//yield '**Methods**';
|
|
50
|
+
for (const member of publicMembers)
|
|
51
|
+
yield* renderMethod(member);
|
|
52
|
+
continue;
|
|
53
|
+
default:
|
|
54
|
+
console.log(group);
|
|
55
|
+
throw Error('Unknown group title');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (declaration.type) {
|
|
59
|
+
yield `\n**Type:** <code>${formatType(declaration.type)}</code>`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function* renderMethod(declaration, isConstructor = false) {
|
|
63
|
+
if (declaration.signatures?.length !== 1)
|
|
64
|
+
throw Error('should be 1');
|
|
65
|
+
const [signature] = declaration.signatures;
|
|
66
|
+
const functionName = signature.name;
|
|
67
|
+
const parameters = formatMethodParameters(signature.parameters ?? []);
|
|
68
|
+
const returnType = signature.type;
|
|
69
|
+
const prefix = isConstructor ? 'Constructor' : 'Method';
|
|
70
|
+
yield `## ${prefix}: \`${functionName}(${parameters})\``;
|
|
71
|
+
yield '';
|
|
72
|
+
yield* renderSummaryBlock(signature);
|
|
73
|
+
if (signature.parameters && signature.parameters.length > 0) {
|
|
74
|
+
yield '';
|
|
75
|
+
yield '**Parameters:**';
|
|
76
|
+
for (const parameter of signature.parameters) {
|
|
77
|
+
yield formatParameter(parameter);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (returnType) {
|
|
81
|
+
yield '';
|
|
82
|
+
yield `**Returns:** <code>${formatType(returnType)}</code>`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function formatMethodParameters(parameters) {
|
|
86
|
+
return parameters.map(param => param.name).join(', ');
|
|
87
|
+
}
|
|
88
|
+
// Helper Functions
|
|
89
|
+
function formatTypeName(kind) {
|
|
90
|
+
switch (kind) {
|
|
91
|
+
case ReflectionKind.Class: return 'Class';
|
|
92
|
+
case ReflectionKind.Interface: return 'Interface';
|
|
93
|
+
case ReflectionKind.TypeAlias: return 'Type';
|
|
94
|
+
default: throw new Error(`Unknown reflection kind: ${kind}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function formatParameter(ref) {
|
|
98
|
+
let line = ` - <code>${ref.name}${resolveTypeDeclaration(ref.type)}</code>`;
|
|
99
|
+
if (ref.flags.isOptional)
|
|
100
|
+
line += ' (optional)';
|
|
101
|
+
const summary = extractSummary(ref.comment);
|
|
102
|
+
if (summary)
|
|
103
|
+
line += ' \n ' + summary;
|
|
104
|
+
return line;
|
|
105
|
+
}
|
|
106
|
+
function extractSummary(comment) {
|
|
107
|
+
if (!comment)
|
|
108
|
+
return '';
|
|
109
|
+
return comment.summary.map(line => line.text).join('');
|
|
110
|
+
}
|
|
111
|
+
function* renderSummaryBlock(ref) {
|
|
112
|
+
yield '';
|
|
113
|
+
if (ref.comment) {
|
|
114
|
+
yield formatComment(ref.comment);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { type } = ref;
|
|
118
|
+
if (type?.type === 'reflection') {
|
|
119
|
+
if (type.declaration.signatures?.length !== 1)
|
|
120
|
+
throw Error();
|
|
121
|
+
const [signature] = type.declaration.signatures;
|
|
122
|
+
if (signature.comment) {
|
|
123
|
+
yield formatComment(signature.comment);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
yield generateSourceLink(ref) + '\n';
|
|
128
|
+
return;
|
|
129
|
+
function formatComment(comment) {
|
|
130
|
+
return (extractSummary(comment) + ' ' + generateSourceLink(ref)).replace(/\n/m, ' \n') + '\n';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function resolveTypeDeclaration(someType) {
|
|
134
|
+
if (!someType)
|
|
135
|
+
return '';
|
|
136
|
+
return `: ${formatType(someType)}`;
|
|
137
|
+
}
|
|
138
|
+
function formatType(someType) {
|
|
139
|
+
return getTypeRec(someType);
|
|
140
|
+
function getTypeRec(some) {
|
|
141
|
+
switch (some.type) {
|
|
142
|
+
case 'intrinsic':
|
|
143
|
+
return some.name;
|
|
144
|
+
case 'literal':
|
|
145
|
+
return JSON.stringify(some.value);
|
|
146
|
+
case 'reference':
|
|
147
|
+
let result = some.name;
|
|
148
|
+
if (some.reflection)
|
|
149
|
+
result = `[${result}](#${generateAnchor(some.reflection)})`;
|
|
150
|
+
if (some.typeArguments?.length ?? 0)
|
|
151
|
+
result += '<'
|
|
152
|
+
+ (some.typeArguments ?? [])
|
|
153
|
+
.map(getTypeRec).join(',')
|
|
154
|
+
+ '>';
|
|
155
|
+
return result;
|
|
156
|
+
case 'reflection':
|
|
157
|
+
if (!some.declaration.signatures)
|
|
158
|
+
throw Error();
|
|
159
|
+
if (some.declaration.signatures.length !== 1)
|
|
160
|
+
throw Error();
|
|
161
|
+
const [signature] = some.declaration.signatures;
|
|
162
|
+
const type = signature.type ? getTypeRec(signature.type) : 'void';
|
|
163
|
+
const parameters = (signature.parameters ?? [])
|
|
164
|
+
.map(p => {
|
|
165
|
+
return p.name + (p.type ? ': ' + getTypeRec(p.type) : '');
|
|
166
|
+
}).join(', ');
|
|
167
|
+
return `(${parameters}) => ${type}`;
|
|
168
|
+
case 'tuple':
|
|
169
|
+
return `[${some.elements.map(getTypeRec).join(', ')}]`;
|
|
170
|
+
case 'union':
|
|
171
|
+
return some.types.map(getTypeRec).join(' | ');
|
|
172
|
+
default:
|
|
173
|
+
console.log(some);
|
|
174
|
+
throw Error();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function generateSourceLink(ref) {
|
|
179
|
+
if (!ref.sources || ref.sources.length < 1)
|
|
180
|
+
return '';
|
|
181
|
+
if (ref.sources.length > 1)
|
|
182
|
+
throw Error();
|
|
183
|
+
const [source] = ref.sources;
|
|
184
|
+
return `<sup><a href="${source.url}">[src]</a></sup>`;
|
|
185
|
+
}
|
|
186
|
+
function generateAnchor(ref) {
|
|
187
|
+
let typeName;
|
|
188
|
+
switch (ref.kind) {
|
|
189
|
+
case ReflectionKind.Class:
|
|
190
|
+
typeName = 'class';
|
|
191
|
+
break;
|
|
192
|
+
case ReflectionKind.Interface:
|
|
193
|
+
typeName = 'interface';
|
|
194
|
+
break;
|
|
195
|
+
case ReflectionKind.TypeAlias:
|
|
196
|
+
typeName = 'type';
|
|
197
|
+
break;
|
|
198
|
+
default:
|
|
199
|
+
console.log(ref);
|
|
200
|
+
throw new Error('Unknown reflection kind');
|
|
201
|
+
}
|
|
202
|
+
return `${typeName}_${ref.name}`.toLowerCase();
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=typedoc.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@versatiles/release-tool",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "VersaTiles release and documentation tools",
|
|
5
|
+
"bin": {
|
|
6
|
+
"vrt": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/**/*.js",
|
|
10
|
+
"dist/**/*.d.ts"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/index.js",
|
|
14
|
+
"check": "npm run lint && npm run test && npm run build",
|
|
15
|
+
"doc": "npx vrt cmd2md vrt | npx vrt insertmd README.md '# Command'",
|
|
16
|
+
"lint": "eslint . --color",
|
|
17
|
+
"prepublish": "npm run build && npm run doc",
|
|
18
|
+
"test": "cd ..; NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern versatiles-release-tool"
|
|
19
|
+
},
|
|
20
|
+
"author": "yetzt <node@yetzt.me>, Michael Kreil <versatiles@michael-kreil.de>",
|
|
21
|
+
"license": "Unlicense",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/versatiles-org/node-versatiles.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/versatiles-org/node-versatiles/blob/main/versatiles-release-tool/README.md",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.9.2",
|
|
30
|
+
"remark": "^15.0.1",
|
|
31
|
+
"tsx": "^4.1.4",
|
|
32
|
+
"typedoc": "^0.25.3",
|
|
33
|
+
"typescript": "^5.2.2"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^11.1.0"
|
|
37
|
+
}
|
|
38
|
+
}
|