Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-dingos-detect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@node-core/doc-kit': patch
---

Automatically load `doc-kit.config.mjs` from the current working directory.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about:
.config.js
.config.cjs
.config.json?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

their aren't supported by the code.
IMO we should also support ts, cts, mts as types strippable code

63 changes: 59 additions & 4 deletions src/utils/configuration/__tests__/index.test.mjs
Original file line number Diff line number Diff line change
@@ -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: {},
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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/' } })
Expand Down Expand Up @@ -204,14 +254,19 @@ 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,
});

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);
});

Expand Down
11 changes: 9 additions & 2 deletions src/utils/configuration/index.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -123,15 +125,20 @@ 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.
*
* @param {import('../../../bin/commands/generate.mjs').CLIOptions} options - User-provided configuration options
* @returns {Promise<import('./types').Configuration>} 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);
Comment on lines 135 to +141

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the logic here is wrong you should do something like this:

resolve configuration file:

  if --config option is provided:
    use the provided file
  else:
    search for a configuration file named:
      doc-kit.config.js
      doc-kit.config.cjs
      doc-kit.config.mjs
      doc-kit.config.ts
      doc-kit.config.cts
      doc-kit.config.mts

    use the first file that exists (following a defined priority)

  pass the resolved path to loadConfigFile()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — understood. I will replace the single .mjs probe with explicit --config-file precedence followed by discovery across the six names you listed, and add coverage for every extension plus the multiple-files case. Before I update the branch, should the order in your example (.js, .cjs, .mjs, .ts, .cts, .mts) also be the precedence when more than one exists? I will leave .json out unless you want it included.

config.target &&= enforceArray(config.target);

// Merge with defaults
Expand Down