From 11b7d0480e6cde6a215507db4d370c4f50ed525b Mon Sep 17 00:00:00 2001 From: Gar Date: Mon, 3 Jun 2024 09:56:13 -0700 Subject: [PATCH 01/21] fix: move from `main` to `exports` in package.json BREAKING CHANGE: This module now defines an `exports` field in `package.json`. You will no longer be able to require individual files directly. --- package.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 38d9034..c8a3fbe 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,16 @@ "name": "npm-install-checks", "version": "6.3.0", "description": "Check the engines and platform fields in package.json", - "main": "lib/index.js", + "main": "./lib/index.js", + "exports": { + ".": [ + { + "default": "./lib/index.js" + }, + "./lib/index.js" + ] + }, + "type": "commonjs", "dependencies": { "semver": "^7.1.1" }, From 3d0d00b60a03864c1b6f203c6490f8ad64d88ab1 Mon Sep 17 00:00:00 2001 From: Gar Date: Mon, 3 Jun 2024 10:33:45 -0700 Subject: [PATCH 02/21] deps: add proc-log@4.2.0 --- lib/index.js | 86 +++++++++++++++++++++++++++++++++++++++++++---- package.json | 1 + test/check-dev.js | 50 +++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 test/check-dev.js diff --git a/lib/index.js b/lib/index.js index 545472b..ea94d24 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,72 @@ -const semver = require('semver') +const process = require('node:process') +const satisfies = require('semver/functions/satisfies') +const validRange = require('semver/ranges/valid') -const checkEngine = (target, npmVer, nodeVer, force = false) => { +/* + +interface DevEngines { + cpu?: DevEngineDependency | DevEngineDependency[]; + libc?: DevEngineDependency | DevEngineDependency[]; + os?: DevEngineDependency | DevEngineDependency[]; + packageManager?: DevEngineDependency | DevEngineDependency[]; + runtime?: DevEngineDependency | DevEngineDependency[]; +} + +interface DevEngineDependency { + name: string; + version?: string; + onFail?: 'ignore' | 'warn' | 'error' | 'download'; + download?: { + url: string; + algorithm?: string; + digest?: string; + } +} + +*/ + +const envNames = ['packageManager', 'runtime', 'cpu', 'libc', 'os'] + +// Returns an object with the last failing entry of any given wanted entry, null if no failures +function checkDev (wanted = {}, current = {}, opts = {}) { + const failures = {} + for (const env of envNames) { + let wantedEnv = wanted[env] + if (wantedEnv) { + const currentEnv = current[env] + if (!Array.isArray(wantedEnv)) { + wantedEnv = [wantedEnv] + } + // In case of failure we return the last entry to fail so we walk backwards and return the first failure + for (let i = wantedEnv.length - 1; i > -1; i--) { + const w = wantedEnv[i] + if (!currentEnv) { + failures[env] = w + break + } + if (w.name !== currentEnv.name) { + failures[env] = w + break + } + if (validRange(w.version)) { + if (!satisfies(currentEnv.version, w.version, opts.semver)) { + failures[env] = w + break + } + } else if (currentEnv.version !== w.version) { + failures[env] = w + break + } + } + } + } + if (Object.keys(failures).length) { + return failures + } + return null +} + +function checkEngine (target, npmVer, nodeVer, force = false) { const nodev = force ? null : nodeVer const eng = target.engines const opt = { includePrerelease: true } @@ -8,8 +74,8 @@ const checkEngine = (target, npmVer, nodeVer, force = false) => { return } - const nodeFail = nodev && eng.node && !semver.satisfies(nodev, eng.node, opt) - const npmFail = npmVer && eng.npm && !semver.satisfies(npmVer, eng.npm, opt) + const nodeFail = nodev && eng.node && !satisfies(nodev, eng.node, opt) + const npmFail = npmVer && eng.npm && !satisfies(npmVer, eng.npm, opt) if (nodeFail || npmFail) { throw Object.assign(new Error('Unsupported engine'), { pkgid: target._id, @@ -20,9 +86,11 @@ const checkEngine = (target, npmVer, nodeVer, force = false) => { } } -const isMusl = (file) => file.includes('libc.musl-') || file.includes('ld-musl-') +function isMusl (file) { + return file.includes('libc.musl-') || file.includes('ld-musl-') +} -const checkPlatform = (target, force = false, environment = {}) => { +function checkPlatform (target, force = false, environment = {}) { if (force) { return } @@ -69,7 +137,7 @@ const checkPlatform = (target, force = false, environment = {}) => { } } -const checkList = (value, list) => { +function checkList (value, list) { if (typeof list === 'string') { list = [list] } @@ -96,6 +164,10 @@ const checkList = (value, list) => { } module.exports = { + // Used by npm-pick-manifest, "npm install -g npm", and arborist build-ideal-tree/reify checkEngine, + // used by arborist build-ideal-tree/reify checkPlatform, + // used by npm install for devEngines + checkDev, } diff --git a/package.json b/package.json index c8a3fbe..a9f0616 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "type": "commonjs", "dependencies": { + "proc-log": "^4.2.0", "semver": "^7.1.1" }, "devDependencies": { diff --git a/test/check-dev.js b/test/check-dev.js new file mode 100644 index 0000000..2a7729c --- /dev/null +++ b/test/check-dev.js @@ -0,0 +1,50 @@ +const t = require('tap') +const { checkDev } = require('..') + +t.test('empty params', async t => { + t.equal(checkDev(), null) +}) + +t.test('tests all the right fields', async t => { + for (const env of ['packageManager', 'runtime', 'cpu', 'libc', 'os']) { + t.test(`field - ${env}`, async t => { + t.test('current name does not match, wanted has extra attribute', async t => { + const wanted = { name: `test-${env}-wanted`, extra: `test-${env}-extra` } + const current = { name: `test-${env}-current` } + t.same(checkDev({ [env]: wanted }, { [env]: current }), { [env]: wanted }) + }) + t.test('current is not given', async t => { + const wanted = { name: `test-${env}-wanted` } + t.same(checkDev({ [env]: wanted }), { [env]: wanted }) + }) + t.test('non-semver version is not the same', async t => { + const wanted = { name: `test-name`, version: 'test-version-wanted' } + const current = { name: `test-name`, version: 'test-version-current' } + t.same(checkDev({ [env]: wanted }, { [env]: current }), { [env]: wanted }) + }) + t.test('non-semver version is the same', async t => { + const wanted = { name: `test-name`, version: 'test-version' } + const current = { name: `test-name`, version: 'test-version' } + t.same(checkDev({ [env]: wanted }, { [env]: current }), null) + }) + t.test('semver version is not in range', async t => { + const wanted = { name: `test-name`, version: '^1.0.0' } + const current = { name: `test-name`, version: '2.0.0' } + t.same(checkDev({ [env]: wanted }, { [env]: current }), { [env]: wanted }) + }) + t.test('semver version is in range', async t => { + const wanted = { name: `test-name`, version: '^1.0.0' } + const current = { name: `test-name`, version: '1.0.0' } + t.same(checkDev({ [env]: wanted }, { [env]: current }), null) + }) + t.test('returns the last failure', async t => { + const wanted = [ + { name: `test-name`, version: 'test-version-one' }, + { name: `test-name`, version: 'test-version-two' }, + ] + const current = { name: `test-name`, version: 'test-version-three' } + t.same(checkDev({ [env]: wanted }, { [env]: current }), { [env]: wanted[1] }) + }) + }) + } +}) From 634bfd2e7c0ddefa9e4adede5c832ff06ed29029 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 17 Jul 2024 11:32:36 -0700 Subject: [PATCH 03/21] wip: adding devEngines Super preliminary loop that will not even work right w/ libc --- lib/current-env.js | 31 +++++++++++++++++++++++++++++++ lib/env.js | 31 +++++++++++++++++++++++++++++++ lib/index.js | 33 +++++++++++++-------------------- test/check-dev.js | 5 +++++ test/check-platform.js | 1 + 5 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 lib/current-env.js create mode 100644 lib/env.js diff --git a/lib/current-env.js b/lib/current-env.js new file mode 100644 index 0000000..e676bcd --- /dev/null +++ b/lib/current-env.js @@ -0,0 +1,31 @@ +const process = require('node:process') + +function os () { + return process.platform +} + +function cpu () { + return process.arch +} + +function isMusl (file) { + return file.includes('libc.musl-') || file.includes('ld-musl-') +} + +// libc checks only work in linux, environment and os check needs to happen out of band from this function +function libcFamily () { + let family = null + const report = process.report.getReport() + if (report.header?.glibcVersionRuntime) { + family = 'glibc' + } else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) { + family = 'musl' + } + return family +} + +module.exports = { + cpu, + libcFamily, + os, +} diff --git a/lib/env.js b/lib/env.js new file mode 100644 index 0000000..a67820a --- /dev/null +++ b/lib/env.js @@ -0,0 +1,31 @@ +const process = require('node:process') + +function os () { + return process.platform +} + +function cpu () { + return process.arch +} + +function isMusl (file) { + return file.includes('libc.musl-') || file.includes('ld-musl-') +} + +// libc checks only work in linux, environment and os check needs to happen out of band from this function +function libcFamily (environment) { + let family = null + const report = process.report.getReport() + if (report.header?.glibcVersionRuntime) { + family = 'glibc' + } else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) { + family = 'musl' + } + return family +} + +module.exports = { + cpu, + libcFamily, + os, +} diff --git a/lib/index.js b/lib/index.js index ea94d24..b942c64 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,6 @@ -const process = require('node:process') const satisfies = require('semver/functions/satisfies') const validRange = require('semver/ranges/valid') +const currentEnv = require('./current-env.js') /* @@ -48,14 +48,16 @@ function checkDev (wanted = {}, current = {}, opts = {}) { failures[env] = w break } - if (validRange(w.version)) { - if (!satisfies(currentEnv.version, w.version, opts.semver)) { + if (w.version) { + if (validRange(w.version)) { + if (!satisfies(currentEnv.version, w.version, opts.semver)) { + failures[env] = w + break + } + } else if (currentEnv.version !== w.version) { failures[env] = w break } - } else if (currentEnv.version !== w.version) { - failures[env] = w - break } } } @@ -86,19 +88,15 @@ function checkEngine (target, npmVer, nodeVer, force = false) { } } -function isMusl (file) { - return file.includes('libc.musl-') || file.includes('ld-musl-') -} - function checkPlatform (target, force = false, environment = {}) { if (force) { return } - const platform = environment.os || process.platform - const arch = environment.cpu || process.arch + const platform = environment.os || currentEnv.os() + const cpu = environment.cpu || currentEnv.cpu() const osOk = target.os ? checkList(platform, target.os) : true - const cpuOk = target.cpu ? checkList(arch, target.cpu) : true + const cpuOk = target.cpu ? checkList(cpu, target.cpu) : true let libcOk = true let libcFamily = null @@ -109,12 +107,7 @@ function checkPlatform (target, force = false, environment = {}) { } else if (platform !== 'linux') { libcOk = false } else { - const report = process.report.getReport() - if (report.header?.glibcVersionRuntime) { - libcFamily = 'glibc' - } else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) { - libcFamily = 'musl' - } + libcFamily = currentEnv.libcFamily() libcOk = libcFamily ? checkList(libcFamily, target.libc) : false } } @@ -124,7 +117,7 @@ function checkPlatform (target, force = false, environment = {}) { pkgid: target._id, current: { os: platform, - cpu: arch, + cpu, libc: libcFamily, }, required: { diff --git a/test/check-dev.js b/test/check-dev.js index 2a7729c..657c7db 100644 --- a/test/check-dev.js +++ b/test/check-dev.js @@ -17,6 +17,11 @@ t.test('tests all the right fields', async t => { const wanted = { name: `test-${env}-wanted` } t.same(checkDev({ [env]: wanted }), { [env]: wanted }) }) + t.test('name only', async t => { + const wanted = { name: 'test-name' } + const current = { name: 'test-name' } + t.same(checkDev({ [env]: wanted }, { [env]: current}), null) + }) t.test('non-semver version is not the same', async t => { const wanted = { name: `test-name`, version: 'test-version-wanted' } const current = { name: `test-name`, version: 'test-version-current' } diff --git a/test/check-platform.js b/test/check-platform.js index 3d262d6..070338e 100644 --- a/test/check-platform.js +++ b/test/check-platform.js @@ -1,4 +1,5 @@ const t = require('tap') +const process = require('node:process') const { checkPlatform } = require('..') t.test('target cpu wrong', async t => From 2841ec8f9ef68d62275b39cc18c1bae328f05e75 Mon Sep 17 00:00:00 2001 From: Thomas Reggi Date: Wed, 4 Sep 2024 14:26:17 -0400 Subject: [PATCH 04/21] devEngines v1 --- lib/current-env.js | 31 ------- lib/dev-engines.js | 121 ++++++++++++++++++++++++++ lib/env.js | 40 +++++++-- lib/index.js | 110 +++--------------------- package.json | 2 +- test/check-dev.js | 55 ------------ test/parse-dev-engine.js | 178 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 346 insertions(+), 191 deletions(-) delete mode 100644 lib/current-env.js create mode 100644 lib/dev-engines.js delete mode 100644 test/check-dev.js create mode 100644 test/parse-dev-engine.js diff --git a/lib/current-env.js b/lib/current-env.js deleted file mode 100644 index e676bcd..0000000 --- a/lib/current-env.js +++ /dev/null @@ -1,31 +0,0 @@ -const process = require('node:process') - -function os () { - return process.platform -} - -function cpu () { - return process.arch -} - -function isMusl (file) { - return file.includes('libc.musl-') || file.includes('ld-musl-') -} - -// libc checks only work in linux, environment and os check needs to happen out of band from this function -function libcFamily () { - let family = null - const report = process.report.getReport() - if (report.header?.glibcVersionRuntime) { - family = 'glibc' - } else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) { - family = 'musl' - } - return family -} - -module.exports = { - cpu, - libcFamily, - os, -} diff --git a/lib/dev-engines.js b/lib/dev-engines.js new file mode 100644 index 0000000..c935712 --- /dev/null +++ b/lib/dev-engines.js @@ -0,0 +1,121 @@ +const satisfies = require('semver/functions/satisfies') +const validRange = require('semver/ranges/valid') + +const ON_FAIL_IGNORE = 'ignore' +const ON_FAIL_WARN = 'warn' +const ON_FAIL_ERROR = 'error' +const ON_FAIL_DOWNLOAD = 'download' +const recognizedOnFail = [ + ON_FAIL_IGNORE, + ON_FAIL_WARN, + ON_FAIL_ERROR, + ON_FAIL_DOWNLOAD, +] + +const PROP_NAME = 'name' +const PROP_VERSION = 'version' +const PROP_ONFAIL = 'onFail' +const PROP_DOWNLOAD = 'download' +const recognizedProperties = [ + PROP_NAME, + PROP_VERSION, + PROP_ONFAIL, + PROP_DOWNLOAD, +] + +const ENGINE_PACKAGE_MANAGER = 'packageManager' +const ENGINE_RUNTIME = 'runtime' +const ENGINE_CPU = 'cpu' +const ENGINE_LIBC = 'libc' +const ENGINE_OS = 'os' +const recognizedEngines = [ + ENGINE_PACKAGE_MANAGER, + ENGINE_RUNTIME, + ENGINE_CPU, + ENGINE_LIBC, + ENGINE_OS, +] + +function checkDevEnginesDep (wanted = {}, current = {}, opts = {}) { + const { engine } = opts + + const properties = Object.keys(wanted) + for (const prop of properties) { + if (!recognizedProperties.includes(prop)) { + throw new Error(`Invalid property "${prop}" for "${engine}"`) + } + } + + if (wanted.onFail && !recognizedOnFail.includes(wanted.onFail)) { + throw new Error(`Invalid onFail value "${wanted.onFail}" for "${engine}"`) + } + + if (!wanted.name) { + throw new Error(`Missing "name" property for "${engine}"`) + } + + if (!current.name) { + throw new Error(`Unable to determine "name" for "${engine}"`) + } + + if (wanted.name !== current.name) { + return new Error( + `Invalid name "${wanted.name}" does not match "${current.name}" for "${engine}"` + ) + } + + if (wanted.version) { + if (!current.version) { + throw new Error(`Unable to determine "version" for "${engine}" "${wanted.name}"`) + } + if (validRange(wanted.version)) { + if (!satisfies(current.version, wanted.version, opts.semver)) { + return new Error( + // eslint-disable-next-line max-len + `Invalid semver version "${wanted.version}" does not match "${current.version}" for "${engine}"` + ) + } + } else if (wanted.version !== current.version) { + return new Error( + `Invalid version "${wanted.version}" does not match "${current.version}" for "${engine}"` + ) + } + } + + return true +} + +function parseDevEngines (wanted = {}, current = {}, opts = {}) { + return Object.keys(wanted).map(engine => { + if (!recognizedEngines.includes(engine)) { + throw new Error(`Invalid property "${engine}"`) + } + const dependencyAsAuthored = wanted[engine] + const dependencies = [dependencyAsAuthored].flat() + const lastDependency = dependencies[dependencies.length - 1] + let onFail = lastDependency.onFail || ON_FAIL_ERROR + if (onFail === ON_FAIL_DOWNLOAD) { + onFail = ON_FAIL_ERROR + } + const currentEngine = current[engine] || {} + const depErrors = dependencies.map(dep => { + return checkDevEnginesDep(dep, currentEngine, { ...opts, engine }) + }) + if (!depErrors.includes(true)) { + return Object.assign(new Error(`Invalid engine "${engine}"`), { + errors: depErrors.filter(v => v !== true), + engine, + isWarn: onFail === ON_FAIL_WARN, + isError: onFail === ON_FAIL_ERROR, + current: currentEngine, + required: dependencyAsAuthored, + }) + } + return true + }).filter(v => v !== true) +} + +module.exports = { + checkDevEnginesDep, + parseDevEngines, +} diff --git a/lib/env.js b/lib/env.js index a67820a..c742c05 100644 --- a/lib/env.js +++ b/lib/env.js @@ -1,4 +1,9 @@ const process = require('node:process') +const nodeOs = require('node:os') + +function isMusl (file) { + return file.includes('libc.musl-') || file.includes('ld-musl-') +} function os () { return process.platform @@ -8,13 +13,8 @@ function cpu () { return process.arch } -function isMusl (file) { - return file.includes('libc.musl-') || file.includes('ld-musl-') -} - -// libc checks only work in linux, environment and os check needs to happen out of band from this function -function libcFamily (environment) { - let family = null +function libc () { + let family const report = process.report.getReport() if (report.header?.glibcVersionRuntime) { family = 'glibc' @@ -24,8 +24,32 @@ function libcFamily (environment) { return family } +function devEngines (env = {}) { + return { + cpu: { + name: env.cpu || cpu(), + }, + libc: { + name: env.libc || libc(), + }, + os: { + name: env.os || os(), + version: env.osVersion || nodeOs.release(), + }, + packageManager: { + name: 'npm', + version: env.npmVersion, + }, + runtime: { + name: 'node', + version: env.nodeVersion || process.version, + }, + } +} + module.exports = { cpu, - libcFamily, + libc, os, + devEngines, } diff --git a/lib/index.js b/lib/index.js index b942c64..b42080c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,74 +1,7 @@ -const satisfies = require('semver/functions/satisfies') -const validRange = require('semver/ranges/valid') -const currentEnv = require('./current-env.js') +const semver = require('semver') +const currentEnv = require('./env') -/* - -interface DevEngines { - cpu?: DevEngineDependency | DevEngineDependency[]; - libc?: DevEngineDependency | DevEngineDependency[]; - os?: DevEngineDependency | DevEngineDependency[]; - packageManager?: DevEngineDependency | DevEngineDependency[]; - runtime?: DevEngineDependency | DevEngineDependency[]; -} - -interface DevEngineDependency { - name: string; - version?: string; - onFail?: 'ignore' | 'warn' | 'error' | 'download'; - download?: { - url: string; - algorithm?: string; - digest?: string; - } -} - -*/ - -const envNames = ['packageManager', 'runtime', 'cpu', 'libc', 'os'] - -// Returns an object with the last failing entry of any given wanted entry, null if no failures -function checkDev (wanted = {}, current = {}, opts = {}) { - const failures = {} - for (const env of envNames) { - let wantedEnv = wanted[env] - if (wantedEnv) { - const currentEnv = current[env] - if (!Array.isArray(wantedEnv)) { - wantedEnv = [wantedEnv] - } - // In case of failure we return the last entry to fail so we walk backwards and return the first failure - for (let i = wantedEnv.length - 1; i > -1; i--) { - const w = wantedEnv[i] - if (!currentEnv) { - failures[env] = w - break - } - if (w.name !== currentEnv.name) { - failures[env] = w - break - } - if (w.version) { - if (validRange(w.version)) { - if (!satisfies(currentEnv.version, w.version, opts.semver)) { - failures[env] = w - break - } - } else if (currentEnv.version !== w.version) { - failures[env] = w - break - } - } - } - } - } - if (Object.keys(failures).length) { - return failures - } - return null -} - -function checkEngine (target, npmVer, nodeVer, force = false) { +const checkEngine = (target, npmVer, nodeVer, force = false) => { const nodev = force ? null : nodeVer const eng = target.engines const opt = { includePrerelease: true } @@ -76,8 +9,8 @@ function checkEngine (target, npmVer, nodeVer, force = false) { return } - const nodeFail = nodev && eng.node && !satisfies(nodev, eng.node, opt) - const npmFail = npmVer && eng.npm && !satisfies(npmVer, eng.npm, opt) + const nodeFail = nodev && eng.node && !semver.satisfies(nodev, eng.node, opt) + const npmFail = npmVer && eng.npm && !semver.satisfies(npmVer, eng.npm, opt) if (nodeFail || npmFail) { throw Object.assign(new Error('Unsupported engine'), { pkgid: target._id, @@ -88,37 +21,26 @@ function checkEngine (target, npmVer, nodeVer, force = false) { } } -function checkPlatform (target, force = false, environment = {}) { +const checkPlatform = (target, force = false, environment = {}) => { if (force) { return } - const platform = environment.os || currentEnv.os() + const os = environment.os || currentEnv.os() const cpu = environment.cpu || currentEnv.cpu() - const osOk = target.os ? checkList(platform, target.os) : true - const cpuOk = target.cpu ? checkList(cpu, target.cpu) : true + const libc = environment.libc || currentEnv.libc() - let libcOk = true - let libcFamily = null - if (target.libc) { - // libc checks only work in linux, any value is a failure if we aren't - if (environment.libc) { - libcOk = checkList(environment.libc, target.libc) - } else if (platform !== 'linux') { - libcOk = false - } else { - libcFamily = currentEnv.libcFamily() - libcOk = libcFamily ? checkList(libcFamily, target.libc) : false - } - } + const osOk = target.os ? checkList(os, target.os) : true + const cpuOk = target.cpu ? checkList(cpu, target.cpu) : true + const libcOk = target.libc ? !libc ? false : checkList(libc, target.libc) : true if (!osOk || !cpuOk || !libcOk) { throw Object.assign(new Error('Unsupported platform'), { pkgid: target._id, current: { - os: platform, + os, cpu, - libc: libcFamily, + libc, }, required: { os: target.os, @@ -130,7 +52,7 @@ function checkPlatform (target, force = false, environment = {}) { } } -function checkList (value, list) { +const checkList = (value, list) => { if (typeof list === 'string') { list = [list] } @@ -157,10 +79,6 @@ function checkList (value, list) { } module.exports = { - // Used by npm-pick-manifest, "npm install -g npm", and arborist build-ideal-tree/reify checkEngine, - // used by arborist build-ideal-tree/reify checkPlatform, - // used by npm install for devEngines - checkDev, } diff --git a/package.json b/package.json index a9f0616..c41d27c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "type": "commonjs", "dependencies": { "proc-log": "^4.2.0", - "semver": "^7.1.1" + "semver": "^7.6.2" }, "devDependencies": { "@npmcli/eslint-config": "^4.0.0", diff --git a/test/check-dev.js b/test/check-dev.js deleted file mode 100644 index 657c7db..0000000 --- a/test/check-dev.js +++ /dev/null @@ -1,55 +0,0 @@ -const t = require('tap') -const { checkDev } = require('..') - -t.test('empty params', async t => { - t.equal(checkDev(), null) -}) - -t.test('tests all the right fields', async t => { - for (const env of ['packageManager', 'runtime', 'cpu', 'libc', 'os']) { - t.test(`field - ${env}`, async t => { - t.test('current name does not match, wanted has extra attribute', async t => { - const wanted = { name: `test-${env}-wanted`, extra: `test-${env}-extra` } - const current = { name: `test-${env}-current` } - t.same(checkDev({ [env]: wanted }, { [env]: current }), { [env]: wanted }) - }) - t.test('current is not given', async t => { - const wanted = { name: `test-${env}-wanted` } - t.same(checkDev({ [env]: wanted }), { [env]: wanted }) - }) - t.test('name only', async t => { - const wanted = { name: 'test-name' } - const current = { name: 'test-name' } - t.same(checkDev({ [env]: wanted }, { [env]: current}), null) - }) - t.test('non-semver version is not the same', async t => { - const wanted = { name: `test-name`, version: 'test-version-wanted' } - const current = { name: `test-name`, version: 'test-version-current' } - t.same(checkDev({ [env]: wanted }, { [env]: current }), { [env]: wanted }) - }) - t.test('non-semver version is the same', async t => { - const wanted = { name: `test-name`, version: 'test-version' } - const current = { name: `test-name`, version: 'test-version' } - t.same(checkDev({ [env]: wanted }, { [env]: current }), null) - }) - t.test('semver version is not in range', async t => { - const wanted = { name: `test-name`, version: '^1.0.0' } - const current = { name: `test-name`, version: '2.0.0' } - t.same(checkDev({ [env]: wanted }, { [env]: current }), { [env]: wanted }) - }) - t.test('semver version is in range', async t => { - const wanted = { name: `test-name`, version: '^1.0.0' } - const current = { name: `test-name`, version: '1.0.0' } - t.same(checkDev({ [env]: wanted }, { [env]: current }), null) - }) - t.test('returns the last failure', async t => { - const wanted = [ - { name: `test-name`, version: 'test-version-one' }, - { name: `test-name`, version: 'test-version-two' }, - ] - const current = { name: `test-name`, version: 'test-version-three' } - t.same(checkDev({ [env]: wanted }, { [env]: current }), { [env]: wanted[1] }) - }) - }) - } -}) diff --git a/test/parse-dev-engine.js b/test/parse-dev-engine.js new file mode 100644 index 0000000..982c47f --- /dev/null +++ b/test/parse-dev-engine.js @@ -0,0 +1,178 @@ +const t = require('tap') +const { parseDevEngines, checkDevEnginesDep } = require('../lib/dev-engines') +const { devEngines } = require('../lib/env') + +t.test('unrecognized property', async t => { + const wanted = { name: `alpha`, version: '1' } + const current = { name: `alpha` } + t.throws( + () => parseDevEngines({ unrecognized: wanted }, { os: current }), + new Error('Invalid property "unrecognized"') + ) +}) + +t.test('empty devEngines', async t => { + t.same(parseDevEngines({ }, { os: { name: `darwin` } }), []) +}) + +t.test('invalid name', async t => { + const wanted = { name: `alpha`, onFail: 'download' } + const current = { name: `beta` } + t.same(parseDevEngines({ os: wanted }, { os: current }), [ + Object.assign(new Error(`Invalid engine "os"`), { + errors: [ + new Error(`Invalid name "alpha" does not match "beta" for "os"`), + ], + engine: 'os', + isWarn: false, + isError: true, + current, + required: wanted, + }), + ]) +}) + +t.test('no arguments', async t => { + t.throws(() => checkDevEnginesDep()) + t.same(parseDevEngines(), []) +}) + +t.test('default options', async t => { + t.same(parseDevEngines({}, devEngines()), []) +}) + +t.test('tests all the right fields', async t => { + for (const env of ['packageManager', 'runtime', 'cpu', 'libc', 'os']) { + t.test(`field - ${env}`, async t => { + t.test('current name does not match, wanted has extra attribute', async t => { + const wanted = { name: `test-${env}-wanted`, extra: `test-${env}-extra` } + const current = { name: `test-${env}-current` } + t.throws( + () => parseDevEngines({ [env]: wanted }, { [env]: current }), + new Error(`Invalid property "extra" for "${env}"`) + ) + }) + t.test('current is not given', async t => { + const wanted = { name: `test-${env}-wanted` } + t.throws( + () => parseDevEngines({ [env]: wanted }), + new Error(`Unable to determine "name" for "${env}"`) + ) + }) + t.test('name only', async t => { + const wanted = { name: 'test-name' } + const current = { name: 'test-name' } + t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), []) + }) + t.test('non-semver version is not the same', async t => { + const wanted = { name: `test-name`, version: 'test-version-wanted' } + const current = { name: `test-name`, version: 'test-version-current' } + t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), [ + Object.assign(new Error(`Invalid engine "${env}"`), { + errors: [ + // eslint-disable-next-line max-len + new Error(`Invalid version "test-version-wanted" does not match "test-version-current" for "${env}"`), + ], + engine: env, + isWarn: false, + isError: true, + current: { name: `test-name`, version: 'test-version-current' }, + required: { name: `test-name`, version: 'test-version-wanted' }, + }), + ]) + }) + t.test('non-semver version is the same', async t => { + const wanted = { name: `test-name`, version: 'test-version' } + const current = { name: `test-name`, version: 'test-version' } + t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), []) + }) + t.test('semver version is not in range', async t => { + const wanted = { name: `test-name`, version: '^1.0.0' } + const current = { name: `test-name`, version: '2.0.0' } + t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), [ + Object.assign(new Error(`Invalid engine "${env}"`), { + errors: [ + // eslint-disable-next-line max-len + new Error(`Invalid semver version "^1.0.0" does not match "2.0.0" for "${env}"`), + ], + engine: env, + isWarn: false, + isError: true, + current: { name: `test-name`, version: '2.0.0' }, + required: { name: `test-name`, version: '^1.0.0' }, + }), + ]) + }) + t.test('semver version is in range', async t => { + const wanted = { name: `test-name`, version: '^1.0.0' } + const current = { name: `test-name`, version: '1.0.0' } + t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), []) + }) + t.test('returns the last failure', async t => { + const wanted = [ + { name: `test-name`, version: 'test-version-one' }, + { name: `test-name`, version: 'test-version-two' }, + ] + const current = { name: `test-name`, version: 'test-version-three' } + t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), [ + Object.assign(new Error(`Invalid engine "${env}"`), { + errors: [ + // eslint-disable-next-line max-len + new Error(`Invalid version "test-version-one" does not match "test-version-three" for "${env}"`), + // eslint-disable-next-line max-len + new Error(`Invalid version "test-version-two" does not match "test-version-three" for "${env}"`), + ], + engine: env, + isWarn: false, + isError: true, + current: { name: `test-name`, version: 'test-version-three' }, + required: [ + { name: `test-name`, version: 'test-version-one' }, + { name: `test-name`, version: 'test-version-two' }, + ], + }), + ]) + }) + t.test('unrecognized onFail', async t => { + const wanted = { name: `test-name`, version: '^1.0.0', onFail: 'unrecognized' } + const current = { name: `test-name`, version: '1.0.0' } + t.throws( + () => parseDevEngines({ [env]: wanted }, { [env]: current }), + new Error(`Invalid onFail value "unrecognized" for "${env}"`) + ) + }) + t.test('missing name', async t => { + const wanted = { version: '^1.0.0' } + const current = { name: `test-name`, version: '1.0.0' } + t.throws( + () => parseDevEngines({ [env]: wanted }, { [env]: current }), + new Error(`Missing "name" property for "${env}"`) + ) + }) + t.test('invalid name', async t => { + const wanted = { name: `alpha` } + const current = { name: `beta` } + t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), [ + Object.assign(new Error(`Invalid engine "${env}"`), { + errors: [ + new Error(`Invalid name "alpha" does not match "beta" for "${env}"`), + ], + engine: env, + isWarn: false, + isError: true, + current, + required: wanted, + }), + ]) + }) + t.test('missing version', async t => { + const wanted = { name: `alpha`, version: '1' } + const current = { name: `alpha` } + t.throws( + () => parseDevEngines({ [env]: wanted }, { [env]: current }), + new Error(`Unable to determine "version" for "${env}" "alpha"`) + ) + }) + }) + } +}) From 52f82d0e5fb0bad068f2208656aec944aef143c1 Mon Sep 17 00:00:00 2001 From: Thomas Reggi Date: Wed, 4 Sep 2024 15:37:02 -0400 Subject: [PATCH 05/21] accidental prop --- lib/dev-engines.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/dev-engines.js b/lib/dev-engines.js index c935712..1e6d396 100644 --- a/lib/dev-engines.js +++ b/lib/dev-engines.js @@ -15,12 +15,10 @@ const recognizedOnFail = [ const PROP_NAME = 'name' const PROP_VERSION = 'version' const PROP_ONFAIL = 'onFail' -const PROP_DOWNLOAD = 'download' const recognizedProperties = [ PROP_NAME, PROP_VERSION, PROP_ONFAIL, - PROP_DOWNLOAD, ] const ENGINE_PACKAGE_MANAGER = 'packageManager' From 86d04fa0bdee0d1213da688442436746940eb99c Mon Sep 17 00:00:00 2001 From: Thomas Reggi Date: Wed, 4 Sep 2024 15:51:15 -0400 Subject: [PATCH 06/21] remove constants --- lib/dev-engines.js | 46 +++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/lib/dev-engines.js b/lib/dev-engines.js index 1e6d396..3e914b0 100644 --- a/lib/dev-engines.js +++ b/lib/dev-engines.js @@ -1,37 +1,25 @@ const satisfies = require('semver/functions/satisfies') const validRange = require('semver/ranges/valid') -const ON_FAIL_IGNORE = 'ignore' -const ON_FAIL_WARN = 'warn' -const ON_FAIL_ERROR = 'error' -const ON_FAIL_DOWNLOAD = 'download' const recognizedOnFail = [ - ON_FAIL_IGNORE, - ON_FAIL_WARN, - ON_FAIL_ERROR, - ON_FAIL_DOWNLOAD, + 'ignore', + 'warn', + 'error', + 'download', ] -const PROP_NAME = 'name' -const PROP_VERSION = 'version' -const PROP_ONFAIL = 'onFail' const recognizedProperties = [ - PROP_NAME, - PROP_VERSION, - PROP_ONFAIL, + 'name', + 'version', + 'onFail', ] -const ENGINE_PACKAGE_MANAGER = 'packageManager' -const ENGINE_RUNTIME = 'runtime' -const ENGINE_CPU = 'cpu' -const ENGINE_LIBC = 'libc' -const ENGINE_OS = 'os' const recognizedEngines = [ - ENGINE_PACKAGE_MANAGER, - ENGINE_RUNTIME, - ENGINE_CPU, - ENGINE_LIBC, - ENGINE_OS, + 'packageManager', + 'runtime', + 'cpu', + 'libc', + 'os', ] function checkDevEnginesDep (wanted = {}, current = {}, opts = {}) { @@ -91,9 +79,9 @@ function parseDevEngines (wanted = {}, current = {}, opts = {}) { const dependencyAsAuthored = wanted[engine] const dependencies = [dependencyAsAuthored].flat() const lastDependency = dependencies[dependencies.length - 1] - let onFail = lastDependency.onFail || ON_FAIL_ERROR - if (onFail === ON_FAIL_DOWNLOAD) { - onFail = ON_FAIL_ERROR + let onFail = lastDependency.onFail || 'error' + if (onFail === 'download') { + onFail = 'error' } const currentEngine = current[engine] || {} const depErrors = dependencies.map(dep => { @@ -103,8 +91,8 @@ function parseDevEngines (wanted = {}, current = {}, opts = {}) { return Object.assign(new Error(`Invalid engine "${engine}"`), { errors: depErrors.filter(v => v !== true), engine, - isWarn: onFail === ON_FAIL_WARN, - isError: onFail === ON_FAIL_ERROR, + isWarn: onFail === 'warn', + isError: onFail === 'error', current: currentEngine, required: dependencyAsAuthored, }) From ce64c7f911cf27c2000330d86440b67957b37541 Mon Sep 17 00:00:00 2001 From: Thomas Reggi Date: Wed, 4 Sep 2024 15:59:50 -0400 Subject: [PATCH 07/21] remove unnecessary export --- lib/dev-engines.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/dev-engines.js b/lib/dev-engines.js index 3e914b0..3223306 100644 --- a/lib/dev-engines.js +++ b/lib/dev-engines.js @@ -22,7 +22,7 @@ const recognizedEngines = [ 'os', ] -function checkDevEnginesDep (wanted = {}, current = {}, opts = {}) { +function checkDevEnginesDep (wanted, current, opts) { const { engine } = opts const properties = Object.keys(wanted) @@ -102,6 +102,5 @@ function parseDevEngines (wanted = {}, current = {}, opts = {}) { } module.exports = { - checkDevEnginesDep, parseDevEngines, } From 432cd0103a35adaf4448fa0d72b30e76ae0e8d00 Mon Sep 17 00:00:00 2001 From: Thomas Reggi Date: Wed, 4 Sep 2024 16:25:48 -0400 Subject: [PATCH 08/21] only run libc check on linux --- lib/env.js | 11 ++++++++--- lib/index.js | 7 +++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/env.js b/lib/env.js index c742c05..f9d44f0 100644 --- a/lib/env.js +++ b/lib/env.js @@ -13,7 +13,11 @@ function cpu () { return process.arch } -function libc () { +function libc (osName) { + // this is to make it faster on non linux machines + if (osName !== 'linux') { + return undefined + } let family const report = process.report.getReport() if (report.header?.glibcVersionRuntime) { @@ -25,15 +29,16 @@ function libc () { } function devEngines (env = {}) { + const osName = env.os || os() return { cpu: { name: env.cpu || cpu(), }, libc: { - name: env.libc || libc(), + name: env.libc || libc(osName), }, os: { - name: env.os || os(), + name: osName, version: env.osVersion || nodeOs.release(), }, packageManager: { diff --git a/lib/index.js b/lib/index.js index b42080c..6c4308d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -28,11 +28,14 @@ const checkPlatform = (target, force = false, environment = {}) => { const os = environment.os || currentEnv.os() const cpu = environment.cpu || currentEnv.cpu() - const libc = environment.libc || currentEnv.libc() + const libc = environment.libc || currentEnv.libc(os) const osOk = target.os ? checkList(os, target.os) : true const cpuOk = target.cpu ? checkList(cpu, target.cpu) : true - const libcOk = target.libc ? !libc ? false : checkList(libc, target.libc) : true + let libcOk = target.libc ? checkList(libc, target.libc) : true + if (target.libc && !libc) { + libcOk = false + } if (!osOk || !cpuOk || !libcOk) { throw Object.assign(new Error('Unsupported platform'), { From ecf7da41b46252e626d5b464ccb1eeab6353a2a6 Mon Sep 17 00:00:00 2001 From: Thomas Reggi Date: Wed, 4 Sep 2024 16:40:19 -0400 Subject: [PATCH 09/21] guard against non string values --- lib/dev-engines.js | 27 ++++++++++++++------ test/parse-dev-engine.js | 54 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/lib/dev-engines.js b/lib/dev-engines.js index 3223306..9a42f16 100644 --- a/lib/dev-engines.js +++ b/lib/dev-engines.js @@ -26,32 +26,45 @@ function checkDevEnginesDep (wanted, current, opts) { const { engine } = opts const properties = Object.keys(wanted) + for (const prop of properties) { if (!recognizedProperties.includes(prop)) { throw new Error(`Invalid property "${prop}" for "${engine}"`) } } - if (wanted.onFail && !recognizedOnFail.includes(wanted.onFail)) { - throw new Error(`Invalid onFail value "${wanted.onFail}" for "${engine}"`) + if (!properties.includes('name')) { + throw new Error(`Missing "name" property for "${engine}"`) } - if (!wanted.name) { - throw new Error(`Missing "name" property for "${engine}"`) + if (typeof wanted.name !== 'string') { + throw new Error(`Invalid non-string value for "name" within "${engine}"`) } - if (!current.name) { + if (typeof current.name !== 'string') { throw new Error(`Unable to determine "name" for "${engine}"`) } + if (properties.includes('onFail')) { + if (typeof wanted.onFail !== 'string') { + throw new Error(`Invalid non-string value for "onFail" within "${engine}"`) + } + if (!recognizedOnFail.includes(wanted.onFail)) { + throw new Error(`Invalid onFail value "${wanted.onFail}" for "${engine}"`) + } + } + if (wanted.name !== current.name) { return new Error( `Invalid name "${wanted.name}" does not match "${current.name}" for "${engine}"` ) } - if (wanted.version) { - if (!current.version) { + if (properties.includes('version')) { + if (typeof wanted.version !== 'string') { + throw new Error(`Invalid non-string value for "version" within "${engine}"`) + } + if (typeof current.version !== 'string') { throw new Error(`Unable to determine "version" for "${engine}" "${wanted.name}"`) } if (validRange(wanted.version)) { diff --git a/test/parse-dev-engine.js b/test/parse-dev-engine.js index 982c47f..714e20e 100644 --- a/test/parse-dev-engine.js +++ b/test/parse-dev-engine.js @@ -41,6 +41,60 @@ t.test('default options', async t => { t.same(parseDevEngines({}, devEngines()), []) }) +t.test('tests all the right fields', async t => { + for (const nonString of [1, true, false, null, undefined, {}, []]) { + t.test('invalid name value', async t => { + t.throws( + () => parseDevEngines({ + runtime: { + name: nonString, + version: '14', + }, + }, { + runtime: { + name: 'nondescript', + version: '14', + }, + }), + new Error(`Invalid non-string value for "name" within "runtime"`) + ) + }) + t.test('invalid version value', async t => { + t.throws( + () => parseDevEngines({ + runtime: { + name: 'nondescript', + version: nonString, + }, + }, { + runtime: { + name: 'nondescript', + version: '14', + }, + }), + new Error(`Invalid non-string value for "version" within "runtime"`) + ) + }) + t.test('invalid onFail value', async t => { + t.throws( + () => parseDevEngines({ + runtime: { + name: 'nondescript', + version: '14', + onFail: nonString, + }, + }, { + runtime: { + name: 'nondescript', + version: '14', + }, + }), + new Error(`Invalid non-string value for "onFail" within "runtime"`) + ) + }) + } +}) + t.test('tests all the right fields', async t => { for (const env of ['packageManager', 'runtime', 'cpu', 'libc', 'os']) { t.test(`field - ${env}`, async t => { From e02ccf57c38519fef6bda1e1530c68374881670c Mon Sep 17 00:00:00 2001 From: reggi Date: Wed, 4 Sep 2024 17:00:16 -0400 Subject: [PATCH 10/21] removed filters --- lib/dev-engines.js | 51 +++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/dev-engines.js b/lib/dev-engines.js index 9a42f16..4fa01d3 100644 --- a/lib/dev-engines.js +++ b/lib/dev-engines.js @@ -22,7 +22,8 @@ const recognizedEngines = [ 'os', ] -function checkDevEnginesDep (wanted, current, opts) { +/** checks a devEngine dependency */ +function checkDependency (wanted, current, opts) { const { engine } = opts const properties = Object.keys(wanted) @@ -41,7 +42,7 @@ function checkDevEnginesDep (wanted, current, opts) { throw new Error(`Invalid non-string value for "name" within "${engine}"`) } - if (typeof current.name !== 'string') { + if (typeof current.name !== 'string' || current.name === '') { throw new Error(`Unable to determine "name" for "${engine}"`) } @@ -64,7 +65,7 @@ function checkDevEnginesDep (wanted, current, opts) { if (typeof wanted.version !== 'string') { throw new Error(`Invalid non-string value for "version" within "${engine}"`) } - if (typeof current.version !== 'string') { + if (typeof current.version !== 'string' || current.version === '') { throw new Error(`Unable to determine "version" for "${engine}" "${wanted.name}"`) } if (validRange(wanted.version)) { @@ -80,38 +81,50 @@ function checkDevEnginesDep (wanted, current, opts) { ) } } - - return true } +/** checks devEngines package property and returns array of warnings / errors */ function parseDevEngines (wanted = {}, current = {}, opts = {}) { - return Object.keys(wanted).map(engine => { + const errors = [] + + for (const engine of Object.keys(wanted)) { if (!recognizedEngines.includes(engine)) { throw new Error(`Invalid property "${engine}"`) } const dependencyAsAuthored = wanted[engine] const dependencies = [dependencyAsAuthored].flat() - const lastDependency = dependencies[dependencies.length - 1] - let onFail = lastDependency.onFail || 'error' - if (onFail === 'download') { - onFail = 'error' - } const currentEngine = current[engine] || {} - const depErrors = dependencies.map(dep => { - return checkDevEnginesDep(dep, currentEngine, { ...opts, engine }) - }) - if (!depErrors.includes(true)) { - return Object.assign(new Error(`Invalid engine "${engine}"`), { - errors: depErrors.filter(v => v !== true), + + const depErrors = [] + for (const dep of dependencies) { + const result = checkDependency(dep, currentEngine, { ...opts, engine }) + if (result) { + depErrors.push(result) + } + } + + const invalid = depErrors.length === dependencies.length + + if (invalid) { + const lastDependency = dependencies[dependencies.length - 1] + let onFail = lastDependency.onFail || 'error' + if (onFail === 'download') { + onFail = 'error' + } + + const err = Object.assign(new Error(`Invalid engine "${engine}"`), { + errors: depErrors, engine, isWarn: onFail === 'warn', isError: onFail === 'error', current: currentEngine, required: dependencyAsAuthored, }) + + errors.push(err) } - return true - }).filter(v => v !== true) + } + return errors } module.exports = { From 29105a6d8613bbfd1de68eeeedff94194e6c4f6f Mon Sep 17 00:00:00 2001 From: reggi Date: Wed, 4 Sep 2024 17:25:17 -0400 Subject: [PATCH 11/21] guards against non-object values, and empty arrays --- lib/dev-engines.js | 9 +++++++++ test/parse-dev-engine.js | 27 ++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/dev-engines.js b/lib/dev-engines.js index 4fa01d3..9f6eee1 100644 --- a/lib/dev-engines.js +++ b/lib/dev-engines.js @@ -26,6 +26,10 @@ const recognizedEngines = [ function checkDependency (wanted, current, opts) { const { engine } = opts + if ((typeof wanted !== 'object' || wanted === null) || Array.isArray(wanted)) { + throw new Error(`Invalid non-object value for "${engine}"`) + } + const properties = Object.keys(wanted) for (const prop of properties) { @@ -95,6 +99,11 @@ function parseDevEngines (wanted = {}, current = {}, opts = {}) { const dependencies = [dependencyAsAuthored].flat() const currentEngine = current[engine] || {} + // this accounts for empty array eg { runtime: [] } and ignores it + if (dependencies.length === 0) { + break + } + const depErrors = [] for (const dep of dependencies) { const result = checkDependency(dep, currentEngine, { ...opts, engine }) diff --git a/test/parse-dev-engine.js b/test/parse-dev-engine.js index 714e20e..110c3c2 100644 --- a/test/parse-dev-engine.js +++ b/test/parse-dev-engine.js @@ -2,6 +2,12 @@ const t = require('tap') const { parseDevEngines, checkDevEnginesDep } = require('../lib/dev-engines') const { devEngines } = require('../lib/env') +t.test('noop options', async t => { + t.same(parseDevEngines({ + runtime: [], + }, devEngines()), []) +}) + t.test('unrecognized property', async t => { const wanted = { name: `alpha`, version: '1' } const current = { name: `alpha` } @@ -41,7 +47,26 @@ t.test('default options', async t => { t.same(parseDevEngines({}, devEngines()), []) }) -t.test('tests all the right fields', async t => { +t.test('tests non-object engine values', async t => { + const core = [1, true, false, null, undefined] + for (const nonString of [...core, [[]], ...core.map(v => [v])]) { + t.test('invalid engine property', async t => { + t.throws( + () => parseDevEngines({ + runtime: nonString, + }, { + runtime: { + name: 'nondescript', + version: '14', + }, + }), + new Error(`Invalid non-object value for "runtime"`) + ) + }) + } +}) + +t.test('tests non-string dep values ', async t => { for (const nonString of [1, true, false, null, undefined, {}, []]) { t.test('invalid name value', async t => { t.throws( From 8cf4e48554f5aac4d78639c9d8f2c14b081325a1 Mon Sep 17 00:00:00 2001 From: reggi Date: Wed, 4 Sep 2024 17:35:15 -0400 Subject: [PATCH 12/21] added spec 1, 2 --- test/parse-dev-engine.js | 131 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/test/parse-dev-engine.js b/test/parse-dev-engine.js index 110c3c2..90dec8d 100644 --- a/test/parse-dev-engine.js +++ b/test/parse-dev-engine.js @@ -255,3 +255,134 @@ t.test('tests all the right fields', async t => { }) } }) + +t.test('spec 1', async t => { + const example = { + runtime: { + name: 'node', + version: '>= 20.0.0', + onFail: 'error', + }, + packageManager: { + name: 'yarn', + version: '3.2.3', + onFail: 'download', + }, + } + + t.same(parseDevEngines(example, { + os: { name: 'darwin', version: '23.0.0' }, + cpu: { name: 'arm' }, + libc: { name: 'glibc' }, + runtime: { name: 'node', version: '20.0.0' }, + packageManager: { name: 'yarn', version: '3.2.3' }, + }), []) +}) + +t.test('spec 2', async t => { + const example = { + os: { + name: 'darwin', + version: '>= 23.0.0', + }, + cpu: [ + { + name: 'arm', + }, + { + name: 'x86', + }, + ], + libc: { + name: 'glibc', + }, + runtime: [ + { + name: 'bun', + version: '>= 1.0.0', + onFail: 'ignore', + }, + { + name: 'node', + version: '>= 20.0.0', + onFail: 'error', + }, + ], + packageManager: [ + { + name: 'bun', + version: '>= 1.0.0', + onFail: 'ignore', + }, + { + name: 'yarn', + version: '3.2.3', + onFail: 'download', + }, + ], + } + + t.same(parseDevEngines(example, { + os: { name: 'darwin', version: '23.0.0' }, + cpu: { name: 'arm' }, + libc: { name: 'glibc' }, + runtime: { name: 'node', version: '20.0.0' }, + packageManager: { name: 'yarn', version: '3.2.3' }, + }), []) + + t.same(parseDevEngines(example, { + os: { name: 'darwin', version: '10.0.0' }, + cpu: { name: 'arm' }, + libc: { name: 'glibc' }, + runtime: { name: 'node', version: '20.0.0' }, + packageManager: { name: 'yarn', version: '3.2.3' }, + }), [ + Object.assign(new Error(`Invalid engine "os"`), { + errors: [ + // eslint-disable-next-line max-len + new Error(`Invalid semver version ">= 23.0.0" does not match "10.0.0" for "os"`), + ], + engine: 'os', + isWarn: false, + isError: true, + current: { name: 'darwin', version: '10.0.0' }, + required: { + name: 'darwin', + version: '>= 23.0.0', + }, + }), + ]) + + t.same(parseDevEngines(example, { + os: { name: 'darwin', version: '23.0.0' }, + cpu: { name: 'arm' }, + libc: { name: 'glibc' }, + runtime: { name: 'nondescript', version: '20.0.0' }, + packageManager: { name: 'yarn', version: '3.2.3' }, + }), [ + Object.assign(new Error(`Invalid engine "runtime"`), { + errors: [ + // eslint-disable-next-line max-len + new Error(`Invalid name "bun" does not match "nondescript" for "runtime"`), + // eslint-disable-next-line max-len + new Error(`Invalid name "node" does not match "nondescript" for "runtime"`), + ], + engine: 'runtime', + isWarn: false, + isError: true, + current: { name: 'nondescript', version: '20.0.0' }, + required: [ + { + name: 'bun', + version: '>= 1.0.0', + onFail: 'ignore', + }, + { + name: 'node', + version: '>= 20.0.0', + onFail: 'error', + }, + ], + }), + ]) +}) From c9a04e0fde662f1253653ee06bb1bc54c3dba2f3 Mon Sep 17 00:00:00 2001 From: reggi Date: Wed, 4 Sep 2024 17:52:45 -0400 Subject: [PATCH 13/21] top level value type check --- lib/dev-engines.js | 6 +++++- test/parse-dev-engine.js | 27 +++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/dev-engines.js b/lib/dev-engines.js index 9f6eee1..000faf9 100644 --- a/lib/dev-engines.js +++ b/lib/dev-engines.js @@ -88,7 +88,11 @@ function checkDependency (wanted, current, opts) { } /** checks devEngines package property and returns array of warnings / errors */ -function parseDevEngines (wanted = {}, current = {}, opts = {}) { +function parseDevEngines (wanted, current = {}, opts = {}) { + if ((typeof wanted !== 'object' || wanted === null) || Array.isArray(wanted)) { + throw new Error(`Invalid non-object value for devEngines`) + } + const errors = [] for (const engine of Object.keys(wanted)) { diff --git a/test/parse-dev-engine.js b/test/parse-dev-engine.js index 90dec8d..3421dec 100644 --- a/test/parse-dev-engine.js +++ b/test/parse-dev-engine.js @@ -1,5 +1,5 @@ const t = require('tap') -const { parseDevEngines, checkDevEnginesDep } = require('../lib/dev-engines') +const { parseDevEngines } = require('../lib/dev-engines') const { devEngines } = require('../lib/env') t.test('noop options', async t => { @@ -38,22 +38,29 @@ t.test('invalid name', async t => { ]) }) -t.test('no arguments', async t => { - t.throws(() => checkDevEnginesDep()) - t.same(parseDevEngines(), []) -}) - t.test('default options', async t => { t.same(parseDevEngines({}, devEngines()), []) }) -t.test('tests non-object engine values', async t => { +t.test('tests non-object', async t => { const core = [1, true, false, null, undefined] - for (const nonString of [...core, [[]], ...core.map(v => [v])]) { + for (const nonObject of [...core, [[]], ...core.map(v => [v])]) { + t.test('invalid devEngines', async t => { + t.throws( + () => parseDevEngines(nonObject, { + runtime: { + name: 'nondescript', + version: '14', + }, + }), + new Error(`Invalid non-object value for devEngines`) + ) + }) + t.test('invalid engine property', async t => { t.throws( () => parseDevEngines({ - runtime: nonString, + runtime: nonObject, }, { runtime: { name: 'nondescript', @@ -66,7 +73,7 @@ t.test('tests non-object engine values', async t => { } }) -t.test('tests non-string dep values ', async t => { +t.test('tests non-string ', async t => { for (const nonString of [1, true, false, null, undefined, {}, []]) { t.test('invalid name value', async t => { t.throws( From 3a35dd86933acc7c539c8eb3964d2269b0b63a32 Mon Sep 17 00:00:00 2001 From: reggi Date: Thu, 5 Sep 2024 10:41:48 -0400 Subject: [PATCH 14/21] check > parse, export everything, import index in tests --- lib/.DS_Store | Bin 0 -> 6148 bytes lib/dev-engines.js | 4 +- lib/index.js | 3 + ...rse-dev-engine.js => check-dev-engines.js} | 57 +++++++++--------- 4 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 lib/.DS_Store rename test/{parse-dev-engine.js => check-dev-engines.js} (88%) diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 { const nodev = force ? null : nodeVer @@ -84,4 +85,6 @@ const checkList = (value, list) => { module.exports = { checkEngine, checkPlatform, + checkDevEngines, + currentEnv, } diff --git a/test/parse-dev-engine.js b/test/check-dev-engines.js similarity index 88% rename from test/parse-dev-engine.js rename to test/check-dev-engines.js index 3421dec..6cafca9 100644 --- a/test/parse-dev-engine.js +++ b/test/check-dev-engines.js @@ -1,30 +1,29 @@ const t = require('tap') -const { parseDevEngines } = require('../lib/dev-engines') -const { devEngines } = require('../lib/env') +const { checkDevEngines, currentEnv } = require('..') t.test('noop options', async t => { - t.same(parseDevEngines({ + t.same(checkDevEngines({ runtime: [], - }, devEngines()), []) + }, currentEnv.devEngines()), []) }) t.test('unrecognized property', async t => { const wanted = { name: `alpha`, version: '1' } const current = { name: `alpha` } t.throws( - () => parseDevEngines({ unrecognized: wanted }, { os: current }), + () => checkDevEngines({ unrecognized: wanted }, { os: current }), new Error('Invalid property "unrecognized"') ) }) t.test('empty devEngines', async t => { - t.same(parseDevEngines({ }, { os: { name: `darwin` } }), []) + t.same(checkDevEngines({ }, { os: { name: `darwin` } }), []) }) t.test('invalid name', async t => { const wanted = { name: `alpha`, onFail: 'download' } const current = { name: `beta` } - t.same(parseDevEngines({ os: wanted }, { os: current }), [ + t.same(checkDevEngines({ os: wanted }, { os: current }), [ Object.assign(new Error(`Invalid engine "os"`), { errors: [ new Error(`Invalid name "alpha" does not match "beta" for "os"`), @@ -39,7 +38,7 @@ t.test('invalid name', async t => { }) t.test('default options', async t => { - t.same(parseDevEngines({}, devEngines()), []) + t.same(checkDevEngines({}, currentEnv.devEngines()), []) }) t.test('tests non-object', async t => { @@ -47,7 +46,7 @@ t.test('tests non-object', async t => { for (const nonObject of [...core, [[]], ...core.map(v => [v])]) { t.test('invalid devEngines', async t => { t.throws( - () => parseDevEngines(nonObject, { + () => checkDevEngines(nonObject, { runtime: { name: 'nondescript', version: '14', @@ -59,7 +58,7 @@ t.test('tests non-object', async t => { t.test('invalid engine property', async t => { t.throws( - () => parseDevEngines({ + () => checkDevEngines({ runtime: nonObject, }, { runtime: { @@ -77,7 +76,7 @@ t.test('tests non-string ', async t => { for (const nonString of [1, true, false, null, undefined, {}, []]) { t.test('invalid name value', async t => { t.throws( - () => parseDevEngines({ + () => checkDevEngines({ runtime: { name: nonString, version: '14', @@ -93,7 +92,7 @@ t.test('tests non-string ', async t => { }) t.test('invalid version value', async t => { t.throws( - () => parseDevEngines({ + () => checkDevEngines({ runtime: { name: 'nondescript', version: nonString, @@ -109,7 +108,7 @@ t.test('tests non-string ', async t => { }) t.test('invalid onFail value', async t => { t.throws( - () => parseDevEngines({ + () => checkDevEngines({ runtime: { name: 'nondescript', version: '14', @@ -134,26 +133,26 @@ t.test('tests all the right fields', async t => { const wanted = { name: `test-${env}-wanted`, extra: `test-${env}-extra` } const current = { name: `test-${env}-current` } t.throws( - () => parseDevEngines({ [env]: wanted }, { [env]: current }), + () => checkDevEngines({ [env]: wanted }, { [env]: current }), new Error(`Invalid property "extra" for "${env}"`) ) }) t.test('current is not given', async t => { const wanted = { name: `test-${env}-wanted` } t.throws( - () => parseDevEngines({ [env]: wanted }), + () => checkDevEngines({ [env]: wanted }), new Error(`Unable to determine "name" for "${env}"`) ) }) t.test('name only', async t => { const wanted = { name: 'test-name' } const current = { name: 'test-name' } - t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), []) + t.same(checkDevEngines({ [env]: wanted }, { [env]: current }), []) }) t.test('non-semver version is not the same', async t => { const wanted = { name: `test-name`, version: 'test-version-wanted' } const current = { name: `test-name`, version: 'test-version-current' } - t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), [ + t.same(checkDevEngines({ [env]: wanted }, { [env]: current }), [ Object.assign(new Error(`Invalid engine "${env}"`), { errors: [ // eslint-disable-next-line max-len @@ -170,12 +169,12 @@ t.test('tests all the right fields', async t => { t.test('non-semver version is the same', async t => { const wanted = { name: `test-name`, version: 'test-version' } const current = { name: `test-name`, version: 'test-version' } - t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), []) + t.same(checkDevEngines({ [env]: wanted }, { [env]: current }), []) }) t.test('semver version is not in range', async t => { const wanted = { name: `test-name`, version: '^1.0.0' } const current = { name: `test-name`, version: '2.0.0' } - t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), [ + t.same(checkDevEngines({ [env]: wanted }, { [env]: current }), [ Object.assign(new Error(`Invalid engine "${env}"`), { errors: [ // eslint-disable-next-line max-len @@ -192,7 +191,7 @@ t.test('tests all the right fields', async t => { t.test('semver version is in range', async t => { const wanted = { name: `test-name`, version: '^1.0.0' } const current = { name: `test-name`, version: '1.0.0' } - t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), []) + t.same(checkDevEngines({ [env]: wanted }, { [env]: current }), []) }) t.test('returns the last failure', async t => { const wanted = [ @@ -200,7 +199,7 @@ t.test('tests all the right fields', async t => { { name: `test-name`, version: 'test-version-two' }, ] const current = { name: `test-name`, version: 'test-version-three' } - t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), [ + t.same(checkDevEngines({ [env]: wanted }, { [env]: current }), [ Object.assign(new Error(`Invalid engine "${env}"`), { errors: [ // eslint-disable-next-line max-len @@ -223,7 +222,7 @@ t.test('tests all the right fields', async t => { const wanted = { name: `test-name`, version: '^1.0.0', onFail: 'unrecognized' } const current = { name: `test-name`, version: '1.0.0' } t.throws( - () => parseDevEngines({ [env]: wanted }, { [env]: current }), + () => checkDevEngines({ [env]: wanted }, { [env]: current }), new Error(`Invalid onFail value "unrecognized" for "${env}"`) ) }) @@ -231,14 +230,14 @@ t.test('tests all the right fields', async t => { const wanted = { version: '^1.0.0' } const current = { name: `test-name`, version: '1.0.0' } t.throws( - () => parseDevEngines({ [env]: wanted }, { [env]: current }), + () => checkDevEngines({ [env]: wanted }, { [env]: current }), new Error(`Missing "name" property for "${env}"`) ) }) t.test('invalid name', async t => { const wanted = { name: `alpha` } const current = { name: `beta` } - t.same(parseDevEngines({ [env]: wanted }, { [env]: current }), [ + t.same(checkDevEngines({ [env]: wanted }, { [env]: current }), [ Object.assign(new Error(`Invalid engine "${env}"`), { errors: [ new Error(`Invalid name "alpha" does not match "beta" for "${env}"`), @@ -255,7 +254,7 @@ t.test('tests all the right fields', async t => { const wanted = { name: `alpha`, version: '1' } const current = { name: `alpha` } t.throws( - () => parseDevEngines({ [env]: wanted }, { [env]: current }), + () => checkDevEngines({ [env]: wanted }, { [env]: current }), new Error(`Unable to determine "version" for "${env}" "alpha"`) ) }) @@ -277,7 +276,7 @@ t.test('spec 1', async t => { }, } - t.same(parseDevEngines(example, { + t.same(checkDevEngines(example, { os: { name: 'darwin', version: '23.0.0' }, cpu: { name: 'arm' }, libc: { name: 'glibc' }, @@ -329,7 +328,7 @@ t.test('spec 2', async t => { ], } - t.same(parseDevEngines(example, { + t.same(checkDevEngines(example, { os: { name: 'darwin', version: '23.0.0' }, cpu: { name: 'arm' }, libc: { name: 'glibc' }, @@ -337,7 +336,7 @@ t.test('spec 2', async t => { packageManager: { name: 'yarn', version: '3.2.3' }, }), []) - t.same(parseDevEngines(example, { + t.same(checkDevEngines(example, { os: { name: 'darwin', version: '10.0.0' }, cpu: { name: 'arm' }, libc: { name: 'glibc' }, @@ -360,7 +359,7 @@ t.test('spec 2', async t => { }), ]) - t.same(parseDevEngines(example, { + t.same(checkDevEngines(example, { os: { name: 'darwin', version: '23.0.0' }, cpu: { name: 'arm' }, libc: { name: 'glibc' }, From e68fb63260482b98ef32c7a57b6b4900f7f4f3e0 Mon Sep 17 00:00:00 2001 From: reggi Date: Thu, 5 Sep 2024 10:45:01 -0400 Subject: [PATCH 15/21] cleanup pr artifacts --- lib/.DS_Store | Bin 6148 -> 0 bytes package.json | 1 - test/check-platform.js | 1 - 3 files changed, 2 deletions(-) delete mode 100644 lib/.DS_Store diff --git a/lib/.DS_Store b/lib/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 From 36290386b7060fa42b46d6e2434aebd0c4b07e7a Mon Sep 17 00:00:00 2001 From: reggi Date: Thu, 5 Sep 2024 10:46:16 -0400 Subject: [PATCH 16/21] rename current-env --- lib/{env.js => current-env.js} | 0 lib/index.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/{env.js => current-env.js} (100%) diff --git a/lib/env.js b/lib/current-env.js similarity index 100% rename from lib/env.js rename to lib/current-env.js diff --git a/lib/index.js b/lib/index.js index b051b62..7170292 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,5 @@ const semver = require('semver') -const currentEnv = require('./env') +const currentEnv = require('./current-env') const { checkDevEngines } = require('./dev-engines') const checkEngine = (target, npmVer, nodeVer, force = false) => { From 7c15a8f30315d571583a5d14afe35f305ce08e40 Mon Sep 17 00:00:00 2001 From: reggi Date: Thu, 5 Sep 2024 11:56:31 -0400 Subject: [PATCH 17/21] rm commonjs --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 54d78f1..94d062a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "./lib/index.js" ] }, - "type": "commonjs", "dependencies": { "semver": "^7.6.2" }, From dd0c425b4ceee4b0241fc6930ddde87eeecfb0e8 Mon Sep 17 00:00:00 2001 From: reggi Date: Thu, 5 Sep 2024 12:34:33 -0400 Subject: [PATCH 18/21] revert package --- package.json | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 94d062a..3d73f34 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,10 @@ { "name": "npm-install-checks", - "version": "6.3.0", + "version": "7.0.0", "description": "Check the engines and platform fields in package.json", - "main": "./lib/index.js", - "exports": { - ".": [ - { - "default": "./lib/index.js" - }, - "./lib/index.js" - ] - }, + "main": "lib/index.js", "dependencies": { - "semver": "^7.6.2" + "semver": "^7.1.1" }, "devDependencies": { "@npmcli/eslint-config": "^5.0.0", From a1cc700bf2766c766e1685a16b19941a76b8edff Mon Sep 17 00:00:00 2001 From: reggi Date: Thu, 5 Sep 2024 13:58:56 -0400 Subject: [PATCH 19/21] coninute not break in for loop --- lib/dev-engines.js | 2 +- test/check-dev-engines.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/dev-engines.js b/lib/dev-engines.js index 678c696..ac5a182 100644 --- a/lib/dev-engines.js +++ b/lib/dev-engines.js @@ -105,7 +105,7 @@ function checkDevEngines (wanted, current = {}, opts = {}) { // this accounts for empty array eg { runtime: [] } and ignores it if (dependencies.length === 0) { - break + continue } const depErrors = [] diff --git a/test/check-dev-engines.js b/test/check-dev-engines.js index 6cafca9..e464383 100644 --- a/test/check-dev-engines.js +++ b/test/check-dev-engines.js @@ -392,3 +392,28 @@ t.test('spec 2', async t => { }), ]) }) + +t.test('empty array along side error', async t => { + t.same(checkDevEngines({ + cpu: [], + runtime: { + name: 'bun', + onFail: 'error', + }, + }, { + cpu: { name: 'arm' }, + runtime: { name: 'node', version: '20.0.0' }, + }), [Object.assign(new Error(`Invalid engine "runtime"`), { + errors: [ + new Error(`Invalid name "bun" does not match "node" for "runtime"`), + ], + engine: 'runtime', + isWarn: false, + isError: true, + current: { name: 'node', version: '20.0.0' }, + required: { + name: 'bun', + onFail: 'error', + }, + })]) +}) From 38d2f39d8e3fb1b9419e4deb2d9846380e167ddf Mon Sep 17 00:00:00 2001 From: reggi Date: Thu, 5 Sep 2024 14:00:30 -0400 Subject: [PATCH 20/21] revert to 6.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d73f34..a03828e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "npm-install-checks", - "version": "7.0.0", + "version": "6.3.0", "description": "Check the engines and platform fields in package.json", "main": "lib/index.js", "dependencies": { From 83e7c96eeda57d612edbdb0f21863aa6fb7a7ace Mon Sep 17 00:00:00 2001 From: reggi Date: Thu, 5 Sep 2024 14:20:26 -0400 Subject: [PATCH 21/21] add checkDevEngines to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 2dd8c7f..ac854f3 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,10 @@ Check if a package's `os`, `cpu` and `libc` match the running system. `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. Error code: 'EBADPLATFORM' + + +### .checkDevEngines(wanted, current, opts) + +Check if a package's `devEngines` property matches the current system environment. + +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`. \ No newline at end of file