@versatiles/release-tool 1.0.1 → 1.0.3

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/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node --enable-source-maps
1
+ #!/usr/bin/env -S node --enable-source-maps
2
2
  export {};
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- #!/usr/bin/env node --enable-source-maps
1
+ #!/usr/bin/env -S node --enable-source-maps
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { generateMarkdownDocumentation } from './lib/typedoc.js';
4
+ import { generateTsMarkdownDoc } from './lib/typedoc.js';
5
5
  import { injectMarkdown, updateTOC } from './lib/markdown.js';
6
6
  import { Command, InvalidArgumentError } from 'commander';
7
7
  import { cwd } from 'node:process';
@@ -15,14 +15,14 @@ program.command('ts2md')
15
15
  .argument('<typescript>', 'Filename of the TypeScript file', checkFilename)
16
16
  .argument('<tsconfig>', 'Filename of tsconfig.json', checkFilename)
17
17
  .action(async (tsFilename, tsConfig) => {
18
- const mdDocumentation = await generateMarkdownDocumentation([tsFilename], tsConfig);
18
+ const mdDocumentation = await generateTsMarkdownDoc([tsFilename], tsConfig);
19
19
  process.stdout.write(mdDocumentation);
20
20
  });
21
21
  program.command('cmd2md')
22
22
  .description('documents a runnable command and outputs it to stdout')
23
23
  .argument('<command>', 'command to run')
