diff --git a/.changeset/kind-birds-detect.md b/.changeset/kind-birds-detect.md new file mode 100644 index 00000000..55808478 --- /dev/null +++ b/.changeset/kind-birds-detect.md @@ -0,0 +1,5 @@ +--- +'@node-core/doc-kit': minor +--- + +Automatically load `doc-kit.config.mjs` from the current working directory when `--config-file` is omitted. diff --git a/README.md b/README.md index 367f29fd..e1b5d307 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,11 @@ 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. +You must provide the input and target generators through CLI options or a +configuration file. When `--config-file` is omitted, doc-kit automatically +loads `doc-kit.config.mjs` from the current working directory if it exists. An +explicit `--config-file` path takes precedence. Running `generate` without a +usable input and target exits with an error pointing you to the help output. ``` Usage: @node-core/doc-kit generate [options] diff --git a/src/utils/configuration/__tests__/index.test.mjs b/src/utils/configuration/__tests__/index.test.mjs index 3de2dc08..68151d28 100644 --- a/src/utils/configuration/__tests__/index.test.mjs +++ b/src/utils/configuration/__tests__/index.test.mjs @@ -1,10 +1,13 @@ import assert from 'node:assert'; +import fs, * as fsNamedExports from 'node:fs'; +import { resolve } from 'node:path'; import { describe, it, mock, beforeEach } from 'node:test'; // Mock dependencies const mockParseChangelog = mock.fn(async changelog => [changelog]); const mockParseIndex = mock.fn(async index => [index]); const mockImportFromURL = mock.fn(async () => ({})); +const mockExistsSync = mock.fn(() => false); const createMockConfig = (overrides = {}) => ({ global: {}, @@ -12,6 +15,10 @@ const createMockConfig = (overrides = {}) => ({ }); // Mock modules +mock.module('node:fs', { + defaultExport: fs, + namedExports: { ...fsNamedExports, existsSync: mockExistsSync }, +}); mock.module('../../../generators/index.mjs', { namedExports: { allGenerators: { @@ -49,9 +56,12 @@ const { // Helper to reset all mocks const resetAllMocks = () => { - [mockParseChangelog, mockParseIndex, mockImportFromURL].forEach(m => - m.mock.resetCalls() - ); + [ + mockParseChangelog, + mockParseIndex, + mockImportFromURL, + mockExistsSync, + ].forEach(m => m.mock.resetCalls()); }; // Helper to count specific function calls @@ -74,14 +84,34 @@ describe('config.mjs', () => { mockImportFromURL.mock.calls[0].arguments[0], 'path/to/config.mjs' ); + assert.strictEqual(mockExistsSync.mock.calls.length, 0); + }); + + it('should discover doc-kit.config.mjs in the current directory', async () => { + const mockConfig = { custom: 'discovered-config' }; + const defaultConfigPath = resolve('doc-kit.config.mjs'); + mockExistsSync.mock.mockImplementationOnce( + filePath => filePath === defaultConfigPath + ); + mockImportFromURL.mock.mockImplementationOnce(async () => mockConfig); + + const result = await loadConfigFile(); + + assert.deepStrictEqual(result, mockConfig); + assert.strictEqual(mockExistsSync.mock.calls.length, 1); + assert.strictEqual( + mockImportFromURL.mock.calls[0].arguments[0], + defaultConfigPath + ); }); - it('should return empty object for falsy paths', async () => { + it('should return empty object when no config file is provided or found', async () => { for (const falsyValue of ['', null, undefined, 0, false]) { const result = await loadConfigFile(falsyValue); assert.deepStrictEqual(result, {}); } assert.strictEqual(mockImportFromURL.mock.calls.length, 0); + assert.strictEqual(mockExistsSync.mock.calls.length, 5); }); }); diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index e04c0bba..ac1530b6 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -1,4 +1,6 @@ +import { existsSync } from 'node:fs'; import { cpus } from 'node:os'; +import { resolve } from 'node:path'; import { isMainThread } from 'node:worker_threads'; import { coerce } from 'semver'; @@ -12,6 +14,8 @@ import { leftHandAssign } from '../generators.mjs'; import { importFromURL } from '../loaders.mjs'; import { deepMerge, lazy } from '../misc.mjs'; +const DEFAULT_CONFIG_FILE = 'doc-kit.config.mjs'; + /** * Get's the default configuration */ @@ -54,11 +58,17 @@ export const getDefaultConfig = lazy(config => /** * Loads a configuration file from a URL or file path. * - * @param {string} filePath - The URL or file path to the configuration file + * @param {string} [filePath] - The URL or file path to the configuration file * @returns {Promise>} The imported configuration object, or an empty object if no path provided */ -export const loadConfigFile = filePath => - filePath ? importFromURL(filePath) : {}; +export const loadConfigFile = async filePath => { + if (filePath) { + return importFromURL(filePath); + } + + const defaultConfigPath = resolve(DEFAULT_CONFIG_FILE); + return existsSync(defaultConfigPath) ? importFromURL(defaultConfigPath) : {}; +}; /** * Transforms configuration values that need async processing or coercion.