appium 2.0.0-beta.4 → 2.0.0-beta.42

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 (153) hide show
  1. package/README.md +10 -11
  2. package/build/lib/appium.d.ts +204 -0
  3. package/build/lib/appium.d.ts.map +1 -0
  4. package/build/lib/appium.js +256 -131
  5. package/build/lib/cli/args.d.ts +17 -0
  6. package/build/lib/cli/args.d.ts.map +1 -0
  7. package/build/lib/cli/args.js +96 -282
  8. package/build/lib/cli/driver-command.d.ts +37 -0
  9. package/build/lib/cli/driver-command.d.ts.map +1 -0
  10. package/build/lib/cli/driver-command.js +27 -18
  11. package/build/lib/cli/extension-command.d.ts +376 -0
  12. package/build/lib/cli/extension-command.d.ts.map +1 -0
  13. package/build/lib/cli/extension-command.js +287 -156
  14. package/build/lib/cli/extension.d.ts +22 -0
  15. package/build/lib/cli/extension.d.ts.map +1 -0
  16. package/build/lib/cli/extension.js +31 -17
  17. package/build/lib/cli/parser.d.ts +84 -0
  18. package/build/lib/cli/parser.d.ts.map +1 -0
  19. package/build/lib/cli/parser.js +164 -94
  20. package/build/lib/cli/plugin-command.d.ts +34 -0
  21. package/build/lib/cli/plugin-command.d.ts.map +1 -0
  22. package/build/lib/cli/plugin-command.js +26 -19
  23. package/build/lib/cli/utils.d.ts +29 -0
  24. package/build/lib/cli/utils.d.ts.map +1 -0
  25. package/build/lib/cli/utils.js +27 -3
  26. package/build/lib/config-file.d.ts +100 -0
  27. package/build/lib/config-file.d.ts.map +1 -0
  28. package/build/lib/config-file.js +136 -0
  29. package/build/lib/config.d.ts +49 -0
  30. package/build/lib/config.d.ts.map +1 -0
  31. package/build/lib/config.js +119 -96
  32. package/build/lib/constants.d.ts +48 -0
  33. package/build/lib/constants.d.ts.map +1 -0
  34. package/build/lib/constants.js +60 -0
  35. package/build/lib/extension/driver-config.d.ts +81 -0
  36. package/build/lib/extension/driver-config.d.ts.map +1 -0
  37. package/build/lib/extension/driver-config.js +177 -0
  38. package/build/lib/extension/extension-config.d.ts +242 -0
  39. package/build/lib/extension/extension-config.d.ts.map +1 -0
  40. package/build/lib/extension/extension-config.js +436 -0
  41. package/build/lib/extension/index.d.ts +48 -0
  42. package/build/lib/extension/index.d.ts.map +1 -0
  43. package/build/lib/extension/index.js +75 -0
  44. package/build/lib/extension/manifest.d.ts +174 -0
  45. package/build/lib/extension/manifest.d.ts.map +1 -0
  46. package/build/lib/extension/manifest.js +256 -0
  47. package/build/lib/extension/package-changed.d.ts +11 -0
  48. package/build/lib/extension/package-changed.d.ts.map +1 -0
  49. package/build/lib/extension/package-changed.js +66 -0
  50. package/build/lib/extension/plugin-config.d.ts +57 -0
  51. package/build/lib/extension/plugin-config.d.ts.map +1 -0
  52. package/build/lib/extension/plugin-config.js +78 -0
  53. package/build/lib/grid-register.d.ts +10 -0
  54. package/build/lib/grid-register.d.ts.map +1 -0
  55. package/build/lib/grid-register.js +21 -25
  56. package/build/lib/logger.d.ts +3 -0
  57. package/build/lib/logger.d.ts.map +1 -0
  58. package/build/lib/logger.js +4 -6
  59. package/build/lib/logsink.d.ts +4 -0
  60. package/build/lib/logsink.d.ts.map +1 -0
  61. package/build/lib/logsink.js +14 -17
  62. package/build/lib/main.d.ts +55 -0
  63. package/build/lib/main.d.ts.map +1 -0
  64. package/build/lib/main.js +183 -91
  65. package/build/lib/schema/arg-spec.d.ts +143 -0
  66. package/build/lib/schema/arg-spec.d.ts.map +1 -0
  67. package/build/lib/schema/arg-spec.js +119 -0
  68. package/build/lib/schema/cli-args.d.ts +19 -0
  69. package/build/lib/schema/cli-args.d.ts.map +1 -0
  70. package/build/lib/schema/cli-args.js +178 -0
  71. package/build/lib/schema/cli-transformers.d.ts +5 -0
  72. package/build/lib/schema/cli-transformers.d.ts.map +1 -0
  73. package/build/lib/schema/cli-transformers.js +74 -0
  74. package/build/lib/schema/index.d.ts +3 -0
  75. package/build/lib/schema/index.d.ts.map +1 -0
  76. package/build/lib/schema/index.js +34 -0
  77. package/build/lib/schema/keywords.d.ts +24 -0
  78. package/build/lib/schema/keywords.d.ts.map +1 -0
  79. package/build/lib/schema/keywords.js +70 -0
  80. package/build/lib/schema/schema.d.ts +259 -0
  81. package/build/lib/schema/schema.d.ts.map +1 -0
  82. package/build/lib/schema/schema.js +450 -0
  83. package/build/lib/utils.d.ts +66 -0
  84. package/build/lib/utils.d.ts.map +1 -0
  85. package/build/lib/utils.js +35 -139
  86. package/build/tsconfig.tsbuildinfo +1 -0
  87. package/build/types/appium-manifest.d.ts +59 -0
  88. package/build/types/appium-manifest.d.ts.map +1 -0
  89. package/build/types/cli.d.ts +123 -0
  90. package/build/types/cli.d.ts.map +1 -0
  91. package/build/types/extension-manifest.d.ts +55 -0
  92. package/build/types/extension-manifest.d.ts.map +1 -0
  93. package/build/types/index.d.ts +16 -0
  94. package/build/types/index.d.ts.map +1 -0
  95. package/driver.d.ts +1 -0
  96. package/driver.js +14 -0
  97. package/index.js +11 -0
  98. package/lib/appium.js +520 -186
  99. package/lib/cli/args.js +267 -422
  100. package/lib/cli/driver-command.js +58 -23
  101. package/lib/cli/extension-command.js +613 -260
  102. package/lib/cli/extension.js +47 -17
  103. package/lib/cli/parser.js +263 -83
  104. package/lib/cli/plugin-command.js +48 -20
  105. package/lib/cli/utils.js +24 -10
  106. package/lib/config-file.js +219 -0
  107. package/lib/config.js +243 -110
  108. package/lib/constants.js +69 -0
  109. package/lib/extension/driver-config.js +249 -0
  110. package/lib/extension/extension-config.js +677 -0
  111. package/lib/extension/index.js +116 -0
  112. package/lib/extension/manifest.js +475 -0
  113. package/lib/extension/package-changed.js +64 -0
  114. package/lib/extension/plugin-config.js +113 -0
  115. package/lib/grid-register.js +49 -35
  116. package/lib/logger.js +1 -2
  117. package/lib/logsink.js +38 -33
  118. package/lib/main.js +308 -100
  119. package/lib/schema/arg-spec.js +229 -0
  120. package/lib/schema/cli-args.js +238 -0
  121. package/lib/schema/cli-transformers.js +115 -0
  122. package/lib/schema/index.js +2 -0
  123. package/lib/schema/keywords.js +136 -0
  124. package/lib/schema/schema.js +717 -0
  125. package/lib/utils.js +121 -140
  126. package/package.json +85 -85
  127. package/plugin.d.ts +1 -0
  128. package/plugin.js +13 -0
  129. package/scripts/autoinstall-extensions.js +185 -0
  130. package/support.d.ts +1 -0
  131. package/support.js +13 -0
  132. package/test.d.ts +7 -0
  133. package/test.js +13 -0
  134. package/types/appium-manifest.ts +73 -0
  135. package/types/cli.ts +150 -0
  136. package/types/extension-manifest.ts +64 -0
  137. package/types/index.ts +21 -0
  138. package/CHANGELOG.md +0 -3515
  139. package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
  140. package/build/lib/cli/npm.js +0 -206
  141. package/build/lib/cli/parser-helpers.js +0 -82
  142. package/build/lib/driver-config.js +0 -77
  143. package/build/lib/drivers.js +0 -96
  144. package/build/lib/extension-config.js +0 -253
  145. package/build/lib/plugin-config.js +0 -59
  146. package/build/lib/plugins.js +0 -14
  147. package/lib/cli/npm.js +0 -183
  148. package/lib/cli/parser-helpers.js +0 -79
  149. package/lib/driver-config.js +0 -46
  150. package/lib/drivers.js +0 -81
  151. package/lib/extension-config.js +0 -209
  152. package/lib/plugin-config.js +0 -34
  153. package/lib/plugins.js +0 -10
