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.
- package/README.md +9 -9
- package/build/lib/appium-config.schema.json +0 -0
- package/build/lib/appium.js +157 -53
- package/build/lib/cli/argparse-actions.js +104 -0
- package/build/lib/cli/args.js +115 -279
- package/build/lib/cli/driver-command.js +11 -1
- package/build/lib/cli/extension-command.js +60 -8
- package/build/lib/cli/extension.js +30 -7
- package/build/lib/cli/npm.js +17 -14
- package/build/lib/cli/parser.js +152 -89
- package/build/lib/cli/plugin-command.js +11 -1
- package/build/lib/cli/utils.js +29 -3
- package/build/lib/config-file.js +141 -0
- package/build/lib/config.js +76 -61
- package/build/lib/driver-config.js +42 -19
- package/build/lib/drivers.js +8 -4
- package/build/lib/ext-config-io.js +165 -0
- package/build/lib/extension-config.js +130 -61
- package/build/lib/grid-register.js +22 -24
- package/build/lib/logger.js +3 -3
- package/build/lib/logsink.js +11 -13
- package/build/lib/main.js +197 -77
- package/build/lib/plugin-config.js +20 -10
- package/build/lib/plugins.js +4 -2
- package/build/lib/schema/appium-config-schema.js +252 -0
- package/build/lib/schema/arg-spec.js +120 -0
- package/build/lib/schema/cli-args.js +173 -0
- package/build/lib/schema/cli-transformers.js +76 -0
- package/build/lib/schema/index.js +36 -0
- package/build/lib/schema/keywords.js +62 -0
- package/build/lib/schema/schema.js +357 -0
- package/build/lib/utils.js +44 -99
- package/lib/appium-config.schema.json +277 -0
- package/lib/appium.js +201 -65
- package/lib/cli/argparse-actions.js +77 -0
- package/lib/cli/args.js +174 -375
- package/lib/cli/driver-command.js +4 -0
- package/lib/cli/extension-command.js +70 -5
- package/lib/cli/extension.js +25 -5
- package/lib/cli/npm.js +18 -12
- package/lib/cli/parser.js +254 -79
- package/lib/cli/plugin-command.js +4 -0
- package/lib/cli/utils.js +21 -1
- package/lib/config-file.js +227 -0
- package/lib/config.js +109 -62
- package/lib/driver-config.js +66 -11
- package/lib/drivers.js +4 -1
- package/lib/ext-config-io.js +287 -0
- package/lib/extension-config.js +225 -67
- package/lib/grid-register.js +27 -24
- package/lib/logger.js +1 -1
- package/lib/logsink.js +10 -7
- package/lib/main.js +211 -77
- package/lib/plugin-config.js +34 -5
- package/lib/plugins.js +1 -0
- package/lib/schema/appium-config-schema.js +286 -0
- package/lib/schema/arg-spec.js +218 -0
- package/lib/schema/cli-args.js +273 -0
- package/lib/schema/cli-transformers.js +123 -0
- package/lib/schema/index.js +2 -0
- package/lib/schema/keywords.js +119 -0
- package/lib/schema/schema.js +577 -0
- package/lib/utils.js +42 -88
- package/package.json +55 -80
- package/postinstall.js +71 -0
- package/types/appium-config.d.ts +197 -0
- package/types/types.d.ts +201 -0
- package/CHANGELOG.md +0 -3515
- package/build/lib/cli/parser-helpers.js +0 -82
- 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
|
+
*/
|
package/lib/extension-config.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import _ from 'lodash';
|
|
2
|
-
import
|
|
3
|
-
import { fs, mkdirp } from 'appium-support';
|
|
4
|
+
import os from 'os';
|
|
4
5
|
import path from 'path';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
12
|
-
const
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
this.
|
|
37
|
-
|
|
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.
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
|