Skip to content

Commit 34d5149

Browse files
committed
feat: add configuration docs
1 parent 5c8a41e commit 34d5149

4 files changed

Lines changed: 200 additions & 12 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"build:data:blog": "node scripts/data/blog.mjs",
66
"build:data:sponsors": "node scripts/data/sponsors.mjs",
77
"build:md": "npm-run-all build:md:*",
8-
"build:md:api": "node scripts/markdown/api.mjs",
8+
"build:md:api": "node scripts/markdown/api/index.mjs && node scripts/markdown/api/configuration.mjs",
99
"build:md:readmes": "node scripts/markdown/readmes.mjs",
1010
"build:md:governance": "node scripts/markdown/governance.mjs",
1111
"build:html": "node scripts/html/index.mjs",
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { readFile, writeFile } from 'node:fs/promises';
2+
import { join } from 'node:path/posix';
3+
import { major } from 'semver';
4+
import { sources } from './utils.mjs';
5+
6+
const definitionName = ref =>
7+
ref?.startsWith('#/definitions/') ? ref.slice('#/definitions/'.length) : '';
8+
9+
const collapse = text => text?.replace(/\s+/g, ' ').trim();
10+
11+
const trimImports = text =>
12+
collapse(text.replace(/import\((?:"[^"]*"|'[^']*')\)\./g, ''));
13+
14+
const literal = value =>
15+
typeof value === 'string' ? JSON.stringify(value) : String(value);
16+
17+
const isStructural = schema =>
18+
schema.type === 'object' || Boolean(schema.properties);
19+
20+
/**
21+
* Render a schema as the type text used inside a doc-kit `{...}` annotation.
22+
* Named definitions stay names (the type map links them to the generated API
23+
* type pages), `tsType` schemas keep their TypeScript text verbatim, and
24+
* anonymous objects collapse to `object`.
25+
*/
26+
const summarize = (schema, definitions, stack = new Set()) => {
27+
if (!schema) return 'unknown';
28+
29+
const ref = definitionName(schema.$ref);
30+
if (ref) {
31+
const target = definitions[ref];
32+
// Objects keep their linkable name; unions, enums, and scalars read better
33+
// inlined. Cycles (for example RuleSetCondition) fall back to the name.
34+
if (!target || stack.has(ref) || isStructural(target)) return ref;
35+
return summarize(target, definitions, new Set(stack).add(ref));
36+
}
37+
38+
if (schema.tsType) return trimImports(schema.tsType);
39+
40+
if (schema.enum) {
41+
return [...new Set(schema.enum.map(literal))].join(' | ');
42+
}
43+
44+
if (schema.instanceof) return schema.instanceof;
45+
46+
const alternatives = schema.anyOf ?? schema.oneOf;
47+
if (alternatives) {
48+
let parts = [
49+
...new Set(
50+
alternatives.map(alternative =>
51+
summarize(alternative, definitions, stack)
52+
)
53+
),
54+
];
55+
if (parts.includes('true') && parts.includes('false')) {
56+
parts = parts
57+
.map(part => (part === 'true' ? 'boolean' : part))
58+
.filter(part => part !== 'false');
59+
}
60+
return parts.join(' | ');
61+
}
62+
63+
if (schema.type === 'array') {
64+
const item =
65+
schema.items && !Array.isArray(schema.items)
66+
? summarize(schema.items, definitions, stack)
67+
: 'any';
68+
return item.includes(' ') ? `(${item})[]` : `${item}[]`;
69+
}
70+
71+
if (isStructural(schema) || schema.additionalProperties) return 'object';
72+
if (schema.type === 'integer') return 'number';
73+
if (Array.isArray(schema.type)) return schema.type.join(' | ');
74+
if (schema.type) return schema.type;
75+
76+
return 'unknown';
77+
};
78+
79+
/**
80+
* Find the single object-with-properties schema an option resolves to, so its
81+
* sub-options can be documented in place. Options offering several object
82+
* shapes (for example `cache`) are left to their linked type pages.
83+
*/
84+
const expandableObject = (schema, definitions, stack = new Set()) => {
85+
const ref = definitionName(schema.$ref);
86+
if (ref) {
87+
const target = definitions[ref];
88+
if (!target || stack.has(ref)) return null;
89+
return expandableObject(target, definitions, new Set(stack).add(ref));
90+
}
91+
92+
if (schema.properties) return schema;
93+
94+
const alternatives = schema.anyOf ?? schema.oneOf;
95+
if (alternatives) {
96+
const objects = alternatives
97+
.map(alternative => expandableObject(alternative, definitions, stack))
98+
.filter(Boolean);
99+
return objects.length === 1 ? objects[0] : null;
100+
}
101+
102+
return null;
103+
};
104+
105+
const descriptionOf = (schema, definitions) => {
106+
if (schema.description) return collapse(schema.description);
107+
108+
const ref = definitionName(schema.$ref);
109+
return ref ? collapse(definitions[ref]?.description) : undefined;
110+
};
111+
112+
const propertyBullet = (name, schema, definitions) => {
113+
const description = descriptionOf(schema, definitions);
114+
const type = summarize(schema, definitions);
115+
return ` * \`${name}\` {${type}}${description ? ` - ${description}` : ''}`;
116+
};
117+
118+
const renderOption = (path, schema, definitions, depth) => {
119+
const lines = [`${'#'.repeat(depth + 2)} \`${path}\``, ''];
120+
121+
const description = descriptionOf(schema, definitions);
122+
if (description) lines.push(description, '');
123+
124+
lines.push(`* Type: {${summarize(schema, definitions)}}`);
125+
126+
const objectSchema = expandableObject(schema, definitions);
127+
const properties = Object.entries(objectSchema?.properties ?? {});
128+
129+
if (depth === 0) {
130+
lines.push('');
131+
for (const [name, child] of properties) {
132+
lines.push(...renderOption(`${path}.${name}`, child, definitions, 1));
133+
}
134+
} else {
135+
// Sub-option details nest under the type annotation.
136+
for (const [name, child] of properties) {
137+
lines.push(propertyBullet(name, child, definitions));
138+
}
139+
lines.push('');
140+
}
141+
142+
return lines;
143+
};
144+
145+
const generate = async packageDir => {
146+
const { version } = JSON.parse(
147+
await readFile(join(packageDir, 'package.json'), 'utf8')
148+
);
149+
const schema = JSON.parse(
150+
await readFile(join(packageDir, 'schemas', 'WebpackOptions.json'), 'utf8')
151+
);
152+
153+
const outputDir = join('pages', 'docs', 'api', `v${major(version)}.x`);
154+
155+
const { definitions } = schema;
156+
157+
const lines = [
158+
'---',
159+
'source: https://github.com/webpack/webpack/edit/main/schemas/WebpackOptions.json',
160+
'---',
161+
'',
162+
'# Configuration Options',
163+
'',
164+
'webpack is configured with an options object, usually exported from a `webpack.config.js` file. ' +
165+
'Every build validates that object against the options schema ' +
166+
'([`schemas/WebpackOptions.json`](https://github.com/webpack/webpack/blob/main/schemas/WebpackOptions.json)), ' +
167+
'so unknown or malformed options fail with a descriptive error. ' +
168+
'This page is generated from that schema and lists every supported option; ' +
169+
'named types link to their full definitions in the API documentation.',
170+
'',
171+
];
172+
173+
for (const [name, child] of Object.entries(schema.properties)) {
174+
lines.push(...renderOption(name, child, definitions, 0));
175+
}
176+
177+
await writeFile(
178+
join(outputDir, 'configuration.md'),
179+
lines.join('\n'),
180+
'utf8'
181+
);
182+
};
183+
184+
for (const source of sources) {
185+
await generate(source);
186+
}
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { readdir, readFile } from 'node:fs/promises';
1+
import { readFile } from 'node:fs/promises';
22
import { join } from 'node:path/posix';
33
import { Application } from 'typedoc';
44
import { major } from 'semver';
5-
6-
const CACHE_DIR = join('.', '.cache', 'webpack');
5+
import { sources } from './utils.mjs';
76

87
const generate = async packageDir => {
98
const { version } = JSON.parse(
@@ -42,14 +41,6 @@ const generate = async packageDir => {
4241
await app.generateOutputs(project);
4342
};
4443

45-
const [packageDir] = process.argv.slice(2);
46-
47-
const sources = packageDir
48-
? [packageDir]
49-
: (await readdir(CACHE_DIR, { withFileTypes: true }))
50-
.filter(entry => entry.isDirectory())
51-
.map(entry => join(CACHE_DIR, entry.name));
52-
5344
for (const source of sources) {
5445
await generate(source);
5546
}

scripts/markdown/api/utils.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { readdir } from 'node:fs/promises';
2+
import { join } from 'node:path/posix';
3+
4+
const [packageDir] = process.argv.slice(2);
5+
const cacheDir = join('.', '.cache', 'webpack');
6+
7+
export const sources = packageDir
8+
? [packageDir]
9+
: (await readdir(cacheDir, { withFileTypes: true }))
10+
.filter(entry => entry.isDirectory())
11+
.map(entry => join(cacheDir, entry.name));

0 commit comments

Comments
 (0)