appium 2.0.0-beta.17 → 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/build/lib/appium-config.schema.json +0 -0
- package/build/lib/appium.js +84 -69
- package/build/lib/cli/argparse-actions.js +1 -1
- package/build/lib/cli/args.js +87 -223
- package/build/lib/cli/extension-command.js +2 -2
- package/build/lib/cli/extension.js +14 -6
- package/build/lib/cli/parser.js +142 -106
- package/build/lib/cli/utils.js +1 -1
- package/build/lib/config-file.js +141 -0
- package/build/lib/config.js +42 -64
- package/build/lib/driver-config.js +41 -20
- package/build/lib/drivers.js +1 -1
- package/build/lib/ext-config-io.js +165 -0
- package/build/lib/extension-config.js +110 -60
- package/build/lib/grid-register.js +19 -21
- package/build/lib/logsink.js +1 -1
- package/build/lib/main.js +135 -72
- package/build/lib/plugin-config.js +17 -8
- 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 +26 -35
- package/lib/appium-config.schema.json +277 -0
- package/lib/appium.js +99 -75
- package/lib/cli/args.js +138 -335
- package/lib/cli/extension-command.js +7 -6
- package/lib/cli/extension.js +12 -4
- package/lib/cli/parser.js +248 -96
- package/lib/config-file.js +227 -0
- package/lib/config.js +71 -61
- package/lib/driver-config.js +66 -11
- package/lib/ext-config-io.js +287 -0
- package/lib/extension-config.js +209 -66
- package/lib/grid-register.js +24 -21
- package/lib/main.js +139 -68
- package/lib/plugin-config.js +32 -2
- 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 +29 -52
- package/package.json +16 -11
- package/types/appium-config.d.ts +197 -0
- package/types/types.d.ts +201 -0
- package/build/lib/cli/parser-helpers.js +0 -106
- package/lib/cli/parser-helpers.js +0 -106
package/lib/cli/parser.js
CHANGED
|
@@ -1,121 +1,273 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { fs } from '@appium/support';
|
|
3
4
|
import { ArgumentParser } from 'argparse';
|
|
4
|
-
import
|
|
5
|
+
import B from 'bluebird';
|
|
6
|
+
import _ from 'lodash';
|
|
7
|
+
import path from 'path';
|
|
5
8
|
import { DRIVER_TYPE, PLUGIN_TYPE } from '../extension-config';
|
|
9
|
+
import { finalizeSchema, getArgSpec, hasArgSpec } from '../schema';
|
|
6
10
|
import { rootDir } from '../utils';
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
driverConfig,
|
|
13
|
+
getExtensionArgs,
|
|
14
|
+
getServerArgs,
|
|
15
|
+
pluginConfig
|
|
16
|
+
} from './args';
|
|
8
17
|
|
|
18
|
+
export const SERVER_SUBCOMMAND = 'server';
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
/**
|
|
21
|
+
* If the parsed args do not contain any of these values, then we
|
|
22
|
+
* will automatially inject the `server` subcommand.
|
|
23
|
+
*/
|
|
24
|
+
const NON_SERVER_ARGS = Object.freeze(
|
|
25
|
+
new Set([
|
|
26
|
+
DRIVER_TYPE,
|
|
27
|
+
PLUGIN_TYPE,
|
|
28
|
+
SERVER_SUBCOMMAND,
|
|
29
|
+
'-h',
|
|
30
|
+
'--help',
|
|
31
|
+
'-v',
|
|
32
|
+
'--version'
|
|
33
|
+
])
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const version = fs.readPackageJsonFrom(rootDir).version;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A wrapper around `argparse`
|
|
40
|
+
*
|
|
41
|
+
* - Handles instantiation, configuration, and monkeypatching of an
|
|
42
|
+
* `ArgumentParser` instance for Appium server and its extensions
|
|
43
|
+
* - Handles error conditions, messages, and exit behavior
|
|
44
|
+
*/
|
|
45
|
+
class ArgParser {
|
|
46
|
+
/**
|
|
47
|
+
* @param {boolean} [debug] - If true, throw instead of exit on error.
|
|
48
|
+
*/
|
|
49
|
+
constructor (debug = false) {
|
|
50
|
+
const prog = process.argv[1] ? path.basename(process.argv[1]) : 'appium';
|
|
51
|
+
const parser = new ArgumentParser({
|
|
52
|
+
add_help: true,
|
|
53
|
+
description:
|
|
54
|
+
'A webdriver-compatible server that facilitates automation of web, mobile, and other types of apps across various platforms.',
|
|
55
|
+
prog,
|
|
56
|
+
});
|
|
15
57
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
58
|
+
ArgParser._patchExit(parser);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Program name (typically `appium`)
|
|
62
|
+
* @type {string}
|
|
63
|
+
*/
|
|
64
|
+
this.prog = prog;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* If `true`, throw an error on parse failure instead of printing help
|
|
68
|
+
* @type {boolean}
|
|
69
|
+
*/
|
|
70
|
+
this.debug = debug;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Wrapped `ArgumentParser` instance
|
|
74
|
+
* @type {ArgumentParser}
|
|
75
|
+
*/
|
|
76
|
+
this.parser = parser;
|
|
77
|
+
|
|
78
|
+
parser.add_argument('-v', '--version', {
|
|
79
|
+
action: 'version',
|
|
80
|
+
version,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const subParsers = parser.add_subparsers({dest: 'subcommand'});
|
|
84
|
+
|
|
85
|
+
// add the 'server' subcommand, and store the raw arguments on the parser
|
|
86
|
+
// object as a way for other parts of the code to work with the arguments
|
|
87
|
+
// conceptually rather than just through argparse
|
|
88
|
+
const serverArgs = ArgParser._addServerToParser(subParsers);
|
|
89
|
+
|
|
90
|
+
this.rawArgs = serverArgs;
|
|
91
|
+
|
|
92
|
+
// add the 'driver' and 'plugin' subcommands
|
|
93
|
+
ArgParser._addExtensionCommandsToParser(subParsers);
|
|
94
|
+
|
|
95
|
+
// backwards compatibility / drop-in wrapper
|
|
96
|
+
/**
|
|
97
|
+
* @type {ArgParser['parseArgs']}
|
|
98
|
+
*/
|
|
99
|
+
this.parse_args = this.parseArgs;
|
|
24
100
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
addExtensionsToParser(sharedArgs, subParsers, debug);
|
|
39
|
-
|
|
40
|
-
// modify the parse_args function to insert the 'server' subcommand if the
|
|
41
|
-
// user hasn't specified a subcommand or the global help command
|
|
42
|
-
parser._parse_args = parser.parse_args.bind(parser);
|
|
43
|
-
parser.parse_args = function (args, namespace) {
|
|
44
|
-
if (_.isUndefined(args)) {
|
|
45
|
-
args = [...process.argv.slice(2)];
|
|
46
|
-
}
|
|
47
|
-
if (!_.includes([DRIVER_TYPE, PLUGIN_TYPE, 'server', '-h', '--help', '-v', '--version'], args[0])) {
|
|
48
|
-
args.splice(0, 0, 'server');
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse arguments from the command line.
|
|
104
|
+
*
|
|
105
|
+
* If no subcommand is passed in, this method will inject the `server` subcommand.
|
|
106
|
+
*
|
|
107
|
+
* `ArgParser.prototype.parse_args` is an alias of this method.
|
|
108
|
+
* @param {string[]} [args] - Array of arguments, ostensibly from `process.argv`. Gathers args from `process.argv` if not provided.
|
|
109
|
+
* @returns {import('../../types/types').ParsedArgs} - The parsed arguments
|
|
110
|
+
*/
|
|
111
|
+
parseArgs (args = process.argv.slice(2)) {
|
|
112
|
+
if (!NON_SERVER_ARGS.has(args[0])) {
|
|
113
|
+
args.unshift(SERVER_SUBCOMMAND);
|
|
49
114
|
}
|
|
50
|
-
return this._parse_args(args, namespace);
|
|
51
|
-
}.bind(parser);
|
|
52
|
-
return parser;
|
|
53
|
-
}
|
|
54
115
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
116
|
+
try {
|
|
117
|
+
const parsed = this.parser.parse_args(args);
|
|
118
|
+
return ArgParser._transformParsedArgs(parsed);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (this.debug) {
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
// this isn't tested via unit tests (we use `debug: true`) so may escape coverage.
|
|
60
124
|
|
|
61
|
-
|
|
62
|
-
|
|
125
|
+
/* istanbul ignore next */
|
|
126
|
+
{
|
|
127
|
+
// eslint-disable-next-line no-console
|
|
128
|
+
console.error(); // need an extra space since argparse prints usage.
|
|
129
|
+
// eslint-disable-next-line no-console
|
|
130
|
+
console.error(err.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
63
134
|
}
|
|
64
135
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Given an object full of arguments as returned by `argparser.parse_args`,
|
|
138
|
+
* expand the ones for extensions into a nested object structure and rename
|
|
139
|
+
* keys to match the intended destination.
|
|
140
|
+
*
|
|
141
|
+
* E.g., `{'driver-foo-bar': baz}` becomes `{driver: {foo: {bar: 'baz'}}}`
|
|
142
|
+
* @param {object} args
|
|
143
|
+
* @returns {object}
|
|
144
|
+
*/
|
|
145
|
+
static _transformParsedArgs (args) {
|
|
146
|
+
return _.reduce(
|
|
147
|
+
args,
|
|
148
|
+
(unpacked, value, key) => {
|
|
149
|
+
if (hasArgSpec(key)) {
|
|
150
|
+
const {dest} = /** @type {import('../schema/arg-spec').ArgSpec} */(getArgSpec(key));
|
|
151
|
+
_.set(unpacked, dest, value);
|
|
152
|
+
} else {
|
|
153
|
+
// this could be anything that _isn't_ a server arg
|
|
154
|
+
unpacked[key] = value;
|
|
155
|
+
}
|
|
156
|
+
return unpacked;
|
|
157
|
+
},
|
|
158
|
+
{},
|
|
159
|
+
);
|
|
68
160
|
}
|
|
69
161
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Patches the `exit()` method of the parser to throw an error, so we can handle it manually.
|
|
164
|
+
* @param {ArgumentParser} parser
|
|
165
|
+
*/
|
|
166
|
+
static _patchExit (parser) {
|
|
167
|
+
parser.exit = (code, msg) => {
|
|
168
|
+
throw new Error(msg);
|
|
169
|
+
};
|
|
77
170
|
}
|
|
78
|
-
return defaults;
|
|
79
|
-
}
|
|
80
171
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
172
|
+
/**
|
|
173
|
+
*
|
|
174
|
+
* @param {import('argparse').SubParser} subParser
|
|
175
|
+
* @returns {import('./args').ArgumentDefinitions}
|
|
176
|
+
*/
|
|
177
|
+
static _addServerToParser (subParser) {
|
|
178
|
+
const serverParser = subParser.add_parser('server', {
|
|
84
179
|
add_help: true,
|
|
85
|
-
help:
|
|
180
|
+
help: 'Run an Appium server',
|
|
86
181
|
});
|
|
87
|
-
|
|
88
|
-
|
|
182
|
+
|
|
183
|
+
ArgParser._patchExit(serverParser);
|
|
184
|
+
|
|
185
|
+
const serverArgs = getServerArgs();
|
|
186
|
+
for (const [flagsOrNames, opts] of serverArgs) {
|
|
187
|
+
// TS doesn't like the spread operator here.
|
|
188
|
+
// @ts-ignore
|
|
189
|
+
serverParser.add_argument(...flagsOrNames, {...opts});
|
|
89
190
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
191
|
+
|
|
192
|
+
return serverArgs;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Adds extension sub-sub-commands to `driver`/`plugin` subcommands
|
|
197
|
+
* @param {import('argparse').SubParser} subParsers
|
|
198
|
+
*/
|
|
199
|
+
static _addExtensionCommandsToParser (subParsers) {
|
|
200
|
+
for (const type of [DRIVER_TYPE, PLUGIN_TYPE]) {
|
|
201
|
+
const extParser = subParsers.add_parser(type, {
|
|
202
|
+
add_help: true,
|
|
203
|
+
help: `Access the ${type} management CLI commands`,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
ArgParser._patchExit(extParser);
|
|
207
|
+
|
|
208
|
+
const extSubParsers = extParser.add_subparsers({
|
|
209
|
+
dest: `${type}Command`,
|
|
210
|
+
});
|
|
211
|
+
const extensionArgs = getExtensionArgs();
|
|
212
|
+
const parserSpecs = [
|
|
213
|
+
{
|
|
214
|
+
command: 'list',
|
|
215
|
+
args: extensionArgs[type].list,
|
|
216
|
+
help: `List available and installed ${type}s`,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
command: 'install',
|
|
220
|
+
args: extensionArgs[type].install,
|
|
221
|
+
help: `Install a ${type}`,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
command: 'uninstall',
|
|
225
|
+
args: extensionArgs[type].uninstall,
|
|
226
|
+
help: `Uninstall a ${type}`,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
command: 'update',
|
|
230
|
+
args: extensionArgs[type].update,
|
|
231
|
+
help: `Update installed ${type}s to the latest version`,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
command: 'run',
|
|
235
|
+
args: extensionArgs[type].run,
|
|
236
|
+
help:
|
|
237
|
+
`Run a script (defined inside the ${type}'s package.json under the ` +
|
|
238
|
+
`“scripts” field inside the “appium” field) from an installed ${type}`,
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
for (const {command, args, help} of parserSpecs) {
|
|
243
|
+
const parser = extSubParsers.add_parser(command, {help});
|
|
244
|
+
|
|
245
|
+
ArgParser._patchExit(parser);
|
|
246
|
+
|
|
247
|
+
for (const [flagsOrNames, opts] of args) {
|
|
248
|
+
// add_argument mutates params so make sure to send in copies instead
|
|
249
|
+
// @ts-ignore
|
|
250
|
+
parser.add_argument(...flagsOrNames, {...opts});
|
|
251
|
+
}
|
|
115
252
|
}
|
|
116
253
|
}
|
|
117
254
|
}
|
|
118
255
|
}
|
|
119
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Creates a {@link ArgParser} instance. Necessarily reads extension configuration
|
|
259
|
+
* beforehand, and finalizes the config schema.
|
|
260
|
+
*
|
|
261
|
+
* @constructs ArgParser
|
|
262
|
+
* @param {boolean} [debug] - If `true`, throw instead of exit upon parsing error
|
|
263
|
+
* @returns {Promise<ArgParser>}
|
|
264
|
+
*/
|
|
265
|
+
async function getParser (debug = false) {
|
|
266
|
+
await B.all([driverConfig.read(), pluginConfig.read()]);
|
|
267
|
+
finalizeSchema();
|
|
268
|
+
|
|
269
|
+
return new ArgParser(debug);
|
|
270
|
+
}
|
|
271
|
+
|
|
120
272
|
export default getParser;
|
|
121
|
-
export { getParser,
|
|
273
|
+
export { getParser, ArgParser };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import betterAjvErrors from '@sidvind/better-ajv-errors';
|
|
4
|
+
import { lilconfig } from 'lilconfig';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import yaml from 'yaml';
|
|
7
|
+
import { getSchema, validate } from './schema/schema';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* lilconfig loader to handle `.yaml` files
|
|
11
|
+
* @type {import('lilconfig').LoaderSync}
|
|
12
|
+
*/
|
|
13
|
+
function yamlLoader (filepath, content) {
|
|
14
|
+
return yaml.parse(content);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A cache of the raw config file (a JSON string) at a filepath.
|
|
19
|
+
* This is used for better error reporting.
|
|
20
|
+
* Note that config files needn't be JSON, but it helps if they are.
|
|
21
|
+
* @type {Map<string,RawJson>}
|
|
22
|
+
*/
|
|
23
|
+
const rawConfig = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Custom JSON loader that caches the raw config file (for use with `better-ajv-errors`).
|
|
27
|
+
* If it weren't for this cache, this would be unnecessary.
|
|
28
|
+
* @type {import('lilconfig').LoaderSync}
|
|
29
|
+
*/
|
|
30
|
+
function jsonLoader (filepath, content) {
|
|
31
|
+
rawConfig.set(filepath, content);
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Loads a config file from an explicit path
|
|
37
|
+
* @param {LilconfigAsyncSearcher} lc - lilconfig instance
|
|
38
|
+
* @param {string} filepath - Path to config file
|
|
39
|
+
* @returns {Promise<import('lilconfig').LilconfigResult>}
|
|
40
|
+
*/
|
|
41
|
+
async function loadConfigFile (lc, filepath) {
|
|
42
|
+
try {
|
|
43
|
+
// removing "await" will cause any rejection to _not_ be caught in this block!
|
|
44
|
+
return await lc.load(filepath);
|
|
45
|
+
} catch (/** @type {unknown} */err) {
|
|
46
|
+
if (/** @type {NodeJS.ErrnoException} */(err).code === 'ENOENT') {
|
|
47
|
+
/** @type {NodeJS.ErrnoException} */(err).message = `Config file not found at user-provided path: ${filepath}`;
|
|
48
|
+
throw err;
|
|
49
|
+
} else if (err instanceof SyntaxError) {
|
|
50
|
+
// generally invalid JSON
|
|
51
|
+
err.message = `Config file at user-provided path ${filepath} is invalid:\n${err.message}`;
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Searches for a config file
|
|
60
|
+
* @param {LilconfigAsyncSearcher} lc - lilconfig instance
|
|
61
|
+
* @returns {Promise<import('lilconfig').LilconfigResult>}
|
|
62
|
+
*/
|
|
63
|
+
async function searchConfigFile (lc) {
|
|
64
|
+
return await lc.search();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Given an array of errors and the result of loading a config file, generate a
|
|
69
|
+
* helpful string for the user.
|
|
70
|
+
*
|
|
71
|
+
* - If `opts` contains a `json` property, this should be the original JSON
|
|
72
|
+
* _string_ of the config file. This is only applicable if the config file
|
|
73
|
+
* was in JSON format. If present, it will associate line numbers with errors.
|
|
74
|
+
* - If `errors` happens to be empty, this will throw.
|
|
75
|
+
* @param {import('ajv').ErrorObject[]} errors - Non-empty array of errors. Required.
|
|
76
|
+
* @param {ReadConfigFileResult['config']|any} [config] -
|
|
77
|
+
* Configuration & metadata
|
|
78
|
+
* @param {FormatConfigErrorsOptions} [opts]
|
|
79
|
+
* @throws {TypeError} If `errors` is empty
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function formatErrors (errors = [], config = {}, opts = {}) {
|
|
83
|
+
if (errors && !errors.length) {
|
|
84
|
+
throw new TypeError('Array of errors must be non-empty');
|
|
85
|
+
}
|
|
86
|
+
return betterAjvErrors(getSchema(opts.schemaId), config, errors, {
|
|
87
|
+
json: opts.json,
|
|
88
|
+
format: 'cli',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Given an optional path, read a config file. Validates the config file.
|
|
94
|
+
*
|
|
95
|
+
* Call {@link validate} if you already have a config object.
|
|
96
|
+
* @param {string} [filepath] - Path to config file, if we have one
|
|
97
|
+
* @param {ReadConfigFileOptions} [opts] - Options
|
|
98
|
+
* @public
|
|
99
|
+
* @returns {Promise<ReadConfigFileResult>} Contains config and filepath, if found, and any errors
|
|
100
|
+
*/
|
|
101
|
+
export async function readConfigFile (filepath, opts = {}) {
|
|
102
|
+
const lc = lilconfig('appium', {
|
|
103
|
+
loaders: {
|
|
104
|
+
'.yaml': yamlLoader,
|
|
105
|
+
'.yml': yamlLoader,
|
|
106
|
+
'.json': jsonLoader,
|
|
107
|
+
noExt: jsonLoader,
|
|
108
|
+
},
|
|
109
|
+
packageProp: 'appiumConfig'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = filepath
|
|
113
|
+
? await loadConfigFile(lc, filepath)
|
|
114
|
+
: await searchConfigFile(lc);
|
|
115
|
+
|
|
116
|
+
if (result && !result.isEmpty && result.filepath) {
|
|
117
|
+
const {normalize = true, pretty = true} = opts;
|
|
118
|
+
try {
|
|
119
|
+
/** @type {ReadConfigFileResult} */
|
|
120
|
+
let configResult;
|
|
121
|
+
const errors = validate(result.config);
|
|
122
|
+
if (_.isEmpty(errors)) {
|
|
123
|
+
configResult = {...result, errors};
|
|
124
|
+
} else {
|
|
125
|
+
const reason = formatErrors(errors, result.config, {
|
|
126
|
+
json: rawConfig.get(result.filepath),
|
|
127
|
+
pretty,
|
|
128
|
+
});
|
|
129
|
+
configResult = reason
|
|
130
|
+
? {...result, errors, reason}
|
|
131
|
+
: {...result, errors};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (normalize) {
|
|
135
|
+
// normalize (to camel case) all top-level property names of the config file
|
|
136
|
+
configResult.config = normalizeConfig(
|
|
137
|
+
/** @type {AppiumConfig} */ (configResult.config),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return configResult;
|
|
142
|
+
} finally {
|
|
143
|
+
// clean up the raw config file cache, which is only kept to better report errors.
|
|
144
|
+
rawConfig.delete(result.filepath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result ?? {};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Convert schema property names to either a) the value of the `appiumCliDest` property, if any; or b) camel-case
|
|
152
|
+
* @param {AppiumConfig} config - Configuration object
|
|
153
|
+
* @returns {NormalizedAppiumConfig} New object with camel-cased keys (or `dest` keys).
|
|
154
|
+
*/
|
|
155
|
+
function normalizeConfig (config) {
|
|
156
|
+
const schema = getSchema();
|
|
157
|
+
/**
|
|
158
|
+
* @param {AppiumConfig} config
|
|
159
|
+
* @param {string} [section] - Keypath (lodash `_.get()` style) to section of config. If omitted, assume root Appium config schema
|
|
160
|
+
* @returns Normalized section of config
|
|
161
|
+
*/
|
|
162
|
+
const normalize = (config, section) => {
|
|
163
|
+
const obj = _.isUndefined(section) ? config : _.get(config, section, config);
|
|
164
|
+
|
|
165
|
+
const mappedObj = _.mapKeys(obj, (__, prop) =>
|
|
166
|
+
schema.properties[prop]?.appiumCliDest ?? _.camelCase(prop),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return _.mapValues(mappedObj, (value, property) => {
|
|
170
|
+
const nextSection = section ? `${section}.${property}` : property;
|
|
171
|
+
return isSchemaTypeObject(value) ? normalize(config, nextSection) : value;
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns `true` if the schema prop references an object, or if it's an object itself
|
|
177
|
+
* @param {import('ajv').SchemaObject|object} schema - Referencing schema object
|
|
178
|
+
*/
|
|
179
|
+
const isSchemaTypeObject = (schema) => Boolean(schema.properties);
|
|
180
|
+
|
|
181
|
+
return normalize(config);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Result of calling {@link readConfigFile}.
|
|
186
|
+
* @typedef {Object} ReadConfigFileResult
|
|
187
|
+
* @property {import('ajv').ErrorObject[]} [errors] - Validation errors
|
|
188
|
+
* @property {string} [filepath] - The path to the config file, if found
|
|
189
|
+
* @property {boolean} [isEmpty] - If `true`, the config file exists but is empty
|
|
190
|
+
* @property {AppiumConfig} [config] - The parsed configuration
|
|
191
|
+
* @property {string|betterAjvErrors.IOutputError[]} [reason] - Human-readable error messages and suggestions. If the `pretty` option is `true`, this will be a nice string to print.
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Options for {@link readConfigFile}.
|
|
196
|
+
* @typedef {Object} ReadConfigFileOptions
|
|
197
|
+
* @property {boolean} [pretty=true] If `false`, do not use color and fancy formatting in the `reason` property of the {@link ReadConfigFileResult}. The value of `reason` is then suitable for machine-reading.
|
|
198
|
+
* @property {boolean} [normalize=true] If `false`, do not normalize key names to camel case.
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* This is an `AsyncSearcher` which is inexplicably _not_ exported by the `lilconfig` type definition.
|
|
203
|
+
* @typedef {ReturnType<import('lilconfig')["lilconfig"]>} LilconfigAsyncSearcher
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The contents of an Appium config file. Generated from schema
|
|
208
|
+
* @typedef {import('../types/types').AppiumConfig} AppiumConfig
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The contents of an Appium config file with camelcased property names (and using `appiumCliDest` value if present). Generated from {@link AppiumConfig}
|
|
213
|
+
* @typedef {import('../types/types').NormalizedAppiumConfig} NormalizedAppiumConfig
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The string should be a raw JSON string.
|
|
218
|
+
* @typedef {string} RawJson
|
|
219
|
+
*/
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Options for {@link formatErrors}.
|
|
223
|
+
* @typedef {Object} FormatConfigErrorsOptions
|
|
224
|
+
* @property {import('./config-file').RawJson} [json] - Raw JSON config (as string)
|
|
225
|
+
* @property {boolean} [pretty=true] - Whether to format errors as a CLI-friendly string
|
|
226
|
+
* @property {string} [schemaId] - Specific ID of a prop; otherwise entire schema
|
|
227
|
+
*/
|