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.
Files changed (53) hide show
  1. package/build/lib/appium-config.schema.json +0 -0
  2. package/build/lib/appium.js +84 -69
  3. package/build/lib/cli/argparse-actions.js +1 -1
  4. package/build/lib/cli/args.js +87 -223
  5. package/build/lib/cli/extension-command.js +2 -2
  6. package/build/lib/cli/extension.js +14 -6
  7. package/build/lib/cli/parser.js +142 -106
  8. package/build/lib/cli/utils.js +1 -1
  9. package/build/lib/config-file.js +141 -0
  10. package/build/lib/config.js +42 -64
  11. package/build/lib/driver-config.js +41 -20
  12. package/build/lib/drivers.js +1 -1
  13. package/build/lib/ext-config-io.js +165 -0
  14. package/build/lib/extension-config.js +110 -60
  15. package/build/lib/grid-register.js +19 -21
  16. package/build/lib/logsink.js +1 -1
  17. package/build/lib/main.js +135 -72
  18. package/build/lib/plugin-config.js +17 -8
  19. package/build/lib/schema/appium-config-schema.js +252 -0
  20. package/build/lib/schema/arg-spec.js +120 -0
  21. package/build/lib/schema/cli-args.js +173 -0
  22. package/build/lib/schema/cli-transformers.js +76 -0
  23. package/build/lib/schema/index.js +36 -0
  24. package/build/lib/schema/keywords.js +62 -0
  25. package/build/lib/schema/schema.js +357 -0
  26. package/build/lib/utils.js +26 -35
  27. package/lib/appium-config.schema.json +277 -0
  28. package/lib/appium.js +99 -75
  29. package/lib/cli/args.js +138 -335
  30. package/lib/cli/extension-command.js +7 -6
  31. package/lib/cli/extension.js +12 -4
  32. package/lib/cli/parser.js +248 -96
  33. package/lib/config-file.js +227 -0
  34. package/lib/config.js +71 -61
  35. package/lib/driver-config.js +66 -11
  36. package/lib/ext-config-io.js +287 -0
  37. package/lib/extension-config.js +209 -66
  38. package/lib/grid-register.js +24 -21
  39. package/lib/main.js +139 -68
  40. package/lib/plugin-config.js +32 -2
  41. package/lib/schema/appium-config-schema.js +286 -0
  42. package/lib/schema/arg-spec.js +218 -0
  43. package/lib/schema/cli-args.js +273 -0
  44. package/lib/schema/cli-transformers.js +123 -0
  45. package/lib/schema/index.js +2 -0
  46. package/lib/schema/keywords.js +119 -0
  47. package/lib/schema/schema.js +577 -0
  48. package/lib/utils.js +29 -52
  49. package/package.json +16 -11
  50. package/types/appium-config.d.ts +197 -0
  51. package/types/types.d.ts +201 -0
  52. package/build/lib/cli/parser-helpers.js +0 -106
  53. package/lib/cli/parser-helpers.js +0 -106
package/lib/cli/parser.js CHANGED
@@ -1,121 +1,273 @@
1
- import path from 'path';
2
- import _ from 'lodash';
1
+ // @ts-check
2
+
3
+ import { fs } from '@appium/support';
3
4
  import { ArgumentParser } from 'argparse';
4
- import { sharedArgs, serverArgs, extensionArgs } from './args';
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 { fs } from '@appium/support';
11
+ import {
12
+ driverConfig,
13
+ getExtensionArgs,
14
+ getServerArgs,
15
+ pluginConfig
16
+ } from './args';
8
17
 
18
+ export const SERVER_SUBCOMMAND = 'server';
9
19
 
10
- function makeDebugParser (parser) {
11
- parser.exit = (status, message = undefined) => {
12
- throw new Error(message);
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
- function getParser (debug = false) {
17
- const parser = new ArgumentParser({
18
- add_help: true,
19
- description: 'A webdriver-compatible server for use with native and hybrid iOS and Android applications.',
20
- prog: process.argv[1] ? path.basename(process.argv[1]) : 'appium',
21
- });
22
- if (debug) {
23
- makeDebugParser(parser);
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
- parser.add_argument('-v', '--version', {
26
- action: 'version',
27
- version: fs.readPackageJsonFrom(rootDir).version
28
- });
29
- const subParsers = parser.add_subparsers({dest: 'subcommand'});
30
-
31
- // add the 'server' subcommand, and store the raw arguments on the parser
32
- // object as a way for other parts of the code to work with the arguments
33
- // conceptually rather than just through argparse
34
- const serverArgs = addServerToParser(sharedArgs, subParsers, debug);
35
- parser.rawArgs = serverArgs;
36
-
37
- // add the 'driver' and 'plugin' subcommands
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
- function addServerToParser (sharedArgs, subParsers, debug = false) {
56
- const serverParser = subParsers.add_parser('server', {
57
- add_help: true,
58
- help: 'Run an Appium server',
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
- if (debug) {
62
- makeDebugParser(serverParser);
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
- for (const [flagsOrNames, opts] of [...sharedArgs, ...serverArgs]) {
66
- // add_argument mutates arguments so make copies
67
- serverParser.add_argument(...flagsOrNames, {...opts});
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
- return serverArgs;
71
- }
72
-
73
- function getDefaultServerArgs () {
74
- let defaults = {};
75
- for (let [, arg] of serverArgs) {
76
- defaults[arg.dest] = arg.default;
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
- function addExtensionsToParser (sharedArgs, subParsers, debug = false) {
82
- for (const type of [DRIVER_TYPE, PLUGIN_TYPE]) {
83
- const extParser = subParsers.add_parser(type, {
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: `Access the ${type} management CLI commands`,
180
+ help: 'Run an Appium server',
86
181
  });
87
- if (debug) {
88
- makeDebugParser(extParser);
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
- const extSubParsers = extParser.add_subparsers({
91
- dest: `${type}Command`,
92
- });
93
- const parserSpecs = [
94
- {command: 'list', args: extensionArgs[type].list,
95
- help: `List available and installed ${type}s`},
96
- {command: 'install', args: extensionArgs[type].install,
97
- help: `Install a ${type}`},
98
- {command: 'uninstall', args: extensionArgs[type].uninstall,
99
- help: `Uninstall a ${type}`},
100
- {command: 'update', args: extensionArgs[type].update,
101
- help: `Update installed ${type}s to the latest version`},
102
- {command: 'run', args: extensionArgs[type].run,
103
- help: `Run a script (defined inside the ${type}'s package.json under the ` +
104
- `“scripts” field inside the “appium” field) from an installed ${type}`}
105
- ];
106
-
107
- for (const {command, args, help} of parserSpecs) {
108
- const parser = extSubParsers.add_parser(command, {help});
109
- if (debug) {
110
- makeDebugParser(parser);
111
- }
112
- for (const [flagsOrNames, opts] of [...sharedArgs, ...args]) {
113
- // add_argument mutates params so make sure to send in copies instead
114
- parser.add_argument(...flagsOrNames, {...opts});
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, getDefaultServerArgs };
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
+ */