@appium/base-driver 10.2.0 → 10.2.2

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 (138) hide show
  1. package/LICENSE +201 -0
  2. package/build/lib/basedriver/capabilities.js +7 -7
  3. package/build/lib/basedriver/capabilities.js.map +1 -1
  4. package/build/lib/basedriver/commands/event.d.ts +1 -1
  5. package/build/lib/basedriver/commands/event.d.ts.map +1 -1
  6. package/build/lib/basedriver/commands/execute.d.ts +1 -1
  7. package/build/lib/basedriver/commands/execute.d.ts.map +1 -1
  8. package/build/lib/basedriver/commands/find.d.ts +1 -1
  9. package/build/lib/basedriver/commands/find.d.ts.map +1 -1
  10. package/build/lib/basedriver/commands/mixin.d.ts +1 -1
  11. package/build/lib/basedriver/commands/mixin.d.ts.map +1 -1
  12. package/build/lib/basedriver/commands/timeout.d.ts +1 -1
  13. package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
  14. package/build/lib/basedriver/device-settings.d.ts +14 -23
  15. package/build/lib/basedriver/device-settings.d.ts.map +1 -1
  16. package/build/lib/basedriver/device-settings.js +11 -26
  17. package/build/lib/basedriver/device-settings.js.map +1 -1
  18. package/build/lib/basedriver/helpers.d.ts +36 -57
  19. package/build/lib/basedriver/helpers.d.ts.map +1 -1
  20. package/build/lib/basedriver/helpers.js +148 -239
  21. package/build/lib/basedriver/helpers.js.map +1 -1
  22. package/build/lib/basedriver/logger.d.ts +1 -2
  23. package/build/lib/basedriver/logger.d.ts.map +1 -1
  24. package/build/lib/basedriver/logger.js +2 -2
  25. package/build/lib/basedriver/logger.js.map +1 -1
  26. package/build/lib/basedriver/validation.d.ts.map +1 -1
  27. package/build/lib/basedriver/validation.js +3 -3
  28. package/build/lib/basedriver/validation.js.map +1 -1
  29. package/build/lib/constants.d.ts +1 -1
  30. package/build/lib/constants.d.ts.map +1 -1
  31. package/build/lib/express/crash.d.ts +8 -2
  32. package/build/lib/express/crash.d.ts.map +1 -1
  33. package/build/lib/express/crash.js +6 -0
  34. package/build/lib/express/crash.js.map +1 -1
  35. package/build/lib/express/express-logging.d.ts +12 -2
  36. package/build/lib/express/express-logging.d.ts.map +1 -1
  37. package/build/lib/express/express-logging.js +34 -26
  38. package/build/lib/express/express-logging.js.map +1 -1
  39. package/build/lib/express/idempotency.d.ts +4 -10
  40. package/build/lib/express/idempotency.d.ts.map +1 -1
  41. package/build/lib/express/idempotency.js +69 -73
  42. package/build/lib/express/idempotency.js.map +1 -1
  43. package/build/lib/express/logger.d.ts +1 -2
  44. package/build/lib/express/logger.d.ts.map +1 -1
  45. package/build/lib/express/logger.js +2 -2
  46. package/build/lib/express/logger.js.map +1 -1
  47. package/build/lib/express/middleware.d.ts +37 -41
  48. package/build/lib/express/middleware.d.ts.map +1 -1
  49. package/build/lib/express/middleware.js +48 -60
  50. package/build/lib/express/middleware.js.map +1 -1
  51. package/build/lib/express/server.d.ts +57 -101
  52. package/build/lib/express/server.d.ts.map +1 -1
  53. package/build/lib/express/server.js +51 -128
  54. package/build/lib/express/server.js.map +1 -1
  55. package/build/lib/express/static.d.ts +10 -5
  56. package/build/lib/express/static.d.ts.map +1 -1
  57. package/build/lib/express/static.js +32 -42
  58. package/build/lib/express/static.js.map +1 -1
  59. package/build/lib/express/websocket.d.ts +22 -6
  60. package/build/lib/express/websocket.d.ts.map +1 -1
  61. package/build/lib/express/websocket.js +10 -15
  62. package/build/lib/express/websocket.js.map +1 -1
  63. package/build/lib/helpers/capabilities.d.ts +4 -16
  64. package/build/lib/helpers/capabilities.d.ts.map +1 -1
  65. package/build/lib/helpers/capabilities.js +36 -48
  66. package/build/lib/helpers/capabilities.js.map +1 -1
  67. package/build/lib/jsonwp-proxy/protocol-converter.d.ts +42 -78
  68. package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
  69. package/build/lib/jsonwp-proxy/protocol-converter.js +87 -139
  70. package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
  71. package/build/lib/jsonwp-proxy/proxy.d.ts +1 -1
  72. package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
  73. package/build/lib/jsonwp-proxy/proxy.js +2 -2
  74. package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
  75. package/build/lib/jsonwp-status/status.d.ts +113 -158
  76. package/build/lib/jsonwp-status/status.d.ts.map +1 -1
  77. package/build/lib/jsonwp-status/status.js +10 -14
  78. package/build/lib/jsonwp-status/status.js.map +1 -1
  79. package/build/lib/protocol/bidi-commands.d.ts +31 -36
  80. package/build/lib/protocol/bidi-commands.d.ts.map +1 -1
  81. package/build/lib/protocol/bidi-commands.js +5 -5
  82. package/build/lib/protocol/bidi-commands.js.map +1 -1
  83. package/build/lib/protocol/errors.d.ts.map +1 -1
  84. package/build/lib/protocol/helpers.d.ts +7 -11
  85. package/build/lib/protocol/helpers.d.ts.map +1 -1
  86. package/build/lib/protocol/helpers.js +5 -9
  87. package/build/lib/protocol/helpers.js.map +1 -1
  88. package/build/lib/protocol/index.d.ts +4 -21
  89. package/build/lib/protocol/index.d.ts.map +1 -1
  90. package/build/lib/protocol/index.js.map +1 -1
  91. package/build/lib/protocol/protocol.d.ts +15 -1
  92. package/build/lib/protocol/protocol.d.ts.map +1 -1
  93. package/build/lib/protocol/protocol.js +50 -20
  94. package/build/lib/protocol/protocol.js.map +1 -1
  95. package/build/lib/protocol/routes.d.ts +8 -15
  96. package/build/lib/protocol/routes.d.ts.map +1 -1
  97. package/build/lib/protocol/routes.js +18 -33
  98. package/build/lib/protocol/routes.js.map +1 -1
  99. package/lib/basedriver/capabilities.ts +1 -1
  100. package/lib/basedriver/commands/event.ts +2 -2
  101. package/lib/basedriver/commands/execute.ts +2 -2
  102. package/lib/basedriver/commands/find.ts +2 -2
  103. package/lib/basedriver/commands/mixin.ts +1 -1
  104. package/lib/basedriver/commands/timeout.ts +2 -2
  105. package/lib/basedriver/{device-settings.js → device-settings.ts} +24 -35
  106. package/lib/basedriver/{helpers.js → helpers.ts} +208 -266
  107. package/lib/basedriver/logger.ts +3 -0
  108. package/lib/basedriver/validation.ts +2 -2
  109. package/lib/constants.ts +1 -1
  110. package/lib/express/crash.ts +15 -0
  111. package/lib/express/express-logging.ts +84 -0
  112. package/lib/express/{idempotency.js → idempotency.ts} +105 -89
  113. package/lib/express/logger.ts +3 -0
  114. package/lib/express/middleware.ts +187 -0
  115. package/lib/express/{server.js → server.ts} +175 -167
  116. package/lib/express/static.ts +77 -0
  117. package/lib/express/websocket.ts +81 -0
  118. package/lib/helpers/capabilities.ts +83 -0
  119. package/lib/jsonwp-proxy/protocol-converter.ts +284 -0
  120. package/lib/jsonwp-proxy/proxy.js +1 -1
  121. package/lib/jsonwp-status/{status.js → status.ts} +12 -15
  122. package/lib/protocol/{bidi-commands.js → bidi-commands.ts} +7 -5
  123. package/lib/protocol/errors.ts +1 -1
  124. package/lib/protocol/{helpers.js → helpers.ts} +8 -11
  125. package/lib/protocol/protocol.ts +57 -26
  126. package/lib/protocol/{routes.js → routes.ts} +29 -40
  127. package/package.json +11 -11
  128. package/tsconfig.json +3 -1
  129. package/lib/basedriver/logger.js +0 -4
  130. package/lib/express/crash.js +0 -11
  131. package/lib/express/express-logging.js +0 -60
  132. package/lib/express/logger.js +0 -4
  133. package/lib/express/middleware.js +0 -171
  134. package/lib/express/static.js +0 -76
  135. package/lib/express/websocket.js +0 -79
  136. package/lib/helpers/capabilities.js +0 -93
  137. package/lib/jsonwp-proxy/protocol-converter.js +0 -317
  138. /package/lib/protocol/{index.js → index.ts} +0 -0
