appium 3.2.1 → 3.3.0

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/build/lib/cli/args.d.ts +16 -12
  2. package/build/lib/cli/args.d.ts.map +1 -1
  3. package/build/lib/cli/args.js +15 -35
  4. package/build/lib/cli/args.js.map +1 -1
  5. package/build/lib/cli/driver-command.d.ts +51 -93
  6. package/build/lib/cli/driver-command.d.ts.map +1 -1
  7. package/build/lib/cli/driver-command.js +11 -66
  8. package/build/lib/cli/driver-command.js.map +1 -1
  9. package/build/lib/cli/extension-command.d.ts +211 -415
  10. package/build/lib/cli/extension-command.d.ts.map +1 -1
  11. package/build/lib/cli/extension-command.js +384 -653
  12. package/build/lib/cli/extension-command.js.map +1 -1
  13. package/build/lib/cli/extension.d.ts +11 -16
  14. package/build/lib/cli/extension.d.ts.map +1 -1
  15. package/build/lib/cli/extension.js +10 -28
  16. package/build/lib/cli/extension.js.map +1 -1
  17. package/build/lib/cli/parser.d.ts +40 -69
  18. package/build/lib/cli/parser.d.ts.map +1 -1
  19. package/build/lib/cli/parser.js +24 -59
  20. package/build/lib/cli/parser.js.map +1 -1
  21. package/build/lib/cli/plugin-command.d.ts +50 -90
  22. package/build/lib/cli/plugin-command.d.ts.map +1 -1
  23. package/build/lib/cli/plugin-command.js +11 -63
  24. package/build/lib/cli/plugin-command.js.map +1 -1
  25. package/build/lib/cli/setup-command.d.ts +21 -26
  26. package/build/lib/cli/setup-command.d.ts.map +1 -1
  27. package/build/lib/cli/setup-command.js +13 -55
  28. package/build/lib/cli/setup-command.js.map +1 -1
  29. package/build/lib/cli/utils.d.ts +27 -29
  30. package/build/lib/cli/utils.d.ts.map +1 -1
  31. package/build/lib/cli/utils.js +29 -31
  32. package/build/lib/cli/utils.js.map +1 -1
  33. package/build/lib/config-file.d.ts +24 -67
  34. package/build/lib/config-file.d.ts.map +1 -1
  35. package/build/lib/config-file.js +56 -115
  36. package/build/lib/config-file.js.map +1 -1
  37. package/build/lib/config.d.ts +42 -44
  38. package/build/lib/config.d.ts.map +1 -1
  39. package/build/lib/config.js +75 -107
  40. package/build/lib/config.js.map +1 -1
  41. package/build/lib/constants.d.ts +23 -23
  42. package/build/lib/constants.d.ts.map +1 -1
  43. package/build/lib/constants.js +10 -15
  44. package/build/lib/constants.js.map +1 -1
  45. package/build/lib/doctor/doctor.d.ts +40 -57
  46. package/build/lib/doctor/doctor.d.ts.map +1 -1
  47. package/build/lib/doctor/doctor.js +29 -60
  48. package/build/lib/doctor/doctor.js.map +1 -1
  49. package/build/lib/grid-register.d.ts +32 -7
  50. package/build/lib/grid-register.d.ts.map +1 -1
  51. package/build/lib/grid-register.js +84 -48
  52. package/build/lib/grid-register.js.map +1 -1
  53. package/build/lib/logsink.d.ts +13 -22
  54. package/build/lib/logsink.d.ts.map +1 -1
  55. package/build/lib/logsink.js +48 -103
  56. package/build/lib/logsink.js.map +1 -1
  57. package/build/lib/main.js +1 -1
  58. package/build/lib/main.js.map +1 -1
  59. package/build/lib/schema/arg-spec.d.ts +32 -107
  60. package/build/lib/schema/arg-spec.d.ts.map +1 -1
  61. package/build/lib/schema/arg-spec.js +11 -107
  62. package/build/lib/schema/arg-spec.js.map +1 -1
  63. package/build/lib/schema/cli-args.d.ts +3 -15
  64. package/build/lib/schema/cli-args.d.ts.map +1 -1
  65. package/build/lib/schema/cli-args.js +15 -105
  66. package/build/lib/schema/cli-args.js.map +1 -1
  67. package/build/lib/schema/cli-transformers.d.ts +15 -12
  68. package/build/lib/schema/cli-transformers.d.ts.map +1 -1
  69. package/build/lib/schema/cli-transformers.js +15 -45
  70. package/build/lib/schema/cli-transformers.js.map +1 -1
  71. package/build/lib/schema/index.d.ts +2 -2
  72. package/build/lib/schema/index.d.ts.map +1 -1
  73. package/build/lib/schema/index.js.map +1 -1
  74. package/build/lib/schema/keywords.d.ts +12 -20
  75. package/build/lib/schema/keywords.d.ts.map +1 -1
  76. package/build/lib/schema/keywords.js +6 -51
  77. package/build/lib/schema/keywords.js.map +1 -1
  78. package/build/lib/schema/schema.d.ts +106 -231
  79. package/build/lib/schema/schema.d.ts.map +1 -1
  80. package/build/lib/schema/schema.js +75 -345
  81. package/build/lib/schema/schema.js.map +1 -1
  82. package/build/lib/utils.d.ts +59 -238
  83. package/build/lib/utils.d.ts.map +1 -1
  84. package/build/lib/utils.js +55 -207
  85. package/build/lib/utils.js.map +1 -1
  86. package/lib/cli/{args.js → args.ts} +40 -51
  87. package/lib/cli/driver-command.ts +122 -0
  88. package/lib/cli/{extension-command.js → extension-command.ts} +610 -689
  89. package/lib/cli/extension.ts +65 -0
  90. package/lib/cli/{parser.js → parser.ts} +48 -71
  91. package/lib/cli/plugin-command.ts +117 -0
  92. package/lib/cli/{setup-command.js → setup-command.ts} +57 -72
  93. package/lib/cli/utils.ts +97 -0
  94. package/lib/config-file.ts +212 -0
  95. package/lib/{config.js → config.ts} +129 -141
  96. package/lib/{constants.js → constants.ts} +30 -41
  97. package/lib/doctor/{doctor.js → doctor.ts} +81 -91
  98. package/lib/grid-register.ts +250 -0
  99. package/lib/{logsink.js → logsink.ts} +91 -137
  100. package/lib/main.js +1 -1
  101. package/lib/schema/arg-spec.ts +131 -0
  102. package/lib/schema/cli-args.ts +171 -0
  103. package/lib/schema/cli-transformers.ts +83 -0
  104. package/lib/schema/keywords.ts +96 -0
  105. package/lib/schema/schema.ts +449 -0
  106. package/lib/utils.ts +404 -0
  107. package/package.json +19 -20
  108. package/tsconfig.json +1 -1
  109. package/build/package.json +0 -99
  110. package/lib/cli/driver-command.js +0 -174
  111. package/lib/cli/extension.js +0 -74
  112. package/lib/cli/plugin-command.js +0 -164
  113. package/lib/cli/utils.js +0 -91
  114. package/lib/config-file.js +0 -228
  115. package/lib/grid-register.js +0 -146
  116. package/lib/schema/arg-spec.js +0 -229
  117. package/lib/schema/cli-args.js +0 -254
  118. package/lib/schema/cli-transformers.js +0 -113
  119. package/lib/schema/keywords.js +0 -136
  120. package/lib/schema/schema.js +0 -725
  121. package/lib/utils.js +0 -512
  122. /package/lib/schema/{index.js → index.ts} +0 -0
