mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 17:51:49 +00:00
16ef1715e9
* feat(lint): add renovate/prefer-is-helpers and renovate/prefer-is-object rules Flag ad-hoc type checks that have @sindresorhus/is equivalents: - .filter(Boolean) -> .filter(isTruthy) - typeof x === 'string' -> isString(x) - x === null || x === undefined -> isNullOrUndefined(x) (and negation) The typeof x === 'object' shape lives in a separate suggestion-only rule (renovate/prefer-is-object, kept at warn) because isObject()/isPlainObject() are not drop-in equivalents: null, arrays and functions behave differently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: fix renovate/prefer-is-helpers violations Replace ad-hoc type checks with @sindresorhus/is helpers across lib/: - 13x .filter(Boolean) -> .filter(isTruthy) - 36x typeof x ==/!== 'string' -> isString(x) / !isString(x) - 6x x === null || x === undefined -> isNullOrUndefined(x) (and negation) Promote renovate/prefer-is-helpers to error now that the codebase is clean. renovate/prefer-is-object stays at warn: its 15 remaining sites guard parsed YAML/JSON or unknown values where isObject()/isPlainObject() are not provably equivalent (null/array/function semantics). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: enforce renovate/prefer-is-object at error level Resolve the 15 remaining warnings and raise the rule from warn to error: - Migrate 10 sites to isObject() where the guarded value provably cannot be a function (parsed YAML/JSON, typed exec command arguments, or test-controlled fixtures): helm-values, woodpecker, bun, pre-commit managers, exec utils, file package cache, and the github http spec. In the bun manager this also fixes a latent crash where a null `workspaces` value would throw on the `in` operator. - Keep 5 sites as reasoned oxlint-disable comments where excluding functions is load-bearing: the byte-exact fingerprint canonicalizer (2), the circularity walker, the zod result guard, and the generic hasKey helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
286 lines
8.4 KiB
TypeScript
286 lines
8.4 KiB
TypeScript
import {
|
|
isEmptyObject,
|
|
isNonEmptyArray,
|
|
isNonEmptyObject,
|
|
isString,
|
|
} from '@sindresorhus/is';
|
|
import upath from 'upath';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { readLocalFile } from '../../../util/fs/index.ts';
|
|
import { minimatch } from '../../../util/minimatch.ts';
|
|
import { api as semver } from '../../versioning/deno/index.ts';
|
|
import type { PackageDependency, PackageFile } from '../types.ts';
|
|
import {
|
|
detectNodeCompatWorkspaces,
|
|
extractDenoCompatiblePackageJson,
|
|
} from './compat.ts';
|
|
import { DenoLock } from './schema.ts';
|
|
import type { DenoManagerData, LockFile, WorkspaceContext } from './types.ts';
|
|
import { denoLandRegex, depValueRegex } from './utils.ts';
|
|
|
|
export async function getDenoLock(filePath: string): Promise<LockFile> {
|
|
const lockfileContent = await readLocalFile(filePath, 'utf8');
|
|
if (!lockfileContent) {
|
|
logger.debug({ filePath }, 'Deno: unable to read lockfile');
|
|
return { lockedVersions: {} };
|
|
}
|
|
const parsedLockfile = DenoLock.safeParse(lockfileContent);
|
|
if (!parsedLockfile.success) {
|
|
logger.debug(
|
|
{ filePath, err: parsedLockfile.error },
|
|
'Deno: unable to parse lockfile',
|
|
);
|
|
return { lockedVersions: {} };
|
|
}
|
|
if (parsedLockfile.data.lockfileVersion < 5) {
|
|
logger.warn(
|
|
{ filePath },
|
|
'Deno: unsupported lockfile version. Please update the lock file on your own.',
|
|
);
|
|
return { lockedVersions: {} };
|
|
}
|
|
|
|
return parsedLockfile.data;
|
|
}
|
|
|
|
export function getLockedVersion(
|
|
deps: PackageDependency<DenoManagerData>,
|
|
lockFileContent: LockFile,
|
|
): string | undefined | null {
|
|
if (isEmptyObject(lockFileContent)) {
|
|
return null;
|
|
}
|
|
const { datasource, currentRawValue, currentValue, depName } = deps;
|
|
|
|
if (datasource === 'deno') {
|
|
// "remote": {
|
|
// "https://deno.land/std@0.223.0/fs/mod.ts": "c25e6802cbf27f3050f60b26b00c2d8dba1cb7fcdafe34c66006a7473b7b34d4",
|
|
// },
|
|
if (
|
|
lockFileContent.remoteVersions &&
|
|
lockFileContent.remoteVersions.size > 0 &&
|
|
currentRawValue &&
|
|
lockFileContent.remoteVersions.has(currentRawValue)
|
|
) {
|
|
const match = denoLandRegex.exec(currentRawValue);
|
|
return match?.groups?.currentValue;
|
|
}
|
|
|
|
// "redirects": {
|
|
// "https://deno.land/std": "https://deno.land/std@0.224.0"
|
|
// },
|
|
const key =
|
|
currentValue && depName ? `${depName}@${currentValue}` : depName;
|
|
if (
|
|
lockFileContent.redirectVersions &&
|
|
isNonEmptyObject(lockFileContent.redirectVersions) &&
|
|
key &&
|
|
lockFileContent.redirectVersions[key]
|
|
) {
|
|
const match = denoLandRegex.exec(
|
|
// SAFETY: checked above
|
|
lockFileContent.redirectVersions[key],
|
|
);
|
|
return match?.groups?.currentValue;
|
|
}
|
|
}
|
|
|
|
// jsr and npm datasource
|
|
if (datasource === 'jsr' || datasource === 'npm') {
|
|
if (
|
|
!lockFileContent.lockedVersions ||
|
|
isEmptyObject(lockFileContent.lockedVersions)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
// find "jsr:@scope/name@1.2.3" from "jsr:@scope/name@1.2.3"
|
|
if (currentRawValue && lockFileContent.lockedVersions[currentRawValue]) {
|
|
return lockFileContent.lockedVersions[currentRawValue];
|
|
}
|
|
|
|
// find "jsr:@scope/name@*" from "jsr:@scope/name"
|
|
if (
|
|
currentRawValue &&
|
|
lockFileContent.lockedVersions[`${currentRawValue}@*`]
|
|
) {
|
|
return lockFileContent.lockedVersions[`${currentRawValue}@*`];
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(lockFileContent.lockedVersions)) {
|
|
const match = depValueRegex.exec(key);
|
|
// find "jsr:@scope/name@1" intersects "jsr:@scope/name@^1.0.0"
|
|
if (
|
|
isString(depName) &&
|
|
match?.groups?.depName === depName &&
|
|
match?.groups?.datasource === datasource &&
|
|
currentValue &&
|
|
match?.groups?.currentValue &&
|
|
// SAFETY: npm semver define it
|
|
semver.intersects!(match.groups.currentValue, currentValue)
|
|
) {
|
|
return value;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function collectPackageJsonAsWorkspaceMember(
|
|
packageFiles: PackageFile<DenoManagerData>[],
|
|
): Promise<void> {
|
|
// detect package.json as members of a deno workspace
|
|
const workspaceRoots = packageFiles.filter(
|
|
(pkg) =>
|
|
isNonEmptyArray(pkg.managerData?.workspaces) &&
|
|
upath.basename(pkg.packageFile).startsWith('deno.json'),
|
|
);
|
|
for (const workspaceRoot of workspaceRoots) {
|
|
const result = await detectNodeCompatWorkspaces(workspaceRoot);
|
|
const { packagePaths } = result;
|
|
for (const packagePath of packagePaths) {
|
|
const packageFile = await extractDenoCompatiblePackageJson(packagePath);
|
|
if (packageFile) {
|
|
const pkg = {
|
|
...packageFile,
|
|
lockFiles: workspaceRoot.lockFiles,
|
|
};
|
|
packageFiles.push(pkg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function normalizeWorkspace(
|
|
packageFiles: PackageFile<DenoManagerData>[],
|
|
): void {
|
|
// determine workspace root to collect lock files of workspace members
|
|
const workspaceContexts: WorkspaceContext[] = [];
|
|
|
|
// create reference map for packageFile object
|
|
const packageMap: Record<string, PackageFile<DenoManagerData>> = {};
|
|
for (const pkg of packageFiles) {
|
|
packageMap[pkg.packageFile] = pkg;
|
|
}
|
|
|
|
for (const pkg of packageFiles) {
|
|
const workspaces = pkg.managerData?.workspaces;
|
|
if (isNonEmptyArray(workspaces)) {
|
|
const rootDir = upath.dirname(pkg.packageFile);
|
|
const matchers = workspaces.map((pattern) =>
|
|
// allow ./sub/* to match sub
|
|
minimatch(upath.normalize(pattern), {
|
|
dot: true,
|
|
partial: true,
|
|
}),
|
|
);
|
|
workspaceContexts.push({
|
|
lockFiles: pkg.lockFiles,
|
|
rootDir,
|
|
packageFile: pkg.packageFile,
|
|
matchers,
|
|
});
|
|
}
|
|
}
|
|
|
|
// remove nested workspace
|
|
// if the workspace is a subdirectory of another workspace, the nested is invalid
|
|
const validContexts: typeof workspaceContexts = [];
|
|
const invalidPackageFiles = new Set<string>();
|
|
for (const [i, currentContext] of workspaceContexts.entries()) {
|
|
let isNested = false;
|
|
|
|
for (const [j, otherContext] of workspaceContexts.entries()) {
|
|
if (i === j) {
|
|
continue;
|
|
}
|
|
|
|
const found = otherContext.matchers.some((matcher) =>
|
|
matcher.match(currentContext.rootDir),
|
|
);
|
|
if (found) {
|
|
isNested = true;
|
|
invalidPackageFiles.add(currentContext.packageFile);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isNested) {
|
|
validContexts.push(currentContext);
|
|
}
|
|
}
|
|
for (const packageFile of invalidPackageFiles) {
|
|
const pkg = packageMap[packageFile];
|
|
// remove invalid workspace
|
|
delete pkg?.managerData?.workspaces;
|
|
}
|
|
|
|
// supply lock files to workspace members from their root
|
|
const workspaceRootFiles = new Set<string>();
|
|
for (const pkg of packageFiles) {
|
|
const workspaces = pkg.managerData?.workspaces;
|
|
if (isNonEmptyArray(workspaces)) {
|
|
workspaceRootFiles.add(pkg.packageFile);
|
|
}
|
|
}
|
|
|
|
for (const pkg of packageFiles) {
|
|
if (workspaceRootFiles.has(pkg.packageFile)) {
|
|
continue;
|
|
}
|
|
|
|
for (const context of workspaceContexts) {
|
|
const { rootDir, matchers, lockFiles } = context;
|
|
const pkgRelativePath = upath.relative(rootDir, pkg.packageFile);
|
|
const pkgDir = upath.dirname(pkgRelativePath);
|
|
const isMatch = matchers.some((matcher) => matcher.match(pkgDir));
|
|
if (isMatch) {
|
|
pkg.lockFiles = lockFiles;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function applyLockedVersion(
|
|
packageFiles: PackageFile<DenoManagerData>[],
|
|
): Promise<void> {
|
|
// apply locked versions from lock files
|
|
// use cache to avoid reading the same lock file multiple times
|
|
const lockFileCache: Record<string, LockFile> = {};
|
|
for (const pkg of packageFiles) {
|
|
if (!isNonEmptyArray(pkg.lockFiles)) {
|
|
continue;
|
|
}
|
|
const lockFile = pkg.lockFiles[0];
|
|
let lockFileContent = lockFileCache[lockFile];
|
|
|
|
if (!lockFileContent) {
|
|
lockFileContent = await getDenoLock(lockFile);
|
|
lockFileCache[lockFile] = lockFileContent;
|
|
}
|
|
|
|
const withLockedVersionDeps = pkg.deps.map((dep) => {
|
|
const lockedVersion = getLockedVersion(dep, lockFileContent);
|
|
return lockedVersion
|
|
? {
|
|
...dep,
|
|
lockedVersion,
|
|
}
|
|
: dep;
|
|
});
|
|
|
|
pkg.deps = withLockedVersionDeps;
|
|
}
|
|
}
|
|
|
|
export async function postExtract(
|
|
packageFiles: PackageFile<DenoManagerData>[],
|
|
): Promise<void> {
|
|
await collectPackageJsonAsWorkspaceMember(packageFiles);
|
|
normalizeWorkspace(packageFiles);
|
|
await applyLockedVersion(packageFiles);
|
|
}
|