@@ -1,14 +1,24 @@
1
1
  import _ from 'lodash';
2
2
  import path from 'node:path';
3
- import logger from './logger';
3
+ import {log as logger} from './logger';
4
4
  import {tempDir, fs, util, timing, node} from '@appium/support';
5
- import { LRUCache } from 'lru-cache';
5
+ import {LRUCache} from 'lru-cache';
6
6
  import AsyncLock from 'async-lock';
7
7
  import axios from 'axios';
8
8
  import B from 'bluebird';
9
+ import type {
10
+ ConfigureAppOptions,
11
+ CachedAppInfo,
12
+ PostProcessOptions,
13
+ HTTPHeaders,
14
+ DriverHelpers,
15
+ } from '@appium/types';
16
+ import type {AxiosResponseHeaders, RawAxiosRequestHeaders} from 'axios';
17
+ import type {Readable} from 'node:stream';
9
18
 
10
19
  // for compat with running tests transpiled and in-place
11
20
  export const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
21
+
12
22
  const CACHED_APPS_MAX_AGE_MS = 1000 * 60 * toNaturalNumber(60 * 24, 'APPIUM_APPS_CACHE_MAX_AGE');
13
23
  const MAX_CACHED_APPS = toNaturalNumber(1024, 'APPIUM_APPS_CACHE_MAX_ITEMS');
