appium 2.0.0-beta.19 → 2.0.0-beta.22

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 (105) hide show
  1. package/build/check-npm-pack-files.js +23 -0
  2. package/build/commands-yml/parse.js +319 -0
  3. package/build/commands-yml/validator.js +130 -0
  4. package/build/index.js +19 -0
  5. package/build/lib/appium-config.schema.json +0 -0
  6. package/build/lib/appium.js +84 -69
  7. package/build/lib/cli/args.js +87 -223
  8. package/build/lib/cli/extension-command.js +2 -2
  9. package/build/lib/cli/extension.js +14 -6
  10. package/build/lib/cli/parser.js +146 -106
  11. package/build/lib/config-file.js +141 -0
  12. package/build/lib/config.js +28 -77
  13. package/build/lib/driver-config.js +41 -20
  14. package/build/lib/ext-config-io.js +165 -0
  15. package/build/lib/extension-config.js +110 -60
  16. package/build/lib/grid-register.js +19 -21
  17. package/build/lib/main.js +135 -72
  18. package/build/lib/plugin-config.js +18 -9
  19. package/build/lib/schema/appium-config-schema.js +253 -0
  20. package/build/lib/schema/arg-spec.js +120 -0
  21. package/build/lib/schema/cli-args.js +188 -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 +72 -0
  25. package/build/lib/schema/schema.js +357 -0
  26. package/build/lib/utils.js +24 -33
  27. package/build/postinstall.js +90 -0
  28. package/build/test/cli/cli-e2e-specs.js +221 -0
  29. package/build/test/cli/cli-helpers.js +86 -0
  30. package/build/test/cli/cli-specs.js +71 -0
  31. package/build/test/cli/fixtures/test-driver/package.json +27 -0
  32. package/build/test/cli/schema-args-specs.js +48 -0
  33. package/build/test/cli/schema-e2e-specs.js +47 -0
  34. package/build/test/config-e2e-specs.js +112 -0
  35. package/build/test/config-file-e2e-specs.js +209 -0
  36. package/build/test/config-file-specs.js +281 -0
  37. package/build/test/config-specs.js +159 -0
  38. package/build/test/driver-e2e-specs.js +435 -0
  39. package/build/test/driver-specs.js +321 -0
  40. package/build/test/ext-config-io-specs.js +181 -0
  41. package/build/test/extension-config-specs.js +365 -0
  42. package/build/test/fixtures/allow-feat.txt +5 -0
  43. package/build/test/fixtures/caps.json +3 -0
  44. package/build/test/fixtures/config/allow-insecure.txt +3 -0
  45. package/build/test/fixtures/config/appium.config.bad-nodeconfig.json +5 -0
  46. package/build/test/fixtures/config/appium.config.bad.json +32 -0
  47. package/build/test/fixtures/config/appium.config.ext-good.json +9 -0
  48. package/build/test/fixtures/config/appium.config.ext-unknown-props.json +10 -0
  49. package/build/test/fixtures/config/appium.config.good.js +40 -0
  50. package/build/test/fixtures/config/appium.config.good.json +33 -0
  51. package/build/test/fixtures/config/appium.config.good.yaml +30 -0
  52. package/build/test/fixtures/config/appium.config.invalid.json +31 -0
  53. package/build/test/fixtures/config/appium.config.security-array.json +5 -0
  54. package/build/test/fixtures/config/appium.config.security-delimited.json +5 -0
  55. package/build/test/fixtures/config/appium.config.security-path.json +5 -0
  56. package/build/test/fixtures/config/driver-fake.config.json +8 -0
  57. package/build/test/fixtures/config/nodeconfig.json +3 -0
  58. package/build/test/fixtures/config/plugin-fake.config.json +0 -0
  59. package/build/test/fixtures/default-args.js +35 -0
  60. package/build/test/fixtures/deny-feat.txt +5 -0
  61. package/build/test/fixtures/driver.schema.js +20 -0
  62. package/build/test/fixtures/extensions.yaml +27 -0
  63. package/build/test/fixtures/flattened-schema.js +504 -0
  64. package/build/test/fixtures/plugin.schema.js +20 -0
  65. package/build/test/fixtures/schema-with-extensions.js +28 -0
  66. package/build/test/grid-register-specs.js +74 -0
  67. package/build/test/helpers.js +75 -0
  68. package/build/test/logger-specs.js +76 -0
  69. package/build/test/npm-specs.js +20 -0
  70. package/build/test/parser-specs.js +314 -0
  71. package/build/test/plugin-e2e-specs.js +316 -0
  72. package/build/test/schema/arg-spec-specs.js +70 -0
  73. package/build/test/schema/cli-args-specs.js +431 -0
  74. package/build/test/schema/schema-specs.js +389 -0
  75. package/build/test/utils-specs.js +266 -0
  76. package/index.js +11 -0
  77. package/lib/appium-config.schema.json +278 -0
  78. package/lib/appium.js +99 -75
  79. package/lib/cli/args.js +138 -335
  80. package/lib/cli/extension-command.js +7 -6
  81. package/lib/cli/extension.js +12 -4
  82. package/lib/cli/parser.js +251 -96
  83. package/lib/config-file.js +227 -0
  84. package/lib/config.js +63 -79
  85. package/lib/driver-config.js +66 -11
  86. package/lib/ext-config-io.js +287 -0
  87. package/lib/extension-config.js +209 -66
  88. package/lib/grid-register.js +24 -21
  89. package/lib/main.js +145 -71
  90. package/lib/plugin-config.js +33 -3
  91. package/lib/schema/appium-config-schema.js +287 -0
  92. package/lib/schema/arg-spec.js +222 -0
  93. package/lib/schema/cli-args.js +285 -0
  94. package/lib/schema/cli-transformers.js +123 -0
  95. package/lib/schema/index.js +2 -0
  96. package/lib/schema/keywords.js +135 -0
  97. package/lib/schema/schema.js +577 -0
  98. package/lib/utils.js +29 -52
  99. package/package.json +17 -16
  100. package/types/appium-config.d.ts +197 -0
  101. package/types/types.d.ts +201 -0
  102. package/build/lib/cli/argparse-actions.js +0 -104
  103. package/build/lib/cli/parser-helpers.js +0 -106
  104. package/lib/cli/argparse-actions.js +0 -77
  105. package/lib/cli/parser-helpers.js +0 -106
