Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a05df86
feat: implement `minimumNpmReleaseAge` and `minimumNpmReleaseAgeExclu…
bienzaaron Sep 17, 2025
db24b78
fix: support `npm:` prefix for locators too
bienzaaron Sep 17, 2025
900a7d2
test: adding tests for `minimumNpmReleaseAge` and `minimumNpmReleaseA…
bienzaaron Sep 17, 2025
11f5edd
fix: rename options to better align with existing npm-related options
bienzaaron Sep 17, 2025
eccba6c
fix: change the way unknowns are resolved to fix `add`/`up`
bienzaaron Sep 17, 2025
257fa2d
fix: fix release packages based on ci output
bienzaaron Sep 17, 2025
84d4b5d
refactor: rename options
bienzaaron Sep 17, 2025
0c7b130
Revert "fix: change the way unknowns are resolved to fix `add`/`up`"
bienzaaron Sep 17, 2025
ebd41b5
refactor: move exclusion logic to a helper
bienzaaron Sep 17, 2025
79d3aa7
fix: crawl package versions from highest to lowest if `latest` tag do…
bienzaaron Sep 17, 2025
55b5185
refactor: rename gate check function
bienzaaron Sep 17, 2025
9db6b3b
docs: update doc language to match option name
bienzaaron Sep 17, 2025
93957a6
refactor: move npm-related configs into plugin-npm
bienzaaron Sep 17, 2025
98e58ad
fix: if latest is unsuitable, ensure we don't select a higher version
bienzaaron Sep 17, 2025
fb35bc8
simplify `npmPreapprovedPackages`
bienzaaron Sep 17, 2025
ef35b65
Couple of tweaks
arcanis Sep 17, 2025
b0d4031
Merge remote-tracking branch 'origin/master' into pr/bienzaaron/6901
arcanis Sep 17, 2025
0cdbdfb
Merge remote-tracking branch 'origin/master' into pr/bienzaaron/6901
arcanis Sep 17, 2025
e4cd5dd
Versions
arcanis Sep 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: rename options to better align with existing npm-related options
  • Loading branch information
bienzaaron committed Sep 17, 2025
commit 11f5edd381f9c8eecf6d71a63808c0b9eee616d3
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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`, `[email protected]`)).rejects.toThrowError(`No candidates found`);
}),
Expand All @@ -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`);

Expand All @@ -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: [`[email protected]`],
npmMinimumReleaseAge: ONE_DAY_IN_MINUTES,
npmMinimumReleaseAgeExclude: [`[email protected]`],
// 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`,
Expand 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`, `[email protected]`);

Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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/[email protected]`)).rejects.toThrowError(`No candidates found`);
}),
Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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`);
}),
Expand All @@ -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`);

Expand All @@ -225,8 +225,8 @@ describe(`Features`, () => {
makeTemporaryEnv({
dependencies: {[`release-date`]: `^1.0.0`},
}, {
minimumNpmReleaseAge: ONE_DAY_IN_MINUTES,
minimumNpmReleaseAgeExclude: [`[email protected]`],
npmMinimumReleaseAge: ONE_DAY_IN_MINUTES,
npmMinimumReleaseAgeExclude: [`[email protected]`],
}, async ({run, source}) => {
await run(`install`);

Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -327,7 +327,7 @@ describe(`Features`, () => {
makeTemporaryEnv({
dependencies: {[`release-date`]: `^1.0.0`},
}, {
minimumNpmReleaseAge: 0,
npmMinimumReleaseAge: 0,
}, async ({run, source}) => {
await run(`install`);

Expand All @@ -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`);
}),
Expand All @@ -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`);

Expand All @@ -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`);

Expand All @@ -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`);
Expand All @@ -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`);
Expand Down
35 changes: 18 additions & 17 deletions packages/docusaurus/static/configuration/yarnrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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", "[email protected]", "package@npm:1.0.0", "package@npm:^1.0.0"]
},
"pnpmStoreFolder": {
"_package": "@yarnpkg/plugin-pnpm",
"title": "Path where the pnpm store will be stored",
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-npm/sources/NpmSemverResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/yarnpkg-core/sources/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,12 +577,12 @@ export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} =
type: SettingsType.STRING,
default: `throw`,
},
minimumNpmReleaseAge: {
npmMinimumReleaseAge: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
npmMinimumReleaseAge: {
npmMinimalAgeGate: {

I could envision multiple types of gates.

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: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
npmMinimumReleaseAgeExclude: {
npmPreapprovedPackages: {

Same reason, if there were multiple gates we'd want a single setting to bypass them all.

description: `Array of package name glob patterns to exclude from the minimum release age check`,
type: SettingsType.STRING,
isArray: true,
Expand Down Expand Up @@ -710,8 +710,8 @@ export interface ConfigurationValueMap {
enableStrictSettings: boolean;
enableImmutableCache: boolean;
checksumBehavior: string;
minimumNpmReleaseAge: number;
minimumNpmReleaseAgeExclude: Array<string>;
npmMinimumReleaseAge: number;
npmMinimumReleaseAgeExclude: Array<string>;

// Miscellaneous settings
injectEnvironmentFiles: Array<PortablePath>;
Expand Down