14
24
  const HTTP_STATUS_NOT_MODIFIED = 304;
@@ -16,8 +26,7 @@ const DEFAULT_REQ_HEADERS = Object.freeze({
16
26
  'user-agent': `Appium (BaseDriver v${BASEDRIVER_VER})`,
17
27
  });
18
28
  const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
19
- /** @type {LRUCache<string, import('@appium/types').CachedAppInfo>} */
20
- const APPLICATIONS_CACHE = new LRUCache({
29
+ const APPLICATIONS_CACHE = new LRUCache<string, CachedAppInfoEntry>({
21
30
  max: MAX_CACHED_APPS,
22
31
  ttl: CACHED_APPS_MAX_AGE_MS, // expire after 24 hours
23
32
  updateAgeOnGet: true,
@@ -44,65 +53,65 @@ process.on('exit', () => {
44
53
 
45
54
  const appPaths = [...APPLICATIONS_CACHE.values()].map(({fullPath}) => fullPath);
46
55
  logger.debug(
47
- `Performing cleanup of ${appPaths.length} cached ` +
48
- util.pluralize('application', appPaths.length)
56
+ `Performing cleanup of ${util.pluralize('cached application', appPaths.length, true)}`
49
57
  );
50
58
  for (const appPath of appPaths) {
59
+ if (!appPath) {
60
+ continue;
61
+ }
51
62
  try {
52
- // @ts-ignore it's defined
53
63
  fs.rimrafSync(appPath);
54
64
  } catch (e) {
55
- logger.warn(e.message);
65
+ logger.warn((e as Error).message);
56
66
  }
57
67
  }
58
68
  });
59
69
 
60
70
  /**
61
- * Perform initial application package configuration
62
- * to prepare it for the further consumption by a driver:
63
- *
64
- * - Manages caching logic
65
- * - Downloads the app from a remote URL to the local filesystem
66
- * - Determines package name
67
- * - Checks basic requirements on the application package
71
+ * Performs initial application package configuration so the app is ready for driver use.
72
+ * Resolves local paths, downloads remote apps (http/https) with optional caching, and
73
+ * runs optional post-process or custom download hooks.
68
74
  *
69
- * @param {string} app
70
- * @param {string|string[]|import('@appium/types').ConfigureAppOptions} options
71
- * @returns {Promise<string>}
75
+ * @param app - Path to a local app or URL of a downloadable app (http/https).
76
+ * @param options - Supported extensions and optional hooks. Either a single extension
77
+ * string, an array of extension strings, or {@link ConfigureAppOptions} (e.g.
78
+ * `supportedExtensions`, `onPostProcess`, `onDownload`).
79
+ * @returns Resolved path to the application (local path or path to downloaded/cached app).
80
+ * @throws {Error} If supported extensions are missing, the app path/URL is invalid, or download fails.
72
81
  */
73
82
  export async function configureApp(
74
- app,
75
- options = /** @type {import('@appium/types').ConfigureAppOptions} */ ({})
76
- ) {
83
+ app: string,
84
+ options: string | string[] | ConfigureAppOptions = {} as ConfigureAppOptions
85
+ ): Promise<string> {
77
86
  if (!_.isString(app)) {
78
87
  // immediately shortcircuit if not given an app
79
88
  return '';
80
89
  }
81
90
 
82
- /** @type {string[]} */
83
- let supportedAppExtensions;
84
- const onPostProcess = !_.isString(options) && !_.isArray(options) ? options.onPostProcess : undefined;
85
- const onDownload = !_.isString(options) && !_.isArray(options) ? options.onDownload : undefined;
91
+ let supportedAppExtensions: string[];
92
+ const opts = !_.isString(options) && !_.isArray(options) ? options : undefined;
93
+ const onPostProcess = opts?.onPostProcess;
94
+ const onDownload = opts?.onDownload;
86
95
 
87
96
  if (_.isString(options)) {
88
97
  supportedAppExtensions = [options];
89
98
  } else if (_.isArray(options)) {
90
99
  supportedAppExtensions = options;
91
100
  } else if (_.isPlainObject(options)) {
92
- supportedAppExtensions = options.supportedExtensions;
101
+ supportedAppExtensions = options.supportedExtensions ?? [];
102
+ } else {
103
+ supportedAppExtensions = [];
93
104
  }
94
- // @ts-ignore this is OK
105
+
95
106
  if (_.isEmpty(supportedAppExtensions)) {
96
107
  throw new Error(`One or more supported app extensions must be provided`);
97
108
  }
98
109
 
99
110
  let newApp = app;
100
111
  const originalAppLink = app;
101
- let packageHash = null;
102
- /** @type {import('axios').AxiosResponse['headers']|undefined} */
103
- let headers;
104
- /** @type {RemoteAppProps} */
105
- const remoteAppProps = {
112
+ let packageHash: string | null = null;
113
+ let headers: AxiosResponseHeaders | RawAxiosRequestHeaders | undefined;
114
+ const remoteAppProps: RemoteAppProps = {
106
115
  lastModified: null,
107
116
  immutable: false,
108
117
  maxAge: null,
@@ -129,9 +138,7 @@ export async function configureApp(
129
138
  if (isUrl) {
130
139
  // Use the app from remote URL
131
140
  logger.info(`Using downloadable app '${newApp}'`);
132
- const reqHeaders = {
133
- ...DEFAULT_REQ_HEADERS,
134
- };
141
+ const reqHeaders = {...DEFAULT_REQ_HEADERS};
135
142
  if (cachedAppInfo?.etag) {
136
143
  reqHeaders['if-none-match'] = cachedAppInfo.etag;
137
144
  } else if (cachedAppInfo?.lastModified) {
@@ -139,7 +146,9 @@ export async function configureApp(
139
146
  }
140
147
  logger.debug(`Request headers: ${JSON.stringify(reqHeaders)}`);
141
148
 
142
- let {headers, stream, status} = await queryAppLink(newApp, reqHeaders);
149
+ let result = await queryAppLink(newApp, reqHeaders);
150
+ headers = result.headers;
151
+ let {stream, status} = result;
143
152
  logger.debug(`Response status: ${status}`);
144
153
  try {
145
154
  if (!_.isEmpty(headers)) {
@@ -149,21 +158,22 @@ export async function configureApp(
149
158
  }
150
159
  if (headers['last-modified']) {
151
160
  logger.debug(`Last-Modified: ${headers['last-modified']}`);
152
- remoteAppProps.lastModified = new Date(headers['last-modified']);
161
+ remoteAppProps.lastModified = new Date(headers['last-modified'] as string);
153
162
  }
154
163
  if (headers['cache-control']) {
155
164
  logger.debug(`Cache-Control: ${headers['cache-control']}`);
156
- remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
157
- const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
165
+ remoteAppProps.immutable = /\bimmutable\b/i.test(String(headers['cache-control']));
166
+ const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(String(headers['cache-control']));
158
167
  if (maxAgeMatch) {
159
168
  remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
160
169
  }
161
170
  }
162
171
  }
163
172
  if (cachedAppInfo && status === HTTP_STATUS_NOT_MODIFIED) {
164
- if (await isAppIntegrityOk(/** @type {string} */ (cachedAppInfo.fullPath), cachedAppInfo.integrity)) {
165
- logger.info(`Reusing previously downloaded application at '${cachedAppInfo.fullPath}'`);
166
- return verifyAppExtension(/** @type {string} */ (cachedAppInfo.fullPath), supportedAppExtensions);
173
+ const cachedPath = cachedAppInfo.fullPath ?? '';
174
+ if (cachedPath && (await isAppIntegrityOk(cachedPath, cachedAppInfo.integrity))) {
175
+ logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
176
+ return verifyAppExtension(cachedPath, supportedAppExtensions);
167
177
  }
168
178
  logger.info(
169
179
  `The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
@@ -174,17 +184,20 @@ export async function configureApp(
174
184
  if (!stream.closed) {
175
185
  stream.destroy();
176
186
  }
177
- ({stream, headers, status} = await queryAppLink(newApp, {...DEFAULT_REQ_HEADERS}));
187
+ result = await queryAppLink(newApp, {...DEFAULT_REQ_HEADERS});
188
+ stream = result.stream;
189
+ headers = result.headers;
190
+ status = result.status;
178
191
  }
179
192
 
180
193
  if (onDownload) {
181
194
  newApp = await onDownload({
182
195
  url: originalAppLink,
183
- headers: /** @type {import('@appium/types').HTTPHeaders} */ (_.clone(headers)),
196
+ headers: _.clone(headers) as HTTPHeaders,
184
197
  stream,
185
198
  });
186
199
  } else {
187
- const fileName = determineFilename(headers, pathname, supportedAppExtensions);
200
+ const fileName = determineFilename(headers, pathname ?? '', supportedAppExtensions);
188
201
  newApp = await fetchApp(stream, await tempDir.path({
189
202
  prefix: fileName,
190
203
  suffix: '',
@@ -214,13 +227,12 @@ export async function configureApp(
214
227
  packageHash = await calculateFileIntegrity(newApp);
215
228
  }
216
229
 
217
- /** @type {(appPathToCache: string) => Promise<string>} */
218
- const storeAppInCache = async (appPathToCache) => {
230
+ const storeAppInCache = async (appPathToCache: string): Promise<string> => {
219
231
  const cachedFullPath = cachedAppInfo?.fullPath;
220
232
  if (cachedFullPath && cachedFullPath !== appPathToCache) {
221
233
  await fs.rimraf(cachedFullPath);
222
234
  }
223
- const integrity = {};
235
+ const integrity: {file?: string; folder?: number} = {};
224
236
  if ((await fs.stat(appPathToCache)).isDirectory()) {
225
237
  integrity.folder = await calculateFolderIntegrity(appPathToCache);
226
238
  } else {
@@ -237,15 +249,14 @@ export async function configureApp(
237
249
  };
238
250
 
239
251
  if (_.isFunction(onPostProcess)) {
240
- const result = await onPostProcess(
241
- /** @type {import('@appium/types').PostProcessOptions<import('axios').AxiosResponseHeaders>} */ ({
242
- cachedAppInfo: _.clone(cachedAppInfo),
243
- isUrl,
244
- originalAppLink,
245
- headers: _.clone(headers),
246
- appPath: newApp,
247
- })
248
- );
252
+ const postProcessArg: PostProcessOptions = {
253
+ cachedAppInfo: _.clone(cachedAppInfo) as CachedAppInfo | undefined,
254
+ isUrl,
255
+ originalAppLink,
256
+ headers: _.clone(headers) as HTTPHeaders,
257
+ appPath: newApp,
258
+ };
259
+ const result = await onPostProcess(postProcessArg);
249
260
  return !result?.appPath || app === result?.appPath || !(await fs.exists(result?.appPath))
250
261
  ? newApp
251
262
  : await storeAppInCache(result.appPath);
@@ -259,33 +270,35 @@ export async function configureApp(
259
270
  }
260
271
 
261
272
  /**
262
- * @param {string} app
263
- * @returns {boolean}
273
+ * Returns whether the given string looks like a package or bundle identifier
274
+ * (e.g. `com.example.app` or `org.company.AnotherApp`).
275
+ *
276
+ * @param app - Value to check (e.g. app path or bundle id).
277
+ * @returns `true` if the value matches a dot-separated identifier pattern.
264
278
  */
265
- export function isPackageOrBundle(app) {
279
+ export function isPackageOrBundle(app: string): boolean {
266
280
  return /^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/.test(app);
267
281
  }
268
282
 
269
283
  /**
270
- * Finds all instances 'firstKey' and create a duplicate with the key 'secondKey',
271
- * Do the same thing in reverse. If we find 'secondKey', create a duplicate with the key 'firstKey'.
284
+ * Recursively ensures both keys exist with the same value in objects and arrays.
285
+ * For each object, if `firstKey` exists its value is also set at `secondKey`, and vice versa.
272
286
  *
273
- * This will cause keys to be overwritten if the object contains 'firstKey' and 'secondKey'.
274
-
275
- * @param {*} input Any type of input
276
- * @param {String} firstKey The first key to duplicate
277
- * @param {String} secondKey The second key to duplicate
287
+ * @param input - Object, array, or primitive to process (arrays/objects traversed recursively).
288
+ * @param firstKey - First key name to mirror.
289
+ * @param secondKey - Second key name to mirror.
290
+ * @returns A deep copy of `input` with both keys present where objects had either key.
278
291
  */
279
- export function duplicateKeys(input, firstKey, secondKey) {
292
+ export function duplicateKeys<T>(input: T, firstKey: string, secondKey: string): T {
280
293
  // If array provided, recursively call on all elements
281
294
  if (_.isArray(input)) {
282
- return input.map((item) => duplicateKeys(item, firstKey, secondKey));
295
+ return input.map((item) => duplicateKeys(item, firstKey, secondKey)) as T;
283
296
  }
284
297
 
285
298
  // If object, create duplicates for keys and then recursively call on values
286
299
  if (_.isPlainObject(input)) {
287
- const resultObj = {};
288
- for (let [key, value] of _.toPairs(input)) {
300
+ const resultObj: Record<string, unknown> = {};
301
+ for (const [key, value] of _.toPairs(input as Record<string, unknown>)) {
289
302
  const recursivelyCalledValue = duplicateKeys(value, firstKey, secondKey);
290
303
  if (key === firstKey) {
291
304
  resultObj[secondKey] = recursivelyCalledValue;
@@ -294,7 +307,7 @@ export function duplicateKeys(input, firstKey, secondKey) {
294
307
  }
295
308
  resultObj[key] = recursivelyCalledValue;
296
309
  }
297
- return resultObj;
310
+ return resultObj as T;
298
311
  }
299
312
 
300
313
  // Base case. Return primitives without doing anything.
@@ -302,13 +315,14 @@ export function duplicateKeys(input, firstKey, secondKey) {
302
315
  }
303
316
 
304
317
  /**
305
- * Takes a capability value and tries to JSON.parse it as an array,
306
- * and either returns the parsed array or a singleton array.
318
+ * Normalizes a capability value to a string array. If already an array, returns it;
319
+ * if a string, parses as JSON array when possible, otherwise returns a single-element array.
307
320
  *
308
- * @param {string|string[]} capValue Capability value
309
- * @returns {string[]}
321
+ * @param capValue - Capability value: string (including JSON array like `"[\"a\",\"b\"]"`) or string[].
322
+ * @returns Array of strings.
323
+ * @throws {TypeError} If value is not a string/array or JSON parsing fails for array-like input.
310
324
  */
311
- export function parseCapsArray(capValue) {
325
+ export function parseCapsArray(capValue: string | string[]): string[] {
312
326
  if (_.isArray(capValue)) {
313
327
  return capValue;
314
328
  }
@@ -319,7 +333,7 @@ export function parseCapsArray(capValue) {
319
333
  return parsed;
320
334
  }
321
335
  } catch (e) {
322
- const message = `Failed to parse capability as JSON array: ${e.message}`;
336
+ const message = `Failed to parse capability as JSON array: ${(e as Error).message}`;
323
337
  if (_.isString(capValue) && _.startsWith(_.trimStart(capValue), '[')) {
324
338
  throw new TypeError(message);
325
339
  }
@@ -332,86 +346,126 @@ export function parseCapsArray(capValue) {
332
346
  }
333
347
 
334
348
  /**
335
- * Generate a string that uniquely describes driver instance
349
+ * Builds a short log prefix for a driver instance (e.g. `UiAutomator2@a1b2`).
336
350
  *
337
- * @param {object} obj driver instance
338
- * @param {string?} [sessionId=null] session identifier (if exists).
339
- * This parameter is deprecated and is not used.
340
- * @returns {string}
351
+ * @param obj - Driver or other object; its constructor name and a short id are used.
352
+ * @param _sessionId - Deprecated and unused; kept for {@link DriverHelpers} interface compatibility.
353
+ * @returns Prefix string like `DriverName@xxxx`, or `UnknownDriver@????` if `obj` is null.
341
354
  */
342
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
343
- export function generateDriverLogPrefix(obj, sessionId = null) {
355
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- DriverHelpers interface
356
+ export function generateDriverLogPrefix(obj: object | null, _sessionId?: string | null): string {
357
+ if (!obj) {
358
+ // This should not happen
359
+ return 'UnknownDriver@????';
360
+ }
344
361
  return `${obj.constructor.name}@${node.getObjectId(obj).substring(0, 4)}`;
345
362
  }
346
363
 
364
+ // #region Private types and helpers
365
+ interface RemoteAppProps {
366
+ lastModified: Date | null;
367
+ immutable: boolean;
368
+ maxAge: number | null;
369
+ etag: string | null;
370
+ }
371
+
372
+ interface RemoteAppData {
373
+ status: number;
374
+ stream: Readable;
375
+ headers: AxiosResponseHeaders | RawAxiosRequestHeaders;
376
+ }
377
+
378
+ function parseAppLink(appLink: string): URL | {protocol?: string; pathname?: string; href?: string; search?: string} {
379
+ try {
380
+ return new URL(appLink);
381
+ } catch {
382
+ return {};
383
+ }
384
+ }
385
+
386
+ function isEnvOptionEnabled(optionName: string, defaultValue: boolean | null = null): boolean {
387
+ const value = process.env[optionName];
388
+ if (!_.isNull(defaultValue) && _.isEmpty(value)) {
389
+ return defaultValue;
390
+ }
391
+ return !_.isEmpty(value) && !['0', 'false', 'no'].includes(_.toLower(value));
392
+ }
393
+
394
+ function isSupportedUrl(app: string): boolean {
395
+ try {
396
+ const {protocol} = parseAppLink(app);
397
+ return ['http:', 'https:'].includes(protocol ?? '');
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+
347
403
  /**
348
- * Sends a HTTP GET query to fetch the app with caching enabled.
349
- * Follows https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
350
- *
351
- * @param {string} appLink The URL to download an app from
352
- * @param {import('axios').RawAxiosRequestHeaders} reqHeaders Additional HTTP request headers
353
- * @returns {Promise<RemoteAppData>}
404
+ * Transforms the given app link to the cache key.
405
+ * Necessary to properly cache apps having the same address but different query strings,
406
+ * e.g. ones stored in S3 using presigned URLs.
354
407
  */
355
- async function queryAppLink(appLink, reqHeaders) {
408
+ function toCacheKey(app: string): string {
409
+ if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
410
+ return app;
411
+ }
412
+ try {
413
+ const parsed = parseAppLink(app);
414
+ const href = 'href' in parsed ? parsed.href : undefined;
415
+ const search = 'search' in parsed ? parsed.search : undefined;
416
+ if (href && search) {
417
+ return href.replace(search, '');
418
+ }
419
+ if (href) {
420
+ return href;
421
+ }
422
+ } catch {
423
+ // ignore
424
+ }
425
+ return app;
426
+ }
427
+
428
+ async function queryAppLink(appLink: string, reqHeaders: RawAxiosRequestHeaders): Promise<RemoteAppData> {
356
429
  const url = new URL(appLink);
357
430
  // Extract credentials, then remove them from the URL for axios
358
431
  const {username, password} = url;
359
432
  url.username = '';
360
433
  url.password = '';
361
434
  const axiosUrl = url.href;
362
- /** @type {import('axios').AxiosBasicCredentials|undefined} */
363
- const axiosAuth = username ? {
364
- username,
365
- password,
366
- } : undefined;
367
- /**
368
- * @type {import('axios').RawAxiosRequestConfig}
369
- */
435
+ const axiosAuth = username ? {username, password} : undefined;
370
436
  const requestOpts = {
371
437
  url: axiosUrl,
372
438
  auth: axiosAuth,
373
- responseType: 'stream',
439
+ responseType: 'stream' as const,
374
440
  timeout: APP_DOWNLOAD_TIMEOUT_MS,
375
- validateStatus: (status) =>
441
+ validateStatus: (status: number) =>
376
442
  (status >= 200 && status < 300) || status === HTTP_STATUS_NOT_MODIFIED,
377
443
  headers: reqHeaders,
378
444
  };
379
445
  try {
380
446
  const {data: stream, headers, status} = await axios(requestOpts);
381
- return {
382
- stream,
383
- headers,
384
- status,
385
- };
447
+ return {stream, headers, status};
386
448
  } catch (err) {
387
- throw new Error(`Cannot download the app from ${axiosUrl}: ${err.message}`);
449
+ throw new Error(`Cannot download the app from ${axiosUrl}: ${(err as Error).message}`);
388
450
  }
389
451
  }
390
452
 
391
- /**
392
- * Retrieves app payload from the given stream. Also meters the download performance.
393
- *
394
- * @param {import('stream').Readable} srcStream The incoming stream
395
- * @param {string} dstPath The target file path to be written
396
- * @returns {Promise<string>} The same dstPath
397
- * @throws {Error} If there was a failure while downloading the file
398
- */
399
- async function fetchApp(srcStream, dstPath) {
453
+ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
400
454
  const timer = new timing.Timer().start();
401
455
  try {
402
456
  const writer = fs.createWriteStream(dstPath);
403
457
  srcStream.pipe(writer);
404
458
 
405
- await new B((resolve, reject) => {
459
+ await new B<void>((resolve, reject) => {
406
460
  srcStream.once('error', reject);
407
- writer.once('finish', resolve);
408
- writer.once('error', (e) => {
461
+ writer.once('finish', () => resolve());
462
+ writer.once('error', (e: Error) => {
409
463
  srcStream.unpipe(writer);
410
464
  reject(e);
411
465
  });
412
466
  });
413
467
  } catch (err) {
414
- throw new Error(`Cannot fetch the application: ${err.message}`);
468
+ throw new Error(`Cannot fetch the application: ${(err as Error).message}`);
415
469
  }
416
470
 
417
471
  const secondsElapsed = timer.getDuration().asSeconds;
@@ -429,70 +483,20 @@ async function fetchApp(srcStream, dstPath) {
429
483
  return dstPath;
430
484
  }
431
485
 
432
- /**
433
- * Transforms the given app link to the cache key.
434
- * This is necessary to properly cache apps
435
- * having the same address, but different query strings,
436
- * for example ones stored in S3 using presigned URLs.
437
- *
438
- * @param {string} app App link.
439
- * @returns {string} Transformed app link or the original arg if
440
- * no transformation is needed.
441
- */
442
- function toCacheKey(app) {
443
- if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
444
- return app;
445
- }
446
- try {
447
- const {href, search} = parseAppLink(app);
448
- if (href && search) {
449
- return href.replace(search, '');
450
- }
451
- if (href) {
452
- return href;
453
- }
454
- } catch {}
455
- return app;
456
- }
457
-
458
- /**
459
- * Safely parses the given app link to a URL object
460
- *
461
- * @param {string} appLink
462
- * @returns {URL|import('@appium/types').StringRecord} Parsed URL object
463
- * or an empty object if the parsing has failed
464
- */
465
- function parseAppLink(appLink) {
466
- try {
467
- return new URL(appLink);
468
- } catch {
469
- return {};
470
- }
471
- }
472
-
473
- /**
474
- * Tries to determine the file name of the payload that is going
475
- * to be downloaded from an URL
476
- *
477
- * @param {import('axios').RawAxiosRequestHeaders} headers
478
- * @param {string} pathname
479
- * @param {string[]} supportedAppExtensions
480
- * @returns {string}
481
- */
482
- function determineFilename(headers, pathname, supportedAppExtensions) {
486
+ function determineFilename(
487
+ headers: AxiosResponseHeaders | RawAxiosRequestHeaders,
488
+ pathname: string,
489
+ supportedAppExtensions: string[]
490
+ ): string {
483
491
  const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
484
492
  replacement: SANITIZE_REPLACEMENT,
485
493
  });
486
494
  const extname = path.extname(basename);
487
- if (headers['content-disposition'] && /^attachment/i.test(
488
- /** @type {string} */ (headers['content-disposition']
489
- ))) {
495
+ if (headers['content-disposition'] && /^attachment/i.test(String(headers['content-disposition']))) {
490
496
  logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
491
- const match = /filename="([^"]+)/i.exec(/** @type {string} */ (headers['content-disposition']));
497
+ const match = /filename="([^"]+)/i.exec(String(headers['content-disposition']));
492
498
  if (match) {
493
- return fs.sanitizeName(match[1], {
494
- replacement: SANITIZE_REPLACEMENT,
495
- });
499
+ return fs.sanitizeName(match[1], {replacement: SANITIZE_REPLACEMENT});
496
500
  }
497
501
  }
498
502
 
@@ -506,63 +510,12 @@ function determineFilename(headers, pathname, supportedAppExtensions) {
506
510
  `The current file extension '${resultingExt}' is not supported. ` +
507
511
  `Defaulting to '${_.first(supportedAppExtensions)}'`
508
512
  );
509
- resultingExt = /** @type {string} */ (_.first(supportedAppExtensions));
513
+ resultingExt = _.first(supportedAppExtensions) as string;
510
514
  }
511
515
  return `${resultingName}${resultingExt}`;
512
516
  }
513
517
 
514
- /**
515
- * Checks whether we can threat the given app link
516
- * as a URL,
517
- *
518
- * @param {string} app
519
- * @returns {boolean} True if app is a supported URL
520
- */
521
- function isSupportedUrl(app) {
522
- try {
523
- const {protocol} = parseAppLink(app);
524
- return ['http:', 'https:'].includes(protocol);
525
- } catch {
526
- return false;
527
- }
528
- }
529
-
530
- /**
531
- * Check if the given environment option is enabled
532
- *
533
- * @param {string} optionName Option name
534
- * @param {boolean|null} [defaultValue=null] The value to return if the given env value
535
- * is not set explicitly
536
- * @returns {boolean} True if the option is enabled
537
- */
538
- function isEnvOptionEnabled(optionName, defaultValue = null) {
539
- const value = process.env[optionName];
540
- if (!_.isNull(defaultValue) && _.isEmpty(value)) {
541
- return defaultValue;
542
- }
543
- return !_.isEmpty(value) && !['0', 'false', 'no'].includes(_.toLower(value));
544
- }
545
-
546
- /**
547
- *
548
- * @param {string} [envVarName]
549
- * @param {number} defaultValue
550
- * @returns {number}
551
- */
552
- function toNaturalNumber(defaultValue, envVarName) {
553
- if (!envVarName || _.isUndefined(process.env[envVarName])) {
554
- return defaultValue;
555
- }
556
- const num = parseInt(`${process.env[envVarName]}`, 10);
557
- return num > 0 ? num : defaultValue;
558
- }
559
-
560
- /**
561
- * @param {string} app
562
- * @param {string[]} supportedAppExtensions
563
- * @returns {string}
564
- */
565
- function verifyAppExtension(app, supportedAppExtensions) {
518
+ function verifyAppExtension(app: string, supportedAppExtensions: string[]): string {
566
519
  if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
567
520
  return app;
568
521
  }
@@ -573,28 +526,18 @@ function verifyAppExtension(app, supportedAppExtensions) {
573
526
  );
574
527
  }
575
528
 
576
- /**
577
- * @param {string} folderPath
578
- * @returns {Promise<number>}
579
- */
580
- async function calculateFolderIntegrity(folderPath) {
529
+ async function calculateFolderIntegrity(folderPath: string): Promise<number> {
581
530
  return (await fs.glob('**/*', {cwd: folderPath})).length;
582
531
  }
583
532
 
584
- /**
585
- * @param {string} filePath
586
- * @returns {Promise<string>}
587
- */
588
- async function calculateFileIntegrity(filePath) {
533
+ async function calculateFileIntegrity(filePath: string): Promise<string> {
589
534
  return await fs.hash(filePath);
590
535
  }
591
536
 
592
- /**
593
- * @param {string} currentPath
594
- * @param {import('@appium/types').StringRecord} expectedIntegrity
595
- * @returns {Promise<boolean>}
596
- */
597
- async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
537
+ async function isAppIntegrityOk(
538
+ currentPath: string,
539
+ expectedIntegrity: {file?: string; folder?: number} = {}
540
+ ): Promise<boolean> {
598
541
  if (!(await fs.exists(currentPath))) {
599
542
  return false;
600
543
  }
@@ -607,30 +550,29 @@ async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
607
550
  // more precise, but we don't need to be very precise here and also don't want to
608
551
  // overuse RAM and have a performance drop.
609
552
  return (await fs.stat(currentPath)).isDirectory()
610
- ? (await calculateFolderIntegrity(currentPath)) >= expectedIntegrity?.folder
553
+ ? (await calculateFolderIntegrity(currentPath)) >= (expectedIntegrity?.folder ?? 0)
611
554
  : (await calculateFileIntegrity(currentPath)) === expectedIntegrity?.file;
612
555
  }
613
556
 
614
- /** @type {import('@appium/types').DriverHelpers} */
557
+ function toNaturalNumber(defaultValue: number, envVarName?: string): number {
558
+ if (!envVarName || _.isUndefined(process.env[envVarName])) {
559
+ return defaultValue;
560
+ }
561
+ const num = parseInt(`${process.env[envVarName]}`, 10);
562
+ return num > 0 ? num : defaultValue;
563
+ }
564
+
565
+ /** Cache value we store (extends CachedAppInfo with optional packageHash) */
566
+ interface CachedAppInfoEntry extends Omit<CachedAppInfo, 'packageHash'> {
567
+ packageHash?: string | null;
568
+ fullPath?: string;
569
+ }
570
+ // #endregion
571
+
615
572
  export default {
616
573
  configureApp,
617
574
  isPackageOrBundle,
618
575
  duplicateKeys,
619
576
  parseCapsArray,
620
577
  generateDriverLogPrefix,
621
- };
622
-
623
- /**
624
- * @typedef RemoteAppProps
625
- * @property {Date?} lastModified
626
- * @property {boolean} immutable
627
- * @property {number?} maxAge
628
- * @property {string?} etag
629
- */
630
-
631
- /**
632
- * @typedef RemoteAppData Properties of the remote application (e.g. GET HTTP response) to be downloaded.
633
- * @property {number} status The HTTP status of the response
634
- * @property {import('stream').Readable} stream The HTTP response body represented as readable stream
635
- * @property {import('axios').RawAxiosResponseHeaders | import('axios').AxiosResponseHeaders} headers HTTP response headers
636
- */
578
+ } satisfies DriverHelpers;