appium 2.0.0-beta.45 → 2.0.0-beta.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +145 -44
  2. package/build/lib/appium.d.ts +3 -103
  3. package/build/lib/appium.d.ts.map +1 -1
  4. package/build/lib/appium.js +679 -549
  5. package/build/lib/appium.js.map +1 -1
  6. package/build/lib/cli/args.js +247 -127
  7. package/build/lib/cli/args.js.map +1 -1
  8. package/build/lib/cli/driver-command.js +63 -88
  9. package/build/lib/cli/driver-command.js.map +1 -1
  10. package/build/lib/cli/extension-command.d.ts +32 -23
  11. package/build/lib/cli/extension-command.d.ts.map +1 -1
  12. package/build/lib/cli/extension-command.js +730 -512
  13. package/build/lib/cli/extension-command.js.map +1 -1
  14. package/build/lib/cli/extension.d.ts +5 -4
  15. package/build/lib/cli/extension.d.ts.map +1 -1
  16. package/build/lib/cli/extension.js +68 -65
  17. package/build/lib/cli/extension.js.map +1 -1
  18. package/build/lib/cli/parser.d.ts +3 -3
  19. package/build/lib/cli/parser.d.ts.map +1 -1
  20. package/build/lib/cli/parser.js +234 -192
  21. package/build/lib/cli/parser.js.map +1 -1
  22. package/build/lib/cli/plugin-command.js +58 -87
  23. package/build/lib/cli/plugin-command.js.map +1 -1
  24. package/build/lib/cli/utils.js +66 -69
  25. package/build/lib/cli/utils.js.map +1 -1
  26. package/build/lib/config-file.d.ts.map +1 -1
  27. package/build/lib/config-file.js +189 -120
  28. package/build/lib/config-file.js.map +1 -1
  29. package/build/lib/config.d.ts.map +1 -1
  30. package/build/lib/config.js +254 -213
  31. package/build/lib/config.js.map +1 -1
  32. package/build/lib/constants.d.ts +5 -5
  33. package/build/lib/constants.d.ts.map +1 -1
  34. package/build/lib/constants.js +64 -59
  35. package/build/lib/constants.js.map +1 -1
  36. package/build/lib/extension/driver-config.js +199 -164
  37. package/build/lib/extension/driver-config.js.map +1 -1
  38. package/build/lib/extension/extension-config.d.ts +18 -16
  39. package/build/lib/extension/extension-config.d.ts.map +1 -1
  40. package/build/lib/extension/extension-config.js +523 -396
  41. package/build/lib/extension/extension-config.js.map +1 -1
  42. package/build/lib/extension/index.js +98 -68
  43. package/build/lib/extension/index.js.map +1 -1
  44. package/build/lib/extension/manifest-migrations.d.ts +27 -0
  45. package/build/lib/extension/manifest-migrations.d.ts.map +1 -0
  46. package/build/lib/extension/manifest-migrations.js +99 -0
  47. package/build/lib/extension/manifest-migrations.js.map +1 -0
  48. package/build/lib/extension/manifest.d.ts +7 -56
  49. package/build/lib/extension/manifest.d.ts.map +1 -1
  50. package/build/lib/extension/manifest.js +432 -240
  51. package/build/lib/extension/manifest.js.map +1 -1
  52. package/build/lib/extension/package-changed.js +57 -61
  53. package/build/lib/extension/package-changed.js.map +1 -1
  54. package/build/lib/extension/plugin-config.d.ts +2 -3
  55. package/build/lib/extension/plugin-config.d.ts.map +1 -1
  56. package/build/lib/extension/plugin-config.js +94 -70
  57. package/build/lib/extension/plugin-config.js.map +1 -1
  58. package/build/lib/grid-register.js +119 -137
  59. package/build/lib/grid-register.js.map +1 -1
  60. package/build/lib/logger.d.ts +1 -1
  61. package/build/lib/logger.d.ts.map +1 -1
  62. package/build/lib/logger.js +5 -15
  63. package/build/lib/logger.js.map +1 -1
  64. package/build/lib/logsink.d.ts.map +1 -1
  65. package/build/lib/logsink.js +189 -183
  66. package/build/lib/logsink.js.map +1 -1
  67. package/build/lib/main.d.ts +19 -12
  68. package/build/lib/main.d.ts.map +1 -1
  69. package/build/lib/main.js +330 -304
  70. package/build/lib/main.js.map +1 -1
  71. package/build/lib/schema/arg-spec.js +153 -108
  72. package/build/lib/schema/arg-spec.js.map +1 -1
  73. package/build/lib/schema/cli-args.js +203 -164
  74. package/build/lib/schema/cli-args.js.map +1 -1
  75. package/build/lib/schema/cli-transformers.js +117 -72
  76. package/build/lib/schema/cli-transformers.js.map +1 -1
  77. package/build/lib/schema/index.js +17 -32
  78. package/build/lib/schema/index.js.map +1 -1
  79. package/build/lib/schema/keywords.js +125 -67
  80. package/build/lib/schema/keywords.js.map +1 -1
  81. package/build/lib/schema/schema.d.ts.map +1 -1
  82. package/build/lib/schema/schema.js +582 -417
  83. package/build/lib/schema/schema.js.map +1 -1
  84. package/build/lib/utils.d.ts +41 -255
  85. package/build/lib/utils.d.ts.map +1 -1
  86. package/build/lib/utils.js +342 -193
  87. package/build/lib/utils.js.map +1 -1
  88. package/build/tsconfig.tsbuildinfo +1 -1
  89. package/build/types/cli.d.ts +45 -34
  90. package/build/types/cli.d.ts.map +1 -1
  91. package/build/types/cli.js +3 -0
  92. package/build/types/cli.js.map +1 -0
  93. package/build/types/index.d.ts +1 -2
  94. package/build/types/index.d.ts.map +1 -1
  95. package/build/types/index.js +19 -0
  96. package/build/types/index.js.map +1 -0
  97. package/build/types/manifest/base.d.ts +135 -0
  98. package/build/types/manifest/base.d.ts.map +1 -0
  99. package/build/types/manifest/base.js +3 -0
  100. package/build/types/manifest/base.js.map +1 -0
  101. package/build/types/manifest/index.d.ts +19 -0
  102. package/build/types/manifest/index.d.ts.map +1 -0
  103. package/build/types/manifest/index.js +40 -0
  104. package/build/types/manifest/index.js.map +1 -0
  105. package/build/types/manifest/v3.d.ts +139 -0
  106. package/build/types/manifest/v3.d.ts.map +1 -0
  107. package/build/types/manifest/v3.js +3 -0
  108. package/build/types/manifest/v3.js.map +1 -0
  109. package/lib/appium.js +1 -1
  110. package/lib/cli/args.js +1 -1
  111. package/lib/cli/extension-command.js +116 -61
  112. package/lib/cli/extension.js +9 -8
  113. package/lib/cli/parser.js +2 -2
  114. package/lib/config-file.js +2 -3
  115. package/lib/config.js +3 -2
  116. package/lib/constants.js +6 -5
  117. package/lib/extension/extension-config.js +24 -25
  118. package/lib/extension/manifest-migrations.js +99 -0
  119. package/lib/extension/manifest.js +79 -72
  120. package/lib/extension/plugin-config.js +1 -2
  121. package/lib/logsink.js +26 -5
  122. package/lib/main.js +58 -50
  123. package/lib/schema/schema.js +6 -1
  124. package/lib/utils.js +62 -0
  125. package/package.json +23 -24
  126. package/scripts/autoinstall-extensions.js +78 -26
  127. package/types/cli.ts +81 -42
  128. package/types/index.ts +1 -2
  129. package/types/manifest/README.md +30 -0
  130. package/types/manifest/base.ts +158 -0
  131. package/types/manifest/index.ts +27 -0
  132. package/types/manifest/v3.ts +161 -0
  133. package/build/types/appium-manifest.d.ts +0 -59
  134. package/build/types/appium-manifest.d.ts.map +0 -1
  135. package/build/types/extension-manifest.d.ts +0 -55
  136. package/build/types/extension-manifest.d.ts.map +0 -1
  137. package/types/appium-manifest.ts +0 -73
  138. package/types/extension-manifest.ts +0 -64
