From 42a47e6c740e9072fd66d78c241637d6123d504b Mon Sep 17 00:00:00 2001 From: bmuenzenmeyer Date: Wed, 24 Jun 2026 08:56:12 -0500 Subject: [PATCH 1/4] fix(generate): show help output when no arguments or config are provided --- bin/commands/generate.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bin/commands/generate.mjs b/bin/commands/generate.mjs index d4586663..107f73f8 100644 --- a/bin/commands/generate.mjs +++ b/bin/commands/generate.mjs @@ -60,7 +60,13 @@ export default new Command('generate') .addOption(new Option('--type-map ', 'Type map URL or path')) .action( - errorWrap(async opts => { + errorWrap(async (opts, command) => { + // Running `generate` with no arguments or config has nothing to do, so + // show the help output instead of failing later with an opaque error. + if (Object.keys(opts).length === 0) { + command.help(); + } + const config = await setConfig(opts); await runGenerators(config); }) From 1ef7ca173fa6d57265d66cfaa2f81200ed3cae5a Mon Sep 17 00:00:00 2001 From: bmuenzenmeyer Date: Wed, 24 Jun 2026 13:46:22 -0500 Subject: [PATCH 2/4] fix(generate): enforce target or config file requirement for generate command --- README.md | 4 +++ bin/commands/generate.mjs | 8 +----- .../configuration/__tests__/index.test.mjs | 28 ++++++++++++++++++- src/utils/configuration/index.mjs | 9 ++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 607cf69f..583d4371 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ Commands: ### `generate` +You must provide either `--target` (one or more generators to run) or +`--config-file` (which supplies the targets). Running `generate` without either +exits with an error pointing you to the help output. + ``` Usage: @node-core/doc-kit generate [options] diff --git a/bin/commands/generate.mjs b/bin/commands/generate.mjs index 107f73f8..d4586663 100644 --- a/bin/commands/generate.mjs +++ b/bin/commands/generate.mjs @@ -60,13 +60,7 @@ export default new Command('generate') .addOption(new Option('--type-map ', 'Type map URL or path')) .action( - errorWrap(async (opts, command) => { - // Running `generate` with no arguments or config has nothing to do, so - // show the help output instead of failing later with an opaque error. - if (Object.keys(opts).length === 0) { - command.help(); - } - + errorWrap(async opts => { const config = await setConfig(opts); await runGenerators(config); }) diff --git a/src/utils/configuration/__tests__/index.test.mjs b/src/utils/configuration/__tests__/index.test.mjs index a45de3d9..015553b2 100644 --- a/src/utils/configuration/__tests__/index.test.mjs +++ b/src/utils/configuration/__tests__/index.test.mjs @@ -126,6 +126,26 @@ describe('config.mjs', () => { }); describe('createRunConfiguration', () => { + it('should throw when neither target nor config file is provided', async () => { + await assert.rejects( + () => createRunConfiguration({}), + /Either `--target` or `--config-file` must be provided/ + ); + assert.strictEqual(mockImportFromURL.mock.calls.length, 0); + }); + + it('should not throw when only a target is provided', async () => { + await assert.doesNotReject(() => + createRunConfiguration({ target: ['json'] }) + ); + }); + + it('should not throw when only a config file is provided', async () => { + await assert.doesNotReject(() => + createRunConfiguration({ configFile: 'config.mjs' }) + ); + }); + it('should merge config sources in correct order', async () => { mockImportFromURL.mock.mockImplementationOnce(async () => createMockConfig({ global: { input: 'custom-src/' } }) @@ -173,6 +193,7 @@ describe('config.mjs', () => { it('should enforce minimum constraints', async () => { const config = await createRunConfiguration({ + target: ['json'], threads: -5, chunkSize: 0, }); @@ -183,6 +204,7 @@ describe('config.mjs', () => { it('should work without config file', async () => { const config = await createRunConfiguration({ + target: ['json'], version: '20.0.0', threads: 4, }); @@ -212,7 +234,11 @@ describe('config.mjs', () => { describe('setConfig and getConfig', () => { it('should persist config across calls', async () => { - const config = await setConfig({ version: '20.0.0', threads: 2 }); + const config = await setConfig({ + target: ['json'], + version: '20.0.0', + threads: 2, + }); const retrieved = getConfig(); assert.strictEqual(config, retrieved); diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index b20a634a..f604f864 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -113,6 +113,15 @@ export const createConfigFromCLIOptions = options => ({ * @returns {Promise} The configuration */ export const createRunConfiguration = async options => { + // Generating requires somewhere to read targets from: either explicit + // `--target` flags or a `--config-file` that supplies them. + if (!options.target && !options.configFile) { + throw new Error( + 'Either `--target` or `--config-file` must be provided. ' + + 'Run `doc-kit generate --help` for usage.' + ); + } + const config = await loadConfigFile(options.configFile); config.target &&= enforceArray(config.target); From 40d592814e4920156eff7ec9f3696577ebc6baca Mon Sep 17 00:00:00 2001 From: bmuenzenmeyer Date: Wed, 24 Jun 2026 14:14:04 -0500 Subject: [PATCH 3/4] fix(configuration): add assertRunnableOptions to validate target or config file presence --- bin/commands/generate.mjs | 7 +++- .../configuration/__tests__/index.test.mjs | 32 ++++++++----------- src/utils/configuration/index.mjs | 22 ++++++++----- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/bin/commands/generate.mjs b/bin/commands/generate.mjs index d4586663..694d08ec 100644 --- a/bin/commands/generate.mjs +++ b/bin/commands/generate.mjs @@ -2,7 +2,10 @@ import { Command, Option } from 'commander'; import { publicGenerators } from '../../src/generators/index.mjs'; import createGenerator from '../../src/generators.mjs'; -import { setConfig } from '../../src/utils/configuration/index.mjs'; +import { + assertRunnableOptions, + setConfig, +} from '../../src/utils/configuration/index.mjs'; import { errorWrap } from '../utils.mjs'; const { runGenerators } = createGenerator(); @@ -61,6 +64,8 @@ export default new Command('generate') .action( errorWrap(async opts => { + assertRunnableOptions(opts); + const config = await setConfig(opts); await runGenerators(config); }) diff --git a/src/utils/configuration/__tests__/index.test.mjs b/src/utils/configuration/__tests__/index.test.mjs index 015553b2..607e5c65 100644 --- a/src/utils/configuration/__tests__/index.test.mjs +++ b/src/utils/configuration/__tests__/index.test.mjs @@ -33,6 +33,7 @@ mock.module('../../loaders.mjs', { }); const { + assertRunnableOptions, loadConfigFile, createConfigFromCLIOptions, createRunConfiguration, @@ -125,27 +126,26 @@ describe('config.mjs', () => { }); }); - describe('createRunConfiguration', () => { - it('should throw when neither target nor config file is provided', async () => { - await assert.rejects( - () => createRunConfiguration({}), + describe('assertRunnableOptions', () => { + it('should throw when neither target nor config file is provided', () => { + assert.throws( + () => assertRunnableOptions({}), /Either `--target` or `--config-file` must be provided/ ); - assert.strictEqual(mockImportFromURL.mock.calls.length, 0); }); - it('should not throw when only a target is provided', async () => { - await assert.doesNotReject(() => - createRunConfiguration({ target: ['json'] }) - ); + it('should not throw when a target is provided', () => { + assert.doesNotThrow(() => assertRunnableOptions({ target: ['json'] })); }); - it('should not throw when only a config file is provided', async () => { - await assert.doesNotReject(() => - createRunConfiguration({ configFile: 'config.mjs' }) + it('should not throw when a config file is provided', () => { + assert.doesNotThrow(() => + assertRunnableOptions({ configFile: 'config.mjs' }) ); }); + }); + describe('createRunConfiguration', () => { it('should merge config sources in correct order', async () => { mockImportFromURL.mock.mockImplementationOnce(async () => createMockConfig({ global: { input: 'custom-src/' } }) @@ -193,7 +193,6 @@ describe('config.mjs', () => { it('should enforce minimum constraints', async () => { const config = await createRunConfiguration({ - target: ['json'], threads: -5, chunkSize: 0, }); @@ -204,7 +203,6 @@ describe('config.mjs', () => { it('should work without config file', async () => { const config = await createRunConfiguration({ - target: ['json'], version: '20.0.0', threads: 4, }); @@ -234,11 +232,7 @@ describe('config.mjs', () => { describe('setConfig and getConfig', () => { it('should persist config across calls', async () => { - const config = await setConfig({ - target: ['json'], - version: '20.0.0', - threads: 2, - }); + const config = await setConfig({ version: '20.0.0', threads: 2 }); const retrieved = getConfig(); assert.strictEqual(config, retrieved); diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index f604f864..a143f6de 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -105,23 +105,29 @@ export const createConfigFromCLIOptions = options => ({ }); /** - * Creates a complete run configuration by merging config file, user options, and defaults. - * Processes and validates configuration values including version coercion, changelog parsing, - * and constraint enforcement for threads and chunk size. + * Asserts that the CLI was given somewhere to read generator targets from: + * either explicit `--target` flags or a `--config-file` that supplies them. * - * @param {import('../../../bin/commands/generate.mjs').CLIOptions} options - User-provided configuration options - * @returns {Promise} The configuration + * @param {import('../../../bin/commands/generate.mjs').CLIOptions} options - User-provided options */ -export const createRunConfiguration = async options => { - // Generating requires somewhere to read targets from: either explicit - // `--target` flags or a `--config-file` that supplies them. +export const assertRunnableOptions = options => { if (!options.target && !options.configFile) { throw new Error( 'Either `--target` or `--config-file` must be provided. ' + 'Run `doc-kit generate --help` for usage.' ); } +}; +/** + * Creates a complete run configuration by merging config file, user options, and defaults. + * Processes and validates configuration values including version coercion, changelog parsing, + * and constraint enforcement for threads and chunk size. + * + * @param {import('../../../bin/commands/generate.mjs').CLIOptions} options - User-provided configuration options + * @returns {Promise} The configuration + */ +export const createRunConfiguration = async options => { const config = await loadConfigFile(options.configFile); config.target &&= enforceArray(config.target); From 19da3ae57f831035e861ab24ee1f025e9d9ab7b2 Mon Sep 17 00:00:00 2001 From: bmuenzenmeyer Date: Fri, 26 Jun 2026 22:26:54 -0500 Subject: [PATCH 4/4] fix(generate): validate options after configuration is set --- bin/commands/generate.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/commands/generate.mjs b/bin/commands/generate.mjs index 694d08ec..30d9a378 100644 --- a/bin/commands/generate.mjs +++ b/bin/commands/generate.mjs @@ -64,9 +64,9 @@ export default new Command('generate') .action( errorWrap(async opts => { - assertRunnableOptions(opts); - const config = await setConfig(opts); + assertRunnableOptions(config); + await runGenerators(config); }) );