nx
Version:
778 lines (777 loc) • 29.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.readPnpmPolicy = readPnpmPolicy;
exports.pickPnpmVersion = pickPnpmVersion;
const fs_1 = require("fs");
const os_1 = require("os");
const path_1 = require("path");
const semver_1 = require("semver");
const constants_1 = require("../constants");
const errors_1 = require("../errors");
const npmrc_1 = require("../npmrc");
const pick_1 = require("../pick");
// Ordered newest-bound-first so the first matching row applies.
const PNPM_BEHAVIOR_ROWS = [
{
minVersion: '11.1.3',
major: 11,
strictMode: 'default-loose',
strictAutoOnWhenExplicit: true,
excludeGrammar: 'v2-globs-unions',
writesExcludes: true,
uppercaseEnv: true,
},
{
minVersion: '11.1.0',
major: 11,
strictMode: 'default-loose',
strictAutoOnWhenExplicit: true,
excludeGrammar: 'v2-globs-unions',
writesExcludes: false,
uppercaseEnv: true,
},
{
minVersion: '11.0.4',
major: 11,
strictMode: 'default-loose',
strictAutoOnWhenExplicit: true,
excludeGrammar: 'v2-globs-unions',
writesExcludes: false,
uppercaseEnv: false,
},
{
minVersion: '11.0.0',
major: 11,
strictMode: 'default-loose',
strictAutoOnWhenExplicit: false,
excludeGrammar: 'v2-globs-unions',
writesExcludes: false,
uppercaseEnv: false,
},
{
minVersion: '10.19.0',
major: 10,
strictMode: 'always',
strictAutoOnWhenExplicit: false,
excludeGrammar: 'v2-globs-unions',
writesExcludes: false,
uppercaseEnv: false,
},
{
minVersion: '10.16.0',
major: 10,
strictMode: 'always',
strictAutoOnWhenExplicit: false,
excludeGrammar: 'v1-exact-names',
writesExcludes: false,
uppercaseEnv: false,
},
];
function findBehaviorRow(pmVersion) {
return PNPM_BEHAVIOR_ROWS.find((row) => (0, semver_1.gte)(pmVersion, row.minVersion));
}
// Builds a resolved cooldown window, tagging it with the surface label used in
// messaging. windowExplicit is false only for the invisible built-in default.
function okWindow(windowMinutes, excludes, source, windowExplicit = true) {
return {
kind: 'ok',
windowMinutes,
excludes,
sourceDescription: `pnpm minimumReleaseAge (${windowMinutes} min, ${source})`,
windowExplicit,
};
}
/**
* Reads pnpm's effective cooldown configuration once. The surface set and
* precedence depend on the pnpm major: v10 layers npm-conf surfaces
* (workspace yaml > npm_config env > project .npmrc > user .npmrc); v11
* layers pnpm-native surfaces (pnpm_config / PNPM_CONFIG env >
* pnpm-workspace.yaml > global config.yaml > built-in 1440-minute default).
*/
async function readPnpmPolicy(root, pmVersion) {
// A pnpm major newer than the table knows can behave differently; defer to a
// real install rather than guessing.
if ((0, semver_1.gte)(pmVersion, '12.0.0')) {
return {
outcome: 'ambiguous',
reason: `pnpm ${pmVersion} is newer than the known cooldown behavior table.`,
};
}
const row = findBehaviorRow(pmVersion);
if (!row) {
return { outcome: 'inactive' };
}
let resolution;
let strictExplicit;
let ignoreMissingTimeExplicit;
if (row.major === 11) {
const read = readV11Surfaces(root, row);
resolution = read.window;
strictExplicit = read.strictExplicit;
ignoreMissingTimeExplicit = read.ignoreMissingTime;
}
else {
resolution = readV10Surfaces(root);
// v10 strict is hardcoded; no surface toggles it.
strictExplicit = undefined;
}
if (!resolution) {
return { outcome: 'inactive' };
}
if (resolution.kind !== 'ok') {
return { outcome: 'ambiguous', reason: resolution.reason };
}
const { windowMinutes, excludes, sourceDescription, windowExplicit } = resolution;
if (!Number.isFinite(windowMinutes)) {
return {
outcome: 'ambiguous',
reason: `Invalid pnpm minimumReleaseAge value: ${String(windowMinutes)}.`,
};
}
// 0 disables; negatives push the cutoff into the future -> everything passes.
if (windowMinutes <= 0) {
return { outcome: 'inactive' };
}
const windowMs = windowMinutes * constants_1.MS_PER_MINUTE;
const cutoffMs = Date.now() - windowMs;
const strict = resolveStrict(row, strictExplicit, windowExplicit);
// v10 always errors on missing time; v11 defaults to skip unless a surface
// explicitly disabled it.
const ignoreMissingTime = row.major === 11 ? (ignoreMissingTimeExplicit ?? true) : false;
const behavior = {
packageManager: 'pnpm',
strict,
// Loose fallback only exists on v11 when strict is off.
looseFallback: row.major === 11 && !strict,
writesExcludes: row.writesExcludes,
missingTimeMap: row.major === 10 ? 'error' : ignoreMissingTime ? 'skip' : 'error',
};
const isExcluded = buildExcludeMatcher(excludes, row.excludeGrammar);
if (isExcluded === null) {
// An invalid exclude entry is a version-dependent landmine in pnpm; do not
// mimic its crash, just defer.
return {
outcome: 'ambiguous',
reason: 'Invalid pnpm minimumReleaseAgeExclude entry.',
};
}
return {
outcome: 'active',
policy: {
packageManagerVersion: pmVersion,
cutoffMs,
windowMs,
sourceDescription,
isExcluded,
behavior,
},
};
}
function resolveStrict(row, strictExplicit, windowExplicit) {
if (row.strictMode === 'always') {
return true;
}
if (strictExplicit !== undefined) {
return strictExplicit;
}
// >=11.0.4: an explicitly set window with no explicit strict flag turns
// strict on. The invisible built-in 1440 default is NOT in explicitlySetKeys,
// so it stays loose - the normal pnpm 11 state.
return row.strictAutoOnWhenExplicit && windowExplicit;
}
// --- v11 surfaces -----------------------------------------------------------
function readV11Surfaces(root, row) {
const env = process.env;
const keySet = envKeySetForMajor(row);
const wsRead = readYamlWindow((0, path_1.join)(root, 'pnpm-workspace.yaml'));
if (wsRead && wsRead.kind === 'invalid') {
return {
window: wsRead,
strictExplicit: undefined,
ignoreMissingTime: undefined,
};
}
const globalRead = readYamlWindow((0, path_1.join)(getConfigDir(env), 'config.yaml'));
if (globalRead && globalRead.kind === 'invalid') {
return {
window: globalRead,
strictExplicit: undefined,
ignoreMissingTime: undefined,
};
}
const wsYaml = wsRead && wsRead.kind === 'ok' ? wsRead : null;
const globalYaml = globalRead && globalRead.kind === 'ok' ? globalRead : null;
// pnpm v11 resolves each cooldown key independently across surfaces:
// env > pnpm-workspace.yaml > global config.yaml > built-in default.
const strictExplicit = readEnvBoolean(env, keySet, 'minimum_release_age_strict') ??
wsYaml?.strict ??
globalYaml?.strict;
const ignoreMissingTime = readEnvBoolean(env, keySet, 'minimum_release_age_ignore_missing_time') ??
wsYaml?.ignoreMissingTime ??
globalYaml?.ignoreMissingTime;
const envExcludes = readEnvArray(env, keySet, 'minimum_release_age_exclude');
if (envExcludes === 'invalid') {
return {
window: {
kind: 'invalid',
reason: 'Invalid pnpm minimumReleaseAgeExclude entry.',
},
strictExplicit: undefined,
ignoreMissingTime: undefined,
};
}
const excludes = envExcludes ?? wsYaml?.excludes ?? globalYaml?.excludes ?? [];
const envWindow = readEnvNumber(env, keySet, 'minimum_release_age');
// pnpm drops a NaN env value rather than erroring; treat 'invalid' as unset
// and fall through to lower surfaces.
if (envWindow !== undefined && envWindow !== 'invalid') {
return {
window: okWindow(envWindow, excludes, 'env'),
strictExplicit,
ignoreMissingTime,
};
}
if (wsYaml && wsYaml.windowMinutes !== undefined) {
return {
window: okWindow(wsYaml.windowMinutes, excludes, 'pnpm-workspace.yaml'),
strictExplicit,
ignoreMissingTime,
};
}
if (globalYaml && globalYaml.windowMinutes !== undefined) {
return {
window: okWindow(globalYaml.windowMinutes, excludes, 'global config.yaml'),
strictExplicit,
ignoreMissingTime,
};
}
// Built-in 1440-minute (1 day) default, injected programmatically and
// invisible to `pnpm config get`. Loose unless some surface set strict.
return {
window: okWindow(1440, excludes, 'default', false),
strictExplicit,
ignoreMissingTime,
};
}
// --- v10 surfaces -----------------------------------------------------------
function readV10Surfaces(root) {
// v10 layers config via @pnpm/npm-conf, per key:
// workspace yaml > npm_config_* env > project .npmrc > user .npmrc.
const wsRead = readYamlWindow((0, path_1.join)(root, 'pnpm-workspace.yaml'));
if (wsRead && wsRead.kind === 'invalid') {
return wsRead;
}
const wsYaml = wsRead && wsRead.kind === 'ok' ? wsRead : null;
const env = process.env;
const keySet = { prefix: 'npm_config_', uppercase: false };
const rawEnvWindow = readEnvNumber(env, keySet, 'minimum_release_age');
// npm-conf drops values that fail the Number type; treat as unset.
const envWindow = rawEnvWindow === 'invalid' ? undefined : rawEnvWindow;
const projectNpmrc = readNpmrcSurface((0, path_1.join)(root, '.npmrc'));
const userNpmrc = readNpmrcSurface((0, path_1.join)((0, os_1.homedir)(), '.npmrc'));
// v10 reads npm_config_*, which never JSON-parses, so readEnvArray can only
// yield a single-entry array or undefined here - never 'invalid'.
const envExcludes = readEnvArray(env, keySet, 'minimum_release_age_exclude');
const excludes = wsYaml?.excludes ??
(envExcludes === 'invalid' ? undefined : envExcludes) ??
projectNpmrc?.excludes ??
userNpmrc?.excludes ??
[];
if (wsYaml && wsYaml.windowMinutes !== undefined) {
return okWindow(wsYaml.windowMinutes, excludes, 'pnpm-workspace.yaml');
}
if (envWindow !== undefined) {
return okWindow(envWindow, excludes, 'env');
}
for (const npmrc of [projectNpmrc, userNpmrc]) {
if (npmrc && npmrc.windowMinutes !== undefined) {
return okWindow(npmrc.windowMinutes, excludes, '.npmrc');
}
}
return null;
}
function readYamlRaw(path) {
if (!(0, fs_1.existsSync)(path)) {
return null;
}
try {
const { load } = require('@zkochan/js-yaml');
return load((0, fs_1.readFileSync)(path, 'utf-8')) ?? {};
}
catch {
// The file exists but is unparseable. An absent file falls through to lower
// surfaces; a corrupt one can't be reasoned about, so signal it and let the
// caller defer to a real install (matching npm/yarn/bun on a read failure).
return 'invalid';
}
}
function readYamlWindow(path) {
const doc = readYamlRaw(path);
if (doc === 'invalid') {
return { kind: 'invalid', reason: `Unable to parse ${(0, path_1.basename)(path)}.` };
}
if (!doc) {
return null;
}
// Each key is read independently; pnpm merges surfaces per key.
const excludes = readArrayKey(doc, 'minimumReleaseAgeExclude');
const strict = readBooleanKey(doc, 'minimumReleaseAgeStrict');
const ignoreMissingTime = readBooleanKey(doc, 'minimumReleaseAgeIgnoreMissingTime');
const raw = doc['minimumReleaseAge'];
if (raw === undefined || raw === null) {
return { kind: 'ok', excludes, strict, ignoreMissingTime };
}
const num = toNumber(raw);
if (num === null) {
return {
kind: 'invalid',
reason: `Invalid pnpm minimumReleaseAge value: ${String(raw)}.`,
};
}
return {
kind: 'ok',
windowMinutes: num,
excludes,
strict,
ignoreMissingTime,
};
}
function readNpmrcSurface(path) {
const entries = (0, npmrc_1.readNpmrcEntries)(path);
if (entries === null) {
return null;
}
// v10 reads the kebab-case keys via npm-conf; camelCase in .npmrc is never
// honored. Only the two cooldown keys are relevant here.
let windowMinutes;
let excludes;
for (const { key, value: rawValue } of entries) {
const value = stripQuotes(rawValue);
if (key === 'minimum-release-age') {
const num = toNumber(value);
if (num !== null) {
windowMinutes = num;
}
}
else if (key === 'minimum-release-age-exclude') {
// npm-conf accumulates repeated ini keys into an array.
(excludes ??= []).push(value);
}
}
return { windowMinutes, excludes };
}
// pnpm mirrors getConfigDir: XDG_CONFIG_HOME, else per-platform default.
function getConfigDir(env) {
if (env.XDG_CONFIG_HOME) {
return (0, path_1.join)(env.XDG_CONFIG_HOME, 'pnpm');
}
if (process.platform === 'darwin') {
return (0, path_1.join)((0, os_1.homedir)(), 'Library/Preferences/pnpm');
}
if (process.platform !== 'win32') {
return (0, path_1.join)((0, os_1.homedir)(), '.config/pnpm');
}
if (env.LOCALAPPDATA) {
return (0, path_1.join)(env.LOCALAPPDATA, 'pnpm/config');
}
return (0, path_1.join)((0, os_1.homedir)(), '.config/pnpm');
}
function envKeySetForMajor(row) {
return row.major === 11
? { prefix: 'pnpm_config_', uppercase: row.uppercaseEnv }
: { prefix: 'npm_config_', uppercase: false };
}
function envKeys(keySet, suffix) {
const keys = [`${keySet.prefix}${suffix}`];
if (keySet.uppercase) {
keys.push(`${keySet.prefix.toUpperCase()}${suffix.toUpperCase()}`);
}
return keys;
}
function readEnvRaw(env, keySet, suffix) {
for (const key of envKeys(keySet, suffix)) {
if (env[key] !== undefined) {
return env[key];
}
}
return undefined;
}
function readEnvNumber(env, keySet, suffix) {
const raw = readEnvRaw(env, keySet, suffix);
if (raw === undefined) {
return undefined;
}
const num = toNumber(raw);
// pnpm parses env values via Number(); NaN drops the key.
return num === null ? 'invalid' : num;
}
function readEnvBoolean(env, keySet, suffix) {
const raw = readEnvRaw(env, keySet, suffix);
// pnpm's Boolean env schema (config/reader/src/env.ts) yields only true/false
// for the literals; anything else drops the key, so the value falls through to
// lower surfaces / defaults instead of coercing to false.
if (raw === 'true') {
return true;
}
if (raw === 'false') {
return false;
}
return undefined;
}
function readEnvArray(env, keySet, suffix) {
const raw = readEnvRaw(env, keySet, suffix);
if (raw === undefined) {
return undefined;
}
// pnpm v11's [String, Array] env schema tries a JSON array first, then falls
// back to the raw value as a single entry. v10 (nopt) never JSON-parses.
if (keySet.prefix === 'pnpm_config_') {
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
// pnpm passes the array verbatim, then errors at install
// (ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE) on any non-string
// element; signal invalid so the caller defers to a real install.
return parsed.every((e) => typeof e === 'string')
? parsed
: 'invalid';
}
}
catch { }
}
return [raw];
}
// --- value helpers ----------------------------------------------------------
function toNumber(value) {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : null;
}
if (typeof value === 'string') {
const trimmed = value.trim();
if (trimmed === '') {
return null;
}
const num = Number(trimmed);
return Number.isFinite(num) ? num : null;
}
return null;
}
function stripQuotes(value) {
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
return value.slice(1, -1);
}
return value;
}
function readArrayKey(doc, key) {
const value = doc[key];
if (Array.isArray(value)) {
return value.map((v) => String(v));
}
if (typeof value === 'string') {
return [value];
}
return undefined;
}
function readBooleanKey(doc, key) {
const value = doc[key];
if (typeof value === 'boolean') {
return value;
}
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
return undefined;
}
/**
* Builds the exclude predicate matching pnpm's version-policy semantics.
* Returns null when any entry is invalid (pnpm would error on it), so the
* caller can defer to a real install rather than apply a partial policy.
*/
function buildExcludeMatcher(patterns, grammar) {
if (patterns.length === 0) {
return () => false;
}
if (grammar === 'v1-exact-names') {
// 10.16-10.18: exact-name membership only; version specs are not matched
// and invalid entries are silently ignored.
const names = new Set(patterns);
return (name) => names.has(name);
}
const rules = [];
for (const pattern of patterns) {
const parsed = parseVersionPolicyRule(pattern);
if (parsed === null) {
return null;
}
rules.push({
matchesName: createNameMatcher(parsed.packageName),
exactVersions: parsed.exactVersions,
});
}
return (name, version) => {
for (const rule of rules) {
if (!rule.matchesName(name)) {
continue;
}
if (rule.exactVersions.length === 0) {
return true;
}
return rule.exactVersions.includes(version);
}
return false;
};
}
// Mirrors @pnpm/config.version-policy parseVersionPolicyRule. Returns null for
// invalid entries (invalid version union / name pattern with version union).
function parseVersionPolicyRule(pattern) {
const { name: packageName, versionPart } = (0, pick_1.splitPackageDescriptor)(pattern);
if (versionPart === null) {
return { packageName, exactVersions: [] };
}
const exactVersions = [];
for (const versionRaw of versionPart.split('||')) {
const version = (0, semver_1.valid)(versionRaw);
if (version === null) {
// ERR_PNPM_INVALID_VERSION_UNION.
return null;
}
exactVersions.push(version);
}
if (packageName.includes('*')) {
// ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION.
return null;
}
return { packageName, exactVersions };
}
// Mirrors @pnpm/config.matcher for a single pattern: '*' anywhere becomes
// '.*', '!' prefix negates, otherwise exact (case-sensitive) equality.
function createNameMatcher(pattern) {
if (pattern.startsWith('!')) {
const inner = createNameMatcher(pattern.slice(1));
return (name) => !inner(name);
}
if (pattern === '*') {
return () => true;
}
const escaped = escapeRegExp(pattern).replace(/\\\*/g, '.*');
if (escaped === pattern) {
return (name) => name === pattern;
}
const regexp = new RegExp(`^${escaped}$`);
return (name) => regexp.test(name);
}
function escapeRegExp(value) {
return value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
}
// --- pick -------------------------------------------------------------------
/**
* pnpm resolution under an active cooldown. Exact pins and ranges mirror pnpm's
* resolver; dist-tag degrade uses the shared cross-PM rule:
* - exact pin too new -> strict/v10 violation; v11 loose installs it (immature).
* - range -> newest mature; none -> v10/strict violation, v11 loose lowest
* (least-immature) version unfiltered (immature).
* - dist-tag too new -> degrade via the shared channel-aware rule (see
* `degradeTagToCompliant` for the ordering); none compliant -> violation
* (v11 loose installs the original target immature).
*/
function pickPnpmVersion(spec, metadata, policy) {
const behavior = policy.behavior;
if (behavior.packageManager !== 'pnpm') {
throw new Error('pickPnpmVersion received a non-pnpm policy.');
}
const wholeMapMissing = metadata.time === null;
if (wholeMapMissing && behavior.missingTimeMap === 'error') {
throw missingTime(metadata, policy);
}
const type = (0, pick_1.classifySpec)(spec);
if (type === 'exact') {
return pickExact(spec, metadata, policy, behavior);
}
if (type === 'tag') {
return pickTag(spec, metadata, policy, behavior);
}
return pickRange(spec, metadata, policy, behavior, (0, pick_1.newestInRange)(metadata, spec));
}
function pickExact(spec, metadata, policy, behavior) {
const unconstrained = spec;
if (mature(metadata, policy, spec, behavior)) {
return { version: spec, unconstrained };
}
if (behavior.looseFallback) {
// v11 loose installs the pin anyway and records it as immature.
return { version: spec, unconstrained, immature: true };
}
throw violation(metadata, policy, spec, 'exact', [spec]);
}
function pickTag(spec, metadata, policy, behavior) {
const tagTarget = metadata.distTags[spec];
if (!tagTarget) {
throw violation(metadata, policy, spec, 'tag', []);
}
const unconstrained = tagTarget;
if (mature(metadata, policy, tagTarget, behavior)) {
return { version: tagTarget, unconstrained };
}
const degraded = (0, pick_1.degradeTagToCompliant)(tagTarget, metadata, (v) => mature(metadata, policy, v, behavior));
if (degraded) {
return { version: degraded, unconstrained };
}
// The loose fallback requires a real version: a non-semver tag target must
// not be installed immature, or the consumer would write `pkg@<garbage>`
// into minimumReleaseAgeExclude and break every later install
// (ERR_PNPM_INVALID_VERSION_UNION).
if (behavior.looseFallback && (0, semver_1.valid)(tagTarget)) {
// No compliant candidate; loose keeps the original (immature) target.
return { version: tagTarget, unconstrained, immature: true };
}
throw violation(metadata, policy, spec, 'tag', [tagTarget]);
}
function pickRange(spec, metadata, policy, behavior, unconstrained) {
const matureInRange = matureVersions(metadata, policy, behavior);
const newest = (0, semver_1.maxSatisfying)(matureInRange, spec, {
includePrerelease: false,
});
if (newest) {
return { version: newest, unconstrained };
}
if (behavior.looseFallback) {
// pnpm v11 loose falls back to the LOWEST version in range, unfiltered.
const lowest = (0, semver_1.minSatisfying)(metadata.versions, spec, {
includePrerelease: false,
});
if (lowest) {
return {
version: lowest,
unconstrained,
immature: true,
};
}
}
const blocked = metadata.versions
.filter((v) => (0, semver_1.satisfies)(v, spec, { includePrerelease: false }))
.sort(semver_1.rcompare);
throw violation(metadata, policy, spec, 'range', blocked);
}
function matureVersions(metadata, policy, behavior) {
return metadata.versions.filter((v) => mature(metadata, policy, v, behavior));
}
// pnpm blocks a version whose individual time entry is missing (treated too
// new). The whole-map-missing skip case is handled before any pick.
function mature(metadata, policy, version, behavior) {
if (policy.isExcluded(metadata.name, version)) {
return true;
}
if (metadata.time === null) {
// Whole map missing reached here only with missingTimeMap 'skip' -> gate
// is effectively off for this package.
return true;
}
const time = metadata.time[version];
if (!time) {
return false;
}
return Date.parse(time) <= policy.cutoffMs;
}
// --- errors -----------------------------------------------------------------
function violation(metadata, policy, spec, type, blockedVersions) {
return new errors_1.MinReleaseAgeViolationError({
packageManager: 'pnpm',
packageName: metadata.name,
spec,
pmShapedDetail: violationDetail(metadata, policy, spec, type, blockedVersions),
blocked: (0, pick_1.blockedVersionsFrom)(metadata, blockedVersions),
remediation: [
`Add ${metadata.name} (optionally with a version) to minimumReleaseAgeExclude in pnpm-workspace.yaml, wait for a matching version to age past the window, or lower ${policy.sourceDescription}.`,
],
});
}
// Per pnpm version (source-verified against npm-resolver/index.ts and
// installing/commands/policyHandlers.ts):
// - v10: always NO_MATCHING.
// - 11.0.0..11.1.2: range/exact emit the single-version NO_MATURE form;
// tags can't derive an immatureVersion from a tag name, so they fall back to
// NO_MATCHING.
// - >=11.1.3: strict install fails via failOnImmature with the multi-entry
// list form.
function violationDetail(metadata, policy, spec, type, blockedVersions) {
const version = policy.packageManagerVersion;
if (!(0, semver_1.gte)(version, '11.0.0')) {
return noMatchingDetail(metadata, spec);
}
if ((0, semver_1.gte)(version, '11.1.3')) {
return failOnImmatureDetail(metadata, policy, spec, type, blockedVersions);
}
if (type === 'tag') {
return noMatchingDetail(metadata, spec);
}
return noMatureSingleDetail(metadata, spec, blockedVersions);
}
// ERR_PNPM_NO_MATCHING_VERSION headline (10.x always; v11 fallback).
function noMatchingDetail(metadata, spec) {
return `No matching version found for ${metadata.name}@${spec} while fetching it from the registry`;
}
// 11.0.0..11.1.2 single-version NO_MATURE form (NoMatchingVersionError).
function noMatureSingleDetail(metadata, spec, blockedVersions) {
const newest = blockedVersions[0];
const time = newest ? metadata.time?.[newest] : undefined;
const ago = time ? formatTimeAgo(Date.parse(time)) : null;
// pnpm emits the NO_MATURE message only with an immature pick that has a
// known publish time; otherwise it falls back to NO_MATCHING.
if (!newest || !ago) {
return noMatchingDetail(metadata, spec);
}
return `Version ${newest} (released ${ago}) of ${metadata.name} does not meet the minimumReleaseAge constraint`;
}
// >=11.1.3 failOnImmature list form. failOnImmature lists the picks the resolver
// actually selected, one per resolution; resolving a single nx package yields a
// single immature pick. Each entry's reason mirrors detectMinReleaseAgeViolation
// (published-at ISO + cutoff ISO). For a range the resolver loose-picks the
// lowest in-range version; exact/tag carry their single target.
function failOnImmatureDetail(metadata, policy, spec, type, blockedVersions) {
const resolved = type === 'range'
? blockedVersions[blockedVersions.length - 1]
: blockedVersions[0];
const publishedAt = resolved ? metadata.time?.[resolved] : undefined;
if (!resolved || !publishedAt) {
return noMatchingDetail(metadata, spec);
}
const cutoffIso = new Date(policy.cutoffMs).toISOString();
const publishedIso = new Date(publishedAt).toISOString();
return ('1 version does not meet the minimumReleaseAge constraint:\n' +
` ${metadata.name}@${resolved} was published at ${publishedIso}, within the minimumReleaseAge cutoff (${cutoffIso})`);
}
function missingTime(metadata, policy) {
return new errors_1.MinReleaseAgeViolationError({
packageManager: 'pnpm',
packageName: metadata.name,
spec: '',
pmShapedDetail: `The metadata of ${metadata.name} is missing the "time" field`,
blocked: [],
remediation: [
`Set minimumReleaseAgeIgnoreMissingTime to true, or use a registry that serves package publish times.`,
],
});
}
// Mirrors pnpm v11's formatTimeAgo buckets exactly (npm-resolver): 'just now'
// under a minute (and for future dates), days at >=48h, hours at >=90min.
function formatTimeAgo(ts) {
if (Number.isNaN(ts)) {
return null;
}
const diffMs = Date.now() - ts;
if (diffMs < constants_1.MS_PER_MINUTE) {
return 'just now';
}
const diffMin = Math.floor(diffMs / constants_1.MS_PER_MINUTE);
const diffHour = Math.floor(diffMs / (60 * constants_1.MS_PER_MINUTE));
const diffDay = Math.floor(diffMs / constants_1.MS_PER_DAY);
if (diffHour >= 48)
return `${diffDay} day${diffDay === 1 ? '' : 's'} ago`;
if (diffMin >= 90)
return `${diffHour} hour${diffHour === 1 ? '' : 's'} ago`;
return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`;
}