#!/usr/bin/env node const path = require('path'); const fs = require('fs-extra'); const zlib = require('zlib'); const execa = require('execa'); const yargs = require('yargs'); const readPkg = require('read-pkg'); const requireFromString = require('require-from-string'); const tar = require('tar-fs'); const tmp = require('tmp'); tmp.setGracefulCleanup(); const PRELUDE = ` var Module = require('module'); var originalLoader = Module._load Module._load = function(path, parent) { if (path.startsWith('.') || Module.builtinModules.includes(path)) { return originalLoader.apply(this, arguments); } var dummy = new Proxy({}, { get() { return dummy; } }); return dummy; }; `; function main(flags) { if (!Proxy) { console .warn('Skipping pkg-check, detected missing Proxy support') .process.exit(0); } const cwd = flags.cwd || process.cwd(); const skipImport = typeof flags.skipImport === 'boolean' ? flags.skipImport : false; return readPkg({cwd}).then((pkg) => { return getTarballFiles(cwd, {write: !skipImport}).then((tarball) => { return getPackageFiles(cwd).then((pkgFiles) => { let problems = []; if (!flags.skipBin) { problems = problems.concat( pkgFiles.bin .filter((binFile) => tarball.files.indexOf(binFile) === -1) .map((binFile) => ({ type: 'bin', file: binFile, message: `Required bin file ${binFile} not found for ${pkg.name}`, })) ); } if (!flags.skipMain && tarball.files.indexOf(pkgFiles.main) === -1) { problems.push({ type: 'main', file: pkgFiles.main, message: `Required main file ${pkgFiles.main} not found for ${pkg.name}`, }); } if (!flags.skipImport && !flags.skipMain) { const importable = fileImportable( path.join(tarball.dirname, pkgFiles.main) ); if (!importable[1]) { problems.push({ type: 'import', file: pkgFiles.main, message: `Error while importing ${pkgFiles.main}: ${importable[0].message}`, }); } } return { pkg: pkg, pkgFiles: pkgFiles, files: tarball.files, problems: problems, }; }); }); }); } main( yargs .options({ cwd: { description: 'directory to execute in', type: 'string', }, skipMain: { default: false, type: 'boolean', description: 'Skip main checks', }, skipBin: { default: false, type: 'boolean', description: 'Skip bin checks', }, skipImport: { default: false, type: 'boolean', description: 'Skip import smoke test', }, }) .scriptName('pkg-check') .usage('pkg-check\n') .usage('Check if a package creates valid tarballs') .example('$0', '') .help() .version() .strict().argv ) .then((report) => { if (report.problems.length > 0) { console.log( `Found ${report.problems.length} problems while checking tarball for ${report.pkg.name}:` ); report.problems.forEach((problem) => { console.log(problem.message); }); process.exit(1); } }) .catch((err) => { setTimeout(() => { throw err; }, 0); }); async function getTarballFiles(source, options) { const tmpDir = tmp.dirSync({ keep: false, unsafeCleanup: true, }); const cwd = tmpDir.name; const tarball = path.join(cwd, 'test-archive.tgz'); await execa('yarn', ['pack', '--filename', tarball], {cwd: source}); return getArchiveFiles(tarball, options); } function getArchiveFiles(filePath, options) { const write = typeof options.write === 'boolean' ? options.write : true; return new Promise((resolve, reject) => { const files = []; fs.createReadStream(filePath) .pipe(zlib.createGunzip()) .pipe( tar.extract(path.dirname(filePath), { ignore(_, header) { files.push(path.relative('package', header.name)); return !write; }, }) ) .once('error', (err) => reject(err)) .once('finish', () => resolve({ dirname: path.join(path.dirname(filePath), 'package'), files: files, }) ); }); } function getPackageFiles(source) { return readPkg(source).then((pkg) => { return { main: normalizeMainPath(pkg.main || './index.js'), bin: getPkgBinFiles(pkg.bin), }; }); } function normalizeMainPath(mainPath) { const norm = path.normalize(mainPath); if (norm[norm.length - 1] === path.sep) { return `${norm}index.js`; } return norm; } function getPkgBinFiles(bin) { if (!bin) { return []; } if (typeof bin === 'string') { return [path.normalize(bin)]; } if (typeof bin === 'object') { return Object.values(bin).map((b) => path.normalize(b)); } } function fileImportable(file) { try { requireFromString( ` ${PRELUDE} ${fs.readFileSync(file)} `, file ); return [null, true]; } catch (err) { return [err, false]; } }