@@ -0,0 +1,116 @@
1
+ import _ from 'lodash';
2
+ import {USE_ALL_PLUGINS} from '../constants';
3
+ import log from '../logger';
4
+ import {DriverConfig} from './driver-config';
5
+ import {Manifest} from './manifest';
6
+ import {PluginConfig} from './plugin-config';
7
+ import B from 'bluebird';
8
+
9
+ /**
10
+ * Loads extensions and creates `ExtensionConfig` instances.
11
+ *
12
+ * - Reads the manifest file, creating if necessary
13
+ * - Using the parsed extension data, creates/gets the `ExtensionConfig` subclass instances
14
+ * - Returns these instances
15
+ *
16
+ * If `appiumHome` is needed, use `resolveAppiumHome` from the `env` module in `@appium/support`.
17
+ * @param {string} appiumHome
18
+ * @returns {Promise<ExtensionConfigs>}
19
+ */
20
+ export async function loadExtensions(appiumHome) {
21
+ const manifest = Manifest.getInstance(appiumHome);
22
+ await manifest.read();
23
+ const driverConfig = DriverConfig.getInstance(manifest) ?? DriverConfig.create(manifest);
24
+ const pluginConfig = PluginConfig.getInstance(manifest) ?? PluginConfig.create(manifest);
25
+
26
+ await B.all([driverConfig.validate(), pluginConfig.validate()]);
27
+ return {driverConfig, pluginConfig};
28
+ }
29
+
30
+ /**
31
+ * Find any plugin name which has been installed, and which has been requested for activation by
32
+ * using the --use-plugins flag, and turn each one into its class, so we can send them as objects
33
+ * to the server init. We also want to send/assign them to the umbrella driver so it can use them
34
+ * to wrap command execution
35
+ *
36
+ * @param {import('./plugin-config').PluginConfig} pluginConfig - a plugin extension config
37
+ * @param {string[]} usePlugins
38
+ * @returns {PluginNameMap} Mapping of PluginClass to name
39
+ */
40
+ export function getActivePlugins(pluginConfig, usePlugins = []) {
41
+ return new Map(
42
+ _.compact(
43
+ Object.keys(pluginConfig.installedExtensions)
44
+ .filter(
45
+ (pluginName) =>
46
+ _.includes(usePlugins, pluginName) ||
47
+ (usePlugins.length === 1 && usePlugins[0] === USE_ALL_PLUGINS)
48
+ )
49
+ .map((pluginName) => {
50
+ try {
51
+ log.info(`Attempting to load plugin ${pluginName}...`);
52
+ const PluginClass = pluginConfig.require(pluginName);
53
+ return [PluginClass, pluginName];
54
+ } catch (err) {
55
+ log.error(
56
+ `Could not load plugin '${pluginName}', so it will not be available. Error ` +
57
+ `in loading the plugin was: ${err.message}`
58
+ );
59
+ log.debug(err.stack);
60
+ }
61
+ })
62
+ )
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Find any driver name which has been installed, and turn each one into its class, so we can send
68
+ * them as objects to the server init in case they need to add methods/routes or update the server.
69
+ * If the --drivers flag was given, this method only loads the given drivers.
70
+ *
71
+ * @param {import('./driver-config').DriverConfig} driverConfig - a driver extension config
72
+ * @param {string[]} [useDrivers] - optional list of drivers to load
73
+ * @returns {DriverNameMap}
74
+ */
75
+ export function getActiveDrivers(driverConfig, useDrivers = []) {
76
+ return new Map(
77
+ _.compact(
78
+ Object.keys(driverConfig.installedExtensions)
79
+ .filter((driverName) => _.includes(useDrivers, driverName) || useDrivers.length === 0)
80
+ .map((driverName) => {
81
+ try {
82
+ log.info(`Attempting to load driver ${driverName}...`);
83
+ const DriverClass = driverConfig.require(driverName);
84
+ return [DriverClass, driverName];
85
+ } catch (err) {
86
+ log.error(
87
+ `Could not load driver '${driverName}', so it will not be available. Error ` +
88
+ `in loading the driver was: ${err.message}`
89
+ );
90
+ log.debug(err.stack);
91
+ }
92
+ })
93
+ )
94
+ );
95
+ }
96
+
97
+ /**
98
+ * A mapping of {@linkcode PluginClass} classes to their names.
99
+ * @typedef {Map<PluginClass,string>} PluginNameMap
100
+ */
101
+
102
+ /**
103
+ * A mapping of {@linkcode DriverClass} classes to their names.
104
+ * @typedef {Map<DriverClass,string>} DriverNameMap
105
+ */
106
+
107
+ /**
108
+ * @typedef {import('@appium/types').PluginClass} PluginClass
109
+ * @typedef {import('@appium/types').DriverClass} DriverClass
110
+ */
111
+
112
+ /**
113
+ * @typedef ExtensionConfigs
114
+ * @property {import('./driver-config').DriverConfig} driverConfig
115
+ * @property {import('./plugin-config').PluginConfig} pluginConfig
116
+ */
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Module containing {@link Manifest} which handles reading & writing of extension config files.
3
+ */
4
+
5
+ import {env, fs} from '@appium/support';
6
+ import _ from 'lodash';
7
+ import path from 'path';
8
+ import YAML from 'yaml';
9
+ import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
10
+ import log from '../logger';
11
+ import {INSTALL_TYPE_NPM} from './extension-config';
12
+ import {packageDidChange} from './package-changed';
13
+
14
+ /**
15
+ * Default depth to search in directory tree for whatever it is we're looking for.
16
+ *
17
+ * It's 4 because smaller numbers didn't work.
18
+ */
19
+ const DEFAULT_SEARCH_DEPTH = 4;
20
+
21
+ /**
22
+ * Default options for {@link findExtensions}.
23
+ * @type {Readonly<import('klaw').Options>}
24
+ */
25
+ const DEFAULT_FIND_EXTENSIONS_OPTS = Object.freeze({
26
+ depthLimit: DEFAULT_SEARCH_DEPTH,
27
+ /* istanbul ignore next */
28
+ filter: (filepath) => !path.basename(filepath).startsWith('.'),
29
+ });
30
+
31
+ /**
32
+ * Current configuration schema revision!
33
+ */
34
+ const CONFIG_SCHEMA_REV = 2;
35
+
36
+ /**
37
+ * The name of the prop (`drivers`) used in `extensions.yaml` for drivers.
38
+ * @type {`${typeof DRIVER_TYPE}s`}
39
+ */
40
+ const CONFIG_DATA_DRIVER_KEY = `${DRIVER_TYPE}s`;
41
+
42
+ /**
43
+ * The name of the prop (`plugins`) used in `extensions.yaml` for plugins.
44
+ * @type {`${typeof PLUGIN_TYPE}s`}
45
+ */
46
+ const CONFIG_DATA_PLUGIN_KEY = `${PLUGIN_TYPE}s`;
47
+
48
+ /**
49
+ * @type {Readonly<ManifestData>}
50
+ */
51
+ const INITIAL_MANIFEST_DATA = Object.freeze({
52
+ [CONFIG_DATA_DRIVER_KEY]: Object.freeze({}),
53
+ [CONFIG_DATA_PLUGIN_KEY]: Object.freeze({}),
54
+ schemaRev: CONFIG_SCHEMA_REV,
55
+ });
56
+
57
+ /**
58
+ * Given a `package.json` return `true` if it represents an Appium Extension (either a driver or plugin).
59
+ *
60
+ * The `package.json` must have an `appium` property which is an object.
61
+ * @param {any} value
62
+ * @returns {value is ExtPackageJson<ExtensionType>}
63
+ */
64
+ function isExtension(value) {
65
+ return (
66
+ _.isPlainObject(value) &&
67
+ _.isPlainObject(value.appium) &&
68
+ _.isString(value.name) &&
69
+ _.isString(value.version)
70
+ );
71
+ }
72
+ /**
73
+ * Given a `package.json`, return `true` if it represents an Appium Driver.
74
+ *
75
+ * To be considered a driver, a `package.json` must have a fields
76
+ * `appium.driverName`, `appium.automationName` and `appium.platformNames`.
77
+ * @param {any} value - Value to test
78
+ * @returns {value is ExtPackageJson<DriverType>}
79
+ */
80
+ function isDriver(value) {
81
+ return (
82
+ isExtension(value) &&
83
+ _.isString(_.get(value, 'appium.driverName')) &&
84
+ _.isString(_.get(value, 'appium.automationName')) &&
85
+ _.isArray(_.get(value, 'appium.platformNames'))
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Given a `package.json`, return `true` if it represents an Appium Plugin.
91
+ *
92
+ * To be considered a plugin, a `package.json` must have an `appium.pluginName` field.
93
+ * @param {any} value - Value to test
94
+ * @returns {value is ExtPackageJson<PluginType>}
95
+ */
96
+ function isPlugin(value) {
97
+ return isExtension(value) && _.isString(_.get(value, 'appium.pluginName'));
98
+ }
99
+
100
+ /**
101
+ * Handles reading & writing of extension config files.
102
+ *
103
+ * Only one instance of this class exists per value of `APPIUM_HOME`.
104
+ */
105
+ export class Manifest {
106
+ /**
107
+ * The entire contents of a parsed YAML extension config file.
108
+ *
109
+ * Contains proxies for automatic persistence on disk
110
+ * @type {ManifestData}
111
+ * @private
112
+ */
113
+ _data;
114
+
115
+ /**
116
+ * Path to `APPIUM_HOME`.
117
+ * @private
118
+ * @type {Readonly<string>}
119
+ */
120
+ _appiumHome;
121
+
122
+ /**
123
+ * Path to `extensions.yaml`
124
+ * @type {string}
125
+ * Not set until {@link Manifest.read} is called.
126
+ */
127
+ _manifestPath;
128
+
129
+ /**
130
+ * Helps avoid writing multiple times.
131
+ *
132
+ * If this is `undefined`, calling {@link Manifest.write} will cause it to be
133
+ * set to a `Promise`. When the call to `write()` is complete, the `Promise`
134
+ * will resolve and then this value will be set to `undefined`. Concurrent calls
135
+ * made while this value is a `Promise` will return the `Promise` itself.
136
+ * @private
137
+ * @type {Promise<boolean>|undefined}
138
+ */
139
+ _writing;
140
+
141
+ /**
142
+ * Helps avoid reading multiple times.
143
+ *
144
+ * If this is `undefined`, calling {@link Manifest.read} will cause it to be
145
+ * set to a `Promise`. When the call to `read()` is complete, the `Promise`
146
+ * will resolve and then this value will be set to `undefined`. Concurrent calls
147
+ * made while this value is a `Promise` will return the `Promise` itself.
148
+ * @private
149
+ * @type {Promise<void>|undefined}
150
+ */
151
+ _reading;
152
+
153
+ /**
154
+ * Sets internal data to a fresh clone of {@link INITIAL_MANIFEST_DATA}
155
+ *
156
+ * Use {@link Manifest.getInstance} instead.
157
+ * @param {string} appiumHome
158
+ * @private
159
+ */
160
+ constructor(appiumHome) {
161
+ this._appiumHome = appiumHome;
162
+ this._data = _.cloneDeep(INITIAL_MANIFEST_DATA);
163
+ }
164
+
165
+ /**
166
+ * Returns a new or existing {@link Manifest} instance, based on the value of `appiumHome`.
167
+ *
168
+ * Maintains one instance per value of `appiumHome`.
169
+ * @param {string} appiumHome - Path to `APPIUM_HOME`
170
+ * @returns {Manifest}
171
+ */
172
+ static getInstance = _.memoize(function _getInstance(appiumHome) {
173
+ return new Manifest(appiumHome);
174
+ });
175
+
176
+ /**
177
+ * Searches `APPIUM_HOME` for installed extensions and adds them to the manifest.
178
+ * @param {SyncWithInstalledExtensionsOpts} opts
179
+ * @returns {Promise<boolean>} `true` if any extensions were added, `false` otherwise.
180
+ */
181
+ async syncWithInstalledExtensions({depthLimit = DEFAULT_SEARCH_DEPTH} = {}) {
182
+ const walkOpts = _.defaults({depthLimit}, DEFAULT_FIND_EXTENSIONS_OPTS);
183
+ // this could be parallelized, but we can't use fs.walk as an async iterator
184
+ let didChange = false;
185
+ for await (const {stats, path: filepath} of fs.walk(this._appiumHome, walkOpts)) {
186
+ if (filepath !== this._appiumHome && stats.isDirectory()) {
187
+ try {
188
+ const pkg = await env.readPackageInDir(filepath);
189
+ if (pkg && isExtension(pkg)) {
190
+ // it's possible that this extension already exists in the manifest,
191
+ // so only update `didChange` if it's new.
192
+ const added = this.addExtensionFromPackage(pkg, path.join(filepath, 'package.json'));
193
+ didChange = didChange || added;
194
+ }
195
+ } catch {}
196
+ }
197
+ }
198
+ return didChange;
199
+ }
200
+
201
+ /**
202
+ * Returns `true` if driver with name `name` is registered.
203
+ * @param {string} name - Driver name
204
+ * @returns {boolean}
205
+ */
206
+ hasDriver(name) {
207
+ return Boolean(this._data.drivers[name]);
208
+ }
209
+
210
+ /**
211
+ * Returns `true` if plugin with name `name` is registered.
212
+ * @param {string} name - Plugin name
213
+ * @returns {boolean}
214
+ */
215
+ hasPlugin(name) {
216
+ return Boolean(this._data.plugins[name]);
217
+ }
218
+
219
+ /**
220
+ * Given a path to a `package.json`, add it as either a driver or plugin to the manifest.
221
+ *
222
+ * Will _not_ overwrite existing entries.
223
+ * @template {ExtensionType} ExtType
224
+ * @param {ExtPackageJson<ExtType>} pkgJson
225
+ * @param {string} pkgPath
226
+ * @returns {boolean} - `true` upon success, `false` if the extension is already registered.
227
+ */
228
+ addExtensionFromPackage(pkgJson, pkgPath) {
229
+ const extensionPath = path.dirname(pkgPath);
230
+
231
+ /**
232
+ * @type {InternalMetadata}
233
+ */
234
+ const internal = {
235
+ pkgName: pkgJson.name,
236
+ version: pkgJson.version,
237
+ appiumVersion: pkgJson.peerDependencies?.appium,
238
+ installType: INSTALL_TYPE_NPM,
239
+ installSpec: `${pkgJson.name}@${pkgJson.version}`,
240
+ };
241
+
242
+ if (isDriver(pkgJson)) {
243
+ if (!this.hasDriver(pkgJson.appium.driverName)) {
244
+ this.addExtension(DRIVER_TYPE, pkgJson.appium.driverName, {
245
+ ..._.omit(pkgJson.appium, 'driverName'),
246
+ ...internal,
247
+ });
248
+ return true;
249
+ }
250
+ return false;
251
+ } else if (isPlugin(pkgJson)) {
252
+ if (!this.hasPlugin(pkgJson.appium.pluginName)) {
253
+ this.addExtension(PLUGIN_TYPE, pkgJson.appium.pluginName, {
254
+ ..._.omit(pkgJson.appium, 'pluginName'),
255
+ ...internal,
256
+ });
257
+ return true;
258
+ }
259
+ return false;
260
+ } else {
261
+ throw new TypeError(
262
+ `The extension in ${extensionPath} is neither a valid driver nor a valid plugin.`
263
+ );
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Adds an extension to the manifest as was installed by the `appium` CLI. The
269
+ * `extData`, `extType`, and `extName` have already been determined.
270
+ *
271
+ * See {@link Manifest.addExtensionFromPackage} for adding an extension from an on-disk package.
272
+ * @template {ExtensionType} ExtType
273
+ * @param {ExtType} extType - `driver` or `plugin`
274
+ * @param {string} extName - Name of extension
275
+ * @param {ExtManifest<ExtType>} extData - Extension metadata
276
+ * @returns {ExtManifest<ExtType>} A clone of `extData`, potentially with a mutated `appiumVersion` field
277
+ */
278
+ addExtension(extType, extName, extData) {
279
+ const data = _.clone(extData);
280
+ this._data[`${extType}s`][extName] = data;
281
+ return data;
282
+ }
283
+
284
+ /**
285
+ * Returns the APPIUM_HOME path
286
+ */
287
+ get appiumHome() {
288
+ return this._appiumHome;
289
+ }
290
+
291
+ /**
292
+ * Returns the path to the manifest file
293
+ */
294
+ get manifestPath() {
295
+ return this._manifestPath;
296
+ }
297
+
298
+ /**
299
+ * Returns extension data for a particular type.
300
+ *
301
+ * @template {ExtensionType} ExtType
302
+ * @param {ExtType} extType
303
+ * @returns {ExtRecord<ExtType>}
304
+ */
305
+ getExtensionData(extType) {
306
+ return this._data[/** @type {string} */ (`${extType}s`)];
307
+ }
308
+
309
+ /**
310
+ * Reads manifest from disk and _overwrites_ the internal data.
311
+ *
312
+ * If the manifest does not exist on disk, an {@link INITIAL_MANIFEST_DATA "empty"} manifest file will be created.
313
+ *
314
+ * If `APPIUM_HOME` contains a `package.json` with an `appium` dependency, then a hash of the `package.json` will be taken. If this hash differs from the last hash, the contents of `APPIUM_HOME/node_modules` will be scanned for extensions that may have been installed outside of the `appium` CLI. Any found extensions will be added to the manifest file, and if so, the manifest file will be written to disk.
315
+ *
316
+ * Only one read operation should happen at a time. This is controlled via the {@link Manifest._reading} property.
317
+ * @returns {Promise<ManifestData>} The data
318
+ */
319
+ async read() {
320
+ if (this._reading) {
321
+ await this._reading;
322
+ return this._data;
323
+ }
324
+
325
+ this._reading = (async () => {
326
+ /** @type {ManifestData} */
327
+ let data;
328
+ let isNewFile = false;
329
+ await this._setManifestPath();
330
+ try {
331
+ log.debug(`Reading ${this._manifestPath}...`);
332
+ const yaml = await fs.readFile(this._manifestPath, 'utf8');
333
+ data = YAML.parse(yaml);
334
+ log.debug(`Parsed manifest file: ${JSON.stringify(data, null, 2)}`);
335
+ } catch (err) {
336
+ if (err.code === 'ENOENT') {
337
+ data = _.cloneDeep(INITIAL_MANIFEST_DATA);
338
+ isNewFile = true;
339
+ } else {
340
+ if (this._manifestPath) {
341
+ throw new Error(
342
+ `Appium had trouble loading the extension installation ` +
343
+ `cache file (${this._manifestPath}). It may be invalid YAML. Specific error: ${err.message}`
344
+ );
345
+ } else {
346
+ throw new Error(
347
+ `Appium encountered an unknown problem. Specific error: ${err.message}`
348
+ );
349
+ }
350
+ }
351
+ }
352
+
353
+ this._data = data;
354
+ let installedExtensionsChanged = false;
355
+ if (
356
+ (await env.hasAppiumDependency(this.appiumHome)) &&
357
+ (await packageDidChange(this.appiumHome))
358
+ ) {
359
+ installedExtensionsChanged = await this.syncWithInstalledExtensions();
360
+ }
361
+
362
+ if (isNewFile || installedExtensionsChanged) {
363
+ await this.write();
364
+ }
365
+ })();
366
+ try {
367
+ await this._reading;
368
+ return this._data;
369
+ } finally {
370
+ this._reading = undefined;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Ensures {@link Manifest._manifestPath} is set.
376
+ *
377
+ * Creates the directory if necessary.
378
+ * @private
379
+ * @returns {Promise<string>}
380
+ */
381
+ async _setManifestPath() {
382
+ if (!this._manifestPath) {
383
+ this._manifestPath = await env.resolveManifestPath(this._appiumHome);
384
+
385
+ /* istanbul ignore if */
386
+ if (path.relative(this._appiumHome, this._manifestPath).startsWith('.')) {
387
+ throw new Error(
388
+ `Mismatch between location of APPIUM_HOME and manifest file. APPIUM_HOME: ${this.appiumHome}, manifest file: ${this._manifestPath}`
389
+ );
390
+ }
391
+ }
392
+
393
+ return this._manifestPath;
394
+ }
395
+
396
+ /**
397
+ * Writes the data if it need s writing.
398
+ *
399
+ * If the `schemaRev` prop needs updating, the file will be written.
400
+ *
401
+ * @todo If this becomes too much of a bottleneck, throttle it.
402
+ * @returns {Promise<boolean>} Whether the data was written
403
+ */
404
+ async write() {
405
+ if (this._writing) {
406
+ return this._writing;
407
+ }
408
+ this._writing = (async () => {
409
+ await this._setManifestPath();
410
+ try {
411
+ await fs.mkdirp(path.dirname(this._manifestPath));
412
+ } catch (err) {
413
+ throw new Error(
414
+ `Appium could not create the directory for the manifest file: ${path.dirname(
415
+ this._manifestPath
416
+ )}. Original error: ${err.message}`
417
+ );
418
+ }
419
+ try {
420
+ await fs.writeFile(this._manifestPath, YAML.stringify(this._data), 'utf8');
421
+ return true;
422
+ } catch (err) {
423
+ throw new Error(
424
+ `Appium could not write to manifest at ${this._manifestPath} using APPIUM_HOME ${this._appiumHome}. ` +
425
+ `Please ensure it is writable. Original error: ${err.message}`
426
+ );
427
+ }
428
+ })();
429
+ try {
430
+ return await this._writing;
431
+ } finally {
432
+ this._writing = undefined;
433
+ }
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Type of the string referring to a driver (typically as a key or type string)
439
+ * @typedef {import('@appium/types').DriverType} DriverType
440
+ */
441
+
442
+ /**
443
+ * Type of the string referring to a plugin (typically as a key or type string)
444
+ * @typedef {import('@appium/types').PluginType} PluginType
445
+ */
446
+
447
+ /**
448
+ * @typedef SyncWithInstalledExtensionsOpts
449
+ * @property {number} [depthLimit] - Maximum depth to recurse into subdirectories
450
+ */
451
+
452
+ /**
453
+ * @typedef {import('appium/types').ManifestData} ManifestData
454
+ * @typedef {import('appium/types').InternalMetadata} InternalMetadata
455
+ */
456
+
457
+ /**
458
+ * @template T
459
+ * @typedef {import('appium/types').ExtPackageJson<T>} ExtPackageJson
460
+ */
461
+
462
+ /**
463
+ * @template T
464
+ * @typedef {import('appium/types').ExtManifest<T>} ExtManifest
465
+ */
466
+
467
+ /**
468
+ * @template T
469
+ * @typedef {import('appium/types').ExtRecord<T>} ExtRecord
470
+ */
471
+
472
+ /**
473
+ * Either `driver` or `plugin` rn
474
+ * @typedef {import('@appium/types').ExtensionType} ExtensionType
475
+ */
@@ -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
+ }