appium 2.0.0-beta.8 → 2.0.0-rc.1

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