Skip to content

Commit ebf9b9f

Browse files
authored
feat: adds checkDevEngines (#116)
1 parent 3e0e1b6 commit ebf9b9f

File tree

5 files changed

+646
-26
lines changed

5 files changed

+646
-26
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,10 @@ Check if a package's `os`, `cpu` and `libc` match the running system.
2828
`environment` overrides the execution environment which comes from `process.platform` `process.arch` and current `libc` environment by default. `environment.os` `environment.cpu` and `environment.libc` are available.
2929

3030
Error code: 'EBADPLATFORM'
31+
32+
33+
### .checkDevEngines(wanted, current, opts)
34+
35+
Check if a package's `devEngines` property matches the current system environment.
36+
37+
Returns an array of `Error` objects, some of which may be warnings, this can be checked with `.isError` and `.isWarn`. Errors correspond to an error for a given "engine" failure, reasons for each engine "dependency" failure can be found within `.errors`.

lib/current-env.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const process = require('node:process')
2+
const nodeOs = require('node:os')
3+
4+
function isMusl (file) {
5+
return file.includes('libc.musl-') || file.includes('ld-musl-')
6+
}
7+
8+
function os () {
9+
return process.platform
10+
}
11+
12+
function cpu () {
13+
return process.arch
14+
}
15+
16+
function libc (osName) {
17+
// this is to make it faster on non linux machines
18+
if (osName !== 'linux') {
19+
return undefined
20+
}
21+
let family
22+
const report = process.report.getReport()
23+
if (report.header?.glibcVersionRuntime) {
24+
family = 'glibc'
25+
} else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) {
26+
family = 'musl'
27+
}
28+
return family
29+
}
30+
31+
function devEngines (env = {}) {
32+
const osName = env.os || os()
33+
return {
34+
cpu: {
35+
name: env.cpu || cpu(),
36+
},
37+
libc: {
38+
name: env.libc || libc(osName),
39+
},
40+
os: {
41+
name: osName,
42+
version: env.osVersion || nodeOs.release(),
43+
},
44+
packageManager: {
45+
name: 'npm',
46+
version: env.npmVersion,
47+
},
48+
runtime: {
49+
name: 'node',
50+
version: env.nodeVersion || process.version,
51+
},
52+
}
53+
}
54+
55+
module.exports = {
56+
cpu,
57+
libc,
58+
os,
59+
devEngines,
60+
}

lib/dev-engines.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
const satisfies = require('semver/functions/satisfies')
2+
const validRange = require('semver/ranges/valid')
3+
4+
const recognizedOnFail = [
5+
'ignore',
6+
'warn',
7+
'error',
8+
'download',
9+
]
10+
11+
const recognizedProperties = [
12+
'name',
13+
'version',
14+
'onFail',
15+
]
16+
17+
const recognizedEngines = [
18+
'packageManager',
19+
'runtime',
20+
'cpu',
21+
'libc',
22+
'os',
23+
]
24+
25+
/** checks a devEngine dependency */
26+
function checkDependency (wanted, current, opts) {
27+
const { engine } = opts
28+
29+
if ((typeof wanted !== 'object' || wanted === null) || Array.isArray(wanted)) {
30+
throw new Error(`Invalid non-object value for "${engine}"`)
31+
}
32+
33+
const properties = Object.keys(wanted)
34+
35+
for (const prop of properties) {
36+
if (!recognizedProperties.includes(prop)) {
37+
throw new Error(`Invalid property "${prop}" for "${engine}"`)
38+
}
39+
}
40+
41+
if (!properties.includes('name')) {
42+
throw new Error(`Missing "name" property for "${engine}"`)
43+
}
44+
45+
if (typeof wanted.name !== 'string') {
46+
throw new Error(`Invalid non-string value for "name" within "${engine}"`)
47+
}
48+
49+
if (typeof current.name !== 'string' || current.name === '') {
50+
throw new Error(`Unable to determine "name" for "${engine}"`)
51+
}
52+
53+
if (properties.includes('onFail')) {
54+
if (typeof wanted.onFail !== 'string') {
55+
throw new Error(`Invalid non-string value for "onFail" within "${engine}"`)
56+
}
57+
if (!recognizedOnFail.includes(wanted.onFail)) {
58+
throw new Error(`Invalid onFail value "${wanted.onFail}" for "${engine}"`)
59+
}
60+
}
61+
62+
if (wanted.name !== current.name) {
63+
return new Error(
64+
`Invalid name "${wanted.name}" does not match "${current.name}" for "${engine}"`
65+
)
66+
}
67+
68+
if (properties.includes('version')) {
69+
if (typeof wanted.version !== 'string') {
70+
throw new Error(`Invalid non-string value for "version" within "${engine}"`)
71+
}
72+
if (typeof current.version !== 'string' || current.version === '') {
73+
throw new Error(`Unable to determine "version" for "${engine}" "${wanted.name}"`)
74+
}
75+
if (validRange(wanted.version)) {
76+
if (!satisfies(current.version, wanted.version, opts.semver)) {
77+
return new Error(
78+
// eslint-disable-next-line max-len
79+
`Invalid semver version "${wanted.version}" does not match "${current.version}" for "${engine}"`
80+
)
81+
}
82+
} else if (wanted.version !== current.version) {
83+
return new Error(
84+
`Invalid version "${wanted.version}" does not match "${current.version}" for "${engine}"`
85+
)
86+
}
87+
}
88+
}
89+
90+
/** checks devEngines package property and returns array of warnings / errors */
91+
function checkDevEngines (wanted, current = {}, opts = {}) {
92+
if ((typeof wanted !== 'object' || wanted === null) || Array.isArray(wanted)) {
93+
throw new Error(`Invalid non-object value for devEngines`)
94+
}
95+
96+
const errors = []
97+
98+
for (const engine of Object.keys(wanted)) {
99+
if (!recognizedEngines.includes(engine)) {
100+
throw new Error(`Invalid property "${engine}"`)
101+
}
102+
const dependencyAsAuthored = wanted[engine]
103+
const dependencies = [dependencyAsAuthored].flat()
104+
const currentEngine = current[engine] || {}
105+
106+
// this accounts for empty array eg { runtime: [] } and ignores it
107+
if (dependencies.length === 0) {
108+
continue
109+
}
110+
111+
const depErrors = []
112+
for (const dep of dependencies) {
113+
const result = checkDependency(dep, currentEngine, { ...opts, engine })
114+
if (result) {
115+
depErrors.push(result)
116+
}
117+
}
118+
119+
const invalid = depErrors.length === dependencies.length
120+
121+
if (invalid) {
122+
const lastDependency = dependencies[dependencies.length - 1]
123+
let onFail = lastDependency.onFail || 'error'
124+
if (onFail === 'download') {
125+
onFail = 'error'
126+
}
127+
128+
const err = Object.assign(new Error(`Invalid engine "${engine}"`), {
129+
errors: depErrors,
130+
engine,
131+
isWarn: onFail === 'warn',
132+
isError: onFail === 'error',
133+
current: currentEngine,
134+
required: dependencyAsAuthored,
135+
})
136+
137+
errors.push(err)
138+
}
139+
}
140+
return errors
141+
}
142+
143+
module.exports = {
144+
checkDevEngines,
145+
}

