UNPKG

@axway/axway-cli-pm

Version:

Package manager for Axway products

434 lines (383 loc) 11.7 kB
import fs from 'fs-extra'; import npa from 'npm-package-arg'; import npmsearch from 'libnpmsearch'; import pacote from 'pacote'; import path from 'path'; import promiseLimit from 'promise-limit'; import semver from 'semver'; import snooplogg from 'snooplogg'; import spawn from 'cross-spawn'; import which from 'which'; import { locations, loadConfig, createRequestOptions, createNPMRequestArgs } from '@axway/amplify-cli-utils'; import { EventEmitter } from 'events'; import { isDir, mkdirpSync, isFile } from '@axway/amplify-utils'; const scopedPackageRegex = /^@[a-z0-9][\w-.]+\/?/; const { error, log } = snooplogg('pm'); const { alert, highlight } = snooplogg.styles; /** * The path to the Axway CLI packages directory. * @type {String} */ const packagesDir = path.join(locations.axwayHome, 'axway-cli', 'packages'); /** * Finds a specific installed package. * * @param {String} packageName - The name of the package to find. * @returns {Object} */ async function find(packageName) { if (!packageName || typeof packageName !== 'string') { throw new TypeError('Expected package name to be a non-empty string'); } if (!isDir(packagesDir)) { return undefined; } const config = await loadConfig(); const extensions = await config.get('extensions', {}); packageName = packageName.toLowerCase(); for (const name of fs.readdirSync(packagesDir)) { const pkgDir = path.join(packagesDir, name); if (!isDir(pkgDir)) { continue; } if (scopedPackageRegex.test(name)) { for (const pkgSubDir of fs.readdirSync(pkgDir)) { const dir = path.join(pkgDir, pkgSubDir); const pkgName = `${name}/${pkgSubDir}`; if (isDir(dir) && pkgName.toLowerCase() === packageName) { const packageData = loadPackageData(pkgName, extensions, dir); if (packageData.version || Object.keys(packageData.versions).length) { return packageData; } } } } else if (name.toLowerCase() === packageName) { const packageData = loadPackageData(name, extensions, pkgDir); if (packageData.version || Object.keys(packageData.versions).length) { return packageData; } } } } /** * Installs a package from npm. * * @param {String} pkgName - The package and version to install. * @returns {EventEmitter} */ function install(pkgName) { const emitter = new EventEmitter(); setImmediate(async () => { let cfg = await loadConfig(); let previousActivePackage; let info; try { info = await view(pkgName); let npm; try { npm = await which('npm'); } catch (e) { const err = new Error('Unable to find the "npm" executable. Please ensure you have "npm" installed on your machine'); err.code = 'ENONPM'; throw err; } previousActivePackage = await cfg.get(`extensions.${info.name}`); info.path = path.join(packagesDir, info.name, info.version); mkdirpSync(info.path); emitter.emit('download', info); await pacote.extract(`${info.name}@${info.version}`, info.path, createRequestOptions()); emitter.emit('install', info); const args = [ 'install', '--production', '--force', // needed for npm 7 ...createNPMRequestArgs() ]; const opts = { cwd: info.path, env: Object.assign({ NO_UPDATE_NOTIFIER: 1 }, process.env), gid: process.env.SUDO_GID ? parseInt(process.env.SUDO_GID) : undefined, uid: process.env.SUDO_UID ? parseInt(process.env.SUDO_UID) : undefined, windowsHide: true }; log(`node ${highlight(process.version)} npm ${highlight(spawn.sync('npm', [ '-v' ], opts).stdout.toString().trim())}`); log(`Running PWD=${info.path} ${highlight(`${npm} ${args.join(' ')}`)}`); await new Promise((resolve, reject) => { let stderr = ''; const child = spawn(npm, args, opts); child.stdout.on('data', data => log(data.toString().trim())); child.stderr.on('data', data => { const s = data.toString(); stderr += s; log(s.trim()); }); child.on('close', status => { if (status) { reject(new Error(`${stderr ? String(stderr.split(/\r\n|\n/)[0]).replace(/^\s*error:\s*/i, '') : 'unknown error'} (code ${status})`)); } else { resolve(); } }); }); emitter.emit('register', info); cfg = await loadConfig(); await cfg.set(`extensions.${info.name}`, info.path); await cfg.save(); emitter.emit('end', info); } catch (err) { if (info) { if (previousActivePackage === info.path) { // package was reinstalled, but failed and directory is in an unknown state cfg = await loadConfig(); await cfg.delete(`extensions.${info.name}`); await cfg.save(); } else if (previousActivePackage) { // restore the previous value cfg = await loadConfig(); await cfg.set(`extensions.${info.name}`, previousActivePackage); await cfg.save(); } if (info.path) { await fs.remove(info.path); } } emitter.emit('error', err); } }); return emitter; } /** * Detects all installed packages. * * @returns {Promise<Array.<Object>>} */ async function list() { if (!isDir(packagesDir)) { return []; } const config = await loadConfig(); const extensions = await config.get('extensions', {}); const packages = []; for (const name of fs.readdirSync(packagesDir)) { const pkgDir = path.join(packagesDir, name); if (!isDir(pkgDir)) { continue; } if (scopedPackageRegex.test(name)) { for (const pkgSubDir of fs.readdirSync(pkgDir)) { const dir = path.join(pkgDir, pkgSubDir); const pkgName = `${name}/${pkgSubDir}`; if (isDir(dir)) { const packageData = loadPackageData(pkgName, extensions, dir); if (packageData.version || Object.keys(packageData.versions).length) { packages.push(packageData); } } } } else { const packageData = loadPackageData(name, extensions, pkgDir); if (packageData.version || Object.keys(packageData.versions).length) { packages.push(packageData); } } } return packages; } /** * Determines if there are any older versions of packages installed that could be purged. * * @param {String} [pkgName] - A specific package to check if purgable, otherwise checks all * packages. * @returns {Object} */ async function listPurgable(pkgName) { let packages = []; if (pkgName) { const pkg = await find(pkgName); if (!pkg) { throw new Error(`Package "${pkgName}" is not installed`); } packages.push(pkg); } else { packages = await list(); } const purgable = {}; for (const { name, version, versions } of packages) { for (const [ ver, versionData ] of Object.entries(versions)) { // if managed and not in use if (versionData.managed && versionData.path.startsWith(packagesDir) && semver.neq(ver, version)) { if (!purgable[name]) { purgable[name] = []; } purgable[name].push({ ...versionData, version: ver }); } } } return purgable; } /** * Scans a package directory for all installed versions. * * @param {String} name - The package name. * @param {Object} extensions - An object of registered extension names and their paths. * @param {String} pkgDir - The path to the package. * @returns {Object} */ function loadPackageData(name, extensions, pkgDir) { const packageData = { name, description: undefined, version: undefined, versions: {} }; // find all versions for (const version of fs.readdirSync(pkgDir)) { try { const versionDir = path.join(pkgDir, version); const pkgJson = fs.readJsonSync(path.join(versionDir, 'package.json')); packageData.description = pkgJson.description; packageData.versions[version] = { path: versionDir, managed: true }; } catch (e) { // squelch } } // see if this package is an extension and if it's the currently selected version let extPath = extensions[name]; if (!extPath) { name = name.replace(scopedPackageRegex, ''); extPath = extensions[name]; } if (extPath) { const pkgJsonFile = path.join(extPath, 'package.json'); if (isFile(pkgJsonFile)) { const { version } = fs.readJsonSync(pkgJsonFile); packageData.version = version; if (!packageData.versions[version]) { packageData.versions[version] = { path: extPath, managed: false }; } } } return packageData; } /** * Uninstalls a package. * * @param {String} dir - Path to the package to delete. * @returns {Promise} */ async function uninstallPackage(dir) { try { const pkgJson = await fs.readJson(path.join(dir, 'package.json')); if (pkgJson.scripts.uninstall) { log(`Running npm uninstall script: ${highlight(pkgJson.scripts.uninstall)}`); const { status, stderr } = spawn.sync('npm', [ 'run', 'uninstall' ], { cwd: dir }); if (status) { error(alert('Failed to run npm uninstall script:')); error(stderr); } } } catch (e) { // squelch } await fs.remove(dir); } /** * Searches npm for Axway CLI packages. * * @param {Object} [opts] - Various options. * @param {String} [opts.keyword] - A keyword to search for. * @param {Number} [opts.limit=50] - The max number of results to return. * @param {String} [opts.type] - A package type to filter by. * @returns {Promse<Array.<Object>>} */ async function search({ keyword, limit, type } = {}) { const plimit = promiseLimit(10); const requestOpts = createRequestOptions(); const keywords = [ 'amplify-package' ]; if (process.env.TEST) { keywords.push('amplify-test-package'); } if (keyword) { keywords.push(keyword); } const q = 'keywords:' + keywords.join(','); const packages = await npmsearch(q, { ...requestOpts, limit: Math.max(limit && parseInt(limit, 10) || 50, 1) }); const results = []; await Promise.all(packages.map(({ name, version }) => { return plimit(async () => { try { const pkg = await view(`${name}@${version}`, { requestOpts, type }); if (pkg) { results.push(pkg); } } catch (err) { // squelch } }); })); return results.sort((a, b) => a.name.localeCompare(b.name)); } /** * Fetches package information directly from npm and checks that it's a valid package. * * @param {String} pkgName - The package name. * @param {Object} [opts] - Various options. * @param {Object} [opts.requestOpts] - HTTP request options. * @param {String} [opts.type] - The package type to filter by. */ async function view(pkgName, { requestOpts = createRequestOptions(), type } = {}) { if (!pkgName || typeof pkgName !== 'string') { throw new TypeError('Expected package name to be a non-empty string'); } const { name, fetchSpec } = npa(pkgName); let info; if (!name) { throw new Error(`Invalid package name "${pkgName}"`); } try { info = await pacote.packument(name, { ...requestOpts, fullMetadata: true }); } catch (err) { if (err.statusCode === 404) { throw new Error(`Package "${pkgName}" not found`); } throw err; } const version = info['dist-tags']?.[fetchSpec] || fetchSpec; const pkg = info.versions[version]; const maintainers = [ 'appcelerator', 'axway-npm' ]; if (!pkg || !pkg.amplify?.type || (type && pkg.amplify.type !== type) || (pkg.keywords.includes('amplify-test-package') && !process.env.TEST) || !pkg.keywords.includes('amplify-package') || !info?.maintainers.some(m => maintainers.includes(m.name)) ) { throw new Error(`Package "${pkgName}" not found`); } const installed = await find(pkg.name); return { description: pkg.description, installed: installed?.versions || false, name: pkg.name, type: pkg.amplify.type, version, versions: Object.keys(info.versions) }; } export { find, install, list, listPurgable, packagesDir, search, uninstallPackage, view }; //# sourceMappingURL=pm.js.map