appium 2.0.0-beta.35 → 2.0.0-beta.39

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 (122) 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 +3 -3
  8. package/build/lib/cli/driver-command.d.ts.map +1 -1
  9. package/build/lib/cli/driver-command.js +1 -1
  10. package/build/lib/cli/extension-command.d.ts +60 -38
  11. package/build/lib/cli/extension-command.d.ts.map +1 -1
  12. package/build/lib/cli/extension-command.js +115 -59
  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 +1 -1
  20. package/build/lib/cli/plugin-command.d.ts.map +1 -1
  21. package/build/lib/cli/plugin-command.js +1 -1
  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 +10 -15
  84. package/lib/cli/extension-command.js +226 -175
  85. package/lib/cli/extension.js +15 -19
  86. package/lib/cli/parser.js +19 -31
  87. package/lib/cli/plugin-command.js +8 -8
  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/scripts/postinstall.js +0 -71
  122. package/types/extension.ts +0 -56
@@ -1,9 +1,9 @@
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';
5
+ import {npm, util, env, console} from '@appium/support';
6
+ import {spinWith, RingBuffer} from './utils';
7
7
  import {SubProcess} from 'teen_process';
8
8
  import {
9
9
  INSTALL_TYPE_NPM,
@@ -44,49 +44,56 @@ class ExtensionCommand {
44
44
  * Build an ExtensionCommand
45
45
  * @param {ExtensionCommandOptions<ExtType>} opts
46
46
  */
47
- constructor ({config, json}) {
47
+ constructor({config, json}) {
48
48
  this.config = config;
49
- this.isJsonOutput = json;
49
+ this.log = new console.CliConsole({jsonMode: json});
50
+ this.isJsonOutput = Boolean(json);
50
51
  }
51
52
 
52
53
  /**
53
54
  * `driver` or `plugin`, depending on the `ExtensionConfig`.
54
55
  */
55
- get type () {
56
+ get type() {
56
57
  return this.config.extensionType;
57
58
  }
58
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
+
59
74
  /**
60
75
  * Take a CLI parse and run an extension command based on its type
61
76
  *
62
77
  * @param {object} args - a key/value object with CLI flags and values
63
78
  * @return {Promise<object>} the result of the specific command which is executed
64
79
  */
65
- async execute (args) {
80
+ async execute(args) {
66
81
  const cmd = args[`${this.type}Command`];
67
82
  if (!_.isFunction(this[cmd])) {
68
- throw new Error(`Cannot handle ${this.type} command ${cmd}`);
83
+ throw this._createFatalError(`Cannot handle ${this.type} command ${cmd}`);
69
84
  }
70
85
  const executeCmd = this[cmd].bind(this);
71
86
  return await executeCmd(args);
72
87
  }
73
88
 
74
- /**
75
- * @typedef ListOptions
76
- * @property {boolean} showInstalled - whether should show only installed extensions
77
- * @property {boolean} showUpdates - whether should show available updates
78
- */
79
-
80
89
  /**
81
90
  * List extensions
82
91
  *
83
92
  * @param {ListOptions} opts
84
93
  * @return {Promise<ExtensionListData>} map of extension names to extension data
85
94
  */
86
- async list ({showInstalled, showUpdates}) {
87
- const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${
88
- this.type
89
- }s`;
95
+ async list({showInstalled, showUpdates}) {
96
+ const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
90
97
  const installedNames = Object.keys(this.config.installedExtensions);
91
98
  const knownNames = Object.keys(this.knownExtensions);
92
99
  const exts = [...installedNames, ...knownNames].reduce(
@@ -124,8 +131,7 @@ class ExtensionCommand {
124
131
  const updates = await this.checkForExtensionUpdate(ext);
125
132
  data.updateVersion = updates.safeUpdate;
126
133
  data.unsafeUpdateVersion = updates.unsafeUpdate;
127
- data.upToDate =
128
- updates.safeUpdate === null && updates.unsafeUpdate === null;
134
+ data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
129
135
  }
130
136
  });
131
137
 
@@ -143,14 +149,8 @@ class ExtensionCommand {
143
149
  let upToDateTxt = '';
144
150
  let unsafeUpdateTxt = '';
145
151
  if (data.installed) {
146
- const {
147
- installType,
148
- installSpec,
149
- updateVersion,
150
- unsafeUpdateVersion,
151
- version,
152
- upToDate,
153
- } = data;
152
+ const {installType, installSpec, updateVersion, unsafeUpdateVersion, version, upToDate} =
153
+ data;
154
154
  let typeTxt;
155
155
  switch (installType) {
156
156
  case INSTALL_TYPE_GIT:
@@ -163,9 +163,7 @@ class ExtensionCommand {
163
163
  default:
164
164
  typeTxt = '(NPM)';
165
165
  }
166
- installTxt = `@${version.yellow} ${
167
- ('[installed ' + typeTxt + ']').green
168
- }`;
166
+ installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
169
167
 
170
168
  if (showUpdates) {
171
169
  if (updateVersion) {
@@ -175,15 +173,12 @@ class ExtensionCommand {
175
173
  upToDateTxt = ` [Up to date]`.green;
176
174
  }
177
175
  if (unsafeUpdateVersion) {
178
- unsafeUpdateTxt =
179
- ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
176
+ unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
180
177
  }
181
178
  }
182
179
  }
183
180
 
184
- console.log(
185
- `- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`
186
- );
181
+ this.log.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
187
182
  }
188
183
 
189
184
  return listData;
@@ -195,53 +190,57 @@ class ExtensionCommand {
195
190
  * @param {InstallArgs} args
196
191
  * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
197
192
  */
198
- async _install ({installSpec, installType, packageName}) {
199
- /** @type {ExtensionFields<typeof this.type>} */
193
+ async _install({installSpec, installType, packageName}) {
194
+ /** @type {ExtensionFields<ExtType>} */
200
195
  let extData;
201
196
 
202
- if (
203
- packageName &&
204
- [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)
205
- ) {
206
- throw new Error(
207
- `When using --source=${installType}, cannot also use --package`
208
- );
197
+ if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
198
+ throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
209
199
  }
210
200
 
211
- if (
212
- !packageName &&
213
- [INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB].includes(installType)
214
- ) {
215
- throw new Error(
216
- `When using --source=${installType}, must also use --package`
217
- );
201
+ if (!packageName && [INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB].includes(installType)) {
202
+ throw this._createFatalError(`When using --source=${installType}, must also use --package`);
218
203
  }
219
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`
220
219
  if (installType === INSTALL_TYPE_GITHUB) {
221
220
  if (installSpec.split('/').length !== 2) {
222
- throw new Error(
221
+ throw this._createFatalError(
223
222
  `Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
224
223
  'it should be of the form <org>/<repo>'
225
224
  );
226
225
  }
227
- extData = await this.installViaNpm({
226
+ installOpts = {
228
227
  installSpec,
229
228
  pkgName: /** @type {string} */ (packageName),
230
- });
229
+ };
230
+ probableExtName = installSpec;
231
231
  } else if (installType === INSTALL_TYPE_GIT) {
232
232
  // git urls can have '.git' at the end, but this is not necessary and would complicate the
233
233
  // way we download and name directories, so we can just remove it
234
234
  installSpec = installSpec.replace(/\.git$/, '');
235
- extData = await this.installViaNpm({
235
+ installOpts = {
236
236
  installSpec,
237
237
  pkgName: /** @type {string} */ (packageName),
238
- });
238
+ };
239
+ probableExtName = installSpec;
239
240
  } else {
240
241
  let pkgName, pkgVer;
241
242
  if (installType === INSTALL_TYPE_LOCAL) {
242
- pkgName = path.isAbsolute(installSpec)
243
- ? installSpec
244
- : path.resolve(installSpec);
243
+ pkgName = path.isAbsolute(installSpec) ? installSpec : path.resolve(installSpec);
245
244
  } else {
246
245
  // at this point we have either an npm package or an appium verified extension
247
246
  // name or a local path. both of which will be installed via npm.
@@ -271,30 +270,68 @@ class ExtensionCommand {
271
270
  const msg =
272
271
  `Could not resolve ${this.type}; are you sure it's in the list ` +
273
272
  `of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
274
- throw new Error(msg);
273
+ throw this._createFatalError(msg);
275
274
  }
275
+ probableExtName = name;
276
276
  pkgName = this.knownExtensions[name];
277
277
  // given that we'll use the install type in the driver json, store it as
278
278
  // 'npm' now
279
279
  installType = INSTALL_TYPE_NPM;
280
280
  }
281
281
  }
282
+ installOpts = {installSpec, pkgName, pkgVer};
283
+ }
282
284
 
283
- extData = await this.installViaNpm({installSpec, 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
+ );
284
292
  }
285
293
 
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.
286
298
  const extName = extData[/** @type {string} */ (`${this.type}Name`)];
287
- delete extData[/** @type {string} */ (`${this.type}Name`)];
288
299
 
300
+ // check _a second time_ with the more-accurate extName
289
301
  if (this.config.isInstalled(extName)) {
290
- throw new Error(
291
- `A ${this.type} named '${extName}' is already installed. ` +
292
- `Did you mean to update? 'appium ${this.type} update'. See ` +
293
- `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".`
294
306
  );
295
307
  }
296
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>} */
297
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
+
298
335
  await this.config.addExtension(extName, extManifest);
299
336
 
300
337
  // update the if we've changed the local `package.json`
@@ -303,7 +340,7 @@ class ExtensionCommand {
303
340
  }
304
341
 
305
342
  // log info for the user
306
- log(this.isJsonOutput, this.getPostInstallText({extName, extData}));
343
+ this.log.info(this.getPostInstallText({extName, extData}));
307
344
 
308
345
  return this.config.installedExtensions;
309
346
  }
@@ -313,25 +350,22 @@ class ExtensionCommand {
313
350
  *
314
351
  * @param {InstallViaNpmArgs} args
315
352
  */
316
- async installViaNpm ({installSpec, pkgName, pkgVer}) {
353
+ async installViaNpm({installSpec, pkgName, pkgVer}) {
317
354
  const npmSpec = `${pkgName}${pkgVer ? '@' + pkgVer : ''}`;
318
- const specMsg =
319
- npmSpec === installSpec ? '' : ` using NPM install spec '${npmSpec}'`;
355
+ const specMsg = npmSpec === installSpec ? '' : ` using NPM install spec '${npmSpec}'`;
320
356
  const msg = `Installing '${installSpec}'${specMsg}`;
321
357
  try {
322
- const pkgJsonData = await spinWith(
323
- this.isJsonOutput,
324
- msg,
325
- async () =>
326
- await npm.installPackage(this.config.appiumHome, pkgName, {
327
- pkgVer,
328
- })
329
- );
330
- return this.getExtensionFields(pkgJsonData, installSpec);
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
+
366
+ return this.getExtensionFields(pkgJsonData);
331
367
  } catch (err) {
332
- throw new Error(
333
- `Encountered an error when installing package: ${err.message}`
334
- );
368
+ throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
335
369
  }
336
370
  }
337
371
 
@@ -343,8 +377,8 @@ class ExtensionCommand {
343
377
  * @returns {string}
344
378
  */
345
379
  // eslint-disable-next-line no-unused-vars
346
- getPostInstallText (args) {
347
- throw new Error('Must be implemented in final class');
380
+ getPostInstallText(args) {
381
+ throw this._createFatalError('Must be implemented in final class');
348
382
  }
349
383
 
350
384
  /**
@@ -353,27 +387,61 @@ class ExtensionCommand {
353
387
  * load as the main driver class, or to be able to detect incompatibilities between driver and
354
388
  * appium versions.
355
389
  *
356
- * @param {ExtPackageJson<ExtType>} pkgJsonData - the package.json data for a driver module, as if it had been straightforwardly 'require'd
357
- * @param {string} installSpec
390
+ * @param {ExtPackageJson<ExtType>} pkgJson - the package.json data for a driver module, as if it had been straightforwardly 'require'd
358
391
  * @returns {ExtensionFields<ExtType>}
359
392
  */
360
- getExtensionFields (pkgJsonData, installSpec) {
361
- if (!pkgJsonData.appium) {
362
- throw new Error(
363
- `Installed driver did not have an 'appium' section in its ` +
364
- `package.json file as expected`
365
- );
366
- }
367
- const {appium, name, version} = pkgJsonData;
368
- this.validateExtensionFields(appium, installSpec);
393
+ getExtensionFields(pkgJson) {
394
+ const {appium, name, version, peerDependencies} = pkgJson;
395
+
369
396
  /** @type {unknown} */
370
- const result = {...appium, pkgName: name, version};
397
+ const result = {
398
+ ...appium,
399
+ pkgName: name,
400
+ version,
401
+ appiumVersion: peerDependencies?.appium,
402
+ };
371
403
  return /** @type {ExtensionFields<ExtType>} */ (result);
372
404
  }
373
405
 
374
406
  /**
375
- * For any package.json fields which a particular type of extension requires, validate the
376
- * 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
+ }
377
445
 
378
446
  /**
379
447
  * For any `package.json` fields which a particular type of extension requires, validate the
@@ -384,32 +452,30 @@ class ExtensionCommand {
384
452
  * @param {string} installSpec - Extension name/spec
385
453
  */
386
454
  // eslint-disable-next-line no-unused-vars
387
- validateExtensionFields (extMetadata, installSpec) {
388
- throw new Error('Must be implemented in final class');
455
+ validateExtensionFields(extMetadata, installSpec) {
456
+ throw this._createFatalError('Must be implemented in final class');
389
457
  }
390
458
 
391
459
  /**
392
- * 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.
393
465
  *
394
466
  * @param {UninstallOpts} opts
395
- * @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)
396
468
  */
397
- async _uninstall ({installSpec}) {
469
+ async _uninstall({installSpec}) {
398
470
  if (!this.config.isInstalled(installSpec)) {
399
- throw new Error(
471
+ throw this._createFatalError(
400
472
  `Can't uninstall ${this.type} '${installSpec}'; it is not installed`
401
473
  );
402
474
  }
403
- const installPath = this.config.getInstallPath(installSpec);
404
- try {
405
- await fs.rimraf(installPath);
406
- } finally {
407
- await this.config.removeExtension(installSpec);
408
- }
409
- log(
410
- this.isJsonOutput,
411
- `Successfully uninstalled ${this.type} '${installSpec}'`.green
412
- );
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);
413
479
  return this.config.installedExtensions;
414
480
  }
415
481
 
@@ -419,12 +485,12 @@ class ExtensionCommand {
419
485
  * @param {ExtensionUpdateOpts} updateSpec
420
486
  * @return {Promise<ExtensionUpdateResult>}
421
487
  */
422
- async _update ({installSpec, unsafe}) {
488
+ async _update({installSpec, unsafe}) {
423
489
  const shouldUpdateAll = installSpec === UPDATE_ALL;
424
490
  // if we're specifically requesting an update for an extension, make sure it's installed
425
491
  if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
426
- throw new Error(
427
- `The ${this.type} '${installSpec}' was not installed, so can't be updated`
492
+ throw this._createFatalError(
493
+ `The ${this.type} "${installSpec}" was not installed, so can't be updated`
428
494
  );
429
495
  }
430
496
  const extsToUpdate = shouldUpdateAll
@@ -442,18 +508,11 @@ class ExtensionCommand {
442
508
 
443
509
  for (const e of extsToUpdate) {
444
510
  try {
445
- await spinWith(
446
- this.isJsonOutput,
447
- `Checking if ${this.type} '${e}' is updatable`,
448
- () => {
449
- if (
450
- this.config.installedExtensions[e].installType !==
451
- INSTALL_TYPE_NPM
452
- ) {
453
- throw new NotUpdatableError();
454
- }
511
+ await spinWith(this.isJsonOutput, `Checking if ${this.type} '${e}' is updatable`, () => {
512
+ if (this.config.installedExtensions[e].installType !== INSTALL_TYPE_NPM) {
513
+ throw new NotUpdatableError();
455
514
  }
456
- );
515
+ });
457
516
  const update = await spinWith(
458
517
  this.isJsonOutput,
459
518
  `Checking if ${this.type} '${e}' needs an update`,
@@ -466,16 +525,13 @@ class ExtensionCommand {
466
525
  }
467
526
  );
468
527
  if (!unsafe && !update.safeUpdate) {
469
- throw new Error(
528
+ throw this._createFatalError(
470
529
  `The ${this.type} '${e}' has a major revision update ` +
471
530
  `(${update.current} => ${update.unsafeUpdate}), which could include ` +
472
531
  `breaking changes. If you want to apply this update, re-run with --unsafe`
473
532
  );
474
533
  }
475
- const updateVer =
476
- unsafe && update.unsafeUpdate
477
- ? update.unsafeUpdate
478
- : update.safeUpdate;
534
+ const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
479
535
  await spinWith(
480
536
  this.isJsonOutput,
481
537
  `Updating driver '${e}' from ${update.current} to ${updateVer}`,
@@ -487,28 +543,24 @@ class ExtensionCommand {
487
543
  }
488
544
  }
489
545
 
490
- log(this.isJsonOutput, 'Update report:');
546
+ this.log.info('Update report:');
547
+
491
548
  for (const [e, update] of _.toPairs(updates)) {
492
- log(
493
- this.isJsonOutput,
494
- `- ${this.type} ${e} updated: ${update.from} => ${update.to}`.green
495
- );
549
+ this.log.ok(` - ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
496
550
  }
551
+
497
552
  for (const [e, err] of _.toPairs(errors)) {
498
553
  if (err instanceof NotUpdatableError) {
499
- log(
500
- this.isJsonOutput,
501
- `- '${e}' was not installed via npm, so we could not check ` +
502
- `for updates`.yellow
554
+ this.log.warn(
555
+ ` - '${e}' was not installed via npm, so we could not check ` + `for updates`.yellow
503
556
  );
504
557
  } else if (err instanceof NoUpdatesAvailableError) {
505
- log(this.isJsonOutput, `- '${e}' had no updates available`.yellow);
558
+ this.log.info(` - '${e}' had no updates available`.yellow);
506
559
  } else {
507
560
  // otherwise, make it pop with red!
508
- log(this.isJsonOutput, `- '${e}' failed to update: ${err}`.red);
561
+ this.log.error(` - '${e}' failed to update: ${err}`.red);
509
562
  }
510
563
  }
511
-
512
564
  return {updates, errors};
513
565
  }
514
566
 
@@ -519,15 +571,12 @@ class ExtensionCommand {
519
571
  * @param {string} ext - name of extension
520
572
  * @return {Promise<PossibleUpdates>}
521
573
  */
522
- async checkForExtensionUpdate (ext) {
574
+ async checkForExtensionUpdate(ext) {
523
575
  // TODO decide how we want to handle beta versions?
524
576
  // this is a helper method, 'ext' is assumed to already be installed here, and of the npm
525
577
  // install type
526
578
  const {version, pkgName} = this.config.installedExtensions[ext];
527
- let unsafeUpdate = await npm.getLatestVersion(
528
- this.config.appiumHome,
529
- pkgName
530
- );
579
+ let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
531
580
  let safeUpdate = await npm.getLatestSafeUpgradeVersion(
532
581
  this.config.appiumHome,
533
582
  pkgName,
@@ -557,9 +606,8 @@ class ExtensionCommand {
557
606
  * @param {string} version - version string identifier to update extension to
558
607
  * @returns {Promise<void>}
559
608
  */
560
- async updateExtension (installSpec, version) {
609
+ async updateExtension(installSpec, version) {
561
610
  const {pkgName} = this.config.installedExtensions[installSpec];
562
- await fs.rimraf(this.config.getInstallPath(installSpec));
563
611
  const extData = await this.installViaNpm({
564
612
  installSpec,
565
613
  pkgName,
@@ -580,16 +628,16 @@ class ExtensionCommand {
580
628
  * @param {RunOptions} opts
581
629
  * @return {Promise<RunOutput>}
582
630
  */
583
- async _run ({installSpec, scriptName}) {
584
- if (!_.has(this.config.installedExtensions, installSpec)) {
585
- 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`);
586
634
  }
587
635
 
588
636
  const extConfig = this.config.installedExtensions[installSpec];
589
637
 
590
638
  // note: TS cannot understand that _.has() is a type guard
591
639
  if (!extConfig.scripts) {
592
- throw new Error(
640
+ throw this._createFatalError(
593
641
  `The ${this.type} named '${installSpec}' does not contain the ` +
594
642
  `"scripts" field underneath the "appium" field in its package.json`
595
643
  );
@@ -598,13 +646,13 @@ class ExtensionCommand {
598
646
  const extScripts = extConfig.scripts;
599
647
 
600
648
  if (!_.isPlainObject(extScripts)) {
601
- throw new Error(
649
+ throw this._createFatalError(
602
650
  `The ${this.type} named '${installSpec}' "scripts" field must be a plain object`
603
651
  );
604
652
  }
605
653
 
606
654
  if (!_.has(extScripts, scriptName)) {
607
- throw new Error(
655
+ throw this._createFatalError(
608
656
  `The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
609
657
  );
610
658
  }
@@ -617,20 +665,17 @@ class ExtensionCommand {
617
665
 
618
666
  runner.on('stream-line', (line) => {
619
667
  output.enqueue(line);
620
- log(this.isJsonOutput, line);
668
+ this.log.log(line);
621
669
  });
622
670
 
623
671
  await runner.start(0);
624
672
 
625
673
  try {
626
674
  await runner.join();
627
- log(this.isJsonOutput, `${scriptName} successfully ran`.green);
675
+ this.log.ok(`${scriptName} successfully ran`.green);
628
676
  return {output: output.getBuff()};
629
677
  } catch (err) {
630
- log(
631
- this.isJsonOutput,
632
- `Encountered an error when running '${scriptName}': ${err.message}`.red
633
- );
678
+ this.log.error(`Encountered an error when running '${scriptName}': ${err.message}`.red);
634
679
  return {error: err.message, output: output.getBuff()};
635
680
  }
636
681
  }
@@ -658,14 +703,14 @@ export {ExtensionCommand};
658
703
  */
659
704
 
660
705
  /**
661
- * @typedef {import('../../types').ExtensionType} ExtensionType
662
- * @typedef {import('../../types').DriverType} DriverType
663
- * @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
664
709
  */
665
710
 
666
711
  /**
667
712
  * @template {ExtensionType} ExtType
668
- * @typedef {import('../../types/appium-manifest').ExtRecord<ExtType>} ExtRecord
713
+ * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
669
714
  */
670
715
 
671
716
  /**
@@ -675,17 +720,17 @@ export {ExtensionCommand};
675
720
 
676
721
  /**
677
722
  * @template {ExtensionType} ExtType
678
- * @typedef {import('../../types/external-manifest').ExtMetadata<ExtType>} ExtMetadata
723
+ * @typedef {import('appium/types').ExtMetadata<ExtType>} ExtMetadata
679
724
  */
680
725
 
681
726
  /**
682
727
  * @template {ExtensionType} ExtType
683
- * @typedef {import('../../types/appium-manifest').ExtManifest<ExtType>} ExtManifest
728
+ * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
684
729
  */
685
730
 
686
731
  /**
687
732
  * @template {ExtensionType} ExtType
688
- * @typedef {import('../../types/external-manifest').ExtPackageJson<ExtType>} ExtPackageJson
733
+ * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
689
734
  */
690
735
 
691
736
  /**
@@ -697,7 +742,7 @@ export {ExtensionCommand};
697
742
 
698
743
  /**
699
744
  * Possible return value for {@linkcode ExtensionCommand.list}
700
- * @typedef {import('../../types/appium-manifest').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
745
+ * @typedef {import('appium/types').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
701
746
  */
702
747
 
703
748
  /**
@@ -774,17 +819,23 @@ export {ExtensionCommand};
774
819
  * Options for {@linkcode ExtensionCommand._install}
775
820
  * @typedef InstallArgs
776
821
  * @property {string} installSpec - the name or spec of an extension to install
777
- * @property {import('../../types/appium-manifest').InstallType} installType - how to install this extension. One of the INSTALL_TYPES
822
+ * @property {import('appium/types').InstallType} installType - how to install this extension. One of the INSTALL_TYPES
778
823
  * @property {string} [packageName] - for git/github installs, the extension node package name
779
824
  */
780
825
 
781
826
  /**
782
827
  * Returned by {@linkcode ExtensionCommand.getExtensionFields}
783
828
  * @template {ExtensionType} ExtType
784
- * @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
785
830
  */
786
831
 
787
832
  /**
788
833
  * @template {ExtensionType} ExtType
789
834
  * @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
790
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
+ */