@appium/support 7.0.5 → 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.
Files changed (114) hide show
  1. package/LICENSE +201 -0
  2. package/build/lib/console.d.ts +42 -88
  3. package/build/lib/console.d.ts.map +1 -1
  4. package/build/lib/console.js +20 -80
  5. package/build/lib/console.js.map +1 -1
  6. package/build/lib/doctor.d.ts +6 -18
  7. package/build/lib/doctor.d.ts.map +1 -1
  8. package/build/lib/doctor.js +0 -15
  9. package/build/lib/doctor.js.map +1 -1
  10. package/build/lib/env.d.ts +14 -20
  11. package/build/lib/env.d.ts.map +1 -1
  12. package/build/lib/env.js +13 -50
  13. package/build/lib/env.js.map +1 -1
  14. package/build/lib/fs.d.ts +109 -148
  15. package/build/lib/fs.d.ts.map +1 -1
  16. package/build/lib/fs.js +88 -188
  17. package/build/lib/fs.js.map +1 -1
  18. package/build/lib/image-util.d.ts +7 -6
  19. package/build/lib/image-util.d.ts.map +1 -1
  20. package/build/lib/image-util.js +9 -6
  21. package/build/lib/image-util.js.map +1 -1
  22. package/build/lib/index.d.ts +19 -17
  23. package/build/lib/index.d.ts.map +1 -1
  24. package/build/lib/logger.d.ts +1 -1
  25. package/build/lib/logger.d.ts.map +1 -1
  26. package/build/lib/logger.js +1 -1
  27. package/build/lib/logger.js.map +1 -1
  28. package/build/lib/logging.d.ts +7 -15
  29. package/build/lib/logging.d.ts.map +1 -1
  30. package/build/lib/logging.js +36 -62
  31. package/build/lib/logging.js.map +1 -1
  32. package/build/lib/mjpeg.d.ts +19 -56
  33. package/build/lib/mjpeg.d.ts.map +1 -1
  34. package/build/lib/mjpeg.js +53 -76
  35. package/build/lib/mjpeg.js.map +1 -1
  36. package/build/lib/mkdirp.d.ts +4 -1
  37. package/build/lib/mkdirp.d.ts.map +1 -1
  38. package/build/lib/mkdirp.js +1 -2
  39. package/build/lib/mkdirp.js.map +1 -1
  40. package/build/lib/net.d.ts +52 -90
  41. package/build/lib/net.d.ts.map +1 -1
  42. package/build/lib/net.js +104 -193
  43. package/build/lib/net.js.map +1 -1
  44. package/build/lib/node.d.ts +16 -17
  45. package/build/lib/node.d.ts.map +1 -1
  46. package/build/lib/node.js +106 -111
  47. package/build/lib/node.js.map +1 -1
  48. package/build/lib/npm.d.ts +65 -86
  49. package/build/lib/npm.d.ts.map +1 -1
  50. package/build/lib/npm.js +59 -117
  51. package/build/lib/npm.js.map +1 -1
  52. package/build/lib/plist.d.ts +36 -29
  53. package/build/lib/plist.d.ts.map +1 -1
  54. package/build/lib/plist.js +62 -59
  55. package/build/lib/plist.js.map +1 -1
  56. package/build/lib/process.d.ts +19 -2
  57. package/build/lib/process.d.ts.map +1 -1
  58. package/build/lib/process.js +24 -7
  59. package/build/lib/process.js.map +1 -1
  60. package/build/lib/system.d.ts +41 -6
  61. package/build/lib/system.d.ts.map +1 -1
  62. package/build/lib/system.js +46 -11
  63. package/build/lib/system.js.map +1 -1
  64. package/build/lib/tempdir.d.ts +26 -49
  65. package/build/lib/tempdir.d.ts.map +1 -1
  66. package/build/lib/tempdir.js +41 -73
  67. package/build/lib/tempdir.js.map +1 -1
  68. package/build/lib/timing.d.ts +28 -22
  69. package/build/lib/timing.d.ts.map +1 -1
  70. package/build/lib/timing.js +16 -17
  71. package/build/lib/timing.js.map +1 -1
  72. package/build/lib/util.d.ts +164 -181
  73. package/build/lib/util.d.ts.map +1 -1
  74. package/build/lib/util.js +193 -247
  75. package/build/lib/util.js.map +1 -1
  76. package/build/lib/zip.d.ts +81 -139
  77. package/build/lib/zip.d.ts.map +1 -1
  78. package/build/lib/zip.js +210 -258
  79. package/build/lib/zip.js.map +1 -1
  80. package/lib/console.ts +139 -0
  81. package/lib/{doctor.js → doctor.ts} +6 -20
  82. package/lib/{env.js → env.ts} +31 -59
  83. package/lib/fs.ts +453 -0
  84. package/lib/image-util.ts +40 -0
  85. package/lib/index.ts +1 -0
  86. package/lib/{logger.js → logger.ts} +1 -1
  87. package/lib/logging.ts +157 -0
  88. package/lib/mjpeg.ts +186 -0
  89. package/lib/{mkdirp.js → mkdirp.ts} +2 -2
  90. package/lib/net.ts +305 -0
  91. package/lib/{node.js → node.ts} +134 -133
  92. package/lib/npm.ts +291 -0
  93. package/lib/plist.ts +187 -0
  94. package/lib/process.ts +62 -0
  95. package/lib/system.ts +95 -0
  96. package/lib/tempdir.ts +115 -0
  97. package/lib/{timing.js → timing.ts} +28 -33
  98. package/lib/util.ts +561 -0
  99. package/lib/{zip.js → zip.ts} +341 -296
  100. package/package.json +20 -22
  101. package/tsconfig.json +3 -5
  102. package/index.js +0 -1
  103. package/lib/console.js +0 -173
  104. package/lib/fs.js +0 -496
  105. package/lib/image-util.js +0 -32
  106. package/lib/logging.js +0 -145
  107. package/lib/mjpeg.js +0 -207
  108. package/lib/net.js +0 -336
  109. package/lib/npm.js +0 -310
  110. package/lib/plist.js +0 -182
  111. package/lib/process.js +0 -46
  112. package/lib/system.js +0 -48
  113. package/lib/tempdir.js +0 -131
  114. package/lib/util.js +0 -584
