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