UNPKG

@oclif/plugin-plugins

Version:
177 lines (174 loc) 7.99 kB
/* eslint-disable no-await-in-loop */ import { Args, Command, Errors, Flags, ux } from '@oclif/core'; import { bold, cyan } from 'ansis'; import validate from 'validate-npm-package-name'; import { determineLogLevel } from '../../log-level.js'; import Plugins from '../../plugins.js'; export default class PluginsInstall extends Command { static aliases = ['plugins:add']; static args = { plugin: Args.string({ description: 'Plugin to install.', required: true }), }; static description = `Uses npm to install plugins. Installation of a user-installed plugin will override a core plugin. Use the <%= config.scopedEnvVarKey('NPM_LOG_LEVEL') %> environment variable to set the npm loglevel. Use the <%= config.scopedEnvVarKey('NPM_REGISTRY') %> environment variable to set the npm registry.`; static enableJsonFlag = true; static examples = [ { command: '<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || "myplugin" %> ', description: 'Install a plugin from npm registry.', }, { command: '<%= config.bin %> <%= command.id %> https://github.com/someuser/someplugin', description: 'Install a plugin from a github url.', }, { command: '<%= config.bin %> <%= command.id %> someuser/someplugin', description: 'Install a plugin from a github slug.', }, ]; static flags = { force: Flags.boolean({ char: 'f', description: 'Force npm to fetch remote resources even if a local copy exists on disk.', }), help: Flags.help({ char: 'h' }), jit: Flags.boolean({ hidden: true, async parse(input, ctx) { if (input === false || input === undefined) return input; const requestedPlugins = ctx.argv.filter((a) => !a.startsWith('-')); if (requestedPlugins.length === 0) return input; const jitPluginsConfig = ctx.config.pjson.oclif.jitPlugins ?? {}; if (Object.keys(jitPluginsConfig).length === 0) return input; const plugins = new Plugins({ config: ctx.config }); const nonJitPlugins = await Promise.all(requestedPlugins.map(async (plugin) => { const name = await plugins.maybeUnfriendlyName(plugin); return { jit: Boolean(jitPluginsConfig[name]), name }; })); const nonJitPluginsNames = nonJitPlugins.filter((p) => !p.jit).map((p) => p.name); if (nonJitPluginsNames.length > 0) { throw new Errors.CLIError(`The following plugins are not JIT plugins: ${nonJitPluginsNames.join(', ')}`); } return input; }, }), silent: Flags.boolean({ char: 's', description: 'Silences npm output.', exclusive: ['verbose'], }), verbose: Flags.boolean({ char: 'v', description: 'Show verbose npm output.', exclusive: ['silent'], }), }; static strict = false; static summary = 'Installs a plugin into <%= config.bin %>.'; flags; // In this case we want these operations to happen // sequentially so the `no-await-in-loop` rule is ignored async parsePlugin(plugins, input) { // git ssh url if (input.startsWith('git+ssh://') || input.endsWith('.git')) { return { type: 'repo', url: input }; } const getNameAndTag = async (input) => { const regexAtSymbolNotAtBeginning = /(?<!^)@/; const [splitName, tag = 'latest'] = input.split(regexAtSymbolNotAtBeginning); const name = splitName.startsWith('@') ? splitName : await plugins.maybeUnfriendlyName(splitName); validateNpmPkgName(name); if (this.flags.jit) { const jitVersion = this.config.pjson.oclif?.jitPlugins?.[name]; if (jitVersion) { if (regexAtSymbolNotAtBeginning.test(input)) this.warn(`--jit flag is present along side a tag. Ignoring tag ${tag} and using the version specified in package.json (${jitVersion}).`); return { name, tag: jitVersion }; } this.warn(`--jit flag is present but ${name} is not a JIT plugin. Installing ${tag} instead.`); return { name, tag }; } return { name, tag }; }; // scoped npm package, e.g. @oclif/plugin-version if (input.startsWith('@') && input.includes('/')) { const { name, tag } = await getNameAndTag(input); return { name, tag, type: 'npm' }; } if (input.includes('/')) { // github url, e.g. https://github.com/oclif/plugin-version if (input.includes(':')) return { type: 'repo', url: input }; // github org/repo, e.g. oclif/plugin-version return { type: 'repo', url: `https://github.com/${input}` }; } // unscoped npm package, e.g. my-oclif-plugin // friendly plugin name, e.g. version instead of @oclif/plugin-version (requires `scope` to be set in root plugin's package.json) const { name, tag } = await getNameAndTag(input); return { name, tag, type: 'npm' }; } async run() { const { argv, flags } = await this.parse(PluginsInstall); this.flags = flags; const plugins = new Plugins({ config: this.config, logLevel: determineLogLevel(this.config, this.flags, 'notice'), }); const aliases = this.config.pjson.oclif.aliases || {}; const count = argv.length; for (let name of argv) { if (aliases[name] === null) this.error(`${name} is blocked`); name = aliases[name] || name; // Ignore the root plugin. Error if it's the only plugin being installed. if (name === this.config.name) { const updateInstructions = // this.config.binPath is set when the CLI is installed from an installer but not when it's installed from npm this.config.binPath && this.config.plugins.get('@oclif/plugin-update') ? ` Use "${this.config.bin} update" to update ${this.config.name}.` : ''; const msg = `Ignoring top-level CLI plugin ${this.config.name}.${updateInstructions}`; if (count === 1) { this.error(msg); } else { this.warn(msg); } continue; } const p = await this.parsePlugin(plugins, name); let plugin; await this.config.runHook('plugins:preinstall', { plugin: p, }); try { if (p.type === 'npm') { ux.action.start(`${this.config.name}: Installing plugin ${cyan(plugins.friendlyName(p.name) + '@' + p.tag)}`); plugin = await plugins.install(p.name, { force: flags.force, tag: p.tag, }); } else { ux.action.start(`${this.config.name}: Installing plugin ${cyan(p.url)}`); plugin = await plugins.install(p.url, { force: flags.force }); } } catch (error) { ux.action.stop(bold.red('failed')); throw error; } ux.action.stop(`installed v${plugin.version}`); } } } function validateNpmPkgName(name) { if (!validate(name).validForNewPackages) { throw new Errors.CLIError('Invalid npm package name.'); } }