appium 2.0.0-beta.2 → 2.0.0-beta.20

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 (70) hide show
  1. package/README.md +9 -9
  2. package/build/lib/appium-config.schema.json +0 -0
  3. package/build/lib/appium.js +157 -53
  4. package/build/lib/cli/argparse-actions.js +104 -0
  5. package/build/lib/cli/args.js +115 -279
  6. package/build/lib/cli/driver-command.js +11 -1
  7. package/build/lib/cli/extension-command.js +60 -8
  8. package/build/lib/cli/extension.js +30 -7
  9. package/build/lib/cli/npm.js +17 -14
  10. package/build/lib/cli/parser.js +152 -89
  11. package/build/lib/cli/plugin-command.js +11 -1
  12. package/build/lib/cli/utils.js +29 -3
  13. package/build/lib/config-file.js +141 -0
  14. package/build/lib/config.js +76 -61
  15. package/build/lib/driver-config.js +42 -19
  16. package/build/lib/drivers.js +8 -4
  17. package/build/lib/ext-config-io.js +165 -0
  18. package/build/lib/extension-config.js +130 -61
  19. package/build/lib/grid-register.js +22 -24
  20. package/build/lib/logger.js +3 -3
  21. package/build/lib/logsink.js +11 -13
  22. package/build/lib/main.js +197 -77
  23. package/build/lib/plugin-config.js +20 -10
  24. package/build/lib/plugins.js +4 -2
  25. package/build/lib/schema/appium-config-schema.js +252 -0
  26. package/build/lib/schema/arg-spec.js +120 -0
  27. package/build/lib/schema/cli-args.js +173 -0
  28. package/build/lib/schema/cli-transformers.js +76 -0
  29. package/build/lib/schema/index.js +36 -0
  30. package/build/lib/schema/keywords.js +62 -0
  31. package/build/lib/schema/schema.js +357 -0
  32. package/build/lib/utils.js +44 -99
  33. package/lib/appium-config.schema.json +277 -0
  34. package/lib/appium.js +201 -65
  35. package/lib/cli/argparse-actions.js +77 -0
  36. package/lib/cli/args.js +174 -375
  37. package/lib/cli/driver-command.js +4 -0
  38. package/lib/cli/extension-command.js +70 -5
  39. package/lib/cli/extension.js +25 -5
  40. package/lib/cli/npm.js +18 -12
  41. package/lib/cli/parser.js +254 -79
  42. package/lib/cli/plugin-command.js +4 -0
  43. package/lib/cli/utils.js +21 -1
  44. package/lib/config-file.js +227 -0
  45. package/lib/config.js +109 -62
  46. package/lib/driver-config.js +66 -11
  47. package/lib/drivers.js +4 -1
  48. package/lib/ext-config-io.js +287 -0
  49. package/lib/extension-config.js +225 -67
  50. package/lib/grid-register.js +27 -24
  51. package/lib/logger.js +1 -1
  52. package/lib/logsink.js +10 -7
  53. package/lib/main.js +211 -77
  54. package/lib/plugin-config.js +34 -5
  55. package/lib/plugins.js +1 -0
  56. package/lib/schema/appium-config-schema.js +286 -0
  57. package/lib/schema/arg-spec.js +218 -0
  58. package/lib/schema/cli-args.js +273 -0
  59. package/lib/schema/cli-transformers.js +123 -0
  60. package/lib/schema/index.js +2 -0
  61. package/lib/schema/keywords.js +119 -0
  62. package/lib/schema/schema.js +577 -0
  63. package/lib/utils.js +42 -88
  64. package/package.json +55 -80
  65. package/postinstall.js +71 -0
  66. package/types/appium-config.d.ts +197 -0
  67. package/types/types.d.ts +201 -0
  68. package/CHANGELOG.md +0 -3515
  69. package/build/lib/cli/parser-helpers.js +0 -82
  70. package/lib/cli/parser-helpers.js +0 -79
