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);
+ }, {});
};