24
- .action((command) => {
25
- const mdDocumentation = generateCommandDocumentation(command);
24
+ .action(async (command) => {
25
+ const mdDocumentation = await generateCommandDocumentation(command);
26
26
  process.stdout.write(mdDocumentation);
27
27
  });
28
28
  program.command('insertmd')
@@ -1 +1,6 @@
1
- export declare function generateCommandDocumentation(command: string): string;
1
+ /**
2
+ * Generates documentation for a CLI command and its subcommands.
3
+ * @param command The base CLI command to document.
4
+ * @returns A Promise resolving to a string containing the generated Markdown documentation.
5
+ */
6
+ export declare function generateCommandDocumentation(command: string): Promise<string>;
@@ -1,42 +1,83 @@
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
- }
1
+ import cp from 'child_process';
2
+ import { getErrorMessage } from './utils.js';
3
+ /**
4
+ * Generates documentation for a CLI command and its subcommands.
5
+ * @param command The base CLI command to document.
6
+ * @returns A Promise resolving to a string containing the generated Markdown documentation.
7
+ */
8
+ export async function generateCommandDocumentation(command) {
9
+ // Get the base command's documentation and list of subcommands.
10
+ // eslint-disable-next-line prefer-const
11
+ let { markdown, subcommands } = await getCommandResults(command);
12
+ // Iterate over each subcommand to generate its documentation.
13
+ markdown += (await Promise.all(subcommands.map(async (subcommand) => {
14
+ const fullCommand = `${command} ${subcommand}`;
15
+ try {
16
+ // Get documentation for each subcommand.
17
+ const { markdown: subcommandMarkdown } = await getCommandResults(fullCommand);
18
+ return `\n# Subcommand: \`${fullCommand}\`\n\n${subcommandMarkdown}`;
19
+ }
20
+ catch (error) {
21
+ // Handle errors in generating subcommand documentation.
22
+ throw new Error(`Error generating documentation for subcommand '${fullCommand}': ${getErrorMessage(error)}`);
23
+ }
24
+ }))).join('');
10
25
  return markdown;
11
26
  }
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')
27
+ /**
28
+ * Executes a CLI command with the '--help' flag and parses the output.
29
+ * @param command The CLI command to execute.
30
+ * @returns A Promise resolving to an object containing the Markdown documentation and a list of subcommands.
31
+ */
32
+ async function getCommandResults(command) {
33
+ return new Promise((resolve, reject) => {
34
+ // Spawn a child process to run the command with the '--help' flag.
35
+ const childProcess = cp.spawn('npx', [...command.split(' '), '--help']);
36
+ let output = '';
37
+ // Collect output data from the process.
38
+ childProcess.stdout.on('data', data => output += String(data));
39
+ childProcess.stderr.on('data', data => {
40
+ console.error(`stderr: ${data}`);
41
+ });
42
+ // Handle process errors.
43
+ childProcess.on('error', error => {
44
+ reject(new Error(`Failed to start subprocess: ${error.message}`));
45
+ });
46
+ // Handle process exit.
47
+ childProcess.on('close', (code) => {
48
+ if (code !== 0) {
49
+ reject(new Error(`Command failed with exit code ${code}`));
50
+ return;
51
+ }
52
+ const result = output.trim();
53
+ // Resolve with the formatted output and a list of subcommands.
54
+ resolve({
55
+ markdown: `\`\`\`console\n$ ${command}\n${result}\n\`\`\`\n`,
56
+ subcommands: extractSubcommands(result),
57
+ });
58
+ });
59
+ });
60
+ }
61
+ /**
62
+ * Extracts a list of subcommands from the help output of a command.
63
+ * @param result The string output from a command's help flag.
64
+ * @returns An array of subcommand names.
65
+ */
66
+ function extractSubcommands(result) {
67
+ return result
68
+ .replace(/.*\nCommands:/msgi, '') // Remove everything before the "Commands:" section.
69
+ .replace(/\n[a-z]+:.*/msi, '') // Remove everything after the subcommands list.
70
+ .split('\n') // Split by newline to process each line.
21
71
  .flatMap((line) => {
72
+ // Extract subcommand names from each line.
22
73
  const extract = /^ ([^ ]{2,})/.exec(line);
23
74
  if (!extract)
24
75
  return [];
25
- // eslint-disable-next-line @typescript-eslint/prefer-destructuring
26
- const subcommand = extract[1];
76
+ const [, subcommand] = extract;
77
+ // Ignore the 'help' subcommand.
27
78
  if (subcommand === 'help')
28
79
  return [];
29
80
  return [subcommand];
30
81
  });
31
- return {
32
- markdown: [
33
- '```console',
34
- '$ ' + command,
35
- result,
36
- '```',
37
- '',
38
- ].join('\n'),
39
- subcommands,
40
- };
41
82
  }
42
83
  //# sourceMappingURL=command.js.map
@@ -1,2 +1,21 @@
1
+ import type { Root, PhrasingContent } from 'mdast';
2
+ /**
3
+ * Injects a Markdown segment under a specified heading in a Markdown document.
4
+ * Optionally, the injected segment can be made foldable for better readability.
5
+ * @param document The original Markdown document.
6
+ * @param segment The Markdown segment to be injected.
7
+ * @param heading The heading under which the segment is to be injected.
8
+ * @param foldable If true, makes the segment foldable.
9
+ * @returns The modified Markdown document.
10
+ */
1
11
  export declare function injectMarkdown(document: string, segment: string, heading: string, foldable?: boolean): string;
12
+ /**
13
+ * Updates the Table of Contents (TOC) of a Markdown document.
14
+ * The TOC is rebuilt based on the headings present in the document.
15
+ * @param main The main Markdown document.
16
+ * @param heading The heading under which the TOC is to be updated.
17
+ * @returns The Markdown document with an updated TOC.
18
+ */
2
19
  export declare function updateTOC(main: string, heading: string): string;
20
+ export declare function nodeToHtml(node: PhrasingContent): string;
21
+ export declare function parseMarkdown(document: string): Root;
@@ -1,79 +1,139 @@
1
1
  import { remark } from 'remark';
2
+ import remarkGfm from 'remark-gfm';
3
+ import { getErrorMessage } from './utils.js';
4
+ /**
5
+ * Injects a Markdown segment under a specified heading in a Markdown document.
6
+ * Optionally, the injected segment can be made foldable for better readability.
7
+ * @param document The original Markdown document.
8
+ * @param segment The Markdown segment to be injected.
9
+ * @param heading The heading under which the segment is to be injected.
10
+ * @param foldable If true, makes the segment foldable.
11
+ * @returns The modified Markdown document.
12
+ */
2
13
  // eslint-disable-next-line @typescript-eslint/max-params
3
14
  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);
15
+ // Parse the input strings into Abstract Syntax Trees (ASTs).
16
+ const documentAst = parseMarkdown(document);
17
+ const segmentAst = parseMarkdown(segment);
18
+ const headingAst = parseMarkdown(heading);
19
+ // Initialize the start index of the injection.
7
20
  let startIndex;
8
21
  try {
9
- startIndex = findSegmentStart(documentAst, headingAst);
22
+ // Find the start index where the new segment should be injected.
23
+ startIndex = findSegmentStartIndex(documentAst, headingAst);
10
24
  }
11
25
  catch (error) {
12
- console.error(`While searching for segment "${heading}" …`);
26
+ // Handle errors during the search for the start index.
27
+ console.error(`Error while searching for segment "${heading}": ${getErrorMessage(error)}`);
13
28
  throw error;
14
29
  }
30
+ // Get the depth of the specified heading to maintain the structure.
15
31
  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);
