ls-engines
Version:
Determine if your dependency graph's stated "engines" criteria is met.
169 lines (143 loc) • 5.74 kB
JavaScript
;
require('./mock');
const test = require('tape');
const { readdirSync, promises: fs } = require('fs');
const { exec, execSync } = require('child_process');
const path = require('path');
const semver = require('semver');
const EXITS = require('../exit-codes');
const getGraphEntries = require('../getGraphEntries');
const binPath = path.join(__dirname, '..', 'bin', 'ls-engines');
const fixturePath = path.join(__dirname, 'fixtures');
const mockPath = path.join(__dirname, 'mock');
const { GREP, FIXTURE, UPDATE_SNAPSHOTS } = process.env;
const grepRegex = GREP && new RegExp(GREP);
const fixtures = readdirSync(fixturePath)
.filter((x) => !FIXTURE || FIXTURE === x || (grepRegex && grepRegex.test(x)));
const npmVersion = `v${execSync('npm --version')}`.trim();
const isNPM7 = semver.satisfies(npmVersion, '>= 7');
const replacement = '<node versions for below semver range>';
function normalizeNodeVersion(output) {
return output && output.replace(
new RegExp(`^(│\\s+node\\s+│\\s+)${process.version}\\s+(│)`, 'm'),
// eslint-disable-next-line no-template-curly-in-string
'$1${process.version} $2',
).replace(
/Currently available latest releases of each valid node major version: [^\n]+/,
'Currently available latest releases of each valid node major version: <node versions for above semver range>',
).replace(
/\(node:\d{4}\) ExperimentalWarning: The fs.promises API is experimental\n/,
'',
).replace(
/\(node:\d{4}\) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time\n/,
'',
).replace(
isNPM7 ? /^Lockfile/ : /^v1 lockfile/,
'v1 lockfile',
).replace(
/(?<before>│ )(?<versions>(?:v[^, │]+(?:, )?)+ *)(?<after>│)$/gm,
(_, __, ___, ____, _____, ______, { before, versions, after }) => `${before}${replacement.padEnd(versions.length - 1, ' ')} ${after}`,
);
}
function filename(mode, flag, kind) {
return [mode, flag.replace(/-/g, ''), kind].filter(Boolean).join('-');
}
function findCodes(code) {
if (code === 0) {
return ['SUCCESS'];
}
return Object.keys(EXITS).filter((name) => EXITS[name] & code);
}
async function getOrUpdate(cwd, cmd, mode, flag, err, res, actualEntries) {
const errPath = path.join(cwd, filename(mode, flag, 'stderr'));
const outPath = path.join(cwd, filename(mode, flag, 'stdout'));
const codePath = path.join(cwd, filename(mode, flag, 'code'));
const entriesPath = path.join(cwd, filename(mode, flag, 'entries'));
if (UPDATE_SNAPSHOTS) {
const codes = findCodes(err ? err.code : 0);
const stderr = (err && normalizeNodeVersion(err.message.slice(`Command failed: ${cmd}\n\n`.length))) || null;
const stdout = normalizeNodeVersion(res) || null;
await Promise.all([
fs.writeFile(codePath, `${codes.join('\n')}\n`),
fs.writeFile(errPath, stderr || ''),
fs.writeFile(outPath, stdout || ''),
fs.writeFile(entriesPath, JSON.stringify(actualEntries, null, '\t') || ''),
]);
return { codes, stderr, stdout, entries: actualEntries };
}
const [
codes,
stderr,
stdout,
entries,
] = await Promise.all([
fs.readFile(codePath, 'utf-8').then((x) => x.trim().split('\n')),
fs.readFile(errPath, 'utf-8').then((x) => x || null),
fs.readFile(outPath, 'utf-8').then((x) => x || null),
fs.readFile(entriesPath, 'utf-8').then((x) => JSON.parse(x) || null),
]);
return { codes, stderr, stdout, entries };
}
function testMode(t, fixture, cwd, mode) {
return ['', '--dev', '--production', '--peer'].reduce(async (prev, flag) => {
await prev;
const cmd = `${path.relative(cwd, binPath)} --mode=${mode} ${flag}`.trim();
t.comment(`## ${fixture}: running \`ls-engines --mode=${`${mode} ${flag}`.trim()}\`...`);
const mockLogger = t.captureFn(() => {});
const actualEntries = await getGraphEntries({
mode,
dev: flag === '' || flag === '--dev',
path: cwd,
peer: flag === '' || flag === '--peer',
production: flag === '' || flag === '--production',
selectedEngines: ['node'],
logger: mockLogger,
});
return new Promise((resolve) => {
exec(cmd, {
cwd,
env: {
...process.env,
FORCE_COLOR: 0,
NODE_OPTIONS: `--require="${mockPath}"`,
},
}, (err, res) => {
resolve();
t.test(`fixture: ${fixture}, mode: ${mode}${flag ? `, flag: ${flag}` : ''}`, async (st) => {
st.plan(5);
const {
codes,
stderr,
stdout,
entries,
} = await getOrUpdate(cwd, cmd, mode, flag, err, res, actualEntries);
const succeeds = codes.length === 1 && codes[0] === 'SUCCESS';
if (succeeds) {
st.equal(err, null, 'command succeeds, as expected');
} else {
st.ok(err, 'command fails, as expected');
}
const actualCode = succeeds ? 0 : err && err.code;
const derivedCodes = findCodes(actualCode);
st.deepEqual(derivedCodes, codes, `exit code is \`${actualCode}\` (${derivedCodes})`);
st.equal(err && normalizeNodeVersion(err.message), stderr && `Command failed: ${cmd}\n\n${stderr}`, 'stderr is as expected');
st.equal(normalizeNodeVersion(res), stdout, 'stdout is as expected');
st.deepEqual(entries, actualEntries, 'entries are as expected');
});
});
});
}, Promise.resolve());
}
test('ls-engines', (t) => {
t.plan(fixtures.length * 3 * 4);
return fixtures.reduce(async (prev, fixture) => {
await prev;
const cwd = path.join(fixturePath, fixture);
t.comment(`## fixture found: ${fixture}`);
await testMode(t, fixture, cwd, 'ideal');
await testMode(t, fixture, cwd, 'virtual');
t.comment(`## ${fixture}: running \`npm install --no-fund --no-audit\`...`);
execSync('npm install --no-fund --no-audit', { cwd });
await testMode(t, fixture, cwd, 'actual');
}, Promise.resolve());
});