appium 2.0.0-beta.34 → 2.0.0-beta.38

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 (123) hide show
  1. package/build/lib/appium.d.ts +41 -52
  2. package/build/lib/appium.d.ts.map +1 -1
  3. package/build/lib/appium.js +32 -15
  4. package/build/lib/cli/args.d.ts +1 -1
  5. package/build/lib/cli/args.d.ts.map +1 -1
  6. package/build/lib/cli/args.js +1 -1
  7. package/build/lib/cli/driver-command.d.ts +5 -5
  8. package/build/lib/cli/driver-command.d.ts.map +1 -1
  9. package/build/lib/cli/driver-command.js +8 -8
  10. package/build/lib/cli/extension-command.d.ts +78 -51
  11. package/build/lib/cli/extension-command.d.ts.map +1 -1
  12. package/build/lib/cli/extension-command.js +135 -80
  13. package/build/lib/cli/extension.d.ts +9 -5
  14. package/build/lib/cli/extension.d.ts.map +1 -1
  15. package/build/lib/cli/extension.js +5 -7
  16. package/build/lib/cli/parser.d.ts +3 -3
  17. package/build/lib/cli/parser.d.ts.map +1 -1
  18. package/build/lib/cli/parser.js +1 -1
  19. package/build/lib/cli/plugin-command.d.ts +9 -15
  20. package/build/lib/cli/plugin-command.d.ts.map +1 -1
  21. package/build/lib/cli/plugin-command.js +8 -8
  22. package/build/lib/cli/utils.js +1 -1
  23. package/build/lib/config-file.d.ts.map +1 -1
  24. package/build/lib/config-file.js +1 -1
  25. package/build/lib/config.d.ts +4 -4
  26. package/build/lib/config.d.ts.map +1 -1
  27. package/build/lib/config.js +1 -1
  28. package/build/lib/constants.d.ts.map +1 -1
  29. package/build/lib/constants.js +1 -1
  30. package/build/lib/extension/driver-config.d.ts +29 -32
  31. package/build/lib/extension/driver-config.d.ts.map +1 -1
  32. package/build/lib/extension/driver-config.js +7 -20
  33. package/build/lib/extension/extension-config.d.ts +108 -36
  34. package/build/lib/extension/extension-config.d.ts.map +1 -1
  35. package/build/lib/extension/extension-config.js +199 -60
  36. package/build/lib/extension/index.d.ts +16 -7
  37. package/build/lib/extension/index.d.ts.map +1 -1
  38. package/build/lib/extension/index.js +15 -18
  39. package/build/lib/extension/manifest.d.ts +12 -12
  40. package/build/lib/extension/manifest.d.ts.map +1 -1
  41. package/build/lib/extension/manifest.js +13 -3
  42. package/build/lib/extension/package-changed.d.ts.map +1 -1
  43. package/build/lib/extension/package-changed.js +1 -1
  44. package/build/lib/extension/plugin-config.d.ts +19 -24
  45. package/build/lib/extension/plugin-config.d.ts.map +1 -1
  46. package/build/lib/extension/plugin-config.js +9 -18
  47. package/build/lib/grid-register.d.ts.map +1 -1
  48. package/build/lib/grid-register.js +1 -1
  49. package/build/lib/logger.d.ts +1 -1
  50. package/build/lib/logger.d.ts.map +1 -1
  51. package/build/lib/logger.js +1 -1
  52. package/build/lib/logsink.d.ts.map +1 -1
  53. package/build/lib/logsink.js +3 -2
  54. package/build/lib/main.d.ts +13 -12
  55. package/build/lib/main.d.ts.map +1 -1
  56. package/build/lib/main.js +4 -4
  57. package/build/lib/schema/arg-spec.d.ts +4 -4
  58. package/build/lib/schema/arg-spec.d.ts.map +1 -1
  59. package/build/lib/schema/arg-spec.js +1 -1
  60. package/build/lib/schema/cli-args.d.ts.map +1 -1
  61. package/build/lib/schema/cli-args.js +1 -1
  62. package/build/lib/schema/cli-transformers.d.ts.map +1 -1
  63. package/build/lib/schema/cli-transformers.js +1 -1
  64. package/build/lib/schema/keywords.d.ts.map +1 -1
  65. package/build/lib/schema/keywords.js +1 -1
  66. package/build/lib/schema/schema.d.ts +2 -2
  67. package/build/lib/schema/schema.d.ts.map +1 -1
  68. package/build/lib/schema/schema.js +1 -1
  69. package/build/lib/utils.d.ts.map +1 -1
  70. package/build/lib/utils.js +1 -1
  71. package/build/tsconfig.tsbuildinfo +1 -1
  72. package/build/types/appium-manifest.d.ts +23 -4
  73. package/build/types/appium-manifest.d.ts.map +1 -1
  74. package/build/types/cli.d.ts.map +1 -1
  75. package/build/types/{external-manifest.d.ts → extension-manifest.d.ts} +15 -7
  76. package/build/types/extension-manifest.d.ts.map +1 -0
  77. package/build/types/index.d.ts +6 -5
  78. package/build/types/index.d.ts.map +1 -1
  79. package/driver.d.ts +1 -0
  80. package/driver.js +14 -0
  81. package/lib/appium.js +208 -124
  82. package/lib/cli/args.js +143 -93
  83. package/lib/cli/driver-command.js +46 -26
  84. package/lib/cli/extension-command.js +314 -157
  85. package/lib/cli/extension.js +15 -19
  86. package/lib/cli/parser.js +19 -31
  87. package/lib/cli/plugin-command.js +39 -24
  88. package/lib/cli/utils.js +8 -14
  89. package/lib/config-file.js +21 -25
  90. package/lib/config.js +82 -64
  91. package/lib/constants.js +4 -13
  92. package/lib/extension/driver-config.js +171 -171
  93. package/lib/extension/extension-config.js +347 -126
  94. package/lib/extension/index.js +72 -58
  95. package/lib/extension/manifest.js +48 -57
  96. package/lib/extension/package-changed.js +9 -8
  97. package/lib/extension/plugin-config.js +62 -62
  98. package/lib/grid-register.js +29 -18
  99. package/lib/logger.js +1 -2
  100. package/lib/logsink.js +29 -31
  101. package/lib/main.js +111 -73
  102. package/lib/schema/arg-spec.js +10 -13
  103. package/lib/schema/cli-args.js +14 -37
  104. package/lib/schema/cli-transformers.js +7 -14
  105. package/lib/schema/keywords.js +15 -13
  106. package/lib/schema/schema.js +58 -75
  107. package/lib/utils.js +50 -25
  108. package/package.json +25 -18
  109. package/plugin.d.ts +1 -0
  110. package/plugin.js +13 -0
  111. package/scripts/autoinstall-extensions.js +177 -0
  112. package/support.d.ts +1 -0
  113. package/support.js +13 -0
  114. package/types/appium-manifest.ts +27 -15
  115. package/types/cli.ts +2 -9
  116. package/types/{external-manifest.ts → extension-manifest.ts} +21 -15
  117. package/types/index.ts +12 -5
  118. package/build/types/extension.d.ts +0 -43
  119. package/build/types/extension.d.ts.map +0 -1
  120. package/build/types/external-manifest.d.ts.map +0 -1
  121. package/lib/appium-config.schema.json +0 -278
  122. package/scripts/postinstall.js +0 -71
  123. package/types/extension.ts +0 -56