package/lib/config.js CHANGED
@@ -5,10 +5,8 @@ import { exec } from 'teen_process';
5
5
  import { rootDir } from './utils';
6
6
  import logger from './logger';
7
7
  import semver from 'semver';
8
- import {
9
- StoreDeprecatedDefaultCapabilityAction, DEFAULT_CAPS_ARG,
10
- } from './cli/argparse-actions';
11
8
  import findUp from 'find-up';
9
+ import { getDefaultsFromSchema } from './schema/schema';
12
10
 
13
11
  const npmPackage = fs.readPackageJsonFrom(__dirname);
14
12
 
@@ -27,11 +25,6 @@ function getNodeVersion () {
27
25
  return semver.coerce(process.version);
28
26
  }
29
27
 
30
- function isSubClass (candidateClass, superClass) {
31
- return _.isFunction(superClass) && _.isFunction(candidateClass)
32
- && (candidateClass.prototype instanceof superClass || candidateClass === superClass);
33
- }
34
-
35
28
  async function updateBuildInfo (useGithubApiFallback = false) {
36
29
  const sha = await getGitRev(useGithubApiFallback);
37
30
  if (!sha) {
@@ -156,80 +149,72 @@ async function showConfig () {
156
149
  console.log(JSON.stringify(getBuildInfo())); // eslint-disable-line no-console
157
150
  }
158
151
 
159
- function getNonDefaultArgs (parser, args) {
160
- return parser.rawArgs.reduce((acc, [, {dest, default: defaultValue}]) => {
161
- if (args[dest] && args[dest] !== defaultValue) {
162
- acc[dest] = args[dest];
163
- }
164
- return acc;
165
- }, {});
166
- }
152
+ function getNonDefaultServerArgs (parser, args) {
153
+ // hopefully these function names are descriptive enough
167
154
 
168
- function getDeprecatedArgs (parser, args) {
169
- // go through the server command line arguments and figure
170
- // out which of the ones used are deprecated
171
- return parser.rawArgs.reduce((acc, [[name], {dest, default: defaultValue, action}]) => {
172
- if (!args[dest] || args[dest] === defaultValue) {
173
- return acc;
174
- }
155
+ function typesDiffer (dest) {
156
+ return typeof args[dest] !== typeof defaultsFromSchema[dest];
157
+ }
175
158
 
176
- if (action?.deprecated_for) {
177
- acc[name] = action.deprecated_for;
178
- } else if (isSubClass(action, StoreDeprecatedDefaultCapabilityAction)) {
179
- acc[name] = DEFAULT_CAPS_ARG;
180
- }
181
- return acc;
182
- }, {});
183
- }
159
+ function defaultValueIsArray (dest) {
160
+ return _.isArray(defaultsFromSchema[dest]);
161
+ }
184
162
 
185
- function checkValidPort (port, portName) {
186
- if (port > 0 && port < 65536) return true; // eslint-disable-line curly
187
- logger.error(`Port '${portName}' must be greater than 0 and less than 65536. Currently ${port}`);
188
- return false;
189
- }
163
+ function argsValueIsArray (dest) {
164
+ return _.isArray(args[dest]);
165
+ }
190
166
 
191
- function validateServerArgs (parser, args) {
192
- // arguments that cannot both be set
193
- let exclusives = [
194
- ['noReset', 'fullReset'],
195
- ['ipa', 'safari'],
196
- ['app', 'safari'],
197
- ['forceIphone', 'forceIpad'],
198
- ['deviceName', 'defaultDevice']
199
- ];
200
-
201
- for (let exSet of exclusives) {
202
- let numFoundInArgs = 0;
203
- for (let opt of exSet) {
204
- if (_.has(args, opt) && args[opt]) {
205
- numFoundInArgs++;
206
- }
207
- }
208
- if (numFoundInArgs > 1) {
209
- throw new Error(`You can't pass in more than one argument from the ` +
210
- `set ${JSON.stringify(exSet)}, since they are ` +
211
- `mutually exclusive`);
212
- }
167
+ function arraysDiffer (dest) {
168
+ return _.difference(args[dest], defaultsFromSchema[dest]).length > 0;
213
169
  }
214
170
 
215
- const validations = {
216
- port: checkValidPort,
217
- callbackPort: checkValidPort,
218
- bootstrapPort: checkValidPort,
219
- chromedriverPort: checkValidPort,
220
- robotPort: checkValidPort,
221
- backendRetries: (r) => r >= 0,
222
- };
223
-
224
- const nonDefaultArgs = getNonDefaultArgs(parser, args);
225
-
226
- for (let [arg, validator] of _.toPairs(validations)) {
227
- if (_.has(nonDefaultArgs, arg)) {
228
- if (!validator(args[arg], arg)) {
229
- throw new Error(`Invalid argument for param ${arg}: ${args[arg]}`);
230
- }
231
- }
171
+ function valuesUnequal (dest) {
172
+ return args[dest] !== defaultsFromSchema[dest];
173
+ }
174
+
175
+ function defaultIsDefined (dest) {
176
+ return !_.isUndefined(defaultsFromSchema[dest]);
232
177
  }
178
+
179
+ // note that `_.overEvery` is like an "AND", and `_.overSome` is like an "OR"
180
+
181
+ const argValueNotArrayOrArraysDiffer = _.overSome([
182
+ _.negate(argsValueIsArray),
183
+ arraysDiffer
184
+ ]);
185
+
186
+ const defaultValueNotArrayAndValuesUnequal = _.overEvery([
187
+ _.negate(defaultValueIsArray), valuesUnequal
188
+ ]);
189
+
190
+ /**
191
+ * This used to be a hideous conditional, but it's broken up into a hideous function instead.
192
+ * hopefully this makes things a little more understandable.
193
+ * - checks if the default value is defined
194
+ * - if so, and the default is not an array:
195
+ * - ensures the types are the same
196
+ * - ensures the values are equal
197
+ * - if so, and the default is an array:
198
+ * - ensures the args value is an array
199
+ * - ensures the args values do not differ from the default values
200
+ * @param {string} dest - argument name (`dest` value)
201
+ * @returns {boolean}
202
+ */
203
+ const isNotDefault = _.overEvery([
204
+ defaultIsDefined,
205
+ _.overSome([
206
+ typesDiffer,
207
+ _.overEvery([
208
+ defaultValueIsArray,
209
+ argValueNotArrayOrArraysDiffer
210
+ ]),
211
+ defaultValueNotArrayAndValuesUnequal
212
+ ])
213
+ ]);
214
+
215
+ const defaultsFromSchema = getDefaultsFromSchema();
216
+
217
+ return _.pickBy(args, (__, key) => isNotDefault(key));
233
218
  }
234
219
 
235
220
  async function validateTmpDir (tmpDir) {
@@ -242,8 +227,7 @@ async function validateTmpDir (tmpDir) {
242
227
  }
243
228
 
244
229
  export {
245
- getBuildInfo, validateServerArgs, checkNodeOk, showConfig,
246
- warnNodeDeprecations, validateTmpDir, getNonDefaultArgs,
247
- getGitRev, checkValidPort, APPIUM_VER, updateBuildInfo,
248
- getDeprecatedArgs,
230
+ getBuildInfo, checkNodeOk, showConfig,
231
+ warnNodeDeprecations, validateTmpDir, getNonDefaultServerArgs,
232
+ getGitRev, APPIUM_VER, updateBuildInfo
249
233
  };
@@ -1,25 +1,74 @@
1
+ // @ts-check
2
+
1
3
  import _ from 'lodash';
2
- import ExtensionConfig, { DRIVER_TYPE } from './extension-config';
4
+ import ExtensionConfig from './extension-config';
5
+ import { DRIVER_TYPE } from './ext-config-io';
3
6
 
4
7
  export default class DriverConfig extends ExtensionConfig {
5
- constructor (appiumHome, logFn = null) {
8
+ /**
9
+ * A mapping of `APPIUM_HOME` values to {@link DriverConfig} instances.
10
+ * Each `APPIUM_HOME` should only have one associated `DriverConfig` instance.
11
+ * @type {Record<string,DriverConfig>}
12
+ * @private
13
+ */
14
+ static _instances = {};
15
+
16
+ /**
17
+ * Call {@link DriverConfig.getInstance} instead.
18
+ * @private
19
+ * @param {string} appiumHome
20
+ * @param {(...args: any[]) => void} [logFn]
21
+ */
22
+ constructor (appiumHome, logFn) {
6
23
  super(appiumHome, DRIVER_TYPE, logFn);
24
+ /** @type {Set<string>} */
25
+ this.knownAutomationNames = new Set();
26
+ }
27
+
28
+ async read () {
29
+ this.knownAutomationNames.clear();
30
+ return await super.read();
7
31
  }
8
32
 
9
- getConfigProblems (driver) {
33
+ /**
34
+ * Creates or gets an instance of {@link DriverConfig} based value of `appiumHome`
35
+ * @param {string} appiumHome - `APPIUM_HOME` path
36
+ * @param {(...args: any[]) => void} [logFn] - Optional logging function
37
+ * @returns {DriverConfig}
38
+ */
39
+ static getInstance (appiumHome, logFn) {
40
+ const instance = DriverConfig._instances[appiumHome] ?? new DriverConfig(appiumHome, logFn);
41
+ DriverConfig._instances[appiumHome] = instance;
42
+ return instance;
43
+ }
44
+
45
+ /**
46
+ *
47
+ * @param {object} extData
48
+ * @param {string} extName
49
+ * @returns {import('./extension-config').Problem[]}
50
+ */
51
+ // eslint-disable-next-line no-unused-vars
52
+ getConfigProblems (extData, extName) {
10
53
  const problems = [];
11
- const automationNames = [];
12
- const {platformNames, automationName} = driver;
54
+ const {platformNames, automationName} = extData;
13
55
 
14
56
  if (!_.isArray(platformNames)) {
15
57
  problems.push({
16
- err: 'Missing or incorrect supported platformName list.',
58
+ err: 'Missing or incorrect supported platformNames list.',
17
59
  val: platformNames
18
60
  });
19
61
  } else {
20
- for (const pName of platformNames) {
21
- if (!_.isString(pName)) {
22
- problems.push({err: 'Incorrectly formatted platformName.', val: pName});
62
+ if (_.isEmpty(platformNames)) {
63
+ problems.push({
64
+ err: 'Empty platformNames list.',
65
+ val: platformNames
66
+ });
67
+ } else {
68
+ for (const pName of platformNames) {
69
+ if (!_.isString(pName)) {
70
+ problems.push({err: 'Incorrectly formatted platformName.', val: pName});
71
+ }
23
72
  }
24
73
  }
25
74
  }
@@ -28,17 +77,23 @@ export default class DriverConfig extends ExtensionConfig {
28
77
  problems.push({err: 'Missing or incorrect automationName', val: automationName});
29
78
  }
30
79
 
31
- if (_.includes(automationNames, automationName)) {
80
+ if (this.knownAutomationNames.has(automationName)) {
32
81
  problems.push({
33
82
  err: 'Multiple drivers claim support for the same automationName',
34
83
  val: automationName
35
84
  });
36
85
  }
37
- automationNames.push(automationName);
86
+
87
+ // should we retain the name at the end of this function, once we've checked there are no problems?
88
+ this.knownAutomationNames.add(automationName);
38
89
 
39
90
  return problems;
40
91
  }
41
92
 
93
+ /**
94
+ * @param {string} driverName
95
+ * @param {object} extData
96
+ */
42
97
  extensionDesc (driverName, {version, automationName}) {
43
98
  return `${driverName}@${version} (automationName '${automationName}')`;
44
99
  }
@@ -0,0 +1,287 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Module containing {@link ExtConfigIO} which handles reading & writing of extension config files.
5
+ */
6
+
7
+ import { fs, mkdirp } from '@appium/support';
8
+ import _ from 'lodash';
9
+ import path from 'path';
10
+ import YAML from 'yaml';
11
+
12
+ const CONFIG_FILE_NAME = 'extensions.yaml';
13
+
14
+ /**
15
+ * Current configuration schema revision!
16
+ */
17
+ const CONFIG_SCHEMA_REV = 2;
18
+
19
+ export const DRIVER_TYPE = 'driver';
20
+ export const PLUGIN_TYPE = 'plugin';
21
+
22
+ /**
23
+ * Set of valid extension types.
24
+ * @type {Readonly<Set<ExtensionType>>}
25
+ */
26
+ const VALID_EXT_TYPES = new Set([DRIVER_TYPE, PLUGIN_TYPE]);
27
+
28
+ const CONFIG_DATA_DRIVER_KEY = `${DRIVER_TYPE}s`;
29
+ const CONFIG_DATA_PLUGIN_KEY = `${PLUGIN_TYPE}s`;
30
+
31
+ /**
32
+ * Handles reading & writing of extension config files.
33
+ *
34
+ * Only one instance of this class exists per value of `APPIUM_HOME`.
35
+ */
36
+ class ExtConfigIO {
37
+ /**
38
+ * "Dirty" flag. If true, the data has changed since the last write.
39
+ * @type {boolean}
40
+ * @private
41
+ */
42
+ _dirty;
43
+
44
+ /**
45
+ * The entire contents of a parsed YAML extension config file.
46
+ * @type {object?}
47
+ * @private
48
+ */
49
+ _data;
50
+
51
+ /**
52
+ * A mapping of extension type to configuration data. Configuration data is
53
+ * keyed on extension name.
54
+ *
55
+ * Consumers get the values of this `Map` (corresponding to the
56
+ * `extensionType` of the consumer, which will be a subclass of
57
+ * `ExtensionConfig`) and do not have access to the entire data object.
58
+ * @private
59
+ * @type {Map<ExtensionType,object>}
60
+ */
61
+ _extDataByType = new Map();
62
+
63
+ /**
64
+ * Path to config file.
65
+ * @private
66
+ * @type {Readonly<string>}
67
+ */
68
+ _filepath;
69
+
70
+ /**
71
+ * Path to `APPIUM_HOME`
72
+ * @private
73
+ * @type {Readonly<string>}
74
+ */
75
+ _appiumHome;
76
+
77
+ /**
78
+ * Helps avoid writing multiple times.
79
+ *
80
+ * If this is `null`, calling {@link ExtConfigIO.write} will cause it to be
81
+ * set to a `Promise`. When the call to `write()` is complete, the `Promise`
82
+ * will resolve and then this value will be set to `null`. Concurrent calls
83
+ * made while this value is a `Promise` will return the `Promise` itself.
84
+ * @private
85
+ * @type {Promise<boolean>?}
86
+ */
87
+ _writing = null;
88
+
89
+ /**
90
+ * Helps avoid reading multiple times.
91
+ *
92
+ * If this is `null`, calling {@link ExtConfigIO.read} will cause it to be
93
+ * set to a `Promise`. When the call to `read()` is complete, the `Promise`
94
+ * will resolve and then this value will be set to `null`. Concurrent calls
95
+ * made while this value is a `Promise` will return the `Promise` itself.
96
+ * @private
97
+ * @type {Promise<void>?}
98
+ */
99
+ _reading = null;
100
+
101
+ /**
102
+ * @param {string} appiumHome
103
+ */
104
+ constructor (appiumHome) {
105
+ this._filepath = path.resolve(appiumHome, CONFIG_FILE_NAME);
106
+ this._appiumHome = appiumHome;
107
+ }
108
+
109
+ /**
110
+ * Creaes a `Proxy` which watches for changes to the extension-type-specific
111
+ * config data.
112
+ *
113
+ * When changes are detected, it sets a `_dirty` flag. The next call to
114
+ * {@link ExtConfigIO.write} will check if this flag is `true` before
115
+ * proceeding.
116
+ * @param {ExtensionType} extensionType
117
+ * @param {Record<string,object>} data - Extension config data, keyed by name
118
+ * @private
119
+ * @returns {Record<string,object>}
120
+ */
121
+ _createProxy (extensionType, data) {
122
+ return new Proxy(data[`${extensionType}s`], {
123
+ set: (target, prop, value) => {
124
+ if (value !== target[prop]) {
125
+ this._dirty = true;
126
+ }
127
+ target[prop] = value;
128
+ return Reflect.set(target, prop, value);
129
+ },
130
+ deleteProperty: (target, prop) => {
131
+ if (prop in target) {
132
+ this._dirty = true;
133
+ }
134
+ return Reflect.deleteProperty(target, prop);
135
+ },
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Returns the path to the config file.
141
+ */
142
+ get filepath () {
143
+ return this._filepath;
144
+ }
145
+
146
+ /**
147
+ * Gets data for an extension type. Reads the config file if necessary.
148
+ *
149
+ * Force-reading is _not_ supported, as it's likely to be a source of
150
+ * bugs--it's easy to mutate the data and then overwrite memory with the file
151
+ * contents
152
+ *
153
+ * Ideally this will only ever read the file _once_.
154
+ * @param {ExtensionType} extensionType - Which bit of the config data we
155
+ * want
156
+ * @returns {Promise<object>} The data
157
+ */
158
+ async read (extensionType) {
159
+ if (this._reading) {
160
+ await this._reading;
161
+ return this._extDataByType.get(extensionType);
162
+ }
163
+
164
+ this._reading = (async () => {
165
+ if (!VALID_EXT_TYPES.has(extensionType)) {
166
+ throw new TypeError(
167
+ `Invalid extension type: ${extensionType}. Valid values are: ${[
168
+ ...VALID_EXT_TYPES,
169
+ ].join(', ')}`,
170
+ );
171
+ }
172
+ if (this._extDataByType.has(extensionType)) {
173
+ return;
174
+ }
175
+
176
+ let data;
177
+ let isNewFile = false;
178
+ try {
179
+ await mkdirp(this._appiumHome);
180
+ const yaml = await fs.readFile(this.filepath, 'utf8');
181
+ data = YAML.parse(yaml);
182
+ } catch (err) {
183
+ if (err.code === 'ENOENT') {
184
+ data = {
185
+ [CONFIG_DATA_DRIVER_KEY]: {},
186
+ [CONFIG_DATA_PLUGIN_KEY]: {},
187
+ schemaRev: CONFIG_SCHEMA_REV,
188
+ };
189
+ isNewFile = true;
190
+ } else {
191
+ throw new Error(
192
+ `Appium had trouble loading the extension installation ` +
193
+ `cache file (${this.filepath}). Ensure it exists and is ` +
194
+ `readable. Specific error: ${err.message}`,
195
+ );
196
+ }
197
+ }
198
+
199
+ this._data = data;
200
+ this._extDataByType.set(
201
+ DRIVER_TYPE,
202
+ this._createProxy(DRIVER_TYPE, data),
203
+ );
204
+ this._extDataByType.set(
205
+ PLUGIN_TYPE,
206
+ this._createProxy(PLUGIN_TYPE, data),
207
+ );
208
+
209
+ if (isNewFile) {
210
+ await this.write(true);
211
+ }
212
+ })();
213
+ try {
214
+ await this._reading;
215
+ return this._extDataByType.get(extensionType);
216
+ } finally {
217
+ this._reading = null;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Writes the data if it needs writing.
223
+ *
224
+ * If the `schemaRev` prop needs updating, the file will be written.
225
+ * @param {boolean} [force=false] - Whether to force a write even if the data is clean
226
+ * @returns {Promise<boolean>} Whether the data was written
227
+ */
228
+ async write (force = false) {
229
+ if (this._writing) {
230
+ return this._writing;
231
+ }
232
+ this._writing = (async () => {
233
+ try {
234
+ if (!this._dirty && !force) {
235
+ return false;
236
+ }
237
+
238
+ if (!this._data) {
239
+ throw new ReferenceError('No data to write. Call `read()` first');
240
+ }
241
+
242
+ const dataToWrite = {
243
+ ...this._data,
244
+ [CONFIG_DATA_DRIVER_KEY]: this._extDataByType.get(DRIVER_TYPE),
245
+ [CONFIG_DATA_PLUGIN_KEY]: this._extDataByType.get(PLUGIN_TYPE),
246
+ };
247
+
248
+ try {
249
+ await fs.writeFile(
250
+ this.filepath,
251
+ YAML.stringify(dataToWrite),
252
+ 'utf8',
253
+ );
254
+ this._dirty = false;
255
+ return true;
256
+ } catch {
257
+ throw new Error(
258
+ `Appium could not parse or write from the Appium Home directory ` +
259
+ `(${this._appiumHome}). Please ensure it is writable.`,
260
+ );
261
+ }
262
+ } finally {
263
+ this._writing = null;
264
+ }
265
+ })();
266
+ return await this._writing;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Factory function for {@link ExtConfigIO}.
272
+ *
273
+ * Maintains one instance per value of `APPIUM_HOME`.
274
+ * @param {string} appiumHome - `APPIUM_HOME`
275
+ * @returns {ExtConfigIO}
276
+ */
277
+ export const getExtConfigIOInstance = _.memoize(
278
+ (appiumHome) => new ExtConfigIO(appiumHome),
279
+ );
280
+
281
+ /**
282
+ * @typedef {ExtConfigIO} ExtensionConfigIO
283
+ */
284
+
285
+ /**
286
+ * @typedef {typeof DRIVER_TYPE | typeof PLUGIN_TYPE} ExtensionType
287
+ */