diff --git a/package.json b/package.json index 4670cad..caf575d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "execa": "^8.0.1", "pony-cause": "^2.1.9", "semver": "^7.5.4", + "validate-npm-package-name": "^5.0.0", "which": "^3.0.0", "yaml": "^2.2.2", "yargs": "^17.7.1" @@ -52,6 +53,7 @@ "@types/node": "^17.0.23", "@types/prettier": "^2.7.3", "@types/rimraf": "^4.0.5", + "@types/validate-npm-package-name": "^4.0.2", "@types/which": "^3.0.0", "@types/yargs": "^17.0.10", "@typescript-eslint/eslint-plugin": "^5.62.0", diff --git a/src/package-manifest.test.ts b/src/package-manifest.test.ts index af9f77c..08c02cc 100644 --- a/src/package-manifest.test.ts +++ b/src/package-manifest.test.ts @@ -67,6 +67,7 @@ describe('package-manifest', () => { b: '^2.0.0', c: '~4.3.0', d: 'workspace:^', + e: 'npm:@a/abc@^2.0.0', }, }; const validated = { @@ -79,6 +80,7 @@ describe('package-manifest', () => { b: '^2.0.0', c: '~4.3.0', d: 'workspace:^', + e: 'npm:@a/abc@^2.0.0', }, peerDependencies: {}, }; @@ -315,7 +317,9 @@ describe('package-manifest', () => { name: 'foo', version: '1.0.0', peerDependencies: { - a: 12345, + a: 'npm:@foo', + b: 'npm:foo@', + c: '12345', }, }), ); diff --git a/src/package-manifest.ts b/src/package-manifest.ts index a4aa6d5..eea83e5 100644 --- a/src/package-manifest.ts +++ b/src/package-manifest.ts @@ -4,6 +4,7 @@ import { ManifestDependencyFieldNames as PackageManifestDependenciesFieldNames, } from '@metamask/action-utils'; import { isPlainObject } from '@metamask/utils'; +import validateNPMPackageName from 'validate-npm-package-name'; import { readJsonObjectFile } from './fs.js'; import { isTruthyString } from './misc-utils.js'; import { semver, SemVer } from './semver.js'; @@ -144,8 +145,10 @@ function isValidPackageManifestVersionField( /** * Type guard to ensure that the provided version value is a valid dependency version - * specifier for a package manifest. This function validates both semantic versioning - * ranges and the special 'workspace:^' notation. + * specifier for a package manifest. This function validates: + * semantic versioning ranges + * 'workspace:^' notation + * 'npm:{packageName}:{semverRange}' redirections. * * @param version - The value to check. * @returns `true` if the version is a valid string that either @@ -155,9 +158,35 @@ function isValidPackageManifestVersionField( function isValidPackageManifestDependencyValue( version: unknown, ): version is string { - return ( - isValidPackageManifestVersionField(version) || version === 'workspace:^' - ); + if (typeof version !== 'string') { + return false; + } + + if ( + isValidPackageManifestVersionField(version) || + version === 'workspace:^' + ) { + return true; + } + + const redirectedDependencyRegexp = /^npm:(.*)@(.*?)$/u; + + try { + const redirectedDependencyMatch = redirectedDependencyRegexp.exec(version); + + /* istanbul ignore if */ + if (!redirectedDependencyMatch) { + return false; + } + + const [, redirectedName, redirectedVersion] = redirectedDependencyMatch; + return ( + validateNPMPackageName(redirectedName)?.validForOldPackages && + isValidPackageManifestVersionField(redirectedVersion) + ); + } catch (e) /* istanbul ignore next */ { + return false; + } } /** diff --git a/yarn.lock b/yarn.lock index 5674337..69462c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2090,6 +2090,7 @@ __metadata: "@types/node": ^17.0.23 "@types/prettier": ^2.7.3 "@types/rimraf": ^4.0.5 + "@types/validate-npm-package-name": ^4.0.2 "@types/which": ^3.0.0 "@types/yargs": ^17.0.10 "@typescript-eslint/eslint-plugin": ^5.62.0 @@ -2117,6 +2118,7 @@ __metadata: stdio-mock: ^1.2.0 tsx: ^4.6.1 typescript: ~5.1.6 + validate-npm-package-name: ^5.0.0 which: ^3.0.0 yaml: ^2.2.2 yargs: ^17.7.1 @@ -2606,6 +2608,13 @@ __metadata: languageName: node linkType: hard +"@types/validate-npm-package-name@npm:^4.0.2": + version: 4.0.2 + resolution: "@types/validate-npm-package-name@npm:4.0.2" + checksum: 3f35a3cc8ddd919b456843f36d55a4f1df5f03d5d9b6494b4d8f5f3b24e3f24a11c922772d9970a67f1249214da18c157776e9c6d2e72227799459849dfd9c76 + languageName: node + linkType: hard + "@types/which@npm:^3.0.0": version: 3.0.0 resolution: "@types/which@npm:3.0.0"