-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
executable file
·152 lines (137 loc) · 5.37 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env node
// @ts-check
'use strict';
const fs = require('fs');
const semver = require('semver');
const yargs = require('yargs');
const getIntersection = require('./getIntersection.js');
const argv = yargs(process.argv.slice(2))
.alias({ v: 'version' })
.option('package', {
describe: 'The location of package.json',
normalize: true,
type: 'string',
})
.option('lockfile', {
describe: 'The location of package-lock.json',
normalize: true,
type: 'string',
})
.option('engine', {
default: 'node',
describe: 'Which engine to check',
type: 'string',
})
.option('find-limits', {
conflicts: 'package',
describe: 'Instead of validating package.json, find the limits of the lockfile',
type: 'boolean',
})
.option('quiet', {
alias: 'q',
describe: "Don't report which packages fail",
type: 'boolean',
}).argv;
/** @param {Awaited<typeof argv>} argv */
const run = argv => {
/** @param {string} msg */
const error = msg => {
process.exitCode = 1;
if (!argv.quiet) {
console.error(msg);
}
};
/**
* @type {{
* lockfileVersion?: number,
* packages: { [key: string]: { version: string, engines?: Partial<Record<string, string>> }}
* }}
*/
const lockfile = JSON.parse(fs.readFileSync(argv.lockfile || './package-lock.json').toString());
if (lockfile.lockfileVersion === undefined || lockfile.lockfileVersion < 2) {
error(
"Older lockfiles don't include the engines object. Please upgrade to lockfile v2 or v3."
);
return;
} else if (lockfile.lockfileVersion > 3 && !argv.quiet) {
console.warn(`Lockfile v${lockfile.lockfileVersion} is newer than expected.`);
}
/**
* @type {(readonly [string, string, string])[]}
* @ts-expect-error The filter call ensures there's no undefineds */
const versions = Object.entries(lockfile.packages)
.map(el => [
el[0].replace(/node_modules\//g, ''),
el[1].version,
el[1].engines && el[1].engines[argv.engine],
])
.filter(el => el[0] && el[2]);
if (argv.findLimits) {
// Currently unused. Could find which exact packages are limiting it.
// /** @type {Set<readonly [string, string, string]>} */
// const limitList = new Set();
// outer: for (const triple of versions) {
// for (const limit of limitList) {
// if (semver.subset(limit[2], triple[2])) {
// continue outer;
// }
// if (semver.subset(triple[2], limit[2])) {
// limitList.delete(limit);
// }
// }
// limitList.add(triple);
// }
/** @type {semver.Range | null} */
let intersection = new semver.Range('*');
for (const [, , version] of versions) {
intersection = getIntersection(version, intersection);
if (intersection === null) {
error('No valid intersection of ranges exists.');
return;
}
}
let cleanStr = intersection.toString()
// Replace e.g. '>=1.0.0 <2.0.0-0' with '^1.0.0'
.replace(/>=([1-9]\d*)\.(\d+\.\d+) <(\d+)\.0\.0-0/g, (match, firstMajor, firstMinorPatch, secondMajor) => {
if (+firstMajor + 1 === +secondMajor) return `^${firstMajor}.${firstMinorPatch}`;
return match;
})
// Replace e.g. '>=1.0.0 <1.1.0-0' with '~1.0.0'
.replace(/>=(\d+)\.(\d+)\.(\d+) <\1\.(\d+)\.0-0/g, (match, major, firstMinor, patch, secondMinor) => {
if (major === '0' && firstMinor === '0') return match;
if (+firstMinor + 1 === +secondMinor) return `~${major}.${firstMinor}.${patch}`;
return match;
})
// Replace e.g. '>=1.0.0 <=1.2.3' with '1.0.0 - 1.2.3'
.replace(/>=([^\s<>=^~]+) <=([^\s<>=^~]+)/g, (match, first, second) => `${first} - ${second}`)
// Insert spaces around ||
.replace(/\|\|/g, ' || ');
console.log(cleanStr);
} else {
const fileName = argv.package || './package.json';
/** @type {{ engines?: Partial<Record<string, string>>}} */
const packageJson = JSON.parse(fs.readFileSync(fileName).toString());
if (!packageJson.engines || !packageJson.engines[argv.engine]) {
process.exitCode = 1;
console.error(`No engines.${[argv.engine]} is present in ${fileName}.`);
return;
}
if (!semver.validRange(packageJson.engines[argv.engine])) {
error(
`engines.${[argv.engine]} "${packageJson.engines[argv.engine]}" is not a valid semver range.`
);
return;
}
for (const [packageName, packageVersion, engineVersions] of versions) {
/** @ts-expect-error It can do narrowing on dot access but not bracket access */
if (!semver.subset(packageJson.engines[argv.engine], engineVersions)) {
error(`${packageName}@${packageVersion} requires ${argv.engine} "${engineVersions}".`);
}
}
}
};
if (argv instanceof Promise) argv.then(run, e => {
process.exitCode = 1;
console.error(e);
});
else run(argv);