Skip to content

Commit 48dc584

Browse files
mdonnalleysvc-cli-bot
andauthoredMay 15, 2024
feat: fall back to global npm and yarn if not found (#871)
* 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 <Svc_cli_bot@salesforce.com>
1 parent c14596b commit 48dc584

File tree

8 files changed

+84
-70
lines changed

8 files changed

+84
-70
lines changed
 

‎.github/workflows/test.yml

+7
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ jobs:
4242
matrix:
4343
os: [ubuntu-latest, windows-latest]
4444
test: ['test:integration:install', 'test:integration:link']
45+
no-local-package-managers: [true, false]
4546
exclude:
4647
- os: windows-latest
4748
test: test:integration:link
49+
- no-local-package-managers: true
50+
test: test:integration:link
4851
runs-on: ${{matrix.os}}
4952
steps:
5053
- uses: actions/checkout@v4
@@ -53,5 +56,9 @@ jobs:
5356
node-version: latest
5457
- uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@main
5558
- run: yarn build
59+
- name: Remove package managers
60+
if: ${{matrix.no-local-package-managers}}
61+
run: |
62+
yarn remove yarn npm
5663
- name: Run tests
5764
run: yarn ${{matrix.test}}

‎README.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ EXAMPLES
117117
$ mycli plugins
118118
```
119119

120-
_See code: [src/commands/plugins/index.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/index.ts)_
120+
_See code: [src/commands/plugins/index.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/index.ts)_
121121

122122
## `mycli plugins:inspect PLUGIN...`
123123

@@ -144,7 +144,7 @@ EXAMPLES
144144
$ mycli plugins inspect myplugin
145145
```
146146

147-
_See code: [src/commands/plugins/inspect.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/inspect.ts)_
147+
_See code: [src/commands/plugins/inspect.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/inspect.ts)_
148148

149149
## `mycli plugins install PLUGIN`
150150

@@ -193,7 +193,7 @@ EXAMPLES
193193
$ mycli plugins install someuser/someplugin
194194
```
195195

196-
_See code: [src/commands/plugins/install.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/install.ts)_
196+
_See code: [src/commands/plugins/install.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/install.ts)_
197197

198198
## `mycli plugins link PATH`
199199

@@ -223,7 +223,7 @@ EXAMPLES
223223
$ mycli plugins link myplugin
224224
```
225225

226-
_See code: [src/commands/plugins/link.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/link.ts)_
226+
_See code: [src/commands/plugins/link.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/link.ts)_
227227

228228
## `mycli plugins reset`
229229

@@ -238,7 +238,7 @@ FLAGS
238238
--reinstall Reinstall all plugins after uninstalling.
239239
```
240240

241-
_See code: [src/commands/plugins/reset.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/reset.ts)_
241+
_See code: [src/commands/plugins/reset.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/reset.ts)_
242242

243243
## `mycli plugins uninstall [PLUGIN]`
244244

@@ -266,7 +266,7 @@ EXAMPLES
266266
$ mycli plugins uninstall myplugin
267267
```
268268

269-
_See code: [src/commands/plugins/uninstall.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/uninstall.ts)_
269+
_See code: [src/commands/plugins/uninstall.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/uninstall.ts)_
270270

271271
## `mycli plugins update`
272272

@@ -284,7 +284,7 @@ DESCRIPTION
284284
Update installed plugins.
285285
```
286286

287-
_See code: [src/commands/plugins/update.ts](https://github.com/oclif/plugin-plugins/blob/5.0.21/src/commands/plugins/update.ts)_
287+
_See code: [src/commands/plugins/update.ts](https://github.com/oclif/plugin-plugins/blob/5.0.22-dev.1/src/commands/plugins/update.ts)_
288288

289289
<!-- commandsstop -->
290290

‎package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@oclif/plugin-plugins",
33
"description": "plugins plugin for oclif",
4-
"version": "5.0.21",
4+
"version": "5.0.22-dev.1",
55
"author": "Salesforce",
66
"bugs": "https://github.com/oclif/plugin-plugins/issues",
77
"dependencies": {
@@ -14,6 +14,7 @@
1414
"object-treeify": "^4.0.1",
1515
"semver": "^7.6.2",
1616
"validate-npm-package-name": "^5.0.0",
17+
"which": "^4.0.0",
1718
"yarn": "^1.22.22"
1819
},
1920
"devDependencies": {
@@ -28,6 +29,7 @@
2829
"@types/semver": "^7.5.8",
2930
"@types/sinon": "^17",
3031
"@types/validate-npm-package-name": "^4.0.2",
32+
"@types/which": "^3.0.3",
3133
"chai": "^4.4.1",
3234
"commitlint": "^18",
3335
"eslint": "^8.56.0",

‎src/npm.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import {Interfaces, ux} from '@oclif/core'
1+
import {Errors, Interfaces, ux} from '@oclif/core'
22
import makeDebug from 'debug'
33
import {readFile} from 'node:fs/promises'
44
import {createRequire} from 'node:module'
55
import {join, sep} from 'node:path'
66

7-
import {ExecOptions, Output, fork} from './fork.js'
87
import {LogLevel} from './log-level.js'
8+
import {ExecOptions, Output, spawn} from './spawn.js'
99

1010
const debug = makeDebug('@oclif/plugin-plugins:npm')
1111

@@ -36,7 +36,7 @@ export class NPM {
3636

3737
debug(`${options.cwd}: ${bin} ${args.join(' ')}`)
3838
try {
39-
const output = await fork(bin, args, options)
39+
const output = await spawn(bin, args, options)
4040
debug('npm done')
4141
return output
4242
} catch (error: unknown) {
@@ -64,17 +64,28 @@ export class NPM {
6464

6565
/**
6666
* Get the path to the npm CLI file.
67-
* This will always resolve npm to the pinned version in `@oclif/plugin-plugins/package.json`.
67+
* This will resolve npm to the pinned version in `@oclif/plugin-plugins/package.json` if it exists.
68+
* Otherwise, it will use the globally installed npm.
6869
*
6970
* @returns The path to the `npm/bin/npm-cli.js` file.
7071
*/
7172
private async findNpm(): Promise<string> {
7273
if (this.bin) return this.bin
7374

74-
const npmPjsonPath = createRequire(import.meta.url).resolve('npm/package.json')
75-
const npmPjson = JSON.parse(await readFile(npmPjsonPath, {encoding: 'utf8'}))
76-
const npmPath = npmPjsonPath.slice(0, Math.max(0, npmPjsonPath.lastIndexOf(sep)))
77-
this.bin = join(npmPath, npmPjson.bin.npm)
75+
try {
76+
const npmPjsonPath = createRequire(import.meta.url).resolve('npm/package.json')
77+
const npmPjson = JSON.parse(await readFile(npmPjsonPath, {encoding: 'utf8'}))
78+
const npmPath = npmPjsonPath.slice(0, Math.max(0, npmPjsonPath.lastIndexOf(sep)))
79+
this.bin = join(npmPath, npmPjson.bin.npm)
80+
} catch {
81+
const {default: which} = await import('which')
82+
this.bin = await which('npm')
83+
}
84+
85+
if (!this.bin) {
86+
throw new Errors.CLIError('npm not found')
87+
}
88+
7889
return this.bin
7990
}
8091
}

‎src/plugins.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {basename, dirname, join, resolve} from 'node:path'
77
import {fileURLToPath} from 'node:url'
88
import {gt, valid, validRange} from 'semver'
99

10-
import {Output} from './fork.js'
1110
import {LogLevel} from './log-level.js'
1211
import {NPM} from './npm.js'
12+
import {Output} from './spawn.js'
1313
import {uniqWith} from './util.js'
1414
import {Yarn} from './yarn.js'
1515

@@ -330,7 +330,6 @@ export default class Plugins {
330330
}
331331

332332
public async update(): Promise<void> {
333-
// eslint-disable-next-line unicorn/no-await-expression-member
334333
let plugins = (await this.list()).filter((p): p is Interfaces.PJSON.PluginTypes.User => p.type === 'user')
335334
if (plugins.length === 0) return
336335

‎src/fork.ts ‎src/spawn.ts

+21-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Errors, ux} from '@oclif/core'
22
import makeDebug from 'debug'
3-
import {fork as cpFork} from 'node:child_process'
3+
import {spawn as cpSpawn} from 'node:child_process'
44
import {npmRunPathEnv} from 'npm-run-path'
55

66
import {LogLevel} from './log-level.js'
@@ -15,27 +15,29 @@ export type Output = {
1515
stdout: string[]
1616
}
1717

18-
const debug = makeDebug('@oclif/plugin-plugins:fork')
18+
const debug = makeDebug('@oclif/plugin-plugins:spawn')
1919

20-
export async function fork(modulePath: string, args: string[] = [], {cwd, logLevel}: ExecOptions): Promise<Output> {
20+
export async function spawn(modulePath: string, args: string[] = [], {cwd, logLevel}: ExecOptions): Promise<Output> {
2121
return new Promise((resolve, reject) => {
22-
const forked = cpFork(modulePath, args, {
22+
// 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.
23+
if (process.platform === 'win32' && modulePath.endsWith('.js')) {
24+
args.unshift(`"${modulePath}"`)
25+
modulePath = 'node'
26+
}
27+
28+
debug('modulePath', modulePath)
29+
debug('args', args)
30+
const spawned = cpSpawn(modulePath, args, {
2331
cwd,
2432
env: {
2533
...npmRunPathEnv(),
2634
// Disable husky hooks because a plugin might be trying to install them, which will
2735
// break the install since the install location isn't a .git directory.
2836
HUSKY: '0',
2937
},
30-
execArgv: process.execArgv
31-
.join(' ')
32-
// Remove --loader ts-node/esm from execArgv so that the subprocess doesn't fail if it can't find ts-node.
33-
// The ts-node/esm loader isn't need to execute npm or yarn commands anyways.
34-
.replace('--loader ts-node/esm', '')
35-
.replace('--loader=ts-node/esm', '')
36-
.split(' ')
37-
.filter(Boolean),
38-
stdio: [0, null, null, 'ipc'],
38+
stdio: 'pipe',
39+
windowsVerbatimArguments: true,
40+
...(process.platform === 'win32' && modulePath.toLowerCase().endsWith('.cmd') && {shell: true}),
3941
})
4042

4143
const possibleLastLinesOfNpmInstall = ['up to date', 'added']
@@ -56,8 +58,8 @@ export async function fork(modulePath: string, args: string[] = [], {cwd, logLev
5658
return logLevel !== 'silent'
5759
}
5860

59-
forked.stderr?.setEncoding('utf8')
60-
forked.stderr?.on('data', (d: Buffer) => {
61+
spawned.stderr?.setEncoding('utf8')
62+
spawned.stderr?.on('data', (d: Buffer) => {
6163
const output = d.toString().trim()
6264
stderr.push(output)
6365
if (shouldPrint(output)) {
@@ -66,8 +68,8 @@ export async function fork(modulePath: string, args: string[] = [], {cwd, logLev
6668
} else debug(output)
6769
})
6870

69-
forked.stdout?.setEncoding('utf8')
70-
forked.stdout?.on('data', (d: Buffer) => {
71+
spawned.stdout?.setEncoding('utf8')
72+
spawned.stdout?.on('data', (d: Buffer) => {
7173
const output = d.toString().trim()
7274
stdout.push(output)
7375
if (shouldPrint(output)) {
@@ -76,8 +78,8 @@ export async function fork(modulePath: string, args: string[] = [], {cwd, logLev
7678
} else debug(output)
7779
})
7880

79-
forked.on('error', reject)
80-
forked.on('exit', (code: number) => {
81+
spawned.on('error', reject)
82+
spawned.on('exit', (code: number) => {
8183
if (code === 0) {
8284
resolve({stderr, stdout})
8385
} else {

‎src/yarn.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import {Interfaces, ux} from '@oclif/core'
1+
import {Errors, Interfaces, ux} from '@oclif/core'
22
import makeDebug from 'debug'
33
import {createRequire} from 'node:module'
44
import {fileURLToPath} from 'node:url'
55

6-
import {ExecOptions, Output, fork} from './fork.js'
76
import {LogLevel} from './log-level.js'
7+
import {ExecOptions, Output, spawn} from './spawn.js'
88

99
const require = createRequire(import.meta.url)
1010
const debug = makeDebug('@oclif/plugin-plugins:yarn')
1111

1212
export class Yarn {
13+
private bin: string | undefined
1314
private config: Interfaces.Config
1415
private logLevel: LogLevel
1516

@@ -31,7 +32,7 @@ export class Yarn {
3132

3233
debug(`${options.cwd}: ${bin} ${args.join(' ')}`)
3334
try {
34-
const output = await fork(bin, args, options)
35+
const output = await spawn(bin, args, options)
3536
debug('yarn done')
3637
return output
3738
} catch (error: unknown) {
@@ -45,6 +46,18 @@ export class Yarn {
4546
}
4647

4748
private async findYarn(): Promise<string> {
48-
return require.resolve('yarn/bin/yarn.js', {paths: [this.config.root, fileURLToPath(import.meta.url)]})
49+
if (this.bin) return this.bin
50+
try {
51+
this.bin = require.resolve('yarn/bin/yarn.js', {paths: [this.config.root, fileURLToPath(import.meta.url)]})
52+
} catch {
53+
const {default: which} = await import('which')
54+
this.bin = await which('yarn')
55+
}
56+
57+
if (!this.bin) {
58+
throw new Errors.CLIError('yarn not found')
59+
}
60+
61+
return this.bin
4962
}
5063
}

‎yarn.lock

+9-29
Original file line numberDiff line numberDiff line change
@@ -1994,6 +1994,11 @@
19941994
resolved "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz"
19951995
integrity sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==
19961996

1997+
"@types/which@^3.0.3":
1998+
version "3.0.3"
1999+
resolved "https://registry.yarnpkg.com/@types/which/-/which-3.0.3.tgz#41142ed5a4743128f1bc0b69c46890f0453ddb89"
2000+
integrity sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==
2001+
19972002
"@types/wrap-ansi@^3.0.0":
19982003
version "3.0.0"
19992004
resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd"
@@ -6417,16 +6422,7 @@ string-argv@0.3.2:
64176422
resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz"
64186423
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
64196424

6420-
"string-width-cjs@npm:string-width@^4.2.0":
6421-
version "4.2.3"
6422-
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
6423-
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
6424-
dependencies:
6425-
emoji-regex "^8.0.0"
6426-
is-fullwidth-code-point "^3.0.0"
6427-
strip-ansi "^6.0.1"
6428-
6429-
"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:
6425+
"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:
64306426
version "4.2.3"
64316427
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
64326428
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -6487,14 +6483,7 @@ string_decoder@^1.1.1:
64876483
dependencies:
64886484
safe-buffer "~5.2.0"
64896485

6490-
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
6491-
version "6.0.1"
6492-
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
6493-
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
6494-
dependencies:
6495-
ansi-regex "^5.0.1"
6496-
6497-
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
6486+
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
64986487
version "6.0.1"
64996488
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
65006489
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -6917,7 +6906,7 @@ which@^2.0.1:
69176906

69186907
which@^4.0.0:
69196908
version "4.0.0"
6920-
resolved "https://registry.npmjs.org/which/-/which-4.0.0.tgz"
6909+
resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a"
69216910
integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==
69226911
dependencies:
69236912
isexe "^3.1.1"
@@ -6946,7 +6935,7 @@ workerpool@6.2.1:
69466935
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz"
69476936
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
69486937

6949-
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
6938+
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
69506939
version "7.0.0"
69516940
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
69526941
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -6964,15 +6953,6 @@ wrap-ansi@^6.2.0:
69646953
string-width "^4.1.0"
69656954
strip-ansi "^6.0.0"
69666955

6967-
wrap-ansi@^7.0.0:
6968-
version "7.0.0"
6969-
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
6970-
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
6971-
dependencies:
6972-
ansi-styles "^4.0.0"
6973-
string-width "^4.1.0"
6974-
strip-ansi "^6.0.0"
6975-
69766956
wrap-ansi@^8.1.0:
69776957
version "8.1.0"
69786958
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"

0 commit comments

Comments
 (0)
Please sign in to comment.