Skip to content

Commit 9828ed2

Browse files
committed
chore: self review and improvements
1 parent e97f09e commit 9828ed2

File tree

17 files changed

+312
-494
lines changed

17 files changed

+312
-494
lines changed

bin/cli.mjs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import process from 'node:process';
55
import { Command, Option } from 'commander';
66

77
import commands from './commands/index.mjs';
8-
import interactive from './commands/interactive.mjs';
98
import { errorWrap } from './utils.mjs';
109
import logger from '../src/logger/index.mjs';
1110

@@ -22,11 +21,9 @@ program.addOption(
2221

2322
// Set log level before any command runs
2423
program.hook('preAction', thisCommand => {
25-
const logLevel = thisCommand.opts().logLevel;
24+
const { logLevel } = thisCommand.opts();
2625

27-
if (logLevel) {
28-
logger.setLogLevel(logLevel);
29-
}
26+
logger.setLogLevel(logLevel);
3027
});
3128

3229
// Registering commands
@@ -54,11 +51,5 @@ commands.forEach(({ name, description, options, action }) => {
5451
cmd.action(errorWrap(action));
5552
});
5653

57-
// Register the interactive command
58-
program
59-
.command('interactive')
60-
.description('Launch guided CLI wizard')
61-
.action(errorWrap(interactive));
62-
6354
// Parse and execute command-line arguments
6455
program.parse(process.argv);

bin/commands/generate.mjs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@ import { loadFromURL } from '../../src/utils/parser.mjs';
1414
const availableGenerators = Object.keys(publicGenerators);
1515

1616
/**
17-
* @typedef {Object} Options
18-
* @property {Array<string>|string} input - Specifies the glob/path for input files.
19-
* @property {Array<string>|string} [ignore] - Specifies the glob/path for ignoring files.
20-
* @property {Array<keyof publicGenerators>} target - Specifies the generator target mode.
21-
* @property {string} version - Specifies the target Node.js version.
22-
* @property {string} changelog - Specifies the path to the Node.js CHANGELOG.md file.
23-
* @property {string} typeMap - Specifies the path to the Node.js Type Map.
24-
* @property {string} [gitRef] - Git ref/commit URL.
25-
* @property {number} [threads] - Number of threads to allow.
26-
* @property {number} [chunkSize] - Number of items to process per worker thread.
17+
* @typedef {{
18+
* input: Array<string> | string;
19+
* ignore?: Array<string> | string;
20+
* target: Array<keyof publicGenerators>;
21+
* version: string;
22+
* changelog: string;
23+
* typeMap: string;
24+
* gitRef?: string;
25+
* threads?: number;
26+
* chunkSize?: number;
27+
* }} Options
2728
*/
2829

2930
/**

bin/commands/index.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import generate from './generate.mjs';
2+
import interactive from './interactive.mjs';
23

3-
export default [generate];
4+
export default [generate, interactive];

bin/commands/interactive.mjs

Lines changed: 123 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
cancel,
1313
} from '@clack/prompts';
1414

15-
import commands from './index.mjs';
1615
import logger from '../../src/logger/index.mjs';
1716

1817
/**
@@ -53,127 +52,143 @@ function escapeShellArg(arg) {
5352
}
5453

5554
/**
56-
* Main interactive function for the API Docs Tooling command line interface.
57-
* Guides the user through a series of prompts, validates inputs, and generates a command to run.
58-
* @returns {Promise<void>} Resolves once the command is generated and executed.
55+
* @type {import('../utils.mjs').Command}
5956
*/
60-
export default async function interactive() {
61-
// Step 1: Introduction to the tool
62-
intro('Welcome to API Docs Tooling');
63-
64-
// Step 2: Choose the action based on available command definitions
65-
const actionOptions = commands.map(({ description }, i) => ({
66-
label: description,
67-
value: i,
68-
}));
69-
70-
const selectedAction = await select({
71-
message: 'What would you like to do?',
72-
options: actionOptions,
73-
});
74-
75-
if (isCancel(selectedAction)) {
76-
cancel('Cancelled.');
77-
process.exit(0);
78-
}
79-
80-
// Retrieve the options for the selected action
81-
const { options, name } = commands[selectedAction];
82-
const answers = {}; // Store answers from user prompts
83-
84-
// Step 3: Collect input for each option
85-
for (const [key, { prompt }] of Object.entries(options)) {
86-
let response;
87-
const promptMessage = getMessage(prompt);
88-
89-
switch (prompt.type) {
90-
case 'text':
91-
response = await text({
92-
message: promptMessage,
93-
initialValue: prompt.initialValue || '',
94-
validate: prompt.required ? requireValue : undefined,
95-
});
96-
if (response) {
97-
// Store response; split into an array if variadic
98-
answers[key] = prompt.variadic
99-
? response.split(',').map(s => s.trim())
100-
: response;
101-
}
102-
break;
103-
104-
case 'confirm':
105-
response = await confirm({
106-
message: promptMessage,
107-
initialValue: prompt.initialValue,
108-
});
109-
answers[key] = response;
110-
break;
111-
112-
case 'multiselect':
113-
response = await multiselect({
114-
message: promptMessage,
115-
options: prompt.options,
116-
required: !!prompt.required,
117-
});
118-
answers[key] = response;
119-
break;
120-
121-
case 'select':
122-
response = await select({
123-
message: promptMessage,
124-
options: prompt.options,
125-
});
126-
answers[key] = response;
127-
break;
128-
}
57+
export default {
58+
name: 'interactive',
59+
description: 'Launch guided CLI wizard',
60+
options: {},
61+
/**
62+
* Main interactive function for the API Docs Tooling command line interface.
63+
* Guides the user through a series of prompts, validates inputs, and generates a command to run.
64+
* @returns {Promise<void>} Resolves once the command is generated and executed.
65+
*/
66+
async action() {
67+
// Import commands dynamically to avoid circular dependency
68+
const { default: commands } = await import('./index.mjs');
69+
70+
// Filter out the interactive command itself
71+
const availableCommands = commands.filter(
72+
cmd => cmd.name !== 'interactive'
73+
);
74+
75+
// Step 1: Introduction to the tool
76+
intro('Welcome to API Docs Tooling');
77+
78+
// Step 2: Choose the action based on available command definitions
79+
const actionOptions = availableCommands.map((cmd, i) => ({
80+
label: cmd.description,
81+
value: i,
82+
}));
83+
84+
const selectedAction = await select({
85+
message: 'What would you like to do?',
86+
options: actionOptions,
87+
});
12988

130-
// Handle cancellation
131-
if (isCancel(response)) {
89+
if (isCancel(selectedAction)) {
13290
cancel('Cancelled.');
13391
process.exit(0);
13492
}
135-
}
13693

137-
// Step 4: Build the final command by escaping values
138-
const cmdParts = ['npx', 'doc-kit', name];
139-
const executionArgs = [name];
94+
// Retrieve the options for the selected action
95+
const { options, name } = availableCommands[selectedAction];
96+
const answers = {}; // Store answers from user prompts
97+
98+
// Step 3: Collect input for each option
99+
for (const [key, { prompt }] of Object.entries(options)) {
100+
let response;
101+
const promptMessage = getMessage(prompt);
102+
103+
switch (prompt.type) {
104+
case 'text':
105+
response = await text({
106+
message: promptMessage,
107+
initialValue: prompt.initialValue || '',
108+
validate: prompt.required ? requireValue : undefined,
109+
});
110+
if (response) {
111+
// Store response; split into an array if variadic
112+
answers[key] = prompt.variadic
113+
? response.split(',').map(s => s.trim())
114+
: response;
115+
}
116+
break;
117+
118+
case 'confirm':
119+
response = await confirm({
120+
message: promptMessage,
121+
initialValue: prompt.initialValue,
122+
});
123+
answers[key] = response;
124+
break;
125+
126+
case 'multiselect':
127+
response = await multiselect({
128+
message: promptMessage,
129+
options: prompt.options,
130+
required: !!prompt.required,
131+
});
132+
answers[key] = response;
133+
break;
134+
135+
case 'select':
136+
response = await select({
137+
message: promptMessage,
138+
options: prompt.options,
139+
});
140+
answers[key] = response;
141+
break;
142+
}
140143

141-
for (const [key, { flags }] of Object.entries(options)) {
142-
const value = answers[key];
143-
// Skip empty values
144-
if (value == null || (Array.isArray(value) && value.length === 0)) {
145-
continue;
144+
// Handle cancellation
145+
if (isCancel(response)) {
146+
cancel('Cancelled.');
147+
process.exit(0);
148+
}
146149
}
147150

148-
const flag = flags[0].split(/[\s,]+/)[0]; // Use the first flag
151+
// Step 4: Build the final command by escaping values
152+
const cmdParts = ['npx', 'doc-kit', name];
153+
const executionArgs = [name];
149154

150-
// Handle different value types (boolean, array, string)
151-
if (typeof value === 'boolean') {
152-
if (value) {
153-
cmdParts.push(flag);
154-
executionArgs.push(flag);
155+
for (const [key, { flags }] of Object.entries(options)) {
156+
const value = answers[key];
157+
// Skip empty values
158+
if (value == null || (Array.isArray(value) && value.length === 0)) {
159+
continue;
155160
}
156-
} else if (Array.isArray(value)) {
157-
for (const item of value) {
158-
cmdParts.push(flag, escapeShellArg(item));
159-
executionArgs.push(flag, item);
161+
162+
const flag = flags[0].split(/[\s,]+/)[0]; // Use the first flag
163+
164+
// Handle different value types (boolean, array, string)
165+
if (typeof value === 'boolean') {
166+
if (value) {
167+
cmdParts.push(flag);
168+
executionArgs.push(flag);
169+
}
170+
} else if (Array.isArray(value)) {
171+
for (const item of value) {
172+
cmdParts.push(flag, escapeShellArg(item));
173+
executionArgs.push(flag, item);
174+
}
175+
} else {
176+
cmdParts.push(flag, escapeShellArg(value));
177+
executionArgs.push(flag, value);
160178
}
161-
} else {
162-
cmdParts.push(flag, escapeShellArg(value));
163-
executionArgs.push(flag, value);
164179
}
165-
}
166180

167-
const finalCommand = cmdParts.join(' ');
181+
const finalCommand = cmdParts.join(' ');
168182

169-
logger.info(`\nGenerated command:\n${finalCommand}\n`);
183+
logger.info(`\nGenerated command:\n${finalCommand}\n`);
170184

171-
// Step 5: Confirm and execute the generated command
172-
if (await confirm({ message: 'Run now?', initialValue: true })) {
173-
spawnSync(process.execPath, [process.argv[1], ...executionArgs], {
174-
stdio: 'inherit',
175-
});
176-
}
185+
// Step 5: Confirm and execute the generated command
186+
if (await confirm({ message: 'Run now?', initialValue: true })) {
187+
spawnSync(process.execPath, [process.argv[1], ...executionArgs], {
188+
stdio: 'inherit',
189+
});
190+
}
177191

178-
outro('Done!');
179-
}
192+
outro('Done!');
193+
},
194+
};

bin/utils.mjs

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,4 @@
1-
import createMarkdownLoader from '../src/loaders/markdown.mjs';
21
import logger from '../src/logger/index.mjs';
3-
import createMarkdownParser from '../src/parsers/markdown.mjs';
4-
5-
/**
6-
* Generic lazy initializer.
7-
* @template T
8-
* @param {() => T} factory - Function to create the instance.
9-
* @returns {() => T} - A function that returns the singleton instance.
10-
*/
11-
export const lazy = factory => {
12-
let instance;
13-
return args => (instance ??= factory(args));
14-
};
15-
16-
// Instantiate loader and parser once to reuse,
17-
// but only if/when we actually need them. No need
18-
// to create these objects just to load a different
19-
// utility.
20-
const loader = lazy(createMarkdownLoader);
21-
const parser = lazy(createMarkdownParser);
22-
23-
/**
24-
* Load and parse markdown API docs.
25-
* @param {string[]} input - Glob patterns for input files.
26-
* @param {string[]} [ignore] - Glob patterns to ignore.
27-
* @returns {Promise<Array<ParserOutput<import('mdast').Root>>>}
28-
*/
29-
export async function loadAndParse(input, ignore) {
30-
const files = await loader().loadFiles(input, ignore);
31-
return parser().parseApiDocs(files);
32-
}
332

343
/**
354
* Wraps a function to catch both synchronous and asynchronous errors.
@@ -47,26 +16,3 @@ export const errorWrap =
4716
process.exit(1);
4817
}
4918
};
50-
51-
/**
52-
* Represents a command-line option for the CLI.
53-
* @typedef {Object} Option
54-
* @property {string[]} flags - Command-line flags, e.g., ['-i, --input <patterns...>'].
55-
* @property {string} desc - Description of the option.
56-
* @property {Object} [prompt] - Optional prompt configuration.
57-
* @property {'text'|'confirm'|'select'|'multiselect'} prompt.type - Type of the prompt.
58-
* @property {string} prompt.message - Message displayed in the prompt.
59-
* @property {boolean} [prompt.variadic] - Indicates if the prompt accepts multiple values.
60-
* @property {boolean} [prompt.required] - Whether the prompt is required.
61-
* @property {boolean} [prompt.initialValue] - Default value for confirm prompts.
62-
* @property {{label: string, value: string}[]} [prompt.options] - Options for select/multiselect prompts.
63-
*/
64-
65-
/**
66-
* Represents a command-line subcommand
67-
* @typedef {Object} Command
68-
* @property {{ [key: string]: Option }} options
69-
* @property {string} name
70-
* @property {string} description
71-
* @property {Function} action
72-
*/

0 commit comments

Comments
 (0)