32
+ // Find the index of the next heading of the same depth to define the end of the segment.
33
+ const endIndex = findNextHeadingIndex(documentAst, startIndex + 1, depth);
34
+ // Adjust the indentation of the segment to align with the specified depth.
35
+ indentSegmentToDepth(segmentAst, depth);
36
+ // Convert the segment to a foldable section if required.
37
+ if (foldable === true)
38
+ convertToFoldable(segmentAst);
39
+ // Merge the original document AST with the new segment AST.
40
+ mergeSegments(documentAst, segmentAst, startIndex + 1, endIndex);
41
+ // Convert the modified AST back to a Markdown string and return.
21
42
  return remark().stringify(documentAst);
22
43
  }
44
+ /**
45
+ * Updates the Table of Contents (TOC) of a Markdown document.
46
+ * The TOC is rebuilt based on the headings present in the document.
47
+ * @param main The main Markdown document.
48
+ * @param heading The heading under which the TOC is to be updated.
49
+ * @returns The Markdown document with an updated TOC.
50
+ */
23
51
  export function updateTOC(main, heading) {
24
- const mainAst = remark().parse(main);
25
- const headingText = getMDText(remark().parse(heading));
52
+ // Parse the main document and the heading for the TOC.
53
+ const mainAst = parseMarkdown(main);
54
+ const headingText = extractTextFromMDAsHTML(parseMarkdown(heading));
55
+ // Build the TOC by iterating over each heading in the document.
26
56
  const toc = mainAst.children
27
57
  .flatMap(c => {
28
- if (c.type !== 'heading')
29
- return [];
30
- const text = getMDText(c);
31
- if (text === headingText)
58
+ // Skip non-heading nodes and the specified heading.
59
+ if (c.type !== 'heading' || extractTextFromMDAsHTML(c) === headingText)
32
60
  return [];
61
+ // Format each heading as a TOC entry.
33
62
  const indention = ' '.repeat((c.depth - 1) * 2);
34
63
  const anchor = getMDAnchor(c);
35
- return `${indention}* [${text}](#${anchor})\n`;
64
+ return `${indention}* [${extractTextFromMDAsHTML(c)}](#${anchor})\n`;
36
65
  })
37
66
  .join('');
67
+ // Inject the newly generated TOC under the specified heading in the document.
38
68
  return injectMarkdown(main, toc, heading);
39
69
  }
