import { groups as presetGroups } from '../../lib/config/presets/internal/index.ts';
import { logger } from '../../lib/logger/index.ts';
import { updateFile } from '../utils/index.ts';
function jsUcfirst(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function presetRefToLink(presetRef: string): string | null {
// Separate base name from arguments: ":followTag(typescript, next)" -> base=":followTag", argCount=2
let baseName = presetRef;
let argCount = 0;
const parenMatch = /^(?[^(]+)\((?.+)\)$/.exec(presetRef);
if (parenMatch) {
baseName = parenMatch.groups!.base;
argCount = parenMatch.groups!.args.split(/,\s*/).length;
}
let group: string;
const colonIdx = baseName.indexOf(':');
if (colonIdx === 0) {
group = 'default';
} else if (colonIdx > 0) {
group = baseName.slice(0, colonIdx);
} else {
return null;
}
// Reconstruct the heading text as it appears on the target page,
// where concrete args are replaced with , , etc.
let headingText = baseName;
if (argCount > 0) {
const argList = Array.from({ length: argCount }, (_, i) => ``);
headingText += `(${argList.join(', ')})`;
}
// Slugify matching python-markdown's toc extension default behavior
const slug = headingText
.replace(/[^\w\s-]/g, '')
.trim()
.toLowerCase()
.replace(/[-\s]+/g, '-');
return `./presets-${group}.md#${slug}`;
}
function getPresetDescription(
presetRef: string,
descriptions: Map,
): string | undefined {
// Try exact match first
let desc = descriptions.get(presetRef);
if (desc) {
return desc;
}
// Parameterized preset references like ":semanticCommitType(chore)" will be looked up as `:semanticCommitType` (which is the preset name)
const [prefix] = presetRef.split('(');
if (prefix) {
desc = descriptions.get(prefix);
if (desc) {
return desc;
}
}
return undefined;
}
function generateCodeBlock(
value: Record,
descriptions: Map,
): string {
const json = JSON.stringify(value, null, 2);
const lines = json.split('\n');
let inExtends = false;
let counter = 0;
const annotations: {
num: number;
ref: string;
link: string;
description: string | undefined;
}[] = [];
const annotatedLines = lines.map((line) => {
if (/^\s+"extends":\s*\[/.test(line)) {
inExtends = true;
return line;
}
if (inExtends) {
if (/^\s+\]/.test(line)) {
inExtends = false;
return line;
}
const match = /^(?\s+"(?[[^"]+)")(?,?)$/.exec(line);
if (match) {
const presetRef = match.groups!.ref;
const link = presetRefToLink(presetRef);
if (link) {
counter++;
const description = getPresetDescription(presetRef, descriptions);
annotations.push({ num: counter, ref: presetRef, link, description });
// note that we use a trailing `!` to strip the comment from the resulting code block
return `${match.groups!.quoted}${match.groups!.comma} // (${counter})!`;
}
}
}
return line;
});
if (annotations.length === 0) {
return `\n\`\`\`json\n${json}\n\`\`\`\n`;
}
let result = '\n``` { .json .annotate }\n';
result += annotatedLines.join('\n');
result += '\n```\n\n';
for (const ann of annotations) {
if (ann.description) {
result += `${ann.num}. [\`${ann.ref}\`](${ann.link}): ${ann.description}\n`;
} else {
result += `${ann.num}. [\`${ann.ref}\`](${ann.link})\n`;
}
}
return result;
}
function getEditUrl(name: string): string {
const url =
'https://github.com/renovatebot/renovate/edit/main/lib/config/presets/internal/';
const dataUrl = 'https://github.com/renovatebot/renovate/edit/main/lib/data/';
switch (name) {
case 'customManagers':
return `${url}custom-managers.ts`;
case 'mergeConfidence':
return `${url}merge-confidence.ts`;
case 'monorepo':
return `${dataUrl}${name}.json`;
case 'replacements':
return `${dataUrl}${name}.json`;
default:
return `${url}${name}.ts`;
}
}
/**
* @param {string} presetTitle
* @param {number} order
* @param {string} presetName
*/
function generateFrontMatter(
presetTitle: string,
order: number,
presetName: string,
): string {
return `---
date: 2017-12-07
title: ${presetTitle} Presets
categories:
- config-presets
type: Document
order: ${order}
edit_url: ${getEditUrl(presetName)}
---
`;
}
export async function generatePresets(dist: string): Promise {
// Calculate the descriptions for each preset ahead-of-time, as we remove them during generation
const descriptions = new Map();
for (const [groupName, presetConfig] of Object.entries(presetGroups)) {
for (const [presetName, value] of Object.entries(presetConfig)) {
const ref =
groupName === 'default'
? `:${presetName}`
: `${groupName}:${presetName}`;
const desc =
(value.description as string | undefined) ??
(value.overrideDescription as string | undefined) ??
(value.packageRules?.[0]?.description as string | undefined);
if (desc) {
descriptions.set(ref, desc);
}
}
}
let index = 0;
for (const [name, presetConfig] of Object.entries(presetGroups)) {
index += 1;
const formattedName = jsUcfirst(name)
.replace('Js', 'JS')
.replace(/s$/, '')
.replace(/^Config$/, 'Full Config');
const frontMatter = generateFrontMatter(formattedName, index, name);
let content = `\n`;
for (const [preset, value] of Object.entries(presetConfig)) {
let header = `\n### \`${name === 'default' ? '' : name}:${preset}\``;
let presetDescription = value.description as string;
delete value.description;
if (!presetDescription && value.overrideDescription) {
presetDescription = value.overrideDescription as string;
}
delete value.overrideDescription;
if (!presetDescription && value.packageRules?.[0].description) {
presetDescription = value.packageRules[0].description as string;
delete value.packageRules[0].description;
}
let body = '';
if (presetDescription) {
body += `\n\n${presetDescription}\n`;
} else {
logger.warn(
{ preset: `${name}:${preset}` },
'Preset has no description',
);
}
body += generateCodeBlock(value, descriptions);
body += '\n----\n';
if (body.includes('{{arg0}}')) {
header += '(``';
if (body.includes('{{arg1}}')) {
header += ', ``';
if (body.includes('{{arg2}}')) {
header += ', ``';
}
}
header += ')';
body = body.replace(/{{(?arg\d+)}}/g, '$');
}
content += header + body;
}
await updateFile(`${dist}/presets-${name}.md`, frontMatter + content);
}
}
]