import stringify from 'json-stringify-pretty-compact'; import { getOptions } from '../../lib/config/options'; import { getManagerList } from '../../lib/modules/manager'; import { getCliName } from '../../lib/workers/global/config/parse/cli'; import { getEnvName } from '../../lib/workers/global/config/parse/env'; import { readFile, updateFile } from '../utils'; const options = getOptions(); const managers = new Set(getManagerList()); /** * Merge string arrays one by one * Example: let arr1 = ['a','b','c'], arr2 = ['1','2','3','4','5'] * merge(arr1,arr2) = ['a','1','b','2','c','3','4','5'] * @param array1 * @param array2 */ function merge(array1: string[], array2: string[]): string[] { const arr1 = [...array1]; const arr2 = [...array2]; const merged: string[] = []; for (const str1 of arr1) { merged.push(str1); const str2 = arr2.pop(); if (str2 !== undefined) { merged.push(str2); } } return merged.concat(arr2); } function indent( strings: TemplateStringsArray, ...keys: (string | number | boolean)[] ): string { const indent = ' '; const strs = [...strings]; let amount = 0; // validate input if (typeof keys[0] === 'number' && strings[0] === '') { amount = keys.shift() as number; strs.shift(); } return indent.repeat(amount) + merge(strs, keys.map(String)).join(''); } function buildHtmlTable(data: string[][]): string { // skip empty tables if (data.length < 2) { return ''; } let table = `\n`; for (const [i, row] of data.entries()) { if (i === 0) { table += indent`${1}\n`; } if (i === 1) { table += indent`${1}\n` + indent`${1}\n`; } table += indent`${2}\n`; for (const col of row) { if (i === 0) { table += indent`${3}\n`; continue; } table += indent`${3}\n`; } table += indent`${2}\n`; } table += indent`${1}\n
${col}${col}` + (`${col}`.endsWith('\n') ? indent`${3}` : '') + `
\n`; return table; } function genTable(obj: [string, string][], type: string, def: any): string { const data = [['Name', 'Value']]; const name = obj[0][1]; const ignoredKeys = [ 'name', 'description', 'default', 'stage', 'allowString', 'admin', 'globalOnly', 'experimental', 'experimentalDescription', 'experimentalIssues', ]; obj.forEach(([key, val]) => { const el = [key, val]; if (key === 'cli' && !val) { ignoredKeys.push('cli'); } if (key === 'env' && !val) { ignoredKeys.push('env'); } if ( !ignoredKeys.includes(el[0]) || (el[0] === 'default' && (typeof el[1] !== 'object' || ['array', 'object'].includes(type)) && name !== 'prBody') ) { if (type === 'string' && el[0] === 'default') { el[1] = `"${el[1]}"`; } if ( (type === 'boolean' && el[0] === 'default') || el[0] === 'cli' || el[0] === 'env' ) { el[1] = `${el[1]}`; } // objects and arrays should be printed in JSON notation if ((type === 'object' || type === 'array') && el[0] === 'default') { // only show array and object defaults if they are not null and are not empty if (Object.keys(el[1] ?? []).length === 0) { return; } el[1] = `\n\`\`\`json\n${stringify(el[1], { indent: 2 })}\n\`\`\`\n`; } data.push(el); } }); if (type === 'list') { data.push(['default', '`[]`']); } if (type === 'string' && def === undefined) { data.push(['default', 'null']); } if (type === 'boolean' && def === undefined) { data.push(['default', 'true']); } if (type === 'boolean' && def === null) { data.push(['default', 'null']); } return buildHtmlTable(data); } function stringifyArrays(el: Record): void { const ignoredKeys = ['default', 'experimentalIssues']; for (const [key, value] of Object.entries(el)) { if (!ignoredKeys.includes(key) && Array.isArray(value)) { el[key] = value.join(', '); } } } function genExperimentalMsg(el: Record): string { const ghIssuesUrl = 'https://github.com/renovatebot/renovate/issues/'; let warning = '\n\n!!! warning "This feature is flagged as experimental"\n'; if (el.experimentalDescription) { warning += indent`${2}${el.experimentalDescription}`; } else { warning += indent`${2}Experimental features might be changed or even removed at any time.`; } const issues = el.experimentalIssues ?? []; if (issues.length > 0) { warning += `
To track this feature visit the following GitHub ${ issues.length > 1 ? 'issues' : 'issue' } `; warning += (issues .map((issue: number) => `[#${issue}](${ghIssuesUrl}${issue})`) .join(', ') as string) + '.'; } return warning + '\n'; } function indexMarkdown(lines: string[]): Record { const indexed: Record = {}; let optionName = ''; let start = 0; for (const [i, line] of lines.entries()) { if (line.startsWith('## ') || line.startsWith('### ')) { if (optionName) { indexed[optionName] = [start, i - 1]; } start = i; optionName = line.split(' ')[1]; } } indexed[optionName] = [start, lines.length - 1]; return indexed; } export async function generateConfig(dist: string, bot = false): Promise { let configFile = `configuration-options.md`; if (bot) { configFile = `self-hosted-configuration.md`; } const configOptionsRaw = (await readFile(`docs/usage/${configFile}`)).split( '\n' ); const indexed = indexMarkdown(configOptionsRaw); options .filter( (option) => !!option.globalOnly === bot && !managers.has(option.name) ) .forEach((option) => { // TODO: fix types (#7154,#9610) const el: Record = { ...option }; if (!indexed[option.name]) { throw new Error( `Config option "${option.name}" is missing an entry in ${configFile}` ); } const [headerIndex, footerIndex] = indexed[option.name]; el.cli = getCliName(option); el.env = getEnvName(option); stringifyArrays(el); configOptionsRaw[headerIndex] += `\n${option.description}\n\n` + genTable(Object.entries(el), option.type, option.default); if (el.experimental) { configOptionsRaw[footerIndex] += genExperimentalMsg(el); } }); await updateFile(`${dist}/${configFile}`, configOptionsRaw.join('\n')); }