appium 2.0.0-beta.5 → 2.0.0-beta.52

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 (199) hide show
  1. package/README.md +156 -63
  2. package/build/lib/appium.d.ts +226 -0
  3. package/build/lib/appium.d.ts.map +1 -0
  4. package/build/lib/appium.js +694 -439
  5. package/build/lib/appium.js.map +1 -0
  6. package/build/lib/cli/args.d.ts +17 -0
  7. package/build/lib/cli/args.d.ts.map +1 -0
  8. package/build/lib/cli/args.js +253 -319
  9. package/build/lib/cli/args.js.map +1 -0
  10. package/build/lib/cli/driver-command.d.ts +102 -0
  11. package/build/lib/cli/driver-command.d.ts.map +1 -0
  12. package/build/lib/cli/driver-command.js +126 -81
  13. package/build/lib/cli/driver-command.js.map +1 -0
  14. package/build/lib/cli/extension-command.d.ts +385 -0
  15. package/build/lib/cli/extension-command.d.ts.map +1 -0
  16. package/build/lib/cli/extension-command.js +731 -383
  17. package/build/lib/cli/extension-command.js.map +1 -0
  18. package/build/lib/cli/extension.d.ts +23 -0
  19. package/build/lib/cli/extension.d.ts.map +1 -0
  20. package/build/lib/cli/extension.js +71 -54
  21. package/build/lib/cli/extension.js.map +1 -0
  22. package/build/lib/cli/parser.d.ts +84 -0
  23. package/build/lib/cli/parser.d.ts.map +1 -0
  24. package/build/lib/cli/parser.js +240 -128
  25. package/build/lib/cli/parser.js.map +1 -0
  26. package/build/lib/cli/plugin-command.d.ts +99 -0
  27. package/build/lib/cli/plugin-command.d.ts.map +1 -0
  28. package/build/lib/cli/plugin-command.js +120 -81
  29. package/build/lib/cli/plugin-command.js.map +1 -0
  30. package/build/lib/cli/utils.d.ts +29 -0
  31. package/build/lib/cli/utils.d.ts.map +1 -0
  32. package/build/lib/cli/utils.js +72 -51
  33. package/build/lib/cli/utils.js.map +1 -0
  34. package/build/lib/config-file.d.ts +100 -0
  35. package/build/lib/config-file.d.ts.map +1 -0
  36. package/build/lib/config-file.js +207 -0
  37. package/build/lib/config-file.js.map +1 -0
  38. package/build/lib/config.d.ts +49 -0
  39. package/build/lib/config.d.ts.map +1 -0
  40. package/build/lib/config.js +265 -201
  41. package/build/lib/config.js.map +1 -0
  42. package/build/lib/constants.d.ts +49 -0
  43. package/build/lib/constants.d.ts.map +1 -0
  44. package/build/lib/constants.js +66 -0
  45. package/build/lib/constants.js.map +1 -0
  46. package/build/lib/extension/driver-config.d.ts +82 -0
  47. package/build/lib/extension/driver-config.d.ts.map +1 -0
  48. package/build/lib/extension/driver-config.js +212 -0
  49. package/build/lib/extension/driver-config.js.map +1 -0
  50. package/build/lib/extension/extension-config.d.ts +249 -0
  51. package/build/lib/extension/extension-config.d.ts.map +1 -0
  52. package/build/lib/extension/extension-config.js +581 -0
  53. package/build/lib/extension/extension-config.js.map +1 -0
  54. package/build/lib/extension/index.d.ts +48 -0
  55. package/build/lib/extension/index.d.ts.map +1 -0
  56. package/build/lib/extension/index.js +105 -0
  57. package/build/lib/extension/index.js.map +1 -0
  58. package/build/lib/extension/manifest-migrations.d.ts +27 -0
  59. package/build/lib/extension/manifest-migrations.d.ts.map +1 -0
  60. package/build/lib/extension/manifest-migrations.js +118 -0
  61. package/build/lib/extension/manifest-migrations.js.map +1 -0
  62. package/build/lib/extension/manifest.d.ts +145 -0
  63. package/build/lib/extension/manifest.d.ts.map +1 -0
  64. package/build/lib/extension/manifest.js +521 -0
  65. package/build/lib/extension/manifest.js.map +1 -0
  66. package/build/lib/extension/package-changed.d.ts +11 -0
  67. package/build/lib/extension/package-changed.d.ts.map +1 -0
  68. package/build/lib/extension/package-changed.js +62 -0
  69. package/build/lib/extension/package-changed.js.map +1 -0
  70. package/build/lib/extension/plugin-config.d.ts +56 -0
  71. package/build/lib/extension/plugin-config.d.ts.map +1 -0
  72. package/build/lib/extension/plugin-config.js +102 -0
  73. package/build/lib/extension/plugin-config.js.map +1 -0
  74. package/build/lib/grid-register.d.ts +10 -0
  75. package/build/lib/grid-register.d.ts.map +1 -0
  76. package/build/lib/grid-register.js +122 -144
  77. package/build/lib/grid-register.js.map +1 -0
  78. package/build/lib/logger.d.ts +3 -0
  79. package/build/lib/logger.d.ts.map +1 -0
  80. package/build/lib/logger.js +5 -17
  81. package/build/lib/logger.js.map +1 -0
  82. package/build/lib/logsink.d.ts +4 -0
  83. package/build/lib/logsink.d.ts.map +1 -0
  84. package/build/lib/logsink.js +190 -187
  85. package/build/lib/logsink.js.map +1 -0
  86. package/build/lib/main.d.ts +62 -0
  87. package/build/lib/main.d.ts.map +1 -0
  88. package/build/lib/main.js +348 -228
  89. package/build/lib/main.js.map +1 -0
  90. package/build/lib/schema/arg-spec.d.ts +143 -0
  91. package/build/lib/schema/arg-spec.d.ts.map +1 -0
  92. package/build/lib/schema/arg-spec.js +164 -0
  93. package/build/lib/schema/arg-spec.js.map +1 -0
  94. package/build/lib/schema/cli-args.d.ts +19 -0
  95. package/build/lib/schema/cli-args.d.ts.map +1 -0
  96. package/build/lib/schema/cli-args.js +217 -0
  97. package/build/lib/schema/cli-args.js.map +1 -0
  98. package/build/lib/schema/cli-transformers.d.ts +5 -0
  99. package/build/lib/schema/cli-transformers.d.ts.map +1 -0
  100. package/build/lib/schema/cli-transformers.js +124 -0
  101. package/build/lib/schema/cli-transformers.js.map +1 -0
  102. package/build/lib/schema/index.d.ts +3 -0
  103. package/build/lib/schema/index.d.ts.map +1 -0
  104. package/build/lib/schema/index.js +19 -0
  105. package/build/lib/schema/index.js.map +1 -0
  106. package/build/lib/schema/keywords.d.ts +24 -0
  107. package/build/lib/schema/keywords.d.ts.map +1 -0
  108. package/build/lib/schema/keywords.js +128 -0
  109. package/build/lib/schema/keywords.js.map +1 -0
  110. package/build/lib/schema/schema.d.ts +259 -0
  111. package/build/lib/schema/schema.d.ts.map +1 -0
  112. package/build/lib/schema/schema.js +615 -0
  113. package/build/lib/schema/schema.js.map +1 -0
  114. package/build/lib/utils.d.ts +121 -0
  115. package/build/lib/utils.d.ts.map +1 -0
  116. package/build/lib/utils.js +351 -273
  117. package/build/lib/utils.js.map +1 -0
  118. package/build/tsconfig.tsbuildinfo +1 -0
  119. package/build/types/cli.d.ts +134 -0
  120. package/build/types/cli.d.ts.map +1 -0
  121. package/build/types/cli.js +3 -0
  122. package/build/types/cli.js.map +1 -0
  123. package/build/types/index.d.ts +15 -0
  124. package/build/types/index.d.ts.map +1 -0
  125. package/build/types/index.js +19 -0
  126. package/build/types/index.js.map +1 -0
  127. package/build/types/manifest/base.d.ts +135 -0
  128. package/build/types/manifest/base.d.ts.map +1 -0
  129. package/build/types/manifest/base.js +3 -0
  130. package/build/types/manifest/base.js.map +1 -0
  131. package/build/types/manifest/index.d.ts +19 -0
  132. package/build/types/manifest/index.d.ts.map +1 -0
  133. package/build/types/manifest/index.js +40 -0
  134. package/build/types/manifest/index.js.map +1 -0
  135. package/build/types/manifest/v3.d.ts +139 -0
  136. package/build/types/manifest/v3.d.ts.map +1 -0
  137. package/build/types/manifest/v3.js +3 -0
  138. package/build/types/manifest/v3.js.map +1 -0
  139. package/driver.d.ts +1 -0
  140. package/driver.js +14 -0
  141. package/index.js +11 -0
  142. package/lib/appium.js +535 -186
  143. package/lib/cli/args.js +267 -422
  144. package/lib/cli/driver-command.js +126 -23
  145. package/lib/cli/extension-command.js +679 -271
  146. package/lib/cli/extension.js +48 -17
  147. package/lib/cli/parser.js +263 -83
  148. package/lib/cli/plugin-command.js +115 -20
  149. package/lib/cli/utils.js +24 -10
  150. package/lib/config-file.js +220 -0
  151. package/lib/config.js +244 -110
  152. package/lib/constants.js +71 -0
  153. package/lib/extension/driver-config.js +250 -0
  154. package/lib/extension/extension-config.js +682 -0
  155. package/lib/extension/index.js +116 -0
  156. package/lib/extension/manifest-migrations.js +120 -0
  157. package/lib/extension/manifest.js +573 -0
  158. package/lib/extension/package-changed.js +64 -0
  159. package/lib/extension/plugin-config.js +112 -0
  160. package/lib/grid-register.js +49 -35
  161. package/lib/logger.js +1 -2
  162. package/lib/logsink.js +64 -38
  163. package/lib/main.js +321 -101
  164. package/lib/schema/arg-spec.js +229 -0
  165. package/lib/schema/cli-args.js +238 -0
  166. package/lib/schema/cli-transformers.js +119 -0
  167. package/lib/schema/index.js +2 -0
  168. package/lib/schema/keywords.js +136 -0
  169. package/lib/schema/schema.js +722 -0
  170. package/lib/utils.js +291 -167
  171. package/package.json +83 -84
  172. package/plugin.d.ts +1 -0
  173. package/plugin.js +13 -0
  174. package/scripts/autoinstall-extensions.js +237 -0
  175. package/support.d.ts +1 -0
  176. package/support.js +13 -0
  177. package/tsconfig.json +25 -0
  178. package/types/cli.ts +189 -0
  179. package/types/index.ts +20 -0
  180. package/types/manifest/README.md +30 -0
  181. package/types/manifest/base.ts +158 -0
  182. package/types/manifest/index.ts +27 -0
  183. package/types/manifest/v3.ts +161 -0
  184. package/CHANGELOG.md +0 -3515
  185. package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
  186. package/build/lib/cli/npm.js +0 -208
  187. package/build/lib/cli/parser-helpers.js +0 -82
  188. package/build/lib/driver-config.js +0 -77
  189. package/build/lib/drivers.js +0 -96
  190. package/build/lib/extension-config.js +0 -253
  191. package/build/lib/plugin-config.js +0 -59
  192. package/build/lib/plugins.js +0 -14
  193. package/lib/cli/npm.js +0 -184
  194. package/lib/cli/parser-helpers.js +0 -79
  195. package/lib/driver-config.js +0 -46
  196. package/lib/drivers.js +0 -81
  197. package/lib/extension-config.js +0 -209
  198. package/lib/plugin-config.js +0 -34
  199. package/lib/plugins.js +0 -10
