Files
renovate/lib/modules/manager/npm/extract/common/package-file.ts
Sebastian Poxhofer a473e73b3e fix(linters): forbid inert counts in v8 coverage-ignore comments (#45287)
Adds a custom oxlint rule renovate/v8-ignore-no-count that flags the
count form '/* v8 ignore next <N> */'. The count is not honored by
vitest's ast-v8-to-istanbul hint parser, which captures only the
keyword (if/else/next/file); 'next' always exempts exactly the next
AST node. Trailing counts therefore suggest a line range that is never
applied and mislead readers.

The rule is autofixable (drops the count, preserving actual behavior)
and has been applied to all existing occurrences.
2026-08-22 11:25:31 +00:00

168 lines
5.7 KiB
TypeScript

import { parsePkgAndParentSelector } from '@pnpm/parse-overrides';
import {
isNonEmptyObject,
isNonEmptyString,
isNonEmptyStringAndNotWhitespace,
isObject,
isString,
} from '@sindresorhus/is';
import { CONFIG_VALIDATION } from '../../../../../constants/error-messages.ts';
import { logger } from '../../../../../logger/index.ts';
import { regEx } from '../../../../../util/regex.ts';
import type { PackageDependency, PackageFileContent } from '../../../types.ts';
import type { NpmManagerData } from '../../types.ts';
import { loadPackageJson } from '../../utils.ts';
import type { NpmPackage, NpmPackageDependency } from '../types.ts';
import {
extractDependency,
getExtractedConstraints,
parseDepName,
} from './dependency.ts';
import { setNodeCommitTopic } from './node.ts';
import { extractOverrideDepsRec } from './overrides.ts';
export function extractPackageJson(
packageJson: NpmPackage,
packageFile: string,
): PackageFileContent<NpmManagerData> | null {
logger.trace(`npm.extractPackageJson(${packageFile})`);
const deps: PackageDependency[] = [];
if (packageJson._id && packageJson._args && packageJson._from) {
logger.debug({ packageFile }, 'Ignoring vendorised package.json');
return null;
}
if (packageFile !== 'package.json' && packageJson.renovate) {
const error = new Error(CONFIG_VALIDATION);
error.validationSource = packageFile;
error.validationError =
'Nested package.json must not contain Renovate configuration. Please use `packageRules` with `matchFileNames` in your main config instead.';
throw error;
}
const packageJsonName = packageJson.name;
logger.debug(
`npm file ${packageFile} has name ${JSON.stringify(packageJsonName)}`,
);
const packageFileVersion = packageJson.version;
const depTypes = {
dependencies: 'dependency',
devDependencies: 'devDependency',
optionalDependencies: 'optionalDependency',
peerDependencies: 'peerDependency',
engines: 'engine',
volta: 'volta',
resolutions: 'resolutions',
packageManager: 'packageManager',
overrides: 'overrides',
pnpm: 'pnpm',
};
for (const depType of Object.keys(depTypes) as (keyof typeof depTypes)[]) {
let dependencies = packageJson[depType];
if (dependencies) {
try {
if (depType === 'packageManager') {
const match = regEx('^(?<name>.+)@(?<range>.+)$').exec(
dependencies as string,
);
/* v8 ignore next -- needs test */
if (!match?.groups) {
break;
}
dependencies = { [match.groups.name]: match.groups.range };
}
for (const [key, val] of Object.entries(
dependencies as NpmPackageDependency,
)) {
const depName = parseDepName(depType, key);
let dep: PackageDependency = {
depType,
depName,
};
if (depName !== key) {
dep.managerData = { key };
}
// v8 ignore else -- TODO: add test #40625
if (depType === 'overrides' && !isString(val)) {
// TODO: fix type #22198
deps.push(
...extractOverrideDepsRec(
[depName],
val as unknown as NpmManagerData,
),
);
} else if (depType === 'pnpm' && depName === 'overrides') {
// pnpm overrides
// https://pnpm.io/package_json#pnpmoverrides
for (const [overridesKey, overridesVal] of Object.entries(
val as unknown as NpmPackageDependency,
)) {
if (isString(overridesVal)) {
// Newer flat syntax: `parent>parent>child`
const packageName =
parsePkgAndParentSelector(overridesKey).targetPkg.name;
dep = {
depName: overridesKey,
packageName,
depType: 'pnpm.overrides',
...extractDependency(depName, packageName, overridesVal),
};
setNodeCommitTopic(dep);
// TODO: Is this expected? It's always 'overrides'.
dep.prettyDepType = depTypes[depName];
deps.push(dep);
} else if (isObject(overridesVal)) {
// Older nested object syntax: `parent: { parent: { child: version } }`
deps.push(
...extractOverrideDepsRec([overridesKey], overridesVal),
);
}
}
} else {
// TODO: fix type #22198
dep = { ...dep, ...extractDependency(depType, depName, val!) };
setNodeCommitTopic(dep);
dep.prettyDepType = depTypes[depType];
deps.push(dep);
}
}
} catch (err) /* v8 ignore next -- TODO: add test #40625 */ {
logger.debug(
{ fileName: packageFile, depType, err },
'Error parsing package.json',
);
return null;
}
}
}
const extractedConstraints = getExtractedConstraints(deps);
return {
deps,
extractedConstraints,
packageFileVersion,
managerData: {
packageJsonName,
hasPackageManager:
isNonEmptyStringAndNotWhitespace(packageJson.packageManager) ||
isNonEmptyObject(packageJson.devEngines?.packageManager),
workspaces: packageJson.workspaces,
},
};
}
export async function hasPackageManager(
packageJsonDir: string,
): Promise<boolean> {
logger.trace(`npm.hasPackageManager from package.json`);
const packageJsonResult = await loadPackageJson(packageJsonDir);
return (
isNonEmptyString(packageJsonResult?.packageManager?.name) &&
isNonEmptyString(packageJsonResult?.packageManager?.version)
);
}