UNPKG

@oclif/plugin-plugins

Version:
351 lines (350 loc) 14.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable no-await-in-loop */ const core_1 = require("@oclif/core"); const shelljs = require("shelljs"); const fs = require("fs"); const load_json_file_1 = require("load-json-file"); const path = require("path"); const semver_1 = require("semver"); const util_1 = require("./util"); const yarn_1 = require("./yarn"); const initPJSON = { private: true, oclif: { schema: 1, plugins: [] }, dependencies: {}, }; async function fileExists(filePath) { try { await fs.promises.access(filePath); return true; } catch (_a) { return false; } } class Plugins { constructor(config) { this.config = config; this.verbose = false; this.yarn = new yarn_1.default({ config }); this.debug = require('debug')('@oclif/plugins'); } async pjson() { try { const pjson = await (0, load_json_file_1.default)(this.pjsonPath); return Object.assign(Object.assign(Object.assign({}, initPJSON), { dependencies: {} }), pjson); } catch (error) { this.debug(error); if (error.code !== 'ENOENT') process.emitWarning(error); return initPJSON; } } async list() { const pjson = await this.pjson(); return this.normalizePlugins(pjson.oclif.plugins); } async install(name, { tag = 'latest', force = false } = {}) { try { this.debug(`installing plugin ${name}`); const yarnOpts = { cwd: this.config.dataDir, verbose: this.verbose }; await this.createPJSON(); let plugin; const add = force ? ['add', '--force'] : ['add']; if (name.includes(':')) { // url const url = name; await this.yarn.exec([...add, url], yarnOpts); name = Object.entries((await this.pjson()).dependencies || {}).find(([, u]) => u === url)[0]; plugin = await core_1.Config.load({ devPlugins: false, userPlugins: false, root: path.join(this.config.dataDir, 'node_modules', name), name, }); await this.refresh({ all: true, prod: true }, plugin.root); this.isValidPlugin(plugin); await this.add({ name, url, type: 'user' }); } else { // npm const range = (0, semver_1.validRange)(tag); const unfriendly = this.unfriendlyName(name); if (unfriendly && (await this.npmHasPackage(unfriendly))) { name = unfriendly; } await this.yarn.exec([...add, `${name}@${tag}`], yarnOpts); this.debug(`loading plugin ${name}...`); plugin = await core_1.Config.load({ devPlugins: false, userPlugins: false, root: path.join(this.config.dataDir, 'node_modules', name), name, }); this.debug(`finished loading plugin ${name} at root ${plugin.root}`); this.isValidPlugin(plugin); await this.refresh({ all: true, prod: true }, plugin.root); await this.add({ name, tag: range || tag, type: 'user' }); } return plugin; } catch (error) { this.debug('error installing plugin:', error); await this.uninstall(name).catch(error => this.debug(error)); if (String(error).includes('EACCES')) { throw new core_1.Errors.CLIError(error, { suggestions: [ `Plugin failed to install because of a permissions error.\nDoes your current user own the directory ${this.config.dataDir}?`, ], }); } throw error; } } /** * If a yarn.lock or oclif.lock exists at the root, refresh dependencies by * rerunning yarn. If options.prod is true, only install production dependencies. * * As of v9 npm will always ignore the yarn.lock during `npm pack`] * (see https://github.com/npm/cli/issues/6738). To get around this plugins can * rename yarn.lock to oclif.lock before running `npm pack` using `oclif lock`. * * We still check for the existence of yarn.lock since it could be included if a plugin was * packed using yarn or v8 of npm. Plugins installed directly from a git url will also * have a yarn.lock. * * @param options {prod: boolean, all: boolean} * @param roots string[] * @returns Promise<void> */ async refresh(options, ...roots) { const doRefresh = async (root) => { await this.yarn.exec(options.prod ? ['--prod'] : [], { cwd: root, verbose: this.verbose, }); }; const pluginRoots = [...roots]; if (options.all) { const userPluginsRoots = this.config.getPluginsList() .filter(p => p.type === 'user') .map(p => p.root); pluginRoots.push(...userPluginsRoots); } const deduped = [...new Set(pluginRoots)]; await Promise.all(deduped.map(async (r) => { if (await fileExists(path.join(r, 'yarn.lock'))) { this.debug(`yarn.lock exists at ${r}. Installing prod dependencies`); await doRefresh(r); } else if (await fileExists(path.join(r, 'oclif.lock'))) { this.debug(`oclif.lock exists at ${r}. Installing prod dependencies`); await fs.promises.rename(path.join(r, 'oclif.lock'), path.join(r, 'yarn.lock')); await doRefresh(r); } else { this.debug(`no yarn.lock or oclif.lock exists at ${r}. Skipping dependency refresh`); } })); } async link(p) { const c = await core_1.Config.load(path.resolve(p)); core_1.ux.action.start(`${this.config.name}: linking plugin ${c.name}`); this.isValidPlugin(c); // refresh will cause yarn.lock to install dependencies, including devDeps await this.refresh({ prod: false, all: false }, c.root); await this.add({ type: 'link', name: c.name, root: c.root }); } async add(...plugins) { const pjson = await this.pjson(); pjson.oclif.plugins = (0, util_1.uniq)([...(pjson.oclif.plugins || []), ...plugins]); await this.savePJSON(pjson); } async remove(name) { const pjson = await this.pjson(); if (pjson.dependencies) delete pjson.dependencies[name]; pjson.oclif.plugins = this.normalizePlugins(pjson.oclif.plugins).filter(p => p.name !== name); await this.savePJSON(pjson); } async uninstall(name) { try { const pjson = await this.pjson(); if ((pjson.oclif.plugins || []).find(p => typeof p === 'object' && p.type === 'user' && p.name === name)) { await this.yarn.exec(['remove', name], { cwd: this.config.dataDir, verbose: this.verbose, }); } } catch (error) { core_1.ux.warn(error); } finally { await this.remove(name); } } async update() { var _a; let plugins = (await this.list()).filter((p) => p.type === 'user'); if (plugins.length === 0) return; core_1.ux.action.start(`${this.config.name}: Updating plugins`); // migrate deprecated plugins const aliases = this.config.pjson.oclif.aliases || {}; for (const [name, to] of Object.entries(aliases)) { const plugin = plugins.find(p => p.name === name); if (!plugin) continue; if (to) await this.install(to); await this.uninstall(name); plugins = plugins.filter(p => p.name !== name); } if (plugins.find(p => Boolean(p.url))) { await this.yarn.exec(['upgrade'], { cwd: this.config.dataDir, verbose: this.verbose, }); } const npmPlugins = plugins.filter(p => !p.url); const jitPlugins = (_a = this.config.pjson.oclif.jitPlugins) !== null && _a !== void 0 ? _a : {}; const modifiedPlugins = []; if (npmPlugins.length > 0) { await this.yarn.exec(['add', ...npmPlugins.map(p => { var _a; // a not valid tag indicates that it's a dist-tag like 'latest' if (!(0, semver_1.valid)(p.tag)) return `${p.name}@${p.tag}`; if (p.tag && (0, semver_1.valid)(p.tag) && jitPlugins[p.name] && (0, semver_1.gt)(p.tag, jitPlugins[p.name])) { // The user has installed a version of the JIT plugin that is newer than the one // specified by the root plugin's JIT configuration. In this case, we want to // keep the version installed by the user. return `${p.name}@${p.tag}`; } const tag = (_a = jitPlugins[p.name]) !== null && _a !== void 0 ? _a : p.tag; modifiedPlugins.push(Object.assign(Object.assign({}, p), { tag })); return `${p.name}@${tag}`; })], { cwd: this.config.dataDir, verbose: this.verbose }); } await this.refresh({ all: true, prod: true }); await this.add(...modifiedPlugins); core_1.ux.action.stop(); } /* eslint-enable no-await-in-loop */ async hasPlugin(name) { var _a, _b; const list = await this.list(); const friendly = list.find(p => this.friendlyName(p.name) === this.friendlyName(name)); const unfriendly = list.find(p => this.unfriendlyName(p.name) === this.unfriendlyName(name)); const link = list.find(p => p.type === 'link' && path.resolve(p.root) === path.resolve(name)); return ((_b = (_a = friendly !== null && friendly !== void 0 ? friendly : unfriendly) !== null && _a !== void 0 ? _a : link) !== null && _b !== void 0 ? _b : false); } async yarnNodeVersion() { try { const f = await (0, load_json_file_1.default)(path.join(this.config.dataDir, 'node_modules', '.yarn-integrity')); return f.nodeVersion; } catch (error) { if (error.code !== 'ENOENT') core_1.ux.warn(error); } } unfriendlyName(name) { if (name.includes('@')) return; const scope = this.config.pjson.oclif.scope; if (!scope) return; return `@${scope}/plugin-${name}`; } async maybeUnfriendlyName(name) { const unfriendly = this.unfriendlyName(name); this.debug(`checking registry for expanded package name ${unfriendly}`); if (unfriendly && (await this.npmHasPackage(unfriendly))) { return unfriendly; } this.debug(`expanded package name ${unfriendly} not found, using given package name ${name}`); return name; } friendlyName(name) { const scope = this.config.pjson.oclif.scope; if (!scope) return name; const match = name.match(`@${scope}/plugin-(.+)`); if (!match) return name; return match[1]; } async createPJSON() { if (!fs.existsSync(this.pjsonPath)) { this.debug(`creating ${this.pjsonPath} with pjson: ${JSON.stringify(initPJSON, null, 2)}`); await this.savePJSON(initPJSON); } } get pjsonPath() { return path.join(this.config.dataDir, 'package.json'); } async npmHasPackage(name) { const nodeExecutable = (0, util_1.findNode)(this.config.root); const npmCli = await (0, util_1.findNpm)(); this.debug(`Using node executable located at: ${nodeExecutable}`); this.debug(`Using npm executable located at: ${npmCli}`); const command = `${nodeExecutable} ${npmCli} show ${name} dist-tags`; try { const npmShowResult = shelljs.exec(command, { silent: true, async: false, encoding: 'utf8', }); if (npmShowResult.code !== 0) { this.debug(npmShowResult.stderr); return false; } this.debug(`Found ${name} in the registry.`); return true; } catch (_a) { throw new Error(`Could not run npm show for ${name}`); } } async savePJSON(pjson) { pjson.oclif.plugins = this.normalizePlugins(pjson.oclif.plugins); this.debug(`saving pjson at ${this.pjsonPath}`, JSON.stringify(pjson, null, 2)); await fs.promises.mkdir(path.dirname(this.pjsonPath), { recursive: true }); await fs.promises.writeFile(this.pjsonPath, JSON.stringify(pjson, null, 2)); } normalizePlugins(input) { let plugins = (input || []).map(p => { if (typeof p === 'string') { return { name: p, type: 'user', tag: 'latest', }; } return p; }); plugins = (0, util_1.uniqWith)(plugins, (a, b) => a.name === b.name || (a.type === 'link' && b.type === 'link' && a.root === b.root)); return plugins; } isValidPlugin(p) { if (p.valid) return true; if (!this.config.getPluginsList().find(p => p.name === '@oclif/plugin-legacy') || // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore check if this plugin was loaded by `plugin-legacy` p._base.includes('@oclif/plugin-legacy')) { return true; } throw new core_1.Errors.CLIError('plugin is invalid', { suggestions: [ 'Plugin failed to install because it does not appear to be a valid CLI plugin.\nIf you are sure it is, contact the CLI developer noting this error.', ], }); } } exports.default = Plugins;