@@ -1,7 +1,11 @@
1
-
2
1
  import _ from 'lodash';
2
+ import B from 'bluebird';
3
3
  import path from 'path';
4
4
  import resolveFrom from 'resolve-from';
5
+ import {satisfies} from 'semver';
6
+ import {util} from '@appium/support';
7
+ import {commandClasses} from '../cli/extension';
8
+ import {APPIUM_VER} from '../config';
5
9
  import log from '../logger';
6
10
  import {
7
11
  ALLOWED_SCHEMA_EXTENSIONS,
@@ -38,100 +42,312 @@ export class ExtensionConfig {
38
42
  /** @type {ExtRecord<ExtType>} */
39
43
  installedExtensions;
40
44
 
41
- /** @type {ExtensionLogFn} */
45
+ /** @type {import('@appium/types').AppiumLogger} */
42
46
  log;
43
47
 
44
48
  /** @type {Manifest} */
45
49
  manifest;
46
50
 
51
+ /**
52
+ * @type {ExtensionListData}
53
+ */
54
+ _listDataCache;
55
+
47
56
  /**
48
57
  * @protected
49
58
  * @param {ExtType} extensionType - Type of extension
50
59
  * @param {Manifest} manifest - `Manifest` instance
51
- * @param {ExtensionLogFn} [logFn]
52
60
  */
53
- constructor (extensionType, manifest, logFn) {
54
- const logger = _.isFunction(logFn) ? logFn : log.error.bind(log);
61
+ constructor(extensionType, manifest) {
55
62
  this.extensionType = extensionType;
56
63
  this.configKey = `${extensionType}s`;
57
64
  this.installedExtensions = manifest.getExtensionData(extensionType);
58
- this.log = logger;
59
65
  this.manifest = manifest;
60
66
  }
61
67
 
62
- get manifestPath () {
68
+ get manifestPath() {
63
69
  return this.manifest.manifestPath;
64
70
  }
65
71
 
66
- get appiumHome () {
72
+ get appiumHome() {
67
73
  return this.manifest.appiumHome;
68
74
  }
69
75
 
70
76
  /**
71
- * Checks extensions for problems
72
- * @param {ExtRecord<ExtType>} exts - Extension data
77
+ * Returns a list of errors for a given extension.
78
+ *
79
+ * @param {ExtName<ExtType>} extName
80
+ * @param {ExtManifest<ExtType>} extManifest
81
+ * @returns {ExtManifestProblem[]}
73
82
  */
74
- validate (exts) {
75
- const foundProblems =
76
- /** @type {Record<ExtName<ExtType>,Problem[]>} */ ({});
77
- for (const [
78
- extName,
79
- extData,
80
- ] of /** @type {[ExtName<ExtType>, ExtManifest<ExtType>][]} */ (
81
- _.toPairs(exts)
82
- )) {
83
- foundProblems[extName] = [
84
- ...this.getGenericConfigProblems(extData, extName),
85
- ...this.getConfigProblems(extData),
86
- ...this.getSchemaProblems(extData, extName),
87
- ];
88
- }
83
+ getProblems(extName, extManifest) {
84
+ return [
85
+ ...this.getGenericConfigProblems(extManifest, extName),
86
+ ...this.getConfigProblems(extManifest, extName),
87
+ ...this.getSchemaProblems(extManifest, extName),
88
+ ];
89
+ }
90
+
91
+ /**
92
+ * Returns a list of warnings for a given extension.
93
+ *
94
+ * @param {ExtName<ExtType>} extName
95
+ * @param {ExtManifest<ExtType>} extManifest
96
+ * @returns {Promise<string[]>}
97
+ */
98
+ async getWarnings(extName, extManifest) {
99
+ const [genericConfigWarnings, configWarnings] = await B.all([
100
+ this.getGenericConfigWarnings(extManifest, extName),
101
+ this.getConfigWarnings(extManifest, extName),
102
+ ]);
103
+
104
+ return [...genericConfigWarnings, ...configWarnings];
105
+ }
89
106
 
90
- const problemSummaries = [];
91
- for (const [extName, problems] of _.toPairs(foundProblems)) {
107
+ /**
108
+ * Returns a list of extension-type-specific issues. To be implemented by subclasses.
109
+ * @abstract
110
+ * @param {ExtManifest<ExtType>} extManifest
111
+ * @param {ExtName<ExtType>} extName
112
+ * @returns {Promise<string[]>}
113
+ */
114
+ // eslint-disable-next-line no-unused-vars,require-await
115
+ async getConfigWarnings(extManifest, extName) {
116
+ return [];
117
+ }
118
+
119
+ /**
120
+ *
121
+ * @param {Map<ExtName<ExtType>,ExtManifestProblem[]>} [errorMap]
122
+ * @param {Map<ExtName<ExtType>,string[]>} [warningMap]
123
+ */
124
+ getValidationResultSummaries(errorMap = new Map(), warningMap = new Map()) {
125
+ /**
126
+ * Array of computed strings
127
+ * @type {string[]}
128
+ */
129
+ const errorSummaries = [];
130
+ for (const [extName, problems] of errorMap.entries()) {
92
131
  if (_.isEmpty(problems)) {
93
132
  continue;
94
133
  }
95
134
  // remove this extension from the list since it's not valid
96
- delete exts[extName];
97
- problemSummaries.push(
98
- `${this.extensionType} ${extName} had errors and will not ` +
99
- `be available. Errors:`,
135
+ errorSummaries.push(
136
+ `${this.extensionType} "${extName}" had ${util.pluralize(
137
+ 'error',
138
+ problems.length
139
+ )} and will not be available:`
100
140
  );
101
141
  for (const problem of problems) {
102
- problemSummaries.push(
103
- ` - ${problem.err} (Actual value: ` +
104
- `${JSON.stringify(problem.val)})`,
142
+ errorSummaries.push(
143
+ ` - ${problem.err} (Actual value: ` + `${JSON.stringify(problem.val)})`
105
144
  );
106
145
  }
107
146
  }
147
+ /** @type {string[]} */
148
+ const warningSummaries = [];
149
+ for (const [extName, warnings] of warningMap.entries()) {
150
+ if (_.isEmpty(warnings)) {
151
+ continue;
152
+ }
153
+ const extTypeText = _.capitalize(this.extensionType);
154
+ const problemEnumerationText = util.pluralize('potential problem', warnings.length, true);
155
+ warningSummaries.push(`${extTypeText} "${extName}" has ${problemEnumerationText}: `);
156
+ for (const warning of warnings) {
157
+ warningSummaries.push(` - ${warning}`);
158
+ }
159
+ }
108
160
 
109
- if (!_.isEmpty(problemSummaries)) {
110
- this.log(
111
- `Appium encountered one or more errors while validating ` +
112
- `the ${this.configKey} extension file (${this.manifestPath}):`,
113
- );
114
- for (const summary of problemSummaries) {
115
- this.log(summary);
161
+ return {errorSummaries, warningSummaries};
162
+ }
163
+
164
+ /**
165
+ * Checks extensions for problems. To be called by subclasses' `validate` method.
166
+ *
167
+ * Errors and warnings will be displayed to the user.
168
+ *
169
+ * This method mutates `exts`.
170
+ *
171
+ * @protected
172
+ * @param {ExtRecord<ExtType>} exts - Lookup of extension names to {@linkcode ExtManifest} objects
173
+ * @returns {Promise<ExtRecord<ExtType>>} The same lookup, but picking only error-free extensions
174
+ */
175
+ async _validate(exts) {
176
+ /**
177
+ * Lookup of extension names to {@linkcode ExtManifestProblem ExtManifestProblems}
178
+ * @type {Map<ExtName<ExtType>,ExtManifestProblem[]>}
179
+ */
180
+ const errorMap = new Map();
181
+ /**
182
+ * Lookup of extension names to warnings.
183
+ * @type {Map<ExtName<ExtType>,string[]>}
184
+ */
185
+ const warningMap = new Map();
186
+
187
+ for (const [extName, extManifest] of _.toPairs(exts)) {
188
+ const [errors, warnings] = await B.all([
189
+ this.getProblems(extName, extManifest),
190
+ this.getWarnings(extName, extManifest),
191
+ ]);
192
+ if (errors.length) {
193
+ delete exts[extName];
116
194
  }
195
+ errorMap.set(extName, errors);
196
+ warningMap.set(extName, warnings);
117
197
  }
118
198
 
199
+ const {errorSummaries, warningSummaries} = this.getValidationResultSummaries(
200
+ errorMap,
201
+ warningMap
202
+ );
203
+
204
+ if (!_.isEmpty(errorSummaries)) {
205
+ log.error(
206
+ `Appium encountered ${util.pluralize(
207
+ 'error',
208
+ errorSummaries.length,
209
+ true
210
+ )} while validating ${this.configKey} found in manifest ${this.manifestPath}`
211
+ );
212
+ for (const summary of errorSummaries) {
213
+ log.error(summary);
214
+ }
215
+ } else {
216
+ // only display warnings if there are no errors!
217
+
218
+ if (!_.isEmpty(warningSummaries)) {
219
+ log.warn(
220
+ `Appium encountered ${util.pluralize(
221
+ 'warning',
222
+ warningSummaries.length,
223
+ true
224
+ )} while validating ${this.configKey} found in manifest ${this.manifestPath}`
225
+ );
226
+ for (const summary of warningSummaries) {
227
+ log.warn(summary);
228
+ }
229
+ }
230
+ }
119
231
  return exts;
120
232
  }
121
233
 
122
234
  /**
123
- * @param {ExtManifest<ExtType>} extData
235
+ * Retrieves listing data for extensions via command class.
236
+ * Caches the result in {@linkcode ExtensionConfig._listDataCache}
237
+ * @protected
238
+ * @returns {Promise<ExtensionListData>}
239
+ */
240
+ async getListData() {
241
+ if (this._listDataCache) {
242
+ return this._listDataCache;
243
+ }
244
+ const CommandClass = /** @type {ExtCommand<ExtType>} */ (commandClasses[this.extensionType]);
245
+ const cmd = new CommandClass({config: this, json: true});
246
+ const listData = await cmd.list({showInstalled: true, showUpdates: true});
247
+ this._listDataCache = listData;
248
+ return listData;
249
+ }
250
+
251
+ /**
252
+ * Returns a list of warnings for a particular extension.
253
+ *
254
+ * By definition, a non-empty list of warnings does _not_ imply the extension cannot be loaded,
255
+ * but it may not work as expected or otherwise throw an exception at runtime.
256
+ *
257
+ * @param {ExtManifest<ExtType>} extManifest
124
258
  * @param {ExtName<ExtType>} extName
125
- * @returns {Problem[]}
259
+ * @returns {Promise<string[]>}
260
+ */
261
+ async getGenericConfigWarnings(extManifest, extName) {
262
+ const {appiumVersion, installSpec, installType, pkgName} = extManifest;
263
+ const warnings = [];
264
+
265
+ const invalidFields = [];
266
+ if (!_.isString(installSpec)) {
267
+ invalidFields.push('installSpec');
268
+ }
269
+
270
+ if (!INSTALL_TYPES.has(installType)) {
271
+ invalidFields.push('installType');
272
+ }
273
+
274
+ const extTypeText = _.capitalize(this.extensionType);
275
+
276
+ if (invalidFields.length) {
277
+ const invalidFieldsEnumerationText = util.pluralize(
278
+ 'invalid or missing field',
279
+ invalidFields.length,
280
+ true
281
+ );
282
+ const invalidFieldsText = invalidFields.map((field) => `"${field}"`).join(', ');
283
+
284
+ warnings.push(
285
+ `${extTypeText} "${extName}" (package \`${pkgName}\`) has ${invalidFieldsEnumerationText} (${invalidFieldsText}) in \`extensions.yaml\`; this may cause upgrades done via the \`appium\` CLI tool to fail. Please reinstall with \`appium ${this.extensionType} uninstall ${extName}\` and \`appium ${this.extensionType} install ${extName}\` to attempt a fix.`
286
+ );
287
+ }
288
+
289
+ /**
290
+ * Helps concatenate warning messages related to peer dependencies
291
+ * @param {string} reason
292
+ * @returns string
293
+ */
294
+ const createPeerWarning = (reason) =>
295
+ `${extTypeText} "${extName}" (package \`${pkgName}\`) may be incompatible with the current version of Appium (v${APPIUM_VER}) due to ${reason}`;
296
+
297
+ if (_.isString(appiumVersion) && !satisfies(APPIUM_VER, appiumVersion)) {
298
+ const listData = await this.getListData();
299
+ const extListData = /** @type {InstalledExtensionListData} */ (listData[extName]);
300
+ if (extListData?.installed) {
301
+ const {updateVersion, upToDate} = extListData;
302
+ if (!upToDate) {
303
+ warnings.push(
304
+ createPeerWarning(
305
+ `its peer dependency on older Appium v${appiumVersion}. Please upgrade \`${pkgName}\` to v${updateVersion} or newer.`
306
+ )
307
+ );
308
+ } else {
309
+ warnings.push(
310
+ createPeerWarning(
311
+ `its peer dependency on older Appium v${appiumVersion}. Please ask the developer of \`${pkgName}\` to update the peer dependency on Appium to v${APPIUM_VER}.`
312
+ )
313
+ );
314
+ }
315
+ }
316
+ } else if (!_.isString(appiumVersion)) {
317
+ const listData = await this.getListData();
318
+ const extListData = /** @type {InstalledExtensionListData} */ (listData[extName]);
319
+ if (!extListData?.upToDate && extListData?.updateVersion) {
320
+ warnings.push(
321
+ createPeerWarning(
322
+ `an invalid or missing peer dependency on Appium. A newer version of \`${pkgName}\` is available; please attempt to upgrade "${extName}" to v${extListData.updateVersion} or newer.`
323
+ )
324
+ );
325
+ } else {
326
+ warnings.push(
327
+ createPeerWarning(
328
+ `an invalid or missing peer dependency on Appium. Please ask the developer of \`${pkgName}\` to add a peer dependency on \`^appium@${APPIUM_VER}\`.`
329
+ )
330
+ );
331
+ }
332
+ }
333
+ return warnings;
334
+ }
335
+ /**
336
+ * Returns list of unrecoverable errors (if any) for the given extension _if_ it has a `schema` property.
337
+ *
338
+ * @param {ExtManifest<ExtType>} extManifest - Extension data (from manifest)
339
+ * @param {ExtName<ExtType>} extName - Extension name (from manifest)
340
+ * @returns {ExtManifestProblem[]}
126
341
  */
127
- getSchemaProblems (extData, extName) {
342
+ getSchemaProblems(extManifest, extName) {
343
+ /** @type {ExtManifestProblem[]} */
128
344
  const problems = [];
129
- const {schema: argSchemaPath} = extData;
130
- if (ExtensionConfig.extDataHasSchema(extData)) {
345
+ const {schema: argSchemaPath} = extManifest;
346
+ if (ExtensionConfig.extDataHasSchema(extManifest)) {
131
347
  if (_.isString(argSchemaPath)) {
132
348
  if (isAllowedSchemaFileExtension(argSchemaPath)) {
133
349
  try {
134
- this.readExtensionSchema(extName, extData);
350
+ this.readExtensionSchema(extName, extManifest);
135
351
  } catch (err) {
136
352
  problems.push({
137
353
  err: `Unable to register schema at path ${argSchemaPath}; ${err.message}`,
@@ -148,7 +364,7 @@ export class ExtensionConfig {
148
364
  }
149
365
  } else if (_.isPlainObject(argSchemaPath)) {
150
366
  try {
151
- this.readExtensionSchema(extName, extData);
367
+ this.readExtensionSchema(extName, extManifest);
152
368
  } catch (err) {
153
369
  problems.push({
154
370
  err: `Unable to register embedded schema; ${err.message}`,
@@ -166,44 +382,33 @@ export class ExtensionConfig {
166
382
  }
167
383
 
168
384
  /**
169
- * @param {ExtManifest<ExtType>} extData
170
- * @param {ExtName<ExtType>} extName
171
- * @returns {Problem[]}
385
+ * Return a list of generic unrecoverable errors for the given extension
386
+ * @param {ExtManifest<ExtType>} extManifest - Extension data (from manifest)
387
+ * @param {ExtName<ExtType>} extName - Extension name (from manifest)
388
+ * @returns {ExtManifestProblem[]}
172
389
  */
173
390
  // eslint-disable-next-line no-unused-vars
174
- getGenericConfigProblems (extData, extName) {
175
- const {version, pkgName, installSpec, installType, mainClass} =
176
- extData;
391
+ getGenericConfigProblems(extManifest, extName) {
392
+ const {version, pkgName, mainClass} = extManifest;
177
393
  const problems = [];
178
394
 
179
395
  if (!_.isString(version)) {
180
- problems.push({err: 'Missing or incorrect version', val: version});
181
- }
182
-
183
- if (!_.isString(pkgName)) {
184
396
  problems.push({
185
- err: 'Missing or incorrect NPM package name',
186
- val: pkgName,
397
+ err: `Invalid or missing \`version\` field in my \`package.json\` and/or \`extensions.yaml\` (must be a string)`,
398
+ val: version,
187
399
  });
188
400
  }
189
401
 
190
- if (!_.isString(installSpec)) {
191
- problems.push({
192
- err: 'Missing or incorrect installation spec',
193
- val: installSpec,
194
- });
195
- }
196
-
197
- if (!INSTALL_TYPES.has(installType)) {
402
+ if (!_.isString(pkgName)) {
198
403
  problems.push({
199
- err: 'Missing or incorrect install type',
200
- val: installType,
404
+ err: `Invalid or missing \`name\` field in my \`package.json\` and/or \`extensions.yaml\` (must be a string)`,
405
+ val: pkgName,
201
406
  });
202
407
  }
203
408
 
204
409
  if (!_.isString(mainClass)) {
205
410
  problems.push({
206
- err: 'Missing or incorrect driver class name',
411
+ err: `Invalid or missing \`appium.mainClass\` field in my \`package.json\` and/or \`mainClass\` field in \`extensions.yaml\` (must be a string)`,
207
412
  val: mainClass,
208
413
  });
209
414
  }
@@ -213,23 +418,24 @@ export class ExtensionConfig {
213
418
 
214
419
  /**
215
420
  * @abstract
216
- * @param {ExtManifest<ExtType>} extData
217
- * @returns {Problem[]}
421
+ * @param {ExtManifest<ExtType>} extManifest
422
+ * @param {ExtName<ExtType>} extName
423
+ * @returns {ExtManifestProblem[]}
218
424
  */
219
425
  // eslint-disable-next-line no-unused-vars
220
- getConfigProblems (extData) {
426
+ getConfigProblems(extManifest, extName) {
221
427
  // shoud override this method if special validation is necessary for this extension type
222
428
  return [];
223
429
  }
224
430
 
225
431
  /**
226
432
  * @param {string} extName
227
- * @param {ExtManifest<ExtType>} extData
433
+ * @param {ExtManifest<ExtType>} extManifest
228
434
  * @param {ExtensionConfigMutationOpts} [opts]
229
435
  * @returns {Promise<void>}
230
436
  */
231
- async addExtension (extName, extData, {write = true} = {}) {
232
- this.manifest.addExtension(this.extensionType, extName, extData);
437
+ async addExtension(extName, extManifest, {write = true} = {}) {
438
+ this.manifest.addExtension(this.extensionType, extName, extManifest);
233
439
  if (write) {
234
440
  await this.manifest.write();
235
441
  }
@@ -237,14 +443,14 @@ export class ExtensionConfig {
237
443
 
238
444
  /**
239
445
  * @param {ExtName<ExtType>} extName
240
- * @param {ExtManifest<ExtType>|import('../cli/extension-command').ExtensionFields<ExtType>} extData
446
+ * @param {ExtManifest<ExtType>|import('../cli/extension-command').ExtensionFields<ExtType>} extManifest
241
447
  * @param {ExtensionConfigMutationOpts} [opts]
242
448
  * @returns {Promise<void>}
243
449
  */
244
- async updateExtension (extName, extData, {write = true} = {}) {
450
+ async updateExtension(extName, extManifest, {write = true} = {}) {
245
451
  this.installedExtensions[extName] = {
246
452
  ...this.installedExtensions[extName],
247
- ...extData,
453
+ ...extManifest,
248
454
  };
249
455
  if (write) {
250
456
  await this.manifest.write();
@@ -252,11 +458,13 @@ export class ExtensionConfig {
252
458
  }
253
459
 
254
460
  /**
461
+ * Remove an extension from the list of installed extensions, and optionally avoid a write to the manifest file.
462
+ *
255
463
  * @param {ExtName<ExtType>} extName
256
464
  * @param {ExtensionConfigMutationOpts} [opts]
257
465
  * @returns {Promise<void>}
258
466
  */
259
- async removeExtension (extName, {write = true} = {}) {
467
+ async removeExtension(extName, {write = true} = {}) {
260
468
  delete this.installedExtensions[extName];
261
469
  if (write) {
262
470
  await this.manifest.write();
@@ -268,35 +476,32 @@ export class ExtensionConfig {
268
476
  * @returns {void}
269
477
  */
270
478
  // eslint-disable-next-line no-unused-vars
271
- print (activeNames) {
479
+ print(activeNames) {
272
480
  if (_.isEmpty(this.installedExtensions)) {
273
481
  log.info(
274
482
  `No ${this.configKey} have been installed in ${this.appiumHome}. Use the "appium ${this.extensionType}" ` +
275
- 'command to install the one(s) you want to use.',
483
+ 'command to install the one(s) you want to use.'
276
484
  );
277
485
  return;
278
486
  }
279
487
 
280
488
  log.info(`Available ${this.configKey}:`);
281
- for (const [
282
- extName,
283
- extData,
284
- ] of /** @type {[string, ExtManifest<ExtType>][]} */ (
285
- _.toPairs(this.installedExtensions)
286
- )) {
287
- log.info(` - ${this.extensionDesc(extName, extData)}`);
489
+ for (const [extName, extManifest] of /** @type {[string, ExtManifest<ExtType>][]} */ (
490
+ _.toPairs(this.installedExtensions)
491
+ )) {
492
+ log.info(` - ${this.extensionDesc(extName, extManifest)}`);
288
493
  }
289
494
  }
290
495
 
291
496
  /**
292
497
  * Returns a string describing the extension. Subclasses must implement.
293
498
  * @param {ExtName<ExtType>} extName - Extension name
294
- * @param {ExtManifest<ExtType>} extData - Extension data
499
+ * @param {ExtManifest<ExtType>} extManifest - Extension data
295
500
  * @returns {string}
296
501
  * @abstract
297
502
  */
298
503
  // eslint-disable-next-line no-unused-vars
299
- extensionDesc (extName, extData) {
504
+ extensionDesc(extName, extManifest) {
300
505
  throw new Error('This must be implemented in a subclass');
301
506
  }
302
507
 
@@ -304,7 +509,7 @@ export class ExtensionConfig {
304
509
  * @param {string} extName
305
510
  * @returns {string}
306
511
  */
307
- getInstallPath (extName) {
512
+ getInstallPath(extName) {
308
513
  return path.join(this.appiumHome, 'node_modules', this.installedExtensions[extName].pkgName);
309
514
  }
310
515
 
@@ -313,24 +518,36 @@ export class ExtensionConfig {
313
518
  * @param {ExtName<ExtType>} extName
314
519
  * @returns {ExtClass<ExtType>}
315
520
  */
316
- require (extName) {
521
+ require(extName) {
317
522
  const {mainClass} = this.installedExtensions[extName];
318
523
  const reqPath = this.getInstallPath(extName);
319
- const reqResolved = require.resolve(reqPath);
524
+ /** @type {string} */
525
+ let reqResolved;
526
+ try {
527
+ reqResolved = require.resolve(reqPath);
528
+ } catch (err) {
529
+ throw new ReferenceError(`Could not find a ${this.extensionType} installed at ${reqPath}`);
530
+ }
320
531
  // note: this will only reload the entry point
321
532
  if (process.env.APPIUM_RELOAD_EXTENSIONS && require.cache[reqResolved]) {
322
533
  log.debug(`Removing ${reqResolved} from require cache`);
323
534
  delete require.cache[reqResolved];
324
535
  }
325
536
  log.debug(`Requiring ${this.extensionType} at ${reqPath}`);
326
- return require(reqPath)[mainClass];
537
+ const MainClass = require(reqPath)[mainClass];
538
+ if (!MainClass) {
539
+ throw new ReferenceError(
540
+ `Could not find a class named "${mainClass}" exported by ${this.extensionType} "${extName}"`
541
+ );
542
+ }
543
+ return MainClass;
327
544
  }
328
545
 
329
546
  /**
330
547
  * @param {string} extName
331
548
  * @returns {boolean}
332
549
  */
333
- isInstalled (extName) {
550
+ isInstalled(extName) {
334
551
  return _.includes(Object.keys(this.installedExtensions), extName);
335
552
  }
336
553
 
@@ -341,14 +558,14 @@ export class ExtensionConfig {
341
558
  * @param {string} appiumHome
342
559
  * @param {ExtType} extType
343
560
  * @param {ExtName<ExtType>} extName - Extension name (unique to its type)
344
- * @param {ExtManifestWithSchema<ExtType>} extData - Extension config
561
+ * @param {ExtManifestWithSchema<ExtType>} extManifest - Extension config
345
562
  * @returns {import('ajv').SchemaObject|undefined}
346
563
  */
347
- static _readExtensionSchema (appiumHome, extType, extName, extData) {
348
- const {pkgName, schema: argSchemaPath} = extData;
564
+ static _readExtensionSchema(appiumHome, extType, extName, extManifest) {
565
+ const {pkgName, schema: argSchemaPath} = extManifest;
349
566
  if (!argSchemaPath) {
350
567
  throw new TypeError(
351
- `No \`schema\` property found in config for ${extType} ${pkgName} -- why is this function being called?`,
568
+ `No \`schema\` property found in config for ${extType} ${pkgName} -- why is this function being called?`
352
569
  );
353
570
  }
354
571
  let moduleObject;
@@ -359,9 +576,7 @@ export class ExtensionConfig {
359
576
  moduleObject = argSchemaPath;
360
577
  }
361
578
  // this sucks. default exports should be destroyed
362
- const schema = moduleObject.__esModule
363
- ? moduleObject.default
364
- : moduleObject;
579
+ const schema = moduleObject.__esModule ? moduleObject.default : moduleObject;
365
580
  registerSchema(extType, extName, schema);
366
581
  return schema;
367
582
  }
@@ -370,41 +585,37 @@ export class ExtensionConfig {
370
585
  * Returns `true` if a specific {@link ExtManifest} object has a `schema` prop.
371
586
  * The {@link ExtManifest} object becomes a {@link ExtManifestWithSchema} object.
372
587
  * @template {ExtensionType} ExtType
373
- * @param {ExtManifest<ExtType>} extData
374
- * @returns {extData is ExtManifestWithSchema<ExtType>}
588
+ * @param {ExtManifest<ExtType>} extManifest
589
+ * @returns {extManifest is ExtManifestWithSchema<ExtType>}
375
590
  */
376
- static extDataHasSchema (extData) {
377
- return _.isString(extData?.schema) || _.isObject(extData?.schema);
591
+ static extDataHasSchema(extManifest) {
592
+ return _.isString(extManifest?.schema) || _.isObject(extManifest?.schema);
378
593
  }
379
594
 
380
595
  /**
381
596
  * If an extension provides a schema, this will load the schema and attempt to
382
597
  * register it with the schema registrar.
383
598
  * @param {ExtName<ExtType>} extName - Name of extension
384
- * @param {ExtManifestWithSchema<ExtType>} extData - Extension data
599
+ * @param {ExtManifestWithSchema<ExtType>} extManifest - Extension data
385
600
  * @returns {import('ajv').SchemaObject|undefined}
386
601
  */
387
- readExtensionSchema (extName, extData) {
602
+ readExtensionSchema(extName, extManifest) {
388
603
  return ExtensionConfig._readExtensionSchema(
389
604
  this.appiumHome,
390
605
  this.extensionType,
391
606
  extName,
392
- extData,
607
+ extManifest
393
608
  );
394
609
  }
395
610
  }
396
611
 
397
- export {
398
- INSTALL_TYPE_NPM,
399
- INSTALL_TYPE_GIT,
400
- INSTALL_TYPE_LOCAL,
401
- INSTALL_TYPE_GITHUB,
402
- INSTALL_TYPES,
403
- };
612
+ export {INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GITHUB, INSTALL_TYPES};
404
613
 
405
614
  /**
406
- * Config problem
407
- * @typedef Problem
615
+ * An issue with the {@linkcode ExtManifest} for a particular extension.
616
+ *
617
+ * The existance of such an object implies that the extension cannot be loaded.
618
+ * @typedef ExtManifestProblem
408
619
  * @property {string} err - Error message
409
620
  * @property {any} val - Associated value
410
621
  */
@@ -417,33 +628,38 @@ export {
417
628
  */
418
629
 
419
630
  /**
420
- * @typedef {import('../../types').ExtensionType} ExtensionType
631
+ * @typedef {import('@appium/types').ExtensionType} ExtensionType
421
632
  * @typedef {import('./manifest').Manifest} Manifest
422
633
  */
423
634
 
424
635
  /**
425
636
  * @template T
426
- * @typedef {import('../../types/appium-manifest').ExtManifest<T>} ExtManifest
637
+ * @typedef {import('appium/types').ExtManifest<T>} ExtManifest
638
+ */
639
+
640
+ /**
641
+ * @template T
642
+ * @typedef {import('appium/types').ExtManifestWithSchema<T>} ExtManifestWithSchema
427
643
  */
428
644
 
429
645
  /**
430
646
  * @template T
431
- * @typedef {import('../../types/appium-manifest').ExtManifestWithSchema<T>} ExtManifestWithSchema
647
+ * @typedef {import('appium/types').ExtName<T>} ExtName
432
648
  */
433
649
 
434
650
  /**
435
651
  * @template T
436
- * @typedef {import('../../types/appium-manifest').ExtName<T>} ExtName
652
+ * @typedef {import('appium/types').ExtClass<T>} ExtClass
437
653
  */
438
654
 
439
655
  /**
440
656
  * @template T
441
- * @typedef {import('../../types/extension').ExtClass<T>} ExtClass
657
+ * @typedef {import('appium/types').ExtRecord<T>} ExtRecord
442
658
  */
443
659
 
444
660
  /**
445
661
  * @template T
446
- * @typedef {import('../../types/appium-manifest').ExtRecord<T>} ExtRecord
662
+ * @typedef {import('../cli/extension').ExtCommand<T>} ExtCommand
447
663
  */
448
664
 
449
665
  /**
@@ -456,3 +672,8 @@ export {
456
672
  * A valid install type
457
673
  * @typedef {typeof INSTALL_TYPE_NPM | typeof INSTALL_TYPE_GIT | typeof INSTALL_TYPE_LOCAL | typeof INSTALL_TYPE_GITHUB} InstallType
458
674
  */
675
+
676
+ /**
677
+ * @typedef {import('../cli/extension-command').ExtensionListData} ExtensionListData
678
+ * @typedef {import('../cli/extension-command').InstalledExtensionListData} InstalledExtensionListData
679
+ */