appium 2.0.0-beta.4 → 2.0.0-beta.42

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