-
Notifications
You must be signed in to change notification settings - Fork 22
/
util.ts
178 lines (151 loc) · 5.73 KB
/
util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import {Interfaces, ux} from '@oclif/core'
import * as fs from 'node:fs'
import * as fsPromises from 'node:fs/promises'
import {createRequire} from 'node:module'
import {type} from 'node:os'
import * as path from 'node:path'
type CompareTypes = boolean | number | string | undefined
function compare(a: CompareTypes | CompareTypes[], b: CompareTypes | CompareTypes[]): number {
const itemA = a === undefined ? 0 : a
const itemB = b === undefined ? 0 : b
if (Array.isArray(itemA) && Array.isArray(itemB)) {
if (itemA.length === 0 && itemB.length === 0) return 0
const diff = compare(itemA[0], itemB[0])
if (diff !== 0) return diff
return compare(itemA.slice(1), itemB.slice(1))
}
if (itemA < itemB) return -1
if (itemA > itemB) return 1
return 0
}
export function sortBy<T>(arr: T[], fn: (i: T) => CompareTypes | CompareTypes[]): T[] {
return arr.sort((a, b) => compare(fn(a), fn(b)))
}
export function uniq<T>(arr: T[]): T[] {
return arr.filter((a, i) => arr.indexOf(a) === i)
}
export function uniqWith<T>(arr: T[], fn: (a: T, b: T) => boolean): T[] {
return arr.filter((a, i) => !arr.some((b, j) => j > i && fn(a, b)))
}
const isExecutable = (filepath: string): boolean => {
if (type() === 'Windows_NT') return filepath.endsWith('node.exe')
try {
if (filepath.endsWith('node')) {
// This checks if the filepath is executable on Mac or Linux, if it is not it errors.
fs.accessSync(filepath, fs.constants.X_OK)
return true
}
} catch {
return false
}
return false
}
/**
* Get the path to the node executable
* If using a macos/windows/tarball installer it will use the node version included in it.
* If that fails (or CLI was installed via npm), this will resolve to the global node installed in the system.
* @param root - The root path of the CLI (this.config.root).
* @returns The path to the node executable.
*/
export async function findNode(root: string): Promise<string> {
const cliBinDirs = [path.join(root, 'bin'), path.join(root, 'client', 'bin')].filter((p) => fs.existsSync(p))
const {default: shelljs} = await import('shelljs')
if (cliBinDirs.length > 0) {
// Find the node executable
// eslint-disable-next-line unicorn/no-array-callback-reference
const node = shelljs.find(cliBinDirs).find((file: string) => isExecutable(file))
if (node) {
return fs.realpathSync(node)
}
}
// Check to see if node is installed
const nodeShellString = shelljs.which('node')
if (nodeShellString?.code === 0 && nodeShellString?.stdout) {
return `${nodeShellString.stdout}`
}
const err = new Error('Cannot locate node executable.')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore override readonly .name field
err.name = 'CannotFindNodeExecutable'
throw err
}
/**
* Get the path to the npm CLI file.
* This will always resolve npm to the pinned version in `@oclif/plugin-plugins/package.json`.
*
* @returns The path to the `npm/bin/npm-cli.js` file.
*/
export async function findNpm(): Promise<string> {
const require = createRequire(import.meta.url)
const npmPjsonPath = require.resolve('npm/package.json')
const npmPjson = JSON.parse(await fsPromises.readFile(npmPjsonPath, {encoding: 'utf8'}))
const npmPath = npmPjsonPath.slice(0, Math.max(0, npmPjsonPath.lastIndexOf(path.sep)))
return path.join(npmPath, npmPjson.bin.npm)
}
export class YarnMessagesCache {
private static errors = new Set<string>()
private static instance: YarnMessagesCache
private static warnings = new Set<string>()
public static getInstance(): YarnMessagesCache {
if (!YarnMessagesCache.instance) {
YarnMessagesCache.instance = new YarnMessagesCache()
}
return YarnMessagesCache.instance
}
public addErrors(...errors: string[]): void {
for (const err of errors) {
YarnMessagesCache.errors.add(err)
}
}
public addWarnings(...warnings: string[]): void {
for (const warning of warnings) {
// Skip workspaces warning because it's likely the fault of a transitive dependency and not actionable
// https://github.com/yarnpkg/yarn/issues/8580
if (warning.includes('Workspaces can only be enabled in private projects.')) continue
YarnMessagesCache.warnings.add(warning)
}
}
public flush(plugin?: Interfaces.Config | undefined): void {
if (YarnMessagesCache.warnings.size === 0) return
let count = 0
for (const warning of YarnMessagesCache.warnings) {
if (!plugin) {
ux.warn(warning)
count += 1
return
}
// If flushing for a specific plugin, only show warnings that are specific to that plugin.
if (warning.startsWith(plugin.name) || warning.startsWith(`"${plugin.name}`)) {
count += 1
ux.warn(warning)
}
}
if (plugin && count > 0) {
ux.logToStderr(`\nThese warnings can only be addressed by the owner(s) of ${plugin.name}.`)
if (plugin.pjson.bugs || plugin.pjson.repository) {
ux.logToStderr(
`We suggest that you create an issue at ${extractIssuesLocation(
plugin.pjson.bugs,
plugin.pjson.repository,
)} and ask the plugin owners to address them.\n`,
)
}
}
if (YarnMessagesCache.errors.size === 0) return
ux.logToStderr('\nThe following errors occurred:')
for (const err of YarnMessagesCache.errors) {
ux.error(err, {exit: false})
}
}
}
export function extractIssuesLocation(
bugs: {url: string} | string | undefined,
repository: {type: string; url: string} | string | undefined,
): string | undefined {
if (bugs) {
return typeof bugs === 'string' ? bugs : bugs.url
}
if (repository) {
return typeof repository === 'string' ? repository : repository.url.replace('git+', '').replace('.git', '')
}
}