@@ -6,54 +6,25 @@ import path from 'node:path';
6
6
  import _fs from 'node:fs';
7
7
  import {v4 as uuidV4} from 'uuid';
8
8
 
9
- const ECMA_SIZES = Object.freeze({
10
- STRING: 2,
11
- BOOLEAN: 4,
12
- NUMBER: 8,
13
- });
14
-
15
- /**
16
- * Internal utility to link global package to local context
17
- *
18
- * @param {string} packageName - name of the package to link
19
- * @throws {Error} If the command fails
20
- */
21
- async function linkGlobalPackage(packageName) {
22
- try {
23
- log.debug(`Linking package '${packageName}'`);
24
- const cmd = isWindows() ? 'npm.cmd' : 'npm';
25
- await exec(cmd, ['link', packageName], {timeout: 20000});
26
- } catch (err) {
27
- const msg = `Unable to load package '${packageName}', linking failed: ${err.message}`;
28
- log.debug(msg);
29
- if (err.stderr) {
30
- // log the stderr if there, but do not add to thrown error as it is
31
- // _very_ verbose
32
- log.debug(err.stderr);
33
- }
34
- throw new Error(msg);
35
- }
36
- }
9
+ const OBJECTS_MAPPING = new WeakMap<object, string>();
37
10
 
38
11
  /**
39
12
  * Utility function to extend node functionality, allowing us to require
40
13
  * modules that are installed globally. If the package cannot be required,
41
14
  * this will attempt to link the package and then re-require it
42
15
  *
43
- * @param {string} packageName - the name of the package to be required
44
- * @returns {Promise<unknown>} - the package object
16
+ * @param packageName - the name of the package to be required
17
+ * @returns The package object
45
18
  * @throws {Error} If the package is not found locally or globally
46
19
  */
