dependency-radar 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compareReports = compareReports;
4
+ exports.formatCompareOutput = formatCompareOutput;
5
+ function byName(deps) {
6
+ const map = new Map();
7
+ for (const dep of Object.values(deps || {})) {
8
+ if (!map.has(dep.package.name))
9
+ map.set(dep.package.name, []);
10
+ map.get(dep.package.name).push(dep);
11
+ }
12
+ return map;
13
+ }
14
+ function compareReports(previous, current) {
15
+ const previousIds = new Set(Object.keys(previous.dependencies || {}));
16
+ const currentIds = new Set(Object.keys(current.dependencies || {}));
17
+ const added = Array.from(currentIds).filter((id) => !previousIds.has(id)).sort();
18
+ const removed = Array.from(previousIds).filter((id) => !currentIds.has(id)).sort();
19
+ const previousByName = byName(previous.dependencies || {});
20
+ const currentByName = byName(current.dependencies || {});
21
+ const changedVersions = [];
22
+ for (const [name, previousDeps] of previousByName.entries()) {
23
+ const currentDeps = currentByName.get(name);
24
+ if (!currentDeps || currentDeps.length !== 1 || previousDeps.length !== 1)
25
+ continue;
26
+ const prev = previousDeps[0];
27
+ const next = currentDeps[0];
28
+ if (prev.package.version !== next.package.version) {
29
+ changedVersions.push({ name, from: prev.package.version, to: next.package.version });
30
+ }
31
+ }
32
+ changedVersions.sort((a, b) => a.name.localeCompare(b.name));
33
+ const previousFindingIds = new Set((previous.findings || []).map((finding) => finding.id));
34
+ const currentFindingIds = new Set((current.findings || []).map((finding) => finding.id));
35
+ const byFindingId = (a, b) => (a.id || '').localeCompare(b.id || '');
36
+ const previousFindingById = new Map((previous.findings || []).map((finding) => [finding.id, finding]));
37
+ const serializeFinding = (finding) => JSON.stringify({
38
+ category: finding.category,
39
+ severity: finding.severity,
40
+ packageId: finding.packageId,
41
+ packageName: finding.packageName,
42
+ packageVersion: finding.packageVersion,
43
+ title: finding.title,
44
+ message: finding.message,
45
+ evidence: finding.evidence,
46
+ recommendation: finding.recommendation
47
+ });
48
+ return {
49
+ added,
50
+ removed,
51
+ changedVersions,
52
+ newFindings: (current.findings || []).filter((finding) => !previousFindingIds.has(finding.id)).sort(byFindingId),
53
+ resolvedFindings: (previous.findings || []).filter((finding) => !currentFindingIds.has(finding.id)).sort(byFindingId),
54
+ changedFindings: (current.findings || [])
55
+ .filter((finding) => {
56
+ const previousFinding = previousFindingById.get(finding.id);
57
+ return previousFinding ? serializeFinding(previousFinding) !== serializeFinding(finding) : false;
58
+ })
59
+ .sort(byFindingId)
60
+ };
61
+ }
62
+ function section(title, lines, empty) {
63
+ return [
64
+ title,
65
+ '-'.repeat(title.length),
66
+ ...(lines.length > 0 ? lines : [empty]),
67
+ ''
68
+ ];
69
+ }
70
+ function formatCompareOutput(result) {
71
+ const lines = ['Dependency Radar comparison', '===========================', ''];
72
+ lines.push(...section('Added dependencies', result.added.map((id) => `+ ${id}`), 'none'));
73
+ lines.push(...section('Removed dependencies', result.removed.map((id) => `- ${id}`), 'none'));
74
+ lines.push(...section('Changed versions', result.changedVersions.map((entry) => `${entry.name}: ${entry.from} -> ${entry.to}`), 'none'));
75
+ lines.push(...section('New findings', result.newFindings.map((finding) => `${finding.severity.toUpperCase()} ${finding.packageId}: ${finding.title}`), 'none'));
76
+ lines.push(...section('Changed findings', result.changedFindings.map((finding) => `${finding.severity.toUpperCase()} ${finding.packageId}: ${finding.title}`), 'none'));
77
+ lines.push(...section('Resolved findings', result.resolvedFindings.map((finding) => `${finding.packageId}: ${finding.title}`), 'none'));
78
+ return lines.join('\n').trimEnd();
79
+ }
package/dist/failOn.js CHANGED
@@ -10,7 +10,8 @@ exports.SUPPORTED_FAIL_ON_RULES = [
10
10
  'high-severity-vuln',
11
11
  'licence-mismatch',
12
12
  'copyleft-detected',
13
- 'unknown-licence'
13
+ 'unknown-licence',
14
+ 'supply-chain-source'
14
15
  ];
