appium 3.1.2 → 3.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 (79) hide show
  1. package/README.md +4 -4
  2. package/build/lib/appium.d.ts +2 -2
  3. package/build/lib/appium.d.ts.map +1 -1
  4. package/build/lib/appium.js +5 -7
  5. package/build/lib/appium.js.map +1 -1
  6. package/build/lib/bidi-commands.d.ts +1 -1
  7. package/build/lib/bidi-commands.d.ts.map +1 -1
  8. package/build/lib/bidi-commands.js.map +1 -1
  9. package/build/lib/cli/extension-command.d.ts +109 -0
  10. package/build/lib/cli/extension-command.d.ts.map +1 -1
  11. package/build/lib/cli/extension-command.js +255 -96
  12. package/build/lib/cli/extension-command.js.map +1 -1
  13. package/build/lib/cli/parser.d.ts +10 -0
  14. package/build/lib/cli/parser.d.ts.map +1 -1
  15. package/build/lib/cli/parser.js +20 -2
  16. package/build/lib/cli/parser.js.map +1 -1
  17. package/build/lib/constants.js +3 -3
  18. package/build/lib/constants.js.map +1 -1
  19. package/build/lib/extension/extension-config.js +7 -7
  20. package/build/lib/extension/extension-config.js.map +1 -1
  21. package/build/lib/extension/manifest.js +6 -6
  22. package/build/lib/extension/manifest.js.map +1 -1
  23. package/build/lib/extension/package-changed.js +3 -3
  24. package/build/lib/extension/package-changed.js.map +1 -1
  25. package/build/lib/inspector-commands.d.ts.map +1 -1
  26. package/build/lib/logger.d.ts +1 -1
  27. package/build/lib/logger.d.ts.map +1 -1
  28. package/build/lib/logsink.d.ts.map +1 -1
  29. package/build/lib/logsink.js +9 -7
  30. package/build/lib/logsink.js.map +1 -1
  31. package/build/lib/main.d.ts.map +1 -1
  32. package/build/lib/main.js +11 -2
  33. package/build/lib/main.js.map +1 -1
  34. package/build/lib/schema/cli-args.d.ts.map +1 -1
  35. package/build/lib/schema/cli-args.js +5 -0
  36. package/build/lib/schema/cli-args.js.map +1 -1
  37. package/build/lib/schema/cli-transformers.js +5 -5
  38. package/build/lib/schema/cli-transformers.js.map +1 -1
  39. package/build/lib/schema/schema.js +2 -2
  40. package/build/lib/schema/schema.js.map +1 -1
  41. package/build/lib/utils.d.ts +1 -1
  42. package/build/lib/utils.d.ts.map +1 -1
  43. package/build/lib/utils.js +8 -8
  44. package/build/lib/utils.js.map +1 -1
  45. package/build/package.json +99 -0
  46. package/build/types/cli.d.ts +3 -3
  47. package/build/types/cli.d.ts.map +1 -1
  48. package/build/types/index.d.ts +1 -1
  49. package/build/types/index.d.ts.map +1 -1
  50. package/build/types/manifest/base.d.ts +3 -3
  51. package/build/types/manifest/base.d.ts.map +1 -1
  52. package/build/types/manifest/v3.d.ts +3 -3
  53. package/build/types/manifest/v3.d.ts.map +1 -1
  54. package/build/types/manifest/v4.d.ts +3 -3
  55. package/build/types/manifest/v4.d.ts.map +1 -1
  56. package/index.js +1 -3
  57. package/lib/appium.js +5 -7
  58. package/lib/bidi-commands.ts +4 -6
  59. package/lib/cli/extension-command.js +283 -105
  60. package/lib/cli/parser.js +21 -2
  61. package/lib/constants.js +1 -1
  62. package/lib/extension/extension-config.js +2 -2
  63. package/lib/extension/manifest.js +1 -1
  64. package/lib/extension/package-changed.js +1 -1
  65. package/lib/inspector-commands.ts +1 -1
  66. package/lib/logsink.js +11 -7
  67. package/lib/main.js +13 -3
  68. package/lib/schema/cli-args.js +6 -0
  69. package/lib/schema/cli-transformers.js +1 -1
  70. package/lib/schema/schema.js +1 -1
  71. package/lib/utils.js +3 -3
  72. package/package.json +22 -24
  73. package/scripts/autoinstall-extensions.js +7 -10
  74. package/tsconfig.json +5 -2
  75. package/types/cli.ts +4 -4
  76. package/types/index.ts +1 -1
  77. package/types/manifest/base.ts +3 -3
  78. package/types/manifest/v3.ts +3 -3
  79. package/types/manifest/v4.ts +3 -3
