UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

778 lines (777 loc) • 29.8 kB
"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`; }