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,13 +1,17 @@
1
1
  /* eslint-disable no-console */
2
-
2
+ import B from 'bluebird';
3
3
  import _ from 'lodash';
4
4
  import path from 'path';
5
- import { npm, fs, util, env } from '@appium/support';
6
- import { log, spinWith, RingBuffer } from './utils';
7
- import { SubProcess } from 'teen_process';
8
- import { INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB,
9
- INSTALL_TYPE_LOCAL } from '../extension/extension-config';
10
- import { packageDidChange } from '../extension/package-changed';
5
+ import {npm, util, env, console} from '@appium/support';
6
+ import {spinWith, RingBuffer} from './utils';
7
+ import {SubProcess} from 'teen_process';
8
+ import {
9
+ INSTALL_TYPE_NPM,
10
+ INSTALL_TYPE_GIT,
11
+ INSTALL_TYPE_GITHUB,
12
+ INSTALL_TYPE_LOCAL,
13
+ } from '../extension/extension-config';
14
+ import {packageDidChange} from '../extension/package-changed';
11
15
 
12
16
  const UPDATE_ALL = 'installed';
13
17
 
@@ -40,64 +44,78 @@ class ExtensionCommand {
40
44
  * Build an ExtensionCommand
41
45
  * @param {ExtensionCommandOptions<ExtType>} opts
42
46
  */
43
- constructor ({config, json}) {
47
+ constructor({config, json}) {
44
48
  this.config = config;
45
- this.isJsonOutput = json;
49
+ this.log = new console.CliConsole({jsonMode: json});
50
+ this.isJsonOutput = Boolean(json);
46
51
  }
47
52
 
48
53
  /**
49
54
  * `driver` or `plugin`, depending on the `ExtensionConfig`.
50
55
  */
51
- get type () {
56
+ get type() {
52
57
  return this.config.extensionType;
53
58
  }
54
59
 
60
+ /**
61
+ * Logs a message and returns an {@linkcode Error} to throw.
62
+ *
63
+ * For TS to understand that a function throws an exception, it must actually throw an exception--
64
+ * in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
65
+ * nor is something like `@returns {never}` which does not imply a thrown exception.
66
+ * @param {string} message
67
+ * @protected
68
+ * @returns {Error}
69
+ */
70
+ _createFatalError(message) {
71
+ return new Error(this.log.decorate(message, 'error'));
72
+ }
73
+
55
74
  /**
56
75
  * Take a CLI parse and run an extension command based on its type
57
76
  *
58
77
  * @param {object} args - a key/value object with CLI flags and values
59
78
  * @return {Promise<object>} the result of the specific command which is executed
60
79
  */
61
- async execute (args) {
80
+ async execute(args) {
62
81
  const cmd = args[`${this.type}Command`];
63
82
  if (!_.isFunction(this[cmd])) {
64
- throw new Error(`Cannot handle ${this.type} command ${cmd}`);
83
+ throw this._createFatalError(`Cannot handle ${this.type} command ${cmd}`);
65
84
  }
66
85
  const executeCmd = this[cmd].bind(this);
67
86
  return await executeCmd(args);
68
87
  }
69
88
 
70
- /**
71
- * @typedef ListOptions
72
- * @property {boolean} showInstalled - whether should show only installed extensions
73
- * @property {boolean} showUpdates - whether should show available updates
74
- */
75
-
76
89
  /**
77
90
  * List extensions
78
91
  *
79
92
  * @param {ListOptions} opts
80
93
  * @return {Promise<ExtensionListData>} map of extension names to extension data
81
94
  */
82
- async list ({showInstalled, showUpdates}) {
95
+ async list({showInstalled, showUpdates}) {
83
96
  const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
84
97
  const installedNames = Object.keys(this.config.installedExtensions);
85
98
  const knownNames = Object.keys(this.knownExtensions);
86
- const exts = [...installedNames, ...knownNames].reduce((acc, name) => {
87
- if (!acc[name]) {
88
- if (installedNames.includes(name)) {
89
- acc[name] = {...this.config.installedExtensions[name], installed: true};
90
- } else if (!showInstalled) {
91
- acc[name] = {pkgName: this.knownExtensions[name], installed: false};
99
+ const exts = [...installedNames, ...knownNames].reduce(
100
+ (acc, name) => {
101
+ if (!acc[name]) {
102
+ if (installedNames.includes(name)) {
103
+ acc[name] = {
104
+ ...this.config.installedExtensions[name],
105
+ installed: true,
106
+ };
107
+ } else if (!showInstalled) {
108
+ acc[name] = {pkgName: this.knownExtensions[name], installed: false};
109
+ }
92
110
  }
93
- }
94
- return acc;
95
- },
96
- /**
97
- * This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
98
- * {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
99
- * @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
100
- */({}));
111
+ return acc;
112
+ },
113
+ /**
114
+ * This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
115
+ * {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
116
+ * @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
117
+ */ ({})
118
+ );
101
119
 
102
120
  // if we want to show whether updates are available, put that behind a spinner
103
121
  await spinWith(this.isJsonOutput, lsMsg, async () => {
@@ -117,7 +135,7 @@ class ExtensionCommand {
117
135
  }
118
136
  });
119
137
 
120
- const listData = /** @type {ExtensionListData} */(exts);
138
+ const listData = /** @type {ExtensionListData} */ (exts);
121
139
 
122
140
  // if we're just getting the data, short circuit return here since we don't need to do any
123
141
  // formatting logic
@@ -125,16 +143,14 @@ class ExtensionCommand {
125
143
  return listData;
126
144
  }
127
145
 
128
- for (const [
129
- name,
130
- data
131
- ] of _.toPairs(listData)) {
146
+ for (const [name, data] of _.toPairs(listData)) {
132
147
  let installTxt = ' [not installed]'.grey;
133
148
  let updateTxt = '';
134
149
  let upToDateTxt = '';
135
150
  let unsafeUpdateTxt = '';
136
151
  if (data.installed) {
137
- const {installType, installSpec, updateVersion, unsafeUpdateVersion, version, upToDate} = data;
152
+ const {installType, installSpec, updateVersion, unsafeUpdateVersion, version, upToDate} =
153
+ data;
138
154
  let typeTxt;
139
155
  switch (installType) {
140
156
  case INSTALL_TYPE_GIT:
@@ -162,7 +178,7 @@ class ExtensionCommand {
162
178
  }
163
179
  }
164
180
 
165
- console.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
181
+ this.log.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
166
182
  }
167
183
 
168
184
  return listData;
@@ -174,29 +190,53 @@ class ExtensionCommand {
174
190
  * @param {InstallArgs} args
175
191
  * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
176
192
  */
177
- async _install ({ext, installType, packageName}) {
193
+ async _install({installSpec, installType, packageName}) {
194
+ /** @type {ExtensionFields<ExtType>} */
178
195
  let extData;
179
- let installSpec = ext;
180
196
 
181
197
  if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
182
- throw new Error(`When using --source=${installType}, cannot also use --package`);
198
+ throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
183
199
  }
184
200
 
185
201
  if (!packageName && [INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB].includes(installType)) {
186
- throw new Error(`When using --source=${installType}, must also use --package`);
202
+ throw this._createFatalError(`When using --source=${installType}, must also use --package`);
187
203
  }
188
204
 
205
+ /**
206
+ * @type {InstallViaNpmArgs}
207
+ */
208
+ let installOpts;
209
+
210
+ /**
211
+ * The probable (?) name of the extension derived from the install spec.
212
+ *
213
+ * If using a local install type, this will remain empty.
214
+ * @type {string}
215
+ */
216
+ let probableExtName = '';
217
+
218
+ // depending on `installType`, build the options to pass into `installViaNpm`
189
219
  if (installType === INSTALL_TYPE_GITHUB) {
190
220
  if (installSpec.split('/').length !== 2) {
191
- throw new Error(`Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
192
- 'it should be of the form <org>/<repo>');
221
+ throw this._createFatalError(
222
+ `Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
223
+ 'it should be of the form <org>/<repo>'
224
+ );
193
225
  }
194
- extData = await this.installViaNpm({ext: installSpec, pkgName: /** @type {string} */(packageName)});
226
+ installOpts = {
227
+ installSpec,
228
+ pkgName: /** @type {string} */ (packageName),
229
+ };
230
+ probableExtName = installSpec;
195
231
  } else if (installType === INSTALL_TYPE_GIT) {
196
232
  // git urls can have '.git' at the end, but this is not necessary and would complicate the
197
233
  // way we download and name directories, so we can just remove it
198
234
  installSpec = installSpec.replace(/\.git$/, '');
199
- extData = await this.installViaNpm({ext: installSpec, pkgName: /** @type {string} */(packageName)});
235
+ installOpts = {
236
+ installSpec,
237
+ pkgName: /** @type {string} */ (packageName),
238
+ };
239
+ probableExtName = installSpec;
200
240
  } else {
201
241
  let pkgName, pkgVer;
202
242
  if (installType === INSTALL_TYPE_LOCAL) {
@@ -227,30 +267,71 @@ class ExtensionCommand {
227
267
  // check it exists and get the correct package
228
268
  const knownNames = Object.keys(this.knownExtensions);
229
269
  if (!_.includes(knownNames, name)) {
230
- const msg = `Could not resolve ${this.type}; are you sure it's in the list ` +
231
- `of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
232
- throw new Error(msg);
270
+ const msg =
271
+ `Could not resolve ${this.type}; are you sure it's in the list ` +
272
+ `of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
273
+ throw this._createFatalError(msg);
233
274
  }
275
+ probableExtName = name;
234
276
  pkgName = this.knownExtensions[name];
235
277
  // given that we'll use the install type in the driver json, store it as
236
278
  // 'npm' now
237
279
  installType = INSTALL_TYPE_NPM;
238
280
  }
239
281
  }
282
+ installOpts = {installSpec, pkgName, pkgVer};
283
+ }
240
284
 
241
- extData = await this.installViaNpm({ext, pkgName, pkgVer});
285
+ // fail fast here if we can
286
+ if (probableExtName && this.config.isInstalled(probableExtName)) {
287
+ throw this._createFatalError(
288
+ `A ${this.type} named "${probableExtName}" is already installed. ` +
289
+ `Did you mean to update? Run "appium ${this.type} update". See ` +
290
+ `installed ${this.type}s with "appium ${this.type} list --installed".`
291
+ );
242
292
  }
243
293
 
244
- const extName = extData[/** @type {string} */(`${this.type}Name`)];
245
- delete extData[/** @type {string} */(`${this.type}Name`)];
294
+ extData = await this.installViaNpm(installOpts);
295
+
296
+ // this _should_ be the same as `probablyExtName` as the one derived above unless
297
+ // install type is local.
298
+ const extName = extData[/** @type {string} */ (`${this.type}Name`)];
246
299
 
300
+ // check _a second time_ with the more-accurate extName
247
301
  if (this.config.isInstalled(extName)) {
248
- throw new Error(`A ${this.type} named '${extName}' is already installed. ` +
249
- `Did you mean to update? 'appium ${this.type} update'. See ` +
250
- `installed ${this.type}s with 'appium ${this.type} list --installed'.`);
302
+ throw this._createFatalError(
303
+ `A ${this.type} named "${extName}" is already installed. ` +
304
+ `Did you mean to update? Run "appium ${this.type} update". See ` +
305
+ `installed ${this.type}s with "appium ${this.type} list --installed".`
306
+ );
251
307
  }
252
308
 
309
+ // this field does not exist as such in the manifest (it's used as a property name instead)
310
+ // so that's why it's being removed here.
311
+ delete extData[/** @type {string} */ (`${this.type}Name`)];
312
+
313
+ /** @type {ExtManifest<ExtType>} */
253
314
  const extManifest = {...extData, installType, installSpec};
315
+ const [errors, warnings] = await B.all([
316
+ this.config.getProblems(extName, extManifest),
317
+ this.config.getWarnings(extName, extManifest),
318
+ ]);
319
+ const errorMap = new Map([[extName, errors]]);
320
+ const warningMap = new Map([[extName, warnings]]);
321
+ const {errorSummaries, warningSummaries} = this.config.getValidationResultSummaries(
322
+ errorMap,
323
+ warningMap
324
+ );
325
+
326
+ if (!_.isEmpty(errorSummaries)) {
327
+ throw this._createFatalError(errorSummaries.join('\n'));
328
+ }
329
+
330
+ // note that we won't show any warnings if there were errors.
331
+ if (!_.isEmpty(warningSummaries)) {
332
+ this.log.warn(warningSummaries.join('\n'));
333
+ }
334
+
254
335
  await this.config.addExtension(extName, extManifest);
255
336
 
256
337
  // update the if we've changed the local `package.json`
@@ -259,7 +340,7 @@ class ExtensionCommand {
259
340
  }
260
341
 
261
342
  // log info for the user
262
- log(this.isJsonOutput, this.getPostInstallText({extName, extData}));
343
+ this.log.info(this.getPostInstallText({extName, extData}));
263
344
 
264
345
  return this.config.installedExtensions;
265
346
  }
@@ -269,19 +350,22 @@ class ExtensionCommand {
269
350
  *
270
351
  * @param {InstallViaNpmArgs} args
271
352
  */
272
- async installViaNpm ({ext, pkgName, pkgVer}) {
353
+ async installViaNpm({installSpec, pkgName, pkgVer}) {
273
354
  const npmSpec = `${pkgName}${pkgVer ? '@' + pkgVer : ''}`;
274
- const specMsg = npmSpec === ext ? '' : ` using NPM install spec '${npmSpec}'`;
275
- const msg = `Installing '${ext}'${specMsg}`;
355
+ const specMsg = npmSpec === installSpec ? '' : ` using NPM install spec '${npmSpec}'`;
356
+ const msg = `Installing '${installSpec}'${specMsg}`;
276
357
  try {
277
- const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => (
278
- await npm.installPackage(this.config.appiumHome, pkgName, {
279
- pkgVer
280
- })
281
- ));
358
+ const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => {
359
+ const pkgJsonData = await npm.installPackage(this.config.appiumHome, pkgName, {
360
+ pkgVer,
361
+ });
362
+ this.validatePackageJson(pkgJsonData, installSpec);
363
+ return pkgJsonData;
364
+ });
365
+
282
366
  return this.getExtensionFields(pkgJsonData);
283
367
  } catch (err) {
284
- throw new Error(`Encountered an error when installing package: ${err.message}`);
368
+ throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
285
369
  }
286
370
  }
287
371
 
@@ -293,8 +377,8 @@ class ExtensionCommand {
293
377
  * @returns {string}
294
378
  */
295
379
  // eslint-disable-next-line no-unused-vars
296
- getPostInstallText (args) {
297
- throw new Error('Must be implemented in final class');
380
+ getPostInstallText(args) {
381
+ throw this._createFatalError('Must be implemented in final class');
298
382
  }
299
383
 
300
384
  /**
@@ -303,51 +387,95 @@ class ExtensionCommand {
303
387
  * load as the main driver class, or to be able to detect incompatibilities between driver and
304
388
  * appium versions.
305
389
  *
306
- * @param {ExtPackageJson<ExtType>} pkgJsonData - the package.json data for a driver module, as if it had been
307
- * straightforwardly 'require'd
390
+ * @param {ExtPackageJson<ExtType>} pkgJson - the package.json data for a driver module, as if it had been straightforwardly 'require'd
308
391
  * @returns {ExtensionFields<ExtType>}
309
392
  */
310
- getExtensionFields (pkgJsonData) {
311
- if (!pkgJsonData.appium) {
312
- throw new Error(`Installed driver did not have an 'appium' section in its ` +
313
- `package.json file as expected`);
314
- }
315
- const {appium, name, version} = pkgJsonData;
316
- this.validateExtensionFields(appium);
393
+ getExtensionFields(pkgJson) {
394
+ const {appium, name, version, peerDependencies} = pkgJson;
395
+
317
396
  /** @type {unknown} */
318
- const result = {...appium, pkgName: name, version};
319
- return /** @type {ExtensionFields<ExtType>} */(result);
397
+ const result = {
398
+ ...appium,
399
+ pkgName: name,
400
+ version,
401
+ appiumVersion: peerDependencies?.appium,
402
+ };
403
+ return /** @type {ExtensionFields<ExtType>} */ (result);
320
404
  }
321
405
 
322
406
  /**
323
- * For any package.json fields which a particular type of extension requires, validate the
324
- * presence and form of those fields on the package.json data, throwing an error if anything is
407
+ * Validates the _required_ root fields of an extension's `package.json` file.
408
+ *
409
+ * These required fields are:
410
+ * - `name`
411
+ * - `version`
412
+ * - `appium`
413
+ * @param {import('type-fest').PackageJson} pkgJson - `package.json` of extension
414
+ * @param {string} installSpec - Extension name/spec
415
+ * @throws {ReferenceError} If `package.json` has a missing or invalid field
416
+ * @returns {pkgJson is ExtPackageJson<ExtType>}
417
+ */
418
+ validatePackageJson(pkgJson, installSpec) {
419
+ const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkgJson);
420
+
421
+ /**
422
+ *
423
+ * @param {string} field
424
+ * @returns {ReferenceError}
425
+ */
426
+ const createMissingFieldError = (field) =>
427
+ new ReferenceError(
428
+ `${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
429
+ );
430
+
431
+ if (!name) {
432
+ throw createMissingFieldError('name');
433
+ }
434
+ if (!version) {
435
+ throw createMissingFieldError('version');
436
+ }
437
+ if (!appium) {
438
+ throw createMissingFieldError('appium');
439
+ }
440
+
441
+ this.validateExtensionFields(appium, installSpec);
442
+
443
+ return true;
444
+ }
445
+
446
+ /**
447
+ * For any `package.json` fields which a particular type of extension requires, validate the
448
+ * presence and form of those fields on the `package.json` data, throwing an error if anything is
325
449
  * amiss.
326
450
  *
327
- * @param {ExtMetadata<ExtType>} appiumPkgData - the data in the "appium" field of package.json for an extension
451
+ * @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
452
+ * @param {string} installSpec - Extension name/spec
328
453
  */
329
454
  // eslint-disable-next-line no-unused-vars
330
- validateExtensionFields (appiumPkgData) {
331
- throw new Error('Must be implemented in final class');
455
+ validateExtensionFields(extMetadata, installSpec) {
456
+ throw this._createFatalError('Must be implemented in final class');
332
457
  }
333
458
 
334
459
  /**
335
- * Uninstall an extension
460
+ * Uninstall an extension.
461
+ *
462
+ * First tries to do this via `npm uninstall`, but if that fails, just `rm -rf`'s the extension dir.
463
+ *
464
+ * Will only remove the extension from the manifest if it has been successfully removed.
336
465
  *
337
466
  * @param {UninstallOpts} opts
338
- * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
467
+ * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data (without the extension just uninstalled)
339
468
  */
340
- async _uninstall ({ext}) {
341
- if (!this.config.isInstalled(ext)) {
342
- throw new Error(`Can't uninstall ${this.type} '${ext}'; it is not installed`);
469
+ async _uninstall({installSpec}) {
470
+ if (!this.config.isInstalled(installSpec)) {
471
+ throw this._createFatalError(
472
+ `Can't uninstall ${this.type} '${installSpec}'; it is not installed`
473
+ );
343
474
  }
344
- const installPath = this.config.getInstallPath(ext);
345
- try {
346
- await fs.rimraf(installPath);
347
- } finally {
348
- await this.config.removeExtension(ext);
349
- }
350
- log(this.isJsonOutput, `Successfully uninstalled ${this.type} '${ext}'`.green);
475
+ const pkgName = this.config.installedExtensions[installSpec].pkgName;
476
+ await npm.uninstallPackage(this.config.appiumHome, pkgName);
477
+ await this.config.removeExtension(installSpec);
478
+ this.log.ok(`Successfully uninstalled ${this.type} '${installSpec}'`.green);
351
479
  return this.config.installedExtensions;
352
480
  }
353
481
 
@@ -357,13 +485,17 @@ class ExtensionCommand {
357
485
  * @param {ExtensionUpdateOpts} updateSpec
358
486
  * @return {Promise<ExtensionUpdateResult>}
359
487
  */
360
- async _update ({ext, unsafe}) {
361
- const shouldUpdateAll = ext === UPDATE_ALL;
488
+ async _update({installSpec, unsafe}) {
489
+ const shouldUpdateAll = installSpec === UPDATE_ALL;
362
490
  // if we're specifically requesting an update for an extension, make sure it's installed
363
- if (!shouldUpdateAll && !this.config.isInstalled(ext)) {
364
- throw new Error(`The ${this.type} '${ext}' was not installed, so can't be updated`);
491
+ if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
492
+ throw this._createFatalError(
493
+ `The ${this.type} "${installSpec}" was not installed, so can't be updated`
494
+ );
365
495
  }
366
- const extsToUpdate = shouldUpdateAll ? Object.keys(this.config.installedExtensions) : [ext];
496
+ const extsToUpdate = shouldUpdateAll
497
+ ? Object.keys(this.config.installedExtensions)
498
+ : [installSpec];
367
499
 
368
500
  // 'errors' will have ext names as keys and error objects as values
369
501
  /** @type {Record<string,Error>} */
@@ -381,17 +513,23 @@ class ExtensionCommand {
381
513
  throw new NotUpdatableError();
382
514
  }
383
515
  });
384
- const update = await spinWith(this.isJsonOutput, `Checking if ${this.type} '${e}' needs an update`, async () => {
385
- const update = await this.checkForExtensionUpdate(e);
386
- if (!(update.safeUpdate || update.unsafeUpdate)) {
387
- throw new NoUpdatesAvailableError();
516
+ const update = await spinWith(
517
+ this.isJsonOutput,
518
+ `Checking if ${this.type} '${e}' needs an update`,
519
+ async () => {
520
+ const update = await this.checkForExtensionUpdate(e);
521
+ if (!(update.safeUpdate || update.unsafeUpdate)) {
522
+ throw new NoUpdatesAvailableError();
523
+ }
524
+ return update;
388
525
  }
389
- return update;
390
- });
526
+ );
391
527
  if (!unsafe && !update.safeUpdate) {
392
- throw new Error(`The ${this.type} '${e}' has a major revision update ` +
393
- `(${update.current} => ${update.unsafeUpdate}), which could include ` +
394
- `breaking changes. If you want to apply this update, re-run with --unsafe`);
528
+ throw this._createFatalError(
529
+ `The ${this.type} '${e}' has a major revision update ` +
530
+ `(${update.current} => ${update.unsafeUpdate}), which could include ` +
531
+ `breaking changes. If you want to apply this update, re-run with --unsafe`
532
+ );
395
533
  }
396
534
  const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
397
535
  await spinWith(
@@ -405,22 +543,24 @@ class ExtensionCommand {
405
543
  }
406
544
  }
407
545
 
408
- log(this.isJsonOutput, 'Update report:');
546
+ this.log.info('Update report:');
547
+
409
548
  for (const [e, update] of _.toPairs(updates)) {
410
- log(this.isJsonOutput, `- ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
549
+ this.log.ok(` - ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
411
550
  }
551
+
412
552
  for (const [e, err] of _.toPairs(errors)) {
413
553
  if (err instanceof NotUpdatableError) {
414
- log(this.isJsonOutput, `- '${e}' was not installed via npm, so we could not check ` +
415
- `for updates`.yellow);
554
+ this.log.warn(
555
+ ` - '${e}' was not installed via npm, so we could not check ` + `for updates`.yellow
556
+ );
416
557
  } else if (err instanceof NoUpdatesAvailableError) {
417
- log(this.isJsonOutput, `- '${e}' had no updates available`.yellow);
558
+ this.log.info(` - '${e}' had no updates available`.yellow);
418
559
  } else {
419
560
  // otherwise, make it pop with red!
420
- log(this.isJsonOutput, `- '${e}' failed to update: ${err}`.red);
561
+ this.log.error(` - '${e}' failed to update: ${err}`.red);
421
562
  }
422
563
  }
423
-
424
564
  return {updates, errors};
425
565
  }
426
566
 
@@ -431,13 +571,17 @@ class ExtensionCommand {
431
571
  * @param {string} ext - name of extension
432
572
  * @return {Promise<PossibleUpdates>}
433
573
  */
434
- async checkForExtensionUpdate (ext) {
574
+ async checkForExtensionUpdate(ext) {
435
575
  // TODO decide how we want to handle beta versions?
436
576
  // this is a helper method, 'ext' is assumed to already be installed here, and of the npm
437
577
  // install type
438
578
  const {version, pkgName} = this.config.installedExtensions[ext];
439
579
  let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
440
- let safeUpdate = await npm.getLatestSafeUpgradeVersion(this.config.appiumHome, pkgName, version);
580
+ let safeUpdate = await npm.getLatestSafeUpgradeVersion(
581
+ this.config.appiumHome,
582
+ pkgName,
583
+ version
584
+ );
441
585
  if (!util.compareVersions(unsafeUpdate, '>', version)) {
442
586
  // the latest version is not greater than the current version, so there's no possible update
443
587
  unsafeUpdate = null;
@@ -458,16 +602,19 @@ class ExtensionCommand {
458
602
  * Actually update an extension installed by NPM, using the NPM cli. And update the installation
459
603
  * manifest.
460
604
  *
461
- * @param {string} ext - name of extension to update
605
+ * @param {string} installSpec - name of extension to update
462
606
  * @param {string} version - version string identifier to update extension to
463
607
  * @returns {Promise<void>}
464
608
  */
465
- async updateExtension (ext, version) {
466
- const {pkgName} = this.config.installedExtensions[ext];
467
- await fs.rimraf(this.config.getInstallPath(ext));
468
- const extData = await this.installViaNpm({ext, pkgName, pkgVer: version});
469
- delete extData[/** @type {string} */(`${this.type}Name`)];
470
- await this.config.updateExtension(ext, extData);
609
+ async updateExtension(installSpec, version) {
610
+ const {pkgName} = this.config.installedExtensions[installSpec];
611
+ const extData = await this.installViaNpm({
612
+ installSpec,
613
+ pkgName,
614
+ pkgVer: version,
615
+ });
616
+ delete extData[/** @type {string} */ (`${this.type}Name`)];
617
+ await this.config.updateExtension(installSpec, extData);
471
618
  }
472
619
 
473
620
  /**
@@ -481,50 +628,54 @@ class ExtensionCommand {
481
628
  * @param {RunOptions} opts
482
629
  * @return {Promise<RunOutput>}
483
630
  */
484
- async _run ({ext, scriptName}) {
485
- if (!_.has(this.config.installedExtensions, ext)) {
486
- throw new Error(`please install the ${this.type} first`);
631
+ async _run({installSpec, scriptName}) {
632
+ if (!this.config.isInstalled(installSpec)) {
633
+ throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
487
634
  }
488
635
 
489
- const extConfig = this.config.installedExtensions[ext];
636
+ const extConfig = this.config.installedExtensions[installSpec];
490
637
 
491
638
  // note: TS cannot understand that _.has() is a type guard
492
639
  if (!extConfig.scripts) {
493
- throw new Error(`The ${this.type} named '${ext}' does not contain the ` +
494
- `"scripts" field underneath the "appium" field in its package.json`);
640
+ throw this._createFatalError(
641
+ `The ${this.type} named '${installSpec}' does not contain the ` +
642
+ `"scripts" field underneath the "appium" field in its package.json`
643
+ );
495
644
  }
496
645
 
497
646
  const extScripts = extConfig.scripts;
498
647
 
499
648
  if (!_.isPlainObject(extScripts)) {
500
- throw new Error(`The ${this.type} named '${ext}' "scripts" field must be a plain object`);
649
+ throw this._createFatalError(
650
+ `The ${this.type} named '${installSpec}' "scripts" field must be a plain object`
651
+ );
501
652
  }
502
653
 
503
654
  if (!_.has(extScripts, scriptName)) {
504
- throw new Error(`The ${this.type} named '${ext}' does not support the script: '${scriptName}'`);
655
+ throw this._createFatalError(
656
+ `The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
657
+ );
505
658
  }
506
659
 
507
- const runner = new SubProcess(process.execPath, [
508
- extScripts[scriptName]
509
- ], {
510
- cwd: this.config.getInstallPath(ext)
660
+ const runner = new SubProcess(process.execPath, [extScripts[scriptName]], {
661
+ cwd: this.config.getInstallPath(installSpec),
511
662
  });
512
663
 
513
664
  const output = new RingBuffer(50);
514
665
 
515
666
  runner.on('stream-line', (line) => {
516
667
  output.enqueue(line);
517
- log(this.isJsonOutput, line);
668
+ this.log.log(line);
518
669
  });
519
670
 
520
671
  await runner.start(0);
521
672
 
522
673
  try {
523
674
  await runner.join();
524
- log(this.isJsonOutput, `${scriptName} successfully ran`.green);
675
+ this.log.ok(`${scriptName} successfully ran`.green);
525
676
  return {output: output.getBuff()};
526
677
  } catch (err) {
527
- log(this.isJsonOutput, `Encountered an error when running '${scriptName}': ${err.message}`.red);
678
+ this.log.error(`Encountered an error when running '${scriptName}': ${err.message}`.red);
528
679
  return {error: err.message, output: output.getBuff()};
529
680
  }
530
681
  }
@@ -552,14 +703,14 @@ export {ExtensionCommand};
552
703
  */
553
704
 
554
705
  /**
555
- * @typedef {import('../../types').ExtensionType} ExtensionType
556
- * @typedef {import('../../types').DriverType} DriverType
557
- * @typedef {import('../../types').PluginType} PluginType
706
+ * @typedef {import('@appium/types').ExtensionType} ExtensionType
707
+ * @typedef {import('@appium/types').DriverType} DriverType
708
+ * @typedef {import('@appium/types').PluginType} PluginType
558
709
  */
559
710
 
560
711
  /**
561
712
  * @template {ExtensionType} ExtType
562
- * @typedef {import('../../types/appium-manifest').ExtRecord<ExtType>} ExtRecord
713
+ * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
563
714
  */
564
715
 
565
716
  /**
@@ -569,17 +720,17 @@ export {ExtensionCommand};
569
720
 
570
721
  /**
571
722
  * @template {ExtensionType} ExtType
572
- * @typedef {import('../../types/external-manifest').ExtMetadata<ExtType>} ExtMetadata
723
+ * @typedef {import('appium/types').ExtMetadata<ExtType>} ExtMetadata
573
724
  */
574
725
 
575
726
  /**
576
727
  * @template {ExtensionType} ExtType
577
- * @typedef {import('../../types/appium-manifest').ExtManifest<ExtType>} ExtManifest
728
+ * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
578
729
  */
579
730
 
580
731
  /**
581
732
  * @template {ExtensionType} ExtType
582
- * @typedef {import('../../types/external-manifest').ExtPackageJson<ExtType>} ExtPackageJson
733
+ * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
583
734
  */
584
735
 
585
736
  /**
@@ -591,7 +742,7 @@ export {ExtensionCommand};
591
742
 
592
743
  /**
593
744
  * Possible return value for {@linkcode ExtensionCommand.list}
594
- * @typedef {import('../../types/appium-manifest').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
745
+ * @typedef {import('appium/types').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
595
746
  */
596
747
 
597
748
  /**
@@ -602,7 +753,7 @@ export {ExtensionCommand};
602
753
  /**
603
754
  * Options for {@linkcode ExtensionCommand._run}.
604
755
  * @typedef RunOptions
605
- * @property {string} ext - name of the extension to run a script from
756
+ * @property {string} installSpec - name of the extension to run a script from
606
757
  * @property {string} scriptName - name of the script to run
607
758
  */
608
759
 
@@ -617,7 +768,7 @@ export {ExtensionCommand};
617
768
  /**
618
769
  * Options for {@linkcode ExtensionCommand._update}.
619
770
  * @typedef ExtensionUpdateOpts
620
- * @property {string} ext - the name of the extension to update
771
+ * @property {string} installSpec - the name of the extension to update
621
772
  * @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
622
773
  */
623
774
 
@@ -638,7 +789,7 @@ export {ExtensionCommand};
638
789
  /**
639
790
  * Options for {@linkcode ExtensionCommand._uninstall}.
640
791
  * @typedef UninstallOpts
641
- * @property {string} ext - the name or spec of an extension to uninstall
792
+ * @property {string} installSpec - the name or spec of an extension to uninstall
642
793
  */
643
794
 
644
795
  /**
@@ -651,7 +802,7 @@ export {ExtensionCommand};
651
802
  /**
652
803
  * Options for {@linkcode ExtensionCommand.installViaNpm}
653
804
  * @typedef InstallViaNpmArgs
654
- * @property {string} ext - the name or spec of an extension to install
805
+ * @property {string} installSpec - the name or spec of an extension to install
655
806
  * @property {string} pkgName - the NPM package name of the extension
656
807
  * @property {string} [pkgVer] - the specific version of the NPM package
657
808
  */
@@ -667,18 +818,24 @@ export {ExtensionCommand};
667
818
  /**
668
819
  * Options for {@linkcode ExtensionCommand._install}
669
820
  * @typedef InstallArgs
670
- * @property {string} ext - the name or spec of an extension to install
671
- * @property {import('../../types/appium-manifest').InstallType} installType - how to install this extension. One of the INSTALL_TYPES
821
+ * @property {string} installSpec - the name or spec of an extension to install
822
+ * @property {import('appium/types').InstallType} installType - how to install this extension. One of the INSTALL_TYPES
672
823
  * @property {string} [packageName] - for git/github installs, the extension node package name
673
824
  */
674
825
 
675
826
  /**
676
827
  * Returned by {@linkcode ExtensionCommand.getExtensionFields}
677
828
  * @template {ExtensionType} ExtType
678
- * @typedef {ExtMetadata<ExtType> & { pkgName: string, version: string } & import('../../types/external-manifest').CommonMetadata} ExtensionFields
829
+ * @typedef {ExtMetadata<ExtType> & { pkgName: string, version: string, appiumVersion: string } & import('appium/types').CommonExtMetadata} ExtensionFields
679
830
  */
680
831
 
681
832
  /**
682
833
  * @template {ExtensionType} ExtType
683
834
  * @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
684
835
  */
836
+
837
+ /**
838
+ * @typedef ListOptions
839
+ * @property {boolean} showInstalled - whether should show only installed extensions
840
+ * @property {boolean} showUpdates - whether should show available updates
841
+ */