Skip to content

Commit

Permalink
feat!: use npm for plugin operations (#776)
Browse files Browse the repository at this point in the history
* feat: add npm and pnpm options

* feat!: use npm for installs

BREAKING CHANGES: use npm for all plugin operations

* chore: bump to v5 prerelease

* fix: remove yarn.lock

* test: skip sf integration tests

* fix: force rm yarn.lock

* fix: use fork for npm show

* feat: remove all yarn functionality

* chore: clean up

* test: debug failing windows tests

* feat: add --npm-log-level flag

* chore: clean up

* test: debug failing windows tests

* test: compilation errors

* fix: use CLIError

* test: debug failing windows tests

* test: debug failing windows tests

* test: debug failing windows tests

* chore: remove unused dep

* chore: clean up

* fix: add suggestion for failed install

* fix: npm-run-path is actually needed

* test: debug failing windows tests

* test: debug failing windows tests

* test: extend timeout

* test: renable sf integration tests

* feat: simplify logging options

* fix: clean up yarn.lock and node_modules if they exist

* perf: spawn new process for removing node_modules

* chore(release): 5.0.0-beta.1 [skip ci]

* chore(release): 5.0.0-beta.2 [skip ci]

* chore(release): 5.0.0-beta.3 [skip ci]

* chore(release): 5.0.0-beta.4 [skip ci]

* fix: reinstall plugin from url if applicable

* feat: better logging

* fix: uninstall mis-scoped plugin

* fix: warn about missing expected files after github install

* fix: add type

* feat: improve ux when no output from npm

* chore(release): 5.0.0-beta.5 [skip ci]

* fix: display name when uninstalling wiht no args

* test: set timeout on install test

* fix: ensure dir exists before checking for name

* chore(release): 5.0.0-beta.6 [skip ci]

---------

Co-authored-by: svc-cli-bot <svc_cli_bot@salesforce.com>
Co-authored-by: Eric Willhoit <ewillhoit@salesforce.com>
  • Loading branch information
3 people authored Mar 25, 2024
1 parent ebaff4a commit 8e632b7
Show file tree
Hide file tree
Showing 21 changed files with 1,082 additions and 1,212 deletions.
19 changes: 9 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
{
"name": "@oclif/plugin-plugins",
"description": "plugins plugin for oclif",
"version": "4.3.10",
"version": "5.0.0-beta.6",
"author": "Salesforce",
"bugs": "https://github.com/oclif/plugin-plugins/issues",
"dependencies": {
"@oclif/core": "^3.25.2",
"chalk": "^5.3.0",
"debug": "^4.3.4",
"npm": "10.5.0",
"npm-run-path": "^4.0.1",
"npm-package-arg": "^11.0.1",
"npm-run-path": "^5.2.0",
"semver": "^7.6.0",
"shelljs": "^0.8.5",
"validate-npm-package-name": "^5.0.0",
"yarn": "^1.22.21"
"validate-npm-package-name": "^5.0.0"
},
"devDependencies": {
"@commitlint/config-conventional": "^18",
Expand All @@ -23,6 +22,7 @@
"@types/debug": "^4.1.12",
"@types/mocha": "^10.0.6",
"@types/node": "^18",
"@types/npm-package-arg": "^6.1.4",
"@types/semver": "^7.5.8",
"@types/shelljs": "^0.8.15",
"@types/sinon": "^17",
Expand Down Expand Up @@ -50,8 +50,7 @@
"files": [
"oclif.manifest.json",
"/lib",
"npm-shrinkwrap.json",
"oclif.lock"
"npm-shrinkwrap.json"
],
"homepage": "https://github.com/oclif/plugin-plugins",
"keywords": [
Expand All @@ -77,15 +76,15 @@
"repository": "oclif/plugin-plugins",
"scripts": {
"build": "shx rm -rf lib && tsc",
"clean": "shx rm -f oclif.manifest.json npm-shrinkwrap.json oclif.lock",
"clean": "shx rm -f oclif.manifest.json npm-shrinkwrap.json",
"compile": "tsc",
"lint": "eslint . --ext .ts",
"postpack": "yarn run clean",
"posttest": "yarn lint",
"prepack": "yarn build && oclif manifest && oclif readme && npm shrinkwrap && oclif lock",
"prepack": "yarn build && oclif manifest && oclif readme && npm shrinkwrap",
"prepare": "husky && yarn build",
"pretest": "yarn build --noEmit && tsc -p test --noEmit",
"test:integration:install": "mocha \"test/**/install.integration.ts\"",
"test:integration:install": "mocha \"test/**/install.integration.ts\" --timeout 1200000",
"test:integration:link": "mocha \"test/**/link.integration.ts\"",
"test:integration:sf": "mocha \"test/**/sf.integration.ts\"",
"test:integration": "mocha \"test/**/*.integration.ts\"",
Expand Down
6 changes: 5 additions & 1 deletion src/commands/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ export default class PluginsIndex extends Command {
core: Flags.boolean({description: 'Show core plugins.'}),
}

plugins = new Plugins(this.config)
plugins!: Plugins

async run(): Promise<PluginsJson> {
const {flags} = await this.parse(PluginsIndex)
this.plugins = new Plugins({
config: this.config,
})

let plugins = this.config.getPluginsList()
sortBy(plugins, (p) => this.plugins.friendlyName(p.name))
if (!flags.core) {
Expand Down
8 changes: 6 additions & 2 deletions src/commands/plugins/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import chalk from 'chalk'
import {readFile} from 'node:fs/promises'
import {dirname, join, sep} from 'node:path'

import {determineLogLevel} from '../../log-level.js'
import Plugins from '../../plugins.js'
import {sortBy} from '../../util.js'

Expand Down Expand Up @@ -59,7 +60,7 @@ export default class PluginsInspect extends Command {

static usage = 'plugins:inspect PLUGIN...'

plugins = new Plugins(this.config)
plugins!: Plugins

async findDep(plugin: Plugin, dependency: string): Promise<{pkgPath: null | string; version: null | string}> {
const dependencyPath = join(...dependency.split('/'))
Expand Down Expand Up @@ -140,7 +141,10 @@ export default class PluginsInspect extends Command {
/* eslint-disable no-await-in-loop */
async run(): Promise<PluginWithDeps[]> {
const {argv, flags} = await this.parse(PluginsInspect)
if (flags.verbose) this.plugins.verbose = true
this.plugins = new Plugins({
config: this.config,
logLevel: determineLogLevel(this.config, flags, 'silent'),
})
const aliases = this.config.pjson.oclif.aliases ?? {}
const plugins: PluginWithDeps[] = []
for (let name of argv as string[]) {
Expand Down
65 changes: 37 additions & 28 deletions src/commands/plugins/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {Args, Command, Errors, Flags, Interfaces, ux} from '@oclif/core'
import chalk from 'chalk'
import validate from 'validate-npm-package-name'

import {determineLogLevel} from '../../log-level.js'
import Plugins from '../../plugins.js'
import {YarnMessagesCache} from '../../util.js'

export default class PluginsInstall extends Command {
static aliases = ['plugins:add']
Expand All @@ -13,26 +13,34 @@ export default class PluginsInstall extends Command {
plugin: Args.string({description: 'Plugin to install.', required: true}),
}

static description = `Installs a plugin into the CLI.
Can be installed from npm or a git url.
static description = `Uses bundled npm executable to install plugins into <%= config.dataDir %>
Installation of a user-installed plugin will override a core plugin.
e.g. If you have a core plugin that has a 'hello' command, installing a user-installed plugin with a 'hello' command will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in the CLI without the need to patch and update the whole CLI.
`
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.`

public static enableJsonFlag = true

static examples = [
'<%= config.bin %> <%= command.id %> <%- config.pjson.oclif.examplePlugin || "myplugin" %> ',
'<%= config.bin %> <%= command.id %> https://github.com/someuser/someplugin',
'<%= config.bin %> <%= command.id %> someuser/someplugin',
{
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: 'Run yarn install with force flag.',
description: 'Force npm to fetch remote resources even if a local copy exists on disk.',
}),
help: Flags.help({char: 'h'}),
jit: Flags.boolean({
Expand All @@ -46,7 +54,7 @@ e.g. If you have a core plugin that has a 'hello' command, installing a user-ins
const jitPluginsConfig = ctx.config.pjson.oclif.jitPlugins ?? {}
if (Object.keys(jitPluginsConfig).length === 0) return input

const plugins = new Plugins(ctx.config)
const plugins = new Plugins({config: ctx.config})

const nonJitPlugins = await Promise.all(
requestedPlugins.map(async (plugin) => {
Expand All @@ -65,26 +73,28 @@ e.g. If you have a core plugin that has a 'hello' command, installing a user-ins
}),
silent: Flags.boolean({
char: 's',
description: 'Silences yarn output.',
description: 'Silences npm output.',
exclusive: ['verbose'],
}),
verbose: Flags.boolean({
char: 'v',
description: 'Show verbose yarn output.',
description: 'Show verbose npm output.',
exclusive: ['silent'],
}),
}

static strict = false

static usage = 'plugins:install PLUGIN...'
static summary = 'Installs a plugin into <%= config.bin %>.'

flags!: Interfaces.InferredFlags<typeof PluginsInstall.flags>
plugins = new Plugins(this.config)

// In this case we want these operations to happen
// sequentially so the `no-await-in-loop` rule is ignored
async parsePlugin(input: string): Promise<{name: string; tag: string; type: 'npm'} | {type: 'repo'; url: string}> {
async parsePlugin(
plugins: Plugins,
input: string,
): Promise<{name: string; tag: string; type: 'npm'} | {type: 'repo'; url: string}> {
// git ssh url
if (input.startsWith('git+ssh://') || input.endsWith('.git')) {
return {type: 'repo', url: input}
Expand All @@ -93,7 +103,7 @@ e.g. If you have a core plugin that has a 'hello' command, installing a user-ins
const getNameAndTag = async (input: string): Promise<{name: string; tag: string}> => {
const regexAtSymbolNotAtBeginning = /(?<!^)@/
const [splitName, tag = 'latest'] = input.split(regexAtSymbolNotAtBeginning)
const name = splitName.startsWith('@') ? splitName : await this.plugins.maybeUnfriendlyName(splitName)
const name = splitName.startsWith('@') ? splitName : await plugins.maybeUnfriendlyName(splitName)
validateNpmPkgName(name)

if (this.flags.jit) {
Expand Down Expand Up @@ -135,39 +145,38 @@ e.g. If you have a core plugin that has a 'hello' command, installing a user-ins
async run(): Promise<void> {
const {argv, flags} = await this.parse(PluginsInstall)
this.flags = flags
if (flags.verbose && !flags.silent) this.plugins.verbose = true
if (flags.silent && !flags.verbose) this.plugins.silent = true
const plugins = new Plugins({
config: this.config,
logLevel: determineLogLevel(this.config, this.flags, 'notice'),
})
const aliases = this.config.pjson.oclif.aliases || {}
for (let name of argv as string[]) {
if (aliases[name] === null) this.error(`${name} is blocked`)
name = aliases[name] || name
const p = await this.parsePlugin(name)
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(`Installing plugin ${chalk.cyan(this.plugins.friendlyName(p.name) + '@' + p.tag)}`)
plugin = await this.plugins.install(p.name, {
ux.action.start(
`${this.config.name}: Installing plugin ${chalk.cyan(plugins.friendlyName(p.name) + '@' + p.tag)}`,
)
plugin = await plugins.install(p.name, {
force: flags.force,
tag: p.tag,
})
} else {
ux.action.start(`Installing plugin ${chalk.cyan(p.url)}`)
plugin = await this.plugins.install(p.url, {force: flags.force})
ux.action.start(`${this.config.name}: Installing plugin ${chalk.cyan(p.url)}`)
plugin = await plugins.install(p.url, {force: flags.force})
}
} catch (error) {
ux.action.stop(chalk.bold.red('failed'))
YarnMessagesCache.getInstance().flush(plugin)
throw error
}

ux.action.stop(`installed v${plugin.version}`)

YarnMessagesCache.getInstance().flush(plugin)

this.log(chalk.green(`\nSuccessfully installed ${plugin.name} v${plugin.version}`))
}
}
}
Expand Down
19 changes: 9 additions & 10 deletions src/commands/plugins/link.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Args, Command, Flags, ux} from '@oclif/core'
import chalk from 'chalk'

import {determineLogLevel} from '../../log-level.js'
import Plugins from '../../plugins.js'
import {YarnMessagesCache} from '../../util.js'

export default class PluginsLink extends Command {
static args = {
Expand All @@ -27,17 +27,16 @@ e.g. If you have a user-installed or core plugin that has a 'hello' command, ins
verbose: Flags.boolean({char: 'v'}),
}

static usage = 'plugins:link PLUGIN'

plugins = new Plugins(this.config)

async run(): Promise<void> {
const {args, flags} = await this.parse(PluginsLink)
this.plugins.verbose = flags.verbose
ux.action.start(`Linking plugin ${chalk.cyan(args.path)}`)
await this.plugins.link(args.path, {install: flags.install})
ux.action.stop()

YarnMessagesCache.getInstance().flush()
const plugins = new Plugins({
config: this.config,
logLevel: determineLogLevel(this.config, flags, 'silent'),
})

ux.action.start(`${this.config.name}: Linking plugin ${chalk.cyan(args.path)}`)
await plugins.link(args.path, {install: flags.install})
ux.action.stop()
}
}
9 changes: 6 additions & 3 deletions src/commands/plugins/reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export default class Reset extends Command {

async run(): Promise<void> {
const {flags} = await this.parse(Reset)
const plugins = new Plugins(this.config)
const plugins = new Plugins({
config: this.config,
})
const userPlugins = await plugins.list()

this.log(`Found ${userPlugins.length} plugin${userPlugins.length === 0 ? '' : 's'}:`)
Expand All @@ -44,7 +46,6 @@ export default class Reset extends Command {
}

await Promise.all(filesToDelete.map((file) => rm(file, {force: true, recursive: true})))

for (const plugin of userPlugins) {
this.log(`✅ ${plugin.type === 'link' ? 'Unlinked' : 'Uninstalled'} ${plugin.name}`)
}
Expand Down Expand Up @@ -76,7 +77,9 @@ export default class Reset extends Command {

if (plugin.type === 'user') {
try {
const newPlugin = await plugins.install(plugin.name, {tag: plugin.tag})
const newPlugin = plugin.url
? await plugins.install(plugin.url)
: await plugins.install(plugin.name, {tag: plugin.tag})
const newVersion = chalk.dim(`-> ${newPlugin.version}`)
const tag = plugin.tag ? `@${plugin.tag}` : plugin.url ? ` (${plugin.url})` : ''
this.log(`✅ Reinstalled ${plugin.name}${tag} ${newVersion}`)
Expand Down
25 changes: 12 additions & 13 deletions src/commands/plugins/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import {Args, Command, Flags, ux} from '@oclif/core'
import chalk from 'chalk'

import {determineLogLevel} from '../../log-level.js'
import Plugins from '../../plugins.js'
import {YarnMessagesCache} from '../../util.js'

function removeTags(plugin: string): string {
if (plugin.includes('@')) {
Expand Down Expand Up @@ -38,21 +38,20 @@ export default class PluginsUninstall extends Command {

static strict = false

static usage = 'plugins:uninstall PLUGIN...'

plugins = new Plugins(this.config)

// In this case we want these operations to happen
// sequentially so the `no-await-in-loop` rule is ignored
async run(): Promise<void> {
const {argv, flags} = await this.parse(PluginsUninstall)
this.plugins = new Plugins(this.config)
if (flags.verbose) this.plugins.verbose = true

const plugins = new Plugins({
config: this.config,
logLevel: determineLogLevel(this.config, flags, 'silent'),
})

if (argv.length === 0) argv.push('.')
for (const plugin of argv as string[]) {
const friendly = removeTags(this.plugins.friendlyName(plugin))
ux.action.start(`Uninstalling ${friendly}`)
const unfriendly = await this.plugins.hasPlugin(removeTags(plugin))
const friendly = removeTags(plugins.friendlyName(plugin))
const unfriendly = await plugins.hasPlugin(removeTags(plugin))
if (!unfriendly) {
const p = this.config.getPluginsList().find((p) => p.name === plugin)
if (p?.parent)
Expand All @@ -65,15 +64,15 @@ export default class PluginsUninstall extends Command {

try {
const {name} = unfriendly
await this.plugins.uninstall(name)
const displayName = friendly === '.' ? name : friendly ?? name
ux.action.start(`${this.config.name}: Uninstalling ${displayName}`)
await plugins.uninstall(name)
} catch (error) {
ux.action.stop(chalk.bold.red('failed'))
throw error
}

ux.action.stop()

YarnMessagesCache.getInstance().flush()
}
}
}
Loading

0 comments on commit 8e632b7

Please sign in to comment.