@@ -0,0 +1,573 @@
1
+ /**
2
+ * Module containing {@link Manifest} which handles reading & writing of extension config files.
3
+ */
4
+
5
+ import B from 'bluebird';
6
+ import glob from 'glob';
7
+ import {env, fs} from '@appium/support';
8
+ import _ from 'lodash';
9
+ import path from 'path';
10
+ import YAML from 'yaml';
11
+ import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
12
+ import log from '../logger';
13
+ import {INSTALL_TYPE_NPM} from './extension-config';
14
+ import {packageDidChange} from './package-changed';
15
+ import {migrate} from './manifest-migrations';
16
+
17
+ /**
18
+ * The name of the prop (`drivers`) used in `extensions.yaml` for drivers.
19
+ * @type {`${typeof DRIVER_TYPE}s`}
20
+ */
21
+ const CONFIG_DATA_DRIVER_KEY = `${DRIVER_TYPE}s`;
22
+
23
+ /**
24
+ * The name of the prop (`plugins`) used in `extensions.yaml` for plugins.
25
+ * @type {`${typeof PLUGIN_TYPE}s`}
26
+ */
27
+ const CONFIG_DATA_PLUGIN_KEY = `${PLUGIN_TYPE}s`;
28
+
29
+ /**
30
+ * @type {Readonly<ManifestData>}
31
+ */
32
+ const INITIAL_MANIFEST_DATA = Object.freeze({
33
+ [CONFIG_DATA_DRIVER_KEY]: Object.freeze({}),
34
+ [CONFIG_DATA_PLUGIN_KEY]: Object.freeze({}),
35
+ schemaRev: CURRENT_SCHEMA_REV,
36
+ });
37
+
38
+ /**
39
+ * Given a `package.json` return `true` if it represents an Appium Extension (either a driver or plugin).
40
+ *
41
+ * _This is a type guard; not a validator._
42
+ *
43
+ * The `package.json` must have an `appium` property which is an object.
44
+ * @param {any} value
45
+ * @returns {value is ExtPackageJson<ExtensionType>}
46
+ */
47
+ function isExtension(value) {
48
+ return (
49
+ _.isPlainObject(value) &&
50
+ _.isPlainObject(value.appium) &&
51
+ _.isString(value.name) &&
52
+ _.isString(value.version)
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Given a `package.json`, return `true` if it represents an Appium Driver.
58
+ *
59
+ * _This is a type guard; not a validator._
60
+ *
61
+ * To be considered a driver, a `package.json` must have an `appium.driverName` field.
62
+ *
63
+ * Further validation of the `appium` property happens elsewhere.
64
+ * @param {any} value - Value to test
65
+ * @returns {value is ExtPackageJson<DriverType>}
66
+ */
67
+ function isDriver(value) {
68
+ return isExtension(value) && 'driverName' in value.appium && _.isString(value.appium.driverName);
69
+ }
70
+
71
+ /**
72
+ * Given a `package.json`, return `true` if it represents an Appium Plugin.
73
+ *
74
+ * _This is a type guard; not a validator._
75
+ *
76
+ * To be considered a plugin, a `package.json` must have an `appium.pluginName` field.
77
+ *
78
+ * Further validation of the `appium` property happens elsewhere.
79
+ * @param {any} value - Value to test
80
+ * @returns {value is ExtPackageJson<PluginType>}
81
+ */
82
+ function isPlugin(value) {
83
+ return isExtension(value) && 'pluginName' in value.appium && _.isString(value.appium.pluginName);
84
+ }
85
+
86
+ /**
87
+ * Handles reading & writing of extension config files.
88
+ *
89
+ * Only one instance of this class exists per value of `APPIUM_HOME`.
90
+ */
91
+ export class Manifest {
92
+ /**
93
+ * The entire contents of a parsed YAML extension config file.
94
+ *
95
+ * Contains proxies for automatic persistence on disk
96
+ * @type {ManifestData}
97
+ */
98
+ #data;
99
+
100
+ /**
101
+ * Path to `APPIUM_HOME`.
102
+ * @type {Readonly<string>}
103
+ */
104
+ #appiumHome;
105
+
106
+ /**
107
+ * Path to `extensions.yaml`
108
+ * @type {string}
109
+ * Not set until {@link Manifest.read} is called.
110
+ */
111
+ #manifestPath;
112
+
113
+ /**
114
+ * Helps avoid writing multiple times.
115
+ *
116
+ * If this is `undefined`, calling {@link Manifest.write} will cause it to be
117
+ * set to a `Promise`. When the call to `write()` is complete, the `Promise`
118
+ * will resolve and then this value will be set to `undefined`. Concurrent calls
119
+ * made while this value is a `Promise` will return the `Promise` itself.
120
+ * @type {Promise<boolean>|undefined}
121
+ */
122
+ #writing;
123
+
124
+ /**
125
+ * Helps avoid reading multiple times.
126
+ *
127
+ * If this is `undefined`, calling {@link Manifest.read} will cause it to be
128
+ * set to a `Promise`. When the call to `read()` is complete, the `Promise`
129
+ * will resolve and then this value will be set to `undefined`. Concurrent calls
130
+ * made while this value is a `Promise` will return the `Promise` itself.
131
+ * @type {Promise<void>|undefined}
132
+ */
133
+ #reading;
134
+
135
+ /**
136
+ * Sets internal data to a fresh clone of {@link INITIAL_MANIFEST_DATA}
137
+ *
138
+ * Use {@link Manifest.getInstance} instead.
139
+ * @param {string} appiumHome
140
+ * @private
141
+ */
142
+ constructor(appiumHome) {
143
+ this.#appiumHome = appiumHome;
144
+ this.#data = _.cloneDeep(INITIAL_MANIFEST_DATA);
145
+ }
146
+
147
+ /**
148
+ * Returns a new or existing {@link Manifest} instance, based on the value of `appiumHome`.
149
+ *
150
+ * Maintains one instance per value of `appiumHome`.
151
+ * @param {string} appiumHome - Path to `APPIUM_HOME`
152
+ * @returns {Manifest}
153
+ */
154
+ static getInstance = _.memoize(function _getInstance(appiumHome) {
155
+ return new Manifest(appiumHome);
156
+ });
157
+
158
+ /**
159
+ * Searches `APPIUM_HOME` for installed extensions and adds them to the manifest.
160
+ * @returns {Promise<boolean>} `true` if any extensions were added, `false` otherwise.
161
+ */
162
+ async syncWithInstalledExtensions() {
163
+ // this could be parallelized, but we can't use fs.walk as an async iterator
164
+ let didChange = false;
165
+
166
+ /**
167
+ * Listener for the `match` event of a `glob` instance
168
+ * @param {string} filepath - Path to a `package.json`
169
+ * @returns {Promise<void>}
170
+ */
171
+ const onMatch = async (filepath) => {
172
+ try {
173
+ const pkg = JSON.parse(await fs.readFile(filepath, 'utf8'));
174
+ if (isExtension(pkg)) {
175
+ const extType = isDriver(pkg) ? DRIVER_TYPE : PLUGIN_TYPE;
176
+ /**
177
+ * this should only be 'unknown' if the extension's `package.json` is invalid
178
+ * @type {string}
179
+ */
180
+ const name = isDriver(pkg)
181
+ ? pkg.appium.driverName
182
+ : isPlugin(pkg)
183
+ ? pkg.appium.pluginName
184
+ : '(unknown)';
185
+ if (
186
+ (isDriver(pkg) && !this.hasDriver(name)) ||
187
+ (isPlugin(pkg) && !this.hasPlugin(name))
188
+ ) {
189
+ log.info(`Discovered installed ${extType} "${name}"`);
190
+ }
191
+ const changed = this.addExtensionFromPackage(pkg, filepath);
192
+ didChange = didChange || changed;
193
+ }
194
+ } catch {}
195
+ };
196
+
197
+ /**
198
+ * A list of `Promise`s which read `package.json` files looking for Appium extensions.
199
+ * @type {Promise<void>[]}
200
+ */
201
+ const queue = [
202
+ // look at `package.json` in `APPIUM_HOME` only
203
+ onMatch(path.join(this.#appiumHome, 'package.json')),
204
+ ];
205
+
206
+ // add dependencies to the queue
207
+ await new B((resolve, reject) => {
208
+ glob(
209
+ 'node_modules/{*,@*/*}/package.json',
210
+ {cwd: this.#appiumHome, silent: true, absolute: true},
211
+ // eslint-disable-next-line promise/prefer-await-to-callbacks
212
+ (err) => {
213
+ if (err) {
214
+ reject(err);
215
+ }
216
+ resolve();
217
+ }
218
+ )
219
+ .on('error', reject)
220
+ .on('match', (filepath) => {
221
+ queue.push(onMatch(filepath));
222
+ });
223
+ });
224
+
225
+ // wait for everything to finish
226
+ await B.all(queue);
227
+
228
+ return didChange;
229
+ }
230
+
231
+ /**
232
+ * Returns `true` if driver with name `name` is registered.
233
+ * @param {string} name - Driver name
234
+ * @returns {boolean}
235
+ */
236
+ hasDriver(name) {
237
+ return Boolean(this.#data.drivers[name]);
238
+ }
239
+
240
+ /**
241
+ * Returns `true` if plugin with name `name` is registered.
242
+ * @param {string} name - Plugin name
243
+ * @returns {boolean}
244
+ */
245
+ hasPlugin(name) {
246
+ return Boolean(this.#data.plugins[name]);
247
+ }
248
+
249
+ /**
250
+ * Given a path to a `package.json`, add it as either a driver or plugin to the manifest.
251
+ *
252
+ * @template {ExtensionType} ExtType
253
+ * @param {ExtPackageJson<ExtType>} pkgJson
254
+ * @param {string} pkgPath
255
+ * @returns {boolean} - `true` if this method did anything.
256
+ */
257
+ addExtensionFromPackage(pkgJson, pkgPath) {
258
+ const extensionPath = path.dirname(pkgPath);
259
+
260
+ /**
261
+ * @type {InternalMetadata}
262
+ */
263
+ const internal = {
264
+ pkgName: pkgJson.name,
265
+ version: pkgJson.version,
266
+ appiumVersion: pkgJson.peerDependencies?.appium,
267
+ installType: INSTALL_TYPE_NPM,
268
+ installSpec: `${pkgJson.name}@${pkgJson.version}`,
269
+ installPath: extensionPath,
270
+ };
271
+
272
+ if (isDriver(pkgJson)) {
273
+ const value = {
274
+ ..._.omit(pkgJson.appium, 'driverName'),
275
+ ...internal,
276
+ };
277
+ if (!_.isEqual(value, this.#data.drivers[pkgJson.appium.driverName])) {
278
+ this.setExtension(DRIVER_TYPE, pkgJson.appium.driverName, value);
279
+ return true;
280
+ }
281
+ return false;
282
+ } else if (isPlugin(pkgJson)) {
283
+ const value = {
284
+ ..._.omit(pkgJson.appium, 'pluginName'),
285
+ ...internal,
286
+ };
287
+ if (!_.isEqual(value, this.#data.plugins[pkgJson.appium.pluginName])) {
288
+ this.setExtension(PLUGIN_TYPE, pkgJson.appium.pluginName, value);
289
+ return true;
290
+ }
291
+ return false;
292
+ } else {
293
+ throw new TypeError(
294
+ `The extension in ${extensionPath} is neither a valid ${DRIVER_TYPE} nor a valid ${PLUGIN_TYPE}.`
295
+ );
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Adds an extension to the manifest as was installed by the `appium` CLI. The
301
+ * `extData`, `extType`, and `extName` have already been determined.
302
+ *
303
+ * See {@link Manifest.addExtensionFromPackage} for adding an extension from an on-disk package.
304
+ * @template {ExtensionType} ExtType
305
+ * @param {ExtType} extType - `driver` or `plugin`
306
+ * @param {string} extName - Name of extension
307
+ * @param {ExtManifest<ExtType>} extData - Extension metadata
308
+ * @returns {ExtManifest<ExtType>} A clone of `extData`, potentially with a mutated `appiumVersion` field
309
+ */
310
+ setExtension(extType, extName, extData) {
311
+ const data = _.cloneDeep(extData);
312
+ this.#data[`${extType}s`][extName] = data;
313
+ return data;
314
+ }
315
+
316
+ /**
317
+ * Sets the schema revision
318
+ * @param {keyof import('./manifest-migrations').ManifestDataVersions} rev
319
+ */
320
+ setSchemaRev(rev) {
321
+ this.#data.schemaRev = rev;
322
+ }
323
+
324
+ /**
325
+ * Remove an extension from the manifest.
326
+ * @param {ExtensionType} extType
327
+ * @param {string} extName
328
+ */
329
+ deleteExtension(extType, extName) {
330
+ delete this.#data[`${extType}s`][extName];
331
+ }
332
+
333
+ /**
334
+ * Returns the `APPIUM_HOME` path
335
+ */
336
+ get appiumHome() {
337
+ return this.#appiumHome;
338
+ }
339
+
340
+ /**
341
+ * Returns the path to the manifest file (`extensions.yaml`)
342
+ */
343
+ get manifestPath() {
344
+ return this.#manifestPath;
345
+ }
346
+
347
+ /**
348
+ * Returns the schema rev of this manifest
349
+ */
350
+ get schemaRev() {
351
+ return this.#data.schemaRev;
352
+ }
353
+
354
+ /**
355
+ * Returns extension data for a particular type.
356
+ *
357
+ * @template {ExtensionType} ExtType
358
+ * @param {ExtType} extType
359
+ * @returns {Readonly<ExtRecord<ExtType>>}
360
+ */
361
+ getExtensionData(extType) {
362
+ return this.#data[/** @type {string} */ (`${extType}s`)];
363
+ }
364
+
365
+ /**
366
+ * Reads manifest from disk and _overwrites_ the internal data.
367
+ *
368
+ * If the manifest does not exist on disk, an
369
+ * {@link INITIAL_MANIFEST_DATA "empty"} manifest file will be created, as
370
+ * well as its directory if needed.
371
+ *
372
+ * This will also, if necessary:
373
+ * 1. perform a migration of the manifest data
374
+ * 2. sync the manifest with extensions on-disk (kind of like "auto
375
+ * discovery")
376
+ * 3. write the manifest to disk.
377
+ *
378
+ * Only one read operation can happen at a time.
379
+ *
380
+ * @returns {Promise<ManifestData>} The data
381
+ */
382
+ async read() {
383
+ if (this.#reading) {
384
+ await this.#reading;
385
+ return this.#data;
386
+ }
387
+
388
+ this.#reading = (async () => {
389
+ /** @type {ManifestData} */
390
+ let data;
391
+ /**
392
+ * This will be `true` if, after reading, we need to update the manifest data
393
+ * and write it again to disk.
394
+ */
395
+ let shouldWrite = false;
396
+ await this.#setManifestPath();
397
+ try {
398
+ const yaml = await fs.readFile(this.#manifestPath, 'utf8');
399
+ data = YAML.parse(yaml);
400
+ log.debug(
401
+ `Parsed manifest file at ${this.#manifestPath}: ${JSON.stringify(data, null, 2)}`
402
+ );
403
+ } catch (err) {
404
+ if (err.code === 'ENOENT') {
405
+ log.debug(`No manifest file found at ${this.#manifestPath}; creating`);
406
+ data = _.cloneDeep(INITIAL_MANIFEST_DATA);
407
+ shouldWrite = true;
408
+ } else {
409
+ if (this.#manifestPath) {
410
+ throw new Error(
411
+ `Appium had trouble loading the extension installation ` +
412
+ `cache file (${this.#manifestPath}). It may be invalid YAML. Specific error: ${
413
+ err.message
414
+ }`
415
+ );
416
+ } else {
417
+ throw new Error(
418
+ `Appium encountered an unknown problem. Specific error: ${err.message}`
419
+ );
420
+ }
421
+ }
422
+ }
423
+
424
+ this.#data = data;
425
+
426
+ /**
427
+ * the only way `shouldWrite` is `true` is if we have a new file. a new
428
+ * file will get the latest schema revision, so we can skip the migration.
429
+ */
430
+ if (!shouldWrite && (data.schemaRev ?? 0) < CURRENT_SCHEMA_REV) {
431
+ log.debug(
432
+ `Updating manifest schema from rev ${data.schemaRev ?? '(none)'} to ${CURRENT_SCHEMA_REV}`
433
+ );
434
+ shouldWrite = await migrate(this);
435
+ }
436
+
437
+ /**
438
+ * we still may want to sync with installed extensions even if we have a
439
+ * new file. right now this is limited to the following cases:
440
+ * 1. we have a brand new manifest file
441
+ * 2. we have performed a migration on a manifest file
442
+ * 3. `appium` is a dependency within `package.json`, and `package.json`
443
+ * has changed since last time we checked.
444
+ *
445
+ * It may also make sense to sync with the extensions in an arbitrary
446
+ * `APPIUM_HOME`, but we don't do that here.
447
+ */
448
+ if (
449
+ shouldWrite ||
450
+ ((await env.hasAppiumDependency(this.appiumHome)) &&
451
+ (await packageDidChange(this.appiumHome)))
452
+ ) {
453
+ log.debug('Discovering newly installed extensions...');
454
+ shouldWrite = (await this.syncWithInstalledExtensions()) || shouldWrite;
455
+ }
456
+
457
+ if (shouldWrite) {
458
+ await this.write();
459
+ }
460
+ })();
461
+
462
+ try {
463
+ await this.#reading;
464
+ return this.#data;
465
+ } finally {
466
+ this.#reading = undefined;
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Ensures the internal manifest path is set.
472
+ *
473
+ * Creates the directory if necessary.
474
+ * @returns {Promise<string>}
475
+ */
476
+ async #setManifestPath() {
477
+ if (!this.#manifestPath) {
478
+ this.#manifestPath = await env.resolveManifestPath(this.#appiumHome);
479
+
480
+ /* istanbul ignore if */
481
+ if (path.relative(this.#appiumHome, this.#manifestPath).startsWith('.')) {
482
+ throw new Error(
483
+ `Mismatch between location of APPIUM_HOME and manifest file. APPIUM_HOME: ${
484
+ this.appiumHome
485
+ }, manifest file: ${this.#manifestPath}`
486
+ );
487
+ }
488
+ }
489
+
490
+ return this.#manifestPath;
491
+ }
492
+
493
+ /**
494
+ * Writes the data if it need s writing.
495
+ *
496
+ * If the `schemaRev` prop needs updating, the file will be written.
497
+ *
498
+ * @todo If this becomes too much of a bottleneck, throttle it.
499
+ * @returns {Promise<boolean>} Whether the data was written
500
+ */
501
+ async write() {
502
+ if (this.#writing) {
503
+ return this.#writing;
504
+ }
505
+ this.#writing = (async () => {
506
+ await this.#setManifestPath();
507
+ try {
508
+ await fs.mkdirp(path.dirname(this.#manifestPath));
509
+ } catch (err) {
510
+ throw new Error(
511
+ `Appium could not create the directory for the manifest file: ${path.dirname(
512
+ this.#manifestPath
513
+ )}. Original error: ${err.message}`
514
+ );
515
+ }
516
+ try {
517
+ await fs.writeFile(this.#manifestPath, YAML.stringify(this.#data), 'utf8');
518
+ return true;
519
+ } catch (err) {
520
+ throw new Error(
521
+ `Appium could not write to manifest at ${this.#manifestPath} using APPIUM_HOME ${
522
+ this.#appiumHome
523
+ }. Please ensure it is writable. Original error: ${err.message}`
524
+ );
525
+ }
526
+ })();
527
+ try {
528
+ return await this.#writing;
529
+ } finally {
530
+ this.#writing = undefined;
531
+ }
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Type of the string referring to a driver (typically as a key or type string)
537
+ * @typedef {import('@appium/types').DriverType} DriverType
538
+ */
539
+
540
+ /**
541
+ * Type of the string referring to a plugin (typically as a key or type string)
542
+ * @typedef {import('@appium/types').PluginType} PluginType
543
+ */
544
+
545
+ /**
546
+ * @typedef SyncWithInstalledExtensionsOpts
547
+ * @property {number} [depthLimit] - Maximum depth to recurse into subdirectories
548
+ */
549
+
550
+ /**
551
+ * @typedef {import('appium/types').ManifestData} ManifestData
552
+ * @typedef {import('appium/types').InternalMetadata} InternalMetadata
553
+ */
554
+
555
+ /**
556
+ * @template {ExtensionType} ExtType
557
+ * @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
558
+ */
559
+
560
+ /**
561
+ * @template {ExtensionType} ExtType
562
+ * @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
563
+ */
564
+
565
+ /**
566
+ * @template {ExtensionType} ExtType
567
+ * @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
568
+ */
569
+
570
+ /**
571
+ * Either `driver` or `plugin` rn
572
+ * @typedef {import('@appium/types').ExtensionType} ExtensionType
573
+ */
@@ -0,0 +1,64 @@
1
+ import {fs} from '@appium/support';
2
+ import {isPackageChanged} from 'package-changed';
3
+ import path from 'path';
4
+ import {PKG_HASHFILE_RELATIVE_PATH} from '../constants';
5
+ import log from '../logger';
6
+
7
+ /**
8
+ * Determines if extensions have changed, and updates a hash the `package.json` in `appiumHome` if so.
9
+ *
10
+ * If they have, we need to sync them with the `extensions.yaml` manifest.
11
+ *
12
+ * _Warning: this makes a blocking call to `writeFileSync`._
13
+ * @param {string} appiumHome
14
+ * @returns {Promise<boolean>} `true` if `package.json` `appiumHome` changed
15
+ */
16
+ export async function packageDidChange(appiumHome) {
17
+ const hashFilename = path.join(appiumHome, PKG_HASHFILE_RELATIVE_PATH);
18
+
19
+ // XXX: the types in `package-changed` seem to be wrong.
20
+
21
+ /** @type {boolean} */
22
+ let isChanged;
23
+ /** @type {() => void} */
24
+ let writeHash;
25
+ /** @type {string} */
26
+ let hash;
27
+ /** @type {string|undefined} */
28
+ let oldHash;
29
+
30
+ // first mkdirp the target dir.
31
+ const hashFilenameDir = path.dirname(hashFilename);
32
+ log.debug(`Creating hash file directory: ${hashFilenameDir}`);
33
+ try {
34
+ await fs.mkdirp(hashFilenameDir);
35
+ } catch (err) {
36
+ throw new Error(
37
+ `Appium could not create the directory for hash file: ${hashFilenameDir}. Original error: ${err.message}`
38
+ );
39
+ }
40
+
41
+ try {
42
+ ({isChanged, writeHash, oldHash, hash} = await isPackageChanged({
43
+ cwd: appiumHome,
44
+ hashFilename: PKG_HASHFILE_RELATIVE_PATH,
45
+ }));
46
+ } catch {
47
+ return true;
48
+ }
49
+
50
+ if (isChanged) {
51
+ try {
52
+ writeHash();
53
+ log.debug(
54
+ `Updated hash of ${appiumHome}/package.json from: ${oldHash ?? '(none)'} to: ${hash}`
55
+ );
56
+ } catch (err) {
57
+ throw new Error(
58
+ `Appium could not write hash file: ${hashFilenameDir}. Original error: ${err.message}`
59
+ );
60
+ }
61
+ }
62
+
63
+ return isChanged;
64
+ }