Skip to content

Commit a5d1e6c

Browse files
committedApr 11, 2024··
fix: use yarn to install deps when linking yarn plugins
1 parent e1a6eaa commit a5d1e6c

File tree

6 files changed

+276
-128
lines changed

6 files changed

+276
-128
lines changed
 

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"npm-package-arg": "^11.0.1",
1313
"npm-run-path": "^5.3.0",
1414
"semver": "^7.6.0",
15-
"validate-npm-package-name": "^5.0.0"
15+
"validate-npm-package-name": "^5.0.0",
16+
"yarn": "^1.22.22"
1617
},
1718
"devDependencies": {
1819
"@commitlint/config-conventional": "^18",

‎src/fork.ts

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {Errors, ux} from '@oclif/core'
2+
import makeDebug from 'debug'
3+
import {fork as cpFork} from 'node:child_process'
4+
import {npmRunPathEnv} from 'npm-run-path'
5+
6+
import {LogLevel} from './log-level.js'
7+
8+
export type ExecOptions = {
9+
cwd: string
10+
logLevel: LogLevel
11+
}
12+
13+
export type Output = {
14+
stderr: string[]
15+
stdout: string[]
16+
}
17+
18+
const debug = makeDebug('@oclif/plugin-plugins:fork')
19+
20+
export async function fork(modulePath: string, args: string[] = [], {cwd, logLevel}: ExecOptions): Promise<Output> {
21+
return new Promise((resolve, reject) => {
22+
const forked = cpFork(modulePath, args, {
23+
cwd,
24+
env: {
25+
...npmRunPathEnv(),
26+
// Disable husky hooks because a plugin might be trying to install them, which will
27+
// break the install since the install location isn't a .git directory.
28+
HUSKY: '0',
29+
},
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'],
39+
})
40+
41+
const possibleLastLinesOfNpmInstall = ['up to date', 'added']
42+
const stderr: string[] = []
43+
const stdout: string[] = []
44+
const loggedStderr: string[] = []
45+
const loggedStdout: string[] = []
46+
47+
const shouldPrint = (str: string): boolean => {
48+
// For ux cleanliness purposes, don't print the final line of npm install output if
49+
// the log level is 'notice' and there's no other output.
50+
const noOtherOutput = loggedStderr.length === 0 && loggedStdout.length === 0
51+
const isLastLine = possibleLastLinesOfNpmInstall.some((line) => str.startsWith(line))
52+
if (noOtherOutput && isLastLine && logLevel === 'notice') {
53+
return false
54+
}
55+
56+
return logLevel !== 'silent'
57+
}
58+
59+
forked.stderr?.setEncoding('utf8')
60+
forked.stderr?.on('data', (d: Buffer) => {
61+
const output = d.toString().trim()
62+
stderr.push(output)
63+
if (shouldPrint(output)) {
64+
loggedStderr.push(output)
65+
ux.log(output)
66+
} else debug(output)
67+
})
68+
69+
forked.stdout?.setEncoding('utf8')
70+
forked.stdout?.on('data', (d: Buffer) => {
71+
const output = d.toString().trim()
72+
stdout.push(output)
73+
if (shouldPrint(output)) {
74+
loggedStdout.push(output)
75+
ux.log(output)
76+
} else debug(output)
77+
})
78+
79+
forked.on('error', reject)
80+
forked.on('exit', (code: number) => {
81+
if (code === 0) {
82+
resolve({stderr, stdout})
83+
} else {
84+
reject(
85+
new Errors.CLIError(`${modulePath} ${args.join(' ')} exited with code ${code}`, {
86+
suggestions: ['Run with DEBUG=@oclif/plugin-plugins* to see debug output.'],
87+
}),
88+
)
89+
}
90+
})
91+
})
92+
}

‎src/npm.ts

+7-92
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,18 @@
1-
import {Errors, Interfaces, ux} from '@oclif/core'
1+
import {Interfaces, ux} from '@oclif/core'
22
import makeDebug from 'debug'
3-
import {fork as cpFork} from 'node:child_process'
43
import {readFile} from 'node:fs/promises'
54
import {createRequire} from 'node:module'
65
import {join, sep} from 'node:path'
7-
import {npmRunPathEnv} from 'npm-run-path'
86

7+
import {ExecOptions, Output, fork} from './fork.js'
98
import {LogLevel} from './log-level.js'
109

1110
const debug = makeDebug('@oclif/plugin-plugins:npm')
1211

13-
type ExecOptions = {
14-
cwd: string
15-
logLevel: LogLevel
16-
}
17-
1812
type InstallOptions = ExecOptions & {
1913
prod?: boolean
2014
}
2115

22-
export type NpmOutput = {
23-
stderr: string[]
24-
stdout: string[]
25-
}
26-
27-
async function fork(modulePath: string, args: string[] = [], {cwd, logLevel}: ExecOptions): Promise<NpmOutput> {
28-
return new Promise((resolve, reject) => {
29-
const forked = cpFork(modulePath, args, {
30-
cwd,
31-
env: {
32-
...npmRunPathEnv(),
33-
// Disable husky hooks because a plugin might be trying to install them, which will
34-
// break the install since the install location isn't a .git directory.
35-
HUSKY: '0',
36-
},
37-
execArgv: process.execArgv
38-
.join(' ')
39-
// Remove --loader ts-node/esm from execArgv so that the subprocess doesn't fail if it can't find ts-node.
40-
// The ts-node/esm loader isn't need to execute npm commands anyways.
41-
.replace('--loader ts-node/esm', '')
42-
.replace('--loader=ts-node/esm', '')
43-
.split(' ')
44-
.filter(Boolean),
45-
stdio: [0, null, null, 'ipc'],
46-
})
47-
48-
const possibleLastLinesOfNpmInstall = ['up to date', 'added']
49-
const stderr: string[] = []
50-
const stdout: string[] = []
51-
const loggedStderr: string[] = []
52-
const loggedStdout: string[] = []
53-
54-
const shouldPrint = (str: string): boolean => {
55-
// For ux cleanliness purposes, don't print the final line of npm install output if
56-
// the log level is 'notice' and there's no other output.
57-
const noOtherOutput = loggedStderr.length === 0 && loggedStdout.length === 0
58-
const isLastLine = possibleLastLinesOfNpmInstall.some((line) => str.startsWith(line))
59-
if (noOtherOutput && isLastLine && logLevel === 'notice') {
60-
return false
61-
}
62-
63-
return logLevel !== 'silent'
64-
}
65-
66-
forked.stderr?.setEncoding('utf8')
67-
forked.stderr?.on('data', (d: Buffer) => {
68-
const output = d.toString().trim()
69-
stderr.push(output)
70-
if (shouldPrint(output)) {
71-
loggedStderr.push(output)
72-
ux.log(output)
73-
} else debug(output)
74-
})
75-
76-
forked.stdout?.setEncoding('utf8')
77-
forked.stdout?.on('data', (d: Buffer) => {
78-
const output = d.toString().trim()
79-
stdout.push(output)
80-
if (shouldPrint(output)) {
81-
loggedStdout.push(output)
82-
ux.log(output)
83-
} else debug(output)
84-
})
85-
86-
forked.on('error', reject)
87-
forked.on('exit', (code: number) => {
88-
if (code === 0) {
89-
resolve({stderr, stdout})
90-
} else {
91-
reject(
92-
new Errors.CLIError(`${modulePath} ${args.join(' ')} exited with code ${code}`, {
93-
suggestions: ['Run with DEBUG=@oclif/plugin-plugins* to see debug output.'],
94-
}),
95-
)
96-
}
97-
})
98-
})
99-
}
100-
10116
export class NPM {
10217
private bin: string | undefined
10318
private config: Interfaces.Config
@@ -108,7 +23,7 @@ export class NPM {
10823
this.logLevel = logLevel
10924
}
11025

111-
async exec(args: string[] = [], options: ExecOptions): Promise<NpmOutput> {
26+
async exec(args: string[] = [], options: ExecOptions): Promise<Output> {
11227
const bin = await this.findNpm()
11328
debug('npm binary path', bin)
11429

@@ -130,20 +45,20 @@ export class NPM {
13045
}
13146
}
13247

133-
async install(args: string[], opts: InstallOptions): Promise<NpmOutput> {
48+
async install(args: string[], opts: InstallOptions): Promise<Output> {
13449
const prod = opts.prod ? ['--omit', 'dev'] : []
13550
return this.exec(['install', ...args, ...prod, '--no-audit'], opts)
13651
}
13752

138-
async uninstall(args: string[], opts: ExecOptions): Promise<NpmOutput> {
53+
async uninstall(args: string[], opts: ExecOptions): Promise<Output> {
13954
return this.exec(['uninstall', ...args], opts)
14055
}
14156

142-
async update(args: string[], opts: ExecOptions): Promise<NpmOutput> {
57+
async update(args: string[], opts: ExecOptions): Promise<Output> {
14358
return this.exec(['update', ...args], opts)
14459
}
14560

146-
async view(args: string[], opts: ExecOptions): Promise<NpmOutput> {
61+
async view(args: string[], opts: ExecOptions): Promise<Output> {
14762
return this.exec(['view', ...args], {...opts, logLevel: 'silent'})
14863
}
14964

‎src/plugins.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ 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'
1011
import {LogLevel} from './log-level.js'
11-
import {NPM, NpmOutput} from './npm.js'
12+
import {NPM} from './npm.js'
1213
import {uniqWith} from './util.js'
14+
import {Yarn} from './yarn.js'
1315

1416
type UserPJSON = {
1517
dependencies: Record<string, string>
@@ -58,7 +60,7 @@ function extractIssuesLocation(
5860
}
5961
}
6062

61-
function notifyUser(plugin: Config, output: NpmOutput): void {
63+
function notifyUser(plugin: Config, output: Output): void {
6264
const containsWarnings = [...output.stdout, ...output.stderr].some((l) => l.includes('npm WARN'))
6365
if (containsWarnings) {
6466
ux.logToStderr(chalk.bold.yellow(`\nThese warnings can only be addressed by the owner(s) of ${plugin.name}.`))
@@ -231,11 +233,30 @@ export default class Plugins {
231233
this.isValidPlugin(c)
232234

233235
if (install) {
234-
await this.npm.install([], {
235-
cwd: c.root,
236-
logLevel: this.logLevel,
237-
prod: false,
238-
})
236+
if (await fileExists(join(c.root, 'yarn.lock'))) {
237+
this.debug('installing dependencies with yarn')
238+
const yarn = new Yarn({config: this.config, logLevel: this.logLevel})
239+
await yarn.install([], {
240+
cwd: c.root,
241+
logLevel: this.logLevel,
242+
prod: false,
243+
})
244+
} else if (await fileExists(join(c.root, 'package-lock.json'))) {
245+
this.debug('installing dependencies with npm')
246+
await this.npm.install([], {
247+
cwd: c.root,
248+
logLevel: this.logLevel,
249+
prod: false,
250+
})
251+
} else if (await fileExists(join(c.root, 'pnpm-lock.yaml'))) {
252+
ux.warn(
253+
`pnpm is not supported for installing after a link. The link succeeded, but you may need to run 'pnpm install' in ${c.root}.`,
254+
)
255+
} else {
256+
ux.warn(
257+
`No lockfile found in ${c.root}. The link succeeded, but you may need to install the dependencies in your project.`,
258+
)
259+
}
239260
}
240261

241262
await this.add({name: c.name, root: c.root, type: 'link'})

0 commit comments

Comments
 (0)
Please sign in to comment.