15
16
  const SUPPORTED_FAIL_ON_RULE_SET = new Set(exports.SUPPORTED_FAIL_ON_RULES);
16
17
  /**
@@ -97,7 +98,7 @@ function parseFailOnRules(value) {
97
98
  * @returns An array of PolicyViolation objects for each rule that has one or more matching issues; returns an empty array if no violations are found
98
99
  */
99
100
  function evaluatePolicyViolations(aggregated, rules) {
100
- var _a;
101
+ var _a, _b;
101
102
  if (rules.size === 0)
102
103
  return [];
103
104
  let reachableProductionVulnCount = 0;
@@ -106,6 +107,7 @@ function evaluatePolicyViolations(aggregated, rules) {
106
107
  let licenceMismatchCount = 0;
107
108
  let copyleftDetectedCount = 0;
108
109
  let unknownLicenceCount = 0;
110
+ let supplyChainSourceCount = 0;
109
111
  for (const dep of Object.values(aggregated.dependencies || {})) {
110
112
  const isRuntime = dep.usage.scope === 'runtime';
111
113
  const hasVuln = vulnerabilityCount(dep) > 0;
@@ -130,6 +132,11 @@ function evaluatePolicyViolations(aggregated, rules) {
130
132
  unknownLicenceCount += 1;
131
133
  }
132
134
  }
135
+ supplyChainSourceCount = (((_b = aggregated.supplyChain) === null || _b === void 0 ? void 0 : _b.signals) || []).filter((signal) => signal.type === 'git-dependency' ||
136
+ signal.type === 'file-dependency' ||
137
+ signal.type === 'non-registry-tarball' ||
138
+ signal.type === 'missing-integrity' ||
139
+ signal.type === 'unexpected-registry-host').length;
133
140
  const violations = [];
134
141
  if (rules.has('reachable-vuln') && reachableProductionVulnCount > 0) {
135
142
  violations.push({
@@ -173,5 +180,12 @@ function evaluatePolicyViolations(aggregated, rules) {
173
180
  message: `${unknownLicenceCount} ${pluralize(unknownLicenceCount, 'dependency with unknown licence', 'dependencies with unknown licence')}`
174
181
  });
175
182
  }
183
+ if (rules.has('supply-chain-source') && supplyChainSourceCount > 0) {
184
+ violations.push({
185
+ rule: 'supply-chain-source',
186
+ count: supplyChainSourceCount,
187
+ message: `${supplyChainSourceCount} ${pluralize(supplyChainSourceCount, 'lockfile supply-chain source finding', 'lockfile supply-chain source findings')}`
188
+ });
189
+ }
176
190
  return violations;
177
191
  }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildDependencyFindings = buildDependencyFindings;
4
+ const nodeEngine_1 = require("./nodeEngine");
5
+ function vulnerabilityTotal(dep) {
6
+ const summary = dep.security.summary;
7
+ return ((summary.critical || 0) +
8
+ (summary.high || 0) +
9
+ (summary.moderate || 0) +
10
+ (summary.low || 0));
11
+ }
12
+ function findingId(dep, suffix) {
13
+ return `${dep.package.id}:${suffix}`.replace(/[^a-zA-Z0-9_.@/-]+/g, '-');
14
+ }
15
+ function supportsTargetNode(dep, targetNodeMajor) {
16
+ if (!targetNodeMajor || !dep.upgrade.nodeEngine)
17
+ return undefined;
18
+ return (0, nodeEngine_1.isNodeEngineTargetCompatible)(dep.upgrade.nodeEngine, targetNodeMajor);
19
+ }
20
+ function baseFinding(dep, suffix, fields) {
21
+ return {
22
+ id: findingId(dep, suffix),
23
+ packageId: dep.package.id,
24
+ packageName: dep.package.name,
25
+ packageVersion: dep.package.version,
26
+ ...fields
27
+ };
28
+ }
29
+ function buildDependencyFindings(data, options = {}) {
30
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
31
+ const findings = [];
32
+ for (const dep of Object.values(data.dependencies || {})) {
33
+ const vulnCount = vulnerabilityTotal(dep);
34
+ if (vulnCount > 0) {
35
+ const highest = dep.security.summary.highest;
36
+ findings.push(baseFinding(dep, 'vulnerabilities', {
37
+ category: 'security',
38
+ severity: highest === 'critical' || highest === 'high' ? 'error' : 'warning',
39
+ title: `${vulnCount} known ${vulnCount === 1 ? 'vulnerability' : 'vulnerabilities'}`,
40
+ message: `${dep.package.id} has audit advisories; highest severity is ${highest}.`,
41
+ evidence: (_a = dep.security.advisories) === null || _a === void 0 ? void 0 : _a.map((advisory) => advisory.id).join(', '),
42
+ recommendation: dep.upgrade.latestVersion
43
+ ? `Review and upgrade toward ${dep.upgrade.latestVersion}.`
44
+ : 'Review the advisory and upgrade path.'
45
+ }));
46
+ }
47
+ if (dep.compliance.license.status === 'mismatch') {
48
+ findings.push(baseFinding(dep, 'license-mismatch', {
49
+ category: 'license',
50
+ severity: 'warning',
51
+ title: 'Declared and inferred licenses differ',
52
+ message: `${dep.package.id} declares ${((_b = dep.compliance.license.declared) === null || _b === void 0 ? void 0 : _b.spdxId) || 'unknown'} but the local license file looks like ${((_c = dep.compliance.license.inferred) === null || _c === void 0 ? void 0 : _c.spdxId) || 'unknown'}.`,
53
+ recommendation: 'Verify the installed package license before release or compliance sign-off.'
54
+ }));
55
+ }
56
+ else if (dep.compliance.license.status === 'invalid-spdx' || dep.compliance.license.status === 'unknown') {
57
+ findings.push(baseFinding(dep, `license-${dep.compliance.license.status}`, {
58
+ category: 'license',
59
+ severity: dep.usage.scope === 'runtime' ? 'warning' : 'info',
60
+ title: dep.compliance.license.status === 'unknown' ? 'License is unknown' : 'License declaration is invalid SPDX',
61
+ message: `${dep.package.id} needs license review.`,
62
+ evidence: (_d = dep.compliance.license.declared) === null || _d === void 0 ? void 0 : _d.spdxId,
63
+ recommendation: 'Check package metadata and LICENSE files.'
64
+ }));
65
+ }
66
+ if (dep.compliance.licenseRisk === 'red' && dep.usage.scope === 'runtime') {
67
+ findings.push(baseFinding(dep, 'runtime-license-risk', {
68
+ category: 'license',
69
+ severity: 'error',
70
+ title: 'High-risk runtime license',
71
+ message: `${dep.package.id} is in the runtime tree and has red license risk.`,
72
+ recommendation: 'Review legal/compliance acceptability or replace the package.'
73
+ }));
74
+ }
75
+ if ((_g = (_f = (_e = dep.execution) === null || _e === void 0 ? void 0 : _e.scripts) === null || _f === void 0 ? void 0 : _f.hooks) === null || _g === void 0 ? void 0 : _g.length) {
76
+ findings.push(baseFinding(dep, 'install-scripts', {
77
+ category: 'execution',
78
+ severity: dep.execution.risk === 'red' ? 'error' : 'warning',
79
+ title: 'Install lifecycle script',
80
+ message: `${dep.package.id} runs ${dep.execution.scripts.hooks.join(', ')} during install.`,
81
+ evidence: (_h = dep.execution.scripts.signals) === null || _h === void 0 ? void 0 : _h.join(', '),
82
+ recommendation: 'Review install-time behavior, especially in CI and release environments.'
83
+ }));
84
+ }
85
+ if ((_j = dep.execution) === null || _j === void 0 ? void 0 : _j.native) {
86
+ findings.push(baseFinding(dep, 'native-bindings', {
87
+ category: 'upgrade',
88
+ severity: 'warning',
89
+ title: 'Native binding or build surface',
90
+ message: `${dep.package.id} includes native binding or native build indicators.`,
91
+ recommendation: 'Check platform and Node major compatibility before upgrades.'
92
+ }));
93
+ }
94
+ if (dep.package.deprecated) {
95
+ findings.push(baseFinding(dep, 'deprecated', {
96
+ category: 'supply-chain',
97
+ severity: dep.usage.scope === 'runtime' ? 'warning' : 'info',
98
+ title: 'Package is deprecated',
99
+ message: `${dep.package.id} is marked deprecated in local package metadata.`,
100
+ recommendation: 'Plan migration to a maintained replacement.'
101
+ }));
102
+ }
103
+ const targetSupport = supportsTargetNode(dep, options.targetNodeMajor);
104
+ if (targetSupport === false && options.targetNodeMajor) {
105
+ findings.push(baseFinding(dep, `target-node-${options.targetNodeMajor}`, {
106
+ category: 'upgrade',
107
+ severity: dep.usage.scope === 'runtime' ? 'error' : 'warning',
108
+ title: `May block Node ${options.targetNodeMajor}`,
109
+ message: `${dep.package.id} declares engines.node "${dep.upgrade.nodeEngine}", which does not appear to include Node ${options.targetNodeMajor}.`,
110
+ recommendation: 'Upgrade, replace, or verify engine compatibility manually.'
111
+ }));
112
+ }
113
+ }
114
+ for (const signal of ((_k = data.supplyChain) === null || _k === void 0 ? void 0 : _k.signals) || []) {
115
+ findings.push(buildSupplyChainFinding(signal));
116
+ }
117
+ const signatureAudit = (_l = data.supplyChain) === null || _l === void 0 ? void 0 : _l.signatureAudit;
118
+ if ((signatureAudit === null || signatureAudit === void 0 ? void 0 : signatureAudit.attempted) && !signatureAudit.ok) {
119
+ findings.push({
120
+ id: 'supply-chain:signature-verification-failed',
121
+ category: 'supply-chain',
122
+ severity: 'warning',
123
+ packageId: 'project',
124
+ packageName: 'project',
125
+ packageVersion: '',
126
+ title: 'npm signature/provenance verification failed',
127
+ message: signatureAudit.error || 'npm audit signatures did not complete successfully.',
128
+ evidence: signatureAudit.output,
129
+ recommendation: 'Review npm audit signatures output and verify registry/provenance status.'
130
+ });
131
+ }
132
+ return findings.sort((a, b) => {
133
+ const severityOrder = { error: 2, warning: 1, info: 0 };
134
+ const diff = severityOrder[b.severity] - severityOrder[a.severity];
135
+ if (diff !== 0)
136
+ return diff;
137
+ return a.packageId.localeCompare(b.packageId) || a.id.localeCompare(b.id);
138
+ });
139
+ }
140
+ function buildSupplyChainFinding(signal) {
141
+ const packageId = signal.packageId || (signal.packageName && signal.packageVersion
142
+ ? `${signal.packageName}@${signal.packageVersion}`
143
+ : signal.packageName || 'lockfile');
144
+ const titleByType = {
145
+ 'git-dependency': 'Git dependency source',
146
+ 'file-dependency': 'Local file dependency source',
147
+ 'non-registry-tarball': 'Non-registry tarball source',
148
+ 'missing-integrity': 'Missing lockfile integrity',
149
+ 'unexpected-registry-host': 'Unexpected registry host'
150
+ };
151
+ const severity = signal.type === 'missing-integrity' || signal.type === 'unexpected-registry-host'
152
+ ? 'warning'
153
+ : 'info';
154
+ return {
155
+ id: `${packageId}:${signal.type}`.replace(/[^a-zA-Z0-9_.@/-]+/g, '-'),
156
+ category: 'supply-chain',
157
+ severity,
158
+ packageId,
159
+ packageName: signal.packageName || packageId,
160
+ packageVersion: signal.packageVersion || '',
161
+ title: titleByType[signal.type],
162
+ message: signal.detail,
163
+ evidence: signal.source,
164
+ recommendation: 'Review the dependency source and confirm it is expected for this project.'
165
+ };
166
+ }
@@ -75,6 +75,7 @@ exports.SPDX_LICENSE_IDS = new Set([
75
75
  'Borceux',
76
76
  'Brian-Gladman-2-Clause',
77
77
  'Brian-Gladman-3-Clause',
78
+ 'Brian-Gladman-3-Clause-no-conversion',
78
79
  'BSD-1-Clause',
79
80
  'BSD-2-Clause',
80
81
  'BSD-2-Clause-Darwin',
@@ -488,6 +489,7 @@ exports.SPDX_LICENSE_IDS = new Set([
488
489
  'MulanPSL-2.0',
489
490
  'Multics',
490
491
  'Mup',
492
+ 'MVT-1.1',
491
493
  'NAIST-2003',
492
494
  'NASA-1.3',
493
495
  'Naumen',
@@ -801,6 +803,7 @@ exports.SPDX_EXCEPTION_IDS = new Set([
801
803
  'GNOME-examples-exception',
802
804
  'GNU-compiler-exception',
803
805
  'gnu-javamail-exception',
806
+ 'Google-Patent-WebM',
804
807
  'GPL-3.0-389-ds-base-exception',
805
808
  'GPL-3.0-interface-exception',
806
809
  'GPL-3.0-linking-exception',
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isNodeEngineTargetCompatible = isNodeEngineTargetCompatible;
4
+ function parseVersionParts(value) {
5
+ const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
6
+ if (!match)
7
+ return undefined;
8
+ return {
9
+ version: [Number(match[1]), Number(match[2] || 0), Number(match[3] || 0)],
10
+ parts: match[3] ? 3 : match[2] ? 2 : 1,
11
+ };
12
+ }
13
+ function parseVersion(value) {
14
+ var _a;
15
+ return (_a = parseVersionParts(value)) === null || _a === void 0 ? void 0 : _a.version;
16
+ }
17
+ function compare(a, b) {
18
+ for (let i = 0; i < 3; i += 1)
19
+ if (a[i] !== b[i])
20
+ return a[i] - b[i];
21
+ return 0;
22
+ }
23
+ function satisfiesComparator(target, comparator) {
24
+ const match = comparator.trim().match(/^(<=|>=|<|>|=)?\s*v?(\d+(?:\.\d+){0,2})$/);
25
+ if (!match)
26
+ return false;
27
+ const version = parseVersion(match[2]);
28
+ if (!version)
29
+ return false;
30
+ const diff = compare(target, version);
31
+ const op = match[1] || '=';
32
+ if (op === '<')
33
+ return diff < 0;
34
+ if (op === '<=')
35
+ return diff <= 0;
36
+ if (op === '>')
37
+ return diff > 0;
38
+ if (op === '>=')
39
+ return diff >= 0;
40
+ return diff === 0;
41
+ }
42
+ function comparatorAllowsTargetMajor(comparator, minTarget, maxTarget) {
43
+ const bounds = boundsForComparator(comparator);
44
+ return Boolean(bounds && intervalsOverlap(bounds, { lower: minTarget, lowerInclusive: true, upper: maxTarget, upperInclusive: false }));
45
+ }
46
+ function boundsForComparator(comparator) {
47
+ const match = comparator.trim().match(/^(<=|>=|<|>|=)?\s*v?(\d+(?:\.\d+){0,2})$/);
48
+ if (!match)
49
+ return undefined;
50
+ const parsed = parseVersionParts(match[2]);
51
+ if (!parsed)
52
+ return undefined;
53
+ const version = parsed.version;
54
+ const nextMajor = [version[0] + 1, 0, 0];
55
+ const op = match[1] || '=';
56
+ if (op === '<')
57
+ return { lowerInclusive: true, upper: version, upperInclusive: false };
58
+ if (op === '<=' && parsed.parts === 1)
59
+ return { lowerInclusive: true, upper: nextMajor, upperInclusive: false };
60
+ if (op === '<=')
61
+ return { lowerInclusive: true, upper: version, upperInclusive: true };
62
+ if (op === '>' && parsed.parts === 1)
63
+ return { lower: nextMajor, lowerInclusive: true, upperInclusive: true };
64
+ if (op === '>')
65
+ return { lower: version, lowerInclusive: false, upperInclusive: true };
66
+ if (op === '>=')
67
+ return { lower: version, lowerInclusive: true, upperInclusive: true };
68
+ return { lower: version, lowerInclusive: true, upper: version, upperInclusive: true };
69
+ }
70
+ function laterLower(a, b) {
71
+ if (!a.lower)
72
+ return { lower: b.lower, lowerInclusive: b.lowerInclusive };
73
+ if (!b.lower)
74
+ return { lower: a.lower, lowerInclusive: a.lowerInclusive };
75
+ const diff = compare(a.lower, b.lower);
76
+ if (diff > 0)
77
+ return { lower: a.lower, lowerInclusive: a.lowerInclusive };
78
+ if (diff < 0)
79
+ return { lower: b.lower, lowerInclusive: b.lowerInclusive };
80
+ return { lower: a.lower, lowerInclusive: a.lowerInclusive && b.lowerInclusive };
81
+ }
82
+ function earlierUpper(a, b) {
83
+ if (!a.upper)
84
+ return { upper: b.upper, upperInclusive: b.upperInclusive };
85
+ if (!b.upper)
86
+ return { upper: a.upper, upperInclusive: a.upperInclusive };
87
+ const diff = compare(a.upper, b.upper);
88
+ if (diff < 0)
89
+ return { upper: a.upper, upperInclusive: a.upperInclusive };
90
+ if (diff > 0)
91
+ return { upper: b.upper, upperInclusive: b.upperInclusive };
92
+ return { upper: a.upper, upperInclusive: a.upperInclusive && b.upperInclusive };
93
+ }
94
+ function intervalsOverlap(a, b) {
95
+ const lower = laterLower(a, b);
96
+ const upper = earlierUpper(a, b);
97
+ if (!lower.lower || !upper.upper)
98
+ return true;
99
+ const diff = compare(lower.lower, upper.upper);
100
+ if (diff < 0)
101
+ return true;
102
+ if (diff > 0)
103
+ return false;
104
+ return lower.lowerInclusive && upper.upperInclusive;
105
+ }
106
+ function comparatorsOverlapTargetMajor(comparators, targetMajor) {
107
+ const target = {
108
+ lower: [targetMajor, 0, 0],
109
+ lowerInclusive: true,
110
+ upper: [targetMajor + 1, 0, 0],
111
+ upperInclusive: false,
112
+ };
113
+ let interval = {
114
+ lowerInclusive: true,
115
+ upperInclusive: true,
116
+ };
117
+ for (const comparator of comparators) {
118
+ if (!comparatorAllowsTargetMajor(comparator, target.lower, target.upper))
119
+ return false;
120
+ const bounds = boundsForComparator(comparator);
121
+ if (!bounds)
122
+ continue;
123
+ const lower = laterLower(interval, bounds);
124
+ const upper = earlierUpper(interval, bounds);
125
+ interval = { ...lower, ...upper };
126
+ if (!intervalsOverlap(interval, target))
127
+ return false;
128
+ }
129
+ return intervalsOverlap(interval, target);
130
+ }
131
+ function expandToken(token) {
132
+ var _a, _b;
133
+ const trimmed = token.trim();
134
+ if (!trimmed || trimmed === '*' || /^[xX]$/.test(trimmed))
135
+ return [];
136
+ const xRange = trimmed.match(/^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?$/);
137
+ if (xRange && (((_a = xRange[2]) === null || _a === void 0 ? void 0 : _a.match(/^[xX*]$/)) || ((_b = xRange[3]) === null || _b === void 0 ? void 0 : _b.match(/^[xX*]$/)))) {
138
+ const major = Number(xRange[1]);
139
+ if (!xRange[2] || /^[xX*]$/.test(xRange[2]))
140
+ return [`>=${major}.0.0`, `<${major + 1}.0.0`];
141
+ const minor = Number(xRange[2]);
142
+ return [`>=${major}.${minor}.0`, `<${major}.${minor + 1}.0`];
143
+ }
144
+ const caret = trimmed.match(/^\^\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
145
+ if (caret) {
146
+ const major = Number(caret[1]);
147
+ return [`>=${major}.${caret[2] || 0}.${caret[3] || 0}`, `<${major + 1}.0.0`];
148
+ }
149
+ const tilde = trimmed.match(/^~\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
150
+ if (tilde) {
151
+ const major = Number(tilde[1]);
152
+ if (!tilde[2])
153
+ return [`>=${major}.0.0`, `<${major + 1}.0.0`];
154
+ const minor = Number(tilde[2]);
155
+ return [`>=${major}.${minor}.${tilde[3] || 0}`, `<${major}.${minor + 1}.0`];
156
+ }
157
+ const bare = trimmed.match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
158
+ if (bare) {
159
+ const major = Number(bare[1]);
160
+ if (!bare[2])
161
+ return [`>=${major}.0.0`, `<${major + 1}.0.0`];
162
+ const minor = Number(bare[2]);
163
+ if (!bare[3])
164
+ return [`>=${major}.${minor}.0`, `<${major}.${minor + 1}.0`];
165
+ return [`=${major}.${minor}.${bare[3]}`];
166
+ }
167
+ return [trimmed];
168
+ }
169
+ function isNodeEngineTargetCompatible(range, targetMajor) {
170
+ if (!range || !range.trim())
171
+ return undefined;
172
+ const clauses = range.split('||').map((clause) => clause.trim()).filter(Boolean);
173
+ if (clauses.length === 0)
174
+ return undefined;
175
+ return clauses.some((clause) => {
176
+ const hyphen = clause.match(/^\s*v?(\d+(?:\.\d+){0,2})\s+-\s+v?(\d+(?:\.\d+){0,2})\s*$/);
177
+ const tokens = hyphen ? [`>=${hyphen[1]}`, `<=${hyphen[2]}`] : clause.split(/\s+/);
178
+ const comparators = tokens.flatMap(expandToken);
179
+ return comparators.length === 0 || comparatorsOverlapTargetMajor(comparators, targetMajor);
180
+ });
181
+ }