appium 2.0.0-beta.3 → 2.0.0-beta.30

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 (139) hide show
  1. package/README.md +10 -11
  2. package/build/lib/appium.d.ts +215 -0
  3. package/build/lib/appium.d.ts.map +1 -0
  4. package/build/lib/appium.js +238 -132
  5. package/build/lib/cli/args.d.ts +20 -0
  6. package/build/lib/cli/args.d.ts.map +1 -0
  7. package/build/lib/cli/args.js +96 -282
  8. package/build/lib/cli/driver-command.d.ts +36 -0
  9. package/build/lib/cli/driver-command.d.ts.map +1 -0
  10. package/build/lib/cli/driver-command.js +19 -12
  11. package/build/lib/cli/extension-command.d.ts +345 -0
  12. package/build/lib/cli/extension-command.d.ts.map +1 -0
  13. package/build/lib/cli/extension-command.js +171 -96
  14. package/build/lib/cli/extension.d.ts +14 -0
  15. package/build/lib/cli/extension.d.ts.map +1 -0
  16. package/build/lib/cli/extension.js +31 -16
  17. package/build/lib/cli/parser.d.ts +79 -0
  18. package/build/lib/cli/parser.d.ts.map +1 -0
  19. package/build/lib/cli/parser.js +152 -95
  20. package/build/lib/cli/plugin-command.d.ts +39 -0
  21. package/build/lib/cli/plugin-command.d.ts.map +1 -0
  22. package/build/lib/cli/plugin-command.js +18 -13
  23. package/build/lib/cli/utils.d.ts +29 -0
  24. package/build/lib/cli/utils.d.ts.map +1 -0
  25. package/build/lib/cli/utils.js +27 -3
  26. package/build/lib/config-file.d.ts +100 -0
  27. package/build/lib/config-file.d.ts.map +1 -0
  28. package/build/lib/config-file.js +136 -0
  29. package/build/lib/config.d.ts +40 -0
  30. package/build/lib/config.d.ts.map +1 -0
  31. package/build/lib/config.js +92 -67
  32. package/build/lib/constants.d.ts +48 -0
  33. package/build/lib/constants.d.ts.map +1 -0
  34. package/build/lib/constants.js +60 -0
  35. package/build/lib/extension/driver-config.d.ts +84 -0
  36. package/build/lib/extension/driver-config.d.ts.map +1 -0
  37. package/build/lib/extension/driver-config.js +190 -0
  38. package/build/lib/extension/extension-config.d.ts +170 -0
  39. package/build/lib/extension/extension-config.d.ts.map +1 -0
  40. package/build/lib/extension/extension-config.js +297 -0
  41. package/build/lib/extension/index.d.ts +39 -0
  42. package/build/lib/extension/index.d.ts.map +1 -0
  43. package/build/lib/extension/index.js +77 -0
  44. package/build/lib/extension/manifest.d.ts +174 -0
  45. package/build/lib/extension/manifest.d.ts.map +1 -0
  46. package/build/lib/extension/manifest.js +246 -0
  47. package/build/lib/extension/package-changed.d.ts +11 -0
  48. package/build/lib/extension/package-changed.d.ts.map +1 -0
  49. package/build/lib/extension/package-changed.js +68 -0
  50. package/build/lib/extension/plugin-config.d.ts +62 -0
  51. package/build/lib/extension/plugin-config.d.ts.map +1 -0
  52. package/build/lib/extension/plugin-config.js +87 -0
  53. package/build/lib/grid-register.d.ts +10 -0
  54. package/build/lib/grid-register.d.ts.map +1 -0
  55. package/build/lib/grid-register.js +21 -25
  56. package/build/lib/logger.d.ts +3 -0
  57. package/build/lib/logger.d.ts.map +1 -0
  58. package/build/lib/logger.js +4 -6
  59. package/build/lib/logsink.d.ts +4 -0
  60. package/build/lib/logsink.d.ts.map +1 -0
  61. package/build/lib/logsink.js +12 -16
  62. package/build/lib/main.d.ts +51 -0
  63. package/build/lib/main.d.ts.map +1 -0
  64. package/build/lib/main.js +174 -82
  65. package/build/lib/schema/arg-spec.d.ts +143 -0
  66. package/build/lib/schema/arg-spec.d.ts.map +1 -0
  67. package/build/lib/schema/arg-spec.js +119 -0
  68. package/build/lib/schema/cli-args.d.ts +19 -0
  69. package/build/lib/schema/cli-args.d.ts.map +1 -0
  70. package/build/lib/schema/cli-args.js +180 -0
  71. package/build/lib/schema/cli-transformers.d.ts +5 -0
  72. package/build/lib/schema/cli-transformers.d.ts.map +1 -0
  73. package/build/lib/schema/cli-transformers.js +74 -0
  74. package/build/lib/schema/index.d.ts +3 -0
  75. package/build/lib/schema/index.d.ts.map +1 -0
  76. package/build/lib/schema/index.js +34 -0
  77. package/build/lib/schema/keywords.d.ts +24 -0
  78. package/build/lib/schema/keywords.d.ts.map +1 -0
  79. package/build/lib/schema/keywords.js +70 -0
  80. package/build/lib/schema/schema.d.ts +259 -0
  81. package/build/lib/schema/schema.d.ts.map +1 -0
  82. package/build/lib/schema/schema.js +452 -0
  83. package/build/lib/utils.d.ts +66 -0
  84. package/build/lib/utils.d.ts.map +1 -0
  85. package/build/lib/utils.js +35 -139
  86. package/build/tsconfig.tsbuildinfo +1 -0
  87. package/index.js +11 -0
  88. package/lib/appium-config.schema.json +278 -0
  89. package/lib/appium.js +398 -155
  90. package/lib/cli/args.js +174 -377
  91. package/lib/cli/driver-command.js +22 -7
  92. package/lib/cli/extension-command.js +372 -177
  93. package/lib/cli/extension.js +32 -10
  94. package/lib/cli/parser.js +252 -83
  95. package/lib/cli/plugin-command.js +19 -6
  96. package/lib/cli/utils.js +22 -2
  97. package/lib/config-file.js +223 -0
  98. package/lib/config.js +169 -69
  99. package/lib/constants.js +78 -0
  100. package/lib/extension/driver-config.js +249 -0
  101. package/lib/extension/extension-config.js +458 -0
  102. package/lib/extension/index.js +102 -0
  103. package/lib/extension/manifest.js +486 -0
  104. package/lib/extension/package-changed.js +63 -0
  105. package/lib/extension/plugin-config.js +113 -0
  106. package/lib/grid-register.js +25 -22
  107. package/lib/logger.js +1 -1
  108. package/lib/logsink.js +14 -7
  109. package/lib/main.js +233 -83
  110. package/lib/schema/arg-spec.js +232 -0
  111. package/lib/schema/cli-args.js +261 -0
  112. package/lib/schema/cli-transformers.js +122 -0
  113. package/lib/schema/index.js +2 -0
  114. package/lib/schema/keywords.js +134 -0
  115. package/lib/schema/schema.js +734 -0
  116. package/lib/utils.js +85 -129
  117. package/package.json +62 -85
  118. package/scripts/postinstall.js +71 -0
  119. package/types/appium-manifest.d.ts +61 -0
  120. package/types/cli.d.ts +134 -0
  121. package/types/extension.d.ts +56 -0
  122. package/types/external-manifest.d.ts +58 -0
  123. package/types/index.d.ts +7 -0
  124. package/CHANGELOG.md +0 -3515
  125. package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
  126. package/build/lib/cli/npm.js +0 -206
  127. package/build/lib/cli/parser-helpers.js +0 -82
  128. package/build/lib/driver-config.js +0 -77
  129. package/build/lib/drivers.js +0 -96
  130. package/build/lib/extension-config.js +0 -251
  131. package/build/lib/plugin-config.js +0 -59
  132. package/build/lib/plugins.js +0 -14
  133. package/lib/cli/npm.js +0 -183
  134. package/lib/cli/parser-helpers.js +0 -79
  135. package/lib/driver-config.js +0 -46
  136. package/lib/drivers.js +0 -81
  137. package/lib/extension-config.js +0 -208
  138. package/lib/plugin-config.js +0 -34
  139. package/lib/plugins.js +0 -10
