Skip to content

Commit

Permalink
feat: fall back to global npm and yarn if not found (#871)
Browse files Browse the repository at this point in the history
* feat: publish lite version

* test: lite integration tests

* feat: use spawn instead of fork

* chore: clean up

* chore(release): 5.0.22-dev.0 [skip ci]

* ci: checkout before building lite version

* chore(release): 5.0.22-dev.1 [skip ci]

* ci: no more lite version

---------

Co-authored-by: svc-cli-bot <[email protected]>
  • Loading branch information
mdonnalley and svc-cli-bot authored May 15, 2024
1 parent c14596b commit 48dc584
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 70 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}}
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...`

Expand All @@ -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`

Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -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`

Expand All @@ -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]`

Expand Down Expand Up @@ -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`

Expand All @@ -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)_

<!-- commandsstop -->

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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": {
Expand All @@ -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",
Expand Down
27 changes: 19 additions & 8 deletions src/npm.ts
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string> {
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
}
}
3 changes: 1 addition & 2 deletions src/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -330,7 +330,6 @@ export default class Plugins {
}

public async update(): Promise<void> {
// 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

Expand Down
40 changes: 21 additions & 19 deletions src/fork.ts → src/spawn.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,27 +15,29 @@ 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<Output> {
export async function spawn(modulePath: string, args: string[] = [], {cwd, logLevel}: ExecOptions): Promise<Output> {
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(),
// Disable husky hooks because a plugin might be trying to install them, which will
// 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']
Expand All @@ -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)) {
Expand All @@ -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)) {
Expand All @@ -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 {
Expand Down
21 changes: 17 additions & 4 deletions src/yarn.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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) {
Expand All @@ -45,6 +46,18 @@ export class Yarn {
}

private async findYarn(): Promise<string> {
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
}
}
38 changes: 9 additions & 29 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -6417,16 +6422,7 @@ [email protected]:
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==
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -6946,7 +6935,7 @@ [email protected]:
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==
Expand All @@ -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"
Expand Down

0 comments on commit 48dc584

Please sign in to comment.