Skip to content
Open
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/node-preset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@node-core/doc-kit': minor
---

Add the `@node-core/doc-kit/config` preset
6 changes: 6 additions & 0 deletions .changeset/plain-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@nodejs/doc-kit': patch
'@nodejs/doc-kit-generator-react': minor
---

Defaults are now project-neutral instead of Node.js-specific
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ jobs:
path: node

- name: Build docs
run: npx doc-kit generate -t web -i "./node/doc/api/assert.md" -o out
run: npx doc-kit generate -t web -i "./node/doc/api/assert.md" -o out --config-file ./e2e/doc-kit.config.mjs

- name: Install Playwright browsers
run: npx playwright install --with-deps
Expand Down
2 changes: 1 addition & 1 deletion .vercelignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Ignored the cloned `node` folder
node
/node
2 changes: 2 additions & 0 deletions beta/doc-kit.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default {
extends: '@node-core/doc-kit/config',

html: {
remoteConfigUrl:
'https://raw.githubusercontent.com/nodejs/doc-kit/main/beta/site.json',
Expand Down
87 changes: 52 additions & 35 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ uses the `doc-kit` property:
```json
{
"doc-kit": {
"target": ["json"],
"target": ["json-simple"],
"global": {
"input": "doc/api/*.md",
"output": "out"
Expand All @@ -41,70 +41,86 @@ export default {
// generator module (e.g. '@my-scope/my-package/my-generator').
target: ['orama-db', 'html'],
global: {
version: '20.0.0',
minify: true,
repository: 'nodejs/node',
ref: 'main',
baseURL: 'https://nodejs.org/docs/',
input: 'src/',
project: 'My Project',
version: '1.2.0',
input: 'docs/**/*.md',
output: 'dist/',
ignore: ['node_modules/', 'test/'],
changelog:
'https://raw.githubusercontent.com/nodejs/node/main/CHANGELOG.md',
index:
'https://raw.githubusercontent.com/nodejs/node/main/doc/api/index.md',
baseURL: 'https://example.com/docs/',
},

threads: 4,
chunkSize: 10,

// Generator-specific configurations
json: {
format: 'json',
minify: false, // Override global setting
},

html: {
format: 'html',
title: '{project} Documentation',
},

metadata: {
typeMap: {
String: 'string',
Number: 'number',
Boolean: 'boolean',
MyThing: 'https://example.com/docs/my-thing.html',
},
},
};
```

## Extending Presets

A configuration file may declare `extends`: one or more presets whose values
are merged underneath its own. Each entry is either an import specifier of a
module whose default export is a configuration object, or a path relative to
the configuration file:

```mjs
export default {
// Build the docs the way nodejs.org does — branding, URL layouts,
// and release history included
extends: '@node-core/doc-kit/config',

html: {
// Your own values still win over the preset
title: '{project} {version} API Reference',
},
};
```

`extends` also accepts an array; later presets take precedence over earlier
ones, and the configuration file itself wins over all of them.

The built-in defaults are deliberately project-neutral: no repository,
site URL, release history, or branding is assumed. The
[`@node-core/doc-kit/config`](https://github.com/nodejs/doc-kit/tree/main/packages/node)
preset opts back into everything Node.js-specific.

## Configuration Structure

### Global Configuration

The `global` object contains settings that apply to all generators unless overridden:

| Property | Type | Description | Default |
| ------------ | ------------------ | ------------------------------------------ | -------------------------------------------------- |
| `version` | `string \| SemVer` | Documentation version | `process.version` |
| `minify` | `boolean` | Whether to minify output | `true` |
| `repository` | `string` | GitHub repository in `owner/repo` format | `'nodejs/node'` |
| `ref` | `string` | Git reference (branch, tag, or commit SHA) | `'HEAD'` |
| `baseURL` | `string \| URL` | Base URL for documentation | `'https://nodejs.org/docs'` |
| `input` | `string[]` | Input directory path | - |
| `output` | `string` | Output directory path | - |
| `ignore` | `string[]` | Patterns to ignore | `[]` |
| `changelog` | `string \| URL` | Changelog URL | Auto-generated URL based on `ref` and `repository` |
| `index` | `string \| URL` | Index URL | - |
| Property | Type | Description | Default |
| ------------ | ------------------------ | ----------------------------------------------------------------------------------------- | --------------------------------- |
| `project` | `string` | Name of the project being documented, used in titles, logos, and templated text | The `name` in your `package.json` |
| `version` | `string \| SemVer` | Documentation version | `process.version` |
| `minify` | `boolean` | Whether to minify output | `true` |
| `repository` | `string` | GitHub repository in `owner/repo` format; without one, repository UI is omitted | - |
| `ref` | `string` | Git reference (branch, tag, or commit SHA) | `'HEAD'` |
| `baseURL` | `string \| URL` | Base URL of the published site, used wherever absolute URLs are needed | - |
| `input` | `string[]` | Input directory path | - |
| `output` | `string` | Output directory path | - |
| `ignore` | `string[]` | Patterns to ignore | `[]` |
| `changelog` | `string \| URL \| Array` | Release history used for version selectors; a URL or path to parse, or a pre-parsed array | `[]` (single-version output) |
| `index` | `string \| URL \| Array` | Index URL | - |

### Generator-Specific Configuration

Each generator (e.g., `json`, `html`, `markdown`) can have its own configuration that overrides global settings:
Each generator (e.g., `html`, `legacy-json`) can have its own configuration that overrides global settings:

```javascript
export default {
global: {
version: '20.0.0',
version: '1.2.0',
minify: true,
},

Expand All @@ -121,7 +137,8 @@ precedence):

1. **CLI options** (command-line arguments)
2. **Configuration file** (discovered or selected with `--config-file`)
3. **Default values** (built-in defaults)
3. **Presets** (listed in the configuration file's `extends`)
4. **Default values** (built-in defaults)

## CLI Options Mapping

Expand Down
7 changes: 7 additions & 0 deletions e2e/doc-kit.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
html: {
// The announcement banner under test is opt-in: point it at the URL the
// spec intercepts (no real request is made).
remoteConfigUrl: 'https://nodejs.org/site.json',
},
};
55 changes: 55 additions & 0 deletions packages/core/src/utils/configuration/__tests__/index.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import assert from 'node:assert';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it, mock, beforeEach } from 'node:test';

// Mock dependencies
Expand Down Expand Up @@ -96,6 +99,47 @@ describe('config.mjs', () => {
);
assert.strictEqual(mockConfigSearch.mock.calls.length, 0);
});

it('should merge extends presets underneath the config file', async () => {
const dir = mkdtempSync(join(tmpdir(), 'doc-kit-config-'));

writeFileSync(
join(dir, 'base.mjs'),
'export default { global: { project: "Base", ref: "base" }, html: { a: 1 } };'
);
writeFileSync(
join(dir, 'other.mjs'),
'export default { global: { project: "Other" }, html: { b: 2 } };'
);

mockConfigLoad.mock.mockImplementationOnce(async () => ({
config: {
extends: ['./base.mjs', './other.mjs'],
global: { ref: 'own' },
},
filepath: join(dir, 'doc-kit.config.mjs'),
}));

const result = await loadConfigFile('any');

// Later presets win over earlier ones; the file itself wins over all
assert.deepStrictEqual(result, {
global: { project: 'Other', ref: 'own' },
html: { a: 1, b: 2 },
});
});

it('should resolve extends package specifiers from the config file', async () => {
mockConfigLoad.mock.mockImplementationOnce(async () => ({
config: { extends: '@node-core/doc-kit/config' },
filepath: join(process.cwd(), 'doc-kit.config.mjs'),
}));

const result = await loadConfigFile('any');

assert.strictEqual(result.global.project, 'Node.js');
assert.strictEqual(result.global.repository, 'nodejs/node');
});
});

describe('createConfigFromCLIOptions', () => {
Expand Down Expand Up @@ -250,6 +294,17 @@ describe('config.mjs', () => {
assert.strictEqual(mockConfigSearch.mock.calls.length, 1);
});

it('should default to project-neutral values', async () => {
const config = await createRunConfiguration({});

// No repository, site, or release history is assumed; presets such as
// @node-core/doc-kit/config opt back into the Node.js values
assert.strictEqual(config.global.repository, undefined);
assert.strictEqual(config.global.baseURL, undefined);
assert.deepStrictEqual(config.global.changelog, []);
assert.strictEqual(typeof config.global.project, 'string');
});

it('should handle generator-specific overrides', async () => {
mockConfigLoad.mock.mockImplementationOnce(async () => ({
config: createMockConfig({
Expand Down
60 changes: 52 additions & 8 deletions packages/core/src/utils/configuration/index.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { cpus } from 'node:os';
import { dirname, isAbsolute, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { isMainThread } from 'node:worker_threads';

import { cosmiconfig } from 'cosmiconfig';
import { coerce } from 'semver';

import { CHANGELOG_URL, populate } from './templates.mjs';
import {
loadGenerators,
resolveGeneratorSpecifier,
Expand All @@ -17,6 +20,20 @@ import { deepMerge } from '../misc.mjs';

const configExplorer = cosmiconfig('doc-kit');

/**
* The name of the project being documented, from the manifest in the working
* directory. Generators use it for titles, logos, and templated text.
*
* @returns {string | undefined}
*/
const detectProject = () => {
try {
return JSON.parse(readFileSync('package.json', 'utf-8')).name;
} catch {
return undefined;
}
};

/**
* Get's the default configuration for the loaded generators
*
Expand All @@ -37,15 +54,13 @@ export const getDefaultConfig = (generators, config) =>
},
/** @type {import('./types').Configuration} */ ({
global: {
project: detectProject() ?? 'API Docs',
version: process.version,
minify: true,
repository: 'nodejs/node',
ref: 'HEAD',
baseURL: 'https://nodejs.org/docs',
changelog: populate(CHANGELOG_URL, {
repository: 'nodejs/node',
ref: 'HEAD',
}),
// Without release history there is nothing to build a version picker
// from, so generators render single-version output.
changelog: [],
pathsToCopy: ['assets', 'public', 'static'],
},

Expand All @@ -58,6 +73,23 @@ export const getDefaultConfig = (generators, config) =>
})
);

/**
* Resolves an `extends` entry of a configuration file into an importable
* URL: relative paths resolve against the configuration file, anything else
* resolves as a package import specifier (e.g. `@node-core/doc-kit/config`).
*
* @param {string} specifier - The `extends` entry
* @param {string} configFilePath - The configuration file it appears in
* @returns {string} A `file:` URL to import
*/
const resolveConfigExtends = (specifier, configFilePath) => {
if (specifier.startsWith('.') || isAbsolute(specifier)) {
return pathToFileURL(resolve(dirname(configFilePath), specifier)).href;
}

return pathToFileURL(createRequire(configFilePath).resolve(specifier)).href;
};

/**
* Loads an explicit configuration file or searches for one using cosmiconfig.
*
Expand All @@ -69,7 +101,19 @@ export const loadConfigFile = async filePath => {
? await configExplorer.load(filePath)
: await configExplorer.search();

return result?.config ?? {};
if (!result) {
return {};
}

let { extends: presets, ...config } = result.config ?? {};

for (const preset of enforceArray(presets ?? []).toReversed()) {
const module = await import(resolveConfigExtends(preset, result.filepath));

config = deepMerge(module.default ?? module, config);
}

return config;
};

/**
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/utils/configuration/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ export type Configuration = {
};

export type GlobalConfiguration = {
// The repository
repository: string;
// The name of the project being documented, used for titles, logos, and
// templated text (defaults to the `name` in the working directory's
// `package.json`)
project: string;

// The repository (`owner/name`), used for source and edit links; when
// omitted, repository-specific UI (e.g. the GitHub link) is omitted
repository?: string;

// The path to the input source files. This parameter accepts globs and can
// be a glob when passed to a generator.
Expand All @@ -49,8 +55,9 @@ export type GlobalConfiguration = {
// A list of all the titles of all the documentation files
index: Array<{ section: string; api: string }>;

// The base URL
baseURL: string | URL;
// The base URL of the published site; templates referencing `{baseURL}`
// (e.g. sitemap and llms-txt page URLs) need it to produce absolute URLs
baseURL?: string | URL;

// Git ref (i.e. HEAD)
ref: string;
Expand Down
1 change: 1 addition & 0 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"exports": {
"./addon-verify": "./src/addon-verify/index.mjs",
"./api-links": "./src/api-links/index.mjs",
"./config": "./src/config/index.mjs",
"./man-page": "./src/man-page/index.mjs",
"./package.json": "./package.json"
},
Expand Down
Loading
Loading