@@ -18,6 +18,16 @@ const UPDATE_ALL = 'installed';
18
18
  class NotUpdatableError extends Error {}
19
19
  class NoUpdatesAvailableError extends Error {}
20
20
 
21
+ /**
22
+ * Omits `driverName`/`pluginName` props from the receipt to make a {@linkcode ExtManifest}
23
+ * @template {ExtensionType} ExtType
24
+ * @param {ExtInstallReceipt<ExtType>} receipt
25
+ * @returns {ExtManifest<ExtType>}
26
+ */
27
+ function receiptToManifest(receipt) {
28
+ return /** @type {ExtManifest<ExtType>} */ (_.omit(receipt, 'driverName', 'pluginName'));
29
+ }
30
+
21
31
  /**
22
32
  * @template {ExtensionType} ExtType
23
33
  */
@@ -128,10 +138,14 @@ class ExtensionCommand {
128
138
  // also don't need to check for updates on non-npm exts
129
139
  continue;
130
140
  }
131
- const updates = await this.checkForExtensionUpdate(ext);
132
- data.updateVersion = updates.safeUpdate;
133
- data.unsafeUpdateVersion = updates.unsafeUpdate;
134
- data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
141
+ try {
142
+ const updates = await this.checkForExtensionUpdate(ext);
143
+ data.updateVersion = updates.safeUpdate;
144
+ data.unsafeUpdateVersion = updates.unsafeUpdate;
145
+ data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
146
+ } catch (e) {
147
+ data.updateError = e.message;
148
+ }
135
149
  }
136
150
  });
137
151
 