lib/index.js

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const semver = require('semver')
2+
const currentEnv = require('./current-env')
3+
const { checkDevEngines } = require('./dev-engines')
24

35
const checkEngine = (target, npmVer, nodeVer, force = false) => {
46
const nodev = force ? null : nodeVer
@@ -20,44 +22,29 @@ const checkEngine = (target, npmVer, nodeVer, force = false) => {
2022
}
2123
}
2224

23-
const isMusl = (file) => file.includes('libc.musl-') || file.includes('ld-musl-')
24-
2525
const checkPlatform = (target, force = false, environment = {}) => {
2626
if (force) {
2727
return
2828
}
2929

30-
const platform = environment.os || process.platform
31-
const arch = environment.cpu || process.arch
32-
const osOk = target.os ? checkList(platform, target.os) : true
33-
const cpuOk = target.cpu ? checkList(arch, target.cpu) : true
30+
const os = environment.os || currentEnv.os()
31+
const cpu = environment.cpu || currentEnv.cpu()
32+
const libc = environment.libc || currentEnv.libc(os)
3433

35-
let libcOk = true
36-
let libcFamily = null
37-
if (target.libc) {
38-
// libc checks only work in linux, any value is a failure if we aren't
39-
if (environment.libc) {
40-
libcOk = checkList(environment.libc, target.libc)
41-
} else if (platform !== 'linux') {
42-
libcOk = false
43-
} else {
44-
const report = process.report.getReport()
45-
if (report.header?.glibcVersionRuntime) {
46-
libcFamily = 'glibc'
47-
} else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) {
48-
libcFamily = 'musl'
49-
}
50-
libcOk = libcFamily ? checkList(libcFamily, target.libc) : false
51-
}
34+
const osOk = target.os ? checkList(os, target.os) : true
35+
const cpuOk = target.cpu ? checkList(cpu, target.cpu) : true
36+
let libcOk = target.libc ? checkList(libc, target.libc) : true
37+
if (target.libc && !libc) {
38+
libcOk = false
5239
}
5340

5441
if (!osOk || !cpuOk || !libcOk) {
5542
throw Object.assign(new Error('Unsupported platform'), {
5643
pkgid: target._id,
5744
current: {
58-
os: platform,
59-
cpu: arch,
60-
libc: libcFamily,
45+
os,
46+
cpu,
47+
libc,
6148
},
6249
required: {
6350
os: target.os,
@@ -98,4 +85,6 @@ const checkList = (value, list) => {
9885
module.exports = {
9986
checkEngine,
10087
checkPlatform,
88+
checkDevEngines,
89+
currentEnv,
10190
}

0 commit comments

Comments
 (0)