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
Couple of tweaks
  • Loading branch information
arcanis committed Sep 17, 2025
commit ef35b65b9b87db6d8f298c680f57f53c3a7c052b
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
},
},
});
}),
);
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus/static/configuration/yarnrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@
"type": "string"
},
"default": [],
"examples": ["@scoped-package/*", "package", "package*", "package@^1.0.0", "[email protected]", "package@npm:1.0.0", "package@npm:^1.0.0"]
"examples": ["@scoped-package/*", "package", "package*", "package@^1.0.0", "[email protected]"]
},
"pnpmStoreFolder": {
"_package": "@yarnpkg/plugin-pnpm",
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 @@ -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`);
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 13 additions & 5 deletions packages/plugin-npm/sources/NpmTagResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}`);

Expand Down
72 changes: 47 additions & 25 deletions packages/plugin-npm/sources/npmConfigUtils.ts
Original file line number Diff line number Diff line change
@@ -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`,
Expand Down Expand Up @@ -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<string, string>;
};

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<string, string>;
};

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);
}