diff --git a/.changeset/quiet-dingos-detect.md b/.changeset/quiet-dingos-detect.md new file mode 100644 index 00000000..d7fb8014 --- /dev/null +++ b/.changeset/quiet-dingos-detect.md @@ -0,0 +1,5 @@ +--- +'@node-core/doc-kit': patch +--- + +Automatically discover doc-kit configuration in the current working directory when `--config-file` is omitted. diff --git a/package-lock.json b/package-lock.json index e39cab23..66ae8f8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@swc/html-wasm": "^1.15.43", "acorn": "^8.17.0", "commander": "^15.0.0", + "cosmiconfig": "^9.0.2", "dedent": "^1.7.2", "estree-util-to-js": "^2.0.0", "estree-util-visit": "^2.0.0", @@ -114,6 +115,29 @@ "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", "license": "MIT" }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", @@ -3975,7 +3999,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -4307,6 +4330,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -4540,6 +4572,32 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4835,6 +4893,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -4848,6 +4915,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -6378,6 +6454,31 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6451,6 +6552,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -6963,11 +7070,16 @@ "node": ">=8" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "dev": true, "funding": [ { "type": "github", @@ -7003,6 +7115,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7075,6 +7193,12 @@ "inBundle": true, "license": "MIT" }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/lint-staged": { "version": "17.0.8", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.8.tgz", @@ -8803,6 +8927,18 @@ "quansync": "^0.2.7" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -8838,6 +8974,24 @@ "parse-statements": "1.0.11" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-statements": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", @@ -8915,7 +9069,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { diff --git a/package.json b/package.json index 392aee35..6f5786b5 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@swc/html-wasm": "^1.15.43", "acorn": "^8.17.0", "commander": "^15.0.0", + "cosmiconfig": "^9.0.2", "dedent": "^1.7.2", "estree-util-to-js": "^2.0.0", "estree-util-visit": "^2.0.0", diff --git a/src/utils/configuration/__tests__/index.test.mjs b/src/utils/configuration/__tests__/index.test.mjs index 3de2dc08..6e98e864 100644 --- a/src/utils/configuration/__tests__/index.test.mjs +++ b/src/utils/configuration/__tests__/index.test.mjs @@ -5,6 +5,8 @@ import { describe, it, mock, beforeEach } from 'node:test'; const mockParseChangelog = mock.fn(async changelog => [changelog]); const mockParseIndex = mock.fn(async index => [index]); const mockImportFromURL = mock.fn(async () => ({})); +const mockSearch = mock.fn(async () => null); +const mockCosmiconfig = mock.fn(() => ({ search: mockSearch })); const createMockConfig = (overrides = {}) => ({ global: {}, @@ -37,6 +39,9 @@ mock.module('../../../parsers/markdown.mjs', { mock.module('../../loaders.mjs', { namedExports: { importFromURL: mockImportFromURL }, }); +mock.module('cosmiconfig', { + namedExports: { cosmiconfig: mockCosmiconfig }, +}); const { assertRunnableOptions, @@ -49,9 +54,13 @@ const { // Helper to reset all mocks const resetAllMocks = () => { - [mockParseChangelog, mockParseIndex, mockImportFromURL].forEach(m => - m.mock.resetCalls() + [mockParseChangelog, mockParseIndex, mockImportFromURL, mockSearch].forEach( + m => m.mock.resetCalls() ); + mockCosmiconfig.mock.resetCalls(); + mockImportFromURL.mock.mockImplementation(async () => ({})); + mockSearch.mock.mockImplementation(async () => null); + mockCosmiconfig.mock.mockImplementation(() => ({ search: mockSearch })); }; // Helper to count specific function calls @@ -184,6 +193,43 @@ describe('config.mjs', () => { assert.strictEqual(config.web.showSearchBox, true); }); + it('should discover a config through cosmiconfig', async () => { + mockSearch.mock.mockImplementationOnce(async () => ({ + config: createMockConfig({ + global: { input: 'auto-detected-src/' }, + }), + filepath: `${process.cwd()}/doc-kit.config.mjs`, + })); + + const config = await createRunConfiguration({}); + + assert.strictEqual(config.global.input, 'auto-detected-src/'); + assert.strictEqual(mockCosmiconfig.mock.calls.length, 1); + assert.deepStrictEqual(mockCosmiconfig.mock.calls[0].arguments, [ + 'doc-kit', + ]); + assert.strictEqual(mockSearch.mock.calls.length, 1); + assert.deepStrictEqual(mockSearch.mock.calls[0].arguments, []); + }); + + 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(mockCosmiconfig.mock.calls.length, 0); + assert.strictEqual(mockImportFromURL.mock.calls.length, 1); + assert.strictEqual( + mockImportFromURL.mock.calls[0].arguments[0], + 'explicit-config.mjs' + ); + }); + it('should transform string values only once', async () => { const changelogUrl = 'https://example.com/changelog.md'; const indexUrl = 'https://example.com/index.md'; @@ -223,7 +269,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, @@ -231,6 +277,8 @@ describe('config.mjs', () => { assert.ok(config); assert.strictEqual(config.threads, 4); + assert.strictEqual(mockCosmiconfig.mock.calls.length, 1); + assert.strictEqual(mockSearch.mock.calls.length, 1); assert.strictEqual(mockImportFromURL.mock.calls.length, 0); }); diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index e04c0bba..aa545097 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -1,6 +1,7 @@ import { cpus } from 'node:os'; import { isMainThread } from 'node:worker_threads'; +import { cosmiconfig } from 'cosmiconfig'; import { coerce } from 'semver'; import { CHANGELOG_URL, populate } from './templates.mjs'; @@ -129,7 +130,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. * @@ -137,7 +139,9 @@ export const assertRunnableOptions = config => { * @returns {Promise} The configuration */ export const createRunConfiguration = async options => { - const config = await loadConfigFile(options.configFile); + const config = options.configFile + ? await loadConfigFile(options.configFile) + : ((await cosmiconfig('doc-kit').search())?.config ?? {}); config.target &&= enforceArray(config.target); // Resolve user configuration first so dynamic defaults can use it