appium 2.0.0-beta.2 → 2.0.0-beta.23

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 (122) hide show
  1. package/README.md +9 -9
  2. package/build/check-npm-pack-files.js +23 -0
  3. package/build/commands-yml/parse.js +319 -0
  4. package/build/commands-yml/validator.js +130 -0
  5. package/build/index.js +19 -0
  6. package/build/lib/appium-config.schema.json +0 -0
  7. package/build/lib/appium.js +160 -53
  8. package/build/lib/cli/args.js +115 -279
  9. package/build/lib/cli/driver-command.js +11 -1
  10. package/build/lib/cli/extension-command.js +60 -8
  11. package/build/lib/cli/extension.js +30 -7
  12. package/build/lib/cli/npm.js +43 -29
  13. package/build/lib/cli/parser.js +156 -89
  14. package/build/lib/cli/plugin-command.js +11 -1
  15. package/build/lib/cli/utils.js +29 -3
  16. package/build/lib/config-file.js +141 -0
  17. package/build/lib/config.js +53 -65
  18. package/build/lib/driver-config.js +42 -19
  19. package/build/lib/drivers.js +8 -4
  20. package/build/lib/ext-config-io.js +165 -0
  21. package/build/lib/extension-config.js +130 -61
  22. package/build/lib/grid-register.js +22 -24
  23. package/build/lib/logger.js +3 -3
  24. package/build/lib/logsink.js +11 -13
  25. package/build/lib/main.js +197 -77
  26. package/build/lib/plugin-config.js +21 -11
  27. package/build/lib/plugins.js +4 -2
  28. package/build/lib/schema/appium-config-schema.js +253 -0
  29. package/build/lib/schema/arg-spec.js +120 -0
  30. package/build/lib/schema/cli-args.js +188 -0
  31. package/build/lib/schema/cli-transformers.js +76 -0
  32. package/build/lib/schema/index.js +36 -0
  33. package/build/lib/schema/keywords.js +72 -0
  34. package/build/lib/schema/schema.js +357 -0
  35. package/build/lib/utils.js +44 -99
  36. package/build/postinstall.js +90 -0
  37. package/build/test/cli/cli-e2e-specs.js +221 -0
  38. package/build/test/cli/cli-helpers.js +86 -0
  39. package/build/test/cli/cli-specs.js +71 -0
  40. package/build/test/cli/fixtures/test-driver/package.json +27 -0
  41. package/build/test/cli/schema-args-specs.js +48 -0
  42. package/build/test/cli/schema-e2e-specs.js +47 -0
  43. package/build/test/config-e2e-specs.js +112 -0
  44. package/build/test/config-file-e2e-specs.js +209 -0
  45. package/build/test/config-file-specs.js +281 -0
  46. package/build/test/config-specs.js +159 -0
  47. package/build/test/driver-e2e-specs.js +435 -0
  48. package/build/test/driver-specs.js +321 -0
  49. package/build/test/ext-config-io-specs.js +181 -0
  50. package/build/test/extension-config-specs.js +365 -0
  51. package/build/test/fixtures/allow-feat.txt +5 -0
  52. package/build/test/fixtures/caps.json +3 -0
  53. package/build/test/fixtures/config/allow-insecure.txt +3 -0
  54. package/build/test/fixtures/config/appium.config.bad-nodeconfig.json +5 -0
  55. package/build/test/fixtures/config/appium.config.bad.json +32 -0
  56. package/build/test/fixtures/config/appium.config.ext-good.json +9 -0
  57. package/build/test/fixtures/config/appium.config.ext-unknown-props.json +10 -0
  58. package/build/test/fixtures/config/appium.config.good.js +40 -0
  59. package/build/test/fixtures/config/appium.config.good.json +33 -0
  60. package/build/test/fixtures/config/appium.config.good.yaml +30 -0
  61. package/build/test/fixtures/config/appium.config.invalid.json +31 -0
  62. package/build/test/fixtures/config/appium.config.security-array.json +5 -0
  63. package/build/test/fixtures/config/appium.config.security-delimited.json +5 -0
  64. package/build/test/fixtures/config/appium.config.security-path.json +5 -0
  65. package/build/test/fixtures/config/driver-fake.config.json +8 -0
  66. package/build/test/fixtures/config/nodeconfig.json +3 -0
  67. package/build/test/fixtures/config/plugin-fake.config.json +0 -0
  68. package/build/test/fixtures/default-args.js +35 -0
  69. package/build/test/fixtures/deny-feat.txt +5 -0
  70. package/build/test/fixtures/driver.schema.js +20 -0
  71. package/build/test/fixtures/extensions.yaml +27 -0
  72. package/build/test/fixtures/flattened-schema.js +504 -0
  73. package/build/test/fixtures/plugin.schema.js +20 -0
  74. package/build/test/fixtures/schema-with-extensions.js +28 -0
  75. package/build/test/grid-register-specs.js +74 -0
  76. package/build/test/helpers.js +75 -0
  77. package/build/test/logger-specs.js +76 -0
  78. package/build/test/npm-specs.js +20 -0
  79. package/build/test/parser-specs.js +314 -0
  80. package/build/test/plugin-e2e-specs.js +316 -0
  81. package/build/test/schema/arg-spec-specs.js +70 -0
  82. package/build/test/schema/cli-args-specs.js +431 -0
  83. package/build/test/schema/schema-specs.js +389 -0
  84. package/build/test/utils-specs.js +266 -0
  85. package/index.js +11 -0
  86. package/lib/appium-config.schema.json +278 -0
  87. package/lib/appium.js +207 -65
  88. package/lib/cli/args.js +174 -375
  89. package/lib/cli/driver-command.js +4 -0
  90. package/lib/cli/extension-command.js +70 -5
  91. package/lib/cli/extension.js +25 -5
  92. package/lib/cli/npm.js +86 -18
  93. package/lib/cli/parser.js +257 -79
  94. package/lib/cli/plugin-command.js +4 -0
  95. package/lib/cli/utils.js +21 -1
  96. package/lib/config-file.js +227 -0
  97. package/lib/config.js +84 -63
  98. package/lib/driver-config.js +66 -11
  99. package/lib/drivers.js +4 -1
  100. package/lib/ext-config-io.js +287 -0
  101. package/lib/extension-config.js +225 -67
  102. package/lib/grid-register.js +27 -24
  103. package/lib/logger.js +1 -1
  104. package/lib/logsink.js +10 -7
  105. package/lib/main.js +214 -77
  106. package/lib/plugin-config.js +35 -6
  107. package/lib/plugins.js +1 -0
  108. package/lib/schema/appium-config-schema.js +287 -0
  109. package/lib/schema/arg-spec.js +222 -0
  110. package/lib/schema/cli-args.js +285 -0
  111. package/lib/schema/cli-transformers.js +123 -0
  112. package/lib/schema/index.js +2 -0
  113. package/lib/schema/keywords.js +135 -0
  114. package/lib/schema/schema.js +577 -0
  115. package/lib/utils.js +42 -88
  116. package/package.json +55 -84
  117. package/postinstall.js +71 -0
  118. package/types/appium-config.d.ts +197 -0
  119. package/types/types.d.ts +201 -0
  120. package/CHANGELOG.md +0 -3515
  121. package/build/lib/cli/parser-helpers.js +0 -82
  122. package/lib/cli/parser-helpers.js +0 -79
