-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathmisc-utils.ts
213 lines (193 loc) · 6.5 KB
/
misc-utils.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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import which from 'which';
import { execa, Options } from 'execa';
import createDebug from 'debug';
import { ErrorWithCause } from 'pony-cause';
import { isObject } from '@metamask/utils';
export { isTruthyString } from '@metamask/action-utils';
export { hasProperty, isNullOrUndefined } from '@metamask/utils';
export { isObject };
/**
* A logger object for the implementation part of this project.
*
* @see The [debug](https://www.npmjs.com/package/debug) package.
*/
export const debug = createDebug('create-release-branch:impl');
/**
* Matches URLs in the formats:
*
* - "https://github.com/OrganizationName/RepoName"
* - "https://github.com/OrganizationName/RepoName.git"
*/
const HTTPS_GITHUB_URL_REGEX =
/^https:\/\/github\.com\/(.+?)\/(.+?)(?:\.git)?$/u;
/**
* Matches a URL in the format "[email protected]/OrganizationName/RepoName.git".
*/
const SSH_GITHUB_URL_REGEX = /^git@github\.com:(.+?)\/(.+?)\.git$/u;
/**
* Type guard for determining whether the given value is an instance of Error.
* For errors generated via `fs.promises`, `error instanceof Error` won't work,
* so we have to come up with another way of testing.
*
* @param error - The object to check.
* @returns True or false, depending on the result.
*/
function isError(error: unknown): error is Error {
return (
error instanceof Error ||
(isObject(error) && error.constructor.name === 'Error')
);
}
/**
* Type guard for determining whether the given value is an error object with a
* `code` property such as the type of error that Node throws for filesystem
* operations, etc.
*
* @param error - The object to check.
* @returns True or false, depending on the result.
*/
export function isErrorWithCode(error: unknown): error is { code: string } {
return typeof error === 'object' && error !== null && 'code' in error;
}
/**
* Type guard for determining whether the given value is an error object with a
* `message` property, such as an instance of Error.
*
* @param error - The object to check.
* @returns True or false, depending on the result.
*/
export function isErrorWithMessage(
error: unknown,
): error is { message: string } {
return typeof error === 'object' && error !== null && 'message' in error;
}
/**
* Type guard for determining whether the given value is an error object with a
* `stack` property, such as an instance of Error.
*
* @param error - The object to check.
* @returns True or false, depending on the result.
*/
export function isErrorWithStack(error: unknown): error is { stack: string } {
return typeof error === 'object' && error !== null && 'stack' in error;
}
/**
* Builds a new error object, linking to the original error via the `cause`
* property if it is an Error.
*
* This function is useful to reframe error messages in general, but is
* _critical_ when interacting with any of Node's filesystem functions as
* provided via `fs.promises`, because these do not produce stack traces in the
* case of an I/O error (see <https://github.com/nodejs/node/issues/30944>).
*
* @param message - The desired message of the new error.
* @param originalError - The error that you want to cover (either an Error or
* something throwable).
* @returns A new error object.
*/
export function wrapError(message: string, originalError: unknown) {
if (isError(originalError)) {
const error: any = new ErrorWithCause(message, { cause: originalError });
if (isErrorWithCode(originalError)) {
error.code = originalError.code;
}
return error;
}
return new Error(`${message}: ${originalError}`);
}
/**
* Retrieves the real path of an executable via `which`.
*
* @param executablePath - The path to an executable.
* @returns The resolved path to the executable.
* @throws what `which` throws if it is not a "not found" error.
*/
export async function resolveExecutable(
executablePath: string,
): Promise<string | null> {
try {
return await which(executablePath);
} catch (error) {
if (
isErrorWithMessage(error) &&
new RegExp(`^not found: ${executablePath}$`, 'u').test(error.message)
) {
return null;
}
throw error;
}
}
/**
* Runs a command, discarding its output.
*
* @param command - The command to execute.
* @param args - The positional arguments to the command.
* @param options - The options to `execa`.
* @throws An `execa` error object if the command fails in some way.
* @see `execa`.
*/
export async function runCommand(
command: string,
args?: readonly string[] | undefined,
options?: Options | undefined,
): Promise<void> {
await execa(command, args, options);
}
/**
* Runs a command, retrieving the standard output with leading and trailing
* whitespace removed.
*
* @param command - The command to execute.
* @param args - The positional arguments to the command.
* @param options - The options to `execa`.
* @returns The standard output of the command.
* @throws An `execa` error object if the command fails in some way.
* @see `execa`.
*/
export async function getStdoutFromCommand(
command: string,
args?: readonly string[] | undefined,
options?: Options | undefined,
): Promise<string> {
return (await execa(command, args, options)).stdout.trim();
}
/**
* Run a command, splitting up the immediate output into lines.
*
* @param command - The command to execute.
* @param args - The positional arguments to the command.
* @param options - The options to `execa`.
* @returns The standard output of the command.
* @throws An `execa` error object if the command fails in some way.
* @see `execa`.
*/
export async function getLinesFromCommand(
command: string,
args?: readonly string[] | undefined,
options?: Options | undefined,
): Promise<string[]> {
const { stdout } = await execa(command, args, options);
return stdout.split('\n').filter((value) => value !== '');
}
/**
* Converts the given GitHub repository URL to its HTTPS version.
*
* A "GitHub repository URL" looks like one of:
*
* - https://github.com/OrganizationName/RepositoryName
* - [email protected]:OrganizationName/RepositoryName.git
*
* If the URL does not match either of these patterns, an error is thrown.
*
* @param url - The URL to convert.
* @returns The HTTPS URL of the repository, e.g.
* `https://github.com/OrganizationName/RepositoryName`.
*/
export function convertToHttpsGitHubRepositoryUrl(url: string): string {
const match =
url.match(HTTPS_GITHUB_URL_REGEX) ?? url.match(SSH_GITHUB_URL_REGEX);
if (match) {
return `https://github.com/${match[1]}/${match[2]}`;
}
throw new Error(`Unrecognized repository URL: ${url}`);
}