diff --git a/package.json b/package.json index 6d253ade..252ec863 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "author": "Salesforce", "bugs": "https://github.com/oclif/plugin-plugins/issues", "dependencies": { - "@oclif/core": "^3.26.6", - "chalk": "^5.3.0", + "@oclif/core": "4.0.0-beta.11", + "ansis": "^3.2.0", "debug": "^4.3.4", "npm": "^10.8.0", "npm-package-arg": "^11.0.2", @@ -21,6 +21,7 @@ "@commitlint/config-conventional": "^19", "@oclif/plugin-help": "^6", "@oclif/prettier-config": "^0.2.1", + "@oclif/test": "^4.0.2", "@types/chai": "^4.3.11", "@types/debug": "^4.1.12", "@types/mocha": "^10.0.6", @@ -68,7 +69,7 @@ "@oclif/plugin-help" ], "aliases": { - "aliasme": "@oclif/plugin-test-esm-1" + "aliasme": "@oclif/plugin-version" }, "bin": "mycli", "flexibleTaxonomy": true, diff --git a/src/commands/plugins/index.ts b/src/commands/plugins/index.ts index 95907eba..594b655f 100644 --- a/src/commands/plugins/index.ts +++ b/src/commands/plugins/index.ts @@ -1,5 +1,5 @@ import {Command, Flags, Interfaces, Plugin} from '@oclif/core' -import chalk from 'chalk' +import {dim} from 'ansis' // @ts-expect-error because object-treeify does not have types: https://github.com/blackflux/object-treeify/issues/1077 import treeify from 'object-treeify' @@ -89,17 +89,17 @@ export default class PluginsIndex extends Command { private displayJitPlugins(jitPlugins: JitPlugin[]) { if (jitPlugins.length === 0) return - this.log(chalk.dim('\nUninstalled JIT Plugins:')) + this.log(dim('\nUninstalled JIT Plugins:')) for (const {name, version} of jitPlugins) { - this.log(`${this.plugins.friendlyName(name)} ${chalk.dim(version)}`) + this.log(`${this.plugins.friendlyName(name)} ${dim(version)}`) } } private formatPlugin(plugin: Plugin): string { - let output = `${this.plugins.friendlyName(plugin.name)} ${chalk.dim(plugin.version)}` - if (plugin.type !== 'user') output += chalk.dim(` (${plugin.type})`) + let output = `${this.plugins.friendlyName(plugin.name)} ${dim(plugin.version)}` + if (plugin.type !== 'user') output += dim(` (${plugin.type})`) if (plugin.type === 'link') output += ` ${plugin.root}` - else if (plugin.tag && plugin.tag !== 'latest') output += chalk.dim(` (${String(plugin.tag)})`) + else if (plugin.tag && plugin.tag !== 'latest') output += dim(` (${String(plugin.tag)})`) return output } } diff --git a/src/commands/plugins/inspect.ts b/src/commands/plugins/inspect.ts index f4c0a136..129cd9b8 100644 --- a/src/commands/plugins/inspect.ts +++ b/src/commands/plugins/inspect.ts @@ -1,5 +1,5 @@ import {Args, Command, Flags, Plugin} from '@oclif/core' -import chalk from 'chalk' +import {bold, dim} from 'ansis' import {readFile} from 'node:fs/promises' import {dirname, join, sep} from 'node:path' // @ts-expect-error because object-treeify does not have types: https://github.com/blackflux/object-treeify/issues/1077 @@ -113,7 +113,7 @@ export default class PluginsInspect extends Command { if (!version) continue const from = plugin.pjson.dependencies?.[dep] - const versionMsg = chalk.dim(from ? `${from} => ${version}` : version) + const versionMsg = dim(from ? `${from} => ${version}` : version) const msg = verbose ? `${dep} ${versionMsg} ${pkgPath}` : `${dep} ${versionMsg}` dependencies[msg] = null @@ -121,7 +121,7 @@ export default class PluginsInspect extends Command { } const tree = { - [chalk.bold.cyan(plugin.name)]: { + [bold.cyan(plugin.name)]: { [`version ${plugin.version}`]: null, ...(plugin.tag ? {[`tag ${plugin.tag}`]: null} : {}), ...(plugin.pjson.homepage ? {[`homepage ${plugin.pjson.homepage}`]: null} : {}), @@ -158,7 +158,7 @@ export default class PluginsInspect extends Command { try { plugins.push(await this.inspect(pluginName, flags.verbose)) } catch (error) { - this.log(chalk.bold.red('failed')) + this.log(bold.red('failed')) throw error } } diff --git a/src/commands/plugins/install.ts b/src/commands/plugins/install.ts index 78ba5fca..2b913de5 100644 --- a/src/commands/plugins/install.ts +++ b/src/commands/plugins/install.ts @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ import {Args, Command, Errors, Flags, Interfaces, ux} from '@oclif/core' -import chalk from 'chalk' +import {bold, cyan} from 'ansis' import validate from 'validate-npm-package-name' import {determineLogLevel} from '../../log-level.js' @@ -160,19 +160,17 @@ Use the <%= config.scopedEnvVarKey('NPM_REGISTRY') %> environment variable to se }) try { if (p.type === 'npm') { - ux.action.start( - `${this.config.name}: Installing plugin ${chalk.cyan(plugins.friendlyName(p.name) + '@' + p.tag)}`, - ) + 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 ${chalk.cyan(p.url)}`) + 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(chalk.bold.red('failed')) + ux.action.stop(bold.red('failed')) throw error } diff --git a/src/commands/plugins/link.ts b/src/commands/plugins/link.ts index eff58b02..956e84c0 100644 --- a/src/commands/plugins/link.ts +++ b/src/commands/plugins/link.ts @@ -1,5 +1,5 @@ import {Args, Command, Flags, ux} from '@oclif/core' -import chalk from 'chalk' +import {cyan} from 'ansis' import {determineLogLevel} from '../../log-level.js' import Plugins from '../../plugins.js' @@ -35,7 +35,7 @@ e.g. If you have a user-installed or core plugin that has a 'hello' command, ins logLevel: determineLogLevel(this.config, flags, 'silent'), }) - ux.action.start(`${this.config.name}: Linking plugin ${chalk.cyan(args.path)}`) + ux.action.start(`${this.config.name}: Linking plugin ${cyan(args.path)}`) await plugins.link(args.path, {install: flags.install}) ux.action.stop() } diff --git a/src/commands/plugins/reset.ts b/src/commands/plugins/reset.ts index be072fbe..e1561d3e 100644 --- a/src/commands/plugins/reset.ts +++ b/src/commands/plugins/reset.ts @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ import {Command, Flags} from '@oclif/core' -import chalk from 'chalk' +import {dim} from 'ansis' import {rm} from 'node:fs/promises' import {join} from 'node:path' @@ -28,7 +28,7 @@ export default class Reset extends Command { this.log(`Found ${userPlugins.length} plugin${userPlugins.length === 0 ? '' : 's'}:`) for (const plugin of userPlugins) { this.log( - `- ${plugin.name} ${chalk.dim(this.config.plugins.get(plugin.name)?.version)} ${chalk.dim(`(${plugin.type})`)}`, + `- ${plugin.name} ${dim(this.config.plugins.get(plugin.name)?.version ?? '')} ${dim(`(${plugin.type})`)}`, ) } @@ -68,7 +68,7 @@ export default class Reset extends Command { if (plugin.type === 'link') { try { const newPlugin = await plugins.link(plugin.root, {install: false}) - const newVersion = chalk.dim(`-> ${newPlugin.version}`) + const newVersion = dim(`-> ${newPlugin.version}`) this.log(`✅ Relinked ${plugin.name} ${newVersion}`) } catch { this.warn(`Failed to relink ${plugin.name}`) @@ -80,7 +80,7 @@ export default class Reset extends Command { const newPlugin = plugin.url ? await plugins.install(plugin.url) : await plugins.install(plugin.name, {tag: plugin.tag}) - const newVersion = chalk.dim(`-> ${newPlugin.version}`) + const newVersion = dim(`-> ${newPlugin.version}`) const tag = plugin.tag ? `@${plugin.tag}` : plugin.url ? ` (${plugin.url})` : '' this.log(`✅ Reinstalled ${plugin.name}${tag} ${newVersion}`) } catch { diff --git a/src/commands/plugins/uninstall.ts b/src/commands/plugins/uninstall.ts index e7c3f30a..66766130 100644 --- a/src/commands/plugins/uninstall.ts +++ b/src/commands/plugins/uninstall.ts @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ import {Args, Command, Flags, ux} from '@oclif/core' -import chalk from 'chalk' +import {bold} from 'ansis' import {determineLogLevel} from '../../log-level.js' import Plugins from '../../plugins.js' @@ -68,7 +68,7 @@ export default class PluginsUninstall extends Command { ux.action.start(`${this.config.name}: Uninstalling ${displayName}`) await plugins.uninstall(name) } catch (error) { - ux.action.stop(chalk.bold.red('failed')) + ux.action.stop(bold.red('failed')) throw error } diff --git a/src/npm.ts b/src/npm.ts index 7cecded1..043d7875 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -31,7 +31,7 @@ export class NPM { if (this.config.npmRegistry) args.push(`--registry=${this.config.npmRegistry}`) if (options.logLevel !== 'notice' && options.logLevel !== 'silent') { - ux.logToStderr(`${options.cwd}: ${bin} ${args.join(' ')}`) + ux.stderr(`${options.cwd}: ${bin} ${args.join(' ')}`) } debug(`${options.cwd}: ${bin} ${args.join(' ')}`) diff --git a/src/plugins.ts b/src/plugins.ts index a4028d5c..0b2a2d99 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -1,5 +1,5 @@ import {Config, Errors, Interfaces, ux} from '@oclif/core' -import chalk from 'chalk' +import {bold} from 'ansis' import makeDebug from 'debug' import {spawn} from 'node:child_process' import {access, mkdir, readFile, rename, rm, writeFile} from 'node:fs/promises' @@ -13,10 +13,21 @@ import {Output} from './spawn.js' import {uniqWith} from './util.js' import {Yarn} from './yarn.js' +type Plugin = Interfaces.LinkedPlugin | Interfaces.UserPlugin + type UserPJSON = { dependencies: Record oclif: { - plugins: Array + plugins: Plugin[] + schema: number + } + private: boolean +} + +type NormalizedUserPJSON = { + dependencies: Record + oclif: { + plugins: Plugin[] schema: number } private: boolean @@ -37,14 +48,8 @@ async function fileExists(filePath: string): Promise { } } -function dedupePlugins( - plugins: Interfaces.PJSON.PluginTypes[], -): (Interfaces.PJSON.PluginTypes.Link | Interfaces.PJSON.PluginTypes.User)[] { - return uniqWith( - plugins, - // @ts-expect-error because typescript doesn't think it's possible for a plugin to have the `link` type here - (a, b) => a.name === b.name || (a.type === 'link' && b.type === 'link' && a.root === b.root), - ) as (Interfaces.PJSON.PluginTypes.Link | Interfaces.PJSON.PluginTypes.User)[] +function dedupePlugins(plugins: Plugin[]): Plugin[] { + return uniqWith(plugins, (a, b) => a.name === b.name || (a.type === 'link' && b.type === 'link' && a.root === b.root)) } function extractIssuesLocation( @@ -63,10 +68,10 @@ function extractIssuesLocation( function notifyUser(plugin: Config, output: Output): void { const containsWarnings = [...output.stdout, ...output.stderr].some((l) => l.includes('npm WARN')) if (containsWarnings) { - ux.logToStderr(chalk.bold.yellow(`\nThese warnings can only be addressed by the owner(s) of ${plugin.name}.`)) + ux.stderr(bold.yellow(`\nThese warnings can only be addressed by the owner(s) of ${plugin.name}.`)) if (plugin.pjson.bugs || plugin.pjson.repository) { - ux.logToStderr( + ux.stderr( `We suggest that you create an issue at ${extractIssuesLocation( plugin.pjson.bugs, plugin.pjson.repository, @@ -93,7 +98,7 @@ export default class Plugins { }) } - public async add(...plugins: Interfaces.PJSON.PluginTypes[]): Promise { + public async add(...plugins: Plugin[]): Promise { const pjson = await this.pjson() const mergedPlugins = [...(pjson.oclif.plugins || []), ...plugins] as typeof pjson.oclif.plugins await this.savePJSON({ @@ -112,9 +117,7 @@ export default class Plugins { return match?.[1] ?? name } - public async hasPlugin( - name: string, - ): Promise { + public async hasPlugin(name: string): Promise { const list = await this.list() const friendlyName = this.friendlyName(name) const unfriendlyName = this.unfriendlyName(name) ?? name @@ -263,7 +266,7 @@ export default class Plugins { return c } - public async list(): Promise<(Interfaces.PJSON.PluginTypes.Link | Interfaces.PJSON.PluginTypes.User)[]> { + public async list(): Promise { const pjson = await this.pjson() return pjson.oclif.plugins } @@ -280,7 +283,7 @@ export default class Plugins { return name } - public async pjson(): Promise { + public async pjson(): Promise { const pjson = await this.readPJSON() const plugins = pjson ? normalizePlugins(pjson.oclif.plugins) : [] return { @@ -330,7 +333,7 @@ export default class Plugins { } public async update(): Promise { - let plugins = (await this.list()).filter((p): p is Interfaces.PJSON.PluginTypes.User => p.type === 'user') + let plugins = (await this.list()).filter((p): p is Interfaces.UserPlugin => p.type === 'user') if (plugins.length === 0) return await this.maybeCleanUp() @@ -360,7 +363,7 @@ export default class Plugins { const npmPlugins = plugins.filter((p) => !p.url) const jitPlugins = this.config.pjson.oclif.jitPlugins ?? {} - const modifiedPlugins: Interfaces.PJSON.PluginTypes[] = [] + const modifiedPlugins: Plugin[] = [] if (npmPlugins.length > 0) { await this.npm.install( npmPlugins.map((p) => { @@ -455,9 +458,9 @@ export default class Plugins { return join(this.config.dataDir, 'package.json') } - private async readPJSON(): Promise { + private async readPJSON(): Promise { try { - return JSON.parse(await readFile(this.pjsonPath, 'utf8')) as Interfaces.PJSON.User + return JSON.parse(await readFile(this.pjsonPath, 'utf8')) as UserPJSON } catch (error: unknown) { this.debug(error) const err = error as {code?: string} & Error @@ -466,24 +469,22 @@ export default class Plugins { } private async savePJSON(pjson: UserPJSON) { - this.debug(`saving pjson at ${this.pjsonPath}`, JSON.stringify(pjson, null, 2)) await mkdir(dirname(this.pjsonPath), {recursive: true}) await writeFile(this.pjsonPath, JSON.stringify({name: this.config.name, ...pjson}, null, 2)) } } // if the plugin is a simple string, convert it to an object -const normalizePlugins = ( - input: Interfaces.PJSON.User['oclif']['plugins'], -): (Interfaces.PJSON.PluginTypes.Link | Interfaces.PJSON.PluginTypes.User)[] => - dedupePlugins( - (input ?? []).map((p) => - typeof p === 'string' - ? { - name: p, - tag: 'latest', - type: 'user' as const, - } - : p, - ), +const normalizePlugins = (input: Plugin[]): Plugin[] => { + const normalized = (input ?? []).map((p) => + typeof p === 'string' + ? { + name: p, + tag: 'latest', + type: 'user' as const, + } + : p, ) + + return dedupePlugins(normalized) +} diff --git a/src/spawn.ts b/src/spawn.ts index fe7f0012..51141697 100644 --- a/src/spawn.ts +++ b/src/spawn.ts @@ -64,7 +64,7 @@ export async function spawn(modulePath: string, args: string[] = [], {cwd, logLe stderr.push(output) if (shouldPrint(output)) { loggedStderr.push(output) - ux.log(output) + ux.stdout(output) } else debug(output) }) @@ -74,7 +74,7 @@ export async function spawn(modulePath: string, args: string[] = [], {cwd, logLe stdout.push(output) if (shouldPrint(output)) { loggedStdout.push(output) - ux.log(output) + ux.stdout(output) } else debug(output) }) diff --git a/src/yarn.ts b/src/yarn.ts index b6aaba07..1139509f 100644 --- a/src/yarn.ts +++ b/src/yarn.ts @@ -27,7 +27,7 @@ export class Yarn { if (this.config.npmRegistry) args.push(`--registry=${this.config.npmRegistry}`) if (options.logLevel !== 'notice' && options.logLevel !== 'silent') { - ux.logToStderr(`${options.cwd}: ${bin} ${args.join(' ')}`) + ux.stderr(`${options.cwd}: ${bin} ${args.join(' ')}`) } debug(`${options.cwd}: ${bin} ${args.join(' ')}`) diff --git a/test/integration/install.integration.ts b/test/integration/install.integration.ts index 54486aeb..69e9c5d4 100644 --- a/test/integration/install.integration.ts +++ b/test/integration/install.integration.ts @@ -1,28 +1,24 @@ -import {Errors, ux} from '@oclif/core' +import {runCommand} from '@oclif/test' +import {dim} from 'ansis' import {expect} from 'chai' -import chalk from 'chalk' import {rm} from 'node:fs/promises' import {join, resolve} from 'node:path' -import {SinonSandbox, SinonStub, createSandbox, match} from 'sinon' - -import PluginsIndex from '../../src/commands/plugins/index.js' -import PluginsInstall from '../../src/commands/plugins/install.js' -import PluginsUninstall from '../../src/commands/plugins/uninstall.js' describe('install/uninstall integration tests', () => { - let sandbox: SinonSandbox - let stdoutStub: SinonStub + const plugin = '@oclif/plugin-version' + const pluginShortName = 'version' + const pluginGithubSlug = 'oclif/plugin-version' + const pluginGithubUrl = 'https://github.com/oclif/plugin-version.git' const tmp = resolve('tmp', 'install-integration') const cacheDir = join(tmp, 'plugin-plugins-tests', 'cache') const configDir = join(tmp, 'plugin-plugins-tests', 'config') const dataDir = join(tmp, 'plugin-plugins-tests', 'data') - console.log('process.env.MYCLI_DATA_DIR:', chalk.dim(dataDir)) - console.log('process.env.MYCLI_CACHE_DIR:', chalk.dim(cacheDir)) - console.log('process.env.MYCLI_CONFIG_DIR:', chalk.dim(configDir)) - - const cwd = process.cwd() + console.log('process.env.MYCLI_DATA_DIR:', dim(dataDir)) + console.log('process.env.MYCLI_CACHE_DIR:', dim(cacheDir)) + console.log('process.env.MYCLI_CONFIG_DIR:', dim(configDir)) + console.log('process.env.NODE_ENV:', dim(process.env.NODE_ENV ?? 'not set')) before(async () => { try { @@ -38,16 +34,12 @@ describe('install/uninstall integration tests', () => { }) beforeEach(() => { - sandbox = createSandbox() - stdoutStub = sandbox.stub(ux.write, 'stdout') process.env.MYCLI_CACHE_DIR = cacheDir process.env.MYCLI_CONFIG_DIR = configDir process.env.MYCLI_DATA_DIR = dataDir }) afterEach(() => { - sandbox.restore() - delete process.env.MYCLI_CACHE_DIR delete process.env.MYCLI_CONFIG_DIR delete process.env.MYCLI_DATA_DIR @@ -55,183 +47,142 @@ describe('install/uninstall integration tests', () => { describe('basic', () => { it('should return "No Plugins" if no plugins are installed', async () => { - await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true + const {stdout} = await runCommand('plugins') + expect(stdout).to.contain('No plugins installed.') }) it('should install plugin', async () => { - await PluginsInstall.run(['@oclif/plugin-test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('test-esm-1'))).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true + await runCommand>(`plugins install ${plugin}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain(pluginShortName) + expect(result?.some((r) => r.name === plugin)).to.be.true }) it('should uninstall plugin', async () => { - await PluginsUninstall.run(['@oclif/plugin-test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.false + await runCommand(`plugins uninstall ${plugin}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.') + expect(result?.some((r) => r.name === plugin)).to.be.false }) }) describe('tagged', () => { it('should install plugin from a tag', async () => { - await PluginsInstall.run(['@oclif/plugin-test-esm-1@latest'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('test-esm-1'))).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true + await runCommand(`plugins install ${plugin}@latest`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain(pluginShortName) + expect(result?.some((r) => r.name === plugin)).to.be.true }) it('should uninstall plugin', async () => { - await PluginsUninstall.run(['@oclif/plugin-test-esm-1@latest'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.false + await runCommand(`plugins uninstall ${plugin}@latest`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.') + expect(result?.some((r) => r.name === plugin)).to.be.false }) }) describe('alias', () => { it('should install aliased plugin', async () => { - await PluginsInstall.run(['aliasme'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('test-esm-1'))).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true + await runCommand('plugins install aliasme') + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain(pluginShortName) + expect(result?.some((r) => r.name === plugin)).to.be.true }) it('should uninstall aliased plugin', async () => { - await PluginsUninstall.run(['@oclif/plugin-test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.false + await runCommand(`plugins uninstall ${plugin}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.') + expect(result?.some((r) => r.name === plugin)).to.be.false }) }) describe('github org/repo', () => { it('should install plugin from github org/repo', async () => { - await PluginsInstall.run(['oclif/plugin-test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('test-esm-1'))).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true + await runCommand(`plugins install ${pluginGithubSlug}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain(pluginShortName) + expect(result?.some((r) => r.name === plugin)).to.be.true }) it('should uninstall plugin from github', async () => { - await PluginsUninstall.run(['@oclif/plugin-test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.false + await runCommand(`plugins uninstall ${plugin}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.') + expect(result?.some((r) => r.name === plugin)).to.be.false }) }) describe('github url', () => { it('should install plugin from github url', async () => { - await PluginsInstall.run(['https://github.com/oclif/plugin-test-esm-1.git'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('test-esm-1'))).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true + await runCommand(`plugins install ${pluginGithubUrl}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain(pluginShortName) + expect(result?.some((r) => r.name === plugin)).to.be.true }) it('should uninstall plugin from github', async () => { - await PluginsUninstall.run(['@oclif/plugin-test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.false + await runCommand(`plugins uninstall ${plugin}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.') + expect(result?.some((r) => r.name === plugin)).to.be.false }) }) describe('github tagged url', () => { it('should install plugin from github tagged url', async () => { - await PluginsInstall.run(['https://github.com/oclif/plugin-test-esm-1.git#0.5.4'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('test-esm-1'))).to.be.true - expect(stdoutStub.calledWith(match('0.5.4'))).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true + await runCommand(`plugins install ${pluginGithubUrl}#2.1.2`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain(pluginShortName) + expect(result?.some((r) => r.name === plugin)).to.be.true }) it('should uninstall plugin from github', async () => { - await PluginsUninstall.run(['@oclif/plugin-test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.false - }) - }) - - describe('oclif.lock', () => { - it('should install plugin with oclif.lock', async () => { - await PluginsInstall.run(['@salesforce/plugin-custom-metadata'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('@salesforce/plugin-custom-metadata'))).to.be.true - expect(result.some((r) => r.name === '@salesforce/plugin-custom-metadata')).to.be.true - }) - - it('should uninstall plugin with oclif.lock', async () => { - await PluginsUninstall.run(['@salesforce/plugin-custom-metadata'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@salesforce/plugin-custom-metadata')).to.be.false + await runCommand(`plugins uninstall ${plugin}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.') + expect(result?.some((r) => r.name === plugin)).to.be.false }) }) describe('non-existent plugin', () => { it('should not install non-existent plugin', async () => { - try { - await PluginsInstall.run(['@oclif/DOES_NOT_EXIST'], cwd) - } catch {} - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@oclif/DOES_NOT_EXIST')).to.be.false + await runCommand('plugins install @oclif/DOES_NOT_EXIST') + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.') + expect(result?.some((r) => r.name === '@oclif/DOES_NOT_EXIST')).to.be.false }) it('should handle uninstalling a non-existent plugin', async () => { - try { - await PluginsUninstall.run(['@oclif/DOES_NOT_EXIST'], cwd) - } catch (error) { - const err = error as Errors.CLIError - expect(err.message).to.equal('@oclif/DOES_NOT_EXIST is not installed') - } + const {error} = await runCommand('plugins uninstall @oclif/DOES_NOT_EXIST') + expect(error?.message).to.contain('@oclif/DOES_NOT_EXIST is not installed') }) }) describe('scoped plugin', () => { it('should install scoped plugin', async () => { - await PluginsInstall.run(['test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('test-esm-1'))).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true + await runCommand(`plugins install ${pluginShortName}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain(pluginShortName) + expect(result?.some((r) => r.name === plugin)).to.be.true }) it('should uninstall scoped plugin', async () => { - await PluginsUninstall.run(['test-esm-1'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith('No plugins installed.\n')).to.be.true - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.false + await runCommand(`plugins uninstall ${pluginShortName}`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.') + expect(result?.some((r) => r.name === plugin)).to.be.false }) }) describe('legacy plugin', () => { it('should install legacy plugin', async () => { - await PluginsInstall.run(['@oclif/plugin-legacy'], cwd) - await PluginsInstall.run(['@heroku-cli/plugin-ps-exec', '--silent'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.calledWith(match('@heroku-cli/plugin-ps-exec'))).to.be.true - expect(result.some((r) => r.name === '@heroku-cli/plugin-ps-exec')).to.be.true + await runCommand('plugins install @oclif/plugin-legacy') + await runCommand('plugins install @heroku-cli/plugin-ps-exec --silent') + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.contain('@heroku-cli/plugin-ps-exec') + expect(result?.some((r) => r.name === '@heroku-cli/plugin-ps-exec')).to.be.true }) }) }) diff --git a/test/integration/link.integration.ts b/test/integration/link.integration.ts index 57e5acac..109dc69a 100644 --- a/test/integration/link.integration.ts +++ b/test/integration/link.integration.ts @@ -1,14 +1,9 @@ -import {ux} from '@oclif/core' +import {runCommand} from '@oclif/test' import {expect} from 'chai' import {exec as cpExec} from 'node:child_process' import {rm} from 'node:fs/promises' import {tmpdir} from 'node:os' import {join} from 'node:path' -import {SinonSandbox, SinonStub, createSandbox} from 'sinon' - -import PluginsIndex from '../../src/commands/plugins/index.js' -import PluginsLink from '../../src/commands/plugins/link.js' -import PluginsUninstall from '../../src/commands/plugins/uninstall.js' async function exec(cmd: string, opts?: {cwd?: string}) { return new Promise((resolve, reject) => { @@ -20,15 +15,11 @@ async function exec(cmd: string, opts?: {cwd?: string}) { } describe('link/unlink integration tests', () => { - let sandbox: SinonSandbox - let stdoutStub: SinonStub - const cacheDir = join(tmpdir(), 'plugin-plugins-tests', 'cache') const configDir = join(tmpdir(), 'plugin-plugins-tests', 'config') const dataDir = join(tmpdir(), 'plugin-plugins-tests', 'data') const pluginDir = join(tmpdir(), 'plugin-plugins-tests', 'plugin') const repo = 'https://github.com/oclif/plugin-test-esm-1.git' - const cwd = process.cwd() before(async () => { try { @@ -49,38 +40,32 @@ describe('link/unlink integration tests', () => { }) beforeEach(() => { - sandbox = createSandbox() - stdoutStub = sandbox.stub(ux.write, 'stdout') process.env.MYCLI_CACHE_DIR = cacheDir process.env.MYCLI_CONFIG_DIR = configDir process.env.MYCLI_DATA_DIR = dataDir }) afterEach(() => { - sandbox.restore() - delete process.env.MYCLI_CACHE_DIR delete process.env.MYCLI_CONFIG_DIR delete process.env.MYCLI_DATA_DIR }) it('should return "No Plugins" if no plugins are linked', async () => { - await PluginsIndex.run([], cwd) - expect(stdoutStub.firstCall.firstArg).to.equal('No plugins installed.\n') + const {stdout} = await runCommand('plugins') + expect(stdout).to.contain('No plugins installed.\n') }) it('should link plugin', async () => { - await PluginsLink.run([pluginDir, '--no-install'], cwd) - - const result = await PluginsIndex.run([], cwd) - expect(stdoutStub.firstCall.firstArg).to.include('test-esm-1') - expect(result.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true + await runCommand(`plugins link ${pluginDir} --no-install`) + const {result, stdout} = await runCommand>('plugins') + expect(stdout).to.include('test-esm-1') + expect(result?.some((r) => r.name === '@oclif/plugin-test-esm-1')).to.be.true }) it('should unlink plugin', async () => { - await PluginsUninstall.run([pluginDir], cwd) - - await PluginsIndex.run([], cwd) - expect(stdoutStub.firstCall.firstArg).to.equal('No plugins installed.\n') + await runCommand(`plugins unlink ${pluginDir}`) + const {stdout} = await runCommand>('plugins') + expect(stdout).to.contain('No plugins installed.\n') }) }) diff --git a/test/integration/sf.integration.ts b/test/integration/sf.integration.ts index e36e03ea..7f83f81c 100644 --- a/test/integration/sf.integration.ts +++ b/test/integration/sf.integration.ts @@ -1,16 +1,18 @@ +import {Ansis} from 'ansis' import {expect} from 'chai' -import chalk from 'chalk' import {exec as cpExec} from 'node:child_process' import {mkdir, rm, writeFile} from 'node:fs/promises' import {join, resolve} from 'node:path' +const ansis = new Ansis() + async function exec(command: string): Promise<{code: number; stderr: string; stdout: string}> { return new Promise((resolve, reject) => { cpExec(command, (error, stdout, stderr) => { if (error) { reject(error) } else { - resolve({code: 0, stderr, stdout}) + resolve({code: 0, stderr: ansis.strip(stderr), stdout: ansis.strip(stdout)}) } }) }) @@ -19,7 +21,7 @@ async function exec(command: string): Promise<{code: number; stderr: string; std async function ensureSfExists(): Promise { try { const {stdout} = await exec('sf --version') - console.log('sf version:', chalk.dim(stdout.trim())) + console.log('sf version:', ansis.dim(stdout.trim())) return true } catch { return false @@ -35,9 +37,9 @@ describe('sf Integration', () => { process.env.SF_CACHE_DIR = join(tmp, 'cache') process.env.SF_CONFIG_DIR = join(tmp, 'config') - console.log('process.env.SF_DATA_DIR:', chalk.dim(process.env.SF_DATA_DIR)) - console.log('process.env.SF_CACHE_DIR:', chalk.dim(process.env.SF_CACHE_DIR)) - console.log('process.env.SF_CONFIG_DIR:', chalk.dim(process.env.SF_CONFIG_DIR)) + console.log('process.env.SF_DATA_DIR:', ansis.dim(process.env.SF_DATA_DIR)) + console.log('process.env.SF_CACHE_DIR:', ansis.dim(process.env.SF_CACHE_DIR)) + console.log('process.env.SF_CONFIG_DIR:', ansis.dim(process.env.SF_CONFIG_DIR)) try { // no need to clear out directories in CI since they'll always be empty diff --git a/test/plugins.test.ts b/test/plugins.test.ts index 8710b48a..e9ce9158 100644 --- a/test/plugins.test.ts +++ b/test/plugins.test.ts @@ -11,13 +11,13 @@ describe('Plugins', () => { let saveStub: SinonSpy let config: Config - const userPlugin: Interfaces.PJSON.PluginTypes.User = { + const userPlugin: Interfaces.UserPlugin = { name: '@oclif/plugin-user', tag: 'latest', type: 'user', } - const linkedPlugin: Interfaces.PJSON.PluginTypes.Link = { + const linkedPlugin: Interfaces.LinkedPlugin = { name: '@oclif/plugin-linked', root: join('some', 'path', 'package.json'), type: 'link', diff --git a/yarn.lock b/yarn.lock index 0e6e9040..e7f8eb6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1493,6 +1493,29 @@ proc-log "^4.0.0" which "^4.0.0" +"@oclif/core@4.0.0-beta.11": + version "4.0.0-beta.11" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.0-beta.11.tgz#127deb8c9acd0b3d79ba6a9b6a18e7e3413b7863" + integrity sha512-CHhxoNBD5vaT+VtgzESyLoS8O/vkxJUb1cUX+wrmELdzaiggHfmiGCzWZM7VKvEEnOOg3SUz0eDLRlTH9Xy+/Q== + dependencies: + ansi-escapes "^4.3.2" + ansis "^3.0.1" + clean-stack "^3.0.1" + cli-spinners "^2.9.2" + cosmiconfig "^9.0.0" + debug "^4.3.4" + ejs "^3.1.10" + get-package-type "^0.1.0" + globby "^11.1.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + minimatch "^9.0.4" + string-width "^4.2.3" + supports-color "^9.4.0" + widest-line "^3.1.0" + wordwrap "^1.0.0" + wrap-ansi "^7.0.0" + "@oclif/core@^3.26.5", "@oclif/core@^3.26.6": version "3.26.6" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-3.26.6.tgz#f371868cfa0fe150a6547e6af98b359065d2f971" @@ -1560,6 +1583,14 @@ resolved "https://registry.npmjs.org/@oclif/prettier-config/-/prettier-config-0.2.1.tgz" integrity sha512-XB8kwQj8zynXjIIWRm+6gO/r8Qft2xKtwBMSmq1JRqtA6TpwpqECqiu8LosBCyg2JBXuUy2lU23/L98KIR7FrQ== +"@oclif/test@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@oclif/test/-/test-4.0.2.tgz#e1e2d851540aa4e757907ba631dfa1389b3a5f56" + integrity sha512-2aWjvDzi6tw/NYnKjhSdatiwY8PpHvlsFiv0DDkQ1Z9TqclMcm56xRzKiBKSBq2bVrnItfJYyFB3pb4tvxpfUw== + dependencies: + ansis "^3.2.0" + debug "^4.3.4" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" @@ -2541,6 +2572,11 @@ ansicolors@~0.3.2: resolved "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz" integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== +ansis@^3.0.1, ansis@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.2.0.tgz#0e050c5be94784f32ffdac4b84fccba064aeae4b" + integrity sha512-Yk3BkHH9U7oPyCN3gL5Tc7CpahG/+UFv/6UG03C311Vy9lzRmA5uoxDTpU9CO3rGHL6KzJz/pdDeXZCZ5Mu/Sg== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz"