appium 2.0.0-beta.5 → 2.0.0-beta.53

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 (199) hide show
  1. package/README.md +156 -63
  2. package/build/lib/appium.d.ts +226 -0
  3. package/build/lib/appium.d.ts.map +1 -0
  4. package/build/lib/appium.js +694 -439
  5. package/build/lib/appium.js.map +1 -0
  6. package/build/lib/cli/args.d.ts +17 -0
  7. package/build/lib/cli/args.d.ts.map +1 -0
  8. package/build/lib/cli/args.js +253 -319
  9. package/build/lib/cli/args.js.map +1 -0
  10. package/build/lib/cli/driver-command.d.ts +102 -0
  11. package/build/lib/cli/driver-command.d.ts.map +1 -0
  12. package/build/lib/cli/driver-command.js +126 -81
  13. package/build/lib/cli/driver-command.js.map +1 -0
  14. package/build/lib/cli/extension-command.d.ts +385 -0
  15. package/build/lib/cli/extension-command.d.ts.map +1 -0
  16. package/build/lib/cli/extension-command.js +731 -383
  17. package/build/lib/cli/extension-command.js.map +1 -0
  18. package/build/lib/cli/extension.d.ts +23 -0
  19. package/build/lib/cli/extension.d.ts.map +1 -0
  20. package/build/lib/cli/extension.js +71 -54
  21. package/build/lib/cli/extension.js.map +1 -0
  22. package/build/lib/cli/parser.d.ts +84 -0
  23. package/build/lib/cli/parser.d.ts.map +1 -0
  24. package/build/lib/cli/parser.js +240 -128
  25. package/build/lib/cli/parser.js.map +1 -0
  26. package/build/lib/cli/plugin-command.d.ts +99 -0
  27. package/build/lib/cli/plugin-command.d.ts.map +1 -0
  28. package/build/lib/cli/plugin-command.js +120 -81
  29. package/build/lib/cli/plugin-command.js.map +1 -0
  30. package/build/lib/cli/utils.d.ts +29 -0
  31. package/build/lib/cli/utils.d.ts.map +1 -0
  32. package/build/lib/cli/utils.js +72 -51
  33. package/build/lib/cli/utils.js.map +1 -0
  34. package/build/lib/config-file.d.ts +100 -0
  35. package/build/lib/config-file.d.ts.map +1 -0
  36. package/build/lib/config-file.js +207 -0
  37. package/build/lib/config-file.js.map +1 -0
  38. package/build/lib/config.d.ts +49 -0
  39. package/build/lib/config.d.ts.map +1 -0
  40. package/build/lib/config.js +265 -201
  41. package/build/lib/config.js.map +1 -0
  42. package/build/lib/constants.d.ts +49 -0
  43. package/build/lib/constants.d.ts.map +1 -0
  44. package/build/lib/constants.js +66 -0
  45. package/build/lib/constants.js.map +1 -0
  46. package/build/lib/extension/driver-config.d.ts +82 -0
  47. package/build/lib/extension/driver-config.d.ts.map +1 -0
  48. package/build/lib/extension/driver-config.js +212 -0
  49. package/build/lib/extension/driver-config.js.map +1 -0
  50. package/build/lib/extension/extension-config.d.ts +249 -0
  51. package/build/lib/extension/extension-config.d.ts.map +1 -0
  52. package/build/lib/extension/extension-config.js +581 -0
  53. package/build/lib/extension/extension-config.js.map +1 -0
  54. package/build/lib/extension/index.d.ts +48 -0
  55. package/build/lib/extension/index.d.ts.map +1 -0
  56. package/build/lib/extension/index.js +105 -0
  57. package/build/lib/extension/index.js.map +1 -0
  58. package/build/lib/extension/manifest-migrations.d.ts +27 -0
  59. package/build/lib/extension/manifest-migrations.d.ts.map +1 -0
  60. package/build/lib/extension/manifest-migrations.js +118 -0
  61. package/build/lib/extension/manifest-migrations.js.map +1 -0
  62. package/build/lib/extension/manifest.d.ts +145 -0
  63. package/build/lib/extension/manifest.d.ts.map +1 -0
  64. package/build/lib/extension/manifest.js +521 -0
  65. package/build/lib/extension/manifest.js.map +1 -0
  66. package/build/lib/extension/package-changed.d.ts +11 -0
  67. package/build/lib/extension/package-changed.d.ts.map +1 -0
  68. package/build/lib/extension/package-changed.js +62 -0
  69. package/build/lib/extension/package-changed.js.map +1 -0
  70. package/build/lib/extension/plugin-config.d.ts +56 -0
  71. package/build/lib/extension/plugin-config.d.ts.map +1 -0
  72. package/build/lib/extension/plugin-config.js +102 -0
  73. package/build/lib/extension/plugin-config.js.map +1 -0
  74. package/build/lib/grid-register.d.ts +10 -0
  75. package/build/lib/grid-register.d.ts.map +1 -0
  76. package/build/lib/grid-register.js +122 -144
  77. package/build/lib/grid-register.js.map +1 -0
  78. package/build/lib/logger.d.ts +3 -0
  79. package/build/lib/logger.d.ts.map +1 -0
  80. package/build/lib/logger.js +5 -17
  81. package/build/lib/logger.js.map +1 -0
  82. package/build/lib/logsink.d.ts +4 -0
  83. package/build/lib/logsink.d.ts.map +1 -0
  84. package/build/lib/logsink.js +190 -187
  85. package/build/lib/logsink.js.map +1 -0
  86. package/build/lib/main.d.ts +62 -0
  87. package/build/lib/main.d.ts.map +1 -0
  88. package/build/lib/main.js +348 -228
  89. package/build/lib/main.js.map +1 -0
  90. package/build/lib/schema/arg-spec.d.ts +143 -0
  91. package/build/lib/schema/arg-spec.d.ts.map +1 -0
  92. package/build/lib/schema/arg-spec.js +164 -0
  93. package/build/lib/schema/arg-spec.js.map +1 -0
  94. package/build/lib/schema/cli-args.d.ts +19 -0
  95. package/build/lib/schema/cli-args.d.ts.map +1 -0
  96. package/build/lib/schema/cli-args.js +217 -0
  97. package/build/lib/schema/cli-args.js.map +1 -0
  98. package/build/lib/schema/cli-transformers.d.ts +5 -0
  99. package/build/lib/schema/cli-transformers.d.ts.map +1 -0
  100. package/build/lib/schema/cli-transformers.js +124 -0
  101. package/build/lib/schema/cli-transformers.js.map +1 -0
  102. package/build/lib/schema/index.d.ts +3 -0
  103. package/build/lib/schema/index.d.ts.map +1 -0
  104. package/build/lib/schema/index.js +19 -0
  105. package/build/lib/schema/index.js.map +1 -0
  106. package/build/lib/schema/keywords.d.ts +24 -0
  107. package/build/lib/schema/keywords.d.ts.map +1 -0
  108. package/build/lib/schema/keywords.js +128 -0
  109. package/build/lib/schema/keywords.js.map +1 -0
  110. package/build/lib/schema/schema.d.ts +259 -0
  111. package/build/lib/schema/schema.d.ts.map +1 -0
  112. package/build/lib/schema/schema.js +615 -0
  113. package/build/lib/schema/schema.js.map +1 -0
  114. package/build/lib/utils.d.ts +121 -0
  115. package/build/lib/utils.d.ts.map +1 -0
  116. package/build/lib/utils.js +351 -273
  117. package/build/lib/utils.js.map +1 -0
  118. package/build/tsconfig.tsbuildinfo +1 -0
  119. package/build/types/cli.d.ts +134 -0
  120. package/build/types/cli.d.ts.map +1 -0
  121. package/build/types/cli.js +3 -0
  122. package/build/types/cli.js.map +1 -0
  123. package/build/types/index.d.ts +15 -0
  124. package/build/types/index.d.ts.map +1 -0
  125. package/build/types/index.js +19 -0
  126. package/build/types/index.js.map +1 -0
  127. package/build/types/manifest/base.d.ts +135 -0
  128. package/build/types/manifest/base.d.ts.map +1 -0
  129. package/build/types/manifest/base.js +3 -0
  130. package/build/types/manifest/base.js.map +1 -0
  131. package/build/types/manifest/index.d.ts +19 -0
  132. package/build/types/manifest/index.d.ts.map +1 -0
  133. package/build/types/manifest/index.js +40 -0
  134. package/build/types/manifest/index.js.map +1 -0
  135. package/build/types/manifest/v3.d.ts +139 -0
  136. package/build/types/manifest/v3.d.ts.map +1 -0
  137. package/build/types/manifest/v3.js +3 -0
  138. package/build/types/manifest/v3.js.map +1 -0
  139. package/driver.d.ts +1 -0
  140. package/driver.js +14 -0
  141. package/index.js +11 -0
  142. package/lib/appium.js +535 -186
  143. package/lib/cli/args.js +267 -422
  144. package/lib/cli/driver-command.js +126 -23
  145. package/lib/cli/extension-command.js +679 -271
  146. package/lib/cli/extension.js +48 -17
  147. package/lib/cli/parser.js +263 -83
  148. package/lib/cli/plugin-command.js +115 -20
  149. package/lib/cli/utils.js +24 -10
  150. package/lib/config-file.js +220 -0
  151. package/lib/config.js +244 -110
  152. package/lib/constants.js +71 -0
  153. package/lib/extension/driver-config.js +250 -0
  154. package/lib/extension/extension-config.js +682 -0
  155. package/lib/extension/index.js +116 -0
  156. package/lib/extension/manifest-migrations.js +120 -0
  157. package/lib/extension/manifest.js +573 -0
  158. package/lib/extension/package-changed.js +64 -0
  159. package/lib/extension/plugin-config.js +112 -0
  160. package/lib/grid-register.js +49 -35
  161. package/lib/logger.js +1 -2
  162. package/lib/logsink.js +64 -38
  163. package/lib/main.js +321 -101
  164. package/lib/schema/arg-spec.js +229 -0
  165. package/lib/schema/cli-args.js +238 -0
  166. package/lib/schema/cli-transformers.js +119 -0
  167. package/lib/schema/index.js +2 -0
  168. package/lib/schema/keywords.js +136 -0
  169. package/lib/schema/schema.js +722 -0
  170. package/lib/utils.js +291 -167
  171. package/package.json +83 -84
  172. package/plugin.d.ts +1 -0
  173. package/plugin.js +13 -0
  174. package/scripts/autoinstall-extensions.js +237 -0
  175. package/support.d.ts +1 -0
  176. package/support.js +13 -0
  177. package/tsconfig.json +25 -0
  178. package/types/cli.ts +193 -0
  179. package/types/index.ts +20 -0
  180. package/types/manifest/README.md +30 -0
  181. package/types/manifest/base.ts +158 -0
  182. package/types/manifest/index.ts +27 -0
  183. package/types/manifest/v3.ts +161 -0
  184. package/CHANGELOG.md +0 -3515
  185. package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
  186. package/build/lib/cli/npm.js +0 -208
  187. package/build/lib/cli/parser-helpers.js +0 -82
  188. package/build/lib/driver-config.js +0 -77
  189. package/build/lib/drivers.js +0 -96
  190. package/build/lib/extension-config.js +0 -253
  191. package/build/lib/plugin-config.js +0 -59
  192. package/build/lib/plugins.js +0 -14
  193. package/lib/cli/npm.js +0 -184
  194. package/lib/cli/parser-helpers.js +0 -79
  195. package/lib/driver-config.js +0 -46
  196. package/lib/drivers.js +0 -81
  197. package/lib/extension-config.js +0 -209
  198. package/lib/plugin-config.js +0 -34
  199. package/lib/plugins.js +0 -10
