mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 02:00:10 +00:00
97327c3d28
* refactor(linters): add prefer-coerce-array rule Refs #45217 `x ?? []` and `coerceArray(x)` say the same thing, but only the second one is searchable and consistent. The rule flags a `??` whose default is a bare `[]` literal; a non-empty default or an asserted `([] as Foo[])` still passes. Applying it across `lib/` surfaced three spots the helper cannot cover, which keep `?? []` with a disable comment or a narrower fix: - `package-files.ts` destructures a tuple, which `coerceArray()` widens - `generate.spec.ts` defaults `matchAll()`, an iterator rather than an array - `mise/upgradeable-tooling.ts` declares a `Record` return, so its `?? []` fallback was wrong to begin with and becomes `coerceObject()` * refactor(platform/github): hoist the GHE version header lookup `Object.entries(...).find()` returns a tuple, and defaulting it with `?? []` only to destructure the second element reads as an array default when it is not one. Name the found entry instead, which drops the `?? []` rather than routing a tuple through `coerceArray()`.
190 lines
6.4 KiB
TypeScript
190 lines
6.4 KiB
TypeScript
import { isEmptyArray, isEmptyObject, isTruthy } from '@sindresorhus/is';
|
|
import { logger } from '../../logger/index.ts';
|
|
import type { PackageFile } from '../../modules/manager/types.ts';
|
|
import { clone } from '../../util/clone.ts';
|
|
import { emojify } from '../../util/emoji.ts';
|
|
|
|
export class PackageFiles {
|
|
private static data = new Map<string, Record<string, PackageFile[]> | null>();
|
|
|
|
static add(
|
|
baseBranch: string,
|
|
packageFiles: Record<string, PackageFile[]> | null,
|
|
): void {
|
|
logger.debug(
|
|
{ baseBranch },
|
|
`PackageFiles.add() - Package file saved for base branch`,
|
|
);
|
|
this.data.set(baseBranch, packageFiles);
|
|
}
|
|
|
|
static clear(): void {
|
|
logger.debug('PackageFiles.clear() - Package files deleted');
|
|
this.data.clear();
|
|
}
|
|
|
|
/**
|
|
* Truncates the detected dependencies' section until it fits the available space
|
|
* i.e. It has length smaller than maxLength.
|
|
* This does not mutate the original PackageFiles data
|
|
* Note: setHeader=false is used for testing purposes only
|
|
* Mainly for comparing truncated and non-truncated markdown
|
|
* @param maxLength
|
|
* @param setHeader
|
|
*/
|
|
static getDashboardMarkdown(maxLength: number, setHeader = true): string {
|
|
const note = emojify(
|
|
`> :information_source: **Note**\n> \n> Detected dependencies section has been truncated\n\n`,
|
|
);
|
|
const title = `## Detected Dependencies\n\n`;
|
|
|
|
// exclude header length from the available space
|
|
const maxHeaderLen = setHeader ? (title + note).length : 0;
|
|
const mdMaxLength = maxLength - maxHeaderLen;
|
|
|
|
let md: string;
|
|
let header = '';
|
|
let removed = false;
|
|
let truncated = false;
|
|
|
|
const data = new Map(clone(Array.from(this.data)));
|
|
|
|
// filter all deps with skip reason
|
|
for (const managers of [...data.values()].filter(isTruthy)) {
|
|
for (const files of Object.values(managers).filter(isTruthy)) {
|
|
for (const file of files.filter((f) => isTruthy(f.deps))) {
|
|
file.deps = file.deps.filter(isTruthy).filter((d) => !d.skipReason);
|
|
}
|
|
}
|
|
}
|
|
|
|
do {
|
|
// shorten markdown until it fits
|
|
md = PackageFiles.getDashboardMarkdownInternal(data);
|
|
if (md.length > mdMaxLength) {
|
|
// truncate data
|
|
removed = PackageFiles.pop(data);
|
|
}
|
|
if (removed) {
|
|
truncated = true; // used to set the truncation Note
|
|
}
|
|
} while (removed && md.length > mdMaxLength);
|
|
|
|
header += title;
|
|
header += truncated ? note : '';
|
|
|
|
return (setHeader ? header : '') + md;
|
|
}
|
|
|
|
/**
|
|
* Generates the "detected dependencies" markdown
|
|
* @param data
|
|
*/
|
|
private static getDashboardMarkdownInternal(
|
|
data: Map<string, Record<string, PackageFile[]> | null>,
|
|
): string {
|
|
const none = 'None detected\n\n';
|
|
const pad = data.size > 1; // padding condition for a multi base branch repo
|
|
let deps = '';
|
|
|
|
for (const [branch, packageFiles] of Array.from(data).sort(([a], [b]) =>
|
|
a.localeCompare(b, undefined, { numeric: true }),
|
|
)) {
|
|
deps += pad
|
|
? `<details><summary>Branch ${branch}</summary>\n<blockquote>\n\n`
|
|
: '';
|
|
if (packageFiles === null) {
|
|
deps += none;
|
|
deps += pad ? '</blockquote>\n</details>\n\n' : '';
|
|
continue;
|
|
}
|
|
|
|
const managers = Object.keys(packageFiles).sort();
|
|
if (managers.length === 0) {
|
|
deps += none;
|
|
deps += pad ? '</blockquote>\n</details>\n\n' : '';
|
|
continue;
|
|
}
|
|
|
|
for (const manager of managers) {
|
|
const managerPackageFiles = Array.from(packageFiles[manager]).sort(
|
|
(a, b) => a.packageFile.localeCompare(b.packageFile),
|
|
);
|
|
deps += `<details><summary>${manager} (${managerPackageFiles.length})</summary>\n<blockquote>\n\n`;
|
|
for (const packageFile of managerPackageFiles) {
|
|
deps += `<details><summary>${packageFile.packageFile}${packageFile.deps.length > 0 ? ` (${packageFile.deps.length})` : ''}</summary>\n\n`;
|
|
for (const dep of packageFile.deps) {
|
|
const ver = dep.currentValue;
|
|
const digest = dep.currentDigest;
|
|
const lock = dep.lockedVersion;
|
|
let version;
|
|
if (ver || digest) {
|
|
version = ver && digest ? `${ver}@${digest}` : `${digest ?? ver}`;
|
|
} else if (lock) {
|
|
version = `lock file @ ${lock}`;
|
|
} else {
|
|
version = 'unknown version';
|
|
}
|
|
let updates = '';
|
|
const uniqueUpdates = [
|
|
...new Set(
|
|
dep.updates?.map((update) => `\`${update.newValue}\``),
|
|
),
|
|
];
|
|
if (uniqueUpdates.length > 0) {
|
|
updates = ` → [Updates: ${uniqueUpdates.join(', ')}]`;
|
|
}
|
|
// TODO: types (#22198)
|
|
deps += ` - \`${dep.depName!} ${version}\`${updates}\n`;
|
|
}
|
|
deps += '\n</details>\n\n';
|
|
}
|
|
deps += `</blockquote>\n</details>\n\n`;
|
|
}
|
|
deps += pad ? '</blockquote>\n</details>\n\n' : '';
|
|
}
|
|
|
|
return deps;
|
|
}
|
|
|
|
/**
|
|
* Removes the last dependency/entry in the PackageFiles data
|
|
* i.e. the last line in the tobe generated detected dependency section
|
|
* @param data
|
|
* @Returns true if anything that translates to a markdown written line was deleted
|
|
* otherwise false is returned
|
|
*/
|
|
private static pop(
|
|
data: Map<string, Record<string, PackageFile[]> | null>,
|
|
): boolean {
|
|
// get detected managers list of the last listed base branch
|
|
// oxlint-disable-next-line renovate/prefer-coerce-array -- tuple destructuring default; coerceArray() widens the tuple to a union array
|
|
const [branch, managers] = Array.from(data).pop() ?? [];
|
|
if (!branch) {
|
|
return false;
|
|
}
|
|
|
|
// delete base branch listing if it has no managers left
|
|
if (!managers || isEmptyObject(managers)) {
|
|
return data.delete(branch);
|
|
}
|
|
|
|
// get all manifest files for the last listed manager
|
|
const [manager, packageFiles] = Object.entries(managers).pop()!;
|
|
|
|
// delete current manager if it has no manifest files left
|
|
if (!packageFiles || isEmptyArray(packageFiles)) {
|
|
return delete managers[manager];
|
|
}
|
|
|
|
// delete manifest file if it has no deps left
|
|
const len = packageFiles.length - 1;
|
|
if (isEmptyArray(packageFiles[len].deps)) {
|
|
return !!packageFiles.pop();
|
|
}
|
|
|
|
// remove the last listed dependency
|
|
return !!packageFiles[len].deps.pop();
|
|
}
|
|
}
|