Skip to content
Closed
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/kind-birds-detect.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
38 changes: 34 additions & 4 deletions src/utils/configuration/__tests__/index.test.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
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: {},
...overrides,
});

// Mock modules
mock.module('node:fs', {
defaultExport: fs,
namedExports: { ...fsNamedExports, existsSync: mockExistsSync },
});
mock.module('../../../generators/index.mjs', {
namedExports: {
allGenerators: {
Expand Down Expand Up @@ -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
Expand All @@ -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);
});
});

Expand Down
16 changes: 13 additions & 3 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 { resolve } from 'node:path';
import { isMainThread } from 'node:worker_threads';

import { coerce } from 'semver';
Expand All @@ -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
*/
Expand Down Expand Up @@ -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<Partial<import('./types').Configuration>>} 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.
Expand Down