package/lib/cli/parser.js CHANGED
@@ -1,98 +1,276 @@
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';
11
+ import {
12
+ driverConfig,
13
+ getExtensionArgs,
14
+ getServerArgs,
15
+ pluginConfig
16
+ } from './args';
7
17
 
8
- function getParser (debug = false) {
9
- const parser = new ArgumentParser({
10
- version: require(path.resolve(rootDir, 'package.json')).version,
11
- addHelp: true,
12
- description: 'A webdriver-compatible server for use with native and hybrid iOS and Android applications.',
13
- prog: process.argv[1] ? path.basename(process.argv[1]) : 'appium',
14
- debug
15
- });
16
- const subParsers = parser.addSubparsers({dest: 'subcommand', debug});
17
-
18
- // add the 'server' subcommand, and store the raw arguments on the parser
19
- // object as a way for other parts of the code to work with the arguments
20
- // conceptually rather than just through argparse
21
- const serverArgs = addServerToParser(sharedArgs, subParsers, debug);
22
- parser.rawArgs = serverArgs;
23
-
24
- // add the 'driver' and 'plugin' subcommands
25
- addExtensionsToParser(sharedArgs, subParsers, debug);
26
-
27
- // modify the parseArgs function to insert the 'server' subcommand if the
28
- // user hasn't specified a subcommand or the global help command
29
- parser._parseArgs = parser.parseArgs;
30
- parser.parseArgs = function (args, namespace) {
31
- if (_.isUndefined(args)) {
32
- args = [...process.argv.slice(2)];
33
- }
34
- if (!_.includes([DRIVER_TYPE, PLUGIN_TYPE, 'server', '-h'], args[0])) {
35
- args.splice(0, 0, 'server');
18
+ export const SERVER_SUBCOMMAND = 'server';
19
+
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
+ });
57
+
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;
100
+ }
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);
36
114
  }