@@ -1,50 +1,66 @@
1
1
  /* eslint-disable no-console */
2
2
 
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';
5
+ import { npm, fs, util, env } from '@appium/support';
6
+ import { log, spinWith, RingBuffer } from './utils';
7
+ import { SubProcess } from 'teen_process';
8
8
  import { INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB,
9
- INSTALL_TYPE_LOCAL } from '../extension-config';
9
+ INSTALL_TYPE_LOCAL } from '../extension/extension-config';
10
+ import { packageDidChange } from '../extension/package-changed';
10
11
 
11
12
  const UPDATE_ALL = 'installed';
12
13
 
13
14
  class NotUpdatableError extends Error {}
14
15
  class NoUpdatesAvailableError extends Error {}
15
16
 
16
- export default class ExtensionCommand {
17
+ /**
18
+ * @template {ExtensionType} ExtType
19
+ */
20
+ class ExtensionCommand {
21
+ /**
22
+ * This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
23
+ * @type {ExtensionConfig<ExtType>}
24
+ */
25
+ config;
26
+
27
+ /**
28
+ * {@linkcode Record} of official plugins or drivers.
29
+ * @type {KnownExtensions<ExtType>}
30
+ */
31
+ knownExtensions;
17
32
 
18
33
  /**
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
34
+ * If `true`, command output has been requested as JSON.
35
+ * @type {boolean}
23
36
  */
37
+ isJsonOutput;
24
38
 
25
39
  /**
26
40
  * Build an ExtensionCommand
27
- *
28
- * @param {ExtensionCommandConstructor} opts
29
- * @return {ExtensionCommand}
41
+ * @param {ExtensionCommandOptions<ExtType>} opts
30
42
  */
31
- constructor ({config, json, type}) {
43
+ constructor ({config, json}) {
32
44
  this.config = config;
33
- this.type = type;
34
45
  this.isJsonOutput = json;
35
- this.npm = new NPM(this.config.appiumHome);
36
- this.knownExtensions = {}; // this needs to be overridden in final class
46
+ }
47
+
48
+ /**
49
+ * `driver` or `plugin`, depending on the `ExtensionConfig`.
50
+ */
51
+ get type () {
52
+ return this.config.extensionType;
37
53
  }
38
54
 
39
55
  /**
40
56
  * Take a CLI parse and run an extension command based on its type
41
57
  *
42
58
  * @param {object} args - a key/value object with CLI flags and values
43
- * @return {object} the result of the specific command which is executed
59
+ * @return {Promise<object>} the result of the specific command which is executed
44
60
  */
45
61
  async execute (args) {
46
62
  const cmd = args[`${this.type}Command`];
47
- if (!_.isFunction(ExtensionCommand.prototype[cmd])) {
63
+ if (!_.isFunction(this[cmd])) {
48
64
  throw new Error(`Cannot handle ${this.type} command ${cmd}`);
49
65
  }
50
66
  const executeCmd = this[cmd].bind(this);
@@ -52,7 +68,7 @@ export default class ExtensionCommand {
52
68
  }
53
69
 
54
70
  /**
55
- * @typedef {Object} ListArgs
71
+ * @typedef ListOptions
56
72
  * @property {boolean} showInstalled - whether should show only installed extensions
57
73
  * @property {boolean} showUpdates - whether should show available updates
58
74
  */
@@ -60,8 +76,8 @@ export default class ExtensionCommand {
60
76
  /**
61
77
  * List extensions
62
78
  *
63
- * @param {ListArgs} args
64
- * @return {object} map of extension names to extension data
79
+ * @param {ListOptions} opts
80
+ * @return {Promise<ExtensionListData>} map of extension names to extension data
65
81
  */
66
82
  async list ({showInstalled, showUpdates}) {
67
83
  const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
@@ -76,7 +92,12 @@ export default class ExtensionCommand {
76
92
  }
77
93
  }
78
94
  return acc;
79
- }, {});
95
+ },
96
+ /**
97
+ * This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
98
+ * {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
99
+ * @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
100
+ */({}));
80
101
 
81
102
  // if we want to show whether updates are available, put that behind a spinner
82
103
  await spinWith(this.isJsonOutput, lsMsg, async () => {
@@ -84,8 +105,7 @@ export default class ExtensionCommand {
84
105
  return;
85
106
  }
86
107
  for (const [ext, data] of _.toPairs(exts)) {
87
- const {installed, installType} = data;
88
- if (!installed || installType !== INSTALL_TYPE_NPM) {
108
+ if (!data.installed || data.installType !== INSTALL_TYPE_NPM) {
89
109
  // don't need to check for updates on exts that aren't installed
90
110
  // also don't need to check for updates on non-npm exts
91
111
  continue;
@@ -97,63 +117,64 @@ export default class ExtensionCommand {
97
117
  }
98
118
  });
99
119
 
120
+ const listData = /** @type {ExtensionListData} */(exts);
121
+
100
122
  // if we're just getting the data, short circuit return here since we don't need to do any
101
123
  // formatting logic
102
124
  if (this.isJsonOutput) {
103
- return exts;
125
+ return listData;
104
126
  }
105
127
 
106
128
  for (const [
107
129
  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)';
130
+ data
131
+ ] of _.toPairs(listData)) {
132
+ let installTxt = ' [not installed]'.grey;
133
+ let updateTxt = '';
134
+ let upToDateTxt = '';
135
+ let unsafeUpdateTxt = '';
136
+ if (data.installed) {
137
+ const {installType, installSpec, updateVersion, unsafeUpdateVersion, version, upToDate} = data;
138
+ let typeTxt;
139
+ switch (installType) {
140
+ case INSTALL_TYPE_GIT:
141
+ case INSTALL_TYPE_GITHUB:
142
+ typeTxt = `(cloned from ${installSpec})`.yellow;
143
+ break;
144
+ case INSTALL_TYPE_LOCAL:
145
+ typeTxt = `(linked from ${installSpec})`.magenta;
146
+ break;
147
+ default:
148
+ typeTxt = '(NPM)';
149
+ }
150
+ installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
151
+
152
+ if (showUpdates) {
153
+ if (updateVersion) {
154
+ updateTxt = ` [${updateVersion} available]`.magenta;
155
+ }
156
+ if (upToDate) {
157
+ upToDateTxt = ` [Up to date]`.green;
158
+ }
159
+ if (unsafeUpdateVersion) {
160
+ unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
161
+ }
162
+ }
121
163
  }
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
164
 
135
165
  console.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
136
166
  }
137
167
 
138
- return exts;
168
+ return listData;
139
169
  }
140
170
 
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
171
  /**
149
172
  * Install an extension
150
173
  *
151
174
  * @param {InstallArgs} args
152
- * @return {object} map of all installed extension names to extension data
175
+ * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
153
176
  */
154
- async install ({ext, installType, packageName}) {
155
- log(this.isJsonOutput, `Attempting to find and install ${this.type} '${ext}'`);
156
-
177
+ async _install ({ext, installType, packageName}) {
157
178
  let extData;
158
179
  let installSpec = ext;
159
180
 
@@ -165,66 +186,63 @@ export default class ExtensionCommand {
165
186
  throw new Error(`When using --source=${installType}, must also use --package`);
166
187
  }
167
188
 
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) {
189
+ if (installType === INSTALL_TYPE_GITHUB) {
176
190
  if (installSpec.split('/').length !== 2) {
177
191
  throw new Error(`Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
178
192
  'it should be of the form <org>/<repo>');
179
193
  }
180
- extData = await this.installViaNpm({ext: installSpec, pkgName: packageName});
194
+ extData = await this.installViaNpm({ext: installSpec, pkgName: /** @type {string} */(packageName)});
181
195
  } else if (installType === INSTALL_TYPE_GIT) {
182
196
  // git urls can have '.git' at the end, but this is not necessary and would complicate the
183
197
  // way we download and name directories, so we can just remove it
184
198
  installSpec = installSpec.replace(/\.git$/, '');
185
- extData = await this.installViaNpm({ext: installSpec, pkgName: packageName});
199
+ extData = await this.installViaNpm({ext: installSpec, pkgName: /** @type {string} */(packageName)});
186
200
  } 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]];
201
+ let pkgName, pkgVer;
202
+ if (installType === INSTALL_TYPE_LOCAL) {
203
+ pkgName = path.isAbsolute(installSpec) ? installSpec : path.resolve(installSpec);
197
204
  } else {
198
- // this is the case without an npm org
199
- [name, pkgVer] = splits;
200
- }
201
- let pkgName;
205
+ // at this point we have either an npm package or an appium verified extension
206
+ // name or a local path. both of which will be installed via npm.
207
+ // extensions installed via npm can include versions or tags after the '@'
208
+ // sign, so check for that. We also need to be careful that package names themselves can
209
+ // contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
210
+ let name;
211
+ const splits = installSpec.split('@');
212
+ if (installSpec[0] === '@') {
213
+ // this is the case where we have an npm org included in the package name
214
+ [name, pkgVer] = [`@${splits[1]}`, splits[2]];
215
+ } else {
216
+ // this is the case without an npm org
217
+ [name, pkgVer] = splits;
218
+ }
202
219
 
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);
220
+ if (installType === INSTALL_TYPE_NPM) {
221
+ // if we're installing a named package from npm, we don't need to check
222
+ // against the appium extension list; just use the installSpec as is
223
+ pkgName = name;
224
+ } else {
225
+ // if we're installing a named appium driver (like 'xcuitest') we need to
226
+ // dereference the actual npm package ('appiupm-xcuitest-driver'), so
227
+ // check it exists and get the correct package
228
+ const knownNames = Object.keys(this.knownExtensions);
229
+ if (!_.includes(knownNames, name)) {
230
+ const msg = `Could not resolve ${this.type}; are you sure it's in the list ` +
231
+ `of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
232
+ throw new Error(msg);
233
+ }
234
+ pkgName = this.knownExtensions[name];
235
+ // given that we'll use the install type in the driver json, store it as
236
+ // 'npm' now
237
+ installType = INSTALL_TYPE_NPM;
216
238
  }
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
239
  }
222
240
 
223
241
  extData = await this.installViaNpm({ext, pkgName, pkgVer});
224
242
  }
225
243
 
226
- const extName = extData[`${this.type}Name`];
227
- delete extData[`${this.type}Name`];
244
+ const extName = extData[/** @type {string} */(`${this.type}Name`)];
245
+ delete extData[/** @type {string} */(`${this.type}Name`)];
228
246
 
229
247
  if (this.config.isInstalled(extName)) {
230
248
  throw new Error(`A ${this.type} named '${extName}' is already installed. ` +
@@ -232,9 +250,13 @@ export default class ExtensionCommand {
232
250
  `installed ${this.type}s with 'appium ${this.type} list --installed'.`);
233
251
  }
234
252
 
235
- extData.installType = installType;
236
- extData.installSpec = installSpec;
237
- await this.config.addExtension(extName, extData);
253
+ const extManifest = {...extData, installType, installSpec};
254
+ await this.config.addExtension(extName, extManifest);
255
+
256
+ // update the if we've changed the local `package.json`
257
+ if (await env.hasAppiumDependency(this.config.appiumHome)) {
258
+ await packageDidChange(this.config.appiumHome);
259
+ }
238
260
 
239
261
  // log info for the user
240
262
  log(this.isJsonOutput, this.getPostInstallText({extName, extData}));
@@ -242,13 +264,6 @@ export default class ExtensionCommand {
242
264
  return this.config.installedExtensions;
243
265
  }
244
266
 
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
267
  /**
253
268
  * Install an extension via NPM
254
269
  *
@@ -260,33 +275,25 @@ export default class ExtensionCommand {
260
275
  const msg = `Installing '${ext}'${specMsg}`;
261
276
  try {
262
277
  const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => (
263
- await this.npm.installPackage({
264
- pkgDir: path.resolve(this.config.appiumHome, pkgName),
265
- pkgName,
278
+ await npm.installPackage(this.config.appiumHome, pkgName, {
266
279
  pkgVer
267
280
  })
268
281
  ));
269
- const extData = this.getExtensionFields(pkgJsonData);
270
- extData.installPath = pkgName;
271
- return extData;
282
+ return this.getExtensionFields(pkgJsonData);
272
283
  } catch (err) {
273
284
  throw new Error(`Encountered an error when installing package: ${err.message}`);
274
285
  }
275
286
  }
276
287
 
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
288
  /**
284
289
  * Get the text which should be displayed to the user after an extension has been installed. This
285
290
  * is designed to be overridden by drivers/plugins with their own particular text.
286
291
  *
287
292
  * @param {ExtensionArgs} args
293
+ * @returns {string}
288
294
  */
289
- getPostInstallText (/*{extName, extData}*/) {
295
+ // eslint-disable-next-line no-unused-vars
296
+ getPostInstallText (args) {
290
297
  throw new Error('Must be implemented in final class');
291
298
  }
292
299
 
@@ -296,8 +303,9 @@ export default class ExtensionCommand {
296
303
  * load as the main driver class, or to be able to detect incompatibilities between driver and
297
304
  * appium versions.
298
305
  *
299
- * @param {object} pkgJsonData - the package.json data for a driver module, as if it had been
306
+ * @param {ExtPackageJson<ExtType>} pkgJsonData - the package.json data for a driver module, as if it had been
300
307
  * straightforwardly 'require'd
308
+ * @returns {ExtensionFields<ExtType>}
301
309
  */
302
310
  getExtensionFields (pkgJsonData) {
303
311
  if (!pkgJsonData.appium) {
@@ -306,8 +314,9 @@ export default class ExtensionCommand {
306
314
  }
307
315
  const {appium, name, version} = pkgJsonData;
308
316
  this.validateExtensionFields(appium);
309
-
310
- return {...appium, pkgName: name, version};
317
+ /** @type {unknown} */
318
+ const result = {...appium, pkgName: name, version};
319
+ return /** @type {ExtensionFields<ExtType>} */(result);
311
320
  }
312
321
 
313
322
  /**
@@ -315,30 +324,26 @@ export default class ExtensionCommand {
315
324
  * presence and form of those fields on the package.json data, throwing an error if anything is
316
325
  * amiss.
317
326
  *
318
- * @param {object} appiumPkgData - the data in the "appium" field of package.json for an
319
- * extension
327
+ * @param {ExtMetadata<ExtType>} appiumPkgData - the data in the "appium" field of package.json for an extension
320
328
  */
321
- validateExtensionFields (/*appiumPkgData*/) {
329
+ // eslint-disable-next-line no-unused-vars
330
+ validateExtensionFields (appiumPkgData) {
322
331
  throw new Error('Must be implemented in final class');
323
332
  }
324
333
 
325
- /**
326
- * @typedef {Object} UninstallArgs
327
- * @property {string} ext - the name or spec of an extension to uninstall
328
- */
329
-
330
334
  /**
331
335
  * Uninstall an extension
332
336
  *
333
- * @param {UninstallArgs} args
334
- * @return {object} map of all installed extension names to extension data
337
+ * @param {UninstallOpts} opts
338
+ * @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
335
339
  */
336
- async uninstall ({ext}) {
340
+ async _uninstall ({ext}) {
337
341
  if (!this.config.isInstalled(ext)) {
338
342
  throw new Error(`Can't uninstall ${this.type} '${ext}'; it is not installed`);
339
343
  }
344
+ const installPath = this.config.getInstallPath(ext);
340
345
  try {
341
- await fs.rimraf(this.config.getInstallPath(ext));
346
+ await fs.rimraf(installPath);
342
347
  } finally {
343
348
  await this.config.removeExtension(ext);
344
349
  }
@@ -346,32 +351,13 @@ export default class ExtensionCommand {
346
351
  return this.config.installedExtensions;
347
352
  }
348
353
 
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
354
  /**
369
355
  * Attempt to update one or more drivers using NPM
370
356
  *
371
357
  * @param {ExtensionUpdateOpts} updateSpec
372
- * @return {ExtensionUpdateResult}
358
+ * @return {Promise<ExtensionUpdateResult>}
373
359
  */
374
- async update ({ext, unsafe}) {
360
+ async _update ({ext, unsafe}) {
375
361
  const shouldUpdateAll = ext === UPDATE_ALL;
376
362
  // if we're specifically requesting an update for an extension, make sure it's installed
377
363
  if (!shouldUpdateAll && !this.config.isInstalled(ext)) {
@@ -380,10 +366,12 @@ export default class ExtensionCommand {
380
366
  const extsToUpdate = shouldUpdateAll ? Object.keys(this.config.installedExtensions) : [ext];
381
367
 
382
368
  // 'errors' will have ext names as keys and error objects as values
369
+ /** @type {Record<string,Error>} */
383
370
  const errors = {};
384
371
 
385
372
  // 'updates' will have ext names as keys and update objects as values, where an update
386
373
  // object is of the form {from: versionString, to: versionString}
374
+ /** @type {Record<string,UpdateReport>} */
387
375
  const updates = {};
388
376
 
389
377
  for (const e of extsToUpdate) {
@@ -436,27 +424,20 @@ export default class ExtensionCommand {
436
424
  return {updates, errors};
437
425
  }
438
426
 
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
427
  /**
447
428
  * Given an extension name, figure out what its highest possible version upgrade is, and also the
448
429
  * highest possible safe upgrade.
449
430
  *
450
431
  * @param {string} ext - name of extension
451
- * @return {PossibleUpdates}
432
+ * @return {Promise<PossibleUpdates>}
452
433
  */
453
434
  async checkForExtensionUpdate (ext) {
454
435
  // TODO decide how we want to handle beta versions?
455
436
  // this is a helper method, 'ext' is assumed to already be installed here, and of the npm
456
437
  // install type
457
438
  const {version, pkgName} = this.config.installedExtensions[ext];
458
- let unsafeUpdate = await this.npm.getLatestVersion(pkgName);
459
- let safeUpdate = await this.npm.getLatestSafeUpgradeVersion(pkgName, version);
439
+ let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
440
+ let safeUpdate = await npm.getLatestSafeUpgradeVersion(this.config.appiumHome, pkgName, version);
460
441
  if (!util.compareVersions(unsafeUpdate, '>', version)) {
461
442
  // the latest version is not greater than the current version, so there's no possible update
462
443
  unsafeUpdate = null;
@@ -479,11 +460,225 @@ export default class ExtensionCommand {
479
460
  *
480
461
  * @param {string} ext - name of extension to update
481
462
  * @param {string} version - version string identifier to update extension to
463
+ * @returns {Promise<void>}
482
464
  */
483
465
  async updateExtension (ext, version) {
484
466
  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();
467
+ await fs.rimraf(this.config.getInstallPath(ext));
468
+ const extData = await this.installViaNpm({ext, pkgName, pkgVer: version});
469
+ delete extData[/** @type {string} */(`${this.type}Name`)];
470
+ await this.config.updateExtension(ext, extData);
471
+ }
472
+
473
+ /**
474
+ * Runs a script cached inside the "scripts" field under "appium"
475
+ * inside of the driver/plugins "package.json" file. Will throw
476
+ * an error if the driver/plugin does not contain a "scripts" field
477
+ * underneath the "appium" field in its package.json, if the
478
+ * "scripts" field is not a plain object, or if the scriptName is
479
+ * not found within "scripts" object.
480
+ *
481
+ * @param {RunOptions} opts
482
+ * @return {Promise<RunOutput>}
483
+ */
484
+ async _run ({ext, scriptName}) {
485
+ if (!_.has(this.config.installedExtensions, ext)) {
486
+ throw new Error(`please install the ${this.type} first`);
487
+ }
488
+
489
+ const extConfig = this.config.installedExtensions[ext];
490
+
491
+ // note: TS cannot understand that _.has() is a type guard
492
+ if (!extConfig.scripts) {
493
+ throw new Error(`The ${this.type} named '${ext}' does not contain the ` +
494
+ `"scripts" field underneath the "appium" field in its package.json`);
495
+ }
496
+
497
+ const extScripts = extConfig.scripts;
498
+
499
+ if (!_.isPlainObject(extScripts)) {
500
+ throw new Error(`The ${this.type} named '${ext}' "scripts" field must be a plain object`);
501
+ }
502
+
503
+ if (!_.has(extScripts, scriptName)) {
504
+ throw new Error(`The ${this.type} named '${ext}' does not support the script: '${scriptName}'`);
505
+ }
506
+
507
+ const runner = new SubProcess(process.execPath, [
508
+ extScripts[scriptName]
509
+ ], {
510
+ cwd: this.config.getInstallPath(ext)
511
+ });
512
+
513
+ const output = new RingBuffer(50);
514
+
515
+ runner.on('stream-line', (line) => {
516
+ output.enqueue(line);
517
+ log(this.isJsonOutput, line);
518
+ });
519
+
520
+ await runner.start(0);
521
+
522
+ try {
523
+ await runner.join();
524
+ log(this.isJsonOutput, `${scriptName} successfully ran`.green);
525
+ return {output: output.getBuff()};
526
+ } catch (err) {
527
+ log(this.isJsonOutput, `Encountered an error when running '${scriptName}': ${err.message}`.red);
528
+ return {error: err.message, output: output.getBuff()};
529
+ }
488
530
  }
489
531
  }
532
+
533
+ export default ExtensionCommand;
534
+ export {ExtensionCommand};
535
+
536
+ /**
537
+ * Options for the {@linkcode ExtensionCommand} constructor
538
+ * @template {ExtensionType} ExtType
539
+ * @typedef ExtensionCommandOptions
540
+ * @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
541
+ * @property {boolean} json - whether the output of this command should be JSON or text
542
+ */
543
+
544
+ /**
545
+ * Extra stuff about extensions; used indirectly by {@linkcode ExtensionCommand.list}.
546
+ *
547
+ * @typedef ExtensionMetadata
548
+ * @property {boolean} installed - If `true`, the extension is installed
549
+ * @property {string?} updateVersion - If the extension is installed, the version it can be updated to
550
+ * @property {string?} unsafeUpdateVersion - Same as above, but a major version bump
551
+ * @property {boolean} upToDate - If the extension is installed and the latest
552
+ */
553
+
554
+ /**
555
+ * @typedef {import('../../types').ExtensionType} ExtensionType
556
+ * @typedef {import('../../types').DriverType} DriverType
557
+ * @typedef {import('../../types').PluginType} PluginType
558
+ */
559
+
560
+ /**
561
+ * @template {ExtensionType} ExtType
562
+ * @typedef {import('../../types/appium-manifest').ExtRecord<ExtType>} ExtRecord
563
+ */
564
+
565
+ /**
566
+ * @template {ExtensionType} ExtType
567
+ * @typedef {import('../extension/extension-config').ExtensionConfig<ExtType>} ExtensionConfig
568
+ */
569
+
570
+ /**
571
+ * @template {ExtensionType} ExtType
572
+ * @typedef {import('../../types/external-manifest').ExtMetadata<ExtType>} ExtMetadata
573
+ */
574
+
575
+ /**
576
+ * @template {ExtensionType} ExtType
577
+ * @typedef {import('../../types/appium-manifest').ExtManifest<ExtType>} ExtManifest
578
+ */
579
+
580
+ /**
581
+ * @template {ExtensionType} ExtType
582
+ * @typedef {import('../../types/external-manifest').ExtPackageJson<ExtType>} ExtPackageJson
583
+ */
584
+
585
+ /**
586
+ * Possible return value for {@linkcode ExtensionCommand.list}
587
+ * @typedef UninstalledExtensionListData
588
+ * @property {string} pkgName
589
+ * @property {false} installed
590
+ */
591
+
592
+ /**
593
+ * Possible return value for {@linkcode ExtensionCommand.list}
594
+ * @typedef {import('../../types/appium-manifest').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
595
+ */
596
+
597
+ /**
598
+ * Return value of {@linkcode ExtensionCommand.list}.
599
+ * @typedef {Record<string,InstalledExtensionListData|UninstalledExtensionListData>} ExtensionListData
600
+ */
601
+
602
+ /**
603
+ * Options for {@linkcode ExtensionCommand._run}.
604
+ * @typedef RunOptions
605
+ * @property {string} ext - name of the extension to run a script from
606
+ * @property {string} scriptName - name of the script to run
607
+ */
608
+
609
+ /**
610
+ * Return value of {@linkcode ExtensionCommand._run}
611
+ *
612
+ * @typedef RunOutput
613
+ * @property {string} [error] - error message if script ran unsuccessfully, otherwise undefined
614
+ * @property {string[]} output - script output
615
+ */
616
+
617
+ /**
618
+ * Options for {@linkcode ExtensionCommand._update}.
619
+ * @typedef ExtensionUpdateOpts
620
+ * @property {string} ext - the name of the extension to update
621
+ * @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
622
+ */
623
+
624
+ /**
625
+ * Return value of {@linkcode ExtensionCommand._update}.
626
+ * @typedef ExtensionUpdateResult
627
+ * @property {Record<string,Error>} errors - map of ext names to error objects
628
+ * @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
629
+ */
630
+
631
+ /**
632
+ * Part of result of {@linkcode ExtensionCommand._update}.
633
+ * @typedef UpdateReport
634
+ * @property {string} from - version the extension was updated from
635
+ * @property {string} to - version the extension was updated to
636
+ */
637
+
638
+ /**
639
+ * Options for {@linkcode ExtensionCommand._uninstall}.
640
+ * @typedef UninstallOpts
641
+ * @property {string} ext - the name or spec of an extension to uninstall
642
+ */
643
+
644
+ /**
645
+ * Used by {@linkcode ExtensionCommand.getPostInstallText}
646
+ * @typedef ExtensionArgs
647
+ * @property {string} extName - the name of an extension
648
+ * @property {object} extData - the data for an installed extension
649
+ */
650
+
651
+ /**
652
+ * Options for {@linkcode ExtensionCommand.installViaNpm}
653
+ * @typedef InstallViaNpmArgs
654
+ * @property {string} ext - the name or spec of an extension to install
655
+ * @property {string} pkgName - the NPM package name of the extension
656
+ * @property {string} [pkgVer] - the specific version of the NPM package
657
+ */
658
+
659
+ /**
660
+ * Object returned by {@linkcode ExtensionCommand.checkForExtensionUpdate}
661
+ * @typedef PossibleUpdates
662
+ * @property {string} current - current version
663
+ * @property {string?} safeUpdate - version we can safely update to if it exists, or null
664
+ * @property {string?} unsafeUpdate - version we can unsafely update to if it exists, or null
665
+ */
666
+
667
+ /**
668
+ * Options for {@linkcode ExtensionCommand._install}
669
+ * @typedef InstallArgs
670
+ * @property {string} ext - the name or spec of an extension to install
671
+ * @property {import('../../types/appium-manifest').InstallType} installType - how to install this extension. One of the INSTALL_TYPES
672
+ * @property {string} [packageName] - for git/github installs, the extension node package name
673
+ */
674
+
675
+ /**
676
+ * Returned by {@linkcode ExtensionCommand.getExtensionFields}
677
+ * @template {ExtensionType} ExtType
678
+ * @typedef {ExtMetadata<ExtType> & { pkgName: string, version: string } & import('../../types/external-manifest').CommonMetadata} ExtensionFields
679
+ */
680
+
681
+ /**
682
+ * @template {ExtensionType} ExtType
683
+ * @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
684
+ */