@@ -149,8 +163,15 @@ class ExtensionCommand {
149
163
  let upToDateTxt = '';
150
164
  let unsafeUpdateTxt = '';
151
165
  if (data.installed) {
152
- const {installType, installSpec, updateVersion, unsafeUpdateVersion, version, upToDate} =
153
- data;
166
+ const {
167
+ installType,
168
+ installSpec,
169
+ updateVersion,
170
+ unsafeUpdateVersion,
171
+ version,
172
+ upToDate,
173
+ updateError,
174
+ } = data;
154
175
  let typeTxt;
155
176
  switch (installType) {
156
177
  case INSTALL_TYPE_GIT:
@@ -166,14 +187,18 @@ class ExtensionCommand {
166
187
  installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
167
188
 
168
189
  if (showUpdates) {
169
- if (updateVersion) {
170
- updateTxt = ` [${updateVersion} available]`.magenta;
171
- }
172
- if (upToDate) {
173
- upToDateTxt = ` [Up to date]`.green;
174
- }
175
- if (unsafeUpdateVersion) {
176
- unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
190
+ if (updateError) {
191
+ updateTxt = ` [Cannot check for updates: ${updateError}]`.red;
192
+ } else {
193
+ if (updateVersion) {
194
+ updateTxt = ` [${updateVersion} available]`.magenta;
195
+ }
196
+ if (upToDate) {
197
+ upToDateTxt = ` [Up to date]`.green;
198
+ }
199
+ if (unsafeUpdateVersion) {
200
+ unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
201
+ }
177
202
  }
178
203
  }
179
204
  }
@@ -187,12 +212,12 @@ class ExtensionCommand {
187
212
  /**
188
213
  * Install an extension
189
214
  *
190
- * @param {InstallArgs} args
215
+ * @param {InstallOpts} opts
191
216
  * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
192
217
  */
193
218
  async _install({installSpec, installType, packageName}) {
194
- /** @type {ExtensionFields<ExtType>} */
195
- let extData;
219
+ /** @type {ExtInstallReceipt<ExtType>} */
220
+ let receipt;
196
221
 
197
222
  if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
198
223
  throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
@@ -205,7 +230,7 @@ class ExtensionCommand {
205
230
  /**
206
231
  * @type {InstallViaNpmArgs}
207
232
  */
208
- let installOpts;
233
+ let installViaNpmOpts;
209
234
 
210
235
  /**
211
236
  * The probable (?) name of the extension derived from the install spec.
@@ -223,8 +248,9 @@ class ExtensionCommand {
223
248
  'it should be of the form <org>/<repo>'
224
249
  );
225
250
  }
226
- installOpts = {
251
+ installViaNpmOpts = {
227
252
  installSpec,
253
+ installType,
228
254
  pkgName: /** @type {string} */ (packageName),
229
255
  };
230
256
  probableExtName = installSpec;
@@ -232,8 +258,9 @@ class ExtensionCommand {
232
258
  // git urls can have '.git' at the end, but this is not necessary and would complicate the
233
259
  // way we download and name directories, so we can just remove it
234
260
  installSpec = installSpec.replace(/\.git$/, '');
235
- installOpts = {
261
+ installViaNpmOpts = {
236
262
  installSpec,
263
+ installType,
237
264
  pkgName: /** @type {string} */ (packageName),
238
265
  };
239
266
  probableExtName = installSpec;
@@ -279,7 +306,7 @@ class ExtensionCommand {
279
306
  installType = INSTALL_TYPE_NPM;
280
307
  }
281
308
  }
282
- installOpts = {installSpec, pkgName, pkgVer};
309
+ installViaNpmOpts = {installSpec, pkgName, pkgVer, installType};
283
310
  }
284
311
 
285
312
  // fail fast here if we can
@@ -291,11 +318,11 @@ class ExtensionCommand {
291
318
  );
292
319
  }
293
320
 
294
- extData = await this.installViaNpm(installOpts);
321
+ receipt = await this.installViaNpm(installViaNpmOpts);
295
322
 
296
323
  // this _should_ be the same as `probablyExtName` as the one derived above unless
297
324
  // install type is local.
298
- const extName = extData[/** @type {string} */ (`${this.type}Name`)];
325
+ const extName = receipt[/** @type {string} */ (`${this.type}Name`)];
299
326
 
300
327
  // check _a second time_ with the more-accurate extName
301
328
  if (this.config.isInstalled(extName)) {
@@ -308,10 +335,9 @@ class ExtensionCommand {
308
335
 
309
336
  // this field does not exist as such in the manifest (it's used as a property name instead)
310
337
  // so that's why it's being removed here.
311
- delete extData[/** @type {string} */ (`${this.type}Name`)];
312
-
313
338
  /** @type {ExtManifest<ExtType>} */
314
- const extManifest = {...extData, installType, installSpec};
339
+ const extManifest = receiptToManifest(receipt);
340
+
315
341
  const [errors, warnings] = await B.all([
316
342
  this.config.getProblems(extName, extManifest),
317
343
  this.config.getWarnings(extName, extManifest),
@@ -340,7 +366,7 @@ class ExtensionCommand {
340
366
  }
341
367
 
342
368
  // log info for the user
343
- this.log.info(this.getPostInstallText({extName, extData}));
369
+ this.log.info(this.getPostInstallText({extName, extData: receipt}));
344
370
 
345
371
  return this.config.installedExtensions;
346
372
  }
@@ -349,21 +375,28 @@ class ExtensionCommand {
349
375
  * Install an extension via NPM
350
376
  *
351
377
  * @param {InstallViaNpmArgs} args
378
+ * @returns {Promise<ExtInstallReceipt<ExtType>>}
352
379
  */
353
- async installViaNpm({installSpec, pkgName, pkgVer}) {
380
+ async installViaNpm({installSpec, pkgName, pkgVer, installType}) {
354
381
  const npmSpec = `${pkgName}${pkgVer ? '@' + pkgVer : ''}`;
355
382
  const specMsg = npmSpec === installSpec ? '' : ` using NPM install spec '${npmSpec}'`;
356
383
  const msg = `Installing '${installSpec}'${specMsg}`;
357
384
  try {
358
- const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => {
359
- const pkgJsonData = await npm.installPackage(this.config.appiumHome, pkgName, {
385
+ const {pkg, path} = await spinWith(this.isJsonOutput, msg, async () => {
386
+ const {pkg, installPath: path} = await npm.installPackage(this.config.appiumHome, pkgName, {
360
387
  pkgVer,
388
+ installType,
361
389
  });
362
- this.validatePackageJson(pkgJsonData, installSpec);
363
- return pkgJsonData;
390
+ this.validatePackageJson(pkg, installSpec);
391
+ return {pkg, path};
364
392
  });
365
393
 
366
- return this.getExtensionFields(pkgJsonData);
394
+ return this.getInstallationReceipt({
395
+ pkg,
396
+ installPath: path,
397
+ installType,
398
+ installSpec,
399
+ });
367
400
  } catch (err) {
368
401
  throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
369
402
  }
@@ -382,25 +415,31 @@ class ExtensionCommand {
382
415
  }
383
416
 
384
417
  /**
385
- * Take an NPM module's package.json and extract Appium driver information from a special
386
- * 'appium' field in the JSON data. We need this information to e.g. determine which class to
387
- * load as the main driver class, or to be able to detect incompatibilities between driver and
388
- * appium versions.
418
+ * Once a package is installed on-disk, this gathers some necessary metadata for validation.
389
419
  *
390
- * @param {ExtPackageJson<ExtType>} pkgJson - the package.json data for a driver module, as if it had been straightforwardly 'require'd
391
- * @returns {ExtensionFields<ExtType>}
420
+ * @param {GetInstallationReceiptOpts<ExtType>} opts
421
+ * @returns {ExtInstallReceipt<ExtType>}
392
422
  */
393
- getExtensionFields(pkgJson) {
394
- const {appium, name, version, peerDependencies} = pkgJson;
423
+ getInstallationReceipt({pkg, installPath, installType, installSpec}) {
424
+ const {appium, name, version, peerDependencies} = pkg;
395
425
 
396
- /** @type {unknown} */
397
- const result = {
398
- ...appium,
426
+ /** @type {import('appium/types').InternalMetadata} */
427
+ const internal = {
399
428
  pkgName: name,
400
429
  version,
430
+ installType,
431
+ installSpec,
432
+ installPath,
401
433
  appiumVersion: peerDependencies?.appium,
402
434
  };
403
- return /** @type {ExtensionFields<ExtType>} */ (result);
435
+
436
+ /** @type {ExtMetadata<ExtType>} */
437
+ const extMetadata = appium;
438
+
439
+ return {
440
+ ...internal,
441
+ ...extMetadata,
442
+ };
404
443
  }
405
444
 
406
445
  /**
@@ -410,13 +449,13 @@ class ExtensionCommand {
410
449
  * - `name`
411
450
  * - `version`
412
451
  * - `appium`
413
- * @param {import('type-fest').PackageJson} pkgJson - `package.json` of extension
452
+ * @param {import('type-fest').PackageJson} pkg - `package.json` of extension
414
453
  * @param {string} installSpec - Extension name/spec
415
454
  * @throws {ReferenceError} If `package.json` has a missing or invalid field
416
- * @returns {pkgJson is ExtPackageJson<ExtType>}
455
+ * @returns {pkg is ExtPackageJson<ExtType>}
417
456
  */
418
- validatePackageJson(pkgJson, installSpec) {
419
- const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkgJson);
457
+ validatePackageJson(pkg, installSpec) {
458
+ const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkg);
420
459
 
421
460
  /**
422
461
  *
@@ -608,9 +647,10 @@ class ExtensionCommand {
608
647
  * @returns {Promise<void>}
609
648
  */
610
649
  async updateExtension(installSpec, version) {
611
- const {pkgName} = this.config.installedExtensions[installSpec];
650
+ const {pkgName, installType} = this.config.installedExtensions[installSpec];
612
651
  const extData = await this.installViaNpm({
613
652
  installSpec,
653
+ installType,
614
654
  pkgName,
615
655
  pkgVer: version,
616
656
  });
@@ -637,7 +677,7 @@ class ExtensionCommand {
637
677
  const extConfig = this.config.installedExtensions[installSpec];
638
678
 
639
679
  // note: TS cannot understand that _.has() is a type guard
640
- if (!extConfig.scripts) {
680
+ if (!('scripts' in extConfig)) {
641
681
  throw this._createFatalError(
642
682
  `The ${this.type} named '${installSpec}' does not contain the ` +
643
683
  `"scripts" field underneath the "appium" field in its package.json`
@@ -646,13 +686,13 @@ class ExtensionCommand {
646
686
 
647
687
  const extScripts = extConfig.scripts;
648
688
 
649
- if (!_.isPlainObject(extScripts)) {
689
+ if (!extScripts || !_.isPlainObject(extScripts)) {
650
690
  throw this._createFatalError(
651
691
  `The ${this.type} named '${installSpec}' "scripts" field must be a plain object`
652
692
  );
653
693
  }
654
694
 
655
- if (!_.has(extScripts, scriptName)) {
695
+ if (!(scriptName in extScripts)) {
656
696
  throw this._createFatalError(
657
697
  `The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
658
698
  );
@@ -701,6 +741,7 @@ export {ExtensionCommand};
701
741
  * @property {string?} updateVersion - If the extension is installed, the version it can be updated to
702
742
  * @property {string?} unsafeUpdateVersion - Same as above, but a major version bump
703
743
  * @property {boolean} upToDate - If the extension is installed and the latest
744
+ * @property {string?} updateError - Update check error message (if present)
704
745
  */
705
746
 
706
747
  /**
@@ -734,6 +775,11 @@ export {ExtensionCommand};
734
775
  * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
735
776
  */
736
777
 
778
+ /**
779
+ * @template {ExtensionType} ExtType
780
+ * @typedef {import('appium/types').ExtInstallReceipt<ExtType>} ExtInstallReceipt
781
+ */
782
+
737
783
  /**
738
784
  * Possible return value for {@linkcode ExtensionCommand.list}
739
785
  * @typedef UninstalledExtensionListData
@@ -806,6 +852,7 @@ export {ExtensionCommand};
806
852
  * @typedef InstallViaNpmArgs
807
853
  * @property {string} installSpec - the name or spec of an extension to install
808
854
  * @property {string} pkgName - the NPM package name of the extension
855
+ * @property {import('appium/types').InstallType} installType - type of install
809
856
  * @property {string} [pkgVer] - the specific version of the NPM package
810
857
  */
811
858
 
@@ -819,18 +866,12 @@ export {ExtensionCommand};
819
866
 
820
867
  /**
821
868
  * Options for {@linkcode ExtensionCommand._install}
822
- * @typedef InstallArgs
869
+ * @typedef InstallOpts
823
870
  * @property {string} installSpec - the name or spec of an extension to install
824
- * @property {import('appium/types').InstallType} installType - how to install this extension. One of the INSTALL_TYPES
871
+ * @property {InstallType} installType - how to install this extension. One of the INSTALL_TYPES
825
872
  * @property {string} [packageName] - for git/github installs, the extension node package name
826
873
  */
827
874
 
828
- /**
829
- * Returned by {@linkcode ExtensionCommand.getExtensionFields}
830
- * @template {ExtensionType} ExtType
831
- * @typedef {ExtMetadata<ExtType> & { pkgName: string, version: string, appiumVersion: string } & import('appium/types').CommonExtMetadata} ExtensionFields
832
- */
833
-
834
875
  /**
835
876
  * @template {ExtensionType} ExtType
836
877
  * @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
@@ -841,3 +882,17 @@ export {ExtensionCommand};
841
882
  * @property {boolean} showInstalled - whether should show only installed extensions
842
883
  * @property {boolean} showUpdates - whether should show available updates
843
884
  */
885
+
886
+ /**
887
+ * Opts for {@linkcode ExtensionCommand.getInstallationReceipt}
888
+ * @template {ExtensionType} ExtType
889
+ * @typedef GetInstallationReceiptOpts
890
+ * @property {string} installPath
891
+ * @property {string} installSpec
892
+ * @property {ExtPackageJson<ExtType>} pkg
893
+ * @property {InstallType} installType
894
+ */
895
+
896
+ /**
897
+ * @typedef {import('appium/types').InstallType} InstallType
898
+ */
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable no-console */
2
+ import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
3
+ import {isExtensionCommandArgs} from '../utils';
2
4
  import DriverCommand from './driver-command';
3
5
  import PluginCommand from './plugin-command';
4
- import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
5
6
  import {errAndQuit, JSON_SPACES} from './utils';
6
7
 
7
8
  export const commandClasses = Object.freeze(
@@ -15,18 +16,18 @@ export const commandClasses = Object.freeze(
15
16
  * Run a subcommand of the 'appium driver' type. Each subcommand has its own set of arguments which
16
17
  * can be represented as a JS object.
17
18
  *
18
- * @param {import('appium/types').Args<import('appium/types').WithExtSubcommand>} args - JS object where the key is the parameter name (as defined in
19
+ * @template {import('appium/types').CliExtensionCommand} Cmd
20
+ * @template {import('appium/types').CliExtensionSubcommand} SubCmd
21
+ * @param {import('appium/types').Args<Cmd, SubCmd>} args - JS object where the key is the parameter name (as defined in
19
22
  * driver-parser.js)
20
- * @template {ExtensionType} ExtType
21
- * @param {import('../extension/extension-config').ExtensionConfig<ExtType>} config - Extension config object
23
+ * @param {import('../extension/extension-config').ExtensionConfig<Cmd>} config - Extension config object
22
24
  */
23
25
  async function runExtensionCommand(args, config) {
24
26
  // TODO driver config file should be locked while any of these commands are
25
27
  // running to prevent weird situations
26
28
  let jsonResult = null;
27
- const {extensionType: type} = config;
28
- const extCmd = args[`${type}Command`];
29
- if (!extCmd) {
29
+ const {extensionType: type} = config; // NOTE this is the same as `args.subcommand`
30
+ if (!isExtensionCommandArgs(args)) {
30
31
  throw new TypeError(`Cannot call ${type} command without a subcommand like 'install'`);
31
32
  }
32
33
  let {json, suppressOutput} = args;
@@ -34,7 +35,7 @@ async function runExtensionCommand(args, config) {
34
35
  if (suppressOutput) {
35
36
  json = true;
36
37
  }
37
- const CommandClass = /** @type {ExtCommand<ExtType>} */ (commandClasses[type]);
38
+ const CommandClass = /** @type {ExtCommand<Cmd>} */ (commandClasses[type]);
38
39
  const cmd = new CommandClass({config, json});
39
40
  try {
40
41
  jsonResult = await cmd.execute(args);
package/lib/cli/parser.js CHANGED
@@ -89,9 +89,9 @@ class ArgParser {
89
89
  * If no subcommand is passed in, this method will inject the `server` subcommand.
90
90
  *
91
91
  * `ArgParser.prototype.parse_args` is an alias of this method.
92
- * @template [T=import('appium/types').WithServerSubcommand]
92
+ * @template {import('appium/types').CliCommand} [Cmd=import('appium/types').ServerCommand]
93
93
  * @param {string[]} [args] - Array of arguments, ostensibly from `process.argv`. Gathers args from `process.argv` if not provided.
94
- * @returns {import('appium/types').Args<T>} - The parsed arguments
94
+ * @returns {import('appium/types').Args<Cmd>} - The parsed arguments
95
95
  */
96
96
  parseArgs(args = process.argv.slice(2)) {
97
97
  if (!NON_SERVER_ARGS.has(args[0])) {
@@ -154,9 +154,8 @@ export function normalizeConfig(config) {
154
154
  const normalize = (config, section) => {
155
155
  const obj = _.isUndefined(section) ? config : _.get(config, section, config);
156
156
 
157
- const mappedObj = _.mapKeys(
158
- obj,
159
- (__, prop) => schema.properties[prop]?.appiumCliDest ?? _.camelCase(prop)
157
+ const mappedObj = _.mapKeys(obj, (__, prop) =>
158
+ _.get(schema, `properties.server.properties[${prop}].appiumCliDest`, _.camelCase(prop))
160
159
  );
161
160
 
162
161
  return _.mapValues(mappedObj, (value, property) => {
package/lib/config.js CHANGED
@@ -298,11 +298,12 @@ function showConfig(nonDefaultPreConfigParsedArgs, configResult, defaults, parse
298
298
  } else {
299
299
  console.log(`\n(no configuration file loaded)`);
300
300
  }
301
- if (_.isEmpty(nonDefaultPreConfigParsedArgs)) {
301
+ const compactedNonDefaultPreConfigArgs = compactConfig(nonDefaultPreConfigParsedArgs);
302
+ if (_.isEmpty(compactedNonDefaultPreConfigArgs)) {
302
303
  console.log(`\n(no CLI parameters provided)`);
303
304
  } else {
304
305
  console.log('\nvia CLI or function call:\n');
305
- console.dir(compactConfig(nonDefaultPreConfigParsedArgs));
306
+ console.dir(compactedNonDefaultPreConfigArgs);
306
307
  }
307
308
  console.log('\nfinal configuration:\n');
308
309
  console.dir(compactConfig(parsedArgs));
package/lib/constants.js CHANGED
@@ -29,6 +29,7 @@ export const KNOWN_PLUGINS = Object.freeze(
29
29
  images: '@appium/images-plugin',
30
30
  'execute-driver': '@appium/execute-driver-plugin',
31
31
  'relaxed-caps': '@appium/relaxed-caps-plugin',
32
+ 'universal-xml': '@appium/universal-xml-plugin',
32
33
  })
33
34
  );
34
35
 
@@ -40,13 +41,8 @@ export const KNOWN_DRIVERS = Object.freeze(
40
41
  /** @type {const} */ ({
41
42
  uiautomator2: 'appium-uiautomator2-driver',
42
43
  xcuitest: 'appium-xcuitest-driver',
43
- youiengine: 'appium-youiengine-driver',
44
- windows: 'appium-windows-driver',
45
- mac: 'appium-mac-driver',
46
44
  mac2: 'appium-mac2-driver',
47
45
  espresso: 'appium-espresso-driver',
48
- tizen: 'appium-tizen-driver',
49
- flutter: 'appium-flutter-driver',
50
46
  safari: 'appium-safari-driver',
51
47
  gecko: 'appium-geckodriver',
52
48
  })
@@ -67,3 +63,8 @@ export const EXT_SUBCOMMAND_INSTALL = 'install';
67
63
  export const EXT_SUBCOMMAND_UNINSTALL = 'uninstall';
68
64
  export const EXT_SUBCOMMAND_UPDATE = 'update';
69
65
  export const EXT_SUBCOMMAND_RUN = 'run';
66
+
67
+ /**
68
+ * Current revision of the manifest (`extensions.yaml`) schema
69
+ */
70
+ export const CURRENT_SCHEMA_REV = 3;
@@ -441,7 +441,7 @@ export class ExtensionConfig {
441
441
 
442
442
  /**
443
443
  * @param {ExtName<ExtType>} extName
444
- * @param {ExtManifest<ExtType>|import('../cli/extension-command').ExtensionFields<ExtType>} extManifest
444
+ * @param {ExtManifest<ExtType>} extManifest
445
445
  * @param {ExtensionConfigMutationOpts} [opts]
446
446
  * @returns {Promise<void>}
447
447
  */
@@ -504,11 +504,17 @@ export class ExtensionConfig {
504
504
  }
505
505
 
506
506
  /**
507
- * @param {string} extName
507
+ * Returns--with reasonable accuracy--the path on disk to the extension.
508
+ *
509
+ * If `installPath` is present in the manifest, then it is used; otherwise we just guess.
510
+ * @param {keyof typeof this.installedExtensions} extName
508
511
  * @returns {string}
509
512
  */
510
513
  getInstallPath(extName) {
511
- return path.join(this.appiumHome, 'node_modules', this.installedExtensions[extName].pkgName);
514
+ return (
515
+ this.installedExtensions[extName]?.installPath ??
516
+ path.join(this.appiumHome, 'node_modules', this.installedExtensions[extName].pkgName)
517
+ );
512
518
  }
513
519
 
514
520
  /**
@@ -628,36 +634,39 @@ export {INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GIT
628
634
  /**
629
635
  * @typedef {import('@appium/types').ExtensionType} ExtensionType
630
636
  * @typedef {import('./manifest').Manifest} Manifest
637
+ * @typedef {import('../cli/extension-command').ExtensionListData} ExtensionListData
638
+ * @typedef {import('../cli/extension-command').InstalledExtensionListData} InstalledExtensionListData
639
+ * @typedef {import('appium/types').InstallType} InstallType
631
640
  */
632
641
 
633
642
  /**
634
- * @template T
635
- * @typedef {import('appium/types').ExtManifest<T>} ExtManifest
643
+ * @template {ExtensionType} ExtType
644
+ * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
636
645
  */
637
646
 
638
647
  /**
639
- * @template T
640
- * @typedef {import('appium/types').ExtManifestWithSchema<T>} ExtManifestWithSchema
648
+ * @template {ExtensionType} ExtType
649
+ * @typedef {ExtManifest<ExtType> & {schema: NonNullable<ExtManifest<ExtType>['schema']>}} ExtManifestWithSchema
641
650
  */
642
651
 
643
652
  /**
644
- * @template T
645
- * @typedef {import('appium/types').ExtName<T>} ExtName
653
+ * @template {ExtensionType} ExtType
654
+ * @typedef {import('appium/types').ExtName<ExtType>} ExtName
646
655
  */
647
656
 
648
657
  /**
649
- * @template T
650
- * @typedef {import('appium/types').ExtClass<T>} ExtClass
658
+ * @template {ExtensionType} ExtType
659
+ * @typedef {import('appium/types').ExtClass<ExtType>} ExtClass
651
660
  */
652
661
 
653
662
  /**
654
- * @template T
655
- * @typedef {import('appium/types').ExtRecord<T>} ExtRecord
663
+ * @template {ExtensionType} ExtType
664
+ * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
656
665
  */
657
666
 
658
667
  /**
659
- * @template T
660
- * @typedef {import('../cli/extension').ExtCommand<T>} ExtCommand
668
+ * @template {ExtensionType} ExtType
669
+ * @typedef {import('../cli/extension').ExtCommand<ExtType>} ExtCommand
661
670
  */
662
671
 
663
672
  /**
@@ -665,13 +674,3 @@ export {INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GIT
665
674
  * @typedef ExtensionConfigMutationOpts
666
675
  * @property {boolean} [write=true] Whether or not to write the manifest to disk after a mutation operation
667
676
  */
668
-
669
- /**
670
- * A valid install type
671
- * @typedef {typeof INSTALL_TYPE_NPM | typeof INSTALL_TYPE_GIT | typeof INSTALL_TYPE_LOCAL | typeof INSTALL_TYPE_GITHUB} InstallType
672
- */
673
-
674
- /**
675
- * @typedef {import('../cli/extension-command').ExtensionListData} ExtensionListData
676
- * @typedef {import('../cli/extension-command').InstalledExtensionListData} InstalledExtensionListData
677
- */
@@ -0,0 +1,99 @@
1
+ import log from '../logger';
2
+
3
+ /**
4
+ * This contains logic to migrate old versions of `extensions.yaml` to new versions.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ /**
10
+ * Constant for v3 of the schema rev.
11
+ */
12
+ const SCHEMA_REV_3 = 3;
13
+
14
+ /**
15
+ * Collection of functions to migrate from one version to another
16
+ * @type {{[P in keyof ManifestDataVersions]?: Migration}}
17
+ */
18
+ const Migrations = {
19
+ /**
20
+ * If `installPath` is missing from the `ExtManifest` for any extension, return `true`.
21
+ *
22
+ * We cannot easily determine the path of any given extension here, but returning `true`
23
+ * will cause the `Manifest` to sync with the filesystem, which will populate the missing field.
24
+ *
25
+ * @type {Migration}
26
+ */
27
+ [SCHEMA_REV_3]: (data) => {
28
+ let shouldSync = false;
29
+ const allExtData = [...Object.values(data.drivers), ...Object.values(data.plugins)];
30
+ for (const metadata of allExtData) {
31
+ if (!('installPath' in metadata)) {
32
+ shouldSync = true;
33
+ break;
34
+ }
35
+ }
36
+ return shouldSync;
37
+ },
38
+ };
39
+
40
+ /**
41
+ * Set `schemaRev` to a specific version
42
+ * @param {AnyManifestDataVersion} data
43
+ * @param {keyof ManifestDataVersions} version
44
+ * @returns {boolean} Whether the data was modified
45
+ */
46
+ function setSchemaRev(data, version) {
47
+ if (data.schemaRev ?? 0 < version) {
48
+ data.schemaRev = version;
49
+ return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * Applies a series of migration functions to a manifest to update its manifest schema version.
56
+ *
57
+ * `data` is modified in-place.
58
+ *
59
+ * @param {Manifest} manifest
60
+ * @param {AnyManifestDataVersion} data
61
+ * @returns {Promise<boolean>} If `true` existing packages should be synced from disk and the manifest should be persisted.
62
+ */
63
+ export async function migrate(manifest, data) {
64
+ let didChange = false;
65
+ for await (const [v, migration] of Object.entries(Migrations)) {
66
+ const version = /** @type {keyof ManifestDataVersions} */ (Number(v));
67
+ didChange = (await migration(data, manifest)) || didChange;
68
+ didChange = setSchemaRev(data, version) || didChange;
69
+ }
70
+
71
+ if (didChange) {
72
+ // this is not _technically_ true, since we don't actually write the file here.
73
+ log.info(`Upgraded extension manifest to schema v${data.schemaRev}`);
74
+ }
75
+ return didChange;
76
+ }
77
+
78
+ /**
79
+ * Migration functions take a {@linkcode Manifest} and its data and **mutates `data` in-place**.
80
+ *
81
+ * A migration function should not modify `schemaRev`, as this is done automatically.
82
+ *
83
+ * A migration function can also call out to, say, {@linkcode Manifest.syncWithInstalledExtensions}, which
84
+ * may apply the mutation itself.
85
+ *
86
+ * Note that at the time of writing, we're not able to determine the version of the _current_ manifest file if there is no `schemaRev` prop present (and we may not want to trust it anyway).
87
+ * @callback Migration
88
+ * @param {AnyManifestDataVersion} data - The raw data from `Manifest`
89
+ * @param {Manifest} manifest - The `Manifest` instance
90
+ * @returns {boolean|Promise<boolean>} If `true`, then something changed
91
+ */
92
+
93
+ /**
94
+ * @typedef {import('appium/types').ManifestData} ManifestData
95
+ * @typedef {import('appium/types').AnyManifestDataVersion} AnyManifestDataVersion
96
+ * @typedef {import('appium/types').ManifestDataVersions} ManifestDataVersions
97
+ * @typedef {import('appium/types').ManifestV2.ManifestData} ManifestDataV2
98
+ * @typedef {import('./manifest').Manifest} Manifest
99
+ */