| |
| |
| |
| |
| |
|
|
| import path from 'node:path'; |
| import { fileURLToPath, pathToFileURL } from 'node:url'; |
| import { readFile, writeFile } from 'node:fs/promises'; |
| import { generateSettingsSchema } from './generate-settings-schema.js'; |
| import { |
| escapeBackticks, |
| formatDefaultValue, |
| formatWithPrettier, |
| injectBetweenMarkers, |
| normalizeForCompare, |
| } from './utils/autogen.js'; |
|
|
| import type { |
| SettingDefinition, |
| SettingsSchema, |
| SettingsSchemaType, |
| } from '../packages/cli/src/config/settingsSchema.js'; |
|
|
| const START_MARKER = '<!-- SETTINGS-AUTOGEN:START -->'; |
| const END_MARKER = '<!-- SETTINGS-AUTOGEN:END -->'; |
|
|
| const MANUAL_TOP_LEVEL = new Set(['mcpServers', 'telemetry', 'extensions']); |
|
|
| interface DocEntry { |
| path: string; |
| type: string; |
| label: string; |
| category: string; |
| description: string; |
| defaultValue: string; |
| requiresRestart: boolean; |
| enumValues?: string[]; |
| } |
|
|
| export async function main(argv = process.argv.slice(2)) { |
| const checkOnly = argv.includes('--check'); |
|
|
| await generateSettingsSchema({ checkOnly }); |
|
|
| const repoRoot = path.resolve( |
| path.dirname(fileURLToPath(import.meta.url)), |
| '..', |
| ); |
| const docPath = path.join(repoRoot, 'docs/reference/configuration.md'); |
| const cliSettingsDocPath = path.join(repoRoot, 'docs/cli/settings.md'); |
|
|
| const { getSettingsSchema } = await loadSettingsSchemaModule(); |
| const schema = getSettingsSchema(); |
| const allSettingsSections = collectEntries(schema, { includeAll: true }); |
| const filteredSettingsSections = collectEntries(schema, { |
| includeAll: false, |
| }); |
|
|
| const generatedBlock = renderSections(allSettingsSections); |
| const generatedTableBlock = renderTableSections(filteredSettingsSections); |
|
|
| await updateFile(docPath, generatedBlock, checkOnly); |
| await updateFile(cliSettingsDocPath, generatedTableBlock, checkOnly); |
| } |
|
|
| async function updateFile( |
| filePath: string, |
| newContent: string, |
| checkOnly: boolean, |
| ) { |
| const doc = await readFile(filePath, 'utf8'); |
| const injectedDoc = injectBetweenMarkers({ |
| document: doc, |
| startMarker: START_MARKER, |
| endMarker: END_MARKER, |
| newContent: newContent, |
| paddingBefore: '\n', |
| paddingAfter: '\n', |
| }); |
| const formattedDoc = await formatWithPrettier(injectedDoc, filePath); |
|
|
| if (normalizeForCompare(doc) === normalizeForCompare(formattedDoc)) { |
| if (!checkOnly) { |
| console.log( |
| `Settings documentation (${path.basename(filePath)}) already up to date.`, |
| ); |
| } |
| return; |
| } |
|
|
| if (checkOnly) { |
| console.error( |
| 'Settings documentation (' + |
| path.basename(filePath) + |
| ') is out of date. Run `npm run docs:settings` to regenerate.', |
| ); |
| process.exitCode = 1; |
| return; |
| } |
|
|
| await writeFile(filePath, formattedDoc); |
| console.log( |
| `Settings documentation (${path.basename(filePath)}) regenerated.`, |
| ); |
| } |
|
|
| async function loadSettingsSchemaModule() { |
| const modulePath = '../packages/cli/src/config/settingsSchema.ts'; |
| return import(modulePath); |
| } |
|
|
| function collectEntries( |
| schema: SettingsSchemaType, |
| options: { includeAll?: boolean } = {}, |
| ) { |
| const sections = new Map<string, DocEntry[]>(); |
|
|
| const visit = ( |
| current: SettingsSchema, |
| pathSegments: string[], |
| topLevel?: string, |
| ) => { |
| for (const [key, definition] of Object.entries(current)) { |
| if (pathSegments.length === 0 && MANUAL_TOP_LEVEL.has(key)) { |
| continue; |
| } |
|
|
| const newPathSegments = [...pathSegments, key]; |
| const sectionKey = topLevel ?? key; |
| const hasChildren = |
| definition.type === 'object' && |
| definition.properties && |
| Object.keys(definition.properties).length > 0; |
|
|
| if (definition.ignoreInDocs) { |
| continue; |
| } |
|
|
| if (!hasChildren && (options.includeAll || definition.showInDialog)) { |
| if (!sections.has(sectionKey)) { |
| sections.set(sectionKey, []); |
| } |
|
|
| sections.get(sectionKey)!.push({ |
| path: newPathSegments.join('.'), |
| type: formatType(definition), |
| label: definition.label, |
| category: definition.category, |
| description: formatDescription(definition), |
| defaultValue: formatDefaultValue(definition.default, { |
| quoteStrings: true, |
| }), |
| requiresRestart: Boolean(definition.requiresRestart), |
| enumValues: definition.options?.map((option) => |
| formatDefaultValue(option.value, { quoteStrings: true }), |
| ), |
| }); |
| } |
|
|
| if (hasChildren && definition.properties) { |
| visit(definition.properties, newPathSegments, sectionKey); |
| } |
| } |
| }; |
|
|
| visit(schema, []); |
| return sections; |
| } |
|
|
| function formatDescription(definition: SettingDefinition) { |
| if (definition.description?.trim()) { |
| return definition.description.trim(); |
| } |
| return 'Description not provided.'; |
| } |
|
|
| function formatType(definition: SettingDefinition): string { |
| switch (definition.ref) { |
| case 'StringOrStringArray': |
| return 'string | string[]'; |
| case 'BooleanOrString': |
| return 'boolean | string'; |
| default: |
| return definition.type; |
| } |
| } |
|
|
| function renderSections(sections: Map<string, DocEntry[]>) { |
| const lines: string[] = []; |
|
|
| for (const [section, entries] of sections) { |
| if (entries.length === 0) { |
| continue; |
| } |
|
|
| lines.push('#### `' + section + '`'); |
| lines.push(''); |
|
|
| for (const entry of entries) { |
| lines.push('- **`' + entry.path + '`** (' + entry.type + '):'); |
| lines.push(' - **Description:** ' + entry.description); |
|
|
| if (entry.defaultValue.includes('\n')) { |
| lines.push(' - **Default:**'); |
| lines.push(''); |
| lines.push(' ```json'); |
| lines.push( |
| entry.defaultValue |
| .split('\n') |
| .map((line) => ' ' + line) |
| .join('\n'), |
| ); |
| lines.push(' ```'); |
| } else { |
| lines.push( |
| ' - **Default:** `' + escapeBackticks(entry.defaultValue) + '`', |
| ); |
| } |
|
|
| if (entry.enumValues && entry.enumValues.length > 0) { |
| const values = entry.enumValues |
| .map((value) => '`' + escapeBackticks(value) + '`') |
| .join(', '); |
| lines.push(' - **Values:** ' + values); |
| } |
|
|
| if (entry.requiresRestart) { |
| lines.push(' - **Requires restart:** Yes'); |
| } |
|
|
| lines.push(''); |
| } |
| } |
|
|
| return lines.join('\n').trimEnd(); |
| } |
|
|
| function renderTableSections(sections: Map<string, DocEntry[]>) { |
| const lines: string[] = []; |
|
|
| for (const [section, entries] of sections) { |
| if (entries.length === 0) { |
| continue; |
| } |
|
|
| let title = section.charAt(0).toUpperCase() + section.slice(1); |
| if (title === 'Ui') { |
| title = 'UI'; |
| } else if (title === 'Ide') { |
| title = 'IDE'; |
| } |
| lines.push(`### ${title}`); |
| lines.push(''); |
| lines.push('| UI Label | Setting | Description | Default |'); |
| lines.push('| --- | --- | --- | --- |'); |
|
|
| for (const entry of entries) { |
| const val = entry.defaultValue.replace(/\n/g, ' '); |
| const defaultVal = '`' + escapeBackticks(val) + '`'; |
| lines.push( |
| '| ' + |
| entry.label + |
| ' | `' + |
| entry.path + |
| '` | ' + |
| entry.description + |
| ' | ' + |
| defaultVal + |
| ' |', |
| ); |
| } |
|
|
| lines.push(''); |
| } |
|
|
| return lines.join('\n').trimEnd(); |
| } |
|
|
| if (process.argv[1]) { |
| const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href; |
| if (entryUrl === import.meta.url) { |
| await main(); |
| } |
| } |
|
|