@@ -1,22 +1,26 @@
1
+ import type {ParsedArgs} from 'appium/types';
2
+ import type {MessageObject} from '@appium/logger';
3
+ import type {Logger, Logform} from 'winston';
4
+ import type Transport from 'winston-transport';
1
5
  import globalLog from '@appium/logger';
2
6
  import {createLogger, format, transports} from 'winston';
3
7
  import {fs} from '@appium/support';
4
8
  import _ from 'lodash';
5
- import { adler32 } from './utils';
6
- import { LRUCache } from 'lru-cache';
9
+ import {adler32} from './utils';
10
+ import {LRUCache} from 'lru-cache';
7
11
 
8
12
  const LEVELS_MAP = {
9
13
  debug: 4,
10
14
  info: 3,
11
15
  warn: 2,
12
16
  error: 1,
13
- };
17
+ } as const;
14
18
  const COLORS_MAP = {
15
19
  info: 'cyan',
16
20
  debug: 'grey',
17
21
  warn: 'yellow',
18
22
  error: 'red',
19
- };
23
+ } as const;
20
24
  const TO_WINSTON_LEVELS_MAP = {
21
25
  silly: 'debug',
22
26
  verbose: 'debug',
@@ -25,49 +29,48 @@ const TO_WINSTON_LEVELS_MAP = {
25
29
  http: 'info',
26
30
  warn: 'warn',
27
31
  error: 'error',
28
- };
32
+ } as const;
29
33
  const COLOR_CODE_PATTERN = /\u001b\[(\d+(;\d+)*)?m/g; // eslint-disable-line no-control-regex
30
34
 
31
35
  // https://www.ditig.com/publications/256-colors-cheat-sheet
32
36
  const MIN_COLOR = 17;
33
37
  const MAX_COLOR = 231;
34
- /** @type {LRUCache<string, number>} */
35
- const COLORS_CACHE = new LRUCache({
38
+ const COLORS_CACHE = new LRUCache<string, number>({
36
39
  max: 1024,
37
40
  ttl: 1000 * 60 * 60 * 24, // expire after 24 hours
38
41
  updateAgeOnGet: true,
39
42
  });
40
43
 
41
44
  // npmlog is used only for emitting, we use winston for output (global is set by support)
42
- /** @type {import('winston').Logger?} */
43
- let log = null;
45
+ let log: Logger | null = null;
44
46
 
45
47
  /**
48
+ * Initialize the log sink from parsed CLI/server args.
49
+ * Sets up Winston transports (console, optional file, optional webhook) and forwards
50
+ * npmlog messages to them. Call this before other logging setup.
46
51
  *
47
- * @param {import('../types').ParsedArgs} args
48
- * @returns {Promise<void>}
52
+ * @param args - Parsed server/CLI arguments (e.g. `loglevel`, `logFile`, `webhook`).
49
53
  */
50
- export async function init(args) {
54
+ export async function init(args: ParsedArgs): Promise<void> {
51
55
  globalLog.level = 'silent';
52
56
 
53
57
  // clean up in case we have initiated before since npmlog is a global object
54
58
  clear();
55
59
 
56
- const transports = await createTransports(args);
57
- const transportNames = new Set(transports.map((tr) => tr.constructor.name));
60
+ const transportList = await createTransports(args);
61
+ const transportNames = new Set(transportList.map((tr) => tr.constructor.name));
58
62
  log = createLogger({
59
- transports,
63
+ transports: transportList,
60
64
  levels: LEVELS_MAP,
61
65
  handleExceptions: true,
62
- exitOnError: false
66
+ exitOnError: false,
63
67
  });
64
68
 
65
- const reportedLoggerErrors = new Set();
69
+ const reportedLoggerErrors = new Set<string>();
66
70
  // Capture logs emitted via npmlog and pass them through winston
67
- globalLog.on('log', (/** @type {MessageObject} */{level, message, prefix}) => {
71
+ globalLog.on('log', ({level, message, prefix}: MessageObject) => {
68
72
  const {sessionSignature} = globalLog.asyncStorage.getStore() ?? {};
69
- /** @type {string[]} */
70
- const prefixes = [];
73
+ const prefixes: string[] = [];
71
74
  if (sessionSignature) {
72
75
  prefixes.push(sessionSignature);
73
76
  }
@@ -78,24 +81,26 @@ export async function init(args) {
78
81
  if (!_.isEmpty(prefixes)) {
79
82
  const finalPrefix = prefixes
80
83
  .map(toDecoratedPrefix)
81
- .map((pfx) => isLogColorEnabled(args) ? colorizePrefix(pfx) : pfx)
84
+ .map((pfx) => (isLogColorEnabled(args) ? colorizePrefix(pfx) : pfx))
82
85
  .join('');
83
86
  msg = `${finalPrefix} ${msg}`;
84
87
  }
85
88
  const winstonLevel = TO_WINSTON_LEVELS_MAP[level] || 'info';
86
89
  try {
87
- /** @type {import('winston').Logger} */(log)[winstonLevel](msg);
90
+ (log as Logger)[winstonLevel as keyof Logger](msg);
88
91
  if (_.isFunction(args.logHandler)) {
89
92
  args.logHandler(level, msg);
90
93
  }
91
94
  } catch (e) {
92
- if (!reportedLoggerErrors.has(e.message) && process.stderr.writable) {
95
+ const err = e as Error;
96
+ if (!reportedLoggerErrors.has(err.message) && process.stderr.writable) {
93
97
  // eslint-disable-next-line no-console
94
98
  console.error(
95
99
  `The log message '${_.truncate(msg, {length: 30})}' cannot be written into ` +
96
- `one or more requested destinations: ${transportNames}. Original error: ${e.message}`
100
+ `one or more requested destinations: ${[...transportNames].join(', ')}. ` +
101
+ `Original error: ${err.message}`
97
102
  );
98
- reportedLoggerErrors.add(e.message);
103
+ reportedLoggerErrors.add(err.message);
99
104
  }
100
105
  }
101
106
  });
@@ -104,37 +109,21 @@ export async function init(args) {
104
109
  }
105
110
 
106
111
  /**
107
- * @returns {void}
112
+ * Clear the log sink and remove global log listeners.
113
+ * Safe to call before re-initializing with `init`.
108
114
  */
109
- export function clear() {
115
+ export function clear(): void {
110
116
  log?.clear();
111
117
  globalLog.removeAllListeners('log');
112
118
  }
113
119
 
114
- // set the custom colors
115
- const colorizeFormat = format.colorize({
116
- colors: COLORS_MAP,
117
- });
118
-
119
- // Strip the color marking within messages
120
- const stripColorFormat = format(function stripColor(info) {
121
- return {
122
- ...info,
123
- level: stripColorCodes(info.level),
124
- message: _.isString(info.message) ? stripColorCodes(info.message) : info.message,
125
- };
126
- })();
120
+ // #region private helpers
127
121
 
128
- /**
129
- *
130
- * @param {ParsedArgs} args
131
- * @param {string} logLvl
132
- * @returns {transports.ConsoleTransportInstance}
133
- */
134
- function createConsoleTransport(args, logLvl) {
135
- /** @type {AppiumConsoleTransportOptions} */
136
- const opt = {
137
- name: 'console',
122
+ function createConsoleTransport(
123
+ args: ParsedArgs,
124
+ logLvl: string
125
+ ): transports.ConsoleTransportInstance {
126
+ const opt: transports.ConsoleTransportOptions = {
138
127
  level: logLvl,
139
128
  stderrLevels: ['error'],
140
129
  format: format.combine(
@@ -146,16 +135,11 @@ function createConsoleTransport(args, logLvl) {
146
135
  return new transports.Console(opt);
147
136
  }
148
137
 
149
- /**
150
- *
151
- * @param {ParsedArgs} args
152
- * @param {string} logLvl
153
- * @returns {transports.FileTransportInstance}
154
- */
155
- function createFileTransport(args, logLvl) {
156
- /** @type {AppiumFileTransportOptions} */
157
- const opt = {
158
- name: 'file',
138
+ function createFileTransport(
139
+ args: ParsedArgs,
140
+ logLvl: string
141
+ ): transports.FileTransportInstance {
142
+ const opt: transports.FileTransportOptions = {
159
143
  filename: args.logFile,
160
144
  maxFiles: 1,
161
145
  level: logLvl,
@@ -168,13 +152,10 @@ function createFileTransport(args, logLvl) {
168
152
  return new transports.File(opt);
169
153
  }
170
154
 
171
- /**
172
- *
173
- * @param {ParsedArgs} args
174
- * @param {string} logLvl
175
- * @returns {transports.HttpTransportInstance}
176
- */
177
- function createHttpTransport(args, logLvl) {
155
+ function createHttpTransport(
156
+ args: ParsedArgs,
157
+ logLvl: string
158
+ ): transports.HttpTransportInstance {
178
159
  let host = '127.0.0.1';
179
160
  let port = 9003;
180
161
 
@@ -184,32 +165,20 @@ function createHttpTransport(args, logLvl) {
184
165
  port = parseInt(hostAndPort[1], 10);
185
166
  }
186
167
 
187
- /** @type {AppiumHttpTransportOptions} */
188
- const opt = {
189
- name: 'http',
168
+ const opt: transports.HttpTransportOptions = {
190
169
  host,
191
170
  port,
192
171
  path: '/',
193
172
  level: logLvl,
194
- format: format.combine(
195
- stripColorFormat,
196
- formatLog(args, false),
197
- ),
173
+ format: format.combine(stripColorFormat, formatLog(args, false)),
198
174
  };
199
175
  return new transports.Http(opt);
200
176
  }
201
177
 
202
- /**
203
- *
204
- * @param {ParsedArgs} args
205
- * @returns {Promise<import('winston-transport')[]>}
206
- */
207
- async function createTransports(args) {
208
- const transports = [];
209
- /** @type {string} */
210
- let consoleLogLevel;
211
- /** @type {string} */
212
- let fileLogLevel;
178
+ async function createTransports(args: ParsedArgs): Promise<Transport[]> {
179
+ const transportList: Transport[] = [];
180
+ let consoleLogLevel: string;
181
+ let fileLogLevel: string;
213
182
 
214
183
  // Server args are normalized in main so we only see dest form (`loglevel`).
215
184
  // Fall back to schema default so Winston never sees undefined.
@@ -223,7 +192,7 @@ async function createTransports(args) {
223
192
  consoleLogLevel = fileLogLevel = rawLogLevel;
224
193
  }
225
194
 
226
- transports.push(createConsoleTransport(args, consoleLogLevel));
195
+ transportList.push(createConsoleTransport(args, consoleLogLevel));
227
196
 
228
197
  if (args.logFile) {
229
198
  try {
@@ -234,36 +203,34 @@ async function createTransports(args) {
234
203
  await fs.unlink(args.logFile);
235
204
  }
236
205
 
237
- transports.push(createFileTransport(args, fileLogLevel));
206
+ transportList.push(createFileTransport(args, fileLogLevel));
238
207
  } catch (e) {
208
+ const err = e as Error;
239
209
  // eslint-disable-next-line no-console
240
210
  console.log(
241
- `Tried to attach logging to file '${args.logFile}' but an error ` + `occurred: ${e.message}`
211
+ `Tried to attach logging to file '${args.logFile}' but an error ` +
212
+ `occurred: ${err.message}`
242
213
  );
243
214
  }
244
215
  }
245
216
 
246
217
  if (args.webhook) {
247
218
  try {
248
- transports.push(createHttpTransport(args, fileLogLevel));
219
+ transportList.push(createHttpTransport(args, fileLogLevel));
249
220
  } catch (e) {
221
+ const err = e as Error;
250
222
  // eslint-disable-next-line no-console
251
223
  console.log(
252
224
  `Tried to attach logging to Http at ${args.webhook} but ` +
253
- `an error occurred: ${e.message}`
225
+ `an error occurred: ${err.message}`
254
226
  );
255
227
  }
256
228
  }
257
229
 
258
- return transports;
230
+ return transportList;
259
231
  }
260
232
 
261
- /**
262
- *
263
- * @param {string} text
264
- * @returns {string}
265
- */
266
- function toDecoratedPrefix(text) {
233
+ function toDecoratedPrefix(text: string): string {
267
234
  return `[${text}]`;
268
235
  }
269
236
 
@@ -271,27 +238,19 @@ function toDecoratedPrefix(text) {
271
238
  * Selects the color of the text in terminal from the MIN_COLOR..MAX_COLOR
272
239
  * range. We use adler32 hashing to ensure that equal prefixes would always have
273
240
  * same colors.
274
- *
275
- * @param {string} text Initial text
276
- * @returns {string} Colorized text (with pseudocode cchars added)
277
241
  */
278
- function colorizePrefix(text) {
242
+ function colorizePrefix(text: string): string {
279
243
  let colorIndex = COLORS_CACHE.get(text);
280
244
  if (!colorIndex) {
281
245
  const hash = adler32(text);
282
- colorIndex = MIN_COLOR + hash % (MAX_COLOR - MIN_COLOR);
246
+ colorIndex = MIN_COLOR + (hash % (MAX_COLOR - MIN_COLOR));
283
247
  COLORS_CACHE.set(text, colorIndex);
284
248
  }
285
249
  return `\x1b[38;5;${colorIndex}m${text}\x1b[0m`;
286
250
  }
287
251
 
288
- /**
289
- * @param {ParsedArgs} args
290
- * @param {boolean} targetConsole
291
- * @returns {import('logform').Format}
292
- */
293
- function formatLog(args, targetConsole) {
294
- if (['json', 'pretty_json'].includes(args.logFormat)) {
252
+ function formatLog(args: ParsedArgs, targetConsole: boolean): Logform.Format {
253
+ if (['json', 'pretty_json'].includes(args.logFormat ?? '')) {
295
254
  return format.combine(
296
255
  format((info) => {
297
256
  const infoCopy = {...info};
@@ -311,7 +270,7 @@ function formatLog(args, targetConsole) {
311
270
  );
312
271
  }
313
272
 
314
- return format.printf((info) => {
273
+ return format.printf((info: {timestamp?: string; message?: unknown}) => {
315
274
  if (targetConsole) {
316
275
  return `${args.logTimestamp ? `${info.timestamp} - ` : ''}${info.message}`;
317
276
  }
@@ -319,13 +278,8 @@ function formatLog(args, targetConsole) {
319
278
  });
320
279
  }
321
280
 
322
- /**
323
- * add the timestamp in the correct format to the log info object
324
- *
325
- * @param {ParsedArgs} args
326
- * @returns {import('logform').Format}
327
- */
328
- function formatTimestamp(args) {
281
+ /** Add the timestamp in the correct format to the log info object. */
282
+ function formatTimestamp(args: ParsedArgs): Logform.Format {
329
283
  return format.timestamp({
330
284
  format() {
331
285
  let date = new Date();
@@ -338,31 +292,31 @@ function formatTimestamp(args) {
338
292
  });
339
293
  }
340
294
 
295
+ // set the custom colors
296
+ const colorizeFormat = format.colorize({
297
+ colors: COLORS_MAP,
298
+ });
299
+
341
300
  /**
342
- * Strips color control codes from the given string
301
+ * Strips ANSI color control codes from a string.
343
302
  *
344
- * @param {string} text
345
- * @returns {string}
303
+ * @param text - String that may contain escape codes (e.g. `\u001b[31m`).
304
+ * @returns The string with all color codes removed.
346
305
  */
347
- export function stripColorCodes(text) {
306
+ export function stripColorCodes(text: string): string {
348
307
  return text.replace(COLOR_CODE_PATTERN, '');
349
308
  }
350
309
 
351
- /**
352
- *
353
- * @param {ParsedArgs} args
354
- * @returns {boolean}
355
- */
356
- function isLogColorEnabled(args) {
310
+ // Strip the color marking within messages (depends on stripColorCodes)
311
+ const stripColorFormat = format(function stripColor(info: {level: string; message: unknown; [key: string]: unknown}) {
312
+ return {
313
+ ...info,
314
+ level: stripColorCodes(info.level),
315
+ message: _.isString(info.message) ? stripColorCodes(info.message) : info.message,
316
+ };
317
+ })();
318
+
319
+ function isLogColorEnabled(args: ParsedArgs): boolean {
357
320
  return !args.logNoColors && args.logFormat === 'text';
358
321
  }
359
-
360
- export default init;
361
-
362
- /**
363
- * @typedef {import('appium/types').ParsedArgs} ParsedArgs
364
- * @typedef {import('@appium/logger').MessageObject} MessageObject
365
- * @typedef {transports.ConsoleTransportOptions & {name: string}} AppiumConsoleTransportOptions
366
- * @typedef {transports.FileTransportOptions & {name: string}} AppiumFileTransportOptions
367
- * @typedef {transports.HttpTransportOptions & {name: string}} AppiumHttpTransportOptions
368
- */
322
+ // #endregion
package/lib/main.js CHANGED
@@ -443,7 +443,7 @@ async function main(args) {
443
443
  }
444
444
  appiumDriver.server = server;
445
445
  try {
446
- // configure as node on grid, if necessary
446
+ // configure as node on Selenium Grid 3 hub, if necessary
447
447
  // falsy values should not cause this to run
448
448
  if (parsedArgs.nodeconfig) {
449
449
  await registerNode(
@@ -0,0 +1,131 @@
1
+ import _ from 'lodash';
2
+ import type {ExtensionType} from '@appium/types';
3
+
4
+ /**
5
+ * The original ID of the Appium config schema.
6
+ * We use this in the CLI to convert it to `argparse` options.
7
+ */
8
+ export const APPIUM_CONFIG_SCHEMA_ID = 'appium.json';
9
+ /**
10
+ * The schema prop containing server-related options. Everything in here
11
+ * is "native" to Appium.
12
+ */
13
+ export const SERVER_PROP_NAME = 'server';
14
+ const SCHEMA_ID_REGEXP = /^(?<extType>.+?)-(?<normalizedExtName>.+)\.json$/;
15
+ const PROPERTIES = 'properties';
16
+
17
+ export interface ArgSpecOptions<D = unknown> {
18
+ extName?: string;
19
+ extType?: ExtensionType;
20
+ dest?: string;
21
+ defaultValue?: D;
22
+ }
23
+
24
+ /**
25
+ * An `ArgSpec` is a class representing metadata about an argument (or config
26
+ * option) used for cross-referencing.
27
+ *
28
+ * This class has no instance methods beyond stringification and is effectively
29
+ * a read-only struct.
30
+ */
31
+ export class ArgSpec<D = unknown> {
32
+ readonly name: string;
33
+ readonly extType?: ExtensionType;
34
+ readonly extName?: string;
35
+ readonly ref: string;
36
+ readonly arg: string;
37
+ readonly dest: string;
38
+ readonly rawDest: string;
39
+ readonly defaultValue?: D;
40
+
41
+ /**
42
+ * Builds computed fields and assigns them to the instance.
43
+ * Use {@link ArgSpec.create} instead of `new ArgSpec()`.
44
+ */
45
+ constructor(name: string, {extType, extName, dest, defaultValue}: ArgSpecOptions<D> = {}) {
46
+ const arg = ArgSpec.toArg(name, extType, extName);
47
+ const ref = ArgSpec.toSchemaRef(name, extType, extName);
48
+ const rawDest = _.camelCase(dest ?? name);
49
+ const destKeypath = extType && extName ? [extType, extName, rawDest].join('.') : rawDest;
50
+
51
+ this.defaultValue = defaultValue;
52
+ this.name = name;
53
+ this.extType = extType;
54
+ this.extName = extName;
55
+ this.arg = arg;
56
+ this.dest = destKeypath;
57
+ this.ref = ref;
58
+ this.rawDest = rawDest;
59
+ }
60
+
61
+ /**
62
+ * Return the schema ID (`$id`) for the argument given the parameters.
63
+ */
64
+ static toSchemaRef(name: string, extType?: ExtensionType, extName?: string): string {
65
+ const baseRef = ArgSpec.toSchemaBaseRef(extType, extName);
66
+ if (extType && extName) {
67
+ return [`${baseRef}#`, PROPERTIES, name].join('/');
68
+ }
69
+ return [`${baseRef}#`, PROPERTIES, SERVER_PROP_NAME, PROPERTIES, name].join('/');
70
+ }
71
+
72
+ /**
73
+ * Return the root schema ID for an extension or Appium base schema.
74
+ */
75
+ static toSchemaBaseRef(extType?: ExtensionType, extName?: string): string {
76
+ if (extType && extName) {
77
+ return `${extType}-${ArgSpec.toNormalizedExtName(extName)}.json`;
78
+ }
79
+ return APPIUM_CONFIG_SCHEMA_ID;
80
+ }
81
+
82
+ /**
83
+ * Return the unique CLI argument key for the argument.
84
+ */
85
+ static toArg(name: string, extType?: ExtensionType, extName?: string): string {
86
+ const properName = _.kebabCase(name.replace(/^--?/, ''));
87
+ if (extType && extName) {
88
+ return [extType, _.kebabCase(extName), properName].join('-');
89
+ }
90
+ return properName;
91
+ }
92
+
93
+ /**
94
+ * Normalizes a raw extension name (not including type).
95
+ */
96
+ static toNormalizedExtName(extName: string): string {
97
+ return _.kebabCase(extName);
98
+ }
99
+
100
+ /**
101
+ * Parse root schema ID (`<extType>-<normalizedExtName>.json`) to extension info.
102
+ */
103
+ static extensionInfoFromRootSchemaId(
104
+ schemaId: string
105
+ ): {extType?: ExtensionType; normalizedExtName?: string} {
106
+ const matches = schemaId.match(SCHEMA_ID_REGEXP);
107
+ if (matches?.groups) {
108
+ const {extType, normalizedExtName} = matches.groups as {
109
+ extType: ExtensionType;
110
+ normalizedExtName: string;
111
+ };
112
+ return {extType, normalizedExtName};
113
+ }
114
+ return {};
115
+ }
116
+
117
+ /**
118
+ * Creates a frozen `ArgSpec`.
119
+ */
120
+ static create<D = unknown>(name: string, opts?: ArgSpecOptions<D>): Readonly<ArgSpec<D>> {
121
+ return Object.freeze(new ArgSpec(name, opts));
122
+ }
123
+
124
+ toString(): string {
125
+ let str = `[ArgSpec] ${this.name} (${this.ref})`;
126
+ if (this.extType && this.extName) {
127
+ str += ` (ext: ${this.extType}/${this.extName})`;
128
+ }
129
+ return str;
130
+ }
131
+ }