@appium/support 7.0.4 → 7.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/console.d.ts +42 -88
- package/build/lib/console.d.ts.map +1 -1
- package/build/lib/console.js +25 -85
- package/build/lib/console.js.map +1 -1
- package/build/lib/doctor.d.ts +6 -18
- package/build/lib/doctor.d.ts.map +1 -1
- package/build/lib/doctor.js +0 -15
- package/build/lib/doctor.js.map +1 -1
- package/build/lib/env.d.ts +14 -20
- package/build/lib/env.d.ts.map +1 -1
- package/build/lib/env.js +24 -61
- package/build/lib/env.js.map +1 -1
- package/build/lib/fs.d.ts +109 -148
- package/build/lib/fs.d.ts.map +1 -1
- package/build/lib/fs.js +130 -230
- package/build/lib/fs.js.map +1 -1
- package/build/lib/image-util.d.ts +7 -6
- package/build/lib/image-util.d.ts.map +1 -1
- package/build/lib/image-util.js +9 -6
- package/build/lib/image-util.js.map +1 -1
- package/build/lib/index.d.ts +19 -17
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/logger.d.ts +1 -1
- package/build/lib/logger.d.ts.map +1 -1
- package/build/lib/logger.js +1 -1
- package/build/lib/logger.js.map +1 -1
- package/build/lib/logging.d.ts +7 -15
- package/build/lib/logging.d.ts.map +1 -1
- package/build/lib/logging.js +36 -62
- package/build/lib/logging.js.map +1 -1
- package/build/lib/mjpeg.d.ts +19 -56
- package/build/lib/mjpeg.d.ts.map +1 -1
- package/build/lib/mjpeg.js +55 -78
- package/build/lib/mjpeg.js.map +1 -1
- package/build/lib/mkdirp.d.ts +4 -1
- package/build/lib/mkdirp.d.ts.map +1 -1
- package/build/lib/mkdirp.js +1 -2
- package/build/lib/mkdirp.js.map +1 -1
- package/build/lib/net.d.ts +52 -90
- package/build/lib/net.d.ts.map +1 -1
- package/build/lib/net.js +104 -193
- package/build/lib/net.js.map +1 -1
- package/build/lib/node.d.ts +16 -17
- package/build/lib/node.d.ts.map +1 -1
- package/build/lib/node.js +115 -120
- package/build/lib/node.js.map +1 -1
- package/build/lib/npm.d.ts +65 -86
- package/build/lib/npm.d.ts.map +1 -1
- package/build/lib/npm.js +64 -122
- package/build/lib/npm.js.map +1 -1
- package/build/lib/plist.d.ts +36 -29
- package/build/lib/plist.d.ts.map +1 -1
- package/build/lib/plist.js +62 -59
- package/build/lib/plist.js.map +1 -1
- package/build/lib/process.d.ts +19 -2
- package/build/lib/process.d.ts.map +1 -1
- package/build/lib/process.js +24 -7
- package/build/lib/process.js.map +1 -1
- package/build/lib/system.d.ts +41 -6
- package/build/lib/system.d.ts.map +1 -1
- package/build/lib/system.js +49 -14
- package/build/lib/system.js.map +1 -1
- package/build/lib/tempdir.d.ts +26 -49
- package/build/lib/tempdir.d.ts.map +1 -1
- package/build/lib/tempdir.js +46 -78
- package/build/lib/tempdir.js.map +1 -1
- package/build/lib/timing.d.ts +28 -22
- package/build/lib/timing.d.ts.map +1 -1
- package/build/lib/timing.js +16 -17
- package/build/lib/timing.js.map +1 -1
- package/build/lib/util.d.ts +164 -181
- package/build/lib/util.d.ts.map +1 -1
- package/build/lib/util.js +198 -253
- package/build/lib/util.js.map +1 -1
- package/build/lib/zip.d.ts +81 -139
- package/build/lib/zip.d.ts.map +1 -1
- package/build/lib/zip.js +235 -283
- package/build/lib/zip.js.map +1 -1
- package/lib/console.ts +139 -0
- package/lib/{doctor.js → doctor.ts} +6 -20
- package/lib/{env.js → env.ts} +34 -62
- package/lib/fs.ts +453 -0
- package/lib/image-util.ts +40 -0
- package/lib/index.ts +1 -0
- package/lib/{logger.js → logger.ts} +1 -1
- package/lib/logging.ts +157 -0
- package/lib/mjpeg.ts +186 -0
- package/lib/{mkdirp.js → mkdirp.ts} +2 -2
- package/lib/net.ts +305 -0
- package/lib/{node.js → node.ts} +136 -135
- package/lib/npm.ts +291 -0
- package/lib/plist.ts +187 -0
- package/lib/process.ts +62 -0
- package/lib/system.ts +95 -0
- package/lib/tempdir.ts +115 -0
- package/lib/{timing.js → timing.ts} +28 -33
- package/lib/util.ts +561 -0
- package/lib/{zip.js → zip.ts} +344 -299
- package/package.json +24 -26
- package/tsconfig.json +3 -5
- package/index.js +0 -1
- package/lib/console.js +0 -173
- package/lib/fs.js +0 -496
- package/lib/image-util.js +0 -32
- package/lib/logging.js +0 -145
- package/lib/mjpeg.js +0 -207
- package/lib/net.js +0 -336
- package/lib/npm.js +0 -310
- package/lib/plist.js +0 -182
- package/lib/process.js +0 -46
- package/lib/system.js +0 -48
- package/lib/tempdir.js +0 -131
- package/lib/util.js +0 -585
package/lib/util.ts
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import B from 'bluebird';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import stream from 'node:stream';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import {asyncmap} from 'asyncbox';
|
|
8
|
+
import {fs} from './fs';
|
|
9
|
+
import * as semver from 'semver';
|
|
10
|
+
import {quote as shellQuote, parse as shellParse} from 'shell-quote';
|
|
11
|
+
export {shellParse};
|
|
12
|
+
import pluralizeLib from 'pluralize';
|
|
13
|
+
import {Base64Encode} from 'base64-stream';
|
|
14
|
+
export {v1 as uuidV1, v3 as uuidV3, v4 as uuidV4, v5 as uuidV5} from 'uuid';
|
|
15
|
+
import * as _lockfile from 'lockfile';
|
|
16
|
+
import type {Element} from '@appium/types';
|
|
17
|
+
|
|
18
|
+
/** W3C WebDriver element identifier key used in element objects. */
|
|
19
|
+
export const W3C_WEB_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
|
|
20
|
+
|
|
21
|
+
/** Size of one kibibyte in bytes (1024). */
|
|
22
|
+
export const KiB = 1024;
|
|
23
|
+
/** Size of one mebibyte in bytes (1024 * 1024). */
|
|
24
|
+
export const MiB = KiB * 1024;
|
|
25
|
+
/** Size of one gibibyte in bytes (1024 * 1024 * 1024). */
|
|
26
|
+
export const GiB = MiB * 1024;
|
|
27
|
+
|
|
28
|
+
/** A string which is never `''`. */
|
|
29
|
+
export type NonEmptyString<T extends string = string> = T extends '' ? never : T;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Type guard: returns true if the value is a non-empty string.
|
|
33
|
+
*
|
|
34
|
+
* @param val - Value to check
|
|
35
|
+
* @returns `true` if `val` is a string with at least one character
|
|
36
|
+
*/
|
|
37
|
+
export function hasContent(val: unknown): val is NonEmptyString {
|
|
38
|
+
return _.isString(val) && val !== '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Type guard: returns true if the value is not `undefined`, `null`, or `NaN`.
|
|
43
|
+
*
|
|
44
|
+
* @param val - Value to check
|
|
45
|
+
* @returns `true` if `val` is non-null and non-undefined (and not NaN for numbers)
|
|
46
|
+
*/
|
|
47
|
+
export function hasValue<T>(val: T): val is NonNullable<T> {
|
|
48
|
+
if (_.isNumber(val)) {
|
|
49
|
+
return !_.isNaN(val);
|
|
50
|
+
}
|
|
51
|
+
return !_.isUndefined(val) && !_.isNull(val);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Escapes spaces in a string for use in command-line arguments (e.g. ` ` → `\ `).
|
|
56
|
+
*
|
|
57
|
+
* @param str - String that may contain spaces
|
|
58
|
+
* @returns String with spaces escaped by a backslash
|
|
59
|
+
*/
|
|
60
|
+
export function escapeSpace(str: string): string {
|
|
61
|
+
return str.split(/ /).join('\\ ');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Escapes special characters in a string (backslash, slash, quotes, control chars).
|
|
66
|
+
* If `quoteEscape` is provided, that character is also escaped.
|
|
67
|
+
*
|
|
68
|
+
* @param str - String to escape, or non-string value (returned unchanged)
|
|
69
|
+
* @param quoteEscape - Optional character to escape, or `false` to skip
|
|
70
|
+
* @returns Escaped string, or original value if `str` is not a string
|
|
71
|
+
*/
|
|
72
|
+
export function escapeSpecialChars(
|
|
73
|
+
str: string | unknown,
|
|
74
|
+
quoteEscape?: string | false
|
|
75
|
+
): string | unknown {
|
|
76
|
+
if (typeof str !== 'string') {
|
|
77
|
+
return str;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = str
|
|
81
|
+
.replace(/[\\]/g, '\\\\')
|
|
82
|
+
.replace(/[/]/g, '\\/')
|
|
83
|
+
.replace(/[\b]/g, '\\b')
|
|
84
|
+
.replace(/[\f]/g, '\\f')
|
|
85
|
+
.replace(/[\n]/g, '\\n')
|
|
86
|
+
.replace(/[\r]/g, '\\r')
|
|
87
|
+
.replace(/[\t]/g, '\\t')
|
|
88
|
+
.replace(/["]/g, '\\"')
|
|
89
|
+
.replace(/\\'/g, "\\'");
|
|
90
|
+
if (!quoteEscape) {
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
const re = new RegExp(quoteEscape, 'g');
|
|
94
|
+
return result.replace(re, `\\${quoteEscape}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns the first non-internal IPv4 address of the machine, if any.
|
|
99
|
+
*
|
|
100
|
+
* @returns The local IPv4 address, or `undefined` if none found
|
|
101
|
+
*/
|
|
102
|
+
export function localIp(): string | undefined {
|
|
103
|
+
const ifaces = os.networkInterfaces();
|
|
104
|
+
for (const addrs of Object.values(ifaces)) {
|
|
105
|
+
if (!addrs) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const iface of addrs) {
|
|
109
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
110
|
+
return iface.address;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a promise that resolves after a delay and can be cancelled via `.cancel()`.
|
|
119
|
+
*
|
|
120
|
+
* @param ms - Delay in milliseconds before the promise resolves
|
|
121
|
+
* @returns A Bluebird promise with a `cancel()` method; cancel rejects with CancellationError
|
|
122
|
+
*/
|
|
123
|
+
// TODO: replace with a native implementation in Appium 4
|
|
124
|
+
export function cancellableDelay(ms: number): B<void> & {cancel: () => void} {
|
|
125
|
+
let timer: NodeJS.Timeout;
|
|
126
|
+
let resolve: () => void;
|
|
127
|
+
let reject: (err: Error) => void;
|
|
128
|
+
|
|
129
|
+
const delay = new B<void>((_resolve, _reject) => {
|
|
130
|
+
resolve = _resolve;
|
|
131
|
+
reject = _reject;
|
|
132
|
+
timer = setTimeout(() => resolve(), ms);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
(delay as B<void> & {cancel: () => void}).cancel = function () {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
reject(new B.CancellationError());
|
|
138
|
+
};
|
|
139
|
+
return delay as B<void> & {cancel: () => void};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolves each root path with the given path segments, returning an array of absolute paths.
|
|
144
|
+
*
|
|
145
|
+
* @param roots - Base directory paths to resolve against
|
|
146
|
+
* @param args - Path segments to join with each root (e.g. 'foo', 'bar' → root/foo/bar)
|
|
147
|
+
* @returns Array of absolute paths, one per root
|
|
148
|
+
*/
|
|
149
|
+
export function multiResolve(roots: string[], ...args: string[]): string[] {
|
|
150
|
+
return roots.map((root) => path.resolve(root, ...args));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parses a value as JSON if it is a string; otherwise returns the value as-is.
|
|
155
|
+
*
|
|
156
|
+
* @param obj - String (to parse) or other value (returned unchanged)
|
|
157
|
+
* @returns Parsed object or original value
|
|
158
|
+
*/
|
|
159
|
+
export function safeJsonParse<T>(obj: unknown): T {
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(obj as string) as T;
|
|
162
|
+
} catch {
|
|
163
|
+
return obj as T;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Stringifies an object to JSON, converting Buffers to strings for readable output.
|
|
169
|
+
*
|
|
170
|
+
* @param obj - Object to serialize
|
|
171
|
+
* @param replacer - Optional replacer function (same as JSON.stringify)
|
|
172
|
+
* @param space - Indentation for pretty-printing. Defaults to 2
|
|
173
|
+
* @returns JSON string
|
|
174
|
+
*/
|
|
175
|
+
export function jsonStringify(
|
|
176
|
+
obj: unknown,
|
|
177
|
+
replacer: ((key: string, value: unknown) => unknown) | null = null,
|
|
178
|
+
space: number | string = 2
|
|
179
|
+
): string {
|
|
180
|
+
const replacerFunc = _.isFunction(replacer) ? replacer : (_k: string, v: unknown) => v;
|
|
181
|
+
|
|
182
|
+
const bufferToJSON = Buffer.prototype.toJSON;
|
|
183
|
+
delete (Buffer.prototype as Record<string, unknown>).toJSON;
|
|
184
|
+
try {
|
|
185
|
+
return JSON.stringify(
|
|
186
|
+
obj,
|
|
187
|
+
(key, value) => {
|
|
188
|
+
const updatedValue = Buffer.isBuffer(value) ? value.toString('utf8') : value;
|
|
189
|
+
return replacerFunc(key, updatedValue);
|
|
190
|
+
},
|
|
191
|
+
space
|
|
192
|
+
);
|
|
193
|
+
} finally {
|
|
194
|
+
(Buffer.prototype as Record<string, unknown>).toJSON = bufferToJSON;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Extracts the element ID from a W3C or JSONWP element object, or returns the string if already an ID.
|
|
200
|
+
*
|
|
201
|
+
* @param el - Element object (with ELEMENT or W3C identifier) or raw element ID string
|
|
202
|
+
* @returns The element ID string
|
|
203
|
+
*/
|
|
204
|
+
export function unwrapElement(el: Element | string): string {
|
|
205
|
+
const elObj = el as unknown as Record<string, string>;
|
|
206
|
+
for (const propName of [W3C_WEB_ELEMENT_IDENTIFIER, 'ELEMENT']) {
|
|
207
|
+
if (_.has(elObj, propName)) {
|
|
208
|
+
return elObj[propName];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return el as string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Wraps an element ID string in an element object compatible with both W3C and JSONWP.
|
|
216
|
+
*
|
|
217
|
+
* @param elementId - The element ID to wrap
|
|
218
|
+
* @returns Element object with both ELEMENT and W3C identifier keys
|
|
219
|
+
*/
|
|
220
|
+
export function wrapElement(elementId: string): Element {
|
|
221
|
+
return {
|
|
222
|
+
ELEMENT: elementId,
|
|
223
|
+
[W3C_WEB_ELEMENT_IDENTIFIER]: elementId,
|
|
224
|
+
} as Element;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Returns a copy of the object containing only properties that pass the predicate.
|
|
229
|
+
* If the predicate is missing, removes properties whose values are undefined.
|
|
230
|
+
* If the predicate is a scalar, keeps only properties whose value equals that scalar.
|
|
231
|
+
* If the predicate is a function, calls it for each (value, obj) and keeps properties where it returns true.
|
|
232
|
+
*
|
|
233
|
+
* @param obj - Source object to filter
|
|
234
|
+
* @param predicate - Optional filter: undefined (drop undefined values), scalar (value match), or function
|
|
235
|
+
* @returns New object with only the properties that pass the predicate
|
|
236
|
+
*/
|
|
237
|
+
export function filterObject<T extends Record<string, unknown>>(
|
|
238
|
+
obj: T,
|
|
239
|
+
predicate?: ((value: unknown, obj: T) => boolean) | unknown
|
|
240
|
+
): Partial<T> {
|
|
241
|
+
const newObj = _.clone(obj) as Record<string, unknown>;
|
|
242
|
+
let pred: (v: unknown, o: T) => boolean;
|
|
243
|
+
if (_.isUndefined(predicate)) {
|
|
244
|
+
pred = (v) => !_.isUndefined(v);
|
|
245
|
+
} else if (!_.isFunction(predicate)) {
|
|
246
|
+
const valuePredicate = predicate;
|
|
247
|
+
pred = (v) => v === valuePredicate;
|
|
248
|
+
} else {
|
|
249
|
+
pred = predicate as (v: unknown, o: T) => boolean;
|
|
250
|
+
}
|
|
251
|
+
for (const key of Object.keys(obj)) {
|
|
252
|
+
if (!pred(obj[key], obj)) {
|
|
253
|
+
delete newObj[key];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return newObj as Partial<T>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Converts a byte count to a human-readable size string (e.g. "1.50 MB").
|
|
261
|
+
*
|
|
262
|
+
* @param bytes - Number of bytes (or string coercible to a number)
|
|
263
|
+
* @returns Formatted string like "123 B", "1.50 KB", "2.00 MB", "3.00 GB"
|
|
264
|
+
* @throws {Error} If bytes cannot be converted to a non-negative integer
|
|
265
|
+
*/
|
|
266
|
+
export function toReadableSizeString(bytes: number | string): string {
|
|
267
|
+
const intBytes = parseInt(String(bytes), 10);
|
|
268
|
+
if (isNaN(intBytes) || intBytes < 0) {
|
|
269
|
+
throw new Error(`Cannot convert '${bytes}' to a readable size format`);
|
|
270
|
+
}
|
|
271
|
+
if (intBytes >= GiB) {
|
|
272
|
+
return `${(intBytes / (GiB * 1.0)).toFixed(2)} GB`;
|
|
273
|
+
} else if (intBytes >= MiB) {
|
|
274
|
+
return `${(intBytes / (MiB * 1.0)).toFixed(2)} MB`;
|
|
275
|
+
} else if (intBytes >= KiB) {
|
|
276
|
+
return `${(intBytes / (KiB * 1.0)).toFixed(2)} KB`;
|
|
277
|
+
}
|
|
278
|
+
return `${intBytes} B`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Checks whether the given path is a subpath of the given root folder.
|
|
283
|
+
*
|
|
284
|
+
* @param originalPath - The absolute file or folder path to test
|
|
285
|
+
* @param root - The absolute root folder path
|
|
286
|
+
* @param forcePosix - If true, interpret paths in POSIX format (e.g. on Windows)
|
|
287
|
+
* @returns `true` if `originalPath` is under `root`
|
|
288
|
+
* @throws {Error} If either path is not absolute
|
|
289
|
+
*/
|
|
290
|
+
export function isSubPath(
|
|
291
|
+
originalPath: string,
|
|
292
|
+
root: string,
|
|
293
|
+
forcePosix: boolean | null = null
|
|
294
|
+
): boolean {
|
|
295
|
+
const pathObj = forcePosix ? path.posix : path;
|
|
296
|
+
for (const p of [originalPath, root]) {
|
|
297
|
+
if (!pathObj.isAbsolute(p)) {
|
|
298
|
+
throw new Error(`'${p}' is expected to be an absolute path`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const normalizedRoot = pathObj.normalize(root);
|
|
302
|
+
const normalizedPath = pathObj.normalize(originalPath);
|
|
303
|
+
return normalizedPath.startsWith(normalizedRoot);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Checks whether the given paths refer to the same file system entity (same inode).
|
|
308
|
+
* All paths must exist.
|
|
309
|
+
*
|
|
310
|
+
* @param path1 - First path
|
|
311
|
+
* @param path2 - Second path
|
|
312
|
+
* @param pathN - Additional paths to compare
|
|
313
|
+
* @returns `true` if all paths resolve to the same file/directory
|
|
314
|
+
*/
|
|
315
|
+
export async function isSameDestination(
|
|
316
|
+
path1: string,
|
|
317
|
+
path2: string,
|
|
318
|
+
...pathN: string[]
|
|
319
|
+
): Promise<boolean> {
|
|
320
|
+
const allPaths = [path1, path2, ...pathN];
|
|
321
|
+
if (!(await asyncmap(allPaths, async (p) => fs.exists(p))).every(Boolean)) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const areAllItemsEqual = (arr: unknown[]) => !!arr.reduce((a, b) => (a === b ? a : NaN));
|
|
326
|
+
if (areAllItemsEqual(allPaths)) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const mapCb = async (x: string) => (await fs.stat(x, {bigint: true})).ino;
|
|
331
|
+
return areAllItemsEqual(await asyncmap(allPaths, mapCb));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Coerces a value to a valid semver string (e.g. "1.0" → "1.0.0").
|
|
336
|
+
*
|
|
337
|
+
* @param ver - Version string or number to coerce
|
|
338
|
+
* @param strict - If true, throws when coercion fails; if false, returns null
|
|
339
|
+
* @returns Valid semver string, or null when strict is false and coercion fails
|
|
340
|
+
* @throws {Error} When strict is true and ver cannot be coerced
|
|
341
|
+
*/
|
|
342
|
+
export function coerceVersion(ver: string, strict: true): string;
|
|
343
|
+
export function coerceVersion(ver: string, strict?: false): string | null;
|
|
344
|
+
export function coerceVersion(ver: string, strict = true): string | null {
|
|
345
|
+
let result = semver.valid(`${ver}`);
|
|
346
|
+
if (!result) {
|
|
347
|
+
result = semver.valid(semver.coerce(`${ver}`));
|
|
348
|
+
}
|
|
349
|
+
if (strict && !result) {
|
|
350
|
+
throw new Error(`'${ver}' cannot be coerced to a valid version number`);
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const SUPPORTED_OPERATORS = ['==', '!=', '>', '<', '>=', '<=', '='];
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Compares two version strings using the given operator.
|
|
359
|
+
*
|
|
360
|
+
* @param ver1 - First version string
|
|
361
|
+
* @param operator - One of: ==, !=, >, <, >=, <=, =
|
|
362
|
+
* @param ver2 - Second version string
|
|
363
|
+
* @returns `true` if ver1 operator ver2 holds (e.g. "2.0.0" >= "1.0.0")
|
|
364
|
+
* @throws {Error} If operator is unsupported or either version cannot be coerced
|
|
365
|
+
*/
|
|
366
|
+
export function compareVersions(
|
|
367
|
+
ver1: string,
|
|
368
|
+
operator: string,
|
|
369
|
+
ver2: string
|
|
370
|
+
): boolean {
|
|
371
|
+
if (!SUPPORTED_OPERATORS.includes(operator)) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`The '${operator}' comparison operator is not supported. ` +
|
|
374
|
+
`Only '${JSON.stringify(SUPPORTED_OPERATORS)}' operators are supported`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const semverOperator = ['==', '!='].includes(operator) ? '=' : operator;
|
|
379
|
+
const v1 = coerceVersion(ver1, true);
|
|
380
|
+
const v2 = coerceVersion(ver2, true);
|
|
381
|
+
const result = semver.satisfies(v1, `${semverOperator}${v2}`);
|
|
382
|
+
return operator === '!=' ? !result : result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Quotes and escapes command-line arguments so they can be safely passed to a shell.
|
|
387
|
+
*
|
|
388
|
+
* @param args - Single argument or array of arguments to quote
|
|
389
|
+
* @returns Quoted string suitable for shell parsing
|
|
390
|
+
*/
|
|
391
|
+
export function quote(args: string | string[]): string {
|
|
392
|
+
return shellQuote(_.castArray(args));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Options for pluralize(). */
|
|
396
|
+
export interface PluralizeOptions {
|
|
397
|
+
/** If true, prefix the result with the count (e.g. "3 ducks"). */
|
|
398
|
+
inclusive?: boolean;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Returns the plural or singular form of a word appropriate to the count (e.g. "duck" + 1 → "duck", + 2 → "ducks").
|
|
403
|
+
*
|
|
404
|
+
* @param word - The word to pluralize (or singularize when count is 1)
|
|
405
|
+
* @param count - The count used to choose singular vs plural
|
|
406
|
+
* @param options - Options object or boolean: use `inclusive: true` (or `true`) to prefix with the number (e.g. "3 ducks")
|
|
407
|
+
* @returns The correctly inflected word, optionally prefixed with the count
|
|
408
|
+
*/
|
|
409
|
+
export function pluralize(
|
|
410
|
+
word: string,
|
|
411
|
+
count: number,
|
|
412
|
+
options: PluralizeOptions | boolean = {}
|
|
413
|
+
): string {
|
|
414
|
+
let inclusive = false;
|
|
415
|
+
if (_.isBoolean(options)) {
|
|
416
|
+
inclusive = options;
|
|
417
|
+
} else if (_.isBoolean(options?.inclusive)) {
|
|
418
|
+
inclusive = options.inclusive;
|
|
419
|
+
}
|
|
420
|
+
return pluralizeLib(word, count, inclusive);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Options for toInMemoryBase64(). */
|
|
424
|
+
export interface EncodingOptions {
|
|
425
|
+
/** Maximum size of the resulting buffer in bytes. Default 1GB. */
|
|
426
|
+
maxSize?: number;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Reads a file and returns its contents as a base64-encoded buffer.
|
|
431
|
+
*
|
|
432
|
+
* @param srcPath - Full path to the file to encode
|
|
433
|
+
* @param opts - Encoding options (e.g. maxSize to cap buffer size)
|
|
434
|
+
* @returns Buffer containing the base64-encoded file content
|
|
435
|
+
* @throws {Error} If the file does not exist, is a directory, cannot be read, or exceeds maxSize
|
|
436
|
+
*/
|
|
437
|
+
export async function toInMemoryBase64(
|
|
438
|
+
srcPath: string,
|
|
439
|
+
opts: EncodingOptions = {}
|
|
440
|
+
): Promise<Buffer> {
|
|
441
|
+
if (!(await fs.exists(srcPath)) || (await fs.stat(srcPath)).isDirectory()) {
|
|
442
|
+
throw new Error(`No such file: ${srcPath}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const {maxSize = 1 * GiB} = opts;
|
|
446
|
+
const resultBuffers: Buffer[] = [];
|
|
447
|
+
let resultBuffersSize = 0;
|
|
448
|
+
const resultWriteStream = new stream.Writable({
|
|
449
|
+
write(buffer: Buffer, _encoding: string, next: (err?: Error) => void) {
|
|
450
|
+
resultBuffers.push(buffer);
|
|
451
|
+
resultBuffersSize += buffer.length;
|
|
452
|
+
if (maxSize > 0 && resultBuffersSize > maxSize) {
|
|
453
|
+
resultWriteStream.emit(
|
|
454
|
+
'error',
|
|
455
|
+
new Error(
|
|
456
|
+
`The size of the resulting buffer must not be greater than ${toReadableSizeString(maxSize)}`
|
|
457
|
+
)
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
next();
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const readerStream = fs.createReadStream(srcPath);
|
|
465
|
+
const base64EncoderStream = new Base64Encode();
|
|
466
|
+
const encoderWritable = base64EncoderStream as NodeJS.WritableStream;
|
|
467
|
+
const encoderReadable = base64EncoderStream as NodeJS.ReadableStream;
|
|
468
|
+
const resultWriteStreamPromise = new Promise<void>((resolve, reject) => {
|
|
469
|
+
resultWriteStream.once('error', (e: Error) => {
|
|
470
|
+
readerStream.unpipe(encoderWritable);
|
|
471
|
+
encoderReadable.unpipe(resultWriteStream);
|
|
472
|
+
readerStream.destroy();
|
|
473
|
+
reject(e);
|
|
474
|
+
});
|
|
475
|
+
resultWriteStream.once('finish', () => resolve());
|
|
476
|
+
});
|
|
477
|
+
const readStreamPromise = new Promise<void>((resolve, reject) => {
|
|
478
|
+
readerStream.once('close', () => resolve());
|
|
479
|
+
readerStream.once('error', (e: Error) =>
|
|
480
|
+
reject(new Error(`Failed to read '${srcPath}': ${e.message}`))
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
readerStream.pipe(encoderWritable);
|
|
484
|
+
encoderReadable.pipe(resultWriteStream);
|
|
485
|
+
|
|
486
|
+
await Promise.all([readStreamPromise, resultWriteStreamPromise]);
|
|
487
|
+
return Buffer.concat(resultBuffers);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Options for getLockFileGuard(). */
|
|
491
|
+
export interface LockFileOptions {
|
|
492
|
+
/** Max time in seconds to wait for the lock. Default 120. */
|
|
493
|
+
timeout?: number;
|
|
494
|
+
/** If true, attempt to unlock and retry once if the first acquisition times out (e.g. stale lock). */
|
|
495
|
+
tryRecovery?: boolean;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Guard function that runs the given behavior under the lock. */
|
|
499
|
+
type LockFileGuardFn<T> = (behavior: () => Promise<T> | T) => Promise<T>;
|
|
500
|
+
|
|
501
|
+
/** Return type of getLockFileGuard: guard function with a .check() method. */
|
|
502
|
+
type LockFileGuard<T> = LockFileGuardFn<T> & {check: () => Promise<boolean>};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Creates a guard that serializes access to a critical section using a lock file.
|
|
506
|
+
* The returned function acquires the lock, runs the given behavior, then releases the lock.
|
|
507
|
+
* Also exposes `.check()` to test whether the lock is currently held.
|
|
508
|
+
*
|
|
509
|
+
* @param lockFile - Full path to the lock file
|
|
510
|
+
* @param opts - Options (see {@link LockFileOptions})
|
|
511
|
+
* @returns Async function that accepts a callback to run under the lock, plus a `.check()` method
|
|
512
|
+
*/
|
|
513
|
+
export function getLockFileGuard<T>(
|
|
514
|
+
lockFile: string,
|
|
515
|
+
opts: LockFileOptions = {}
|
|
516
|
+
): LockFileGuard<T> {
|
|
517
|
+
const {timeout = 120, tryRecovery = false} = opts;
|
|
518
|
+
|
|
519
|
+
const lock = promisify(_lockfile.lock) as (
|
|
520
|
+
lockfile: string,
|
|
521
|
+
opts: {wait: number}
|
|
522
|
+
) => Promise<void>;
|
|
523
|
+
const checkLock = promisify(_lockfile.check) as (lockfile: string) => Promise<boolean>;
|
|
524
|
+
const unlock = promisify(_lockfile.unlock) as (lockfile: string) => Promise<void>;
|
|
525
|
+
|
|
526
|
+
const guard: LockFileGuard<T> = Object.assign(
|
|
527
|
+
async (behavior: () => Promise<T> | T): Promise<T> => {
|
|
528
|
+
let triedRecovery = false;
|
|
529
|
+
let acquired = false;
|
|
530
|
+
while (!acquired) {
|
|
531
|
+
try {
|
|
532
|
+
if (_lockfile.checkSync(lockFile)) {
|
|
533
|
+
await lock(lockFile, {wait: timeout * 1000});
|
|
534
|
+
} else {
|
|
535
|
+
_lockfile.lockSync(lockFile);
|
|
536
|
+
}
|
|
537
|
+
acquired = true;
|
|
538
|
+
} catch (e) {
|
|
539
|
+
const err = e as Error;
|
|
540
|
+
if (_.includes(err.message, 'EEXIST') && tryRecovery && !triedRecovery) {
|
|
541
|
+
_lockfile.unlockSync(lockFile);
|
|
542
|
+
triedRecovery = true;
|
|
543
|
+
} else {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Could not acquire lock on '${lockFile}' after ${timeout}s. ` +
|
|
546
|
+
`Original error: ${err.message}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
return await behavior();
|
|
553
|
+
} finally {
|
|
554
|
+
await unlock(lockFile);
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
{check: () => checkLock(lockFile)}
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
return guard;
|
|
561
|
+
}
|