Skip to content

Commit a0bdd2d

Browse files
committed
fix: #897
Signed-off-by: Aysajan Eziz <aeziz@northset.ai>
1 parent 11d012a commit a0bdd2d

3 files changed

Lines changed: 73 additions & 6 deletions

File tree

.changeset/quiet-dingos-detect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@node-core/doc-kit': patch
3+
---
4+
5+
Automatically load `doc-kit.config.mjs` from the current working directory.

src/utils/configuration/__tests__/index.test.mjs

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import assert from 'node:assert';
2+
import * as nodeFs from 'node:fs';
3+
import { join } from 'node:path';
24
import { describe, it, mock, beforeEach } from 'node:test';
35

46
// Mock dependencies
57
const mockParseChangelog = mock.fn(async changelog => [changelog]);
68
const mockParseIndex = mock.fn(async index => [index]);
79
const mockImportFromURL = mock.fn(async () => ({}));
10+
const mockExistsSync = mock.fn(() => false);
811

912
const createMockConfig = (overrides = {}) => ({
1013
global: {},
@@ -31,6 +34,9 @@ mock.module('../../../parsers/markdown.mjs', {
3134
mock.module('../../loaders.mjs', {
3235
namedExports: { importFromURL: mockImportFromURL },
3336
});
37+
mock.module('node:fs', {
38+
namedExports: { ...nodeFs, existsSync: mockExistsSync },
39+
});
3440

3541
const {
3642
assertRunnableOptions,
@@ -43,9 +49,12 @@ const {
4349

4450
// Helper to reset all mocks
4551
const resetAllMocks = () => {
46-
[mockParseChangelog, mockParseIndex, mockImportFromURL].forEach(m =>
47-
m.mock.resetCalls()
48-
);
52+
[
53+
mockParseChangelog,
54+
mockParseIndex,
55+
mockImportFromURL,
56+
mockExistsSync,
57+
].forEach(m => m.mock.resetCalls());
4958
};
5059

5160
// Helper to count specific function calls
@@ -149,6 +158,47 @@ describe('config.mjs', () => {
149158
});
150159

151160
describe('createRunConfiguration', () => {
161+
it('should auto-detect a config file in the current directory', async () => {
162+
const defaultConfigFile = join(process.cwd(), 'doc-kit.config.mjs');
163+
const mockConfig = createMockConfig({
164+
global: { input: 'auto-detected-src/' },
165+
});
166+
mockExistsSync.mock.mockImplementationOnce(() => true);
167+
mockImportFromURL.mock.mockImplementationOnce(async () => mockConfig);
168+
169+
const config = await createRunConfiguration({});
170+
171+
assert.strictEqual(config.global.input, 'auto-detected-src/');
172+
assert.strictEqual(mockExistsSync.mock.calls.length, 1);
173+
assert.strictEqual(
174+
mockExistsSync.mock.calls[0].arguments[0],
175+
defaultConfigFile
176+
);
177+
assert.strictEqual(mockImportFromURL.mock.calls.length, 1);
178+
assert.strictEqual(
179+
mockImportFromURL.mock.calls[0].arguments[0],
180+
defaultConfigFile
181+
);
182+
});
183+
184+
it('should prefer an explicit config file', async () => {
185+
mockImportFromURL.mock.mockImplementationOnce(async () =>
186+
createMockConfig({ global: { input: 'explicit-src/' } })
187+
);
188+
189+
const config = await createRunConfiguration({
190+
configFile: 'explicit-config.mjs',
191+
});
192+
193+
assert.strictEqual(config.global.input, 'explicit-src/');
194+
assert.strictEqual(mockExistsSync.mock.calls.length, 0);
195+
assert.strictEqual(mockImportFromURL.mock.calls.length, 1);
196+
assert.strictEqual(
197+
mockImportFromURL.mock.calls[0].arguments[0],
198+
'explicit-config.mjs'
199+
);
200+
});
201+
152202
it('should merge config sources in correct order', async () => {
153203
mockImportFromURL.mock.mockImplementationOnce(async () =>
154204
createMockConfig({ global: { input: 'custom-src/' } })
@@ -204,14 +254,19 @@ describe('config.mjs', () => {
204254
assert.strictEqual(config.chunkSize, 1);
205255
});
206256

207-
it('should work without config file', async () => {
257+
it('should use an empty config when no config file is present', async () => {
208258
const config = await createRunConfiguration({
209259
version: '20.0.0',
210260
threads: 4,
211261
});
212262

213263
assert.ok(config);
214264
assert.strictEqual(config.threads, 4);
265+
assert.strictEqual(mockExistsSync.mock.calls.length, 1);
266+
assert.strictEqual(
267+
mockExistsSync.mock.calls[0].arguments[0],
268+
join(process.cwd(), 'doc-kit.config.mjs')
269+
);
215270
assert.strictEqual(mockImportFromURL.mock.calls.length, 0);
216271
});
217272

src/utils/configuration/index.mjs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { existsSync } from 'node:fs';
12
import { cpus } from 'node:os';
3+
import { join } from 'node:path';
24
import { isMainThread } from 'node:worker_threads';
35

46
import { coerce } from 'semver';
@@ -123,15 +125,20 @@ export const assertRunnableOptions = config => {
123125
};
124126

125127
/**
126-
* Creates a complete run configuration by merging config file, user options, and defaults.
128+
* Creates a complete run configuration by merging an explicit or auto-detected
129+
* config file, user options, and defaults.
127130
* Processes and validates configuration values including version coercion, changelog parsing,
128131
* and constraint enforcement for threads and chunk size.
129132
*
130133
* @param {import('../../../bin/commands/generate.mjs').CLIOptions} options - User-provided configuration options
131134
* @returns {Promise<import('./types').Configuration>} The configuration
132135
*/
133136
export const createRunConfiguration = async options => {
134-
const config = await loadConfigFile(options.configFile);
137+
const defaultConfigFile = join(process.cwd(), 'doc-kit.config.mjs');
138+
const configFile =
139+
options.configFile ??
140+
(existsSync(defaultConfigFile) ? defaultConfigFile : undefined);
141+
const config = await loadConfigFile(configFile);
135142
config.target &&= enforceArray(config.target);
136143

137144
// Merge with defaults

0 commit comments

Comments
 (0)