package/lib/appium.js CHANGED
@@ -560,16 +560,14 @@ class AppiumDriver extends DriverCore {
560
560
  /**
561
561
  * Get the appropriate plugins for a session (or sessionless plugins)
562
562
  *
563
- * @param {?string} sessionId - the sessionId (or null) to use to find plugins
563
+ * @param {string|null} [sessionId=null] - the sessionId (or null) to use to find plugins
564
564
  * @returns {Array<import('@appium/types').Plugin>} - array of plugin instances
565
565
  */
566
566
  pluginsForSession(sessionId = null) {
567
567
  if (sessionId) {
568
- if (!this.sessionPlugins[sessionId]) {
569
- const driverId = generateDriverLogPrefix(this.sessions[sessionId]);
570
- this.sessionPlugins[sessionId] = this.createPluginInstances(driverId || null);
571
- }
572
- return this.sessionPlugins[sessionId];
568
+ const driver = this.sessions[sessionId];
569
+ return this.sessionPlugins[sessionId]
570
+ ?? (driver ? this.createPluginInstances(generateDriverLogPrefix(driver)) : []);
573
571
  }
574
572
 
575
573
  if (_.isEmpty(this.sessionlessPlugins)) {
@@ -598,7 +596,7 @@ class AppiumDriver extends DriverCore {
598
596
 
599
597
  /**
600
598
  * Creates instances of all of the enabled Plugin classes
601
- * @param {string|null} driverId - ID to use for linking a driver to a plugin in logs
599
+ * @param {string|null} [driverId=null] - ID to use for linking a driver to a plugin in logs
602
600
  * @returns {Plugin[]}
603
601
  */
604
602
  createPluginInstances(driverId = null) {
@@ -1,9 +1,7 @@
1
1
  import _ from 'lodash';
2
2
  import B from 'bluebird';
3
- import {
4
- errors,
5
- ExtensionCore,
6
- } from '@appium/base-driver';
3
+ import type {ExtensionCore} from '@appium/base-driver';
4
+ import {errors} from '@appium/base-driver';
7
5
  import {BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
8
6
  import WebSocket from 'ws';
9
7
  import os from 'node:os';
@@ -23,7 +21,7 @@ import type {
23
21
  BiDiResultData
24
22
  } from '@appium/types';
25
23
 
26
- type ExtensionPlugin = Plugin & ExtensionCore
24
+ type ExtensionPlugin = Plugin & ExtensionCore;
27
25
  type AnyDriver = ExternalDriver | AppiumDriver;
28
26
  type SendData = (data: string | Buffer) => Promise<void>;
29
27
  type LogSocketError = (err: Error) => void;
@@ -448,7 +446,7 @@ function initBidiEventListeners(
448
446
  // sure the client is subscribed and then pass it on
449
447
  const eventLogCounts: Record<string, number> = BIDI_EVENTS_MAP.get(bidiHandlerDriver) ?? {};
450
448
  BIDI_EVENTS_MAP.set(bidiHandlerDriver, eventLogCounts);
451
- const eventListenerFactory = (extType: 'driver'|'plugin', ext: ExtensionCore) => {
449
+ const eventListenerFactory = (extType: 'driver' | 'plugin', ext: ExtensionCore) => {
452
450
  const eventListener = async ({context, method, params = {}}) => {
453
451
  // if the driver didn't specify a context, use the empty context
454
452
  if (!context) {
@@ -1,6 +1,6 @@
1
1
  import B from 'bluebird';
2
2
  import _ from 'lodash';
3
- import path from 'path';
3
+ import path from 'node:path';
4
4
  import {npm, util, env, console, fs, system} from '@appium/support';
5
5
  import {spinWith, RingBuffer} from './utils';
6
6
  import {
@@ -12,14 +12,15 @@ import {
12
12
  } from '../extension/extension-config';
13
13
  import {SubProcess} from 'teen_process';
14
14
  import {packageDidChange} from '../extension/package-changed';
15
- import {spawn} from 'child_process';
15
+ import {spawn} from 'node:child_process';
16
16
  import {inspect} from 'node:util';
17
- import {pathToFileURL} from 'url';
17
+ import {pathToFileURL} from 'node:url';
18
18
  import {Doctor, EXIT_CODE as DOCTOR_EXIT_CODE} from '../doctor/doctor';
19
19
  import {getAppiumModuleRoot, npmPackage} from '../utils';
20
20
  import * as semver from 'semver';
21
21
 
22
22
  const UPDATE_ALL = 'installed';
23
+ const MAX_CONCURRENT_REPO_FETCHES = 5;
23
24
 
24
25
  class NotUpdatableError extends Error {}
25
26
  class NoUpdatesAvailableError extends Error {}
@@ -96,6 +97,7 @@ class ExtensionCliCommand {
96
97
  * For TS to understand that a function throws an exception, it must actually throw an exception--
97
98
  * in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
98
99
  * nor is something like `@returns {never}` which does not imply a thrown exception.
100
+ *
99
101
  * @param {string} message
100
102
  * @protected
101
103
  * @throws {Error}
@@ -121,18 +123,45 @@ class ExtensionCliCommand {
121
123
 
122
124
  /**
123
125
  * List extensions
126
+ *
124
127
  * @template {ExtensionType} ExtType
125
128
  * @param {ListOptions} opts
126
129
  * @return {Promise<ExtensionList<ExtType>>} map of extension names to extension data
127
130
  */
128
131
  async list({showInstalled, showUpdates, verbose = false}) {
129
- let lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
132
+ const listData = this._buildListData(showInstalled);
133
+
134
+ const lsMsg =
135
+ `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s` +
136
+ (verbose ? ' (verbose mode)' : ' (rerun with --verbose for more info)');
137
+ await this._checkForUpdates(listData, showUpdates, lsMsg);
138
+
139
+ if (this.isJsonOutput) {
140
+ await this._addRepositoryUrlsToListData(listData);
141
+ return listData;
142
+ }
143
+
130
144
  if (verbose) {
131
- lsMsg += ' (verbose mode)';
145
+ await this._addRepositoryUrlsToListData(listData);
146
+ this.log.log(inspect(listData, {colors: true, depth: null}));
147
+ return listData;
132
148
  }
149
+
150
+ return await this._displayNormalListOutput(listData, showUpdates);
151
+ }
152
+
153
+ /**
154
+ * Build the initial list data structure from installed and known extensions
155
+ *
156
+ * @template {ExtensionType} ExtType
157
+ * @param {boolean} showInstalled
158
+ * @returns {ExtensionList<ExtType>}
159
+ * @private
160
+ */
161
+ _buildListData(showInstalled) {
133
162
  const installedNames = Object.keys(this.config.installedExtensions);
134
163
  const knownNames = Object.keys(this.knownExtensions);
135
- const listData = [...installedNames, ...knownNames].reduce((acc, name) => {
164
+ return [...installedNames, ...knownNames].reduce((acc, name) => {
136
165
  if (!acc[name]) {
137
166
  if (installedNames.includes(name)) {
138
167
  acc[name] = {
@@ -148,99 +177,233 @@ class ExtensionCliCommand {
148
177
  }
149
178
  return acc;
150
179
  }, /** @type {ExtensionList<ExtType>} */ ({}));
180
+ }
151
181
 
152
- // if we want to show whether updates are available, put that behind a spinner
182
+ /**
183
+ * Check for available updates for installed extensions
184
+ *
185
+ * @template {ExtensionType} ExtType
186
+ * @param {ExtensionList<ExtType>} listData
187
+ * @param {boolean} showUpdates
188
+ * @param {string} lsMsg
189
+ * @returns {Promise<void>}
190
+ * @private
191
+ */
192
+ async _checkForUpdates(listData, showUpdates, lsMsg) {
153
193
  await spinWith(this.isJsonOutput, lsMsg, async () => {
194
+ // We'd like to still show lsMsg even if showUpdates is false
154
195
  if (!showUpdates) {
155
196
  return;
156
197
  }
157
- for (const [ext, data] of _.toPairs(listData)) {
158
- if (!data.installed || data.installType !== INSTALL_TYPE_NPM) {
159
- // don't need to check for updates on exts that aren't installed
160
- // also don't need to check for updates on non-npm exts
161
- continue;
162
- }
163
- try {
164
- const updates = await this.checkForExtensionUpdate(ext);
165
- data.updateVersion = updates.safeUpdate;
166
- data.unsafeUpdateVersion = updates.unsafeUpdate;
167
- data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
168
- } catch (e) {
169
- data.updateError = e.message;
170
- }
171
- }
198
+
199
+ // Filter to only extensions that need update checks (installed npm packages)
200
+ const extensionsToCheck = _.toPairs(listData).filter(
201
+ ([, data]) => data.installed && data.installType === INSTALL_TYPE_NPM
202
+ );
203
+
204
+ await B.map(
205
+ extensionsToCheck,
206
+ async ([ext, data]) => {
207
+ try {
208
+ const updates = await this.checkForExtensionUpdate(ext);
209
+ data.updateVersion = updates.safeUpdate;
210
+ data.unsafeUpdateVersion = updates.unsafeUpdate;
211
+ data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
212
+ } catch (e) {
213
+ data.updateError = e.message;
214
+ }
215
+ },
216
+ {concurrency: MAX_CONCURRENT_REPO_FETCHES}
217
+ );
172
218
  });
219
+ }
173
220
 
174
- /**
175
- * Type guard to narrow "installed" extensions, which have more data
176
- * @param {any} data
177
- * @returns {data is InstalledExtensionListData<ExtType>}
178
- */
179
- const extIsInstalled = (data) => Boolean(data.installed);
221
+ /**
222
+ * Add repository URLs to list data for all extensions
223
+ *
224
+ * @template {ExtensionType} ExtType
225
+ * @param {ExtensionList<ExtType>} listData
226
+ * @returns {Promise<void>}
227
+ * @private
228
+ */
229
+ async _addRepositoryUrlsToListData(listData) {
230
+ await spinWith(this.isJsonOutput, 'Fetching repository information', async () => {
231
+ await B.map(
232
+ _.values(listData),
233
+ async (data) => {
234
+ const repoUrl = await this._getRepositoryUrl(data);
235
+ if (repoUrl) {
236
+ data.repositoryUrl = repoUrl;
237
+ }
238
+ },
239
+ {concurrency: MAX_CONCURRENT_REPO_FETCHES}
240
+ );
241
+ });
242
+ }
180
243
 
181
- // if we're just getting the data, short circuit return here since we don't need to do any
182
- // formatting logic
183
- if (this.isJsonOutput) {
184
- return listData;
244
+ /**
245
+ * Display normal formatted output
246
+ *
247
+ * @template {ExtensionType} ExtType
248
+ * @param {ExtensionList<ExtType>} listData
249
+ * @param {boolean} showUpdates
250
+ * @returns {Promise<ExtensionList<ExtType>>}
251
+ * @private
252
+ */
253
+ async _displayNormalListOutput(listData, showUpdates) {
254
+ for (const [name, data] of _.toPairs(listData)) {
255
+ const line = await this._formatExtensionLine(name, data, showUpdates);
256
+ this.log.log(line);
185
257
  }
186
258
 
187
- if (verbose) {
188
- this.log.log(inspect(listData, {colors: true, depth: null}));
189
- return listData;
259
+ return listData;
260
+ }
261
+
262
+ /**
263
+ * Format a single extension line for display
264
+ *
265
+ * @template {ExtensionType} ExtType
266
+ * @param {string} name
267
+ * @param {ExtensionListData<ExtType>} data
268
+ * @param {boolean} showUpdates
269
+ * @returns {Promise<string>}
270
+ * @private
271
+ */
272
+ async _formatExtensionLine(name, data, showUpdates) {
273
+ if (data.installed) {
274
+ const installTxt = this._formatInstallText(/** @type {InstalledExtensionListData<ExtType>} */ (data));
275
+ const updateTxt = showUpdates ? this._formatUpdateText(/** @type {InstalledExtensionListData<ExtType>} */ (data)) : '';
276
+ return `- ${name.yellow}${installTxt}${updateTxt}`;
190
277
  }
191
- for (const [name, data] of _.toPairs(listData)) {
192
- let installTxt = ' [not installed]'.grey;
193
- let updateTxt = '';
194
- let upToDateTxt = '';
195
- let unsafeUpdateTxt = '';
196
- if (extIsInstalled(data)) {
197
- const {
198
- installType,
199
- installSpec,
200
- updateVersion,
201
- unsafeUpdateVersion,
202
- version,
203
- upToDate,
204
- updateError,
205
- } = data;
206
- let typeTxt;
207
- switch (installType) {
208
- case INSTALL_TYPE_GIT:
209
- case INSTALL_TYPE_GITHUB:
210
- typeTxt = `(cloned from ${installSpec})`.yellow;
211
- break;
212
- case INSTALL_TYPE_LOCAL:
213
- typeTxt = `(linked from ${installSpec})`.magenta;
214
- break;
215
- case INSTALL_TYPE_DEV:
216
- typeTxt = '(dev mode)';
217
- break;
218
- default:
219
- typeTxt = '(npm)';
220
- }
221
- installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
222
-
223
- if (showUpdates) {
224
- if (updateError) {
225
- updateTxt = ` [Cannot check for updates: ${updateError}]`.red;
226
- } else {
227
- if (updateVersion) {
228
- updateTxt = ` [${updateVersion} available]`.magenta;
229
- }
230
- if (upToDate) {
231
- upToDateTxt = ` [Up to date]`.green;
232
- }
233
- if (unsafeUpdateVersion) {
234
- unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
235
- }
278
+ const installTxt = ' [not installed]'.grey;
279
+ return `- ${name.yellow}${installTxt}`;
280
+ }
281
+
282
+ /**
283
+ * Format installation status text
284
+ *
285
+ * @template {ExtensionType} ExtType
286
+ * @param {InstalledExtensionListData<ExtType>} data
287
+ * @returns {string}
288
+ * @private
289
+ */
290
+ _formatInstallText(data) {
291
+ const {installType, installSpec, version} = data;
292
+ let typeTxt;
293
+ switch (installType) {
294
+ case INSTALL_TYPE_GIT:
295
+ case INSTALL_TYPE_GITHUB:
296
+ typeTxt = `(cloned from ${installSpec})`.yellow;
297
+ break;
298
+ case INSTALL_TYPE_LOCAL:
299
+ typeTxt = `(linked from ${installSpec})`.magenta;
300
+ break;
301
+ case INSTALL_TYPE_DEV:
302
+ typeTxt = '(dev mode)';
303
+ break;
304
+ default:
305
+ typeTxt = '(npm)';
306
+ }
307
+ return `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
308
+ }
309
+
310
+ /**
311
+ * Format update information text
312
+ *
313
+ * @template {ExtensionType} ExtType
314
+ * @param {InstalledExtensionListData<ExtType>} data
315
+ * @returns {string}
316
+ * @private
317
+ */
318
+ _formatUpdateText(data) {
319
+ const {updateVersion, unsafeUpdateVersion, upToDate, updateError} = data;
320
+ if (updateError) {
321
+ return ` [Cannot check for updates: ${updateError}]`.red;
322
+ }
323
+ let txt = '';
324
+ if (updateVersion) {
325
+ txt += ` [${updateVersion} available]`.magenta;
326
+ }
327
+ if (upToDate) {
328
+ txt += ` [Up to date]`.green;
329
+ }
330
+ if (unsafeUpdateVersion) {
331
+ txt += ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
332
+ }
333
+ return txt;
334
+ }
335
+
336
+ /**
337
+ * Get repository URL from package data
338
+ *
339
+ * @template {ExtensionType} ExtType
340
+ * @param {ExtensionListData<ExtType>} data
341
+ * @returns {Promise<string|null>}
342
+ * @private
343
+ */
344
+ async _getRepositoryUrl(data) {
345
+ if (data.installed && data.installPath) {
346
+ return await this._getRepositoryUrlFromInstalled(
347
+ /** @type {InstalledExtensionListData<ExtType>} */ (data)
348
+ );
349
+ }
350
+ if (data.pkgName && !data.installed) {
351
+ return await this._getRepositoryUrlFromNpm(data.pkgName);
352
+ }
353
+ return null;
354
+ }
355
+
356
+ /**
357
+ * Get repository URL from installed extension's package.json
358
+ *
359
+ * @template {ExtensionType} ExtType
360
+ * @param {InstalledExtensionListData<ExtType>} data
361
+ * @returns {Promise<string|null>}
362
+ * @private
363
+ */
364
+ async _getRepositoryUrlFromInstalled(data) {
365
+ try {
366
+ const pkgJsonPath = path.join(data.installPath, 'package.json');
367
+ if (await fs.exists(pkgJsonPath)) {
368
+ const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
369
+ if (pkg.repository) {
370
+ if (typeof pkg.repository === 'string') {
371
+ return pkg.repository;
372
+ }
373
+ if (pkg.repository.url) {
374
+ return pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
236
375
  }
237
376
  }
238
377
  }
239
-
240
- this.log.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
378
+ } catch {
379
+ // Ignore errors reading package.json
241
380
  }
381
+ return null;
382
+ }
242
383
 
243
- return listData;
384
+ /**
385
+ * Get repository URL from npm for a package name
386
+ *
387
+ * @param {string} pkgName
388
+ * @returns {Promise<string|null>}
389
+ * @private
390
+ */
391
+ async _getRepositoryUrlFromNpm(pkgName) {
392
+ try {
393
+ const repoInfo = await npm.getPackageInfo(pkgName, ['repository']);
394
+ // When requesting only 'repository', npm.getPackageInfo returns the repository object directly
395
+ if (repoInfo) {
396
+ if (typeof repoInfo === 'string') {
397
+ return repoInfo;
398
+ }
399
+ if (repoInfo.url) {
400
+ return repoInfo.url.replace(/^git\+/, '').replace(/\.git$/, '');
401
+ }
402
+ }
403
+ } catch {
404
+ // Ignore errors fetching from npm
405
+ }
406
+ return null;
244
407
  }
245
408
 
246
409
  /**
@@ -438,34 +601,24 @@ class ExtensionCliCommand {
438
601
  * @returns {Promise<ExtInstallReceipt<ExtType>>}
439
602
  */
440
603
  async installViaNpm({installSpec, pkgName, pkgVer, installType}) {
441
- const msg = `Installing '${installSpec}'`;
604
+ const installMsg = `Installing '${installSpec}'`;
605
+ const validateMsg = `Validating '${installSpec}'`;
442
606
 
443
607
  // the string used for installation is either <name>@<ver> in the case of a standard NPM
444
608
  // package, or whatever the user sent in otherwise.
445
609
  const installStr = installType === INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
446
610
  const appiumHome = this.config.appiumHome;
447
611
  try {
448
- const {pkg, installPath} = await spinWith(this.isJsonOutput, msg, async () => {
449
- const {pkg, installPath} = await npm.installPackage(appiumHome, installStr, {
450
- pkgName,
451
- installType,
452
- });
612
+ const {pkg, installPath} = await spinWith(
613
+ this.isJsonOutput,
614
+ installMsg,
615
+ async () => await npm.installPackage(appiumHome, installStr, {pkgName, installType})
616
+ );
617
+
618
+ await spinWith(this.isJsonOutput, validateMsg, async () => {
453
619
  this.validatePackageJson(pkg, installSpec);
454
- return {pkg, installPath};
455
620
  });
456
621
 
457
- /** @type {Promise<void>[]} */
458
- const symlinkInjectionPromises = _.uniq([
459
- ...Object.values(this.config.installedExtensions).map((ext) => ext.installPath),
460
- installPath,
461
- ]).map((instPath) => injectAppiumSymlink.bind(this)(path.join(instPath, 'node_modules')));
462
- // After the extension is installed, we try to inject the appium module symlink
463
- // into the extension's node_modules folder if it is not there yet.
464
- // We also inject the symlink into other installed extensions' node_modules folders
465
- // as these might be cleaned up unexpectedly by npm
466
- // (see https://github.com/appium/python-client/pull/1177#issuecomment-3419826643).
467
- await Promise.all(symlinkInjectionPromises);
468
-
469
622
  return this.getInstallationReceipt({
470
623
  pkg,
471
624
  installPath,
@@ -971,11 +1124,35 @@ class ExtensionCliCommand {
971
1124
  * This is needed to ensure proper module resolution for installed extensions,
972
1125
  * especially ESM ones.
973
1126
  *
974
- * @this {ExtensionCliCommand}
1127
+ * @param {ExtensionConfig<ExtensionType>} driverConfig
1128
+ * @param {ExtensionConfig<ExtensionType>} pluginConfig
1129
+ * @param {import('@appium/types').AppiumLogger} logger
1130
+ */
1131
+ export async function injectAppiumSymlinks(driverConfig, pluginConfig, logger) {
1132
+ const installPaths = _.compact([
1133
+ ...Object.values(driverConfig.installedExtensions || {}),
1134
+ ...Object.values(pluginConfig.installedExtensions || {})
1135
+ ].filter((details) => details.installType === INSTALL_TYPE_NPM)
1136
+ .map((details) => details.installPath));
1137
+ // After the extension is installed, we try to inject the appium module symlink
1138
+ // into the extension's node_modules folder if it is not there yet.
1139
+ // We also inject the symlink into other installed extensions' node_modules folders
1140
+ // as these might be cleaned up unexpectedly by npm
1141
+ // (see https://github.com/appium/python-client/pull/1177#issuecomment-3419826643).
1142
+ await Promise.all(
1143
+ installPaths.map((installPath) => injectAppiumSymlink(path.join(installPath, 'node_modules'), logger))
1144
+ );
1145
+ }
1146
+
1147
+ /**
1148
+ * This is needed to ensure proper module resolution for installed extensions,
1149
+ * especially ESM ones.
1150
+ *
975
1151
  * @param {string} dstFolder The destination folder where the symlink should be created
1152
+ * @param {import('@appium/types').AppiumLogger} logger
976
1153
  * @returns {Promise<void>}
977
1154
  */
978
- async function injectAppiumSymlink(dstFolder) {
1155
+ async function injectAppiumSymlink(dstFolder, logger) {
979
1156
  let appiumModuleRoot;
980
1157
  try {
981
1158
  appiumModuleRoot = getAppiumModuleRoot();
@@ -985,7 +1162,7 @@ async function injectAppiumSymlink(dstFolder) {
985
1162
  }
986
1163
  } catch (error) {
987
1164
  // This error is not fatal, we may still doing just fine if the module being loaded is a CJS one
988
- this.log.info(
1165
+ logger.info(
989
1166
  `Cannot create a symlink to the appium module '${appiumModuleRoot}' in '${dstFolder}'. ` +
990
1167
  `Original error: ${error.message}`
991
1168
  );
@@ -1013,6 +1190,7 @@ export {ExtensionCliCommand as ExtensionCommand};
1013
1190
  * @property {string|null} unsafeUpdateVersion - Same as above, but a major version bump
1014
1191
  * @property {string} [updateError] - Update check error message (if present)
1015
1192
  * @property {boolean} [devMode] - If Appium is run from an extension's working copy
1193
+ * @property {string} [repositoryUrl] - Repository URL for the extension (if available)
1016
1194
  */
1017
1195
 
1018
1196
  /**
package/lib/cli/parser.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {fs} from '@appium/support';
2
2
  import {ArgumentParser} from 'argparse';
3
3
  import _ from 'lodash';
4
- import path from 'path';
4
+ import path from 'node:path';
5
5
  import {
6
6
  DRIVER_TYPE,
7
7
  EXT_SUBCOMMAND_DOCTOR,
@@ -14,7 +14,7 @@ import {
14
14
  SERVER_SUBCOMMAND,
15
15
  SETUP_SUBCOMMAND
16
16
  } from '../constants';
17
- import {finalizeSchema, getArgSpec, hasArgSpec} from '../schema';
17
+ import {finalizeSchema, getAllArgSpecs, getArgSpec, hasArgSpec} from '../schema';
18
18
  import {rootDir} from '../config';
19
19
  import {getExtensionArgs, getServerArgs} from './args';
20
20
  import {
@@ -158,6 +158,25 @@ class ArgParser {
158
158
  }
159
159
  }
160
160
 
161
+ /**
162
+ * Normalize hyphenated server arg keys (e.g. "log-level") to dest form (e.g. "loglevel").
163
+ * Use when server args come from programmatic init rather than the CLI, so they match
164
+ * the shape produced by parseArgs() / _transformParsedArgs().
165
+ * Mutates the given object.
166
+ *
167
+ * @param {object} obj - Object that may contain server args with schema property names
168
+ * @returns {object} The same object with keys normalized
169
+ */
170
+ static normalizeServerArgs(obj) {
171
+ for (const spec of getAllArgSpecs().values()) {
172
+ if (!spec.extType && obj[spec.name] !== undefined && spec.rawDest !== spec.name) {
173
+ obj[spec.rawDest] = obj[spec.name] ?? obj[spec.rawDest];
174
+ delete obj[spec.name];
175
+ }
176
+ }
177
+ return obj;
178
+ }
179
+
161
180
  /**
162
181
  * Given an object full of arguments as returned by `argparser.parse_args`,
163
182
  * expand the ones for extensions into a nested object structure and rename
package/lib/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- import path from 'path';
1
+ import path from 'node:path';
2
2
 
3
3
  /**
4
4
  * The name of the extension type for drivers
@@ -1,7 +1,7 @@
1
1
  import {util, fs, system} from '@appium/support';
2
2
  import B from 'bluebird';
3
3
  import _ from 'lodash';
4
- import path from 'path';
4
+ import path from 'node:path';
5
5
  import resolveFrom from 'resolve-from';
6
6
  import {satisfies} from 'semver';
7
7
  import {commandClasses} from '../cli/extension';
@@ -12,7 +12,7 @@ import {
12
12
  isAllowedSchemaFileExtension,
13
13
  registerSchema,
14
14
  } from '../schema/schema';
15
- import { pathToFileURL } from 'url';
15
+ import { pathToFileURL } from 'node:url';
16
16
 
17
17
  const DEFAULT_ENTRY_POINT = 'index.js';
18
18
  /**
@@ -5,7 +5,7 @@
5
5
  import B from 'bluebird';
6
6
  import {env, fs} from '@appium/support';
7
7
  import _ from 'lodash';
8
- import path from 'path';
8
+ import path from 'node:path';
9
9
  import * as YAML from 'yaml';
10
10
  import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
11
11
  import {INSTALL_TYPE_NPM, INSTALL_TYPE_DEV} from './extension-config';
@@ -1,6 +1,6 @@
1
1
  import {fs} from '@appium/support';
2
2
  import {isPackageChanged} from 'package-changed';
3
- import path from 'path';
3
+ import path from 'node:path';
4
4
  import {PKG_HASHFILE_RELATIVE_PATH} from '../constants';
5
5
  import log from '../logger';
6
6
 
@@ -19,7 +19,7 @@ import type {
19
19
  BiDiCommandNamesToInfosMap,
20
20
  ExecuteMethodMap,
21
21
  } from '@appium/types';
22
- import type { AppiumDriver } from './appium';
22
+ import type {AppiumDriver} from './appium';
23
23
 
24
24
 
25
25
  export async function listCommands(this: AppiumDriver, sessionId?: string): Promise<ListCommandsResponse> {