@@ -0,0 +1,287 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Module containing {@link ExtConfigIO} which handles reading & writing of extension config files.
5
+ */
6
+
7
+ import { fs, mkdirp } from '@appium/support';
8
+ import _ from 'lodash';
9
+ import path from 'path';
10
+ import YAML from 'yaml';
11
+
12
+ const CONFIG_FILE_NAME = 'extensions.yaml';
13
+
14
+ /**
15
+ * Current configuration schema revision!
16
+ */
17
+ const CONFIG_SCHEMA_REV = 2;
18
+
19
+ export const DRIVER_TYPE = 'driver';
20
+ export const PLUGIN_TYPE = 'plugin';
21
+
22
+ /**
23
+ * Set of valid extension types.
24
+ * @type {Readonly<Set<ExtensionType>>}
25
+ */
26
+ const VALID_EXT_TYPES = new Set([DRIVER_TYPE, PLUGIN_TYPE]);
27
+
28
+ const CONFIG_DATA_DRIVER_KEY = `${DRIVER_TYPE}s`;
29
+ const CONFIG_DATA_PLUGIN_KEY = `${PLUGIN_TYPE}s`;
30
+
31
+ /**
32
+ * Handles reading & writing of extension config files.
33
+ *
34
+ * Only one instance of this class exists per value of `APPIUM_HOME`.
35
+ */
36
+ class ExtConfigIO {
37
+ /**
38
+ * "Dirty" flag. If true, the data has changed since the last write.
39
+ * @type {boolean}
40
+ * @private
41
+ */
42
+ _dirty;
43
+
44
+ /**
45
+ * The entire contents of a parsed YAML extension config file.
46
+ * @type {object?}
47
+ * @private
48
+ */
49
+ _data;
50
+
51
+ /**
52
+ * A mapping of extension type to configuration data. Configuration data is
53
+ * keyed on extension name.
54
+ *
55
+ * Consumers get the values of this `Map` (corresponding to the
56
+ * `extensionType` of the consumer, which will be a subclass of
57
+ * `ExtensionConfig`) and do not have access to the entire data object.
58
+ * @private
59
+ * @type {Map<ExtensionType,object>}
60
+ */
61
+ _extDataByType = new Map();
62
+
63
+ /**
64
+ * Path to config file.
65
+ * @private
66
+ * @type {Readonly<string>}
67
+ */
68
+ _filepath;
69
+
70
+ /**
71
+ * Path to `APPIUM_HOME`
72
+ * @private
73
+ * @type {Readonly<string>}
74
+ */
75
+ _appiumHome;
76
+
77
+ /**
78
+ * Helps avoid writing multiple times.
79
+ *
80
+ * If this is `null`, calling {@link ExtConfigIO.write} will cause it to be
81
+ * set to a `Promise`. When the call to `write()` is complete, the `Promise`
82
+ * will resolve and then this value will be set to `null`. Concurrent calls
83
+ * made while this value is a `Promise` will return the `Promise` itself.
84
+ * @private
85
+ * @type {Promise<boolean>?}
86
+ */
87
+ _writing = null;
88
+
89
+ /**
90
+ * Helps avoid reading multiple times.
91
+ *
92
+ * If this is `null`, calling {@link ExtConfigIO.read} will cause it to be
93
+ * set to a `Promise`. When the call to `read()` is complete, the `Promise`
94
+ * will resolve and then this value will be set to `null`. Concurrent calls
95
+ * made while this value is a `Promise` will return the `Promise` itself.
96
+ * @private
97
+ * @type {Promise<void>?}
98
+ */
99
+ _reading = null;
100
+
101
+ /**
102
+ * @param {string} appiumHome
103
+ */
104
+ constructor (appiumHome) {
105
+ this._filepath = path.resolve(appiumHome, CONFIG_FILE_NAME);
106
+ this._appiumHome = appiumHome;
107
+ }
108
+
109
+ /**
110
+ * Creaes a `Proxy` which watches for changes to the extension-type-specific
111
+ * config data.
112
+ *
113
+ * When changes are detected, it sets a `_dirty` flag. The next call to
114
+ * {@link ExtConfigIO.write} will check if this flag is `true` before
115
+ * proceeding.
116
+ * @param {ExtensionType} extensionType
117
+ * @param {Record<string,object>} data - Extension config data, keyed by name
118
+ * @private
119
+ * @returns {Record<string,object>}
120
+ */
121
+ _createProxy (extensionType, data) {
122
+ return new Proxy(data[`${extensionType}s`], {
123
+ set: (target, prop, value) => {
124
+ if (value !== target[prop]) {
125
+ this._dirty = true;
126
+ }
127
+ target[prop] = value;
128
+ return Reflect.set(target, prop, value);
129
+ },
130
+ deleteProperty: (target, prop) => {
131
+ if (prop in target) {
132
+ this._dirty = true;
133
+ }
134
+ return Reflect.deleteProperty(target, prop);
135
+ },
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Returns the path to the config file.
141
+ */
142
+ get filepath () {
143
+ return this._filepath;
144
+ }
145
+
146
+ /**
147
+ * Gets data for an extension type. Reads the config file if necessary.
148
+ *
149
+ * Force-reading is _not_ supported, as it's likely to be a source of
150
+ * bugs--it's easy to mutate the data and then overwrite memory with the file
151
+ * contents
152
+ *
153
+ * Ideally this will only ever read the file _once_.
154
+ * @param {ExtensionType} extensionType - Which bit of the config data we
155
+ * want
156
+ * @returns {Promise<object>} The data
157
+ */
158
+ async read (extensionType) {
159
+ if (this._reading) {
160
+ await this._reading;
161
+ return this._extDataByType.get(extensionType);
162
+ }
163
+
164
+ this._reading = (async () => {
165
+ if (!VALID_EXT_TYPES.has(extensionType)) {
166
+ throw new TypeError(
167
+ `Invalid extension type: ${extensionType}. Valid values are: ${[
168
+ ...VALID_EXT_TYPES,
169
+ ].join(', ')}`,
170
+ );
171
+ }
172
+ if (this._extDataByType.has(extensionType)) {
173
+ return;
174
+ }
175
+
176
+ let data;
177
+ let isNewFile = false;
178
+ try {
179
+ await mkdirp(this._appiumHome);
180
+ const yaml = await fs.readFile(this.filepath, 'utf8');
181
+ data = YAML.parse(yaml);
182
+ } catch (err) {
183
+ if (err.code === 'ENOENT') {
184
+ data = {
185
+ [CONFIG_DATA_DRIVER_KEY]: {},
186
+ [CONFIG_DATA_PLUGIN_KEY]: {},
187
+ schemaRev: CONFIG_SCHEMA_REV,
188
+ };
189
+ isNewFile = true;
190
+ } else {
191
+ throw new Error(
192
+ `Appium had trouble loading the extension installation ` +
193
+ `cache file (${this.filepath}). Ensure it exists and is ` +
194
+ `readable. Specific error: ${err.message}`,
195
+ );
196
+ }
197
+ }
198
+
199
+ this._data = data;
200
+ this._extDataByType.set(
201
+ DRIVER_TYPE,
202
+ this._createProxy(DRIVER_TYPE, data),
203
+ );
204
+ this._extDataByType.set(
205
+ PLUGIN_TYPE,
206
+ this._createProxy(PLUGIN_TYPE, data),
207
+ );
208
+
209
+ if (isNewFile) {
210
+ await this.write(true);
211
+ }
212
+ })();
213
+ try {
214
+ await this._reading;
215
+ return this._extDataByType.get(extensionType);
216
+ } finally {
217
+ this._reading = null;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Writes the data if it needs writing.
223
+ *
224
+ * If the `schemaRev` prop needs updating, the file will be written.
225
+ * @param {boolean} [force=false] - Whether to force a write even if the data is clean
226
+ * @returns {Promise<boolean>} Whether the data was written
227
+ */
228
+ async write (force = false) {
229
+ if (this._writing) {
230
+ return this._writing;
231
+ }
232
+ this._writing = (async () => {
233
+ try {
234
+ if (!this._dirty && !force) {
235
+ return false;
236
+ }
237
+
238
+ if (!this._data) {
239
+ throw new ReferenceError('No data to write. Call `read()` first');
240
+ }
241
+
242
+ const dataToWrite = {
243
+ ...this._data,
244
+ [CONFIG_DATA_DRIVER_KEY]: this._extDataByType.get(DRIVER_TYPE),
245
+ [CONFIG_DATA_PLUGIN_KEY]: this._extDataByType.get(PLUGIN_TYPE),
246
+ };
247
+
248
+ try {
249
+ await fs.writeFile(
250
+ this.filepath,
251
+ YAML.stringify(dataToWrite),
252
+ 'utf8',
253
+ );
254
+ this._dirty = false;
255
+ return true;
256
+ } catch {
257
+ throw new Error(
258
+ `Appium could not parse or write from the Appium Home directory ` +
259
+ `(${this._appiumHome}). Please ensure it is writable.`,
260
+ );
261
+ }
262
+ } finally {
263
+ this._writing = null;
264
+ }
265
+ })();
266
+ return await this._writing;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Factory function for {@link ExtConfigIO}.
272
+ *
273
+ * Maintains one instance per value of `APPIUM_HOME`.
274
+ * @param {string} appiumHome - `APPIUM_HOME`
275
+ * @returns {ExtConfigIO}
276
+ */
277
+ export const getExtConfigIOInstance = _.memoize(
278
+ (appiumHome) => new ExtConfigIO(appiumHome),
279
+ );
280
+
281
+ /**
282
+ * @typedef {ExtConfigIO} ExtensionConfigIO
283
+ */
284
+
285
+ /**
286
+ * @typedef {typeof DRIVER_TYPE | typeof PLUGIN_TYPE} ExtensionType
287
+ */
@@ -1,15 +1,15 @@
1
+ // @ts-check
2
+
1
3
  import _ from 'lodash';
2
- import log from './logger';
3
- import { fs, mkdirp } from 'appium-support';
4
+ import os from 'os';
4
5
  import path from 'path';
5
- import YAML from 'yaml';
6
-
7
- const DRIVER_TYPE = 'driver';
8
- const PLUGIN_TYPE = 'plugin';
9
- const DEFAULT_APPIUM_HOME = path.resolve(process.env.HOME, '.appium');
6
+ import resolveFrom from 'resolve-from';
7
+ import { getExtConfigIOInstance } from './ext-config-io';
8
+ import log from './logger';
9
+ import { ALLOWED_SCHEMA_EXTENSIONS, isAllowedSchemaFileExtension, registerSchema } from './schema/schema';
10
10
 
11
- const CONFIG_FILE_NAME = 'extensions.yaml';
12
- const CONFIG_SCHEMA_REV = 2;
11
+ const DEFAULT_APPIUM_HOME = path.resolve(os.homedir(), '.appium');
12
+ const APPIUM_HOME = process.env.APPIUM_HOME || DEFAULT_APPIUM_HOME;
13
13
 
14
14
  const INSTALL_TYPE_NPM = 'npm';
15
15
  const INSTALL_TYPE_LOCAL = 'local';
@@ -22,27 +22,44 @@ const INSTALL_TYPES = [
22
22
  INSTALL_TYPE_NPM
23
23
  ];
24
24
 
25
-
26
25
  export default class ExtensionConfig {
27
- constructor (appiumHome, extensionType, logFn = null) {
28
- if (logFn === null) {
29
- logFn = log.error.bind(log);
30
- }
26
+ /**
27
+ *
28
+ * @param {string} appiumHome - `APPIUM_HOME`
29
+ * @param {ExtensionType} extensionType - Type of extension
30
+ * @param {(...args: any[]) => void} [logFn]
31
+ */
32
+ constructor (appiumHome, extensionType, logFn) {
33
+ const logger = _.isFunction(logFn) ? logFn : log.error.bind(log);
34
+ /** @type {string} */
31
35
  this.appiumHome = appiumHome;
32
- this.configFile = path.resolve(this.appiumHome, CONFIG_FILE_NAME);
36
+ /** @type {Record<string,object>} */
33
37
  this.installedExtensions = {};
38
+ /** @type {import('./ext-config-io').ExtensionConfigIO} */
39
+ this.io = getExtConfigIOInstance(appiumHome);
40
+ /** @type {ExtensionType} */
34
41
  this.extensionType = extensionType;
35
- this.configKey = `${extensionType}s`;
36
- this.yamlData = {[`${DRIVER_TYPE}s`]: {}, [`${PLUGIN_TYPE}s`]: {}};
37
- this.log = logFn;
42
+ /** @type {'drivers'|'plugins'} */
43
+ this.configKey = `${extensionType}s`; // todo use template type
44
+ /**
45
+ * @type {(...args: any[])=>void}
46
+ */
47
+ this.log = logger;
38
48
  }
39
49
 
50
+ /**
51
+ * Checks extensions for problems
52
+ * @template ExtData
53
+ * @param {ExtData[]} exts - Array of extData objects
54
+ * @returns {ExtData[]}
55
+ */
40
56
  validate (exts) {
41
57
  const foundProblems = {};
42
58
  for (const [extName, extData] of _.toPairs(exts)) {
43
59
  foundProblems[extName] = [
44
- ...this.getGenericConfigProblems(extData),
45
- ...this.getConfigProblems(extData)
60
+ ...this.getGenericConfigProblems(extData, extName),
61
+ ...this.getConfigProblems(extData, extName),
62
+ ...this.getSchemaProblems(extData, extName)
46
63
  ];
47
64
  }
48
65
 
@@ -63,7 +80,7 @@ export default class ExtensionConfig {
63
80
 
64
81
  if (!_.isEmpty(problemSummaries)) {
65
82
  this.log(`Appium encountered one or more errors while validating ` +
66
- `the ${this.configKey} extension file (${this.configFile}):`);
83
+ `the ${this.configKey} extension file (${this.io.filepath}):`);
67
84
  for (const summary of problemSummaries) {
68
85
  this.log(summary);
69
86
  }
@@ -72,8 +89,52 @@ export default class ExtensionConfig {
72
89
  return exts;
73
90
  }
74
91
 
75
- getGenericConfigProblems (ext) {
76
- const {version, pkgName, installSpec, installType, installPath, mainClass} = ext;
92
+ /**
93
+ * @param {object} extData
94
+ * @param {string} extName
95
+ * @returns {Problem[]}
96
+ */
97
+ getSchemaProblems (extData, extName) {
98
+ const problems = [];
99
+ const {schema: argSchemaPath} = extData;
100
+ if (argSchemaPath) {
101
+ if (_.isString(argSchemaPath)) {
102
+ if (isAllowedSchemaFileExtension(argSchemaPath)) {
103
+ try {
104
+ this.readExtensionSchema(extName, extData);
105
+ } catch (err) {
106
+ problems.push({err: `Unable to register schema at path ${argSchemaPath}; ${err.message}`, val: argSchemaPath});
107
+ }
108
+ } else {
109
+ problems.push({
110
+ err: `Schema file has unsupported extension. Allowed: ${[...ALLOWED_SCHEMA_EXTENSIONS].join(', ')}`,
111
+ val: argSchemaPath
112
+ });
113
+ }
114
+ } else if (_.isPlainObject(argSchemaPath)) {
115
+ try {
116
+ this.readExtensionSchema(extName, extData);
117
+ } catch (err) {
118
+ problems.push({err: `Unable to register embedded schema; ${err.message}`, val: argSchemaPath});
119
+ }
120
+ } else {
121
+ problems.push({
122
+ err: 'Incorrectly formatted schema field; must be a path to a schema file or a schema object.',
123
+ val: argSchemaPath
124
+ });
125
+ }
126
+ }
127
+ return problems;
128
+ }
129
+
130
+ /**
131
+ * @param {object} extData
132
+ * @param {string} extName
133
+ * @returns {Problem[]}
134
+ */
135
+ // eslint-disable-next-line no-unused-vars
136
+ getGenericConfigProblems (extData, extName) {
137
+ const {version, pkgName, installSpec, installType, installPath, mainClass} = extData;
77
138
  const problems = [];
78
139
 
79
140
  if (!_.isString(version)) {
@@ -103,62 +164,60 @@ export default class ExtensionConfig {
103
164
  return problems;
104
165
  }
105
166
 
106
- getConfigProblems (/*ext*/) {
167
+ /**
168
+ * @param {object} extData
169
+ * @param {string} extName
170
+ * @returns {Problem[]}
171
+ */
172
+ // eslint-disable-next-line no-unused-vars
173
+ getConfigProblems (extData, extName) {
107
174
  // shoud override this method if special validation is necessary for this extension type
108
175
  return [];
109
176
  }
110
177
 
111
- applySchemaMigrations () {
112
- if (this.yamlData.schemaRev < 2 && _.isUndefined(this.yamlData[PLUGIN_TYPE])) {
113
- // at schema revision 2, we started including plugins as well as drivers in the file,
114
- // so make sure we at least have an empty section for it
115
- this.yamlData[PLUGIN_TYPE] = {};
116
- }
117
- }
118
-
178
+ /**
179
+ * @returns {Promise<typeof this.installedExtensions>}
180
+ */
119
181
  async read () {
120
- await mkdirp(this.appiumHome); // ensure appium home exists
121
- try {
122
- this.yamlData = YAML.parse(await fs.readFile(this.configFile, 'utf8'));
123
- this.applySchemaMigrations();
124
-
125
- // set the list of drivers the user has installed
126
- this.installedExtensions = this.validate(this.yamlData[this.configKey]);
127
- } catch (err) {
128
- if (await fs.exists(this.configFile)) {
129
- // if the file exists and we couldn't parse it, that's a problem
130
- throw new Error(`Appium had trouble loading the extension installation ` +
131
- `cache file (${this.configFile}). Ensure it exists and is ` +
132
- `readable. Specific error: ${err.message}`);
133
- }
134
-
135
- // if the config file doesn't exist, try to write an empty one, to make
136
- // sure we actually have write privileges, and complain if we don't
137
- try {
138
- await this.write();
139
- } catch {
140
- throw new Error(`Appium could not read or write from the Appium Home directory ` +
141
- `(${this.appiumHome}). Please ensure it is writable.`);
142
- }
143
- }
182
+ const extensions = await this.io.read(this.extensionType);
183
+ this.installedExtensions = this.validate(extensions);
144
184
  return this.installedExtensions;
145
185
  }
146
186
 
147
-
187
+ /**
188
+ * @returns {Promise<boolean>}
189
+ */
148
190
  async write () {
149
- const newYamlData = {
150
- ...this.yamlData,
151
- schemaRev: CONFIG_SCHEMA_REV,
152
- [this.configKey]: this.installedExtensions
153
- };
154
- await fs.writeFile(this.configFile, YAML.stringify(newYamlData), 'utf8');
191
+ return await this.io.write();
155
192
  }
156
193
 
194
+ /**
195
+ * @param {string} extName
196
+ * @param {object} extData
197
+ * @returns {Promise<void>}
198
+ */
157
199
  async addExtension (extName, extData) {
158
200
  this.installedExtensions[extName] = extData;
159
201
  await this.write();
160
202
  }
161
203
 
204
+ /**
205
+ * @param {string} extName
206
+ * @param {object} extData
207
+ * @returns {Promise<void>}
208
+ */
209
+ async updateExtension (extName, extData) {
210
+ this.installedExtensions[extName] = {
211
+ ...this.installedExtensions[extName],
212
+ ...extData,
213
+ };
214
+ await this.write();
215
+ }
216
+
217
+ /**
218
+ * @param {string} extName
219
+ * @returns {Promise<void>}
220
+ */
162
221
  async removeExtension (extName) {
163
222
  delete this.installedExtensions[extName];
164
223
  await this.write();
@@ -178,31 +237,130 @@ export default class ExtensionConfig {
178
237
  }
179
238
  }
180
239
 
181
- extensionDesc () {
182
- throw new Error('This must be implemented in a final class');
240
+ /**
241
+ * Returns a string describing the extension. Subclasses must implement.
242
+ * @param {string} extName - Extension name
243
+ * @param {object} extData - Extension data
244
+ * @returns {string}
245
+ * @abstract
246
+ */
247
+ // eslint-disable-next-line no-unused-vars
248
+ extensionDesc (extName, extData) {
249
+ throw new Error('This must be implemented in a subclass');
183
250
  }
184
251
 
252
+ /**
253
+ * @param {string} extName
254
+ * @returns {string}
255
+ */
185
256
  getExtensionRequirePath (extName) {
186
257
  const {pkgName, installPath} = this.installedExtensions[extName];
187
258
  return path.resolve(this.appiumHome, installPath, 'node_modules', pkgName);
188
259
  }
189
260
 
261
+ /**
262
+ * @param {string} extName
263
+ * @returns {string}
264
+ */
190
265
  getInstallPath (extName) {
191
266
  const {installPath} = this.installedExtensions[extName];
192
267
  return path.resolve(this.appiumHome, installPath);
193
268
  }
194
269
 
270
+ /**
271
+ * Loads extension and returns its main class
272
+ * @param {string} extName
273
+ * @returns {(...args: any[]) => object }
274
+ */
195
275
  require (extName) {
196
276
  const {mainClass} = this.installedExtensions[extName];
197
- return require(this.getExtensionRequirePath(extName))[mainClass];
277
+ const reqPath = this.getExtensionRequirePath(extName);
278
+ const reqResolved = require.resolve(reqPath);
279
+ if (process.env.APPIUM_RELOAD_EXTENSIONS && require.cache[reqResolved]) {
280
+ log.debug(`Removing ${reqResolved} from require cache`);
281
+ delete require.cache[reqResolved];
282
+ }
283
+ return require(reqPath)[mainClass];
198
284
  }
199
285
 
286
+ /**
287
+ * @param {string} extName
288
+ * @returns {boolean}
289
+ */
200
290
  isInstalled (extName) {
201
291
  return _.includes(Object.keys(this.installedExtensions), extName);
202
292
  }
293
+
294
+ /**
295
+ * Intended to be called by corresponding instance methods of subclass.
296
+ * @private
297
+ * @param {string} appiumHome
298
+ * @param {ExtensionType} extType
299
+ * @param {string} extName - Extension name (unique to its type)
300
+ * @param {ExtData} extData - Extension config
301
+ * @returns {import('ajv').SchemaObject|undefined}
302
+ */
303
+ static _readExtensionSchema (appiumHome, extType, extName, extData) {
304
+ const {installPath, pkgName, schema: argSchemaPath} = extData;
305
+ if (!argSchemaPath) {
306
+ throw new TypeError(
307
+ `No \`schema\` property found in config for ${extType} ${pkgName} -- why is this function being called?`,
308
+ );
309
+ }
310
+ let moduleObject;
311
+ if (_.isString(argSchemaPath)) {
312
+ const schemaPath = resolveFrom(
313
+ path.resolve(appiumHome, installPath),
314
+ // this path sep is fine because `resolveFrom` uses Node's module resolution
315
+ path.normalize(`${pkgName}/${argSchemaPath}`),
316
+ );
317
+ moduleObject = require(schemaPath);
318
+ } else {
319
+ moduleObject = argSchemaPath;
320
+ }
321
+ // this sucks. default exports should be destroyed
322
+ const schema = moduleObject.__esModule
323
+ ? moduleObject.default
324
+ : moduleObject;
325
+ registerSchema(extType, extName, schema);
326
+ return schema;
327
+ }
328
+
329
+ /**
330
+ * If an extension provides a schema, this will load the schema and attempt to
331
+ * register it with the schema registrar.
332
+ * @param {string} extName - Name of extension
333
+ * @param {ExtData} extData - Extension data
334
+ * @returns {import('ajv').SchemaObject|undefined}
335
+ */
336
+ readExtensionSchema (extName, extData) {
337
+ return ExtensionConfig._readExtensionSchema(this.appiumHome, this.extensionType, extName, extData);
338
+ }
203
339
  }
204
340
 
341
+ export { DRIVER_TYPE, PLUGIN_TYPE } from './ext-config-io';
205
342
  export {
206
343
  INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GITHUB,
207
- INSTALL_TYPES, DEFAULT_APPIUM_HOME, DRIVER_TYPE, PLUGIN_TYPE,
344
+ INSTALL_TYPES, DEFAULT_APPIUM_HOME, APPIUM_HOME
208
345
  };
346
+
347
+ /**
348
+ * Config problem
349
+ * @typedef {Object} Problem
350
+ * @property {string} err - Error message
351
+ * @property {any} val - Associated value
352
+ */
353
+
354
+ /**
355
+ * Alias
356
+ * @typedef {import('./ext-config-io').ExtensionType} ExtensionType
357
+ */
358
+
359
+ /**
360
+ * Extension data (pulled from config YAML)
361
+ * @typedef {Object} ExtData
362
+ * @property {string|import('ajv').SchemaObject} [schema] - Optional schema path if the ext defined it
363
+ * @property {string} pkgName - Package name
364
+ * @property {string} installPath - Actually looks more like a module identifier? Resolved from `APPIUM_HOME`
365
+ */
366
+