diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e03e2b3..cda274df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,9 +42,12 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] test: ['test:integration:install', 'test:integration:link'] + no-local-package-managers: [true, false] exclude: - os: windows-latest test: test:integration:link + - no-local-package-managers: true + test: test:integration:link runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v4 @@ -53,5 +56,9 @@ jobs: node-version: latest - uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@main - run: yarn build + - name: Remove package managers + if: ${{matrix.no-local-package-managers}} + run: | + yarn remove yarn npm - name: Run tests run: yarn ${{matrix.test}} diff --git a/README.md b/README.md index 6cba83a8..c48b9c25 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ EXAMPLES $ mycli plugins ``` -_See code: [src/commands/plugins/index.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/index.ts)_ +_See code: [src/commands/plugins/index.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/index.ts)_ ## `mycli plugins:inspect PLUGIN...` @@ -144,7 +144,7 @@ EXAMPLES $ mycli plugins inspect myplugin ``` -_See code: [src/commands/plugins/inspect.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/inspect.ts)_ +_See code: [src/commands/plugins/inspect.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/inspect.ts)_ ## `mycli plugins install PLUGIN` @@ -193,7 +193,7 @@ EXAMPLES $ mycli plugins install someuser/someplugin ``` -_See code: [src/commands/plugins/install.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/install.ts)_ +_See code: [src/commands/plugins/install.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/install.ts)_ ## `mycli plugins link PATH` @@ -223,7 +223,7 @@ EXAMPLES $ mycli plugins link myplugin ``` -_See code: [src/commands/plugins/link.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/link.ts)_ +_See code: [src/commands/plugins/link.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/link.ts)_ ## `mycli plugins reset` @@ -238,7 +238,7 @@ FLAGS --reinstall Reinstall all plugins after uninstalling. ``` -_See code: [src/commands/plugins/reset.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/reset.ts)_ +_See code: [src/commands/plugins/reset.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/reset.ts)_ ## `mycli plugins uninstall [PLUGIN]` @@ -266,7 +266,7 @@ EXAMPLES $ mycli plugins uninstall myplugin ``` -_See code: [src/commands/plugins/uninstall.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/uninstall.ts)_ +_See code: [src/commands/plugins/uninstall.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/uninstall.ts)_ ## `mycli plugins update` @@ -284,7 +284,7 @@ DESCRIPTION Update installed plugins. ``` -_See code: [src/commands/plugins/update.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/update.ts)_ +_See code: [src/commands/plugins/update.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/update.ts)_ diff --git a/package.json b/package.json index 980d36e0..b001e11f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@oclif/plugin-plugins", "description": "plugins plugin for oclif", - "version": "5.0.21", + "version": "5.0.22-dev.1", "author": "Salesforce", "bugs": "https://github.com/oclif/plugin-plugins/issues", "dependencies": { @@ -14,6 +14,7 @@ "object-treeify": "^4.0.1", "semver": "^7.6.2", "validate-npm-package-name": "^5.0.0", + "which": "^4.0.0", "yarn": "^1.22.22" }, "devDependencies": { @@ -28,6 +29,7 @@ "@types/semver": "^7.5.8", "@types/sinon": "^17", "@types/validate-npm-package-name": "^4.0.2", + "@types/which": "^3.0.3", "chai": "^4.4.1", "commitlint": "^18", "eslint": "^8.56.0", diff --git a/src/npm.ts b/src/npm.ts index 1e508ff4..7cecded1 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -1,11 +1,11 @@ -import {Interfaces, ux} from '@oclif/core' +import {Errors, Interfaces, ux} from '@oclif/core' import makeDebug from 'debug' import {readFile} from 'node:fs/promises' import {createRequire} from 'node:module' import {join, sep} from 'node:path' -import {ExecOptions, Output, fork} from './fork.js' import {LogLevel} from './log-level.js' +import {ExecOptions, Output, spawn} from './spawn.js' const debug = makeDebug('@oclif/plugin-plugins:npm') @@ -36,7 +36,7 @@ export class NPM { debug(`${options.cwd}: ${bin} ${args.join(' ')}`) try { - const output = await fork(bin, args, options) + const output = await spawn(bin, args, options) debug('npm done') return output } catch (error: unknown) { @@ -64,17 +64,28 @@ export class NPM { /** * Get the path to the npm CLI file. - * This will always resolve npm to the pinned version in `@oclif/plugin-plugins/package.json`. + * This will resolve npm to the pinned version in `@oclif/plugin-plugins/package.json` if it exists. + * Otherwise, it will use the globally installed npm. * * @returns The path to the `npm/bin/npm-cli.js` file. */ private async findNpm(): Promise { if (this.bin) return this.bin - const npmPjsonPath = createRequire(import.meta.url).resolve('npm/package.json') - const npmPjson = JSON.parse(await readFile(npmPjsonPath, {encoding: 'utf8'})) - const npmPath = npmPjsonPath.slice(0, Math.max(0, npmPjsonPath.lastIndexOf(sep))) - this.bin = join(npmPath, npmPjson.bin.npm) + try { + const npmPjsonPath = createRequire(import.meta.url).resolve('npm/package.json') + const npmPjson = JSON.parse(await readFile(npmPjsonPath, {encoding: 'utf8'})) + const npmPath = npmPjsonPath.slice(0, Math.max(0, npmPjsonPath.lastIndexOf(sep))) + this.bin = join(npmPath, npmPjson.bin.npm) + } catch { + const {default: which} = await import('which') + this.bin = await which('npm') + } + + if (!this.bin) { + throw new Errors.CLIError('npm not found') + } + return this.bin } } diff --git a/src/plugins.ts b/src/plugins.ts index 6cd1d13f..a4028d5c 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -7,9 +7,9 @@ import {basename, dirname, join, resolve} from 'node:path' import {fileURLToPath} from 'node:url' import {gt, valid, validRange} from 'semver' -import {Output} from './fork.js' import {LogLevel} from './log-level.js' import {NPM} from './npm.js' +import {Output} from './spawn.js' import {uniqWith} from './util.js' import {Yarn} from './yarn.js' @@ -330,7 +330,6 @@ export default class Plugins { } public async update(): Promise { - // eslint-disable-next-line unicorn/no-await-expression-member let plugins = (await this.list()).filter((p): p is Interfaces.PJSON.PluginTypes.User => p.type === 'user') if (plugins.length === 0) return diff --git a/src/fork.ts b/src/spawn.ts similarity index 66% rename from src/fork.ts rename to src/spawn.ts index 20e797d2..fe7f0012 100644 --- a/src/fork.ts +++ b/src/spawn.ts @@ -1,6 +1,6 @@ import {Errors, ux} from '@oclif/core' import makeDebug from 'debug' -import {fork as cpFork} from 'node:child_process' +import {spawn as cpSpawn} from 'node:child_process' import {npmRunPathEnv} from 'npm-run-path' import {LogLevel} from './log-level.js' @@ -15,11 +15,19 @@ export type Output = { stdout: string[] } -const debug = makeDebug('@oclif/plugin-plugins:fork') +const debug = makeDebug('@oclif/plugin-plugins:spawn') -export async function fork(modulePath: string, args: string[] = [], {cwd, logLevel}: ExecOptions): Promise { +export async function spawn(modulePath: string, args: string[] = [], {cwd, logLevel}: ExecOptions): Promise { return new Promise((resolve, reject) => { - const forked = cpFork(modulePath, args, { + // On windows, the global path to npm could be .cmd, .exe, or .js. If it's a .js file, we need to run it with node. + if (process.platform === 'win32' && modulePath.endsWith('.js')) { + args.unshift(`"${modulePath}"`) + modulePath = 'node' + } + + debug('modulePath', modulePath) + debug('args', args) + const spawned = cpSpawn(modulePath, args, { cwd, env: { ...npmRunPathEnv(), @@ -27,15 +35,9 @@ export async function fork(modulePath: string, args: string[] = [], {cwd, logLev // break the install since the install location isn't a .git directory. HUSKY: '0', }, - execArgv: process.execArgv - .join(' ') - // Remove --loader ts-node/esm from execArgv so that the subprocess doesn't fail if it can't find ts-node. - // The ts-node/esm loader isn't need to execute npm or yarn commands anyways. - .replace('--loader ts-node/esm', '') - .replace('--loader=ts-node/esm', '') - .split(' ') - .filter(Boolean), - stdio: [0, null, null, 'ipc'], + stdio: 'pipe', + windowsVerbatimArguments: true, + ...(process.platform === 'win32' && modulePath.toLowerCase().endsWith('.cmd') && {shell: true}), }) const possibleLastLinesOfNpmInstall = ['up to date', 'added'] @@ -56,8 +58,8 @@ export async function fork(modulePath: string, args: string[] = [], {cwd, logLev return logLevel !== 'silent' } - forked.stderr?.setEncoding('utf8') - forked.stderr?.on('data', (d: Buffer) => { + spawned.stderr?.setEncoding('utf8') + spawned.stderr?.on('data', (d: Buffer) => { const output = d.toString().trim() stderr.push(output) if (shouldPrint(output)) { @@ -66,8 +68,8 @@ export async function fork(modulePath: string, args: string[] = [], {cwd, logLev } else debug(output) }) - forked.stdout?.setEncoding('utf8') - forked.stdout?.on('data', (d: Buffer) => { + spawned.stdout?.setEncoding('utf8') + spawned.stdout?.on('data', (d: Buffer) => { const output = d.toString().trim() stdout.push(output) if (shouldPrint(output)) { @@ -76,8 +78,8 @@ export async function fork(modulePath: string, args: string[] = [], {cwd, logLev } else debug(output) }) - forked.on('error', reject) - forked.on('exit', (code: number) => { + spawned.on('error', reject) + spawned.on('exit', (code: number) => { if (code === 0) { resolve({stderr, stdout}) } else { diff --git a/src/yarn.ts b/src/yarn.ts index b14eb2c1..b6aaba07 100644 --- a/src/yarn.ts +++ b/src/yarn.ts @@ -1,15 +1,16 @@ -import {Interfaces, ux} from '@oclif/core' +import {Errors, Interfaces, ux} from '@oclif/core' import makeDebug from 'debug' import {createRequire} from 'node:module' import {fileURLToPath} from 'node:url' -import {ExecOptions, Output, fork} from './fork.js' import {LogLevel} from './log-level.js' +import {ExecOptions, Output, spawn} from './spawn.js' const require = createRequire(import.meta.url) const debug = makeDebug('@oclif/plugin-plugins:yarn') export class Yarn { + private bin: string | undefined private config: Interfaces.Config private logLevel: LogLevel @@ -31,7 +32,7 @@ export class Yarn { debug(`${options.cwd}: ${bin} ${args.join(' ')}`) try { - const output = await fork(bin, args, options) + const output = await spawn(bin, args, options) debug('yarn done') return output } catch (error: unknown) { @@ -45,6 +46,18 @@ export class Yarn { } private async findYarn(): Promise { - return require.resolve('yarn/bin/yarn.js', {paths: [this.config.root, fileURLToPath(import.meta.url)]}) + if (this.bin) return this.bin + try { + this.bin = require.resolve('yarn/bin/yarn.js', {paths: [this.config.root, fileURLToPath(import.meta.url)]}) + } catch { + const {default: which} = await import('which') + this.bin = await which('yarn') + } + + if (!this.bin) { + throw new Errors.CLIError('yarn not found') + } + + return this.bin } } diff --git a/yarn.lock b/yarn.lock index 13caac9a..cf8f9451 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1994,6 +1994,11 @@ resolved "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz" integrity sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw== +"@types/which@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/which/-/which-3.0.3.tgz#41142ed5a4743128f1bc0b69c46890f0453ddb89" + integrity sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g== + "@types/wrap-ansi@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" @@ -6417,16 +6422,7 @@ string-argv@0.3.2: resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6487,14 +6483,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6917,7 +6906,7 @@ which@^2.0.1: which@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/which/-/which-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== dependencies: isexe "^3.1.1" @@ -6946,7 +6935,7 @@ workerpool@6.2.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -6964,15 +6953,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"