40
- function findSegmentStart(mainAst, headingAst) {
70
+ // Below are the helper functions used in the above main functions. Each helper function
71
+ // is designed for a specific operation and is provided with detailed comments for clarity.
72
+ /**
73
+ * Finds the start index of the segment under a specific heading in the AST.
74
+ * @param mainAst The AST of the main document.
75
+ * @param headingAst The AST of the heading under which the segment is to be inserted.
76
+ * @returns The start index of the segment in the main document AST.
77
+ * @throws Error if headingAst does not have exactly one child or the child is not a heading.
78
+ */
79
+ function findSegmentStartIndex(mainAst, headingAst) {
80
+ // Verify the structure of the headingAst.
41
81
  if (headingAst.children.length !== 1)
42
- throw Error();
82
+ throw Error('headingAst.children.length !== 1');
43
83
  if (headingAst.children[0].type !== 'heading')
44
- throw Error();
84
+ throw Error('headingAst.children[0].type !== \'heading\'');
45
85
  const sectionDepth = headingAst.children[0].depth;
46
- const sectionText = getMDText(headingAst);
86
+ const sectionText = extractTextFromMDAsHTML(headingAst);
87
+ // Search for the index of the heading in the main document AST.
47
88
  const indexes = mainAst.children.flatMap((c, index) => {
48
- if ((c.type === 'heading') && (c.depth === sectionDepth) && getMDText(c).startsWith(sectionText)) {
89
+ if ((c.type === 'heading') && (c.depth === sectionDepth) && extractTextFromMDAsHTML(c).startsWith(sectionText)) {
49
90
  return [index];
50
91
  }
51
92
  return [];
52
93
  });
94
+ // Handle the cases of no match or multiple matches.
53
95
  if (indexes.length < 1)
54
96
  throw Error('section not found');
55
97
  if (indexes.length > 1)
56
98
  throw Error('too many sections found');
57
99
  return indexes[0];
58
100
  }
59
- function findNextHeading(mainAst, startIndex, depth) {
101
+ /**
102
+ * Finds the index of the next heading at the same depth in the AST.
103
+ * @param mainAst The AST of the main document.
104
+ * @param startIndex The index to start searching from.
105
+ * @param depth The depth of the headings to match.
106
+ * @returns The index of the next heading of the same depth, or the length of the children array if none is found.
107
+ */
108
+ function findNextHeadingIndex(mainAst, startIndex, depth) {
109
+ // Iterate over the AST nodes starting from startIndex.
60
110
  for (let i = startIndex; i < mainAst.children.length; i++) {
61
111
  const child = mainAst.children[i];
62
- if (child.type !== 'heading')
63
- continue;
64
- if (child.depth !== depth)
65
- continue;
66
- return i;
112
+ // Return the index of the next heading at the same depth.
113
+ if (child.type === 'heading' && child.depth === depth)
114
+ return i;
67
115
  }
68
116
  return mainAst.children.length;
69
117
  }
118
+ /**
119
+ * Gets the depth of the heading at a given index in the AST.
120
+ * @param mainAst The AST of the main document.
121
+ * @param index The index of the heading.
122
+ * @returns The depth of the heading.
123
+ * @throws Error if the node at the index is not a heading.
124
+ */
70
125
  function getHeadingDepth(mainAst, index) {
71
126
  const node = mainAst.children[index];
72
127
  if (node.type !== 'heading')
73
- throw Error();
128
+ throw Error('node.type !== \'heading\'');
74
129
  return node.depth;
75
130
  }
76
- function indentChapter(segmentAst, depth) {
131
+ /**
132
+ * Indents each segment in the AST to match a specified depth, modifying headings accordingly.
133
+ * @param segmentAst The AST of the segment to be indented.
134
+ * @param depth The depth to which the segment should be indented.
135
+ */
136
+ function indentSegmentToDepth(segmentAst, depth) {
77
137
  segmentAst.children.forEach(node => {
78
138
  switch (node.type) {
79
139
  case 'code':
@@ -91,18 +151,31 @@ function indentChapter(segmentAst, depth) {
91
151
  }
92
152
  });
93
153
  }
154
+ /**
155
+ * Merges a segment AST into the main document AST at specified start and end indexes.
156
+ * @param mainAst The AST of the main document.
157
+ * @param segmentAst The AST of the segment to be merged.
158
+ * @param startIndex The start index in the main AST.
159
+ * @param endIndex The end index in the main AST.
160
+ */
94
161
  // eslint-disable-next-line @typescript-eslint/max-params
95
- function spliceAst(mainAst, segmentAst, startIndex, endIndex) {
162
+ function mergeSegments(mainAst, segmentAst, startIndex, endIndex) {
96
163
  mainAst.children.splice(startIndex, endIndex - startIndex, ...segmentAst.children);
97
164
  }
98
- function getMDText(node) {
165
+ /**
166
+ * Extracts the textual content from a node in the AST.
167
+ * @param node The AST node.
168
+ * @returns The extracted text content.
169
+ * @throws Error if the node type is unknown.
170
+ */
171
+ function extractTextFromMDAsHTML(node) {
99
172
  switch (node.type) {
100
173
  case 'inlineCode':
101
174
  case 'text':
102
- return node.value;
175
+ return textToHtml(node.value);
103
176
  case 'heading':
104
177
  case 'root':
105
- return node.children.map(getMDText).join('');
178
+ return node.children.map(extractTextFromMDAsHTML).join('');
106
179
  case 'html':
107
180
  return '';
108
181
  default:
@@ -110,9 +183,16 @@ function getMDText(node) {
110
183
  throw Error('unknown type: ' + node.type);
111
184
  }
112
185
  }
186
+ /**
187
+ * Generates an anchor ID for a Markdown heading based on its text content.
188
+ * @param node The heading node.
189
+ * @returns The generated anchor ID.
190
+ * @throws Error if the child node type is unknown.
191
+ */
113
192
  function getMDAnchor(node) {
114
193
  let text = '';
115
194
  for (const c of node.children) {
195
+ // Handle different types of child nodes to construct the anchor text.
116
196
  switch (c.type) {
117
197
  case 'html':
118
198
  const match = /<a\s.*id\s*=\s*['"]([^'"]+)/i.exec(c.value);
@@ -128,13 +208,18 @@ function getMDAnchor(node) {
128
208
  throw Error('unknown type: ' + c.type);
129
209
  }
130
210
  }
211
+ // Format the text to create a suitable anchor ID.
131
212
  text = text.toLowerCase()
132
213
  .replace(/[()]+/g, '')
133
214
  .replace(/[^a-z0-9]+/g, '-')
134
215
  .replace(/^\-+|\-+$/g, '');
135
216
  return text;
136
217
  }
137
- function makeFoldable(ast) {
218
+ /**
219
+ * Converts a segment of the AST into a foldable HTML element.
220
+ * @param ast The AST of the segment to be converted.
221
+ */
222
+ function convertToFoldable(ast) {
138
223
  const openDetails = [];
139
224
  const children = [];
140
225
  ast.children.forEach((c) => {
@@ -163,31 +248,52 @@ function makeFoldable(ast) {
163
248
  }
164
249
  }
165
250
  }
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}>`;
251
+ function lineToHtml(heading) {
252
+ return `<h${heading.depth}>${nodesToHtml(heading.children)}</h${heading.depth}>`;
253
+ }
254
+ export function nodeToHtml(node) {
255
+ switch (node.type) {
256
+ case 'html':
257
+ return node.value;
258
+ case 'text':
259
+ return textToHtml(node.value);
260
+ case 'inlineCode':
261
+ return `<code>${textToHtml(node.value)}</code>`;
262
+ case 'break':
263
+ return '<br />';
264
+ case 'delete':
265
+ return `<del>${nodesToHtml(node.children)}</del>`;
266
+ case 'emphasis':
267
+ return `<em>${nodesToHtml(node.children)}</em>`;
268
+ case 'strong':
269
+ return `<strong>${nodesToHtml(node.children)}</strong>`;
270
+ case 'link':
271
+ return `<a href="${node.url}"${node.title == null ? '' : ` title="${node.title}"`}>${nodesToHtml(node.children)}</a>`;
272
+ case 'linkReference':
273
+ throw new Error('"linkReference to html" not implemented');
274
+ case 'footnoteReference':
275
+ throw new Error('"footnoteReference to html" not implemented');
276
+ case 'image':
277
+ const attributes = [`src="${node.url}"`];
278
+ if (node.alt ?? '')
279
+ attributes.push(`alt="${node.alt}"`);
280
+ if (node.title ?? '')
281
+ attributes.push(`title="${node.title}"`);
282
+ return `<img ${attributes.join(' ')} />`;
283
+ case 'imageReference':
284
+ throw new Error('"imageReference to html" not implemented');
285
+ default:
286
+ console.log(node);
287
+ throw Error('unknown type');
288
+ }
289
+ }
290
+ function nodesToHtml(children) {
291
+ return children.map(nodeToHtml).join('');
189
292
  }
190
293
  function textToHtml(text) {
191
294
  return text.replace(/[^a-z0-9 ,.-:_?@äöüß]/gi, c => `&#${c.charCodeAt(0)};`);
192
295
  }
296
+ export function parseMarkdown(document) {
297
+ return remark().use(remarkGfm).parse(document);
298
+ }
193
299
  //# sourceMappingURL=markdown.js.map
@@ -1,6 +1,6 @@
1
1
  /**
2
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.
3
+ * @param sourceFilePaths - Array of absolute TypeScript file paths.
4
+ * @param tsConfigPath - Absolute file path of tsconfig.json.
5
5
  */
6
- export declare function generateMarkdownDocumentation(entryPoints: string[], tsconfig: string): Promise<string>;
6
+ export declare function generateTsMarkdownDoc(sourceFilePaths: string[], tsConfigPath: string): Promise<string>;
@@ -1,31 +1,70 @@
1
1
  import { Application, ReflectionKind, } from 'typedoc';
2
2
  /**
3
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.
4
+ * @param sourceFilePaths - Array of absolute TypeScript file paths.
5
+ * @param tsConfigPath - Absolute file path of tsconfig.json.
6
6
  */
7
- export async function generateMarkdownDocumentation(entryPoints, tsconfig) {
8
- const app = await Application.bootstrap({ entryPoints, tsconfig });
7
+ export async function generateTsMarkdownDoc(sourceFilePaths, tsConfigPath) {
8
+ const app = await Application.bootstrap({ entryPoints: sourceFilePaths, tsconfig: tsConfigPath });
9
9
  const project = await app.convert();
10
10
  if (!project) {
11
- throw new Error('Failed to convert project.');
11
+ throw new Error('Failed to convert TypeScript project.');
12
12
  }
13
- return Array.from(renderProjectDocumentation(project)).join('\n');
13
+ return Array.from(documentProject(project)).join('\n');
14
14
  }
15
- function* renderProjectDocumentation(project) {
15
+ function* documentProject(project) {
16
16
  if (!project.groups) {
17
- throw new Error('No code to document found! Is this a lib?');
17
+ throw new Error('No TypeScript code to document found! Is this a lib?');
18
18
  }
19
19
  for (const group of project.groups) {
20
+ yield '\n# ' + group.title;
20
21
  for (const declaration of group.children) {
21
- yield* renderDeclaration(declaration);
22
+ switch (declaration.kind) {
23
+ case ReflectionKind.Class:
24
+ yield* documentClass(declaration);
25
+ break;
26
+ case ReflectionKind.Function:
27
+ yield* documentMethod(declaration, 2);
28
+ break;
29
+ case ReflectionKind.Interface:
30
+ yield* documentInterface(declaration);
31
+ break;
32
+ case ReflectionKind.TypeAlias:
33
+ yield* documentType(declaration);
34
+ break;
35
+ case ReflectionKind.Variable:
36
+ yield* documentVariable(declaration);
37
+ break;
38
+ default:
39
+ throw new Error('implement ' + declaration.kind);
40
+ }
22
41
  }
23
42
  }
24
43
  }
25
- function* renderDeclaration(declaration) {
26
- const typeName = formatTypeName(declaration.kind);
27
- yield `# ${typeName}: \`${declaration.name}\`<a id="${generateAnchor(declaration)}"></a>`;
28
- yield* renderSummaryBlock(declaration);
44
+ function* documentInterface(declaration) {
45
+ yield `\n## Interface: \`${declaration.name}\`<a id="${createAnchorId(declaration)}"></a>`;
46
+ yield '\n```typescript';
47
+ yield 'interface {';
48
+ for (const child of declaration.children ?? []) {
49
+ if (child.kind !== ReflectionKind.Property)
50
+ throw Error('should be a property inside an interface');
51
+ if (child.type == null)
52
+ throw Error('should have a type');
53
+ const name = child.name + (child.flags.isOptional ? '?' : '');
54
+ yield ` ${name}: ${formatTypeDeclaration(child.type)};`;
55
+ }
56
+ yield '}';
57
+ yield '```';
58
+ }
59
+ function* documentType(declaration) {
60
+ yield `\n## Type: \`${declaration.name}\`<a id="${createAnchorId(declaration)}"></a>`;
61
+ if (declaration.type) {
62
+ yield `\n**Type:** <code>${formatTypeDeclaration(declaration.type)}</code>`;
63
+ }
64
+ }
65
+ function* documentClass(declaration) {
66
+ yield `\n## Class: \`${declaration.name}\`<a id="${createAnchorId(declaration)}"></a>`;
67
+ yield* documentSummaryBlock(declaration);
29
68
  for (const group of declaration.groups ?? []) {
30
69
  const publicMembers = group.children.filter(member => !member.flags.isPrivate && !member.flags.isProtected);
31
70
  if (publicMembers.length === 0)
@@ -35,80 +74,93 @@ function* renderDeclaration(declaration) {
35
74
  switch (group.title) {
36
75
  case 'Constructors':
37
76
  if (publicMembers.length !== 1)
38
- throw Error();
39
- yield* renderMethod(publicMembers[0], true);
77
+ throw Error('publicMembers.length !== 1');
78
+ yield* documentMethod(publicMembers[0], 3, true);
79
+ continue;
80
+ case 'Accessors':
81
+ yield '\n### Accessors';
82
+ for (const member of publicMembers)
83
+ yield documentAccessor(member);
40
84
  continue;
41
85
  case 'Properties':
42
- //yield '';
43
- //yield '**Properties**';
86
+ yield '\n### Properties';
44
87
  for (const member of publicMembers)
45
- yield formatParameter(member);
88
+ yield documentProperty(member);
46
89
  continue;
47
90
  case 'Methods':
48
- //yield '';
49
- //yield '**Methods**';
50
91
  for (const member of publicMembers)
51
- yield* renderMethod(member);
92
+ yield* documentMethod(member, 3);
52
93
  continue;
53
94
  default:
54
95
  console.log(group);
55
96
  throw Error('Unknown group title');
56
97
  }
57
98
  }
58
- if (declaration.type) {
59
- yield `\n**Type:** <code>${formatType(declaration.type)}</code>`;
60
- }
61
99
  }
62
- function* renderMethod(declaration, isConstructor = false) {
63
- if (declaration.signatures?.length !== 1)
100
+ function* documentMethod(method, depth, isConstructor = false) {
101
+ if (method.signatures?.length !== 1)
64
102
  throw Error('should be 1');
65
- const [signature] = declaration.signatures;
66
- const functionName = signature.name;
103
+ const [signature] = method.signatures;
104
+ const methodName = signature.name;
67
105
  const parameters = formatMethodParameters(signature.parameters ?? []);
68
106
  const returnType = signature.type;
69
- const prefix = isConstructor ? 'Constructor' : 'Method';
70
- yield `## ${prefix}: \`${functionName}(${parameters})\``;
71
- yield '';
72
- yield* renderSummaryBlock(signature);
107
+ const methodType = isConstructor ? 'Constructor' : 'Method';
108
+ yield `\n${'#'.repeat(depth)} ${methodType}: \`${methodName}(${parameters})\``;
109
+ yield* documentSummaryBlock(signature);
73
110
  if (signature.parameters && signature.parameters.length > 0) {
74
111
  yield '';
75
112
  yield '**Parameters:**';
76
113
  for (const parameter of signature.parameters) {
77
- yield formatParameter(parameter);
114
+ yield documentProperty(parameter);
78
115
  }
79
116
  }
80
- if (returnType) {
81
- yield '';
82
- yield `**Returns:** <code>${formatType(returnType)}</code>`;
117
+ if (returnType && !isConstructor) {
118
+ yield `\n**Returns:** <code>${formatTypeDeclaration(returnType)}</code>`;
83
119
  }
84
120
  }
85
121
  function formatMethodParameters(parameters) {
86
122
  return parameters.map(param => param.name).join(', ');
87
123
  }
88
124
  // Helper Functions
89
- function formatTypeName(kind) {
125
+ function getDeclarationKindName(kind) {
90
126
  switch (kind) {
91
127
  case ReflectionKind.Class: return 'Class';
128
+ case ReflectionKind.Function: return 'Function';
92
129
  case ReflectionKind.Interface: return 'Interface';
93
130
  case ReflectionKind.TypeAlias: return 'Type';
131
+ case ReflectionKind.Variable: return 'Variable';
94
132
  default: throw new Error(`Unknown reflection kind: ${kind}`);
95
133
  }
96
134
  }
97
- function formatParameter(ref) {
135
+ function documentProperty(ref) {
98
136
  let line = ` - <code>${ref.name}${resolveTypeDeclaration(ref.type)}</code>`;
99
137
  if (ref.flags.isOptional)
100
138
  line += ' (optional)';
101
139
  const summary = extractSummary(ref.comment);
102
- if (summary)
140
+ if (summary != null)
141
+ line += ' \n ' + summary;
142
+ return line;
143
+ }
144
+ function* documentVariable(ref) {
145
+ const prefix = ref.flags.isConst ? 'const' : 'let';
146
+ yield `\n## \`${prefix} ${ref.name}\``;
147
+ const summary = extractSummary(ref.comment);
148
+ if (summary != null)
149
+ yield summary;
150
+ }
151
+ function documentAccessor(ref) {
152
+ let line = ` - <code>${ref.name}${resolveTypeDeclaration(ref.type)}</code>`;
153
+ const summary = extractSummary(ref.comment);
154
+ if (summary != null)
103
155
  line += ' \n ' + summary;
104
156
  return line;
105
157
  }
106
158
  function extractSummary(comment) {
107
159
  if (!comment)
108
- return '';
160
+ return null;
109
161
  return comment.summary.map(line => line.text).join('');
110
162
  }
111
- function* renderSummaryBlock(ref) {
163
+ function* documentSummaryBlock(ref) {
112
164
  yield '';
113
165
  if (ref.comment) {
114
166
  yield formatComment(ref.comment);
@@ -117,25 +169,31 @@ function* renderSummaryBlock(ref) {
117
169
  const { type } = ref;
118
170
  if (type?.type === 'reflection') {
119
171
  if (type.declaration.signatures?.length !== 1)
120
- throw Error();
172
+ throw Error('type.declaration.signatures?.length !== 1');
121
173
  const [signature] = type.declaration.signatures;
122
174
  if (signature.comment) {
123
175
  yield formatComment(signature.comment);
124
176
  return;
125
177
  }
126
178
  }
127
- yield generateSourceLink(ref) + '\n';
179
+ const sourceLink = createSourceLink(ref);
180
+ if (sourceLink != null)
181
+ yield sourceLink;
128
182
  return;
129
183
  function formatComment(comment) {
130
- return (extractSummary(comment) + ' ' + generateSourceLink(ref)).replace(/\n/m, ' \n') + '\n';
184
+ let summary = extractSummary(comment) ?? '';
185
+ const link = createSourceLink(ref);
186
+ if (link != null)
187
+ summary += ' ' + link;
188
+ return summary.replace(/\n/m, ' \n') + '\n';
131
189
  }
132
190
  }
133
191
  function resolveTypeDeclaration(someType) {
134
192
  if (!someType)
135
193
  return '';
136
- return `: ${formatType(someType)}`;
194
+ return `: ${formatTypeDeclaration(someType)}`;
137
195
  }
138
- function formatType(someType) {
196
+ function formatTypeDeclaration(someType) {
139
197
  return getTypeRec(someType);
140
198
  function getTypeRec(some) {
141
199
  switch (some.type) {
@@ -146,7 +204,7 @@ function formatType(someType) {
146
204
  case 'reference':
147
205
  let result = some.name;
148
206
  if (some.reflection)
149
- result = `[${result}](#${generateAnchor(some.reflection)})`;
207
+ result = `[${result}](#${createAnchorId(some.reflection)})`;
150
208
  if (some.typeArguments?.length ?? 0)
151
209
  result += '<'
152
210
  + (some.typeArguments ?? [])
@@ -154,51 +212,67 @@ function formatType(someType) {
154
212
  + '>';
155
213
  return result;
156
214
  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}`;
215
+ switch (some.declaration.kind) {
216
+ case ReflectionKind.TypeLiteral: return decodeReflectionTypeLiteral(some.declaration);
217
+ default:
218
+ console.log('declarationKindName', getDeclarationKindName(some.declaration.kind));
219
+ console.dir(some, { depth: 4 });
220
+ throw Error();
221
+ }
168
222
  case 'tuple':
169
223
  return `[${some.elements.map(getTypeRec).join(', ')}]`;
170
224
  case 'union':
171
225
  return some.types.map(getTypeRec).join(' | ');
226
+ case 'array':
227
+ return getTypeRec(some.elementType) + '[]';
172
228
  default:
173
229
  console.log(some);
230
+ throw Error(some.type);
231
+ }
232
+ function decodeReflectionTypeLiteral(ref) {
233
+ try {
234
+ if (ref.variant !== 'declaration')
235
+ throw Error();
236
+ if (ref.groups && !ref.signatures) {
237
+ if (!Array.isArray(ref.groups))
238
+ throw Error();
239
+ if (ref.groups.length !== 1)
240
+ throw Error();
241
+ const [group] = ref.groups;
242
+ if (group.title !== 'Properties')
243
+ throw Error();
244
+ const properties = group.children.map(r => r.escapedName + ':?');
245
+ return `{${properties.join(', ')}}`;
246
+ }
247
+ if (!ref.groups && ref.signatures) {
248
+ if (ref.signatures.length !== 1)
249
+ throw Error('ref.signatures.length !== 1');
250
+ const [signature] = ref.signatures;
251
+ const returnType = signature.type ? getTypeRec(signature.type) : 'void';
252
+ const parameters = (signature.parameters ?? [])
253
+ .map(p => {
254
+ return p.name + (p.type ? ': ' + getTypeRec(p.type) : '');
255
+ }).join(', ');
256
+ return `(${parameters}) => ${returnType}`;
257
+ }
174
258
  throw Error();
259
+ }
260
+ catch (error) {
261
+ console.dir(ref, { depth: 3 });
262
+ throw error;
263
+ }
175
264
  }
176
265
  }
177
266
  }
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;
267
+ function createSourceLink(reference) {
268
+ if (!reference.sources || reference.sources.length < 1)
269
+ return null;
270
+ if (reference.sources.length > 1)
271
+ throw Error('ref.sources.length > 1');
272
+ const [source] = reference.sources;
184
273
  return `<sup><a href="${source.url}">[src]</a></sup>`;
185
274
  }
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();
275
+ function createAnchorId(reference) {
276
+ return `${getDeclarationKindName(reference.kind)}_${reference.name}`.toLowerCase();
203
277
  }
204
278
  //# sourceMappingURL=typedoc.js.map
@@ -0,0 +1 @@
1
+ export declare function getErrorMessage(error: unknown): string;
@@ -0,0 +1,10 @@
1
+ export function getErrorMessage(error) {
2
+ if (error == null)
3
+ return 'unknown';
4
+ if (typeof error === 'object') {
5
+ if ('message' in error)
6
+ return String(error.message);
7
+ }
8
+ return 'unknown';
9
+ }
10
+ //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versatiles/release-tool",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "VersaTiles release and documentation tools",
5
5
  "bin": {
6
6
  "vrt": "./dist/index.js"
@@ -14,7 +14,7 @@
14
14
  "check": "npm run lint && npm run test && npm run build",
15
15
  "doc": "npx vrt cmd2md vrt | npx vrt insertmd README.md '# Command'",
16
16
  "lint": "eslint . --color",
17
- "prepublish": "npm run build && npm run doc",
17
+ "prepack": "npm run build && npm run doc",
18
18
  "test": "cd ..; NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern versatiles-release-tool"
19
19
  },
20
20
  "author": "yetzt <node@yetzt.me>, Michael Kreil <versatiles@michael-kreil.de>",
@@ -26,13 +26,14 @@
26
26
  },
27
27
  "homepage": "https://github.com/versatiles-org/node-versatiles/blob/main/versatiles-release-tool/README.md",
28
28
  "devDependencies": {
29
- "@types/node": "^20.9.2",
30
- "tsx": "^4.1.4",
29
+ "@types/node": "^20.10.0",
30
+ "tsx": "^4.4.0",
31
31
  "typescript": "^5.2.2"
32
32
  },
33
33
  "dependencies": {
34
34
  "commander": "^11.1.0",
35
35
  "remark": "^15.0.1",
36
+ "remark-gfm": "^4.0.0",
36
37
  "typedoc": "^0.25.3"
37
38
  }
38
39
  }