Files
renovate/lib/modules/manager/deno/post.ts
Sebastian Poxhofer 16ef1715e9 refactor(linters): add rules for @sindresorhus/is helpers (#45229)
* 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>
2026-08-13 14:22:03 +00:00

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);
}