37
- return this._parseArgs(args, namespace);
38
- }.bind(parser);
39
- return parser;
40
- }
41
115
 
42
- function addServerToParser (sharedArgs, subParsers, debug) {
43
- const serverParser = subParsers.addParser('server', {
44
- addHelp: true,
45
- help: 'Run an Appium server',
46
- debug
47
- });
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.
48
124
 
49
- for (const [flags, opts] of [...sharedArgs, ...serverArgs]) {
50
- // addArgument mutates arguments so make copies
51
- serverParser.addArgument([...flags], {...opts});
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
+ }
52
134
  }
53
135
 
54
- return serverArgs;
55
- }
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
+ );
160
+ }
56
161
 
57
- function getDefaultServerArgs () {
58
- let defaults = {};
59
- for (let [, arg] of serverArgs) {
60
- defaults[arg.dest] = arg.defaultValue;
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
+ if (code) {
169
+ throw new Error(msg);
170
+ }
171
+ process.exit();
172
+ };
61
173
  }
62
- return defaults;
63
- }
64
174
 
65
- function addExtensionsToParser (sharedArgs, subParsers, debug) {
66
- for (const type of [DRIVER_TYPE, PLUGIN_TYPE]) {
67
- const extParser = subParsers.addParser(type, {
68
- addHelp: true,
69
- help: `Access the ${type} management CLI commands`,
70
- debug
71
- });
72
- const extSubParsers = extParser.addSubparsers({
73
- dest: `${type}Command`,
74
- debug
175
+ /**
176
+ *
177
+ * @param {import('argparse').SubParser} subParser
178
+ * @returns {import('./args').ArgumentDefinitions}
179
+ */
180
+ static _addServerToParser (subParser) {
181
+ const serverParser = subParser.add_parser('server', {
182
+ add_help: true,
183
+ help: 'Run an Appium server',
75
184
  });
76
- const parserSpecs = [
77
- {command: 'list', args: extensionArgs[type].list,
78
- help: `List available and installed ${type}s`},
79
- {command: 'install', args: extensionArgs[type].install,
80
- help: `Install a ${type}`},
81
- {command: 'uninstall', args: extensionArgs[type].uninstall,
82
- help: `Uninstall a ${type}`},
83
- {command: 'update', args: extensionArgs[type].update,
84
- help: `Update installed ${type}s to the latest version`},
85
- ];
86
-
87
- for (const {command, args, help} of parserSpecs) {
88
- const parser = extSubParsers.addParser(command, {help, debug});
89
- for (const [flags, opts] of [...sharedArgs, ...args]) {
90
- // addArgument mutates params so make sure to send in copies instead
91
- parser.addArgument([...flags], {...opts});
185
+
186
+ ArgParser._patchExit(serverParser);
187
+
188
+ const serverArgs = getServerArgs();
189
+ for (const [flagsOrNames, opts] of serverArgs) {
190
+ // TS doesn't like the spread operator here.
191
+ // @ts-ignore
192
+ serverParser.add_argument(...flagsOrNames, {...opts});
193
+ }
194
+
195
+ return serverArgs;
196
+ }
197
+
198
+ /**
199
+ * Adds extension sub-sub-commands to `driver`/`plugin` subcommands
200
+ * @param {import('argparse').SubParser} subParsers
201
+ */
202
+ static _addExtensionCommandsToParser (subParsers) {
203
+ for (const type of [DRIVER_TYPE, PLUGIN_TYPE]) {
204
+ const extParser = subParsers.add_parser(type, {
205
+ add_help: true,
206
+ help: `Access the ${type} management CLI commands`,
207
+ });
208
+
209
+ ArgParser._patchExit(extParser);
210
+
211
+ const extSubParsers = extParser.add_subparsers({
212
+ dest: `${type}Command`,
213
+ });
214
+ const extensionArgs = getExtensionArgs();
215
+ const parserSpecs = [
216
+ {
217
+ command: 'list',
218
+ args: extensionArgs[type].list,
219
+ help: `List available and installed ${type}s`,
220
+ },
221
+ {
222
+ command: 'install',
223
+ args: extensionArgs[type].install,
224
+ help: `Install a ${type}`,
225
+ },
226
+ {
227
+ command: 'uninstall',
228
+ args: extensionArgs[type].uninstall,
229
+ help: `Uninstall a ${type}`,
230
+ },
231
+ {
232
+ command: 'update',
233
+ args: extensionArgs[type].update,
234
+ help: `Update installed ${type}s to the latest version`,
235
+ },
236
+ {
237
+ command: 'run',
238
+ args: extensionArgs[type].run,
239
+ help:
240
+ `Run a script (defined inside the ${type}'s package.json under the ` +
241
+ `“scripts” field inside the “appium” field) from an installed ${type}`,
242
+ },
243
+ ];
244
+
245
+ for (const {command, args, help} of parserSpecs) {
246
+ const parser = extSubParsers.add_parser(command, {help});
247
+
248
+ ArgParser._patchExit(parser);
249
+
250
+ for (const [flagsOrNames, opts] of args) {
251
+ // add_argument mutates params so make sure to send in copies instead
252
+ // @ts-ignore
253
+ parser.add_argument(...flagsOrNames, {...opts});
254
+ }
92
255
  }
93
256
  }
94
257
  }
95
258
  }
96
259
 
260
+ /**
261
+ * Creates a {@link ArgParser} instance. Necessarily reads extension configuration
262
+ * beforehand, and finalizes the config schema.
263
+ *
264
+ * @constructs ArgParser
265
+ * @param {boolean} [debug] - If `true`, throw instead of exit upon parsing error
266
+ * @returns {Promise<ArgParser>}
267
+ */
268
+ async function getParser (debug = false) {
269
+ await B.all([driverConfig.read(), pluginConfig.read()]);
270
+ finalizeSchema();
271
+
272
+ return new ArgParser(debug);
273
+ }
274
+
97
275
  export default getParser;
98
- export { getParser, getDefaultServerArgs };
276
+ export { getParser, ArgParser };
@@ -24,6 +24,10 @@ export default class PluginCommand extends ExtensionCommand {
24
24
  return await super.update({ext: plugin, unsafe});
25
25
  }
26
26
 
27
+ async run ({plugin, scriptName}) {
28
+ return await super.run({ext: plugin, scriptName});
29
+ }
30
+
27
31
  getPostInstallText ({extName, extData}) {
28
32
  return `Plugin ${extName}@${extData.version} successfully installed`.green;
29
33
  }
package/lib/cli/utils.js CHANGED
@@ -52,9 +52,29 @@ async function spinWith (json, msg, fn) {
52
52
  }
53
53
  }
54
54
 
55
+ class RingBuffer {
56
+ constructor (size = 50) {
57
+ this.size = size;
58
+ this.buffer = [];
59
+ }
60
+ getBuff () {
61
+ return this.buffer;
62
+ }
63
+ dequeue () {
64
+ this.buffer.shift();
65
+ }
66
+ enqueue (item) {
67
+ if (this.buffer.length >= this.size) {
68
+ this.dequeue();
69
+ }
70
+ this.buffer.push(item);
71
+ }
72
+ }
73
+
55
74
  export {
56
75
  errAndQuit,
57
76
  log,
58
77
  spinWith,
59
- JSON_SPACES
78
+ JSON_SPACES,
79
+ RingBuffer
60
80
  };
@@ -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
+ */