47
- async function requirePackage(packageName) {
48
- // first, get it in the normal way (see https://nodejs.org/api/modules.html#modules_all_together)
20
+ export async function requirePackage(packageName: string): Promise<unknown> {
49
21
  try {
50
22
  log.debug(`Loading local package '${packageName}'`);
51
23
  return require(packageName);
52
24
  } catch (err) {
53
- log.debug(`Failed to load local package '${packageName}': ${err.message}`);
25
+ log.debug(`Failed to load local package '${packageName}': ${(err as Error).message}`);
54
26
  }
55
27
 
56
- // second, get it from where it ought to be in the global node_modules
57
28
  try {
58
29
  const globalPackageName = path.resolve(
59
30
  process.env.npm_config_prefix ?? '',
@@ -64,112 +35,45 @@ async function requirePackage(packageName) {
64
35
  log.debug(`Loading global package '${globalPackageName}'`);
65
36
  return require(globalPackageName);
66
37
  } catch (err) {
67
- log.debug(`Failed to load global package '${packageName}': ${err.message}`);
38
+ log.debug(`Failed to load global package '${packageName}': ${(err as Error).message}`);
68
39
  }
69
40
 
70
- // third, link the file and get locally
71
41
  try {
72
42
  await linkGlobalPackage(packageName);
73
43
  log.debug(`Retrying load of linked package '${packageName}'`);
74
44
  return require(packageName);
75
45
  } catch (err) {
76
- throw log.errorWithException(`Unable to load package '${packageName}': ${err.message}`);
77
- }
78
- }
79
-
80
- function extractAllProperties(obj) {
81
- const stringProperties = [];
82
- for (const prop in obj) {
83
- stringProperties.push(prop);
84
- }
85
- if (_.isFunction(Object.getOwnPropertySymbols)) {
86
- stringProperties.push(...Object.getOwnPropertySymbols(obj));
87
- }
88
- return stringProperties;
89
- }
90
-
91
- function _getSizeOfObject(seen, object) {
92
- if (_.isNil(object)) {
93
- return 0;
94
- }
95
-
96
- let bytes = 0;
97
- const properties = extractAllProperties(object);
98
- for (const key of properties) {
99
- // Do not recalculate circular references
100
- if (typeof object[key] === 'object' && !_.isNil(object[key])) {
101
- if (seen.has(object[key])) {
102
- continue;
103
- }
104
- seen.add(object[key]);
105
- }
106
-
107
- bytes += getCalculator(seen)(key);
108
- try {
109
- bytes += getCalculator(seen)(object[key]);
110
- } catch (ex) {
111
- if (ex instanceof RangeError) {
112
- // circular reference detected, final result might be incorrect
113
- // let's be nice and not throw an exception
114
- bytes = 0;
115
- }
116
- }
46
+ throw log.errorWithException(
47
+ `Unable to load package '${packageName}': ${(err as Error).message}`
48
+ );
117
49
  }
118
-
119
- return bytes;
120
- }
121
-
122
- function getCalculator(seen) {
123
- return function calculator(obj) {
124
- if (_.isBuffer(obj)) {
125
- return obj.length;
126
- }
127
-
128
- switch (typeof obj) {
129
- case 'string':
130
- return obj.length * ECMA_SIZES.STRING;
131
- case 'boolean':
132
- return ECMA_SIZES.BOOLEAN;
133
- case 'number':
134
- return ECMA_SIZES.NUMBER;
135
- case 'symbol':
136
- return _.isFunction(Symbol.keyFor) && Symbol.keyFor(obj)
137
- ? /** @type {string} */ (Symbol.keyFor(obj)).length * ECMA_SIZES.STRING
138
- : (obj.toString().length - 8) * ECMA_SIZES.STRING;
139
- case 'object':
140
- return _.isArray(obj)
141
- ? obj.map(getCalculator(seen)).reduce((acc, curr) => acc + curr, 0)
142
- : _getSizeOfObject(seen, obj);
143
- default:
144
- return 0;
145
- }
146
- };
147
50
  }
148
51
 
149
52
  /**
150
53
  * Calculate the in-depth size in memory of the provided object.
151
54
  * The original implementation is borrowed from https://github.com/miktam/sizeof.
152
55
  *
153
- * @param {*} obj An object whose size should be calculated
154
- * @returns {number} Object size in bytes.
56
+ * @param obj - An object whose size should be calculated
57
+ * @returns Object size in bytes.
155
58
  */
156
- function getObjectSize(obj) {
59
+ export function getObjectSize(obj: unknown): number {
157
60
  return getCalculator(new WeakSet())(obj);
158
61
  }
159
62
 
160
- const OBJECTS_MAPPING = new WeakMap();
161
-
162
63
  /**
163
64
  * Calculates a unique object identifier
164
65
  *
165
- * @param {object} object Any valid ECMA object
166
- * @returns {string} A uuidV4 string that uniquely identifies given object
66
+ * @param object - Any valid ECMA object
67
+ * @returns A uuidV4 string that uniquely identifies given object
167
68
  */
168
- function getObjectId(object) {
169
- if (!OBJECTS_MAPPING.has(object)) {
170
- OBJECTS_MAPPING.set(object, uuidV4());
69
+ export function getObjectId(object: object): string {
70
+ const existing = OBJECTS_MAPPING.get(object);
71
+ if (existing !== undefined) {
72
+ return existing;
171
73
  }
172
- return OBJECTS_MAPPING.get(object);
74
+ const id = uuidV4();
75
+ OBJECTS_MAPPING.set(object, id);
76
+ return id;
173
77
  }
174
78
 
175
79
  /**
@@ -181,50 +85,147 @@ function getObjectId(object) {
181
85
  * ! This function changes the given object,
182
86
  * so it becomes immutable.
183
87
  *
184
- * @param {*} object Any valid ECMA object
185
- * @returns {*} The same object that was passed to the
186
- * function after it was made immutable.
88
+ * @param object - Any valid ECMA object
89
+ * @returns The same object that was passed after it was made immutable.
187
90
  */
188
- function deepFreeze(object) {
189
- let propNames;
91
+ export function deepFreeze<T>(object: T): T {
92
+ let propNames: string[];
190
93
  try {
191
- propNames = Object.getOwnPropertyNames(object);
94
+ propNames = Object.getOwnPropertyNames(object as object);
192
95
  } catch {
193
96
  return object;
194
97
  }
195
98
  for (const name of propNames) {
196
- const value = object[name];
99
+ const value = (object as Record<string, unknown>)[name];
197
100
  if (value && typeof value === 'object') {
198
101
  deepFreeze(value);
199
102
  }
200
103
  }
201
- return Object.freeze(object);
104
+ return Object.freeze(object) as T;
202
105
  }
203
106
 
204
107
  /**
205
108
  * Tries to synchronously detect the absolute path to the folder
206
109
  * where the given `moduleName` is located.
207
110
  *
208
- * @param {string} moduleName The name of the module as it is written in package.json
209
- * @param {string} filePath Full path to any of files that `moduleName` contains. Use
111
+ * @param moduleName - The name of the module as it is written in package.json
112
+ * @param filePath - Full path to any of files that `moduleName` contains. Use
210
113
  * `__filename` to find the root of the module where this helper is called.
211
- * @returns {string?} Full path to the module root
114
+ * @returns Full path to the module root, or null if not found
212
115
  */
213
- function getModuleRootSync (moduleName, filePath) {
116
+ export function getModuleRootSync(moduleName: string, filePath: string): string | null {
214
117
  let currentDir = path.dirname(path.resolve(filePath));
215
118
  let isAtFsRoot = false;
216
119
  while (!isAtFsRoot) {
217
120
  const manifestPath = path.join(currentDir, 'package.json');
218
121
  try {
219
- if (_fs.existsSync(manifestPath) &&
220
- JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === moduleName) {
122
+ if (
123
+ _fs.existsSync(manifestPath) &&
124
+ (JSON.parse(_fs.readFileSync(manifestPath, 'utf8')) as {name?: string}).name === moduleName
125
+ ) {
221
126
  return currentDir;
222
127
  }
223
- } catch {}
128
+ } catch {
129
+ // ignore
130
+ }
224
131
  currentDir = path.dirname(currentDir);
225
132
  isAtFsRoot = currentDir.length <= path.dirname(currentDir).length;
226
133
  }
227
134
  return null;
228
135
  }
229
136
 
230
- export {requirePackage, getObjectSize, getObjectId, deepFreeze, getModuleRootSync};
137
+ // #region Private
138
+
139
+ const ECMA_SIZES = Object.freeze({
140
+ STRING: 2,
141
+ BOOLEAN: 4,
142
+ NUMBER: 8,
143
+ });
144
+
145
+ type SizeCalculator = (obj: unknown) => number;
146
+
147
+ async function linkGlobalPackage(packageName: string): Promise<void> {
148
+ try {
149
+ log.debug(`Linking package '${packageName}'`);
150
+ const cmd = isWindows() ? 'npm.cmd' : 'npm';
151
+ await exec(cmd, ['link', packageName], {timeout: 20000});
152
+ } catch (err) {
153
+ const e = err as Error & {stderr?: string};
154
+ const msg = `Unable to load package '${packageName}', linking failed: ${e.message}`;
155
+ log.debug(msg);
156
+ if (e.stderr) {
157
+ log.debug(e.stderr);
158
+ }
159
+ throw new Error(msg);
160
+ }
161
+ }
162
+
163
+ function extractAllProperties(obj: object): (string | symbol)[] {
164
+ const stringProperties: (string | symbol)[] = [];
165
+ for (const prop in obj) {
166
+ stringProperties.push(prop);
167
+ }
168
+ if (_.isFunction(Object.getOwnPropertySymbols)) {
169
+ stringProperties.push(...Object.getOwnPropertySymbols(obj));
170
+ }
171
+ return stringProperties;
172
+ }
173
+
174
+ function _getSizeOfObject(seen: WeakSet<object>, object: object): number {
175
+ if (_.isNil(object)) {
176
+ return 0;
177
+ }
178
+
179
+ let bytes = 0;
180
+ const properties = extractAllProperties(object);
181
+ const calc = getCalculator(seen);
182
+ for (const key of properties) {
183
+ const val = (object as Record<string | symbol, unknown>)[key];
184
+ if (typeof val === 'object' && !_.isNil(val)) {
185
+ if (seen.has(val as object)) {
186
+ continue;
187
+ }
188
+ seen.add(val as object);
189
+ }
190
+
191
+ bytes += calc(key);
192
+ try {
193
+ bytes += calc(val);
194
+ } catch (ex) {
195
+ if (ex instanceof RangeError) {
196
+ bytes = 0;
197
+ }
198
+ }
199
+ }
200
+
201
+ return bytes;
202
+ }
203
+
204
+ function getCalculator(seen: WeakSet<object>): SizeCalculator {
205
+ return function calculator(obj: unknown): number {
206
+ if (_.isBuffer(obj)) {
207
+ return (obj as Buffer).length;
208
+ }
209
+
210
+ switch (typeof obj) {
211
+ case 'string':
212
+ return obj.length * ECMA_SIZES.STRING;
213
+ case 'boolean':
214
+ return ECMA_SIZES.BOOLEAN;
215
+ case 'number':
216
+ return ECMA_SIZES.NUMBER;
217
+ case 'symbol':
218
+ return _.isFunction(Symbol.keyFor) && Symbol.keyFor(obj)
219
+ ? (Symbol.keyFor(obj) as string).length * ECMA_SIZES.STRING
220
+ : (obj.toString().length - 8) * ECMA_SIZES.STRING;
221
+ case 'object':
222
+ return _.isArray(obj)
223
+ ? obj.map(getCalculator(seen)).reduce((acc, curr) => acc + curr, 0)
224
+ : _getSizeOfObject(seen, obj as object);
225
+ default:
226
+ return 0;
227
+ }
228
+ };
229
+ }
230
+
231
+ // #endregion
package/lib/npm.ts ADDED
@@ -0,0 +1,291 @@
1
+ import path from 'node:path';
2
+ import * as semver from 'semver';
3
+ import type {PackageJson} from 'type-fest';
4
+ import type {StringRecord} from '@appium/types';
5
+ import {hasAppiumDependency} from './env';
6
+ import {exec} from 'teen_process';
7
+ import type {ExecError, TeenProcessExecOptions} from 'teen_process';
8
+ import {fs} from './fs';
9
+ import * as util from './util';
10
+ import * as system from './system';
11
+ import resolveFrom from 'resolve-from';
12
+
13
+ /**
14
+ * Relative path to directory containing any Appium internal files
15
+ * XXX: this is duplicated in `appium/lib/constants.js`.
16
+ */
17
+ export const CACHE_DIR_RELATIVE_PATH = path.join('node_modules', '.cache', 'appium');
18
+
19
+ /**
20
+ * Relative path to lockfile used when installing an extension via `appium`
21
+ */
22
+ export const INSTALL_LOCKFILE_RELATIVE_PATH = path.join(CACHE_DIR_RELATIVE_PATH, '.install.lock');
23
+
24
+ /** Options for {@link NPM.exec} */
25
+ export interface ExecOpts {
26
+ /** Current working directory */
27
+ cwd: string;
28
+ /** If true, supply `--json` flag to npm and resolve w/ parsed JSON */
29
+ json?: boolean;
30
+ /** Path to lockfile to use */
31
+ lockFile?: string;
32
+ }
33
+
34
+ /** Options for {@link NPM.installPackage} */
35
+ export interface InstallPackageOpts {
36
+ /** Name of the package to install */
37
+ pkgName: string;
38
+ /** Whether to install from a local path or from npm */
39
+ installType?: 'local' | string;
40
+ }
41
+
42
+ /** Result of {@link NPM.installPackage} */
43
+ export interface NpmInstallReceipt {
44
+ /** Path to installed package */
45
+ installPath: string;
46
+ /** Package data */
47
+ pkg: PackageJson;
48
+ }
49
+
50
+ export interface NpmExecResult {
51
+ stdout: string;
52
+ stderr: string;
53
+ code: number | null;
54
+ json?: unknown;
55
+ }
56
+
57
+ /**
58
+ * XXX: This should probably be a singleton, but it isn't. Maybe this module should just export functions?
59
+ */
60
+ export class NPM {
61
+ /**
62
+ * Execute `npm` with given args.
63
+ *
64
+ * If the process exits with a nonzero code, the contents of `STDOUT` and `STDERR` will be in the
65
+ * `message` of any rejected error.
66
+ */
67
+ async exec(
68
+ cmd: string,
69
+ args: string[],
70
+ opts: ExecOpts,
71
+ execOpts: Omit<TeenProcessExecOptions, 'cwd'> = {}
72
+ ): Promise<NpmExecResult> {
73
+ const {cwd, json, lockFile} = opts;
74
+
75
+ const teenProcessExecOpts: TeenProcessExecOptions = {
76
+ ...execOpts,
77
+ shell: system.isWindows() || execOpts.shell,
78
+ cwd,
79
+ };
80
+
81
+ const argsCopy = [cmd, ...args];
82
+ if (json) {
83
+ argsCopy.push('--json');
84
+ }
85
+ const npmCmd = system.isWindows() ? 'npm.cmd' : 'npm';
86
+ type ExecRunnerResult = {stdout: string; stderr: string; code: number | null};
87
+ let runner = async (): Promise<ExecRunnerResult> =>
88
+ await exec(npmCmd, argsCopy, teenProcessExecOpts);
89
+ if (lockFile) {
90
+ const acquireLock = util.getLockFileGuard(lockFile);
91
+ const _runner = runner;
92
+ runner = async () => (await acquireLock(_runner)) as ExecRunnerResult;
93
+ }
94
+
95
+ let ret: NpmExecResult;
96
+ try {
97
+ const {stdout, stderr, code} = await runner();
98
+ ret = {stdout, stderr, code};
99
+ try {
100
+ ret.json = JSON.parse(stdout);
101
+ } catch {
102
+ // ignore
103
+ }
104
+ } catch (e) {
105
+ const {stdout = '', stderr = '', code = null} = e as ExecError;
106
+ throw new Error(
107
+ `npm command '${argsCopy.join(' ')}' failed with code ${code}.\n\nSTDOUT:\n${stdout.trim()}\n\nSTDERR:\n${stderr.trim()}`
108
+ );
109
+ }
110
+ return ret;
111
+ }
112
+
113
+ /**
114
+ * Gets the latest published version of a package from npm registry.
115
+ *
116
+ * @param cwd - Current working directory for npm command
117
+ * @param pkg - Package name to query
118
+ * @returns Latest version string, or `null` if package not found (e.g. 404)
119
+ */
120
+ async getLatestVersion(cwd: string, pkg: string): Promise<string | null> {
121
+ try {
122
+ const result = await this.exec('view', [pkg, 'dist-tags'], {json: true, cwd});
123
+ const json = result.json as {latest?: string} | undefined;
124
+ return json?.latest ?? null;
125
+ } catch (err) {
126
+ if (!(err instanceof Error) || !err.message.includes('E404')) {
127
+ throw err;
128
+ }
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Gets the latest version of a package that is a safe upgrade from the current version
135
+ * (same major, no prereleases). Fetches versions from npm and delegates to
136
+ * {@link NPM.getLatestSafeUpgradeFromVersions}.
137
+ *
138
+ * @param cwd - Current working directory for npm command
139
+ * @param pkg - Package name to query
140
+ * @param curVersion - Current installed version
141
+ * @returns Latest safe upgrade version string, or `null` if none or package not found
142
+ */
143
+ async getLatestSafeUpgradeVersion(
144
+ cwd: string,
145
+ pkg: string,
146
+ curVersion: string
147
+ ): Promise<string | null> {
148
+ try {
149
+ const result = await this.exec('view', [pkg, 'versions'], {json: true, cwd});
150
+ const allVersions = result.json;
151
+ if (!Array.isArray(allVersions)) {
152
+ return null;
153
+ }
154
+ return this.getLatestSafeUpgradeFromVersions(curVersion, allVersions as string[]);
155
+ } catch (err) {
156
+ if (!(err instanceof Error) || !err.message.includes('E404')) {
157
+ throw err;
158
+ }
159
+ return null;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Runs `npm ls`, optionally for a particular package.
165
+ */
166
+ async list(cwd: string, pkg?: string): Promise<unknown> {
167
+ return (await this.exec('list', pkg ? [pkg] : [], {cwd, json: true})).json;
168
+ }
169
+
170
+ /**
171
+ * Given a current version and a list of all versions for a package, return the version which is
172
+ * the highest safely-upgradable version (meaning not crossing any major revision boundaries, and
173
+ * not including any alpha/beta/rc versions)
174
+ */
175
+ getLatestSafeUpgradeFromVersions(
176
+ curVersion: string,
177
+ allVersions: string[]
178
+ ): string | null {
179
+ let safeUpgradeVer: semver.SemVer | null = null;
180
+ const curSemver = semver.parse(curVersion) ?? semver.parse(semver.coerce(curVersion));
181
+ if (curSemver === null) {
182
+ throw new Error(`Could not parse current version '${curVersion}'`);
183
+ }
184
+ for (const testVer of allVersions) {
185
+ const testSemver = semver.parse(testVer) ?? semver.parse(semver.coerce(testVer));
186
+ if (
187
+ testSemver === null ||
188
+ testSemver.prerelease.length > 0 ||
189
+ curSemver.compare(testSemver) === 1 ||
190
+ testSemver.major > curSemver.major
191
+ ) {
192
+ continue;
193
+ }
194
+ if (safeUpgradeVer === null || testSemver.compare(safeUpgradeVer) === 1) {
195
+ safeUpgradeVer = testSemver;
196
+ }
197
+ }
198
+ return safeUpgradeVer ? safeUpgradeVer.format() : null;
199
+ }
200
+
201
+ /**
202
+ * Installs a package w/ `npm`
203
+ */
204
+ async installPackage(
205
+ cwd: string,
206
+ installStr: string,
207
+ opts: InstallPackageOpts
208
+ ): Promise<NpmInstallReceipt> {
209
+ const {pkgName, installType} = opts;
210
+ let dummyPkgJson: Record<string, unknown>;
211
+ const dummyPkgPath = path.join(cwd, 'package.json');
212
+ try {
213
+ dummyPkgJson = JSON.parse(await fs.readFile(dummyPkgPath, 'utf8')) as Record<string, unknown>;
214
+ } catch (err) {
215
+ const nodeErr = err as NodeJS.ErrnoException;
216
+ if (nodeErr.code === 'ENOENT') {
217
+ dummyPkgJson = {};
218
+ await fs.writeFile(dummyPkgPath, JSON.stringify(dummyPkgJson, null, 2), 'utf8');
219
+ } else {
220
+ throw err;
221
+ }
222
+ }
223
+
224
+ const installOpts = ['--save-dev', '--no-progress', '--no-audit'];
225
+ if (!(await hasAppiumDependency(cwd))) {
226
+ if (process.env.APPIUM_OMIT_PEER_DEPS) {
227
+ installOpts.push('--omit=peer');
228
+ }
229
+ installOpts.push('--save-exact', '--global-style', '--no-package-lock');
230
+ }
231
+
232
+ const cmd = installType === 'local' ? 'link' : 'install';
233
+ const res = await this.exec(cmd, [...installOpts, installStr], {
234
+ cwd,
235
+ json: true,
236
+ lockFile: this._getInstallLockfilePath(cwd),
237
+ });
238
+
239
+ if (res.json && typeof res.json === 'object' && 'error' in res.json && res.json.error) {
240
+ throw new Error(String((res.json as {error: unknown}).error));
241
+ }
242
+
243
+ const pkgJsonPath = resolveFrom(cwd, `${pkgName}/package.json`);
244
+ try {
245
+ const pkgJson = await fs.readFile(pkgJsonPath, 'utf8');
246
+ const pkg = JSON.parse(pkgJson) as PackageJson;
247
+ return {installPath: path.dirname(pkgJsonPath), pkg};
248
+ } catch {
249
+ throw new Error(
250
+ 'The package was not downloaded correctly; its package.json ' +
251
+ 'did not exist or was unreadable. We looked for it at ' +
252
+ pkgJsonPath
253
+ );
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Uninstalls a package with `npm uninstall`.
259
+ *
260
+ * @param cwd - Current working directory (project root)
261
+ * @param pkg - Package name to uninstall
262
+ */
263
+ async uninstallPackage(cwd: string, pkg: string): Promise<void> {
264
+ await this.exec('uninstall', [pkg], {
265
+ cwd,
266
+ lockFile: this._getInstallLockfilePath(cwd),
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Fetches package metadata from the npm registry via `npm info`.
272
+ *
273
+ * @param pkg - Npm package spec to query
274
+ * @param entries - Field names to be included into the resulting output. By default all fields are included.
275
+ * @returns Package metadata as a record of string values
276
+ */
277
+ async getPackageInfo(pkg: string, entries: string[] = []): Promise<StringRecord> {
278
+ const result = await this.exec('info', [pkg, ...entries], {
279
+ cwd: process.cwd(),
280
+ json: true,
281
+ });
282
+ return (result.json ?? {}) as StringRecord;
283
+ }
284
+
285
+ /** Returns path to "install" lockfile */
286
+ private _getInstallLockfilePath(cwd: string): string {
287
+ return path.join(cwd, INSTALL_LOCKFILE_RELATIVE_PATH);
288
+ }
289
+ }
290
+
291
+ export const npm = new NPM();