@@ -1,82 +1,131 @@
1
1
  /* eslint-disable no-console */
2
-
2
+ import B from 'bluebird';
3
3
  import _ from 'lodash';
4
- import NPM from './npm';
5
4
  import path from 'path';
6
- import { fs, util } from 'appium-support';
7
- import { log, spinWith } from './utils';
8
- import { INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB,
9
- INSTALL_TYPE_LOCAL } from '../extension-config';
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';
10
15
 
11
16
  const UPDATE_ALL = 'installed';
12
17
 
13
18
  class NotUpdatableError extends Error {}
14
19
  class NoUpdatesAvailableError extends Error {}
15
20
 
16
- export default class ExtensionCommand {
21
+ /**
22
+ * Omits `driverName`/`pluginName` props from the receipt to make a {@linkcode ExtManifest}
23
+ * @template {ExtensionType} ExtType
24
+ * @param {ExtInstallReceipt<ExtType>} receipt
25
+ * @returns {ExtManifest<ExtType>}
26
+ */
27
+ function receiptToManifest(receipt) {
28
+ return /** @type {ExtManifest<ExtType>} */ (_.omit(receipt, 'driverName', 'pluginName'));
29
+ }
30
+
31
+ /**
32
+ * @template {ExtensionType} ExtType
33
+ */
34
+ class ExtensionCommand {
35
+ /**
36
+ * This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
37
+ * @type {ExtensionConfig<ExtType>}
38
+ */
39
+ config;
40
+
41
+ /**
42
+ * {@linkcode Record} of official plugins or drivers.
43
+ * @type {KnownExtensions<ExtType>}
44
+ */
45
+ knownExtensions;
17
46
 
18
47
  /**
19
- * @typedef {Object} ExtensionCommandConstructor
20
- * @property {Object} config - the DriverConfig or PluginConfig object used for this command
21
- * @property {boolean} json - whether the output of this command should be JSON or text
22
- * @property {string} type - DRIVER_TYPE or PLUGIN_TYPE
48
+ * If `true`, command output has been requested as JSON.
49
+ * @type {boolean}
23
50
  */
51
+ isJsonOutput;
24
52
 
25
53
  /**
26
54
  * Build an ExtensionCommand
27
- *
28
- * @param {ExtensionCommandConstructor} opts
29
- * @return {ExtensionCommand}
55
+ * @param {ExtensionCommandOptions<ExtType>} opts
30
56
  */
31
- constructor ({config, json, type}) {
57
+ constructor({config, json}) {
32
58
  this.config = config;
33
- this.type = type;
34
- this.isJsonOutput = json;
35
- this.npm = new NPM(this.config.appiumHome);
36
- this.knownExtensions = {}; // this needs to be overridden in final class
59
+ this.log = new console.CliConsole({jsonMode: json});
60
+ this.isJsonOutput = Boolean(json);
61
+ }
62
+
63
+ /**
64
+ * `driver` or `plugin`, depending on the `ExtensionConfig`.
65
+ */
66
+ get type() {
67
+ return this.config.extensionType;
68
+ }
69
+
70
+ /**
71
+ * Logs a message and returns an {@linkcode Error} to throw.
72
+ *
73
+ * For TS to understand that a function throws an exception, it must actually throw an exception--
74
+ * in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
75
+ * nor is something like `@returns {never}` which does not imply a thrown exception.
76
+ * @param {string} message
77
+ * @protected
78
+ * @throws {Error}
79
+ */
80
+ _createFatalError(message) {
81
+ return new Error(this.log.decorate(message, 'error'));
37
82
  }
38
83
 
39
84
  /**
40
85
  * Take a CLI parse and run an extension command based on its type
41
86
  *
42
87
  * @param {object} args - a key/value object with CLI flags and values
43
- * @return {object} the result of the specific command which is executed
88
+ * @return {Promise<object>} the result of the specific command which is executed
44
89
  */
45
- async execute (args) {
90
+ async execute(args) {
46
91
  const cmd = args[`${this.type}Command`];
47
- if (!_.isFunction(ExtensionCommand.prototype[cmd])) {
48
- throw new Error(`Cannot handle ${this.type} command ${cmd}`);
92
+ if (!_.isFunction(this[cmd])) {
93
+ throw this._createFatalError(`Cannot handle ${this.type} command ${cmd}`);
49
94
  }
50
95
  const executeCmd = this[cmd].bind(this);
51
96
  return await executeCmd(args);
52
97
  }
53
98
 
54
- /**
55
- * @typedef {Object} ListArgs
56
- * @property {boolean} showInstalled - whether should show only installed extensions
57
- * @property {boolean} showUpdates - whether should show available updates
58
- */
59
-
60
99
  /**
61
100
  * List extensions
62
101
  *
63
- * @param {ListArgs} args
64
- * @return {object} map of extension names to extension data
102
+ * @param {ListOptions} opts
103
+ * @return {Promise<ExtensionListData>} map of extension names to extension data
65
104
  */
66
- async list ({showInstalled, showUpdates}) {
105
+ async list({showInstalled, showUpdates}) {
67
106
  const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
68
107
  const installedNames = Object.keys(this.config.installedExtensions);
69
108
  const knownNames = Object.keys(this.knownExtensions);
70
- const exts = [...installedNames, ...knownNames].reduce((acc, name) => {
71
- if (!acc[name]) {
72
- if (installedNames.includes(name)) {
73
- acc[name] = {...this.config.installedExtensions[name], installed: true};
74
- } else if (!showInstalled) {
75
- acc[name] = {pkgName: this.knownExtensions[name], installed: false};
109
+ const exts = [...installedNames, ...knownNames].reduce(
110
+ (acc, name) => {
111
+ if (!acc[name]) {
112
+ if (installedNames.includes(name)) {
113
+ acc[name] = {
114
+ ...this.config.installedExtensions[name],
115
+ installed: true,
116
+ };
117
+ } else if (!showInstalled) {
118
+ acc[name] = {pkgName: this.knownExtensions[name], installed: false};
119
+ }
76
120
  }
77
- }
78
- return acc;
79
- }, {});
121
+ return acc;
122
+ },
123
+ /**
124
+ * This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
125
+ * {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
126
+ * @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
127
+ */ ({})
128
+ );
80
129
 
81
130
  // if we want to show whether updates are available, put that behind a spinner
82
131
  await spinWith(this.isJsonOutput, lsMsg, async () => {
@@ -84,306 +133,417 @@ export default class ExtensionCommand {
84
133
  return;
85
134
  }
86
135
  for (const [ext, data] of _.toPairs(exts)) {
87
- const {installed, installType} = data;
88
- if (!installed || installType !== INSTALL_TYPE_NPM) {
136
+ if (!data.installed || data.installType !== INSTALL_TYPE_NPM) {
89
137
  // don't need to check for updates on exts that aren't installed
90
138
  // also don't need to check for updates on non-npm exts
91
139
  continue;
92
140
  }
93
- const updates = await this.checkForExtensionUpdate(ext);
94
- data.updateVersion = updates.safeUpdate;
95
- data.unsafeUpdateVersion = updates.unsafeUpdate;
96
- data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
141
+ try {
142
+ const updates = await this.checkForExtensionUpdate(ext);
143
+ data.updateVersion = updates.safeUpdate;
144
+ data.unsafeUpdateVersion = updates.unsafeUpdate;
145
+ data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
146
+ } catch (e) {
147
+ data.updateError = e.message;
148
+ }
97
149
  }
98
150
  });
99
151
 
152
+ const listData = /** @type {ExtensionListData} */ (exts);
153
+
100
154
  // if we're just getting the data, short circuit return here since we don't need to do any
101
155
  // formatting logic
102
156
  if (this.isJsonOutput) {
103
- return exts;
157
+ return listData;
104
158
  }
105
159
 
106
- for (const [
107
- name,
108
- {installType, installSpec, installed, updateVersion, unsafeUpdateVersion, version, upToDate}
109
- ] of _.toPairs(exts)) {
110
- let typeTxt;
111
- switch (installType) {
112
- case INSTALL_TYPE_GIT:
113
- case INSTALL_TYPE_GITHUB:
114
- typeTxt = `(cloned from ${installSpec})`.yellow;
115
- break;
116
- case INSTALL_TYPE_LOCAL:
117
- typeTxt = `(linked from ${installSpec})`.magenta;
118
- break;
119
- default:
120
- typeTxt = '(NPM)';
160
+ for (const [name, data] of _.toPairs(listData)) {
161
+ let installTxt = ' [not installed]'.grey;
162
+ let updateTxt = '';
163
+ let upToDateTxt = '';
164
+ let unsafeUpdateTxt = '';
165
+ if (data.installed) {
166
+ const {
167
+ installType,
168
+ installSpec,
169
+ updateVersion,
170
+ unsafeUpdateVersion,
171
+ version,
172
+ upToDate,
173
+ updateError,
174
+ } = data;
175
+ let typeTxt;
176
+ switch (installType) {
177
+ case INSTALL_TYPE_GIT:
178
+ case INSTALL_TYPE_GITHUB:
179
+ typeTxt = `(cloned from ${installSpec})`.yellow;
180
+ break;
181
+ case INSTALL_TYPE_LOCAL:
182
+ typeTxt = `(linked from ${installSpec})`.magenta;
183
+ break;
184
+ default:
185
+ typeTxt = '(NPM)';
186
+ }
187
+ installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
188
+
189
+ if (showUpdates) {
190
+ if (updateError) {
191
+ updateTxt = ` [Cannot check for updates: ${updateError}]`.red;
192
+ } else {
193
+ if (updateVersion) {
194
+ updateTxt = ` [${updateVersion} available]`.magenta;
195
+ }
196
+ if (upToDate) {
197
+ upToDateTxt = ` [Up to date]`.green;
198
+ }
199
+ if (unsafeUpdateVersion) {
200
+ unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
201
+ }
202
+ }
203
+ }
121
204
  }
122
- const installTxt = installed ?
123
- `@${version.yellow} ${('[installed ' + typeTxt + ']').green}` :
124
- ' [not installed]'.grey;
125
- const updateTxt = showUpdates && updateVersion ?
126
- ` [${updateVersion} available]`.magenta :
127
- '';
128
- const upToDateTxt = showUpdates && upToDate ?
129
- ` [Up to date]`.green :
130
- '';
131
- const unsafeUpdateTxt = showUpdates && unsafeUpdateVersion ?
132
- ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan :
133
- '';
134
-
135
- console.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
205
+
206
+ this.log.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
136
207
  }
137
208
 
138
- return exts;
209
+ return listData;
139
210
  }
140
211
 
141
- /**
142
- * @typedef {Object} InstallArgs
143
- * @property {string} ext - the name or spec of an extension to install
144
- * @property {string} installType - how to install this extension. One of the INSTALL_TYPES
145
- * @property {string} [packageName] - for git/github installs, the extension node package name
146
- */
147
-
148
212
  /**
149
213
  * Install an extension
150
214
  *
151
- * @param {InstallArgs} args
152
- * @return {object} map of all installed extension names to extension data
215
+ * @param {InstallOpts} opts
216
+ * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
153
217
  */
154
- async install ({ext, installType, packageName}) {
155
- log(this.isJsonOutput, `Attempting to find and install ${this.type} '${ext}'`);
156
-
157
- let extData;
158
- let installSpec = ext;
218
+ async _install({installSpec, installType, packageName}) {
219
+ /** @type {ExtInstallReceipt<ExtType>} */
220
+ let receipt;
159
221
 
160
222
  if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
161
- throw new Error(`When using --source=${installType}, cannot also use --package`);
223
+ throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
162
224
  }
163
225
 
164
226
  if (!packageName && [INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB].includes(installType)) {
165
- throw new Error(`When using --source=${installType}, must also use --package`);
227
+ throw this._createFatalError(`When using --source=${installType}, must also use --package`);
166
228
  }
167
229
 
168
- if (installType === INSTALL_TYPE_LOCAL) {
169
- const msg = `Linking ${this.type} from local path`;
170
- const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => (
171
- await this.npm.linkPackage(installSpec))
172
- );
173
- extData = this.getExtensionFields(pkgJsonData);
174
- extData.installPath = extData.pkgName;
175
- } else if (installType === INSTALL_TYPE_GITHUB) {
230
+ /**
231
+ * @type {InstallViaNpmArgs}
232
+ */
233
+ let installViaNpmOpts;
234
+
235
+ /**
236
+ * The probable (?) name of the extension derived from the install spec.
237
+ *
238
+ * If using a local install type, this will remain empty.
239
+ * @type {string}
240
+ */
241
+ let probableExtName = '';
242
+
243
+ // depending on `installType`, build the options to pass into `installViaNpm`
244
+ if (installType === INSTALL_TYPE_GITHUB) {
176
245
  if (installSpec.split('/').length !== 2) {
177
- throw new Error(`Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
178
- 'it should be of the form <org>/<repo>');
246
+ throw this._createFatalError(
247
+ `Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
248
+ 'it should be of the form <org>/<repo>'
249
+ );
179
250
  }
180
- extData = await this.installViaNpm({ext: installSpec, pkgName: packageName});
251
+ installViaNpmOpts = {
252
+ installSpec,
253
+ installType,
254
+ pkgName: /** @type {string} */ (packageName),
255
+ };
256
+ probableExtName = installSpec;
181
257
  } else if (installType === INSTALL_TYPE_GIT) {
182
258
  // git urls can have '.git' at the end, but this is not necessary and would complicate the
183
259
  // way we download and name directories, so we can just remove it
184
260
  installSpec = installSpec.replace(/\.git$/, '');
185
- extData = await this.installViaNpm({ext: installSpec, pkgName: packageName});
261
+ installViaNpmOpts = {
262
+ installSpec,
263
+ installType,
264
+ pkgName: /** @type {string} */ (packageName),
265
+ };
266
+ probableExtName = installSpec;
186
267
  } else {
187
- // at this point we have either an npm package or an appium verified extension
188
- // name. both of which will be installed via npm.
189
- // extensions installed via npm can include versions or tags after the '@'
190
- // sign, so check for that. We also need to be careful that package names themselves can
191
- // contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
192
- let name, pkgVer;
193
- const splits = installSpec.split('@');
194
- if (installSpec[0] === '@') {
195
- // this is the case where we have an npm org included in the package name
196
- [name, pkgVer] = [`@${splits[1]}`, splits[2]];
268
+ let pkgName, pkgVer;
269
+ if (installType === INSTALL_TYPE_LOCAL) {
270
+ pkgName = path.isAbsolute(installSpec) ? installSpec : path.resolve(installSpec);
197
271
  } else {
198
- // this is the case without an npm org
199
- [name, pkgVer] = splits;
200
- }
201
- let pkgName;
272
+ // at this point we have either an npm package or an appium verified extension
273
+ // name or a local path. both of which will be installed via npm.
274
+ // extensions installed via npm can include versions or tags after the '@'
275
+ // sign, so check for that. We also need to be careful that package names themselves can
276
+ // contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
277
+ let name;
278
+ const splits = installSpec.split('@');
279
+ if (installSpec[0] === '@') {
280
+ // this is the case where we have an npm org included in the package name
281
+ [name, pkgVer] = [`@${splits[1]}`, splits[2]];
282
+ } else {
283
+ // this is the case without an npm org
284
+ [name, pkgVer] = splits;
285
+ }
202
286
 
203
- if (installType === INSTALL_TYPE_NPM) {
204
- // if we're installing a named package from npm, we don't need to check
205
- // against the appium extension list; just use the installSpec as is
206
- pkgName = name;
207
- } else {
208
- // if we're installing a named appium driver (like 'xcuitest') we need to
209
- // dereference the actual npm package ('appiupm-xcuitest-driver'), so
210
- // check it exists and get the correct package
211
- const knownNames = Object.keys(this.knownExtensions);
212
- if (!_.includes(knownNames, name)) {
213
- const msg = `Could not resolve ${this.type}; are you sure it's in the list ` +
214
- `of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
215
- throw new Error(msg);
287
+ if (installType === INSTALL_TYPE_NPM) {
288
+ // if we're installing a named package from npm, we don't need to check
289
+ // against the appium extension list; just use the installSpec as is
290
+ pkgName = name;
291
+ } else {
292
+ // if we're installing a named appium driver (like 'xcuitest') we need to
293
+ // dereference the actual npm package ('appiupm-xcuitest-driver'), so
294
+ // check it exists and get the correct package
295
+ const knownNames = Object.keys(this.knownExtensions);
296
+ if (!_.includes(knownNames, name)) {
297
+ const msg =
298
+ `Could not resolve ${this.type}; are you sure it's in the list ` +
299
+ `of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
300
+ throw this._createFatalError(msg);
301
+ }
302
+ probableExtName = name;
303
+ pkgName = this.knownExtensions[name];
304
+ // given that we'll use the install type in the driver json, store it as
305
+ // 'npm' now
306
+ installType = INSTALL_TYPE_NPM;
216
307
  }
217
- pkgName = this.knownExtensions[name];
218
- // given that we'll use the install type in the driver json, store it as
219
- // 'npm' now
220
- installType = INSTALL_TYPE_NPM;
221
308
  }
309
+ installViaNpmOpts = {installSpec, pkgName, pkgVer, installType};
310
+ }
222
311
 
223
- extData = await this.installViaNpm({ext, pkgName, pkgVer});
312
+ // fail fast here if we can
313
+ if (probableExtName && this.config.isInstalled(probableExtName)) {
314
+ throw this._createFatalError(
315
+ `A ${this.type} named "${probableExtName}" is already installed. ` +
316
+ `Did you mean to update? Run "appium ${this.type} update". See ` +
317
+ `installed ${this.type}s with "appium ${this.type} list --installed".`
318
+ );
224
319
  }
225
320
 
226
- const extName = extData[`${this.type}Name`];
227
- delete extData[`${this.type}Name`];
321
+ receipt = await this.installViaNpm(installViaNpmOpts);
228
322
 
323
+ // this _should_ be the same as `probablyExtName` as the one derived above unless
324
+ // install type is local.
325
+ /** @type {string} */
326
+ const extName = receipt[/** @type {string} */ (`${this.type}Name`)];
327
+
328
+ // check _a second time_ with the more-accurate extName
229
329
  if (this.config.isInstalled(extName)) {
230
- throw new Error(`A ${this.type} named '${extName}' is already installed. ` +
231
- `Did you mean to update? 'appium ${this.type} update'. See ` +
232
- `installed ${this.type}s with 'appium ${this.type} list --installed'.`);
330
+ throw this._createFatalError(
331
+ `A ${this.type} named "${extName}" is already installed. ` +
332
+ `Did you mean to update? Run "appium ${this.type} update". See ` +
333
+ `installed ${this.type}s with "appium ${this.type} list --installed".`
334
+ );
335
+ }
336
+
337
+ // this field does not exist as such in the manifest (it's used as a property name instead)
338
+ // so that's why it's being removed here.
339
+ /** @type {ExtManifest<ExtType>} */
340
+ const extManifest = receiptToManifest(receipt);
341
+
342
+ const [errors, warnings] = await B.all([
343
+ this.config.getProblems(extName, extManifest),
344
+ this.config.getWarnings(extName, extManifest),
345
+ ]);
346
+ const errorMap = new Map([[extName, errors]]);
347
+ const warningMap = new Map([[extName, warnings]]);
348
+ const {errorSummaries, warningSummaries} = this.config.getValidationResultSummaries(
349
+ errorMap,
350
+ warningMap
351
+ );
352
+
353
+ if (!_.isEmpty(errorSummaries)) {
354
+ throw this._createFatalError(errorSummaries.join('\n'));
355
+ }
356
+
357
+ // note that we won't show any warnings if there were errors.
358
+ if (!_.isEmpty(warningSummaries)) {
359
+ this.log.warn(warningSummaries.join('\n'));
233
360
  }
234
361
 
235
- extData.installType = installType;
236
- extData.installSpec = installSpec;
237
- await this.config.addExtension(extName, extData);
362
+ await this.config.addExtension(extName, extManifest);
363
+
364
+ // update the hash if we've changed the local `package.json`
365
+ if (await env.hasAppiumDependency(this.config.appiumHome)) {
366
+ await packageDidChange(this.config.appiumHome);
367
+ }
238
368
 
239
369
  // log info for the user
240
- log(this.isJsonOutput, this.getPostInstallText({extName, extData}));
370
+ this.log.info(this.getPostInstallText({extName, extData: receipt}));
241
371
 
242
372
  return this.config.installedExtensions;
243
373
  }
244
374
 
245
- /**
246
- * @typedef {Object} InstallViaNpmArgs
247
- * @property {string} ext - the name or spec of an extension to install
248
- * @property {string} pkgName - the NPM package name of the extension
249
- * @property {string} [pkgVer] - the specific version of the NPM package
250
- */
251
-
252
375
  /**
253
376
  * Install an extension via NPM
254
377
  *
255
378
  * @param {InstallViaNpmArgs} args
379
+ * @returns {Promise<ExtInstallReceipt<ExtType>>}
256
380
  */
257
- async installViaNpm ({ext, pkgName, pkgVer}) {
381
+ async installViaNpm({installSpec, pkgName, pkgVer, installType}) {
258
382
  const npmSpec = `${pkgName}${pkgVer ? '@' + pkgVer : ''}`;
259
- const specMsg = npmSpec === ext ? '' : ` using NPM install spec '${npmSpec}'`;
260
- const msg = `Installing '${ext}'${specMsg}`;
383
+ const specMsg = npmSpec === installSpec ? '' : ` using NPM install spec '${npmSpec}'`;
384
+ const msg = `Installing '${installSpec}'${specMsg}`;
261
385
  try {
262
- const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => (
263
- await this.npm.installPackage({
264
- pkgDir: path.resolve(this.config.appiumHome, pkgName),
265
- pkgName,
266
- pkgVer
267
- })
268
- ));
269
- const extData = this.getExtensionFields(pkgJsonData);
270
- extData.installPath = pkgName;
271
- return extData;
386
+ const {pkg, path} = await spinWith(this.isJsonOutput, msg, async () => {
387
+ const {pkg, installPath: path} = await npm.installPackage(this.config.appiumHome, pkgName, {
388
+ pkgVer,
389
+ installType,
390
+ });
391
+ this.validatePackageJson(pkg, installSpec);
392
+ return {pkg, path};
393
+ });
394
+
395
+ return this.getInstallationReceipt({
396
+ pkg,
397
+ installPath: path,
398
+ installType,
399
+ installSpec,
400
+ });
272
401
  } catch (err) {
273
- throw new Error(`Encountered an error when installing package: ${err.message}`);
402
+ throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
274
403
  }
275
404
  }
276
405
 
277
- /**
278
- * @typedef {Object} ExtensionArgs
279
- * @property {string} extName - the name of an extension
280
- * @property {object} extData - the data for an installed extension
281
- */
282
-
283
406
  /**
284
407
  * Get the text which should be displayed to the user after an extension has been installed. This
285
408
  * is designed to be overridden by drivers/plugins with their own particular text.
286
409
  *
287
410
  * @param {ExtensionArgs} args
411
+ * @returns {string}
288
412
  */
289
- getPostInstallText (/*{extName, extData}*/) {
290
- throw new Error('Must be implemented in final class');
413
+ // eslint-disable-next-line no-unused-vars
414
+ getPostInstallText(args) {
415
+ throw this._createFatalError('Must be implemented in final class');
291
416
  }
292
417
 
293
418
  /**
294
- * Take an NPM module's package.json and extract Appium driver information from a special
295
- * 'appium' field in the JSON data. We need this information to e.g. determine which class to
296
- * load as the main driver class, or to be able to detect incompatibilities between driver and
297
- * appium versions.
419
+ * Once a package is installed on-disk, this gathers some necessary metadata for validation.
298
420
  *
299
- * @param {object} pkgJsonData - the package.json data for a driver module, as if it had been
300
- * straightforwardly 'require'd
421
+ * @param {GetInstallationReceiptOpts<ExtType>} opts
422
+ * @returns {ExtInstallReceipt<ExtType>}
301
423
  */
302
- getExtensionFields (pkgJsonData) {
303
- if (!pkgJsonData.appium) {
304
- throw new Error(`Installed driver did not have an 'appium' section in its ` +
305
- `package.json file as expected`);
306
- }
307
- const {appium, name, version} = pkgJsonData;
308
- this.validateExtensionFields(appium);
309
-
310
- return {...appium, pkgName: name, version};
424
+ getInstallationReceipt({pkg, installPath, installType, installSpec}) {
425
+ const {appium, name, version, peerDependencies} = pkg;
426
+
427
+ /** @type {import('appium/types').InternalMetadata} */
428
+ const internal = {
429
+ pkgName: name,
430
+ version,
431
+ installType,
432
+ installSpec,
433
+ installPath,
434
+ appiumVersion: peerDependencies?.appium,
435
+ };
436
+
437
+ /** @type {ExtMetadata<ExtType>} */
438
+ const extMetadata = appium;
439
+
440
+ return {
441
+ ...internal,
442
+ ...extMetadata,
443
+ };
311
444
  }
312
445
 
313
446
  /**
314
- * For any package.json fields which a particular type of extension requires, validate the
315
- * presence and form of those fields on the package.json data, throwing an error if anything is
316
- * amiss.
447
+ * Validates the _required_ root fields of an extension's `package.json` file.
317
448
  *
318
- * @param {object} appiumPkgData - the data in the "appium" field of package.json for an
319
- * extension
449
+ * These required fields are:
450
+ * - `name`
451
+ * - `version`
452
+ * - `appium`
453
+ * @param {import('type-fest').PackageJson} pkg - `package.json` of extension
454
+ * @param {string} installSpec - Extension name/spec
455
+ * @throws {ReferenceError} If `package.json` has a missing or invalid field
456
+ * @returns {pkg is ExtPackageJson<ExtType>}
320
457
  */
321
- validateExtensionFields (/*appiumPkgData*/) {
322
- throw new Error('Must be implemented in final class');
458
+ validatePackageJson(pkg, installSpec) {
459
+ const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkg);
460
+
461
+ /**
462
+ *
463
+ * @param {string} field
464
+ * @returns {ReferenceError}
465
+ */
466
+ const createMissingFieldError = (field) =>
467
+ new ReferenceError(
468
+ `${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
469
+ );
470
+
471
+ if (!name) {
472
+ throw createMissingFieldError('name');
473
+ }
474
+ if (!version) {
475
+ throw createMissingFieldError('version');
476
+ }
477
+ if (!appium) {
478
+ throw createMissingFieldError('appium');
479
+ }
480
+
481
+ this.validateExtensionFields(appium, installSpec);
482
+
483
+ return true;
323
484
  }
324
485
 
325
486
  /**
326
- * @typedef {Object} UninstallArgs
327
- * @property {string} ext - the name or spec of an extension to uninstall
487
+ * For any `package.json` fields which a particular type of extension requires, validate the
488
+ * presence and form of those fields on the `package.json` data, throwing an error if anything is
489
+ * amiss.
490
+ *
491
+ * @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
492
+ * @param {string} installSpec - Extension name/spec
328
493
  */
494
+ // eslint-disable-next-line no-unused-vars
495
+ validateExtensionFields(extMetadata, installSpec) {
496
+ throw this._createFatalError('Must be implemented in final class');
497
+ }
329
498
 
330
499
  /**
331
- * Uninstall an extension
500
+ * Uninstall an extension.
501
+ *
502
+ * First tries to do this via `npm uninstall`, but if that fails, just `rm -rf`'s the extension dir.
332
503
  *
333
- * @param {UninstallArgs} args
334
- * @return {object} map of all installed extension names to extension data
504
+ * Will only remove the extension from the manifest if it has been successfully removed.
505
+ *
506
+ * @param {UninstallOpts} opts
507
+ * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data (without the extension just uninstalled)
335
508
  */
336
- async uninstall ({ext}) {
337
- if (!this.config.isInstalled(ext)) {
338
- throw new Error(`Can't uninstall ${this.type} '${ext}'; it is not installed`);
339
- }
340
- try {
341
- await fs.rimraf(this.config.getInstallPath(ext));
342
- } finally {
343
- await this.config.removeExtension(ext);
509
+ async _uninstall({installSpec}) {
510
+ if (!this.config.isInstalled(installSpec)) {
511
+ throw this._createFatalError(
512
+ `Can't uninstall ${this.type} '${installSpec}'; it is not installed`
513
+ );
344
514
  }
345
- log(this.isJsonOutput, `Successfully uninstalled ${this.type} '${ext}'`.green);
515
+ const pkgName = this.config.installedExtensions[installSpec].pkgName;
516
+ await npm.uninstallPackage(this.config.appiumHome, pkgName);
517
+ await this.config.removeExtension(installSpec);
518
+ this.log.ok(`Successfully uninstalled ${this.type} '${installSpec}'`.green);
346
519
  return this.config.installedExtensions;
347
520
  }
348
521
 
349
- /**
350
- * @typedef {Object} ExtensionUpdateOpts
351
- * @property {string} ext - the name of the extension to update
352
- * @property {boolean} unsafe - if true, will perform unsafe updates past major revision
353
- * boundaries
354
- */
355
-
356
- /**
357
- * @typedef {Object} UpdateReport
358
- * @property {string} from - version updated from
359
- * @property {string} to - version updated to
360
- */
361
-
362
- /**
363
- * @typedef {Object} ExtensionUpdateResult
364
- * @property {Object} errors - map of ext names to error objects
365
- * @property {Object} updates - map of ext names to {@link UpdateReport}s
366
- */
367
-
368
522
  /**
369
523
  * Attempt to update one or more drivers using NPM
370
524
  *
371
525
  * @param {ExtensionUpdateOpts} updateSpec
372
- * @return {ExtensionUpdateResult}
526
+ * @return {Promise<ExtensionUpdateResult>}
373
527
  */
374
- async update ({ext, unsafe}) {
375
- const shouldUpdateAll = ext === UPDATE_ALL;
528
+ async _update({installSpec, unsafe}) {
529
+ const shouldUpdateAll = installSpec === UPDATE_ALL;
376
530
  // if we're specifically requesting an update for an extension, make sure it's installed
377
- if (!shouldUpdateAll && !this.config.isInstalled(ext)) {
378
- throw new Error(`The ${this.type} '${ext}' was not installed, so can't be updated`);
531
+ if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
532
+ throw this._createFatalError(
533
+ `The ${this.type} "${installSpec}" was not installed, so can't be updated`
534
+ );
379
535
  }
380
- const extsToUpdate = shouldUpdateAll ? Object.keys(this.config.installedExtensions) : [ext];
536
+ const extsToUpdate = shouldUpdateAll
537
+ ? Object.keys(this.config.installedExtensions)
538
+ : [installSpec];
381
539
 
382
540
  // 'errors' will have ext names as keys and error objects as values
541
+ /** @type {Record<string,Error>} */
383
542
  const errors = {};
384
543
 
385
544
  // 'updates' will have ext names as keys and update objects as values, where an update
386
545
  // object is of the form {from: versionString, to: versionString}
546
+ /** @type {Record<string,UpdateReport>} */
387
547
  const updates = {};
388
548
 
389
549
  for (const e of extsToUpdate) {
@@ -393,17 +553,23 @@ export default class ExtensionCommand {
393
553
  throw new NotUpdatableError();
394
554
  }
395
555
  });
396
- const update = await spinWith(this.isJsonOutput, `Checking if ${this.type} '${e}' needs an update`, async () => {
397
- const update = await this.checkForExtensionUpdate(e);
398
- if (!(update.safeUpdate || update.unsafeUpdate)) {
399
- throw new NoUpdatesAvailableError();
556
+ const update = await spinWith(
557
+ this.isJsonOutput,
558
+ `Checking if ${this.type} '${e}' needs an update`,
559
+ async () => {
560
+ const update = await this.checkForExtensionUpdate(e);
561
+ if (!(update.safeUpdate || update.unsafeUpdate)) {
562
+ throw new NoUpdatesAvailableError();
563
+ }
564
+ return update;
400
565
  }
401
- return update;
402
- });
566
+ );
403
567
  if (!unsafe && !update.safeUpdate) {
404
- throw new Error(`The ${this.type} '${e}' has a major revision update ` +
405
- `(${update.current} => ${update.unsafeUpdate}), which could include ` +
406
- `breaking changes. If you want to apply this update, re-run with --unsafe`);
568
+ throw this._createFatalError(
569
+ `The ${this.type} '${e}' has a major revision update ` +
570
+ `(${update.current} => ${update.unsafeUpdate}), which could include ` +
571
+ `breaking changes. If you want to apply this update, re-run with --unsafe`
572
+ );
407
573
  }
408
574
  const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
409
575
  await spinWith(
@@ -417,47 +583,47 @@ export default class ExtensionCommand {
417
583
  }
418
584
  }
419
585
 
420
- log(this.isJsonOutput, 'Update report:');
586
+ this.log.info('Update report:');
587
+
421
588
  for (const [e, update] of _.toPairs(updates)) {
422
- log(this.isJsonOutput, `- ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
589
+ this.log.ok(` - ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
423
590
  }
591
+
424
592
  for (const [e, err] of _.toPairs(errors)) {
425
593
  if (err instanceof NotUpdatableError) {
426
- log(this.isJsonOutput, `- '${e}' was not installed via npm, so we could not check ` +
427
- `for updates`.yellow);
594
+ this.log.warn(
595
+ ` - '${e}' was not installed via npm, so we could not check ` + `for updates`.yellow
596
+ );
428
597
  } else if (err instanceof NoUpdatesAvailableError) {
429
- log(this.isJsonOutput, `- '${e}' had no updates available`.yellow);
598
+ this.log.info(` - '${e}' had no updates available`.yellow);
430
599
  } else {
431
600
  // otherwise, make it pop with red!
432
- log(this.isJsonOutput, `- '${e}' failed to update: ${err}`.red);
601
+ this.log.error(` - '${e}' failed to update: ${err}`.red);
433
602
  }
434
603
  }
435
-
436
604
  return {updates, errors};
437
605
  }
438
606
 
439
- /**
440
- * @typedef PossibleUpdates
441
- * @property {string} current - current version
442
- * @property {string|null} safeUpdate - version we can safely update to if it exists, or null
443
- * @property {string|null} unsafeUpdate - version we can unsafely update to if it exists, or null
444
- */
445
-
446
607
  /**
447
608
  * Given an extension name, figure out what its highest possible version upgrade is, and also the
448
609
  * highest possible safe upgrade.
449
610
  *
450
611
  * @param {string} ext - name of extension
451
- * @return {PossibleUpdates}
612
+ * @return {Promise<PossibleUpdates>}
452
613
  */
453
- async checkForExtensionUpdate (ext) {
614
+ async checkForExtensionUpdate(ext) {
454
615
  // TODO decide how we want to handle beta versions?
455
616
  // this is a helper method, 'ext' is assumed to already be installed here, and of the npm
456
617
  // install type
457
618
  const {version, pkgName} = this.config.installedExtensions[ext];
458
- let unsafeUpdate = await this.npm.getLatestVersion(pkgName);
459
- let safeUpdate = await this.npm.getLatestSafeUpgradeVersion(pkgName, version);
460
- if (!util.compareVersions(unsafeUpdate, '>', version)) {
619
+ /** @type {string?} */
620
+ let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
621
+ let safeUpdate = await npm.getLatestSafeUpgradeVersion(
622
+ this.config.appiumHome,
623
+ pkgName,
624
+ version
625
+ );
626
+ if (unsafeUpdate !== null && !util.compareVersions(unsafeUpdate, '>', version)) {
461
627
  // the latest version is not greater than the current version, so there's no possible update
462
628
  unsafeUpdate = null;
463
629
  safeUpdate = null;
@@ -477,13 +643,255 @@ export default class ExtensionCommand {
477
643
  * Actually update an extension installed by NPM, using the NPM cli. And update the installation
478
644
  * manifest.
479
645
  *
480
- * @param {string} ext - name of extension to update
646
+ * @param {string} installSpec - name of extension to update
481
647
  * @param {string} version - version string identifier to update extension to
648
+ * @returns {Promise<void>}
649
+ */
650
+ async updateExtension(installSpec, version) {
651
+ const {pkgName, installType} = this.config.installedExtensions[installSpec];
652
+ const extData = await this.installViaNpm({
653
+ installSpec,
654
+ installType,
655
+ pkgName,
656
+ pkgVer: version,
657
+ });
658
+ delete extData[/** @type {string} */ (`${this.type}Name`)];
659
+ await this.config.updateExtension(installSpec, extData);
660
+ }
661
+
662
+ /**
663
+ * Runs a script cached inside the "scripts" field under "appium"
664
+ * inside of the driver/plugins "package.json" file. Will throw
665
+ * an error if the driver/plugin does not contain a "scripts" field
666
+ * underneath the "appium" field in its package.json, if the
667
+ * "scripts" field is not a plain object, or if the scriptName is
668
+ * not found within "scripts" object.
669
+ *
670
+ * @param {RunOptions} opts
671
+ * @return {Promise<RunOutput>}
482
672
  */
483
- async updateExtension (ext, version) {
484
- const {pkgName} = this.config.installedExtensions[ext];
485
- await this.installViaNpm({ext, pkgName, pkgVer: version});
486
- this.config.installedExtensions[ext].version = version;
487
- await this.config.write();
673
+ async _run({installSpec, scriptName, extraArgs = []}) {
674
+ if (!this.config.isInstalled(installSpec)) {
675
+ throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
676
+ }
677
+
678
+ const extConfig = this.config.installedExtensions[installSpec];
679
+
680
+ // note: TS cannot understand that _.has() is a type guard
681
+ if (!('scripts' in extConfig)) {
682
+ throw this._createFatalError(
683
+ `The ${this.type} named '${installSpec}' does not contain the ` +
684
+ `"scripts" field underneath the "appium" field in its package.json`
685
+ );
686
+ }
687
+
688
+ const extScripts = extConfig.scripts;
689
+
690
+ if (!extScripts || !_.isPlainObject(extScripts)) {
691
+ throw this._createFatalError(
692
+ `The ${this.type} named '${installSpec}' "scripts" field must be a plain object`
693
+ );
694
+ }
695
+
696
+ if (!(scriptName in extScripts)) {
697
+ throw this._createFatalError(
698
+ `The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
699
+ );
700
+ }
701
+
702
+ const runner = new SubProcess(process.execPath, [extScripts[scriptName], ...extraArgs], {
703
+ cwd: this.config.getInstallPath(installSpec),
704
+ });
705
+
706
+ const output = new RingBuffer(50);
707
+
708
+ runner.on('stream-line', (line) => {
709
+ output.enqueue(line);
710
+ this.log.log(line);
711
+ });
712
+
713
+ await runner.start(0);
714
+
715
+ try {
716
+ await runner.join();
717
+ this.log.ok(`${scriptName} successfully ran`.green);
718
+ return {output: output.getBuff()};
719
+ } catch (err) {
720
+ this.log.error(`Encountered an error when running '${scriptName}': ${err.message}`.red);
721
+ return {error: err.message, output: output.getBuff()};
722
+ }
488
723
  }
489
724
  }
725
+
726
+ export default ExtensionCommand;
727
+ export {ExtensionCommand};
728
+
729
+ /**
730
+ * Options for the {@linkcode ExtensionCommand} constructor
731
+ * @template {ExtensionType} ExtType
732
+ * @typedef ExtensionCommandOptions
733
+ * @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
734
+ * @property {boolean} json - whether the output of this command should be JSON or text
735
+ */
736
+
737
+ /**
738
+ * Extra stuff about extensions; used indirectly by {@linkcode ExtensionCommand.list}.
739
+ *
740
+ * @typedef ExtensionMetadata
741
+ * @property {boolean} installed - If `true`, the extension is installed
742
+ * @property {string?} updateVersion - If the extension is installed, the version it can be updated to
743
+ * @property {string?} unsafeUpdateVersion - Same as above, but a major version bump
744
+ * @property {boolean} upToDate - If the extension is installed and the latest
745
+ * @property {string?} updateError - Update check error message (if present)
746
+ */
747
+
748
+ /**
749
+ * @typedef {import('@appium/types').ExtensionType} ExtensionType
750
+ * @typedef {import('@appium/types').DriverType} DriverType
751
+ * @typedef {import('@appium/types').PluginType} PluginType
752
+ */
753
+
754
+ /**
755
+ * @template {ExtensionType} ExtType
756
+ * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
757
+ */
758
+
759
+ /**
760
+ * @template {ExtensionType} ExtType
761
+ * @typedef {import('../extension/extension-config').ExtensionConfig<ExtType>} ExtensionConfig
762
+ */
763
+
764
+ /**
765
+ * @template {ExtensionType} ExtType
766
+ * @typedef {import('appium/types').ExtMetadata<ExtType>} ExtMetadata
767
+ */
768
+
769
+ /**
770
+ * @template {ExtensionType} ExtType
771
+ * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
772
+ */
773
+
774
+ /**
775
+ * @template {ExtensionType} ExtType
776
+ * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
777
+ */
778
+
779
+ /**
780
+ * @template {ExtensionType} ExtType
781
+ * @typedef {import('appium/types').ExtInstallReceipt<ExtType>} ExtInstallReceipt
782
+ */
783
+
784
+ /**
785
+ * Possible return value for {@linkcode ExtensionCommand.list}
786
+ * @typedef {Partial<InstalledExtensionListData> & {pkgName: string, installed: false}} UninstalledExtensionListData
787
+ */
788
+
789
+ /**
790
+ * Possible return value for {@linkcode ExtensionCommand.list}
791
+ * @typedef {import('appium/types').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
792
+ */
793
+
794
+ /**
795
+ * Return value of {@linkcode ExtensionCommand.list}.
796
+ * @typedef {Record<string,InstalledExtensionListData|UninstalledExtensionListData>} ExtensionListData
797
+ */
798
+
799
+ /**
800
+ * Options for {@linkcode ExtensionCommand._run}.
801
+ * @typedef RunOptions
802
+ * @property {string} installSpec - name of the extension to run a script from
803
+ * @property {string} scriptName - name of the script to run
804
+ * @property {string[]} [extraArgs] - arguments to pass to the script
805
+ */
806
+
807
+ /**
808
+ * Return value of {@linkcode ExtensionCommand._run}
809
+ *
810
+ * @typedef RunOutput
811
+ * @property {string} [error] - error message if script ran unsuccessfully, otherwise undefined
812
+ * @property {string[]} output - script output
813
+ */
814
+
815
+ /**
816
+ * Options for {@linkcode ExtensionCommand._update}.
817
+ * @typedef ExtensionUpdateOpts
818
+ * @property {string} installSpec - the name of the extension to update
819
+ * @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
820
+ */
821
+
822
+ /**
823
+ * Return value of {@linkcode ExtensionCommand._update}.
824
+ * @typedef ExtensionUpdateResult
825
+ * @property {Record<string,Error>} errors - map of ext names to error objects
826
+ * @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
827
+ */
828
+
829
+ /**
830
+ * Part of result of {@linkcode ExtensionCommand._update}.
831
+ * @typedef UpdateReport
832
+ * @property {string} from - version the extension was updated from
833
+ * @property {string} to - version the extension was updated to
834
+ */
835
+
836
+ /**
837
+ * Options for {@linkcode ExtensionCommand._uninstall}.
838
+ * @typedef UninstallOpts
839
+ * @property {string} installSpec - the name or spec of an extension to uninstall
840
+ */
841
+
842
+ /**
843
+ * Used by {@linkcode ExtensionCommand.getPostInstallText}
844
+ * @typedef ExtensionArgs
845
+ * @property {string} extName - the name of an extension
846
+ * @property {object} extData - the data for an installed extension
847
+ */
848
+
849
+ /**
850
+ * Options for {@linkcode ExtensionCommand.installViaNpm}
851
+ * @typedef InstallViaNpmArgs
852
+ * @property {string} installSpec - the name or spec of an extension to install
853
+ * @property {string} pkgName - the NPM package name of the extension
854
+ * @property {import('appium/types').InstallType} installType - type of install
855
+ * @property {string} [pkgVer] - the specific version of the NPM package
856
+ */
857
+
858
+ /**
859
+ * Object returned by {@linkcode ExtensionCommand.checkForExtensionUpdate}
860
+ * @typedef PossibleUpdates
861
+ * @property {string} current - current version
862
+ * @property {string?} safeUpdate - version we can safely update to if it exists, or null
863
+ * @property {string?} unsafeUpdate - version we can unsafely update to if it exists, or null
864
+ */
865
+
866
+ /**
867
+ * Options for {@linkcode ExtensionCommand._install}
868
+ * @typedef InstallOpts
869
+ * @property {string} installSpec - the name or spec of an extension to install
870
+ * @property {InstallType} installType - how to install this extension. One of the INSTALL_TYPES
871
+ * @property {string} [packageName] - for git/github installs, the extension node package name
872
+ */
873
+
874
+ /**
875
+ * @template {ExtensionType} ExtType
876
+ * @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
877
+ */
878
+
879
+ /**
880
+ * @typedef ListOptions
881
+ * @property {boolean} showInstalled - whether should show only installed extensions
882
+ * @property {boolean} showUpdates - whether should show available updates
883
+ */
884
+
885
+ /**
886
+ * Opts for {@linkcode ExtensionCommand.getInstallationReceipt}
887
+ * @template {ExtensionType} ExtType
888
+ * @typedef GetInstallationReceiptOpts
889
+ * @property {string} installPath
890
+ * @property {string} installSpec
891
+ * @property {ExtPackageJson<ExtType>} pkg
892
+ * @property {InstallType} installType
893
+ */
894
+
895
+ /**
896
+ * @typedef {import('appium/types').InstallType} InstallType
897
+ */