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}${col} | \n`;
continue;
}
table +=
indent`${3}${col}` +
(`${col}`.endsWith('\n') ? indent`${3}` : '') +
` | \n`;
}
table += indent`${2}
\n`;
}
table += indent`${1}\n
\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'));
}