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 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 && } { }); 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' }); - }); }); 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 a31bc35a..e04c0bba 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -15,10 +15,16 @@ import { deepMerge, lazy } from '../misc.mjs'; /** * Get's the default configuration */ -export const getDefaultConfig = lazy(() => +export const getDefaultConfig = lazy(config => Object.keys(allGenerators).reduce( (acc, k) => { - acc[k] = allGenerators[k].defaultConfiguration ?? {}; + acc[k] = + 'defaultConfiguration' in allGenerators[k] + ? typeof allGenerators[k].defaultConfiguration === 'function' + ? allGenerators[k].defaultConfiguration(config) + : allGenerators[k].defaultConfiguration + : {}; + return acc; }, /** @type {import('./types').Configuration} */ ({ @@ -134,12 +140,10 @@ export const createRunConfiguration = async options => { const config = await loadConfigFile(options.configFile); config.target &&= enforceArray(config.target); - // Merge with defaults - const merged = deepMerge( - config, - createConfigFromCLIOptions(options), - getDefaultConfig() - ); + // 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); + }, {}); };