From a05df867e96c93a18856d93975b36df3cde961e7 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 00:49:02 -0400 Subject: [PATCH 01/19] feat: implement `minimumNpmReleaseAge` and `minimumNpmReleaseAgeExclude` config options --- .pnp.cjs | 44 +++++++++++++++++++ .yarn/versions/25eb93a2.yml | 3 ++ .../static/configuration/yarnrc.json | 17 +++++++ packages/plugin-npm/package.json | 2 + .../plugin-npm/sources/NpmSemverResolver.ts | 21 ++++++++- packages/plugin-npm/sources/npmHttpUtils.ts | 2 + .../yarnpkg-core/sources/Configuration.ts | 13 ++++++ yarn.lock | 2 + 8 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 .yarn/versions/25eb93a2.yml diff --git a/.pnp.cjs b/.pnp.cjs index f937e16f2e04..29bfa6a6bfc2 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -14474,6 +14474,7 @@ const RAW_RUNTIME_STATE = ["virtual:08fe6ad7a76ed00f8dc32e3b968ce66fd4db8ac47424db78612ce3633e63ecb46d41984611094facf53bbef7eae7fbf98bbfd729fb77f5ccde564684f4e3a829#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-62b5fdf575/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14484,6 +14485,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:572569575af06858e5d7f4d0dbe6c0741e76db5e6c65708522a93857213495bf1c60f4e618b217daf88fc14605c64ad49f87b67a6007ad69373fb6d52190ee49#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14499,6 +14501,7 @@ const RAW_RUNTIME_STATE = ["virtual:10635d85d43c1773f587c2d6565f7a30c3bff1c16e39550dcdd44b3745dd69317ced5e20de16484758df2d6dc9314da646bf356d1ef8485a0dcd939b71a3327c#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-9b5e15c7a8/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14509,6 +14512,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:10635d85d43c1773f587c2d6565f7a30c3bff1c16e39550dcdd44b3745dd69317ced5e20de16484758df2d6dc9314da646bf356d1ef8485a0dcd939b71a3327c#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14525,6 +14529,7 @@ const RAW_RUNTIME_STATE = ["virtual:16f564b30745199d7e07a913c371ce0c078051290c6e08b972f07b3f1bf057a6993fe67b7c6ee24931d0b1dd67e1274151612081733a79b961dd8336318fdfb9#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-b849f17967/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14535,6 +14540,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:16f564b30745199d7e07a913c371ce0c078051290c6e08b972f07b3f1bf057a6993fe67b7c6ee24931d0b1dd67e1274151612081733a79b961dd8336318fdfb9#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14551,6 +14557,7 @@ const RAW_RUNTIME_STATE = ["virtual:1c3d72c6b31a8950672985f8306a860ecc80c9a006aac95cf4a7ba13a6e7cc4e095e37186a53c9909e9efe97bc0f7f570a74b3879778e2a2356cdcf407120006#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-d10a34a30c/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14561,6 +14568,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:1c3d72c6b31a8950672985f8306a860ecc80c9a006aac95cf4a7ba13a6e7cc4e095e37186a53c9909e9efe97bc0f7f570a74b3879778e2a2356cdcf407120006#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14577,6 +14585,7 @@ const RAW_RUNTIME_STATE = ["virtual:2351fd5ac4f83ad35b714d8af9fdeea561ada341d529d0dba50742dd5735dc3750df6c56bd680e14833d5b987026a1eab6618211ea0ef1b34b727372b3c77bc9#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-5066cb1bc2/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14587,6 +14596,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:2351fd5ac4f83ad35b714d8af9fdeea561ada341d529d0dba50742dd5735dc3750df6c56bd680e14833d5b987026a1eab6618211ea0ef1b34b727372b3c77bc9#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14603,6 +14613,7 @@ const RAW_RUNTIME_STATE = ["virtual:45a6746f11cef24d8db9429cc5650999571e6bb77a8cfb3904a0e832f542be35246ec490516049308ca15b8678eb03bcf394199e514a8145ec32731af7235c91#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-1d0f0cf8f5/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14613,6 +14624,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:45a6746f11cef24d8db9429cc5650999571e6bb77a8cfb3904a0e832f542be35246ec490516049308ca15b8678eb03bcf394199e514a8145ec32731af7235c91#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14629,6 +14641,7 @@ const RAW_RUNTIME_STATE = ["virtual:4864d30fc563f2fd1b72a5e3869493c5f50bf38f98ed3886173d80c044d981c3f68220dbf17f2b5fc5b4c5fba7d0af2e003926efe3487086484049f41c449852#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-ac09774f7e/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14639,6 +14652,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:4864d30fc563f2fd1b72a5e3869493c5f50bf38f98ed3886173d80c044d981c3f68220dbf17f2b5fc5b4c5fba7d0af2e003926efe3487086484049f41c449852#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14655,6 +14669,7 @@ const RAW_RUNTIME_STATE = ["virtual:4a733c8d9614e2148392368219d98ec1a70b4e8ce99164edd551241b22f6c5233e9d0ccf9f6d83265c8a5aafc617cfd3c4100b3efef1e092a42053c23770ed9a#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-fdb17b9327/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14665,6 +14680,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:4a733c8d9614e2148392368219d98ec1a70b4e8ce99164edd551241b22f6c5233e9d0ccf9f6d83265c8a5aafc617cfd3c4100b3efef1e092a42053c23770ed9a#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14681,6 +14697,7 @@ const RAW_RUNTIME_STATE = ["virtual:4ff153bc11101851444cc464184bde5e42ffd55b3939421c30a4c2b69483c3267c1680de4a4c00a49c98cbbe35e70111bb3c26f5ce8836b703c15cd5b753451a#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-3ea9bf04ef/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14691,6 +14708,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:4ff153bc11101851444cc464184bde5e42ffd55b3939421c30a4c2b69483c3267c1680de4a4c00a49c98cbbe35e70111bb3c26f5ce8836b703c15cd5b753451a#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14707,6 +14725,7 @@ const RAW_RUNTIME_STATE = ["virtual:54c8b951e743ea46368d98ac86d4c1ac7d1aa57c9d31cbf6424fa2d918257654f26f71d51dbfe63844c533e97635ff97de50fd37e6e4bf74f2603a98754d6d22#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-d0a5a66e87/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14717,6 +14736,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:54c8b951e743ea46368d98ac86d4c1ac7d1aa57c9d31cbf6424fa2d918257654f26f71d51dbfe63844c533e97635ff97de50fd37e6e4bf74f2603a98754d6d22#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14733,6 +14753,7 @@ const RAW_RUNTIME_STATE = ["virtual:6fc63e4d1a1b8c6564cfaaeabf378b05cdf49336a90189d76df005175060690d597b069801c0c39b9c60573a6fba29e7646274224b3007bd7f72c95871114cf2#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-347ff97d4b/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14743,6 +14764,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:27ebb8cf1fa70157f710b4926b6d25c44192e74dbac3a766c8dc6505a59ebc433221bfb4b5aabc8cca814bbe95fcb6e1ecffcf94ba96ee6112a57c89364571ac#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14759,6 +14781,7 @@ const RAW_RUNTIME_STATE = ["virtual:712d04b0098634bdb13868ff8f85b327022bd7d3880873ada8c0ae56847ed36cf9da1fd74a88519380129cec528fe2bd2201426bc28ac9d4a8cc6734ff25c538#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-572569575a/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14769,6 +14792,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:572569575af06858e5d7f4d0dbe6c0741e76db5e6c65708522a93857213495bf1c60f4e618b217daf88fc14605c64ad49f87b67a6007ad69373fb6d52190ee49#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14784,6 +14808,7 @@ const RAW_RUNTIME_STATE = ["virtual:8bb72793b532d34e63bbc26264dcbcfc4dc4faa0a42627635e997081722bf229d67b7a677d86a568dad949d756630e45b9d4da97ee14b1b4c506494f8a58ea91#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-badf9df693/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14794,6 +14819,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:8bb72793b532d34e63bbc26264dcbcfc4dc4faa0a42627635e997081722bf229d67b7a677d86a568dad949d756630e45b9d4da97ee14b1b4c506494f8a58ea91#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14810,6 +14836,7 @@ const RAW_RUNTIME_STATE = ["virtual:a4e201fc3c2d8b3ec5632082d407d554bbf8ea8b84182577dde1ce419148ae0981b382a0805280637d50e1132628fef8f78ee6a015164963130b1310a4cca910#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-d0a74e03b3/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14820,6 +14847,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:a4e201fc3c2d8b3ec5632082d407d554bbf8ea8b84182577dde1ce419148ae0981b382a0805280637d50e1132628fef8f78ee6a015164963130b1310a4cca910#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14836,6 +14864,7 @@ const RAW_RUNTIME_STATE = ["virtual:a7c38e9a420fd3b408ea245831c2c9f0e880eac64b268fab3219f5f0b1d6015f44b1f92d23aabfc6e980bbbbda00a23e9faa983fb98544fab94119ccd31f2440#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-2395b4e5d3/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14846,6 +14875,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:a7c38e9a420fd3b408ea245831c2c9f0e880eac64b268fab3219f5f0b1d6015f44b1f92d23aabfc6e980bbbbda00a23e9faa983fb98544fab94119ccd31f2440#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14862,6 +14892,7 @@ const RAW_RUNTIME_STATE = ["virtual:adaf1cec8728346f1bf6a263f1954625a52d60518b8d2084da8a926203282105d2b95fb9da84922062af8d4fc84b8a1c39f220238424024e56f55577bdbc7208#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-8792f06b17/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14872,6 +14903,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:adaf1cec8728346f1bf6a263f1954625a52d60518b8d2084da8a926203282105d2b95fb9da84922062af8d4fc84b8a1c39f220238424024e56f55577bdbc7208#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14888,6 +14920,7 @@ const RAW_RUNTIME_STATE = ["virtual:b4c0e602e8ac4e01a7b08db41bb5808da767dd1f6802758faa5125fb2423614bb0a8806ee1b30c3a0769f86da15ad37377f5118d93cd93fa48df0008a448fb35#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-708f4ba711/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14898,6 +14931,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:b4c0e602e8ac4e01a7b08db41bb5808da767dd1f6802758faa5125fb2423614bb0a8806ee1b30c3a0769f86da15ad37377f5118d93cd93fa48df0008a448fb35#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14914,6 +14948,7 @@ const RAW_RUNTIME_STATE = ["virtual:b63ad861025672af62aed0e7c80dca4cfce3194ca046161e54fc14c498c39e3b82004ea844489c7a58d2f1a31867f388bf25b8128f5ccce46f35305e1f91e9ab#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-8e76aa50aa/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14924,6 +14959,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:b63ad861025672af62aed0e7c80dca4cfce3194ca046161e54fc14c498c39e3b82004ea844489c7a58d2f1a31867f388bf25b8128f5ccce46f35305e1f91e9ab#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14940,6 +14976,7 @@ const RAW_RUNTIME_STATE = ["virtual:c4bd2716e35986fb2e70f5fba6e9570c69eceabc69282df5bcff5d22c6b7d0e696d0cfb4bcbd9a20675fe3e2eb6192b59d41b97baa8b27e1d474b94eeda3f778#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-d0252a53c5/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14950,6 +14987,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:c4bd2716e35986fb2e70f5fba6e9570c69eceabc69282df5bcff5d22c6b7d0e696d0cfb4bcbd9a20675fe3e2eb6192b59d41b97baa8b27e1d474b94eeda3f778#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14966,6 +15004,7 @@ const RAW_RUNTIME_STATE = ["virtual:ce4dc3135569e847b88addae1199f9468fb0b37867e1a86ba6725f71b9df587a8ae43356ae86c3bfe3b0cbbf07dcf8c1a4a95199810d9f20df387eec0a1e1965#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-bfcf790ec1/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -14976,6 +15015,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:ce4dc3135569e847b88addae1199f9468fb0b37867e1a86ba6725f71b9df587a8ae43356ae86c3bfe3b0cbbf07dcf8c1a4a95199810d9f20df387eec0a1e1965#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -14992,6 +15032,7 @@ const RAW_RUNTIME_STATE = ["virtual:d1d72d9e3903ca8b8d9c23a360395cc764db2689e5992ef9af91c79f03a839db10ec675af9e4c1c8f4842aff1a614eb5b115fcc0afe8256630151ef1252de94b#workspace:packages/plugin-npm", {\ "packageLocation": "./.yarn/__virtual__/@yarnpkg-plugin-npm-virtual-87450824e1/1/packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@types/yarnpkg__core", null],\ @@ -15002,6 +15043,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:d1d72d9e3903ca8b8d9c23a360395cc764db2689e5992ef9af91c79f03a839db10ec675af9e4c1c8f4842aff1a614eb5b115fcc0afe8256630151ef1252de94b#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ @@ -15018,6 +15060,7 @@ const RAW_RUNTIME_STATE = ["workspace:packages/plugin-npm", {\ "packageLocation": "./packages/plugin-npm/",\ "packageDependencies": [\ + ["@types/micromatch", "npm:4.0.1"],\ ["@types/semver", "npm:7.5.8"],\ ["@types/ssri", "npm:7.1.5"],\ ["@yarnpkg/core", "workspace:packages/yarnpkg-core"],\ @@ -15026,6 +15069,7 @@ const RAW_RUNTIME_STATE = ["@yarnpkg/plugin-pack", "virtual:572569575af06858e5d7f4d0dbe6c0741e76db5e6c65708522a93857213495bf1c60f4e618b217daf88fc14605c64ad49f87b67a6007ad69373fb6d52190ee49#workspace:packages/plugin-pack"],\ ["enquirer", "npm:2.3.6"],\ ["es-toolkit", "npm:1.39.7"],\ + ["micromatch", "npm:4.0.5"],\ ["semver", "npm:7.6.0"],\ ["sigstore", "npm:3.1.0"],\ ["ssri", "npm:12.0.0"],\ diff --git a/.yarn/versions/25eb93a2.yml b/.yarn/versions/25eb93a2.yml new file mode 100644 index 000000000000..087e49fa5bec --- /dev/null +++ b/.yarn/versions/25eb93a2.yml @@ -0,0 +1,3 @@ +releases: + "@yarnpkg/core": minor + "@yarnpkg/plugin-npm": minor diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index 168f5dd5669c..670a5c4e0858 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -407,6 +407,23 @@ } ] }, + "minimumReleaseAge": { + "_package": "@yarnpkg/core", + "title": "Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation.", + "description": "If a package version is newer than the minimum release age, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages.", + "type": "number", + "default": 0 + }, + "minimumReleaseAgeExcludes": { + "_package": "@yarnpkg/core", + "title": "Array of exact package descriptors, exact package locators, or package name glob patterns to exclude from the minimum release age check.", + "description": "If a package descriptor, locator, or name matches specified pattern, it will not be considered for the minimum release age check. That is, the newest matching package version will be selected for installation regardless of the minimum release age configuration.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, "networkConcurrency": { "_package": "@yarnpkg/core", "title": "Amount of HTTP requests that are allowed to run at the same time.", diff --git a/packages/plugin-npm/package.json b/packages/plugin-npm/package.json index 6979e9a8b768..22c9b9a9a0d1 100644 --- a/packages/plugin-npm/package.json +++ b/packages/plugin-npm/package.json @@ -11,6 +11,7 @@ "@yarnpkg/fslib": "workspace:^", "enquirer": "^2.3.6", "es-toolkit": "^1.39.7", + "micromatch": "^4.0.2", "semver": "^7.1.2", "sigstore": "^3.1.0", "ssri": "^12.0.0", @@ -21,6 +22,7 @@ "@yarnpkg/plugin-pack": "workspace:^" }, "devDependencies": { + "@types/micromatch": "^4.0.1", "@types/semver": "^7.1.0", "@types/ssri": "^7.1.5", "@yarnpkg/core": "workspace:^", diff --git a/packages/plugin-npm/sources/NpmSemverResolver.ts b/packages/plugin-npm/sources/NpmSemverResolver.ts index 34b545f01df6..1379642569d9 100644 --- a/packages/plugin-npm/sources/NpmSemverResolver.ts +++ b/packages/plugin-npm/sources/NpmSemverResolver.ts @@ -2,6 +2,7 @@ import {ReportError, MessageName, Resolver, ResolveOptions, MinimalResolveOption import {Descriptor, Locator, semverUtils} from '@yarnpkg/core'; import {LinkType} from '@yarnpkg/core'; import {structUtils} from '@yarnpkg/core'; +import micromatch from 'micromatch'; import semver from 'semver'; import {NpmSemverFetcher} from './NpmSemverFetcher'; @@ -43,7 +44,8 @@ export class NpmSemverResolver implements Resolver { } async getCandidates(descriptor: Descriptor, dependencies: Record, opts: ResolveOptions) { - const range = semverUtils.validRange(descriptor.range.slice(PROTOCOL.length)); + const rawRange = descriptor.range.slice(PROTOCOL.length); + const range = semverUtils.validRange(rawRange); if (range === null) throw new Error(`Expected a valid range, got ${descriptor.range.slice(PROTOCOL.length)}`); @@ -57,6 +59,23 @@ export class NpmSemverResolver implements Resolver { try { const candidate = new semverUtils.SemVer(version); if (range.test(candidate)) { + const minimumReleaseAge = opts.project.configuration.get(`minimumNpmReleaseAge`); + if (minimumReleaseAge) { + const minimumReleaseAgeExclude = opts.project.configuration.get(`minimumNpmReleaseAgeExclude`); + const shouldExclude = minimumReleaseAgeExclude.some(exclude => + structUtils.stringifyIdent(descriptor) === exclude + || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, version)) === exclude + || micromatch.isMatch(structUtils.stringifyDescriptor({...descriptor, range: rawRange}), exclude) + || micromatch.isMatch(structUtils.stringifyDescriptor(descriptor), exclude), + ); + if (!shouldExclude) { + const versionTime = new Date(registryData.time[version]); + const ageMinutes = (new Date().getTime() - versionTime.getTime()) / 60 / 1000; + if (ageMinutes < minimumReleaseAge) { + return miscUtils.mapAndFilter.skip; + } + } + } return candidate; } } catch { } diff --git a/packages/plugin-npm/sources/npmHttpUtils.ts b/packages/plugin-npm/sources/npmHttpUtils.ts index d5543ec67d09..b932ddaa569a 100644 --- a/packages/plugin-npm/sources/npmHttpUtils.ts +++ b/packages/plugin-npm/sources/npmHttpUtils.ts @@ -286,6 +286,7 @@ export type PackageMetadata = { tarball: string; }; }>; + time: Record; }; function pickPackageMetadata(metadata: PackageMetadata): PackageMetadata { @@ -295,6 +296,7 @@ function pickPackageMetadata(metadata: PackageMetadata): PackageMetadata { key, pick(value, CACHED_FIELDS) as any, ])), + time: metadata.time, }; } diff --git a/packages/yarnpkg-core/sources/Configuration.ts b/packages/yarnpkg-core/sources/Configuration.ts index 5f576ebfa2b4..85bd1dc1579e 100644 --- a/packages/yarnpkg-core/sources/Configuration.ts +++ b/packages/yarnpkg-core/sources/Configuration.ts @@ -577,6 +577,17 @@ export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} = type: SettingsType.STRING, default: `throw`, }, + minimumNpmReleaseAge: { + description: `Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation`, + type: SettingsType.NUMBER, + default: 0, + }, + minimumNpmReleaseAgeExclude: { + description: `Array of package name glob patterns to exclude from the minimum release age check`, + type: SettingsType.STRING, + isArray: true, + default: [], + }, // Miscellaneous settings injectEnvironmentFiles: { @@ -699,6 +710,8 @@ export interface ConfigurationValueMap { enableStrictSettings: boolean; enableImmutableCache: boolean; checksumBehavior: string; + minimumNpmReleaseAge: number; + minimumNpmReleaseAgeExclude: Array; // Miscellaneous settings injectEnvironmentFiles: Array; diff --git a/yarn.lock b/yarn.lock index 12ec25ae0f3d..c74d1e210a15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6425,6 +6425,7 @@ __metadata: version: 0.0.0-use.local resolution: "@yarnpkg/plugin-npm@workspace:packages/plugin-npm" dependencies: + "@types/micromatch": "npm:^4.0.1" "@types/semver": "npm:^7.1.0" "@types/ssri": "npm:^7.1.5" "@yarnpkg/core": "workspace:^" @@ -6432,6 +6433,7 @@ __metadata: "@yarnpkg/plugin-pack": "workspace:^" enquirer: "npm:^2.3.6" es-toolkit: "npm:^1.39.7" + micromatch: "npm:^4.0.2" semver: "npm:^7.1.2" sigstore: "npm:^3.1.0" ssri: "npm:^12.0.0" From db24b783896ca4ebd7eebbf09a4fe0d9843ba0eb Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 00:55:45 -0400 Subject: [PATCH 02/19] fix: support `npm:` prefix for locators too --- packages/plugin-npm/sources/NpmSemverResolver.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin-npm/sources/NpmSemverResolver.ts b/packages/plugin-npm/sources/NpmSemverResolver.ts index 1379642569d9..4b13d7b709f5 100644 --- a/packages/plugin-npm/sources/NpmSemverResolver.ts +++ b/packages/plugin-npm/sources/NpmSemverResolver.ts @@ -65,6 +65,7 @@ export class NpmSemverResolver implements Resolver { const shouldExclude = minimumReleaseAgeExclude.some(exclude => structUtils.stringifyIdent(descriptor) === exclude || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, version)) === exclude + || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, `${PROTOCOL}:${version}`)) === exclude || micromatch.isMatch(structUtils.stringifyDescriptor({...descriptor, range: rawRange}), exclude) || micromatch.isMatch(structUtils.stringifyDescriptor(descriptor), exclude), ); From 900a7d292da0180463af8f3aa8b73ff9dc60059d Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 04:40:59 -0400 Subject: [PATCH 03/19] test: adding tests for `minimumNpmReleaseAge` and `minimumNpmReleaseAgeExclude` --- .../sources/utils/makeTemporaryEnv.ts | 2 +- .../pkg-tests-core/sources/utils/tests.ts | 23 +- .../@scoped__release-date-1.0.0/index.js | 7 + .../@scoped__release-date-1.0.0/package.json | 4 + .../@scoped__release-date-1.1.0/index.js | 7 + .../@scoped__release-date-1.1.0/package.json | 4 + .../@scoped__release-date-1.1.1/index.js | 7 + .../@scoped__release-date-1.1.1/package.json | 4 + .../packages/release-date-1.0.0/index.js | 7 + .../packages/release-date-1.0.0/package.json | 7 + .../packages/release-date-1.1.0/index.js | 7 + .../packages/release-date-1.1.0/package.json | 7 + .../packages/release-date-1.1.1/index.js | 7 + .../packages/release-date-1.1.1/package.json | 7 + .../release-date-transitive-1.0.0/index.js | 7 + .../package.json | 4 + .../release-date-transitive-1.1.0/index.js | 7 + .../package.json | 4 + .../release-date-transitive-1.1.1/index.js | 7 + .../package.json | 4 + .../features/minimumNpmReleaseAge.test.ts | 443 ++++++++++++++++++ 21 files changed, 573 insertions(+), 3 deletions(-) create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/package.json create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/package.json create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/package.json create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.0.0/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.0.0/package.json create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.0/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.0/package.json create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.1/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.1/package.json create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.0.0/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.0.0/package.json create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.0/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.0/package.json create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.1/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.1/package.json create mode 100644 packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts diff --git a/packages/acceptance-tests/pkg-tests-core/sources/utils/makeTemporaryEnv.ts b/packages/acceptance-tests/pkg-tests-core/sources/utils/makeTemporaryEnv.ts index 635884979883..b3fd5460b643 100644 --- a/packages/acceptance-tests/pkg-tests-core/sources/utils/makeTemporaryEnv.ts +++ b/packages/acceptance-tests/pkg-tests-core/sources/utils/makeTemporaryEnv.ts @@ -18,7 +18,7 @@ const mte = generatePkgDriver({ ) { const rcEnv: Record = {}; for (const [key, value] of Object.entries(config)) - rcEnv[`YARN_${key.replace(/([A-Z])/g, `_$1`).toUpperCase()}`] = Array.isArray(value) ? value.join(`;`) : value; + rcEnv[`YARN_${key.replace(/([A-Z])/g, `_$1`).toUpperCase()}`] = Array.isArray(value) ? value.join(`,`) : value; const nativePath = npath.fromPortablePath(path); const nativeHomePath = npath.dirname(nativePath); diff --git a/packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts b/packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts index 40e4efc26647..11f5c357ef7c 100644 --- a/packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts +++ b/packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts @@ -46,7 +46,7 @@ export const TEST_TIMEOUT = os.endianness() === `BE` ? 300000 : 75000; -export type PackageEntry = Map}>; +export type PackageEntry = Map, releaseDate: string | undefined}>; export type PackageRegistry = Map; interface RunDriverOptions extends Record { @@ -177,6 +177,24 @@ export const ADVISORIES = new Map>([ }]], ]); +const RELEASE_DATE_PACKAGES: Record> = { + "release-date": { + "1.0.0": new Date(new Date().getTime() - /* 10 days */ 1000 * 60 * 60 * 24 * 10).toISOString(), + "1.1.0": new Date(new Date().getTime() - /* 5 days */ 1000 * 60 * 60 * 24 * 5).toISOString(), + "1.1.1": new Date().toISOString(), + }, + "release-date-transitive": { + "1.0.0": new Date(new Date().getTime() - /* 10 days */ 1000 * 60 * 60 * 24 * 10).toISOString(), + "1.1.0": new Date(new Date().getTime() - /* 5 days */ 1000 * 60 * 60 * 24 * 5).toISOString(), + "1.1.1": new Date().toISOString(), + }, + "@scoped/release-date": { + "1.0.0": new Date(new Date().getTime() - /* 10 days */ 1000 * 60 * 60 * 24 * 10).toISOString(), + "1.1.0": new Date(new Date().getTime() - /* 5 days */ 1000 * 60 * 60 * 24 * 5).toISOString(), + "1.1.1": new Date().toISOString(), + }, +}; + export const validLogins = { fooUser: new Login(`foo-user`), barUser: new Login(`bar-user`), @@ -233,7 +251,7 @@ export const getPackageRegistry = (): Promise => { const packageFile = ppath.join(packagesDir, packageName, Filename.manifest); const packageJson = await xfs.readJsonPromise(packageFile); - const {name, version} = packageJson; + const {name, version}: {name: string, version: string} = packageJson; if (name.startsWith(`git-`)) continue; @@ -422,6 +440,7 @@ export const startPackageServer = ({type}: {type: keyof typeof packageServerUrls }), )), ), + time: name in RELEASE_DATE_PACKAGES ? RELEASE_DATE_PACKAGES[name] : undefined, [`dist-tags`]: { latest: semver.maxSatisfying(versions, `*`), ...distTags, diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/package.json new file mode 100644 index 000000000000..1ee10b6bd2dd --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/package.json @@ -0,0 +1,4 @@ +{ + "name": "@scoped/release-date", + "version": "1.0.0" +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/package.json new file mode 100644 index 000000000000..3f582ef7a5d3 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/package.json @@ -0,0 +1,4 @@ +{ + "name": "@scoped/release-date", + "version": "1.1.0" +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/package.json new file mode 100644 index 000000000000..784977e2d31b --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/package.json @@ -0,0 +1,4 @@ +{ + "name": "@scoped/release-date", + "version": "1.1.1" +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.0.0/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.0.0/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.0.0/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.0.0/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.0.0/package.json new file mode 100644 index 000000000000..614d91d2f23b --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.0.0/package.json @@ -0,0 +1,7 @@ +{ + "name": "release-date", + "version": "1.0.0", + "dependencies": { + "release-date-transitive": "^1.0.0" + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.0/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.0/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.0/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.0/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.0/package.json new file mode 100644 index 000000000000..4b022847f964 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.0/package.json @@ -0,0 +1,7 @@ +{ + "name": "release-date", + "version": "1.1.0", + "dependencies": { + "release-date-transitive": "^1.0.0" + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.1/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.1/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.1/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.1/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.1/package.json new file mode 100644 index 000000000000..dad165c13ffa --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-1.1.1/package.json @@ -0,0 +1,7 @@ +{ + "name": "release-date", + "version": "1.1.1", + "dependencies": { + "release-date-transitive": "^1.0.0" + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.0.0/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.0.0/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.0.0/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.0.0/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.0.0/package.json new file mode 100644 index 000000000000..1bcdccc6631b --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.0.0/package.json @@ -0,0 +1,4 @@ +{ + "name": "release-date-transitive", + "version": "1.0.0" +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.0/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.0/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.0/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.0/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.0/package.json new file mode 100644 index 000000000000..059e4fc477f1 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.0/package.json @@ -0,0 +1,4 @@ +{ + "name": "release-date-transitive", + "version": "1.1.0" +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.1/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.1/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.1/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.1/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.1/package.json new file mode 100644 index 000000000000..89d229c39d22 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/release-date-transitive-1.1.1/package.json @@ -0,0 +1,4 @@ +{ + "name": "release-date-transitive", + "version": "1.1.1" +} diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts new file mode 100644 index 000000000000..b0e0f61c42d2 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts @@ -0,0 +1,443 @@ +const ONE_DAY_IN_MINUTES = 24 * 60; + +describe(`Features`, () => { + describe(`minimumNpmReleaseAge and minimumNpmReleaseAgeExclude`, () => { + describe(`add`, () => { + // TODO failing + test( + `add should install the latest version allowed by the minimum release age`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run, source}) => { + await run(`add`, `release-date`); + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.0`, + }); + }), + ); + + test( + `it should fail when trying to install exact version that is newer than the minimum release age`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run}) => { + await expect(run(`add`, `release-date@1.1.1`)).rejects.toThrowError(`No candidates found`); + }), + ); + + test( + `it should install older package versions when the minimum release age disallows the newest suitable version`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run, source}) => { + await run(`add`, `release-date@^1.0.0`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.0`, + }); + }), + ); + + test( + `it should install new version when excluded by exact locator; while transitive dependencies are not excluded`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date@1.1.1`], + // we are checking a transitive dependencies version, which the pnp will throw an error for + // disabling these checks for the purpose of this test + pnpFallbackMode: `all`, + pnpMode: `loose`, + }, async ({run, source}) => { + await run(`add`, `release-date@^1.0.0`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + + await expect(source(`require('release-date-transitive/package.json')`)).resolves.toMatchObject({ + name: `release-date-transitive`, + version: `1.1.0`, + }); + }), + ); + + test( + `it should install new version when excluded by npm protocol locator`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date@npm:1.1.1`], + }, async ({run, source}) => { + await run(`add`, `release-date@1.1.1`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by descriptor range`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date@^1.0.0`], + }, async ({run, source}) => { + await run(`add`, `release-date@^1.0.0`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by npm protocol descriptor range`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date@npm:^1.0.0`], + }, async ({run, source}) => { + await run(`add`, `release-date@^1.0.0`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by package name glob pattern`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-*`], + }, async ({run, source}) => { + await run(`add`, `release-date@^1.0.0`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by package ident`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date`], + }, async ({run, source}) => { + await run(`add`, `release-date@^1.0.0`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should not impact semver prioritization of newer versions when multiple versions meet the age requirement`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: 0, + }, async ({run, source}) => { + await run(`add`, `release-date@^1.0.0`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should work with scoped packages`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run}) => { + await expect(run(`add`, `@scoped/release-date@1.1.1`)).rejects.toThrowError(`No candidates found`); + }), + ); + + test( + `it should install scoped package when excluded`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`@scoped/release-date`], + }, async ({run, source}) => { + await run(`add`, `@scoped/release-date@^1.0.0`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install scoped package when excluded by scoped glob pattern`, + makeTemporaryEnv({}, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`@scoped/*`], + }, async ({run, source}) => { + await run(`add`, `@scoped/release-date@^1.0.0`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.1`, + }); + }), + ); + }); + describe(`install`, () => { + test( + `it should fail when trying to install exact version that is too new`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `1.1.1`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run}) => { + await expect(run(`install`)).rejects.toThrowError(`No candidates found`); + }), + ); + + test( + `it should install older package versions when the minimum release age disallows the newest suitable version`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.0`, + }); + }), + ); + + test( + `it should install new version when excluded by exact locator`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date@1.1.1`], + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by npm protocol locator`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `1.1.1`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date@npm:1.1.1`], + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by descriptor range`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date@^1.0.0`], + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by npm protocol descriptor range`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date@npm:^1.0.0`], + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by package name glob pattern`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-*`], + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install new version when excluded by package ident`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`release-date`], + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should not impact semver prioritization of newer versions when multiple versions meet the age requirement`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: 0, + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should work with scoped packages`, + makeTemporaryEnv({ + dependencies: {[`@scoped/release-date`]: `1.1.1`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run}) => { + await expect(run(`install`)).rejects.toThrowError(`No candidates found`); + }), + ); + + test( + `it should install scoped package when excluded`, + makeTemporaryEnv({ + dependencies: {[`@scoped/release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`@scoped/release-date`], + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.1`, + }); + }), + ); + + test( + `it should install scoped package when excluded by scoped glob pattern`, + makeTemporaryEnv({ + dependencies: {[`@scoped/release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + minimumNpmReleaseAgeExclude: [`@scoped/*`], + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.1`, + }); + }), + ); + }); + describe(`up`, () => { + // TODO failing + test( + `it should update to the latest version allowed by the minimum release age`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run, source}) => { + await run(`install`); + await run(`set`, `resolution`, `release-date@npm:^1.0.0`, `npm:1.0.0`); + + const preUpVersion = (await source(`require('release-date/package.json')`)).version; + if (preUpVersion !== `1.0.0`) + throw new Error(`Pre-up version is not 1.0.0`); + + await run(`up`, `release-date`); + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.0`, + }); + }), + ); + // TODO failing + test( + `recursive should update to the latest version allowed by the minimum release age`, + makeTemporaryEnv({ + dependencies: {[`release-date`]: `^1.0.0`}, + }, { + // we are checking a transitive dependencies version, which the pnp will throw an error for + // disabling these checks for the purpose of this test + pnpFallbackMode: `all`, + pnpMode: `loose`, + minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + }, async ({run, source}) => { + await run(`install`); + await run(`set`, `resolution`, `release-date@npm:^1.0.0`, `npm:1.0.0`); + await run(`set`, `resolution`, `release-date-transitive@npm:^1.0.0`, `npm:1.0.0`); + + const preUpVersion = (await source(`require('release-date/package.json')`)).version; + const preUpVersionTransitive = (await source(`require('release-date/package.json')`)).version; + if (preUpVersion !== `1.0.0` || preUpVersionTransitive !== `1.0.0`) + throw new Error(`Pre-up version is not 1.0.0`); + + await run(`up`, `-R`, `*`); + await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + name: `release-date`, + version: `1.1.0`, + }); + await expect(source(`require('release-date-transitive/package.json')`)).resolves.toMatchObject({ + name: `release-date-transitive`, + version: `1.1.0`, + }); + }), + ); + }); + }); +}); From 11f5edd381f9c8eecf6d71a63808c0b9eee616d3 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 04:46:46 -0400 Subject: [PATCH 04/19] fix: rename options to better align with existing npm-related options --- .../features/minimumNpmReleaseAge.test.ts | 88 +++++++++---------- .../static/configuration/yarnrc.json | 35 ++++---- .../plugin-npm/sources/NpmSemverResolver.ts | 4 +- .../yarnpkg-core/sources/Configuration.ts | 8 +- 4 files changed, 68 insertions(+), 67 deletions(-) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts index b0e0f61c42d2..70906b7bef71 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts @@ -1,13 +1,13 @@ const ONE_DAY_IN_MINUTES = 24 * 60; describe(`Features`, () => { - describe(`minimumNpmReleaseAge and minimumNpmReleaseAgeExclude`, () => { + describe(`npmMinimumReleaseAge and npmMinimumReleaseAgeExclude`, () => { describe(`add`, () => { // TODO failing test( `add should install the latest version allowed by the minimum release age`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`add`, `release-date`); await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ @@ -20,7 +20,7 @@ describe(`Features`, () => { test( `it should fail when trying to install exact version that is newer than the minimum release age`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run}) => { await expect(run(`add`, `release-date@1.1.1`)).rejects.toThrowError(`No candidates found`); }), @@ -29,7 +29,7 @@ describe(`Features`, () => { test( `it should install older package versions when the minimum release age disallows the newest suitable version`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -43,8 +43,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by exact locator; while transitive dependencies are not excluded`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date@1.1.1`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date@1.1.1`], // we are checking a transitive dependencies version, which the pnp will throw an error for // disabling these checks for the purpose of this test pnpFallbackMode: `all`, @@ -67,8 +67,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by npm protocol locator`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date@npm:1.1.1`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date@npm:1.1.1`], }, async ({run, source}) => { await run(`add`, `release-date@1.1.1`); @@ -82,8 +82,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by descriptor range`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date@^1.0.0`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date@^1.0.0`], }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -97,8 +97,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by npm protocol descriptor range`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date@npm:^1.0.0`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date@npm:^1.0.0`], }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -112,8 +112,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by package name glob pattern`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-*`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-*`], }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -127,8 +127,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by package ident`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date`], }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -142,7 +142,7 @@ describe(`Features`, () => { test( `it should not impact semver prioritization of newer versions when multiple versions meet the age requirement`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: 0, + npmMinimumReleaseAge: 0, }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -156,7 +156,7 @@ describe(`Features`, () => { test( `it should work with scoped packages`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run}) => { await expect(run(`add`, `@scoped/release-date@1.1.1`)).rejects.toThrowError(`No candidates found`); }), @@ -165,8 +165,8 @@ describe(`Features`, () => { test( `it should install scoped package when excluded`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`@scoped/release-date`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`@scoped/release-date`], }, async ({run, source}) => { await run(`add`, `@scoped/release-date@^1.0.0`); @@ -180,8 +180,8 @@ describe(`Features`, () => { test( `it should install scoped package when excluded by scoped glob pattern`, makeTemporaryEnv({}, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`@scoped/*`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`@scoped/*`], }, async ({run, source}) => { await run(`add`, `@scoped/release-date@^1.0.0`); @@ -198,7 +198,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `1.1.1`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run}) => { await expect(run(`install`)).rejects.toThrowError(`No candidates found`); }), @@ -209,7 +209,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`install`); @@ -225,8 +225,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date@1.1.1`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date@1.1.1`], }, async ({run, source}) => { await run(`install`); @@ -242,8 +242,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `1.1.1`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date@npm:1.1.1`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date@npm:1.1.1`], }, async ({run, source}) => { await run(`install`); @@ -259,8 +259,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date@^1.0.0`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date@^1.0.0`], }, async ({run, source}) => { await run(`install`); @@ -276,8 +276,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date@npm:^1.0.0`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date@npm:^1.0.0`], }, async ({run, source}) => { await run(`install`); @@ -293,8 +293,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-*`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-*`], }, async ({run, source}) => { await run(`install`); @@ -310,8 +310,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`release-date`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`release-date`], }, async ({run, source}) => { await run(`install`); @@ -327,7 +327,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: 0, + npmMinimumReleaseAge: 0, }, async ({run, source}) => { await run(`install`); @@ -343,7 +343,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`@scoped/release-date`]: `1.1.1`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run}) => { await expect(run(`install`)).rejects.toThrowError(`No candidates found`); }), @@ -354,8 +354,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`@scoped/release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`@scoped/release-date`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`@scoped/release-date`], }, async ({run, source}) => { await run(`install`); @@ -371,8 +371,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`@scoped/release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, - minimumNpmReleaseAgeExclude: [`@scoped/*`], + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAgeExclude: [`@scoped/*`], }, async ({run, source}) => { await run(`install`); @@ -390,7 +390,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`install`); await run(`set`, `resolution`, `release-date@npm:^1.0.0`, `npm:1.0.0`); @@ -416,7 +416,7 @@ describe(`Features`, () => { // disabling these checks for the purpose of this test pnpFallbackMode: `all`, pnpMode: `loose`, - minimumNpmReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`install`); await run(`set`, `resolution`, `release-date@npm:^1.0.0`, `npm:1.0.0`); diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index 670a5c4e0858..2a9fa10875dc 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -407,23 +407,6 @@ } ] }, - "minimumReleaseAge": { - "_package": "@yarnpkg/core", - "title": "Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation.", - "description": "If a package version is newer than the minimum release age, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages.", - "type": "number", - "default": 0 - }, - "minimumReleaseAgeExcludes": { - "_package": "@yarnpkg/core", - "title": "Array of exact package descriptors, exact package locators, or package name glob patterns to exclude from the minimum release age check.", - "description": "If a package descriptor, locator, or name matches specified pattern, it will not be considered for the minimum release age check. That is, the newest matching package version will be selected for installation regardless of the minimum release age configuration.", - "type": "array", - "items": { - "type": "string" - }, - "default": [] - }, "networkConcurrency": { "_package": "@yarnpkg/core", "title": "Amount of HTTP requests that are allowed to run at the same time.", @@ -496,6 +479,24 @@ "type": "string", "default": "pnp" }, + "npmMinimumReleaseAge": { + "_package": "@yarnpkg/core", + "title": "Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation.", + "description": "If a package version is newer than the minimum release age, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages.", + "type": "number", + "default": 0 + }, + "npmMinimumReleaseAgeExclude": { + "_package": "@yarnpkg/core", + "title": "Array of exact package descriptors, exact package locators, or package name glob patterns to exclude from the minimum release age check.", + "description": "If a package descriptor, locator, or name matches specified pattern, it will not be considered for the minimum release age check. That is, the newest matching package version will be selected for installation regardless of the minimum release age configuration.", + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "examples": ["@scoped-package/*", "package", "package*", "package@^1.0.0", "package@1.0.0", "package@npm:1.0.0", "package@npm:^1.0.0"] + }, "pnpmStoreFolder": { "_package": "@yarnpkg/plugin-pnpm", "title": "Path where the pnpm store will be stored", diff --git a/packages/plugin-npm/sources/NpmSemverResolver.ts b/packages/plugin-npm/sources/NpmSemverResolver.ts index 4b13d7b709f5..88d6db882309 100644 --- a/packages/plugin-npm/sources/NpmSemverResolver.ts +++ b/packages/plugin-npm/sources/NpmSemverResolver.ts @@ -59,9 +59,9 @@ export class NpmSemverResolver implements Resolver { try { const candidate = new semverUtils.SemVer(version); if (range.test(candidate)) { - const minimumReleaseAge = opts.project.configuration.get(`minimumNpmReleaseAge`); + const minimumReleaseAge = opts.project.configuration.get(`npmMinimumReleaseAge`); if (minimumReleaseAge) { - const minimumReleaseAgeExclude = opts.project.configuration.get(`minimumNpmReleaseAgeExclude`); + const minimumReleaseAgeExclude = opts.project.configuration.get(`npmMinimumReleaseAgeExclude`); const shouldExclude = minimumReleaseAgeExclude.some(exclude => structUtils.stringifyIdent(descriptor) === exclude || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, version)) === exclude diff --git a/packages/yarnpkg-core/sources/Configuration.ts b/packages/yarnpkg-core/sources/Configuration.ts index 85bd1dc1579e..634985519082 100644 --- a/packages/yarnpkg-core/sources/Configuration.ts +++ b/packages/yarnpkg-core/sources/Configuration.ts @@ -577,12 +577,12 @@ export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} = type: SettingsType.STRING, default: `throw`, }, - minimumNpmReleaseAge: { + npmMinimumReleaseAge: { description: `Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation`, type: SettingsType.NUMBER, default: 0, }, - minimumNpmReleaseAgeExclude: { + npmMinimumReleaseAgeExclude: { description: `Array of package name glob patterns to exclude from the minimum release age check`, type: SettingsType.STRING, isArray: true, @@ -710,8 +710,8 @@ export interface ConfigurationValueMap { enableStrictSettings: boolean; enableImmutableCache: boolean; checksumBehavior: string; - minimumNpmReleaseAge: number; - minimumNpmReleaseAgeExclude: Array; + npmMinimumReleaseAge: number; + npmMinimumReleaseAgeExclude: Array; // Miscellaneous settings injectEnvironmentFiles: Array; From eccba6cd52604193f25b8c6b4ab76bc2aec42adf Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 04:48:26 -0400 Subject: [PATCH 05/19] fix: change the way unknowns are resolved to fix `add`/`up` --- .../sources/features/minimumNpmReleaseAge.test.ts | 3 --- packages/plugin-essentials/sources/suggestUtils.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts index 70906b7bef71..1123cde74c88 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts @@ -3,7 +3,6 @@ const ONE_DAY_IN_MINUTES = 24 * 60; describe(`Features`, () => { describe(`npmMinimumReleaseAge and npmMinimumReleaseAgeExclude`, () => { describe(`add`, () => { - // TODO failing test( `add should install the latest version allowed by the minimum release age`, makeTemporaryEnv({}, { @@ -384,7 +383,6 @@ describe(`Features`, () => { ); }); describe(`up`, () => { - // TODO failing test( `it should update to the latest version allowed by the minimum release age`, makeTemporaryEnv({ @@ -406,7 +404,6 @@ describe(`Features`, () => { }); }), ); - // TODO failing test( `recursive should update to the latest version allowed by the minimum release age`, makeTemporaryEnv({ diff --git a/packages/plugin-essentials/sources/suggestUtils.ts b/packages/plugin-essentials/sources/suggestUtils.ts index f54e4b73cb26..b6afd9e966d5 100644 --- a/packages/plugin-essentials/sources/suggestUtils.ts +++ b/packages/plugin-essentials/sources/suggestUtils.ts @@ -215,7 +215,7 @@ type InferenceParameters = { function extractInferenceParametersFromRequest(request: Descriptor): InferenceParameters { if (request.range === `unknown`) - return {type: `resolve`, range: `latest`}; + return {type: `resolve`, range: `*`}; if (semverUtils.validRange(request.range)) return {type: `fixed`, range: request.range}; From 257fa2d011e83cfc2232d0d81c0a7711c6e4826d Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 04:56:54 -0400 Subject: [PATCH 06/19] fix: fix release packages based on ci output --- .yarn/versions/25eb93a2.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.yarn/versions/25eb93a2.yml b/.yarn/versions/25eb93a2.yml index 087e49fa5bec..063ac7398d68 100644 --- a/.yarn/versions/25eb93a2.yml +++ b/.yarn/versions/25eb93a2.yml @@ -1,3 +1,35 @@ releases: "@yarnpkg/core": minor + "@yarnpkg/plugin-essentials": minor "@yarnpkg/plugin-npm": minor + "@yarnpkg/plugin-compat": minor + "@yarnpkg/plugin-constraints": minor + "@yarnpkg/plugin-dlx": minor + "@yarnpkg/plugin-exec": minor + "@yarnpkg/plugin-file": minor + "@yarnpkg/plugin-git": minor + "@yarnpkg/plugin-github": minor + "@yarnpkg/plugin-http": minor + "@yarnpkg/plugin-init": minor + "@yarnpkg/plugin-interactive-tools": minor + "@yarnpkg/plugin-jsr": minor + "@yarnpkg/plugin-link": minor + "@yarnpkg/plugin-nm": minor + "@yarnpkg/plugin-npm-cli": minor + "@yarnpkg/plugin-pack": minor + "@yarnpkg/plugin-patch": minor + "@yarnpkg/plugin-pnp": minor + "@yarnpkg/plugin-pnpm": minor + "@yarnpkg/plugin-stage": minor + "@yarnpkg/plugin-typescript": minor + "@yarnpkg/plugin-version": minor + "@yarnpkg/plugin-workspace-tools": minor + "@yarnpkg/builder": minor + "@yarnpkg/cli": minor + "@yarnpkg/doctor": minor + "@yarnpkg/extensions": minor + "@yarnpkg/nm": minor + "@yarnpkg/pnpify": minor + "@yarnpkg/sdks": minor + + From 84d4b5d321364b1a209166ba23f1d038d4e57ef8 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 05:44:50 -0400 Subject: [PATCH 07/19] refactor: rename options --- ...eAge.test.ts => npmMinimalAgeGate.test.ts} | 88 +++++++++---------- .../static/configuration/yarnrc.json | 4 +- .../plugin-npm/sources/NpmSemverResolver.ts | 10 +-- .../yarnpkg-core/sources/Configuration.ts | 8 +- 4 files changed, 55 insertions(+), 55 deletions(-) rename packages/acceptance-tests/pkg-tests-specs/sources/features/{minimumNpmReleaseAge.test.ts => npmMinimalAgeGate.test.ts} (84%) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts similarity index 84% rename from packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts rename to packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts index 1123cde74c88..fe234ad25672 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/minimumNpmReleaseAge.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts @@ -1,12 +1,12 @@ const ONE_DAY_IN_MINUTES = 24 * 60; describe(`Features`, () => { - describe(`npmMinimumReleaseAge and npmMinimumReleaseAgeExclude`, () => { + describe(`npmMinimalAgeGate and npmPreapprovedPackages`, () => { describe(`add`, () => { test( `add should install the latest version allowed by the minimum release age`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`add`, `release-date`); await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ @@ -19,7 +19,7 @@ describe(`Features`, () => { test( `it should fail when trying to install exact version that is newer than the minimum release age`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run}) => { await expect(run(`add`, `release-date@1.1.1`)).rejects.toThrowError(`No candidates found`); }), @@ -28,7 +28,7 @@ describe(`Features`, () => { test( `it should install older package versions when the minimum release age disallows the newest suitable version`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -42,8 +42,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by exact locator; while transitive dependencies are not excluded`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date@1.1.1`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date@1.1.1`], // we are checking a transitive dependencies version, which the pnp will throw an error for // disabling these checks for the purpose of this test pnpFallbackMode: `all`, @@ -66,8 +66,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by npm protocol locator`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date@npm:1.1.1`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date@npm:1.1.1`], }, async ({run, source}) => { await run(`add`, `release-date@1.1.1`); @@ -81,8 +81,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by descriptor range`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date@^1.0.0`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date@^1.0.0`], }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -96,8 +96,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by npm protocol descriptor range`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date@npm:^1.0.0`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date@npm:^1.0.0`], }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -111,8 +111,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by package name glob pattern`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-*`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-*`], }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -126,8 +126,8 @@ describe(`Features`, () => { test( `it should install new version when excluded by package ident`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date`], }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -141,7 +141,7 @@ describe(`Features`, () => { test( `it should not impact semver prioritization of newer versions when multiple versions meet the age requirement`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: 0, + npmMinimalAgeGate: 0, }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); @@ -155,7 +155,7 @@ describe(`Features`, () => { test( `it should work with scoped packages`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run}) => { await expect(run(`add`, `@scoped/release-date@1.1.1`)).rejects.toThrowError(`No candidates found`); }), @@ -164,8 +164,8 @@ describe(`Features`, () => { test( `it should install scoped package when excluded`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`@scoped/release-date`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`@scoped/release-date`], }, async ({run, source}) => { await run(`add`, `@scoped/release-date@^1.0.0`); @@ -179,8 +179,8 @@ describe(`Features`, () => { test( `it should install scoped package when excluded by scoped glob pattern`, makeTemporaryEnv({}, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`@scoped/*`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`@scoped/*`], }, async ({run, source}) => { await run(`add`, `@scoped/release-date@^1.0.0`); @@ -197,7 +197,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `1.1.1`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run}) => { await expect(run(`install`)).rejects.toThrowError(`No candidates found`); }), @@ -208,7 +208,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`install`); @@ -224,8 +224,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date@1.1.1`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date@1.1.1`], }, async ({run, source}) => { await run(`install`); @@ -241,8 +241,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `1.1.1`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date@npm:1.1.1`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date@npm:1.1.1`], }, async ({run, source}) => { await run(`install`); @@ -258,8 +258,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date@^1.0.0`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date@^1.0.0`], }, async ({run, source}) => { await run(`install`); @@ -275,8 +275,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date@npm:^1.0.0`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date@npm:^1.0.0`], }, async ({run, source}) => { await run(`install`); @@ -292,8 +292,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-*`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-*`], }, async ({run, source}) => { await run(`install`); @@ -309,8 +309,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`release-date`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`release-date`], }, async ({run, source}) => { await run(`install`); @@ -326,7 +326,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: 0, + npmMinimalAgeGate: 0, }, async ({run, source}) => { await run(`install`); @@ -342,7 +342,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`@scoped/release-date`]: `1.1.1`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run}) => { await expect(run(`install`)).rejects.toThrowError(`No candidates found`); }), @@ -353,8 +353,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`@scoped/release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`@scoped/release-date`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`@scoped/release-date`], }, async ({run, source}) => { await run(`install`); @@ -370,8 +370,8 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`@scoped/release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, - npmMinimumReleaseAgeExclude: [`@scoped/*`], + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + npmPreapprovedPackages: [`@scoped/*`], }, async ({run, source}) => { await run(`install`); @@ -388,7 +388,7 @@ describe(`Features`, () => { makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`install`); await run(`set`, `resolution`, `release-date@npm:^1.0.0`, `npm:1.0.0`); @@ -413,7 +413,7 @@ describe(`Features`, () => { // disabling these checks for the purpose of this test pnpFallbackMode: `all`, pnpMode: `loose`, - npmMinimumReleaseAge: ONE_DAY_IN_MINUTES, + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, }, async ({run, source}) => { await run(`install`); await run(`set`, `resolution`, `release-date@npm:^1.0.0`, `npm:1.0.0`); diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index 2a9fa10875dc..ce7ca582a9b1 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -479,14 +479,14 @@ "type": "string", "default": "pnp" }, - "npmMinimumReleaseAge": { + "npmMinimalAgeGate": { "_package": "@yarnpkg/core", "title": "Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation.", "description": "If a package version is newer than the minimum release age, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages.", "type": "number", "default": 0 }, - "npmMinimumReleaseAgeExclude": { + "npmPreapprovedPackages": { "_package": "@yarnpkg/core", "title": "Array of exact package descriptors, exact package locators, or package name glob patterns to exclude from the minimum release age check.", "description": "If a package descriptor, locator, or name matches specified pattern, it will not be considered for the minimum release age check. That is, the newest matching package version will be selected for installation regardless of the minimum release age configuration.", diff --git a/packages/plugin-npm/sources/NpmSemverResolver.ts b/packages/plugin-npm/sources/NpmSemverResolver.ts index 88d6db882309..ab3192e620ae 100644 --- a/packages/plugin-npm/sources/NpmSemverResolver.ts +++ b/packages/plugin-npm/sources/NpmSemverResolver.ts @@ -59,10 +59,10 @@ export class NpmSemverResolver implements Resolver { try { const candidate = new semverUtils.SemVer(version); if (range.test(candidate)) { - const minimumReleaseAge = opts.project.configuration.get(`npmMinimumReleaseAge`); - if (minimumReleaseAge) { - const minimumReleaseAgeExclude = opts.project.configuration.get(`npmMinimumReleaseAgeExclude`); - const shouldExclude = minimumReleaseAgeExclude.some(exclude => + const minimalAgeGate = opts.project.configuration.get(`npmMinimalAgeGate`); + if (minimalAgeGate) { + const preapprovedPackages = opts.project.configuration.get(`npmPreapprovedPackages`); + const shouldExclude = preapprovedPackages.some(exclude => structUtils.stringifyIdent(descriptor) === exclude || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, version)) === exclude || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, `${PROTOCOL}:${version}`)) === exclude @@ -72,7 +72,7 @@ export class NpmSemverResolver implements Resolver { if (!shouldExclude) { const versionTime = new Date(registryData.time[version]); const ageMinutes = (new Date().getTime() - versionTime.getTime()) / 60 / 1000; - if (ageMinutes < minimumReleaseAge) { + if (ageMinutes < minimalAgeGate) { return miscUtils.mapAndFilter.skip; } } diff --git a/packages/yarnpkg-core/sources/Configuration.ts b/packages/yarnpkg-core/sources/Configuration.ts index 634985519082..9ee615207573 100644 --- a/packages/yarnpkg-core/sources/Configuration.ts +++ b/packages/yarnpkg-core/sources/Configuration.ts @@ -577,12 +577,12 @@ export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} = type: SettingsType.STRING, default: `throw`, }, - npmMinimumReleaseAge: { + npmMinimalAgeGate: { description: `Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation`, type: SettingsType.NUMBER, default: 0, }, - npmMinimumReleaseAgeExclude: { + npmPreapprovedPackages: { description: `Array of package name glob patterns to exclude from the minimum release age check`, type: SettingsType.STRING, isArray: true, @@ -710,8 +710,8 @@ export interface ConfigurationValueMap { enableStrictSettings: boolean; enableImmutableCache: boolean; checksumBehavior: string; - npmMinimumReleaseAge: number; - npmMinimumReleaseAgeExclude: Array; + npmMinimalAgeGate: number; + npmPreapprovedPackages: Array; // Miscellaneous settings injectEnvironmentFiles: Array; From 0c7b130bd480982cb342dc147b77fe146e09add6 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 05:45:19 -0400 Subject: [PATCH 08/19] Revert "fix: change the way unknowns are resolved to fix `add`/`up`" This reverts commit eccba6cd52604193f25b8c6b4ab76bc2aec42adf. --- .../pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts | 3 +++ packages/plugin-essentials/sources/suggestUtils.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts index fe234ad25672..5c9fc37b2ea4 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts @@ -3,6 +3,7 @@ const ONE_DAY_IN_MINUTES = 24 * 60; describe(`Features`, () => { describe(`npmMinimalAgeGate and npmPreapprovedPackages`, () => { describe(`add`, () => { + // TODO failing test( `add should install the latest version allowed by the minimum release age`, makeTemporaryEnv({}, { @@ -383,6 +384,7 @@ describe(`Features`, () => { ); }); describe(`up`, () => { + // TODO failing test( `it should update to the latest version allowed by the minimum release age`, makeTemporaryEnv({ @@ -404,6 +406,7 @@ describe(`Features`, () => { }); }), ); + // TODO failing test( `recursive should update to the latest version allowed by the minimum release age`, makeTemporaryEnv({ diff --git a/packages/plugin-essentials/sources/suggestUtils.ts b/packages/plugin-essentials/sources/suggestUtils.ts index b6afd9e966d5..f54e4b73cb26 100644 --- a/packages/plugin-essentials/sources/suggestUtils.ts +++ b/packages/plugin-essentials/sources/suggestUtils.ts @@ -215,7 +215,7 @@ type InferenceParameters = { function extractInferenceParametersFromRequest(request: Descriptor): InferenceParameters { if (request.range === `unknown`) - return {type: `resolve`, range: `*`}; + return {type: `resolve`, range: `latest`}; if (semverUtils.validRange(request.range)) return {type: `fixed`, range: request.range}; From ebd41b5d28c27da63099cd34eba6e49875ac9f17 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 05:56:58 -0400 Subject: [PATCH 09/19] refactor: move exclusion logic to a helper --- .../plugin-npm/sources/NpmSemverResolver.ts | 26 +++----------- packages/plugin-npm/sources/npmConfigUtils.ts | 35 ++++++++++++++++++- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/packages/plugin-npm/sources/NpmSemverResolver.ts b/packages/plugin-npm/sources/NpmSemverResolver.ts index ab3192e620ae..cb7ad3bca62e 100644 --- a/packages/plugin-npm/sources/NpmSemverResolver.ts +++ b/packages/plugin-npm/sources/NpmSemverResolver.ts @@ -2,11 +2,11 @@ import {ReportError, MessageName, Resolver, ResolveOptions, MinimalResolveOption import {Descriptor, Locator, semverUtils} from '@yarnpkg/core'; import {LinkType} from '@yarnpkg/core'; import {structUtils} from '@yarnpkg/core'; -import micromatch from 'micromatch'; import semver from 'semver'; import {NpmSemverFetcher} from './NpmSemverFetcher'; import {PROTOCOL} from './constants'; +import {shouldExcludeCandidate} from './npmConfigUtils'; import * as npmHttpUtils from './npmHttpUtils'; const NODE_GYP_IDENT = structUtils.makeIdent(null, `node-gyp`); @@ -44,8 +44,7 @@ export class NpmSemverResolver implements Resolver { } async getCandidates(descriptor: Descriptor, dependencies: Record, opts: ResolveOptions) { - const rawRange = descriptor.range.slice(PROTOCOL.length); - const range = semverUtils.validRange(rawRange); + const range = semverUtils.validRange(descriptor.range.slice(PROTOCOL.length)); if (range === null) throw new Error(`Expected a valid range, got ${descriptor.range.slice(PROTOCOL.length)}`); @@ -59,24 +58,9 @@ export class NpmSemverResolver implements Resolver { try { const candidate = new semverUtils.SemVer(version); if (range.test(candidate)) { - const minimalAgeGate = opts.project.configuration.get(`npmMinimalAgeGate`); - if (minimalAgeGate) { - const preapprovedPackages = opts.project.configuration.get(`npmPreapprovedPackages`); - const shouldExclude = preapprovedPackages.some(exclude => - structUtils.stringifyIdent(descriptor) === exclude - || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, version)) === exclude - || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, `${PROTOCOL}:${version}`)) === exclude - || micromatch.isMatch(structUtils.stringifyDescriptor({...descriptor, range: rawRange}), exclude) - || micromatch.isMatch(structUtils.stringifyDescriptor(descriptor), exclude), - ); - if (!shouldExclude) { - const versionTime = new Date(registryData.time[version]); - const ageMinutes = (new Date().getTime() - versionTime.getTime()) / 60 / 1000; - if (ageMinutes < minimalAgeGate) { - return miscUtils.mapAndFilter.skip; - } - } - } + if (shouldExcludeCandidate({configuration: opts.project.configuration, descriptor, version, publishTimes: registryData.time})) + return miscUtils.mapAndFilter.skip; + return candidate; } } catch { } diff --git a/packages/plugin-npm/sources/npmConfigUtils.ts b/packages/plugin-npm/sources/npmConfigUtils.ts index 8d8dfb3fd2b0..8435101abef7 100644 --- a/packages/plugin-npm/sources/npmConfigUtils.ts +++ b/packages/plugin-npm/sources/npmConfigUtils.ts @@ -1,4 +1,7 @@ -import {Configuration, Manifest, Ident} from '@yarnpkg/core'; +import {Configuration, Manifest, Ident, structUtils, Descriptor} from '@yarnpkg/core'; +import micromatch from 'micromatch'; + +import {PROTOCOL} from './constants'; export enum RegistryType { AUDIT_REGISTRY = `npmAuditRegistry`, @@ -94,3 +97,33 @@ export function getAuthConfiguration(registry: string, {configuration, ident}: { return registryConfiguration || configuration; } + +export type ShouldExcludeCandidateOptions = { + configuration: Configuration; + descriptor: Descriptor; + version: string; + publishTimes: Record; +}; + +export function shouldExcludeCandidate({configuration, descriptor, version, publishTimes}: ShouldExcludeCandidateOptions) { + const range = descriptor.range.slice(PROTOCOL.length); + const minimalAgeGate = configuration.get(`npmMinimalAgeGate`); + if (minimalAgeGate) { + const preapprovedPackages = configuration.get(`npmPreapprovedPackages`); + const shouldExclude = preapprovedPackages.some(exclude => + structUtils.stringifyIdent(descriptor) === exclude + || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, version)) === exclude + || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, `${PROTOCOL}:${version}`)) === exclude + || micromatch.isMatch(structUtils.stringifyDescriptor({...descriptor, range}), exclude) + || micromatch.isMatch(structUtils.stringifyDescriptor(descriptor), exclude), + ); + if (!shouldExclude) { + const versionTime = new Date(publishTimes[version]); + const ageMinutes = (new Date().getTime() - versionTime.getTime()) / 60 / 1000; + if (ageMinutes < minimalAgeGate) { + return true; + } + } + } + return false; +} From 79d3aa7430b7cae95a4fa0124eea836d76180e96 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 06:22:35 -0400 Subject: [PATCH 10/19] fix: crawl package versions from highest to lowest if `latest` tag does not pass gates --- .../sources/features/npmMinimalAgeGate.test.ts | 3 --- packages/plugin-npm/sources/NpmTagResolver.ts | 9 ++++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts index 5c9fc37b2ea4..fe234ad25672 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts @@ -3,7 +3,6 @@ const ONE_DAY_IN_MINUTES = 24 * 60; describe(`Features`, () => { describe(`npmMinimalAgeGate and npmPreapprovedPackages`, () => { describe(`add`, () => { - // TODO failing test( `add should install the latest version allowed by the minimum release age`, makeTemporaryEnv({}, { @@ -384,7 +383,6 @@ describe(`Features`, () => { ); }); describe(`up`, () => { - // TODO failing test( `it should update to the latest version allowed by the minimum release age`, makeTemporaryEnv({ @@ -406,7 +404,6 @@ describe(`Features`, () => { }); }), ); - // TODO failing test( `recursive should update to the latest version allowed by the minimum release age`, makeTemporaryEnv({ diff --git a/packages/plugin-npm/sources/NpmTagResolver.ts b/packages/plugin-npm/sources/NpmTagResolver.ts index 9ced3a9e0365..8fe0cbb5f506 100644 --- a/packages/plugin-npm/sources/NpmTagResolver.ts +++ b/packages/plugin-npm/sources/NpmTagResolver.ts @@ -5,6 +5,7 @@ import semver import {NpmSemverFetcher} from './NpmSemverFetcher'; import {PROTOCOL} from './constants'; +import {shouldExcludeCandidate} from './npmConfigUtils'; import * as npmHttpUtils from './npmHttpUtils'; export class NpmTagResolver implements Resolver { @@ -52,7 +53,13 @@ export class NpmTagResolver implements Resolver { if (!Object.hasOwn(distTags, tag)) throw new ReportError(MessageName.REMOTE_NOT_FOUND, `Registry failed to return tag "${tag}"`); - const version = distTags[tag]; + const versions = Object.keys(registryData.versions); + const times = registryData.time; + const version = tag === `latest` + ? semver.rsort(versions).find(version => + !shouldExcludeCandidate({configuration: opts.project.configuration, descriptor, version, publishTimes: times}), + ) ?? distTags[tag] + : distTags[tag]; const versionLocator = structUtils.makeLocator(descriptor, `${PROTOCOL}${version}`); const archiveUrl = registryData.versions[version].dist.tarball; From 55b5185652ed9e2b297bef15347049c33bb01f9b Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 06:25:57 -0400 Subject: [PATCH 11/19] refactor: rename gate check function --- packages/plugin-npm/sources/NpmSemverResolver.ts | 4 ++-- packages/plugin-npm/sources/NpmTagResolver.ts | 4 ++-- packages/plugin-npm/sources/npmConfigUtils.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/plugin-npm/sources/NpmSemverResolver.ts b/packages/plugin-npm/sources/NpmSemverResolver.ts index cb7ad3bca62e..268759cd37d1 100644 --- a/packages/plugin-npm/sources/NpmSemverResolver.ts +++ b/packages/plugin-npm/sources/NpmSemverResolver.ts @@ -6,7 +6,7 @@ import semver import {NpmSemverFetcher} from './NpmSemverFetcher'; import {PROTOCOL} from './constants'; -import {shouldExcludeCandidate} from './npmConfigUtils'; +import {checkPackageGates} from './npmConfigUtils'; import * as npmHttpUtils from './npmHttpUtils'; const NODE_GYP_IDENT = structUtils.makeIdent(null, `node-gyp`); @@ -58,7 +58,7 @@ export class NpmSemverResolver implements Resolver { try { const candidate = new semverUtils.SemVer(version); if (range.test(candidate)) { - if (shouldExcludeCandidate({configuration: opts.project.configuration, descriptor, version, publishTimes: registryData.time})) + if (!checkPackageGates({configuration: opts.project.configuration, descriptor, version, publishTimes: registryData.time})) return miscUtils.mapAndFilter.skip; return candidate; diff --git a/packages/plugin-npm/sources/NpmTagResolver.ts b/packages/plugin-npm/sources/NpmTagResolver.ts index 8fe0cbb5f506..79f464713ac8 100644 --- a/packages/plugin-npm/sources/NpmTagResolver.ts +++ b/packages/plugin-npm/sources/NpmTagResolver.ts @@ -5,7 +5,7 @@ import semver import {NpmSemverFetcher} from './NpmSemverFetcher'; import {PROTOCOL} from './constants'; -import {shouldExcludeCandidate} from './npmConfigUtils'; +import {checkPackageGates} from './npmConfigUtils'; import * as npmHttpUtils from './npmHttpUtils'; export class NpmTagResolver implements Resolver { @@ -57,7 +57,7 @@ export class NpmTagResolver implements Resolver { const times = registryData.time; const version = tag === `latest` ? semver.rsort(versions).find(version => - !shouldExcludeCandidate({configuration: opts.project.configuration, descriptor, version, publishTimes: times}), + checkPackageGates({configuration: opts.project.configuration, descriptor, version, publishTimes: times}), ) ?? distTags[tag] : distTags[tag]; const versionLocator = structUtils.makeLocator(descriptor, `${PROTOCOL}${version}`); diff --git a/packages/plugin-npm/sources/npmConfigUtils.ts b/packages/plugin-npm/sources/npmConfigUtils.ts index 8435101abef7..660aedd20757 100644 --- a/packages/plugin-npm/sources/npmConfigUtils.ts +++ b/packages/plugin-npm/sources/npmConfigUtils.ts @@ -98,14 +98,14 @@ export function getAuthConfiguration(registry: string, {configuration, ident}: { return registryConfiguration || configuration; } -export type ShouldExcludeCandidateOptions = { +export type CheckPackageGatesOptions = { configuration: Configuration; descriptor: Descriptor; version: string; publishTimes: Record; }; -export function shouldExcludeCandidate({configuration, descriptor, version, publishTimes}: ShouldExcludeCandidateOptions) { +export function checkPackageGates({configuration, descriptor, version, publishTimes}: CheckPackageGatesOptions) { const range = descriptor.range.slice(PROTOCOL.length); const minimalAgeGate = configuration.get(`npmMinimalAgeGate`); if (minimalAgeGate) { @@ -121,9 +121,9 @@ export function shouldExcludeCandidate({configuration, descriptor, version, publ const versionTime = new Date(publishTimes[version]); const ageMinutes = (new Date().getTime() - versionTime.getTime()) / 60 / 1000; if (ageMinutes < minimalAgeGate) { - return true; + return false; } } } - return false; + return true; } From 9db6b3b66079251072792891d34d0a82ae798875 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 06:31:12 -0400 Subject: [PATCH 12/19] docs: update doc language to match option name --- packages/docusaurus/static/configuration/yarnrc.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index ce7ca582a9b1..3d2a970bf4b3 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -482,14 +482,14 @@ "npmMinimalAgeGate": { "_package": "@yarnpkg/core", "title": "Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation.", - "description": "If a package version is newer than the minimum release age, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages.", + "description": "If a package version is newer than the minimal age gate, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages.", "type": "number", "default": 0 }, "npmPreapprovedPackages": { "_package": "@yarnpkg/core", - "title": "Array of exact package descriptors, exact package locators, or package name glob patterns to exclude from the minimum release age check.", - "description": "If a package descriptor, locator, or name matches specified pattern, it will not be considered for the minimum release age check. That is, the newest matching package version will be selected for installation regardless of the minimum release age configuration.", + "title": "Array of exact package descriptors, exact package locators, or package name glob patterns to exclude from the any package gates.", + "description": "If a package descriptor, locator, or name matches specified pattern, it will not be considered when evaluating any of the package gates.", "type": "array", "items": { "type": "string" From 93957a60498ed723de12ffd71228ea8f1215a006 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 06:37:33 -0400 Subject: [PATCH 13/19] refactor: move npm-related configs into plugin-npm --- packages/plugin-npm/sources/index.ts | 18 ++++++++++++++++++ packages/yarnpkg-core/sources/Configuration.ts | 13 ------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/plugin-npm/sources/index.ts b/packages/plugin-npm/sources/index.ts index 8e2f27663935..5adfb00d2784 100644 --- a/packages/plugin-npm/sources/index.ts +++ b/packages/plugin-npm/sources/index.ts @@ -67,6 +67,20 @@ const registrySettings = { }, }; +const packageGateSettings = { + npmMinimalAgeGate: { + description: `Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation`, + type: SettingsType.NUMBER as const, + default: 0, + }, + npmPreapprovedPackages: { + description: `Array of package name glob patterns to exclude from the minimum release age check`, + type: SettingsType.STRING as const, + isArray: true as const, + default: [], + }, +}; + declare module '@yarnpkg/core' { interface ConfigurationValueMap { npmAlwaysAuth: boolean; @@ -77,6 +91,9 @@ declare module '@yarnpkg/core' { npmPublishRegistry: string | null; npmRegistryServer: string; + npmMinimalAgeGate: number; + npmPreapprovedPackages: Array; + npmScopes: Map; // Miscellaneous settings injectEnvironmentFiles: Array; From 98e58addf99a0c78267a1cec2f1de3ffcaba2c07 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 07:32:37 -0400 Subject: [PATCH 14/19] fix: if latest is unsuitable, ensure we don't select a higher version --- .../pkg-tests-core/sources/utils/tests.ts | 1 + .../@scoped__release-date-1.0.0/package.json | 5 +- .../@scoped__release-date-1.1.0/package.json | 5 +- .../@scoped__release-date-1.1.1/package.json | 5 +- .../@scoped__release-date-1.1.2/index.js | 7 +++ .../@scoped__release-date-1.1.2/package.json | 7 +++ .../features/npmMinimalAgeGate.test.ts | 57 +++++++++++++++++-- packages/plugin-npm/sources/NpmTagResolver.ts | 11 ++-- 8 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.2/index.js create mode 100644 packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.2/package.json diff --git a/packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts b/packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts index 11f5c357ef7c..49a68ae769d9 100644 --- a/packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts +++ b/packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts @@ -192,6 +192,7 @@ const RELEASE_DATE_PACKAGES: Record> = { "1.0.0": new Date(new Date().getTime() - /* 10 days */ 1000 * 60 * 60 * 24 * 10).toISOString(), "1.1.0": new Date(new Date().getTime() - /* 5 days */ 1000 * 60 * 60 * 24 * 5).toISOString(), "1.1.1": new Date().toISOString(), + "1.1.2": new Date(new Date().getTime() - /* 5 days */ 1000 * 60 * 60 * 24 * 5).toISOString(), }, }; diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/package.json index 1ee10b6bd2dd..ce451ff8ffe7 100644 --- a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/package.json +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.0.0/package.json @@ -1,4 +1,7 @@ { "name": "@scoped/release-date", - "version": "1.0.0" + "version": "1.0.0", + "dist-tags": { + "latest": "1.1.1" + } } diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/package.json index 3f582ef7a5d3..cddbb810f4cd 100644 --- a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/package.json +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.0/package.json @@ -1,4 +1,7 @@ { "name": "@scoped/release-date", - "version": "1.1.0" + "version": "1.1.0", + "dist-tags": { + "latest": "1.1.1" + } } diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/package.json index 784977e2d31b..1f68e0fd8572 100644 --- a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/package.json +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.1/package.json @@ -1,4 +1,7 @@ { "name": "@scoped/release-date", - "version": "1.1.1" + "version": "1.1.1", + "dist-tags": { + "latest": "1.1.1" + } } diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.2/index.js b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.2/index.js new file mode 100644 index 000000000000..bb9c6f687615 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.2/index.js @@ -0,0 +1,7 @@ +module.exports = require(`./package.json`); + +for (const key of [`dependencies`, `devDependencies`, `peerDependencies`]) { + for (const dep of Object.keys(module.exports[key] || {})) { + module.exports[key][dep] = require(dep); + } +} diff --git a/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.2/package.json b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.2/package.json new file mode 100644 index 000000000000..a427cbd6e053 --- /dev/null +++ b/packages/acceptance-tests/pkg-tests-fixtures/packages/@scoped__release-date-1.1.2/package.json @@ -0,0 +1,7 @@ +{ + "name": "@scoped/release-date", + "version": "1.1.2", + "dist-tags": { + "latest": "1.1.1" + } +} diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts index fe234ad25672..c22155b2e1bc 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts @@ -171,7 +171,7 @@ describe(`Features`, () => { await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ name: `@scoped/release-date`, - version: `1.1.1`, + version: `1.1.2`, }); }), ); @@ -186,7 +186,22 @@ describe(`Features`, () => { await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ name: `@scoped/release-date`, - version: `1.1.1`, + version: `1.1.2`, + }); + }), + ); + + test( + `it should not install a version that is higher than the latest tag`, + makeTemporaryEnv({ + }, { + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + }, async ({run, source}) => { + await run(`add`, `@scoped/release-date`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.0`, }); }), ); @@ -360,7 +375,7 @@ describe(`Features`, () => { await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ name: `@scoped/release-date`, - version: `1.1.1`, + version: `1.1.2`, }); }), ); @@ -377,7 +392,23 @@ describe(`Features`, () => { await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ name: `@scoped/release-date`, - version: `1.1.1`, + version: `1.1.2`, + }); + }), + ); + + test( + `it should not install a version that is higher than the latest tag`, + makeTemporaryEnv({ + dependencies: {[`@scoped/release-date`]: `latest`}, + }, { + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + }, async ({run, source}) => { + await run(`install`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.0`, }); }), ); @@ -435,6 +466,24 @@ describe(`Features`, () => { }); }), ); + + test( + `it should not update to a version that is higher than the latest tag`, + makeTemporaryEnv({ + dependencies: {[`@scoped/release-date`]: `^1.0.0`}, + }, { + npmMinimalAgeGate: ONE_DAY_IN_MINUTES, + }, async ({run, source}) => { + await run(`set`, `resolution`, `@scoped/release-date@npm:^1.0.0`, `npm:1.0.0`); + + await run(`up`, `@scoped/release-date`); + + await expect(source(`require('@scoped/release-date/package.json')`)).resolves.toMatchObject({ + name: `@scoped/release-date`, + version: `1.1.0`, + }); + }), + ); }); }); }); diff --git a/packages/plugin-npm/sources/NpmTagResolver.ts b/packages/plugin-npm/sources/NpmTagResolver.ts index 79f464713ac8..e20d65ab21cb 100644 --- a/packages/plugin-npm/sources/NpmTagResolver.ts +++ b/packages/plugin-npm/sources/NpmTagResolver.ts @@ -55,11 +55,12 @@ export class NpmTagResolver implements Resolver { const versions = Object.keys(registryData.versions); const times = registryData.time; - const version = tag === `latest` - ? semver.rsort(versions).find(version => - checkPackageGates({configuration: opts.project.configuration, descriptor, version, publishTimes: times}), - ) ?? distTags[tag] - : distTags[tag]; + let version = distTags[tag]; + if (tag === `latest` && !checkPackageGates({configuration: opts.project.configuration, descriptor, version, publishTimes: times})) + version = semver.rsort(versions) + .filter(nextVersion => semver.lt(nextVersion, version)) + .find(nextVersion => checkPackageGates({configuration: opts.project.configuration, descriptor, version: nextVersion, publishTimes: times})) ?? distTags[tag]; + const versionLocator = structUtils.makeLocator(descriptor, `${PROTOCOL}${version}`); const archiveUrl = registryData.versions[version].dist.tarball; From fb35bc84dc1fd89f6b59bb3520c55df6baa3ce37 Mon Sep 17 00:00:00 2001 From: bienzaaron Date: Wed, 17 Sep 2025 08:05:27 -0400 Subject: [PATCH 15/19] simplify `npmPreapprovedPackages` --- .yarn/versions/25eb93a2.yml | 1 + .../features/npmMinimalAgeGate.test.ts | 119 +----------------- .../static/configuration/yarnrc.json | 4 +- packages/plugin-npm/sources/index.ts | 2 +- packages/plugin-npm/sources/npmConfigUtils.ts | 8 +- 5 files changed, 11 insertions(+), 123 deletions(-) diff --git a/.yarn/versions/25eb93a2.yml b/.yarn/versions/25eb93a2.yml index 063ac7398d68..02c5606c9e5c 100644 --- a/.yarn/versions/25eb93a2.yml +++ b/.yarn/versions/25eb93a2.yml @@ -31,5 +31,6 @@ releases: "@yarnpkg/nm": minor "@yarnpkg/pnpify": minor "@yarnpkg/sdks": minor + "@yarnpkg/plugin-catalog": minor diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts index c22155b2e1bc..de3ea7b6bf85 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts @@ -40,10 +40,10 @@ describe(`Features`, () => { ); test( - `it should install new version when excluded by exact locator; while transitive dependencies are not excluded`, + `it should install new version when excluded by a descriptor; while transitive dependencies are not excluded`, makeTemporaryEnv({}, { npmMinimalAgeGate: ONE_DAY_IN_MINUTES, - npmPreapprovedPackages: [`release-date@1.1.1`], + npmPreapprovedPackages: [`release-date@^1.0.0`], // we are checking a transitive dependencies version, which the pnp will throw an error for // disabling these checks for the purpose of this test pnpFallbackMode: `all`, @@ -63,66 +63,6 @@ describe(`Features`, () => { }), ); - test( - `it should install new version when excluded by npm protocol locator`, - makeTemporaryEnv({}, { - npmMinimalAgeGate: ONE_DAY_IN_MINUTES, - npmPreapprovedPackages: [`release-date@npm:1.1.1`], - }, async ({run, source}) => { - await run(`add`, `release-date@1.1.1`); - - await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ - name: `release-date`, - version: `1.1.1`, - }); - }), - ); - - test( - `it should install new version when excluded by descriptor range`, - makeTemporaryEnv({}, { - npmMinimalAgeGate: ONE_DAY_IN_MINUTES, - npmPreapprovedPackages: [`release-date@^1.0.0`], - }, async ({run, source}) => { - await run(`add`, `release-date@^1.0.0`); - - await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ - name: `release-date`, - version: `1.1.1`, - }); - }), - ); - - test( - `it should install new version when excluded by npm protocol descriptor range`, - makeTemporaryEnv({}, { - npmMinimalAgeGate: ONE_DAY_IN_MINUTES, - npmPreapprovedPackages: [`release-date@npm:^1.0.0`], - }, async ({run, source}) => { - await run(`add`, `release-date@^1.0.0`); - - await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ - name: `release-date`, - version: `1.1.1`, - }); - }), - ); - - test( - `it should install new version when excluded by package name glob pattern`, - makeTemporaryEnv({}, { - npmMinimalAgeGate: ONE_DAY_IN_MINUTES, - npmPreapprovedPackages: [`release-*`], - }, async ({run, source}) => { - await run(`add`, `release-date@^1.0.0`); - - await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ - name: `release-date`, - version: `1.1.1`, - }); - }), - ); - test( `it should install new version when excluded by package ident`, makeTemporaryEnv({}, { @@ -192,7 +132,7 @@ describe(`Features`, () => { ); test( - `it should not install a version that is higher than the latest tag`, + `it should not install a version via add that is higher than the latest tag`, makeTemporaryEnv({ }, { npmMinimalAgeGate: ONE_DAY_IN_MINUTES, @@ -235,41 +175,7 @@ describe(`Features`, () => { ); test( - `it should install new version when excluded by exact locator`, - makeTemporaryEnv({ - dependencies: {[`release-date`]: `^1.0.0`}, - }, { - npmMinimalAgeGate: ONE_DAY_IN_MINUTES, - npmPreapprovedPackages: [`release-date@1.1.1`], - }, async ({run, source}) => { - await run(`install`); - - await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ - name: `release-date`, - version: `1.1.1`, - }); - }), - ); - - test( - `it should install new version when excluded by npm protocol locator`, - makeTemporaryEnv({ - dependencies: {[`release-date`]: `1.1.1`}, - }, { - npmMinimalAgeGate: ONE_DAY_IN_MINUTES, - npmPreapprovedPackages: [`release-date@npm:1.1.1`], - }, async ({run, source}) => { - await run(`install`); - - await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ - name: `release-date`, - version: `1.1.1`, - }); - }), - ); - - test( - `it should install new version when excluded by descriptor range`, + `it should install new version when excluded by a descriptor`, makeTemporaryEnv({ dependencies: {[`release-date`]: `^1.0.0`}, }, { @@ -285,23 +191,6 @@ describe(`Features`, () => { }), ); - test( - `it should install new version when excluded by npm protocol descriptor range`, - makeTemporaryEnv({ - dependencies: {[`release-date`]: `^1.0.0`}, - }, { - npmMinimalAgeGate: ONE_DAY_IN_MINUTES, - npmPreapprovedPackages: [`release-date@npm:^1.0.0`], - }, async ({run, source}) => { - await run(`install`); - - await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ - name: `release-date`, - version: `1.1.1`, - }); - }), - ); - test( `it should install new version when excluded by package name glob pattern`, makeTemporaryEnv({ diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index 3d2a970bf4b3..ae0aa3d96b14 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -488,8 +488,8 @@ }, "npmPreapprovedPackages": { "_package": "@yarnpkg/core", - "title": "Array of exact package descriptors, exact package locators, or package name glob patterns to exclude from the any package gates.", - "description": "If a package descriptor, locator, or name matches specified pattern, it will not be considered when evaluating any of the package gates.", + "title": "Array of package descriptors or package name glob patterns to exclude from all of the package gates.", + "description": "If a package descriptor or name matches the specified pattern, it will not be considered when evaluating any of the package gates.", "type": "array", "items": { "type": "string" diff --git a/packages/plugin-npm/sources/index.ts b/packages/plugin-npm/sources/index.ts index 5adfb00d2784..3c1cf5b15827 100644 --- a/packages/plugin-npm/sources/index.ts +++ b/packages/plugin-npm/sources/index.ts @@ -74,7 +74,7 @@ const packageGateSettings = { default: 0, }, npmPreapprovedPackages: { - description: `Array of package name glob patterns to exclude from the minimum release age check`, + description: `Array of package descriptors or package name glob patterns to exclude from the minimum release age check`, type: SettingsType.STRING as const, isArray: true as const, default: [], diff --git a/packages/plugin-npm/sources/npmConfigUtils.ts b/packages/plugin-npm/sources/npmConfigUtils.ts index 660aedd20757..591d6bd01a59 100644 --- a/packages/plugin-npm/sources/npmConfigUtils.ts +++ b/packages/plugin-npm/sources/npmConfigUtils.ts @@ -108,14 +108,12 @@ export type CheckPackageGatesOptions = { export function checkPackageGates({configuration, descriptor, version, publishTimes}: CheckPackageGatesOptions) { const range = descriptor.range.slice(PROTOCOL.length); const minimalAgeGate = configuration.get(`npmMinimalAgeGate`); + const stringifiedIdent = structUtils.stringifyIdent(descriptor); + const stringifiedDescriptor = structUtils.stringifyDescriptor({...descriptor, range}); if (minimalAgeGate) { const preapprovedPackages = configuration.get(`npmPreapprovedPackages`); const shouldExclude = preapprovedPackages.some(exclude => - structUtils.stringifyIdent(descriptor) === exclude - || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, version)) === exclude - || structUtils.stringifyLocator(structUtils.makeLocator(descriptor, `${PROTOCOL}:${version}`)) === exclude - || micromatch.isMatch(structUtils.stringifyDescriptor({...descriptor, range}), exclude) - || micromatch.isMatch(structUtils.stringifyDescriptor(descriptor), exclude), + [stringifiedIdent, stringifiedDescriptor].includes(exclude) || micromatch.isMatch(stringifiedIdent, exclude), ); if (!shouldExclude) { const versionTime = new Date(publishTimes[version]); From ef35b65b9b87db6d8f298c680f57f53c3a7c052b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Wed, 17 Sep 2025 17:53:40 +0200 Subject: [PATCH 16/19] Couple of tweaks --- .../features/npmMinimalAgeGate.test.ts | 17 ++--- .../static/configuration/yarnrc.json | 2 +- .../plugin-npm/sources/NpmSemverResolver.ts | 4 +- packages/plugin-npm/sources/NpmTagResolver.ts | 18 +++-- packages/plugin-npm/sources/npmConfigUtils.ts | 72 ++++++++++++------- 5 files changed, 70 insertions(+), 43 deletions(-) diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts index de3ea7b6bf85..728183476cf6 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts +++ b/packages/acceptance-tests/pkg-tests-specs/sources/features/npmMinimalAgeGate.test.ts @@ -44,21 +44,18 @@ describe(`Features`, () => { makeTemporaryEnv({}, { npmMinimalAgeGate: ONE_DAY_IN_MINUTES, npmPreapprovedPackages: [`release-date@^1.0.0`], - // we are checking a transitive dependencies version, which the pnp will throw an error for - // disabling these checks for the purpose of this test - pnpFallbackMode: `all`, - pnpMode: `loose`, }, async ({run, source}) => { await run(`add`, `release-date@^1.0.0`); - await expect(source(`require('release-date/package.json')`)).resolves.toMatchObject({ + await expect(source(`require('release-date')`)).resolves.toMatchObject({ name: `release-date`, version: `1.1.1`, - }); - - await expect(source(`require('release-date-transitive/package.json')`)).resolves.toMatchObject({ - name: `release-date-transitive`, - version: `1.1.0`, + dependencies: { + [`release-date-transitive`]: { + name: `release-date-transitive`, + version: `1.1.0`, + }, + }, }); }), ); diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index ae0aa3d96b14..d78fa81f6cfb 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -495,7 +495,7 @@ "type": "string" }, "default": [], - "examples": ["@scoped-package/*", "package", "package*", "package@^1.0.0", "package@1.0.0", "package@npm:1.0.0", "package@npm:^1.0.0"] + "examples": ["@scoped-package/*", "package", "package*", "package@^1.0.0", "package@1.0.0"] }, "pnpmStoreFolder": { "_package": "@yarnpkg/plugin-pnpm", diff --git a/packages/plugin-npm/sources/NpmSemverResolver.ts b/packages/plugin-npm/sources/NpmSemverResolver.ts index 268759cd37d1..7d03d762519e 100644 --- a/packages/plugin-npm/sources/NpmSemverResolver.ts +++ b/packages/plugin-npm/sources/NpmSemverResolver.ts @@ -6,7 +6,7 @@ import semver import {NpmSemverFetcher} from './NpmSemverFetcher'; import {PROTOCOL} from './constants'; -import {checkPackageGates} from './npmConfigUtils'; +import {isPackageApproved} from './npmConfigUtils'; import * as npmHttpUtils from './npmHttpUtils'; const NODE_GYP_IDENT = structUtils.makeIdent(null, `node-gyp`); @@ -58,7 +58,7 @@ export class NpmSemverResolver implements Resolver { try { const candidate = new semverUtils.SemVer(version); if (range.test(candidate)) { - if (!checkPackageGates({configuration: opts.project.configuration, descriptor, version, publishTimes: registryData.time})) + if (!isPackageApproved({configuration: opts.project.configuration, ident: descriptor, version, publishTimes: registryData.time})) return miscUtils.mapAndFilter.skip; return candidate; diff --git a/packages/plugin-npm/sources/NpmTagResolver.ts b/packages/plugin-npm/sources/NpmTagResolver.ts index e20d65ab21cb..45eb84e70ca0 100644 --- a/packages/plugin-npm/sources/NpmTagResolver.ts +++ b/packages/plugin-npm/sources/NpmTagResolver.ts @@ -5,7 +5,7 @@ import semver import {NpmSemverFetcher} from './NpmSemverFetcher'; import {PROTOCOL} from './constants'; -import {checkPackageGates} from './npmConfigUtils'; +import {isPackageApproved} from './npmConfigUtils'; import * as npmHttpUtils from './npmHttpUtils'; export class NpmTagResolver implements Resolver { @@ -55,11 +55,19 @@ export class NpmTagResolver implements Resolver { const versions = Object.keys(registryData.versions); const times = registryData.time; + let version = distTags[tag]; - if (tag === `latest` && !checkPackageGates({configuration: opts.project.configuration, descriptor, version, publishTimes: times})) - version = semver.rsort(versions) - .filter(nextVersion => semver.lt(nextVersion, version)) - .find(nextVersion => checkPackageGates({configuration: opts.project.configuration, descriptor, version: nextVersion, publishTimes: times})) ?? distTags[tag]; + + if (tag === `latest` && !isPackageApproved({configuration: opts.project.configuration, ident: descriptor, version, publishTimes: times})) { + const nextVersion = semver.rsort(versions).find(candidateVersion => { + return semver.lt(candidateVersion, version) && isPackageApproved({configuration: opts.project.configuration, ident: descriptor, version: candidateVersion, publishTimes: times}); + }); + + if (!nextVersion) + throw new ReportError(MessageName.REMOTE_NOT_FOUND, `The version for tag "${tag}" is quarantined, and no lower version is available`); + + version = nextVersion; + } const versionLocator = structUtils.makeLocator(descriptor, `${PROTOCOL}${version}`); diff --git a/packages/plugin-npm/sources/npmConfigUtils.ts b/packages/plugin-npm/sources/npmConfigUtils.ts index 591d6bd01a59..383a02bb9122 100644 --- a/packages/plugin-npm/sources/npmConfigUtils.ts +++ b/packages/plugin-npm/sources/npmConfigUtils.ts @@ -1,7 +1,5 @@ -import {Configuration, Manifest, Ident, structUtils, Descriptor} from '@yarnpkg/core'; -import micromatch from 'micromatch'; - -import {PROTOCOL} from './constants'; +import {Configuration, Manifest, Ident, structUtils, semverUtils} from '@yarnpkg/core'; +import micromatch from 'micromatch'; export enum RegistryType { AUDIT_REGISTRY = `npmAuditRegistry`, @@ -98,30 +96,54 @@ export function getAuthConfiguration(registry: string, {configuration, ident}: { return registryConfiguration || configuration; } -export type CheckPackageGatesOptions = { - configuration: Configuration; - descriptor: Descriptor; - version: string; - publishTimes: Record; -}; - -export function checkPackageGates({configuration, descriptor, version, publishTimes}: CheckPackageGatesOptions) { - const range = descriptor.range.slice(PROTOCOL.length); +function shouldBeQuarantined({configuration, version, publishTimes}: IsPackageApprovedOptions) { const minimalAgeGate = configuration.get(`npmMinimalAgeGate`); - const stringifiedIdent = structUtils.stringifyIdent(descriptor); - const stringifiedDescriptor = structUtils.stringifyDescriptor({...descriptor, range}); + if (minimalAgeGate) { - const preapprovedPackages = configuration.get(`npmPreapprovedPackages`); - const shouldExclude = preapprovedPackages.some(exclude => - [stringifiedIdent, stringifiedDescriptor].includes(exclude) || micromatch.isMatch(stringifiedIdent, exclude), - ); - if (!shouldExclude) { - const versionTime = new Date(publishTimes[version]); - const ageMinutes = (new Date().getTime() - versionTime.getTime()) / 60 / 1000; - if (ageMinutes < minimalAgeGate) { - return false; - } + const versionTime = new Date(publishTimes[version]); + const ageMinutes = (new Date().getTime() - versionTime.getTime()) / 60 / 1000; + if (ageMinutes < minimalAgeGate) { + return true; } } + + return false; +} + +function checkIdent(ident: Ident, version: string, entry: string) { + const validator = structUtils.tryParseDescriptor(entry); + if (!validator) + return false; + + if (validator.identHash !== ident.identHash && !micromatch.isMatch(structUtils.stringifyIdent(ident), structUtils.stringifyIdent(validator))) + return false; + + if (validator.range === `unknown`) + return true; + + const validatorRange = semverUtils.validRange(validator.range); + if (!validatorRange) + return false; + + if (!validatorRange.test(version)) + return false; + return true; } + +export type IsPackageApprovedOptions = { + configuration: Configuration; + ident: Ident; + version: string; + publishTimes: Record; +}; + +function isPreapproved({configuration, ident, version}: IsPackageApprovedOptions) { + return configuration.get(`npmPreapprovedPackages`).some(entry => { + return checkIdent(ident, version, entry); + }); +} + +export function isPackageApproved(params: IsPackageApprovedOptions) { + return !shouldBeQuarantined(params) || isPreapproved(params); +} From e4cd5dd4e0fabad59ec632d9e334a7d5f2d56483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Wed, 17 Sep 2025 17:55:50 +0200 Subject: [PATCH 17/19] Versions --- .yarn/versions/25eb93a2.yml | 54 +++++++++++++++---------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/.yarn/versions/25eb93a2.yml b/.yarn/versions/25eb93a2.yml index 02c5606c9e5c..b8af1f116800 100644 --- a/.yarn/versions/25eb93a2.yml +++ b/.yarn/versions/25eb93a2.yml @@ -1,36 +1,24 @@ releases: - "@yarnpkg/core": minor - "@yarnpkg/plugin-essentials": minor - "@yarnpkg/plugin-npm": minor - "@yarnpkg/plugin-compat": minor - "@yarnpkg/plugin-constraints": minor - "@yarnpkg/plugin-dlx": minor - "@yarnpkg/plugin-exec": minor - "@yarnpkg/plugin-file": minor - "@yarnpkg/plugin-git": minor - "@yarnpkg/plugin-github": minor - "@yarnpkg/plugin-http": minor - "@yarnpkg/plugin-init": minor - "@yarnpkg/plugin-interactive-tools": minor - "@yarnpkg/plugin-jsr": minor - "@yarnpkg/plugin-link": minor - "@yarnpkg/plugin-nm": minor - "@yarnpkg/plugin-npm-cli": minor - "@yarnpkg/plugin-pack": minor - "@yarnpkg/plugin-patch": minor - "@yarnpkg/plugin-pnp": minor - "@yarnpkg/plugin-pnpm": minor - "@yarnpkg/plugin-stage": minor - "@yarnpkg/plugin-typescript": minor - "@yarnpkg/plugin-version": minor - "@yarnpkg/plugin-workspace-tools": minor - "@yarnpkg/builder": minor "@yarnpkg/cli": minor - "@yarnpkg/doctor": minor - "@yarnpkg/extensions": minor - "@yarnpkg/nm": minor - "@yarnpkg/pnpify": minor - "@yarnpkg/sdks": minor - "@yarnpkg/plugin-catalog": minor - + "@yarnpkg/plugin-npm": minor +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-nm" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/core" + - "@yarnpkg/doctor" From aa5ac1c8aeab8a24cacdf52277cf2d5b65806b0f Mon Sep 17 00:00:00 2001 From: Dennis Kugelmann Date: Wed, 15 Oct 2025 06:35:20 +0000 Subject: [PATCH 18/19] docs: Clarify additional use-case of npmMinimalAgeGate Updated the description for npmMinimalAgeGate to clarify its purpose and provide additional context regarding unpublishable packages. --- packages/docusaurus/static/configuration/yarnrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index d875cf4cc3e2..a7565d54f00b 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -482,7 +482,7 @@ "npmMinimalAgeGate": { "_package": "@yarnpkg/core", "title": "Minimum age of a package version according to the publish date on the npm registry in minutes to be considered for installation.", - "description": "If a package version is newer than the minimal age gate, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages.", + "description": "If a package version is newer than the minimal age gate, it will not be considered for installation. This can be used to reduce the likelihood of installing compromised packages or not to install packages that are still unpublishable. (e.g. NPM allows to uninstall packages in the first 72h; use `4320` for 3 days)", "type": "number", "default": 0 }, From 821db3fc3bb15cb9b1c5e493b61e39fb35b2e547 Mon Sep 17 00:00:00 2001 From: Dennis Kugelmann Date: Wed, 15 Oct 2025 07:01:31 +0000 Subject: [PATCH 19/19] Delete incorrect versions file --- .yarn/versions/25eb93a2.yml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 .yarn/versions/25eb93a2.yml diff --git a/.yarn/versions/25eb93a2.yml b/.yarn/versions/25eb93a2.yml deleted file mode 100644 index b8af1f116800..000000000000 --- a/.yarn/versions/25eb93a2.yml +++ /dev/null @@ -1,24 +0,0 @@ -releases: - "@yarnpkg/cli": minor - "@yarnpkg/plugin-npm": minor - -declined: - - "@yarnpkg/plugin-compat" - - "@yarnpkg/plugin-constraints" - - "@yarnpkg/plugin-dlx" - - "@yarnpkg/plugin-essentials" - - "@yarnpkg/plugin-init" - - "@yarnpkg/plugin-interactive-tools" - - "@yarnpkg/plugin-nm" - - "@yarnpkg/plugin-npm-cli" - - "@yarnpkg/plugin-pack" - - "@yarnpkg/plugin-patch" - - "@yarnpkg/plugin-pnp" - - "@yarnpkg/plugin-pnpm" - - "@yarnpkg/plugin-stage" - - "@yarnpkg/plugin-typescript" - - "@yarnpkg/plugin-version" - - "@yarnpkg/plugin-workspace-tools" - - "@yarnpkg/builder" - - "@yarnpkg/core" - - "@yarnpkg/doctor"