@appium/base-driver 10.1.2 → 10.2.1

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