From a0bdd2de6a81b60e2f8a607a40d39b4fa0b22751 Mon Sep 17 00:00:00 2001 From: Aysajan Eziz Date: Sun, 12 Jul 2026 18:05:37 -0400 Subject: [PATCH] fix: nodejs/doc-kit#897 Signed-off-by: Aysajan Eziz --- .changeset/quiet-dingos-detect.md | 5 ++ .../configuration/__tests__/index.test.mjs | 63 +++++++++++++++++-- src/utils/configuration/index.mjs | 11 +++- 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 .changeset/quiet-dingos-detect.md diff --git a/.changeset/quiet-dingos-detect.md b/.changeset/quiet-dingos-detect.md new file mode 100644 index 00000000..fb552784 --- /dev/null +++ b/.changeset/quiet-dingos-detect.md @@ -0,0 +1,5 @@ +--- +'@node-core/doc-kit': patch +--- + +Automatically load `doc-kit.config.mjs` from the current working directory. diff --git a/src/utils/configuration/__tests__/index.test.mjs b/src/utils/configuration/__tests__/index.test.mjs index ac7bf504..8ce318f7 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 * as nodeFs from 'node:fs'; +import { join } 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: {}, @@ -31,6 +34,9 @@ mock.module('../../../parsers/markdown.mjs', { mock.module('../../loaders.mjs', { namedExports: { importFromURL: mockImportFromURL }, }); +mock.module('node:fs', { + namedExports: { ...nodeFs, existsSync: mockExistsSync }, +}); const { assertRunnableOptions, @@ -43,9 +49,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 @@ -149,6 +158,47 @@ describe('config.mjs', () => { }); describe('createRunConfiguration', () => { + it('should auto-detect a config file in the current directory', async () => { + const defaultConfigFile = join(process.cwd(), 'doc-kit.config.mjs'); + const mockConfig = createMockConfig({ + global: { input: 'auto-detected-src/' }, + }); + mockExistsSync.mock.mockImplementationOnce(() => true); + mockImportFromURL.mock.mockImplementationOnce(async () => mockConfig); + + const config = await createRunConfiguration({}); + + assert.strictEqual(config.global.input, 'auto-detected-src/'); + assert.strictEqual(mockExistsSync.mock.calls.length, 1); + assert.strictEqual( + mockExistsSync.mock.calls[0].arguments[0], + defaultConfigFile + ); + assert.strictEqual(mockImportFromURL.mock.calls.length, 1); + assert.strictEqual( + mockImportFromURL.mock.calls[0].arguments[0], + defaultConfigFile + ); + }); + + it('should prefer an explicit config file', async () => { + mockImportFromURL.mock.mockImplementationOnce(async () => + createMockConfig({ global: { input: 'explicit-src/' } }) + ); + + const config = await createRunConfiguration({ + configFile: 'explicit-config.mjs', + }); + + assert.strictEqual(config.global.input, 'explicit-src/'); + assert.strictEqual(mockExistsSync.mock.calls.length, 0); + assert.strictEqual(mockImportFromURL.mock.calls.length, 1); + assert.strictEqual( + mockImportFromURL.mock.calls[0].arguments[0], + 'explicit-config.mjs' + ); + }); + it('should merge config sources in correct order', async () => { mockImportFromURL.mock.mockImplementationOnce(async () => createMockConfig({ global: { input: 'custom-src/' } }) @@ -204,7 +254,7 @@ describe('config.mjs', () => { assert.strictEqual(config.chunkSize, 1); }); - it('should work without config file', async () => { + it('should use an empty config when no config file is present', async () => { const config = await createRunConfiguration({ version: '20.0.0', threads: 4, @@ -212,6 +262,11 @@ describe('config.mjs', () => { assert.ok(config); assert.strictEqual(config.threads, 4); + assert.strictEqual(mockExistsSync.mock.calls.length, 1); + assert.strictEqual( + mockExistsSync.mock.calls[0].arguments[0], + join(process.cwd(), 'doc-kit.config.mjs') + ); assert.strictEqual(mockImportFromURL.mock.calls.length, 0); }); diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index a31bc35a..9f5536b6 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 { join } from 'node:path'; import { isMainThread } from 'node:worker_threads'; import { coerce } from 'semver'; @@ -123,7 +125,8 @@ export const assertRunnableOptions = config => { }; /** - * Creates a complete run configuration by merging config file, user options, and defaults. + * Creates a complete run configuration by merging an explicit or auto-detected + * config file, user options, and defaults. * Processes and validates configuration values including version coercion, changelog parsing, * and constraint enforcement for threads and chunk size. * @@ -131,7 +134,11 @@ export const assertRunnableOptions = config => { * @returns {Promise} The configuration */ export const createRunConfiguration = async options => { - const config = await loadConfigFile(options.configFile); + const defaultConfigFile = join(process.cwd(), 'doc-kit.config.mjs'); + const configFile = + options.configFile ?? + (existsSync(defaultConfigFile) ? defaultConfigFile : undefined); + const config = await loadConfigFile(configFile); config.target &&= enforceArray(config.target); // Merge with defaults