From 288d7d923c15bc8b448a5b3f5906f6e799312f68 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 13 Jul 2026 06:59:40 -0700 Subject: [PATCH 1/6] feat: allow dynamic configurations --- src/generators/web/index.mjs | 10 ++++++++-- src/generators/web/ui/components/NavBar.jsx | 4 ++-- src/utils/configuration/index.mjs | 11 ++++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs index 080a88e9..97d6f3c8 100644 --- a/src/generators/web/index.mjs +++ b/src/generators/web/index.mjs @@ -30,7 +30,10 @@ export default createLazyGenerator({ dependsOn: 'jsx-ast', - defaultConfiguration: { + /** + * @param {import('../../utils/configuration/types').Configuration} config + */ + defaultConfiguration: config => ({ templatePath: join(import.meta.dirname, 'template.html'), project: 'Node.js', title: '{project} {version} Documentation', @@ -38,6 +41,9 @@ export default createLazyGenerator({ editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`, pageURL: '{baseURL}/latest-{version}/api{path}.html', remoteConfigUrl: 'https://nodejs.org/site.json', + // By default, the search box is only shown when we are _also_ building search data + showSearchBox: + Array.isArray(config.target) && config.target.includes('orama-db'), // Project-specific document `` contents. `meta` and `links` are // arrays of attribute bags (boolean `true` renders a valueless attribute, @@ -97,5 +103,5 @@ export default createLazyGenerator({ // Options merged into the Rolldown build (client and server), e.g. extra // `plugins`. See the README for the merge semantics. rolldown: {}, - }, + }), }); diff --git a/src/generators/web/ui/components/NavBar.jsx b/src/generators/web/ui/components/NavBar.jsx index 15fa85dd..c0484e80 100644 --- a/src/generators/web/ui/components/NavBar.jsx +++ b/src/generators/web/ui/components/NavBar.jsx @@ -6,7 +6,7 @@ import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub'; import SearchBox from './SearchBox'; import { useTheme } from '../hooks/useTheme.mjs'; -import { repository } from '#theme/config'; +import { repository, showSearchBox } from '#theme/config'; import Logo from '#theme/Logo'; /** @@ -21,7 +21,7 @@ export default ({ metadata }) => { sidebarItemTogglerAriaLabel="Toggle navigation menu" navItems={[]} > - + {showSearchBox && } +export const getDefaultConfig = lazy(config => Object.keys(allGenerators).reduce( (acc, k) => { - acc[k] = allGenerators[k].defaultConfiguration ?? {}; + if ('defaultConfiguration' in allGenerators[k]) { + acc[k] = + typeof allGenerators[k].defaultConfiguration === 'function' + ? allGenerators[k].defaultConfiguration(config) + : allGenerators[k].defaultConfiguration; + } return acc; }, /** @type {import('./types').Configuration} */ ({ @@ -138,7 +143,7 @@ export const createRunConfiguration = async options => { const merged = deepMerge( config, createConfigFromCLIOptions(options), - getDefaultConfig() + getDefaultConfig(config) ); // These need to be coerced From 04874438e6ec4d625515d9547b2fa5cbf8e52618 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 13 Jul 2026 07:05:36 -0700 Subject: [PATCH 2/6] feat: allow dynamic configurations --- src/utils/configuration/index.mjs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index d2cb78a1..01fec003 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -18,12 +18,13 @@ import { deepMerge, lazy } from '../misc.mjs'; export const getDefaultConfig = lazy(config => Object.keys(allGenerators).reduce( (acc, k) => { - if ('defaultConfiguration' in allGenerators[k]) { - acc[k] = - typeof allGenerators[k].defaultConfiguration === 'function' + acc[k] = + 'defaultConfiguration' in allGenerators[k] + ? typeof allGenerators[k].defaultConfiguration === 'function' ? allGenerators[k].defaultConfiguration(config) - : allGenerators[k].defaultConfiguration; - } + : allGenerators[k].defaultConfiguration + : {}; + return acc; }, /** @type {import('./types').Configuration} */ ({ From d4610f445bb60b5dd31a8118de2bec0f8abfe059 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 14 Jul 2026 13:04:09 -0700 Subject: [PATCH 3/6] fixup! --- src/utils/configuration/index.mjs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index 01fec003..74486bb2 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -141,11 +141,8 @@ export const createRunConfiguration = async options => { config.target &&= enforceArray(config.target); // Merge with defaults - const merged = deepMerge( - config, - createConfigFromCLIOptions(options), - getDefaultConfig(config) - ); + const intermediate = deepMerge(config, createConfigFromCLIOptions(options)); + const merged = deepMerge(intermediate, getDefaultConfig(intermediate)); // These need to be coerced merged.threads = Math.max(merged.threads, 1); From 953b64846fa451e697e751e0b66f1d44d8aeb63f Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 14 Jul 2026 14:31:06 -0700 Subject: [PATCH 4/6] Create dynamic-configuration.md --- .changeset/dynamic-configuration.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dynamic-configuration.md diff --git a/.changeset/dynamic-configuration.md b/.changeset/dynamic-configuration.md new file mode 100644 index 00000000..1c2de119 --- /dev/null +++ b/.changeset/dynamic-configuration.md @@ -0,0 +1,5 @@ +--- +'@node-core/doc-kit': patch +--- + +Allow for the specification of dynamically generated configuration values From cb9bc8c6c67d6e072ecb8c00b36f3e23a5d95cff Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 14 Jul 2026 14:52:28 -0700 Subject: [PATCH 5/6] fixup! --- .../configuration/__tests__/index.test.mjs | 23 ++++++++++- src/utils/configuration/index.mjs | 7 ++-- src/utils/misc.mjs | 40 +++++++++---------- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/utils/configuration/__tests__/index.test.mjs b/src/utils/configuration/__tests__/index.test.mjs index ac7bf504..3de2dc08 100644 --- a/src/utils/configuration/__tests__/index.test.mjs +++ b/src/utils/configuration/__tests__/index.test.mjs @@ -18,6 +18,12 @@ mock.module('../../../generators/index.mjs', { json: { defaultConfiguration: { format: 'json' } }, html: { defaultConfiguration: { format: 'html' } }, markdown: {}, + web: { + defaultConfiguration: config => ({ + showSearchBox: + Array.isArray(config.target) && config.target.includes('orama-db'), + }), + }, }, }, }); @@ -149,20 +155,33 @@ describe('config.mjs', () => { }); describe('createRunConfiguration', () => { - it('should merge config sources in correct order', async () => { + it('should let defined CLI options override the config file', async () => { mockImportFromURL.mock.mockImplementationOnce(async () => - createMockConfig({ global: { input: 'custom-src/' } }) + createMockConfig({ + global: { + input: 'custom-src/', + output: 'config-dist/', + version: '18.0.0', + }, + target: ['html'], + threads: 1, + }) ); const config = await createRunConfiguration({ configFile: 'config.mjs', output: 'custom-dist/', + version: '20.0.0', + target: ['html', 'orama-db'], threads: 2, }); assert.strictEqual(config.global.input, 'custom-src/'); assert.strictEqual(config.global.output, 'custom-dist/'); + assert.strictEqual(config.global.version.version, '20.0.0'); + assert.deepStrictEqual(config.target, ['html', 'orama-db']); assert.strictEqual(config.threads, 2); + assert.strictEqual(config.web.showSearchBox, true); }); it('should transform string values only once', async () => { diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index 74486bb2..e04c0bba 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -140,9 +140,10 @@ export const createRunConfiguration = async options => { const config = await loadConfigFile(options.configFile); config.target &&= enforceArray(config.target); - // Merge with defaults - const intermediate = deepMerge(config, createConfigFromCLIOptions(options)); - const merged = deepMerge(intermediate, getDefaultConfig(intermediate)); + // Resolve user configuration first so dynamic defaults can use it + const cliConfig = createConfigFromCLIOptions(options); + const intermediate = deepMerge(config, cliConfig); + const merged = deepMerge(getDefaultConfig(intermediate), intermediate); // These need to be coerced merged.threads = Math.max(merged.threads, 1); diff --git a/src/utils/misc.mjs b/src/utils/misc.mjs index 7b257b64..92d91e01 100644 --- a/src/utils/misc.mjs +++ b/src/utils/misc.mjs @@ -16,8 +16,14 @@ export const lazy = fn => * @param {*} value - The value to check * @returns {boolean} True if the value is a plain object, false otherwise */ -export const isPlainObject = value => - value !== null && typeof value === 'object' && !Array.isArray(value); +export const isPlainObject = value => { + if (value === null || typeof value !== 'object') { + return false; + } + + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +}; /** * Checks if a value is an async generator/iterable. @@ -41,32 +47,26 @@ export const omitKeys = (obj, keys = []) => ); /** - * Recursively merges multiple objects deeply. + * Recursively merges plain objects from left to right. * @template T * @param {...T} objects - Any number of objects to merge * @returns {T} A new object containing the deep merge of all provided objects */ export const deepMerge = (...objects) => { - const base = objects.pop(); - return objects.reduce((result, source) => { - for (const key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - const sourceValue = source[key]; - const isSourcePlainObject = isPlainObject(sourceValue); - - const targetValue = result[key]; - const baseValue = base[key]; - - result[key] = - isSourcePlainObject && isPlainObject(targetValue) - ? deepMerge(targetValue, sourceValue, baseValue ?? {}) - : isSourcePlainObject && isPlainObject(baseValue) - ? deepMerge(sourceValue, baseValue) - : (sourceValue ?? baseValue); + for (const [key, sourceValue] of Object.entries(source)) { + if (sourceValue === undefined) { + continue; } + + const targetValue = result[key]; + result[key] = isPlainObject(sourceValue) + ? deepMerge(isPlainObject(targetValue) ? targetValue : {}, sourceValue) + : Array.isArray(sourceValue) + ? sourceValue.slice() + : sourceValue; } return result; - }, base); + }, {}); }; From 1dda708a4d7d4aae0595f0b32d251d27d4a51613 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 14 Jul 2026 15:32:48 -0700 Subject: [PATCH 6/6] fixup! --- src/utils/__tests__/misc.test.mjs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/utils/__tests__/misc.test.mjs b/src/utils/__tests__/misc.test.mjs index e673ec78..52b739df 100644 --- a/src/utils/__tests__/misc.test.mjs +++ b/src/utils/__tests__/misc.test.mjs @@ -114,18 +114,13 @@ describe('omitKeys', () => { }); describe('deepMerge', () => { - it('should merge flat objects with source taking precedence over base', () => { + it('should merge flat objects with latter-arguments taking precedence', () => { const result = deepMerge({ a: 1, b: 2 }, { b: 10, c: 3 }); - assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 }); + assert.deepStrictEqual(result, { a: 1, b: 10, c: 3 }); }); it('should merge nested objects recursively', () => { const result = deepMerge({ nested: { a: 1 } }, { nested: { b: 2 } }); assert.deepStrictEqual(result, { nested: { a: 1, b: 2 } }); }); - - it('should use base values when source values are undefined', () => { - const result = deepMerge({ a: undefined }, { a: 'base' }); - assert.deepStrictEqual(result, { a: 'base' }); - }); });