mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-15 09:06:25 +00:00
394 lines
12 KiB
TypeScript
394 lines
12 KiB
TypeScript
import { dirname } from 'path';
|
|
import is from '@sindresorhus/is';
|
|
import validateNpmPackageName from 'validate-npm-package-name';
|
|
import { CONFIG_VALIDATION } from '../../../constants/error-messages';
|
|
import * as datasourceGithubTags from '../../../datasource/github-tags';
|
|
import * as datasourceNpm from '../../../datasource/npm';
|
|
import { logger } from '../../../logger';
|
|
import { SkipReason } from '../../../types';
|
|
import {
|
|
deleteLocalFile,
|
|
getSiblingFileName,
|
|
readLocalFile,
|
|
} from '../../../util/fs';
|
|
import * as nodeVersioning from '../../../versioning/node';
|
|
import { isValid, isVersion } from '../../../versioning/npm';
|
|
import {
|
|
ExtractConfig,
|
|
NpmLockFiles,
|
|
PackageDependency,
|
|
PackageFile,
|
|
} from '../../common';
|
|
import { NpmPackage, NpmPackageDependeny } from './common';
|
|
import { getLockedVersions } from './locked-versions';
|
|
import { detectMonorepos } from './monorepo';
|
|
import { mightBeABrowserLibrary } from './type';
|
|
|
|
function parseDepName(depType: string, key: string): string {
|
|
if (depType !== 'resolutions') {
|
|
return key;
|
|
}
|
|
|
|
const [, depName] = /((?:@[^/]+\/)?[^/@]+)$/.exec(key);
|
|
return depName;
|
|
}
|
|
|
|
export async function extractPackageFile(
|
|
content: string,
|
|
fileName: string,
|
|
config: ExtractConfig
|
|
): Promise<PackageFile | null> {
|
|
logger.trace(`npm.extractPackageFile(${fileName})`);
|
|
logger.trace({ content });
|
|
const deps: PackageDependency[] = [];
|
|
let packageJson: NpmPackage;
|
|
try {
|
|
packageJson = JSON.parse(content);
|
|
} catch (err) {
|
|
logger.debug({ fileName }, 'Invalid JSON');
|
|
return null;
|
|
}
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
if (packageJson._id && packageJson._args && packageJson._from) {
|
|
logger.debug('Ignoring vendorised package.json');
|
|
return null;
|
|
}
|
|
if (fileName !== 'package.json' && packageJson.renovate) {
|
|
const error = new Error(CONFIG_VALIDATION);
|
|
error.configFile = fileName;
|
|
error.validationError =
|
|
'Nested package.json must not contain renovate configuration. Please use `packageRules` with `paths` in your main config instead.';
|
|
throw error;
|
|
}
|
|
const packageJsonName = packageJson.name;
|
|
logger.debug(
|
|
`npm file ${fileName} has name ${JSON.stringify(packageJsonName)}`
|
|
);
|
|
const packageJsonVersion = packageJson.version;
|
|
let yarnWorkspacesPackages: string[];
|
|
if (is.array(packageJson.workspaces)) {
|
|
yarnWorkspacesPackages = packageJson.workspaces;
|
|
} else {
|
|
yarnWorkspacesPackages = packageJson.workspaces?.packages;
|
|
}
|
|
const packageJsonType = mightBeABrowserLibrary(packageJson)
|
|
? 'library'
|
|
: 'app';
|
|
|
|
const lockFiles: NpmLockFiles = {
|
|
yarnLock: 'yarn.lock',
|
|
packageLock: 'package-lock.json',
|
|
shrinkwrapJson: 'npm-shrinkwrap.json',
|
|
pnpmShrinkwrap: 'pnpm-lock.yaml',
|
|
};
|
|
|
|
for (const [key, val] of Object.entries(lockFiles)) {
|
|
const filePath = getSiblingFileName(fileName, val);
|
|
if (await readLocalFile(filePath, 'utf8')) {
|
|
lockFiles[key] = filePath;
|
|
} else {
|
|
lockFiles[key] = undefined;
|
|
}
|
|
}
|
|
lockFiles.npmLock = lockFiles.packageLock || lockFiles.shrinkwrapJson;
|
|
delete lockFiles.packageLock;
|
|
delete lockFiles.shrinkwrapJson;
|
|
|
|
let npmrc: string;
|
|
let ignoreNpmrcFile: boolean;
|
|
const npmrcFileName = getSiblingFileName(fileName, '.npmrc');
|
|
// istanbul ignore if
|
|
if (config.ignoreNpmrcFile) {
|
|
await deleteLocalFile(npmrcFileName);
|
|
} else {
|
|
npmrc = await readLocalFile(npmrcFileName, 'utf8');
|
|
if (npmrc?.includes('package-lock')) {
|
|
logger.debug('Stripping package-lock setting from npmrc');
|
|
npmrc = npmrc.replace(/(^|\n)package-lock.*?(\n|$)/g, '\n');
|
|
}
|
|
if (npmrc) {
|
|
if (npmrc.includes('=${') && !(global.trustLevel === 'high')) {
|
|
logger.debug('Discarding .npmrc file with variables');
|
|
ignoreNpmrcFile = true;
|
|
npmrc = undefined;
|
|
await deleteLocalFile(npmrcFileName);
|
|
}
|
|
} else {
|
|
npmrc = undefined;
|
|
}
|
|
}
|
|
const yarnrcFileName = getSiblingFileName(fileName, '.yarnrc');
|
|
const yarnrc = (await readLocalFile(yarnrcFileName, 'utf8')) || undefined;
|
|
|
|
let lernaDir: string;
|
|
let lernaPackages: string[];
|
|
let lernaClient: 'yarn' | 'npm';
|
|
let hasFileRefs = false;
|
|
let lernaJson: { packages: string[]; npmClient: string };
|
|
try {
|
|
const lernaJsonFileName = getSiblingFileName(fileName, 'lerna.json');
|
|
lernaJson = JSON.parse(await readLocalFile(lernaJsonFileName, 'utf8'));
|
|
} catch (err) /* istanbul ignore next */ {
|
|
logger.warn({ err }, 'Could not parse lerna.json');
|
|
}
|
|
if (lernaJson) {
|
|
lernaDir = dirname(fileName);
|
|
lernaPackages = lernaJson.packages;
|
|
lernaClient =
|
|
lernaJson.npmClient === 'yarn' || lockFiles.yarnLock ? 'yarn' : 'npm';
|
|
}
|
|
|
|
const depTypes = {
|
|
dependencies: 'dependency',
|
|
devDependencies: 'devDependency',
|
|
optionalDependencies: 'optionalDependency',
|
|
peerDependencies: 'peerDependency',
|
|
engines: 'engine',
|
|
volta: 'volta',
|
|
resolutions: 'resolutions',
|
|
};
|
|
|
|
const compatibility: Record<string, any> = {};
|
|
|
|
function extractDependency(
|
|
depType: string,
|
|
depName: string,
|
|
input: string
|
|
): PackageDependency {
|
|
const dep: PackageDependency = {};
|
|
if (!validateNpmPackageName(depName).validForOldPackages) {
|
|
dep.skipReason = SkipReason.InvalidName;
|
|
return dep;
|
|
}
|
|
if (typeof input !== 'string') {
|
|
dep.skipReason = SkipReason.InvalidValue;
|
|
return dep;
|
|
}
|
|
dep.currentValue = input.trim();
|
|
if (depType === 'engines') {
|
|
if (depName === 'node') {
|
|
dep.datasource = datasourceGithubTags.id;
|
|
dep.lookupName = 'nodejs/node';
|
|
dep.versioning = nodeVersioning.id;
|
|
compatibility.node = dep.currentValue;
|
|
} else if (depName === 'yarn') {
|
|
dep.datasource = datasourceNpm.id;
|
|
dep.commitMessageTopic = 'Yarn';
|
|
compatibility.yarn = dep.currentValue;
|
|
} else if (depName === 'npm') {
|
|
dep.datasource = datasourceNpm.id;
|
|
dep.commitMessageTopic = 'npm';
|
|
compatibility.npm = dep.currentValue;
|
|
} else if (depName === 'pnpm') {
|
|
dep.datasource = datasourceNpm.id;
|
|
dep.commitMessageTopic = 'pnpm';
|
|
compatibility.pnpm = dep.currentValue;
|
|
} else {
|
|
dep.skipReason = SkipReason.UnknownEngines;
|
|
}
|
|
if (!isValid(dep.currentValue)) {
|
|
dep.skipReason = SkipReason.UnknownVersion;
|
|
}
|
|
return dep;
|
|
}
|
|
|
|
// support for volta
|
|
if (depType === 'volta') {
|
|
if (depName === 'node') {
|
|
dep.datasource = datasourceGithubTags.id;
|
|
dep.lookupName = 'nodejs/node';
|
|
dep.versioning = nodeVersioning.id;
|
|
} else if (depName === 'yarn') {
|
|
dep.datasource = datasourceNpm.id;
|
|
dep.commitMessageTopic = 'Yarn';
|
|
} else {
|
|
dep.skipReason = SkipReason.UnknownVolta;
|
|
}
|
|
if (!isValid(dep.currentValue)) {
|
|
dep.skipReason = SkipReason.UnknownVersion;
|
|
}
|
|
return dep;
|
|
}
|
|
|
|
if (dep.currentValue.startsWith('npm:')) {
|
|
dep.npmPackageAlias = true;
|
|
const valSplit = dep.currentValue.replace('npm:', '').split('@');
|
|
if (valSplit.length === 2) {
|
|
dep.lookupName = valSplit[0];
|
|
dep.currentValue = valSplit[1];
|
|
} else if (valSplit.length === 3) {
|
|
dep.lookupName = valSplit[0] + '@' + valSplit[1];
|
|
dep.currentValue = valSplit[2];
|
|
} else {
|
|
logger.debug('Invalid npm package alias: ' + dep.currentValue);
|
|
}
|
|
}
|
|
if (dep.currentValue.startsWith('file:')) {
|
|
dep.skipReason = SkipReason.File;
|
|
hasFileRefs = true;
|
|
return dep;
|
|
}
|
|
if (isValid(dep.currentValue)) {
|
|
dep.datasource = datasourceNpm.id;
|
|
if (dep.currentValue === '*') {
|
|
dep.skipReason = SkipReason.AnyVersion;
|
|
}
|
|
if (dep.currentValue === '') {
|
|
dep.skipReason = SkipReason.Empty;
|
|
}
|
|
return dep;
|
|
}
|
|
const hashSplit = dep.currentValue.split('#');
|
|
if (hashSplit.length !== 2) {
|
|
dep.skipReason = SkipReason.UnknownVersion;
|
|
return dep;
|
|
}
|
|
const [depNamePart, depRefPart] = hashSplit;
|
|
const githubOwnerRepo = depNamePart
|
|
.replace(/^github:/, '')
|
|
.replace(/^git\+/, '')
|
|
.replace(/^https:\/\/github\.com\//, '')
|
|
.replace(/\.git$/, '');
|
|
const githubRepoSplit = githubOwnerRepo.split('/');
|
|
if (githubRepoSplit.length !== 2) {
|
|
dep.skipReason = SkipReason.UnknownVersion;
|
|
return dep;
|
|
}
|
|
const [githubOwner, githubRepo] = githubRepoSplit;
|
|
const githubValidRegex = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/;
|
|
if (
|
|
!githubValidRegex.test(githubOwner) ||
|
|
!githubValidRegex.test(githubRepo)
|
|
) {
|
|
dep.skipReason = SkipReason.UnknownVersion;
|
|
return dep;
|
|
}
|
|
if (isVersion(depRefPart)) {
|
|
dep.currentRawValue = dep.currentValue;
|
|
dep.currentValue = depRefPart;
|
|
dep.datasource = datasourceGithubTags.id;
|
|
dep.lookupName = githubOwnerRepo;
|
|
dep.pinDigests = false;
|
|
} else if (
|
|
/^[0-9a-f]{7}$/.test(depRefPart) ||
|
|
/^[0-9a-f]{40}$/.test(depRefPart)
|
|
) {
|
|
dep.currentRawValue = dep.currentValue;
|
|
dep.currentValue = null;
|
|
dep.currentDigest = depRefPart;
|
|
dep.datasource = datasourceGithubTags.id;
|
|
dep.lookupName = githubOwnerRepo;
|
|
} else {
|
|
dep.skipReason = SkipReason.UnversionedReference;
|
|
return dep;
|
|
}
|
|
dep.githubRepo = githubOwnerRepo;
|
|
dep.sourceUrl = `https://github.com/${githubOwnerRepo}`;
|
|
dep.gitRef = true;
|
|
return dep;
|
|
}
|
|
|
|
for (const depType of Object.keys(depTypes)) {
|
|
if (packageJson[depType]) {
|
|
try {
|
|
for (const [key, val] of Object.entries(
|
|
packageJson[depType] as NpmPackageDependeny
|
|
)) {
|
|
const depName = parseDepName(depType, key);
|
|
const dep: PackageDependency = {
|
|
depType,
|
|
depName,
|
|
};
|
|
if (depName !== key) {
|
|
dep.managerData = { key };
|
|
}
|
|
Object.assign(dep, extractDependency(depType, depName, val));
|
|
if (depName === 'node') {
|
|
// This is a special case for Node.js to group it together with other managers
|
|
dep.commitMessageTopic = 'Node.js';
|
|
dep.major = { enabled: false };
|
|
}
|
|
dep.prettyDepType = depTypes[depType];
|
|
deps.push(dep);
|
|
}
|
|
} catch (err) /* istanbul ignore next */ {
|
|
logger.debug({ fileName, depType, err }, 'Error parsing package.json');
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
if (deps.length === 0) {
|
|
logger.debug('Package file has no deps');
|
|
if (
|
|
!(
|
|
packageJsonName ||
|
|
packageJsonVersion ||
|
|
npmrc ||
|
|
lernaDir ||
|
|
yarnWorkspacesPackages
|
|
)
|
|
) {
|
|
logger.debug('Skipping file');
|
|
return null;
|
|
}
|
|
}
|
|
let skipInstalls = config.skipInstalls;
|
|
if (skipInstalls === null) {
|
|
if (hasFileRefs) {
|
|
// https://github.com/npm/cli/issues/1432
|
|
// Explanation:
|
|
// - npm install --package-lock-only is buggy for transitive deps in file: references
|
|
// - So we set skipInstalls to false if file: refs are found *and* the user hasn't explicitly set the value already
|
|
logger.debug('Automatically setting skipInstalls to false');
|
|
skipInstalls = false;
|
|
} else {
|
|
skipInstalls = true;
|
|
}
|
|
}
|
|
|
|
return {
|
|
deps,
|
|
packageJsonName,
|
|
packageJsonVersion,
|
|
packageJsonType,
|
|
npmrc,
|
|
ignoreNpmrcFile,
|
|
yarnrc,
|
|
...lockFiles,
|
|
lernaDir,
|
|
lernaClient,
|
|
lernaPackages,
|
|
skipInstalls,
|
|
yarnWorkspacesPackages,
|
|
compatibility,
|
|
};
|
|
}
|
|
|
|
export async function postExtract(packageFiles: PackageFile[]): Promise<void> {
|
|
detectMonorepos(packageFiles);
|
|
await getLockedVersions(packageFiles);
|
|
}
|
|
|
|
export async function extractAllPackageFiles(
|
|
config: ExtractConfig,
|
|
packageFiles: string[]
|
|
): Promise<PackageFile[]> {
|
|
const npmFiles: PackageFile[] = [];
|
|
for (const packageFile of packageFiles) {
|
|
const content = await readLocalFile(packageFile, 'utf8');
|
|
// istanbul ignore else
|
|
if (content) {
|
|
const deps = await extractPackageFile(content, packageFile, config);
|
|
if (deps) {
|
|
npmFiles.push({
|
|
packageFile,
|
|
...deps,
|
|
});
|
|
}
|
|
} else {
|
|
logger.debug({ packageFile }, 'packageFile has no content');
|
|
}
|
|
}
|
|
await postExtract(npmFiles);
|
|
return npmFiles;
|
|
}
|