import { isString } from '@sindresorhus/is'; import validateNpmPackageName from 'validate-npm-package-name'; import { logger } from '../../../../../logger/index.ts'; import { coerceArray } from '../../../../../util/array.ts'; import type { ConstraintName } from '../../../../../util/exec/types.ts'; import { isConstraintName } from '../../../../../util/exec/types.ts'; import { regEx } from '../../../../../util/regex.ts'; import { GithubTagsDatasource } from '../../../../datasource/github-tags/index.ts'; import { NodeVersionDatasource } from '../../../../datasource/node-version/index.ts'; import { NpmDatasource } from '../../../../datasource/npm/index.ts'; import { api, isValid, isVersion, id as npmVersioningId, } from '../../../../versioning/npm/index.ts'; import type { PackageDependency } from '../../../types.ts'; const RE_REPOSITORY_GITHUB_SSH_FORMAT = regEx( /(?:git@)github.com:(?[^/]+)\/(?[^/]+?)(?:\.git)?$/, ); export function parseDepName(depType: string, key: string): string { if (depType !== 'resolutions') { return key; } // Yarn selective dependency resolutions may nest a path of parent // packages before the target package, e.g. `parent/child` or // `@scope/parent/child`, and any segment (including the last) may carry // a `@range` suffix used to disambiguate which version of that package // to match, e.g. `@cypress/request/qs@~6.14.1`. const parts = key.split('/'); const segments: string[] = []; for (let i = 0; i < parts.length; i += 1) { const part = parts[i]; if (part.startsWith('@') && i + 1 < parts.length) { i += 1; segments.push(`${part}/${parts[i]}`); } else { segments.push(part); } } const lastSegment = segments.at(-1); const [, depName] = coerceArray( regEx(/^(?(?:@[^/]+\/)?[^@]+)/).exec(lastSegment ?? ''), ); return depName; } export function extractDependency( depType: string, depName: string, input: string, ): PackageDependency { const dep: PackageDependency = {}; if (!validateNpmPackageName(depName).validForOldPackages) { dep.skipReason = 'invalid-name'; return dep; } if (!isString(input)) { dep.skipReason = 'invalid-value'; return dep; } dep.currentValue = input.trim(); if (depType === 'engines' || depType === 'packageManager') { if (depName === 'node') { dep.datasource = NodeVersionDatasource.id; } else if (depName === 'yarn') { dep.datasource = NpmDatasource.id; dep.commitMessageTopic = 'Yarn'; const major = isVersion(dep.currentValue) && api.getMajor(dep.currentValue); if (major && major > 1) { dep.packageName = '@yarnpkg/cli'; } } else if (depName === 'npm') { dep.datasource = NpmDatasource.id; dep.commitMessageTopic = 'npm'; } else if (depName === 'pnpm') { dep.datasource = NpmDatasource.id; dep.commitMessageTopic = 'pnpm'; } else if (depName === 'vscode') { dep.datasource = GithubTagsDatasource.id; dep.packageName = 'microsoft/vscode'; dep.versioning = npmVersioningId; } else if (depName === 'bun') { dep.datasource = NpmDatasource.id; dep.commitMessageTopic = 'Bun'; } else { dep.skipReason = 'unknown-engines'; } if (!isValid(dep.currentValue)) { dep.skipReason = 'unspecified-version'; } return dep; } // support for volta if (depType === 'volta') { if (depName === 'node') { dep.datasource = NodeVersionDatasource.id; } else if (depName === 'yarn') { dep.datasource = NpmDatasource.id; dep.commitMessageTopic = 'Yarn'; const major = isVersion(dep.currentValue) && api.getMajor(dep.currentValue); if (major && major > 1) { dep.packageName = '@yarnpkg/cli'; } } else if (depName === 'npm') { dep.datasource = NpmDatasource.id; } else if (depName === 'pnpm') { dep.datasource = NpmDatasource.id; dep.commitMessageTopic = 'pnpm'; } else { dep.skipReason = 'unknown-volta'; } if (!isValid(dep.currentValue)) { dep.skipReason = 'unspecified-version'; } return dep; } if (dep.currentValue.startsWith('npm:')) { dep.npmPackageAlias = true; const valSplit = dep.currentValue.replace('npm:', '').split('@'); if (valSplit.length === 1) { dep.packageName = depName; dep.currentValue = valSplit[0]; } else if (valSplit.length === 2) { dep.packageName = valSplit[0]; dep.currentValue = valSplit[1]; } else if (valSplit.length === 3) { dep.packageName = `${valSplit[0]}@${valSplit[1]}`; dep.currentValue = valSplit[2]; } else { logger.debug( `Invalid npm package alias for dependency: "${depName}":"${dep.currentValue}"`, ); } } if (dep.currentValue.startsWith('file:')) { dep.skipReason = 'file'; return dep; } if (isValid(dep.currentValue)) { dep.datasource = NpmDatasource.id; if (dep.currentValue === '') { dep.skipReason = 'empty'; } return dep; } const hashSplit = dep.currentValue.split('#'); if (hashSplit.length !== 2) { dep.skipReason = 'unspecified-version'; return dep; } const [depNamePart, depRefPart] = hashSplit; let githubOwnerRepo: string; let githubOwner: string; let githubRepo: string; const matchUrlSshFormat = RE_REPOSITORY_GITHUB_SSH_FORMAT.exec(depNamePart); if (matchUrlSshFormat === null) { githubOwnerRepo = depNamePart .replace(regEx(/^github:/), '') .replace(regEx(/^git\+/), '') .replace(regEx(/^https:\/\/github\.com\//), '') .replace(regEx(/\.git$/), ''); const githubRepoSplit = githubOwnerRepo.split('/'); if (githubRepoSplit.length !== 2) { dep.skipReason = 'unspecified-version'; return dep; } [githubOwner, githubRepo] = githubRepoSplit; } else { githubOwner = matchUrlSshFormat.groups!.owner; githubRepo = matchUrlSshFormat.groups!.repo; githubOwnerRepo = `${githubOwner}/${githubRepo}`; } // combined with the length check below, this is equivalent to // /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i without the lookahead const githubOwnerRegex = regEx(/^[a-z\d](?:-?[a-z\d]){0,38}$/i); const githubRepoRegex = regEx(/^[a-zA-Z0-9._-]{1,100}$/); if ( githubOwner.length > 39 || !githubOwnerRegex.test(githubOwner) || !githubRepoRegex.test(githubRepo) ) { dep.skipReason = 'unspecified-version'; return dep; } if (isVersion(depRefPart)) { dep.currentRawValue = dep.currentValue; dep.currentValue = depRefPart; dep.datasource = GithubTagsDatasource.id; dep.versioning = npmVersioningId; dep.packageName = githubOwnerRepo; dep.pinDigests = false; } else if ( regEx(/^[0-9a-f]{7}$/).test(depRefPart) || regEx(/^[0-9a-f]{40}$/).test(depRefPart) ) { dep.currentRawValue = dep.currentValue; dep.currentValue = null; dep.currentDigest = depRefPart; dep.datasource = GithubTagsDatasource.id; dep.versioning = npmVersioningId; dep.packageName = githubOwnerRepo; } else { // ://[[:]@][:][:][/][# | #semver:] // https://docs.npmjs.com/cli/v10/configuring-npm/package-json#git-urls-as-dependencies const len = 7; // length of 'semver:' const maybeVersion = depRefPart.substring(len); if (depRefPart.startsWith('semver:') && isValid(maybeVersion)) { dep.currentRawValue = dep.currentValue; dep.currentValue = maybeVersion; dep.datasource = GithubTagsDatasource.id; dep.versioning = npmVersioningId; dep.packageName = githubOwnerRepo; dep.pinDigests = false; } else { dep.skipReason = 'unversioned-reference'; return dep; } } dep.sourceUrl = `https://github.com/${githubOwnerRepo}`; dep.gitRef = true; return dep; } export function getExtractedConstraints( deps: PackageDependency[], ): Partial> { const extractedConstraints: Partial> = {}; const constraints: ConstraintName[] = [ 'bun', 'node', 'yarn', 'npm', 'pnpm', 'vscode', ]; for (const dep of deps) { if ( !dep.skipReason && (dep.depType === 'engines' || dep.depType === 'packageManager') && dep.depName && isConstraintName(dep.depName) && constraints.includes(dep.depName) && isString(dep.currentValue) ) { extractedConstraints[dep.depName] = dep.currentValue; } } return extractedConstraints; }