appium 3.2.2 → 3.3.1

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 (250) hide show
  1. package/build/lib/appium.d.ts +147 -205
  2. package/build/lib/appium.d.ts.map +1 -1
  3. package/build/lib/appium.js +169 -282
  4. package/build/lib/appium.js.map +1 -1
  5. package/build/lib/bidi-commands.d.ts.map +1 -1
  6. package/build/lib/bidi-commands.js +11 -11
  7. package/build/lib/bidi-commands.js.map +1 -1
  8. package/build/lib/bootstrap/appium-initializer.d.ts +21 -0
  9. package/build/lib/bootstrap/appium-initializer.d.ts.map +1 -0
  10. package/build/lib/bootstrap/appium-initializer.js +146 -0
  11. package/build/lib/bootstrap/appium-initializer.js.map +1 -0
  12. package/build/lib/bootstrap/appium-main-runner.d.ts +22 -0
  13. package/build/lib/bootstrap/appium-main-runner.d.ts.map +1 -0
  14. package/build/lib/bootstrap/appium-main-runner.js +109 -0
  15. package/build/lib/bootstrap/appium-main-runner.js.map +1 -0
  16. package/build/lib/bootstrap/config-file.d.ts +37 -0
  17. package/build/lib/bootstrap/config-file.d.ts.map +1 -0
  18. package/build/lib/{config-file.js → bootstrap/config-file.js} +62 -138
  19. package/build/lib/bootstrap/config-file.js.map +1 -0
  20. package/build/lib/bootstrap/grid-v3-register.d.ts +20 -0
  21. package/build/lib/bootstrap/grid-v3-register.d.ts.map +1 -0
  22. package/build/lib/bootstrap/grid-v3-register.js +185 -0
  23. package/build/lib/bootstrap/grid-v3-register.js.map +1 -0
  24. package/build/lib/bootstrap/init-types.d.ts +16 -0
  25. package/build/lib/bootstrap/init-types.d.ts.map +1 -0
  26. package/build/lib/bootstrap/init-types.js +3 -0
  27. package/build/lib/bootstrap/init-types.js.map +1 -0
  28. package/build/lib/bootstrap/main-helpers.d.ts +55 -0
  29. package/build/lib/bootstrap/main-helpers.d.ts.map +1 -0
  30. package/build/lib/bootstrap/main-helpers.js +187 -0
  31. package/build/lib/bootstrap/main-helpers.js.map +1 -0
  32. package/build/lib/bootstrap/node-helpers.d.ts +32 -0
  33. package/build/lib/bootstrap/node-helpers.d.ts.map +1 -0
  34. package/build/lib/bootstrap/node-helpers.js +201 -0
  35. package/build/lib/bootstrap/node-helpers.js.map +1 -0
  36. package/build/lib/bootstrap/startup-config.d.ts +22 -0
  37. package/build/lib/bootstrap/startup-config.d.ts.map +1 -0
  38. package/build/lib/bootstrap/startup-config.js +111 -0
  39. package/build/lib/bootstrap/startup-config.js.map +1 -0
  40. package/build/lib/cli/args.d.ts +16 -12
  41. package/build/lib/cli/args.d.ts.map +1 -1
  42. package/build/lib/cli/args.js +20 -40
  43. package/build/lib/cli/args.js.map +1 -1
  44. package/build/lib/cli/driver-command.d.ts +51 -93
  45. package/build/lib/cli/driver-command.d.ts.map +1 -1
  46. package/build/lib/cli/driver-command.js +11 -66
  47. package/build/lib/cli/driver-command.js.map +1 -1
  48. package/build/lib/cli/extension-command.d.ts +173 -377
  49. package/build/lib/cli/extension-command.d.ts.map +1 -1
  50. package/build/lib/cli/extension-command.js +387 -656
  51. package/build/lib/cli/extension-command.js.map +1 -1
  52. package/build/lib/cli/extension.d.ts +10 -15
  53. package/build/lib/cli/extension.d.ts.map +1 -1
  54. package/build/lib/cli/extension.js +15 -33
  55. package/build/lib/cli/extension.js.map +1 -1
  56. package/build/lib/cli/parser.d.ts +37 -66
  57. package/build/lib/cli/parser.d.ts.map +1 -1
  58. package/build/lib/cli/parser.js +69 -104
  59. package/build/lib/cli/parser.js.map +1 -1
  60. package/build/lib/cli/plugin-command.d.ts +50 -90
  61. package/build/lib/cli/plugin-command.d.ts.map +1 -1
  62. package/build/lib/cli/plugin-command.js +11 -63
  63. package/build/lib/cli/plugin-command.js.map +1 -1
  64. package/build/lib/cli/setup-command.d.ts +21 -26
  65. package/build/lib/cli/setup-command.d.ts.map +1 -1
  66. package/build/lib/cli/setup-command.js +19 -61
  67. package/build/lib/cli/setup-command.js.map +1 -1
  68. package/build/lib/cli/utils.d.ts +33 -35
  69. package/build/lib/cli/utils.d.ts.map +1 -1
  70. package/build/lib/cli/utils.js +48 -50
  71. package/build/lib/cli/utils.js.map +1 -1
  72. package/build/lib/constants.d.ts +23 -23
  73. package/build/lib/constants.d.ts.map +1 -1
  74. package/build/lib/constants.js +10 -15
  75. package/build/lib/constants.js.map +1 -1
  76. package/build/lib/doctor/doctor.d.ts +40 -57
  77. package/build/lib/doctor/doctor.d.ts.map +1 -1
  78. package/build/lib/doctor/doctor.js +31 -62
  79. package/build/lib/doctor/doctor.js.map +1 -1
  80. package/build/lib/extension/driver-config.d.ts +18 -77
  81. package/build/lib/extension/driver-config.d.ts.map +1 -1
  82. package/build/lib/extension/driver-config.js +37 -125
  83. package/build/lib/extension/driver-config.js.map +1 -1
  84. package/build/lib/extension/extension-config.d.ts +103 -210
  85. package/build/lib/extension/extension-config.d.ts.map +1 -1
  86. package/build/lib/extension/extension-config.js +180 -342
  87. package/build/lib/extension/extension-config.js.map +1 -1
  88. package/build/lib/extension/index.d.ts +12 -29
  89. package/build/lib/extension/index.d.ts.map +1 -1
  90. package/build/lib/extension/index.js +33 -75
  91. package/build/lib/extension/index.js.map +1 -1
  92. package/build/lib/extension/manifest-migrations.d.ts +3 -20
  93. package/build/lib/extension/manifest-migrations.d.ts.map +1 -1
  94. package/build/lib/extension/manifest-migrations.js +20 -101
  95. package/build/lib/extension/manifest-migrations.js.map +1 -1
  96. package/build/lib/extension/manifest.d.ts +61 -107
  97. package/build/lib/extension/manifest.d.ts.map +1 -1
  98. package/build/lib/extension/manifest.js +181 -356
  99. package/build/lib/extension/manifest.js.map +1 -1
  100. package/build/lib/extension/package-changed.d.ts +1 -3
  101. package/build/lib/extension/package-changed.d.ts.map +1 -1
  102. package/build/lib/extension/package-changed.js +8 -15
  103. package/build/lib/extension/package-changed.js.map +1 -1
  104. package/build/lib/extension/plugin-config.d.ts +10 -52
  105. package/build/lib/extension/plugin-config.d.ts.map +1 -1
  106. package/build/lib/extension/plugin-config.js +11 -63
  107. package/build/lib/extension/plugin-config.js.map +1 -1
  108. package/build/lib/helpers/build.d.ts +22 -0
  109. package/build/lib/helpers/build.d.ts.map +1 -0
  110. package/build/lib/helpers/build.js +109 -0
  111. package/build/lib/helpers/build.js.map +1 -0
  112. package/build/lib/helpers/capability.d.ts +38 -0
  113. package/build/lib/helpers/capability.d.ts.map +1 -0
  114. package/build/lib/helpers/capability.js +128 -0
  115. package/build/lib/helpers/capability.js.map +1 -0
  116. package/build/lib/helpers/network.d.ts +14 -0
  117. package/build/lib/helpers/network.d.ts.map +1 -0
  118. package/build/lib/helpers/network.js +35 -0
  119. package/build/lib/helpers/network.js.map +1 -0
  120. package/build/lib/insecure-features.js +6 -6
  121. package/build/lib/insecure-features.js.map +1 -1
  122. package/build/lib/inspector-commands.d.ts +6 -0
  123. package/build/lib/inspector-commands.d.ts.map +1 -1
  124. package/build/lib/inspector-commands.js +6 -0
  125. package/build/lib/inspector-commands.js.map +1 -1
  126. package/build/lib/logger.d.ts +2 -3
  127. package/build/lib/logger.d.ts.map +1 -1
  128. package/build/lib/logger.js +2 -3
  129. package/build/lib/logger.js.map +1 -1
  130. package/build/lib/logsink.d.ts +13 -22
  131. package/build/lib/logsink.d.ts.map +1 -1
  132. package/build/lib/logsink.js +48 -103
  133. package/build/lib/logsink.js.map +1 -1
  134. package/build/lib/main.d.ts +15 -58
  135. package/build/lib/main.d.ts.map +1 -1
  136. package/build/lib/main.js +25 -425
  137. package/build/lib/main.js.map +1 -1
  138. package/build/lib/schema/arg-spec.d.ts +32 -107
  139. package/build/lib/schema/arg-spec.d.ts.map +1 -1
  140. package/build/lib/schema/arg-spec.js +11 -107
  141. package/build/lib/schema/arg-spec.js.map +1 -1
  142. package/build/lib/schema/cli-args-guards.d.ts +34 -0
  143. package/build/lib/schema/cli-args-guards.d.ts.map +1 -0
  144. package/build/lib/schema/cli-args-guards.js +49 -0
  145. package/build/lib/schema/cli-args-guards.js.map +1 -0
  146. package/build/lib/schema/cli-args.d.ts +3 -15
  147. package/build/lib/schema/cli-args.d.ts.map +1 -1
  148. package/build/lib/schema/cli-args.js +17 -107
  149. package/build/lib/schema/cli-args.js.map +1 -1
  150. package/build/lib/schema/cli-transformers.d.ts +15 -12
  151. package/build/lib/schema/cli-transformers.d.ts.map +1 -1
  152. package/build/lib/schema/cli-transformers.js +15 -45
  153. package/build/lib/schema/cli-transformers.js.map +1 -1
  154. package/build/lib/schema/format-errors.d.ts +28 -0
  155. package/build/lib/schema/format-errors.d.ts.map +1 -0
  156. package/build/lib/schema/format-errors.js +29 -0
  157. package/build/lib/schema/format-errors.js.map +1 -0
  158. package/build/lib/schema/index.d.ts +4 -2
  159. package/build/lib/schema/index.d.ts.map +1 -1
  160. package/build/lib/schema/index.js +2 -0
  161. package/build/lib/schema/index.js.map +1 -1
  162. package/build/lib/schema/keywords.d.ts +12 -20
  163. package/build/lib/schema/keywords.d.ts.map +1 -1
  164. package/build/lib/schema/keywords.js +6 -51
  165. package/build/lib/schema/keywords.js.map +1 -1
  166. package/build/lib/schema/schema.d.ts +106 -231
  167. package/build/lib/schema/schema.d.ts.map +1 -1
  168. package/build/lib/schema/schema.js +88 -358
  169. package/build/lib/schema/schema.js.map +1 -1
  170. package/build/lib/utils.d.ts +7 -267
  171. package/build/lib/utils.d.ts.map +1 -1
  172. package/build/lib/utils.js +10 -409
  173. package/build/lib/utils.js.map +1 -1
  174. package/lib/{appium.js → appium.ts} +297 -341
  175. package/lib/bidi-commands.ts +10 -14
  176. package/lib/bootstrap/appium-initializer.ts +212 -0
  177. package/lib/bootstrap/appium-main-runner.ts +172 -0
  178. package/lib/bootstrap/config-file.ts +178 -0
  179. package/lib/bootstrap/grid-v3-register.ts +250 -0
  180. package/lib/bootstrap/init-types.ts +31 -0
  181. package/lib/bootstrap/main-helpers.ts +223 -0
  182. package/lib/bootstrap/node-helpers.ts +180 -0
  183. package/lib/bootstrap/startup-config.ts +143 -0
  184. package/lib/cli/{args.js → args.ts} +45 -56
  185. package/lib/cli/driver-command.ts +122 -0
  186. package/lib/cli/{extension-command.js → extension-command.ts} +827 -906
  187. package/lib/cli/extension.ts +65 -0
  188. package/lib/cli/{parser.js → parser.ts} +93 -116
  189. package/lib/cli/plugin-command.ts +117 -0
  190. package/lib/cli/{setup-command.js → setup-command.ts} +59 -74
  191. package/lib/cli/utils.ts +97 -0
  192. package/lib/{constants.js → constants.ts} +30 -41
  193. package/lib/doctor/{doctor.js → doctor.ts} +82 -92
  194. package/lib/extension/driver-config.ts +165 -0
  195. package/lib/extension/{extension-config.js → extension-config.ts} +291 -405
  196. package/lib/extension/index.ts +143 -0
  197. package/lib/extension/manifest-migrations.ts +57 -0
  198. package/lib/extension/manifest.ts +369 -0
  199. package/lib/extension/{package-changed.js → package-changed.ts} +9 -18
  200. package/lib/extension/plugin-config.ts +62 -0
  201. package/lib/helpers/build.ts +111 -0
  202. package/lib/helpers/capability.ts +171 -0
  203. package/lib/helpers/network.ts +30 -0
  204. package/lib/insecure-features.ts +1 -1
  205. package/lib/inspector-commands.ts +6 -1
  206. package/lib/{logger.js → logger.ts} +1 -2
  207. package/lib/{logsink.js → logsink.ts} +91 -137
  208. package/lib/main.ts +60 -0
  209. package/lib/schema/arg-spec.ts +131 -0
  210. package/lib/schema/cli-args-guards.ts +67 -0
  211. package/lib/schema/cli-args.ts +171 -0
  212. package/lib/schema/cli-transformers.ts +83 -0
  213. package/lib/schema/format-errors.ts +43 -0
  214. package/lib/schema/index.ts +4 -0
  215. package/lib/schema/keywords.ts +96 -0
  216. package/lib/schema/schema.ts +448 -0
  217. package/lib/utils.ts +73 -0
  218. package/package.json +17 -18
  219. package/scripts/autoinstall-extensions.js +3 -0
  220. package/build/lib/config-file.d.ts +0 -100
  221. package/build/lib/config-file.d.ts.map +0 -1
  222. package/build/lib/config-file.js.map +0 -1
  223. package/build/lib/config.d.ts +0 -70
  224. package/build/lib/config.d.ts.map +0 -1
  225. package/build/lib/config.js +0 -390
  226. package/build/lib/config.js.map +0 -1
  227. package/build/lib/grid-register.d.ts +0 -10
  228. package/build/lib/grid-register.d.ts.map +0 -1
  229. package/build/lib/grid-register.js +0 -134
  230. package/build/lib/grid-register.js.map +0 -1
  231. package/lib/cli/driver-command.js +0 -174
  232. package/lib/cli/extension.js +0 -74
  233. package/lib/cli/plugin-command.js +0 -164
  234. package/lib/cli/utils.js +0 -91
  235. package/lib/config-file.js +0 -228
  236. package/lib/config.js +0 -389
  237. package/lib/extension/driver-config.js +0 -245
  238. package/lib/extension/index.js +0 -169
  239. package/lib/extension/manifest-migrations.js +0 -136
  240. package/lib/extension/manifest.js +0 -550
  241. package/lib/extension/plugin-config.js +0 -112
  242. package/lib/grid-register.js +0 -146
  243. package/lib/main.js +0 -545
  244. package/lib/schema/arg-spec.js +0 -229
  245. package/lib/schema/cli-args.js +0 -254
  246. package/lib/schema/cli-transformers.js +0 -113
  247. package/lib/schema/index.js +0 -2
  248. package/lib/schema/keywords.js +0 -136
  249. package/lib/schema/schema.js +0 -725
  250. package/lib/utils.js +0 -512
@@ -0,0 +1,143 @@
1
+ import _ from 'lodash';
2
+ import B from 'bluebird';
3
+ import type {DriverClass, ExtensionType, PluginClass} from '@appium/types';
4
+ import type {ExtClass} from 'appium/types';
5
+ import {USE_ALL_PLUGINS} from '../constants';
6
+ import {log} from '../logger';
7
+ import {DriverConfig} from './driver-config';
8
+ import {Manifest} from './manifest';
9
+ import {timing} from '@appium/support';
10
+ import {PluginConfig} from './plugin-config';
11
+
12
+ export type ExtensionConfigs = {
13
+ driverConfig: DriverConfig;
14
+ pluginConfig: PluginConfig;
15
+ };
16
+
17
+ export type PluginNameMap = Map<PluginClass, string>;
18
+ export type DriverNameMap = Map<DriverClass, string>;
19
+
20
+ /**
21
+ * Loads extensions and creates `ExtensionConfig` instances.
22
+ *
23
+ * - Reads the manifest file, creating if necessary
24
+ * - Using the parsed extension data, creates/gets the `ExtensionConfig` subclass instances
25
+ * - Returns these instances
26
+ *
27
+ * If `appiumHome` is needed, use `resolveAppiumHome` from the `env` module in `@appium/support`.
28
+ */
29
+ export async function loadExtensions(appiumHome: string): Promise<ExtensionConfigs> {
30
+ const manifest = Manifest.getInstance(appiumHome);
31
+ await manifest.read();
32
+ const driverConfig = DriverConfig.getInstance(manifest) ?? DriverConfig.create(manifest);
33
+ const pluginConfig = PluginConfig.getInstance(manifest) ?? PluginConfig.create(manifest);
34
+
35
+ await B.all([driverConfig.validate(), pluginConfig.validate()]);
36
+ return {driverConfig, pluginConfig};
37
+ }
38
+
39
+ /**
40
+ * Find any plugin name which has been installed, and which has been requested for activation by
41
+ * using the --use-plugins flag, and turn each one into its class, so we can send them as objects
42
+ * to the server init. We also want to send/assign them to the umbrella driver so it can use them
43
+ * to wrap command execution
44
+ */
45
+ export async function getActivePlugins(
46
+ pluginConfig: PluginConfig,
47
+ maxParallelImports: number,
48
+ usePlugins: string[] = []
49
+ ): Promise<PluginNameMap> {
50
+ if (_.isEmpty(usePlugins)) {
51
+ return new Map();
52
+ }
53
+
54
+ let filteredPluginNames: string[] = [];
55
+ if (usePlugins.length === 1 && usePlugins[0] === USE_ALL_PLUGINS) {
56
+ filteredPluginNames = _.keys(pluginConfig.installedExtensions);
57
+ } else {
58
+ for (const pluginName of usePlugins) {
59
+ if (pluginName in pluginConfig.installedExtensions) {
60
+ filteredPluginNames.push(pluginName);
61
+ } else if (pluginName === USE_ALL_PLUGINS) {
62
+ throw new Error(`The reserved plugin name '${pluginName}' cannot be combined with other names.`);
63
+ } else {
64
+ const suffix = _.isEmpty(pluginConfig.installedExtensions)
65
+ ? `You don't have any plugins installed yet.`
66
+ : `Only the following ${_.size(pluginConfig.installedExtensions) === 1 ? `plugin is` : `plugins are`} ` +
67
+ `available: ${_.keys(pluginConfig.installedExtensions)}`;
68
+ throw new Error(`Could not load the plugin '${pluginName}' because it is not installed. ${suffix}`);
69
+ }
70
+ }
71
+ }
72
+ const pairs = await importExtensions('plugin', pluginConfig, filteredPluginNames, maxParallelImports);
73
+ return new Map(pairs as Array<[PluginClass, string]>);
74
+ }
75
+
76
+ /**
77
+ * Find any driver name which has been installed, and turn each one into its class, so we can send
78
+ * them as objects to the server init in case they need to add methods/routes or update the server.
79
+ * If the --drivers flag was given, this method only loads the given drivers.
80
+ */
81
+ export async function getActiveDrivers(
82
+ driverConfig: DriverConfig,
83
+ maxParallelImports: number,
84
+ useDrivers: string[] = []
85
+ ): Promise<DriverNameMap> {
86
+ let filteredDriverNames: string[] = [];
87
+ if (_.isEmpty(useDrivers)) {
88
+ filteredDriverNames = _.keys(driverConfig.installedExtensions);
89
+ } else {
90
+ for (const driverName of useDrivers) {
91
+ if (driverName in driverConfig.installedExtensions) {
92
+ filteredDriverNames.push(driverName);
93
+ } else {
94
+ const suffix = _.isEmpty(driverConfig.installedExtensions)
95
+ ? `You don't have any drivers installed yet.`
96
+ : `Only the following ${_.size(driverConfig.installedExtensions) === 1 ? `driver is` : `drivers are`} ` +
97
+ `available: ${_.keys(driverConfig.installedExtensions)}`;
98
+ throw new Error(`Could not load the driver '${driverName}' because it is not installed. ${suffix}`);
99
+ }
100
+ }
101
+ }
102
+ const pairs = await importExtensions('driver', driverConfig, filteredDriverNames, maxParallelImports);
103
+ return new Map(pairs as Array<[DriverClass, string]>);
104
+ }
105
+
106
+
107
+ async function importExtensions(
108
+ extType: 'driver' | 'plugin',
109
+ config: DriverConfig | PluginConfig,
110
+ extNames: string[],
111
+ asyncImportChunkSize: number
112
+ ): Promise<Array<[ExtClass<ExtensionType>, string]>> {
113
+ const allPromises: Array<B<ExtClass<ExtensionType> | undefined>> = [];
114
+ const activePromisesChunk: Array<B<ExtClass<ExtensionType> | undefined>> = [];
115
+ for (const extName of extNames) {
116
+ _.remove(activePromisesChunk, (p) => p.isFulfilled());
117
+ if (activePromisesChunk.length >= asyncImportChunkSize) {
118
+ await B.any(activePromisesChunk);
119
+ }
120
+ const promise = B.resolve(
121
+ (async () => {
122
+ log.info(`Attempting to load ${extType} ${extName}...`);
123
+ const timer = new timing.Timer().start();
124
+ try {
125
+ const extClass = await config.requireAsync(extName as never);
126
+ log.debug(`${extClass.name} has been successfully loaded in ${timer.getDuration().asSeconds.toFixed(3)}s`);
127
+ return extClass as ExtClass<ExtensionType>;
128
+ } catch (err: any) {
129
+ log.error(
130
+ `Could not load ${extType} '${extName}', so it will not be available. Error ` +
131
+ `in loading the ${extType} was: ${err.message}`
132
+ );
133
+ log.debug(err.stack);
134
+ }
135
+ })()
136
+ );
137
+ activePromisesChunk.push(promise);
138
+ allPromises.push(promise);
139
+ }
140
+ return _.zip(await B.all(allPromises), extNames).filter(([extClass]) => Boolean(extClass)) as Array<
141
+ [ExtClass<ExtensionType>, string]
142
+ >;
143
+ }
@@ -0,0 +1,57 @@
1
+ import type {ManifestDataVersions} from 'appium/types';
2
+ import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
3
+ import {log} from '../logger';
4
+ import type {Manifest} from './manifest';
5
+
6
+ const SCHEMA_REV_3 = 3;
7
+ const SCHEMA_REV_4 = 4;
8
+
9
+ type Migration = (manifest: Manifest) => boolean | Promise<boolean>;
10
+
11
+ const Migrations: Partial<Record<keyof ManifestDataVersions, Migration>> = {
12
+ [SCHEMA_REV_3]: (manifest) => {
13
+ const drivers = manifest.getExtensionData(DRIVER_TYPE);
14
+ const plugins = manifest.getExtensionData(PLUGIN_TYPE);
15
+ const allExtData = [...Object.values(drivers), ...Object.values(plugins)];
16
+ return allExtData.some((metadata) => !('installPath' in metadata));
17
+ },
18
+ [SCHEMA_REV_4]: (manifest) => {
19
+ if (manifest.schemaRev < SCHEMA_REV_4) {
20
+ const drivers = manifest.getExtensionData(DRIVER_TYPE);
21
+ const plugins = manifest.getExtensionData(PLUGIN_TYPE);
22
+ const allExtData = [...Object.values(drivers), ...Object.values(plugins)];
23
+ return allExtData.some((metadata) => metadata.installType === 'npm');
24
+ }
25
+ return false;
26
+ },
27
+ };
28
+
29
+ /**
30
+ * Applies a series of migration functions to a manifest to update its manifest schema version.
31
+ *
32
+ * `data` is modified in-place.
33
+ *
34
+ * @returns If `true` existing packages should be synced from disk and the manifest should be persisted.
35
+ */
36
+ export async function migrate(manifest: Manifest): Promise<boolean> {
37
+ let didChange = false;
38
+ for (const migration of Object.values(Migrations)) {
39
+ if (migration) {
40
+ didChange = (await migration(manifest)) || didChange;
41
+ }
42
+ }
43
+ didChange = setSchemaRev(manifest, CURRENT_SCHEMA_REV) || didChange;
44
+ if (didChange) {
45
+ log.debug(`Upgraded extension manifest to schema v${manifest.schemaRev}`);
46
+ }
47
+
48
+ return didChange;
49
+ }
50
+
51
+ function setSchemaRev(manifest: Manifest, version: number): boolean {
52
+ if ((manifest.schemaRev ?? 0) < version) {
53
+ manifest.setSchemaRev(version);
54
+ return true;
55
+ }
56
+ return false;
57
+ }
@@ -0,0 +1,369 @@
1
+ import B from 'bluebird';
2
+ import {env, fs} from '@appium/support';
3
+ import _ from 'lodash';
4
+ import path from 'node:path';
5
+ import * as YAML from 'yaml';
6
+ import type {DriverType, ExtensionType, PluginType} from '@appium/types';
7
+ import type {ExtManifest, ExtPackageJson, ExtRecord, InternalMetadata, ManifestData} from 'appium/types';
8
+ import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
9
+ import {INSTALL_TYPE_DEV, INSTALL_TYPE_NPM} from './extension-config';
10
+ import {packageDidChange} from './package-changed';
11
+ import {migrate} from './manifest-migrations';
12
+
13
+ const CONFIG_DATA_DRIVER_KEY = `${DRIVER_TYPE}s` as const;
14
+ const CONFIG_DATA_PLUGIN_KEY = `${PLUGIN_TYPE}s` as const;
15
+
16
+ const INITIAL_MANIFEST_DATA: Readonly<ManifestData> = Object.freeze({
17
+ [CONFIG_DATA_DRIVER_KEY]: Object.freeze({}),
18
+ [CONFIG_DATA_PLUGIN_KEY]: Object.freeze({}),
19
+ schemaRev: CURRENT_SCHEMA_REV,
20
+ }) as Readonly<ManifestData>;
21
+
22
+ /**
23
+ * Handles reading & writing of extension config files.
24
+ *
25
+ * Only one instance of this class exists per value of `APPIUM_HOME`.
26
+ */
27
+ export class Manifest {
28
+ /**
29
+ * Returns the memoized manifest for an `APPIUM_HOME` directory (one instance per home).
30
+ *
31
+ * @param appiumHome - `APPIUM_HOME` path used as the cache key
32
+ */
33
+ static getInstance = _.memoize((appiumHome: string): Manifest => new Manifest(appiumHome));
34
+
35
+ #data!: ManifestData;
36
+ readonly #appiumHome: string;
37
+ #manifestPath: string | undefined = undefined;
38
+ #writing: Promise<boolean> | undefined;
39
+ #reading: Promise<void> | undefined;
40
+
41
+ private constructor(appiumHome: string) {
42
+ this.#appiumHome = appiumHome;
43
+ this.#data = _.cloneDeep(INITIAL_MANIFEST_DATA) as ManifestData;
44
+ }
45
+
46
+ /** `APPIUM_HOME` directory this manifest is tied to. */
47
+ get appiumHome(): string {
48
+ return this.#appiumHome;
49
+ }
50
+
51
+ /**
52
+ * Absolute path to the extension manifest file after {@link Manifest.read} or {@link Manifest.write} has resolved it.
53
+ * Before that, this is `undefined`.
54
+ */
55
+ get manifestPath(): string | undefined {
56
+ return this.#manifestPath;
57
+ }
58
+
59
+ /** Schema revision of the in-memory manifest data (from YAML `schemaRev`). */
60
+ get schemaRev(): number {
61
+ return this.#data.schemaRev;
62
+ }
63
+
64
+ /**
65
+ * Returns the live installed-extension map for drivers or plugins (same object as stored in memory).
66
+ * Mutations affect the manifest until replaced by a new object (e.g. via `read()`); `setExtension` / `deleteExtension` update this record.
67
+ *
68
+ * @param extType - `"driver"` or `"plugin"`
69
+ */
70
+ getExtensionData<ExtType extends ExtensionType>(extType: ExtType): ExtRecord<ExtType> {
71
+ const record = extType === DRIVER_TYPE ? this.#data.drivers : this.#data.plugins;
72
+ return record as ExtRecord<ExtType>;
73
+ }
74
+
75
+ /**
76
+ * Whether a driver with the given manifest key is present.
77
+ *
78
+ * @param name - Driver name as stored under `drivers` in the manifest
79
+ */
80
+ hasDriver(name: string): boolean {
81
+ return Boolean(this.#data.drivers[name]);
82
+ }
83
+
84
+ /**
85
+ * Whether a plugin with the given manifest key is present.
86
+ *
87
+ * @param name - Plugin name as stored under `plugins` in the manifest
88
+ */
89
+ hasPlugin(name: string): boolean {
90
+ return Boolean(this.#data.plugins[name]);
91
+ }
92
+
93
+ /**
94
+ * Loads manifest YAML from disk into memory, runs migration when needed, may sync with installed packages, and writes back if required.
95
+ * Concurrent calls while a read is in flight share the same in-flight work.
96
+ *
97
+ * @returns The parsed in-memory manifest data
98
+ */
99
+ async read(): Promise<ManifestData> {
100
+ if (this.#reading) {
101
+ await this.#reading;
102
+ return this.#data;
103
+ }
104
+
105
+ this.#reading = (async () => {
106
+ let data: ManifestData;
107
+ let shouldWrite = false;
108
+ const manifestPathResolved = await this.#setManifestPath();
109
+ try {
110
+ const yaml = await fs.readFile(manifestPathResolved, 'utf8');
111
+ data = YAML.parse(yaml) as ManifestData;
112
+ } catch (err: any) {
113
+ if (err.code === 'ENOENT') {
114
+ data = _.cloneDeep(INITIAL_MANIFEST_DATA) as ManifestData;
115
+ shouldWrite = true;
116
+ } else {
117
+ throw new Error(
118
+ `Appium had trouble loading the extension installation ` +
119
+ `cache file (${manifestPathResolved}). It may be invalid YAML. Specific error: ${
120
+ err.message
121
+ }`
122
+ );
123
+ }
124
+ }
125
+
126
+ this.#data = data;
127
+
128
+ if (!shouldWrite && (data.schemaRev ?? 0) < CURRENT_SCHEMA_REV) {
129
+ shouldWrite = await migrate(this);
130
+ }
131
+
132
+ const hasAppiumDependency = await env.hasAppiumDependency(this.appiumHome);
133
+
134
+ if (shouldWrite || (hasAppiumDependency && (await packageDidChange(this.appiumHome)))) {
135
+ shouldWrite = (await this.syncWithInstalledExtensions(hasAppiumDependency)) || shouldWrite;
136
+ }
137
+
138
+ if (shouldWrite) {
139
+ await this.write();
140
+ }
141
+ })();
142
+
143
+ try {
144
+ await this.#reading;
145
+ return this.#data;
146
+ } finally {
147
+ this.#reading = undefined;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Serializes the current in-memory manifest to the resolved manifest path.
153
+ * Concurrent calls while a write is in flight share the same in-flight work.
154
+ *
155
+ * @returns `true` when the file was written successfully
156
+ */
157
+ async write(): Promise<boolean> {
158
+ if (this.#writing) {
159
+ return this.#writing;
160
+ }
161
+ this.#writing = (async () => {
162
+ const manifestPathResolved = await this.#setManifestPath();
163
+ try {
164
+ await fs.mkdirp(path.dirname(manifestPathResolved));
165
+ } catch (err: any) {
166
+ throw new Error(
167
+ `Appium could not create the directory for the manifest file: ${path.dirname(
168
+ manifestPathResolved
169
+ )}. Original error: ${err.message}`
170
+ );
171
+ }
172
+ try {
173
+ await fs.writeFile(manifestPathResolved, YAML.stringify(this.#data), 'utf8');
174
+ return true;
175
+ } catch (err: any) {
176
+ throw new Error(
177
+ `Appium could not write to manifest at ${manifestPathResolved} using APPIUM_HOME ${
178
+ this.#appiumHome
179
+ }. Please ensure it is writable. Original error: ${err.message}`
180
+ );
181
+ }
182
+ })();
183
+ try {
184
+ return await this.#writing;
185
+ } finally {
186
+ this.#writing = undefined;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Scans `APPIUM_HOME` (root and `node_modules`) for Appium extension packages and merges them into the manifest.
192
+ *
193
+ * @param hasAppiumDependency - When true and the root `package.json` depends on Appium, matching extensions use the `"dev"` install type
194
+ * @returns `true` if any extension entries changed, `false` otherwise
195
+ */
196
+ async syncWithInstalledExtensions(hasAppiumDependency = false): Promise<boolean> {
197
+ let didChange = false;
198
+
199
+ const onMatch = async (filepath: string, devType = false): Promise<void> => {
200
+ try {
201
+ const pkg = JSON.parse(await fs.readFile(filepath, 'utf8')) as unknown;
202
+ if (isExtension(pkg)) {
203
+ const installType = devType && hasAppiumDependency ? INSTALL_TYPE_DEV : INSTALL_TYPE_NPM;
204
+ const changed = this.addExtensionFromPackage(pkg, filepath, installType);
205
+ didChange = didChange || changed;
206
+ }
207
+ } catch {
208
+ // ignore invalid package.json
209
+ }
210
+ };
211
+
212
+ const queue: Promise<void>[] = [
213
+ onMatch(path.join(this.#appiumHome, 'package.json'), true),
214
+ ];
215
+
216
+ const filepaths = await fs.glob('node_modules/{*,@*/*}/package.json', {
217
+ cwd: this.#appiumHome,
218
+ absolute: true,
219
+ });
220
+ for (const filepath of filepaths) {
221
+ queue.push(onMatch(filepath));
222
+ }
223
+
224
+ await B.all(queue);
225
+
226
+ return didChange;
227
+ }
228
+
229
+ /**
230
+ * Builds manifest metadata from a `package.json` and registers it if it is a driver or plugin and the entry changed.
231
+ *
232
+ * @param pkgJson - Parsed extension `package.json`
233
+ * @param pkgPath - Path to that `package.json` (install path is derived from its directory)
234
+ * @param installType - How the package was discovered (`npm` vs `dev`)
235
+ * @returns `true` if the manifest was updated, `false` if unchanged or already matched
236
+ * @throws TypeError if the package is not a valid driver or plugin extension
237
+ */
238
+ addExtensionFromPackage(
239
+ pkgJson: ExtPackageJson<ExtensionType>,
240
+ pkgPath: string,
241
+ installType: typeof INSTALL_TYPE_NPM | typeof INSTALL_TYPE_DEV = INSTALL_TYPE_NPM
242
+ ): boolean {
243
+ const extensionPath = path.dirname(pkgPath);
244
+
245
+ const internal: InternalMetadata = {
246
+ pkgName: pkgJson.name,
247
+ version: pkgJson.version,
248
+ appiumVersion: pkgJson.peerDependencies?.appium,
249
+ installType,
250
+ installSpec: `${pkgJson.name}@${pkgJson.version}`,
251
+ installPath: extensionPath,
252
+ };
253
+
254
+ if (isDriver(pkgJson)) {
255
+ const driverName = pkgJson.appium.driverName;
256
+ const value = {
257
+ ..._.omit(pkgJson.appium, 'driverName'),
258
+ ...internal,
259
+ };
260
+ if (!_.isEqual(value, this.#data.drivers[driverName])) {
261
+ this.setExtension(DRIVER_TYPE, driverName, value);
262
+ return true;
263
+ }
264
+ return false;
265
+ }
266
+ if (isPlugin(pkgJson)) {
267
+ const pluginName = pkgJson.appium.pluginName;
268
+ const value = {
269
+ ..._.omit(pkgJson.appium, 'pluginName'),
270
+ ...internal,
271
+ };
272
+ if (!_.isEqual(value, this.#data.plugins[pluginName])) {
273
+ this.setExtension(PLUGIN_TYPE, pluginName, value);
274
+ return true;
275
+ }
276
+ return false;
277
+ }
278
+ throw new TypeError(
279
+ `The extension in ${extensionPath} is neither a valid ${DRIVER_TYPE} nor a valid ${PLUGIN_TYPE}.`
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Stores a deep-cloned copy of extension metadata under the given type and name.
285
+ *
286
+ * @param extType - `"driver"` or `"plugin"`
287
+ * @param extName - Manifest key for the extension
288
+ * @param extData - Full extension entry to persist in memory
289
+ * @returns The cloned data now held in the manifest
290
+ */
291
+ setExtension<ExtType extends ExtensionType>(
292
+ extType: ExtType,
293
+ extName: string,
294
+ extData: ExtManifest<ExtType>
295
+ ): ExtManifest<ExtType> {
296
+ const data = _.cloneDeep(extData) as ExtManifest<ExtType>;
297
+ if (extType === DRIVER_TYPE) {
298
+ this.#data.drivers[extName] = data as unknown as ExtManifest<DriverType>;
299
+ } else {
300
+ this.#data.plugins[extName] = data as unknown as ExtManifest<PluginType>;
301
+ }
302
+ return data;
303
+ }
304
+
305
+ /**
306
+ * Updates the in-memory manifest schema revision (typically during migration).
307
+ *
308
+ * @param rev - New `schemaRev` value
309
+ */
310
+ setSchemaRev(rev: number): void {
311
+ this.#data.schemaRev = rev;
312
+ }
313
+
314
+ /**
315
+ * Removes an extension entry from the manifest data in memory (does not write to disk by itself).
316
+ *
317
+ * @param extType - `"driver"` or `"plugin"`
318
+ * @param extName - Manifest key to remove
319
+ */
320
+ deleteExtension(extType: ExtensionType, extName: string): void {
321
+ if (extType === DRIVER_TYPE) {
322
+ delete this.#data.drivers[extName];
323
+ } else {
324
+ delete this.#data.plugins[extName];
325
+ }
326
+ }
327
+
328
+ async #setManifestPath(): Promise<string> {
329
+ if (!this.#manifestPath) {
330
+ const resolved = await env.resolveManifestPath(this.#appiumHome);
331
+ this.#manifestPath = resolved;
332
+
333
+ if (path.relative(this.#appiumHome, resolved).startsWith('.')) {
334
+ throw new Error(
335
+ `Mismatch between location of APPIUM_HOME and manifest file. APPIUM_HOME: ${
336
+ this.appiumHome
337
+ }, manifest file: ${resolved}`
338
+ );
339
+ }
340
+ }
341
+
342
+ return this.#manifestPath;
343
+ }
344
+ }
345
+
346
+ function isExtension(value: unknown): value is ExtPackageJson<ExtensionType> {
347
+ return (
348
+ _.isPlainObject(value) &&
349
+ _.isPlainObject((value as {appium?: unknown}).appium) &&
350
+ _.isString((value as {name?: unknown}).name) &&
351
+ _.isString((value as {version?: unknown}).version)
352
+ );
353
+ }
354
+
355
+ function isDriver(value: unknown): value is ExtPackageJson<DriverType> {
356
+ return (
357
+ isExtension(value) &&
358
+ 'driverName' in value.appium &&
359
+ _.isString((value.appium as {driverName?: unknown}).driverName)
360
+ );
361
+ }
362
+
363
+ function isPlugin(value: unknown): value is ExtPackageJson<PluginType> {
364
+ return (
365
+ isExtension(value) &&
366
+ 'pluginName' in value.appium &&
367
+ _.isString((value.appium as {pluginName?: unknown}).pluginName)
368
+ );
369
+ }
@@ -2,7 +2,7 @@ import {fs} from '@appium/support';
2
2
  import {isPackageChanged} from 'package-changed';
3
3
  import path from 'node:path';
4
4
  import {PKG_HASHFILE_RELATIVE_PATH} from '../constants';
5
- import log from '../logger';
5
+ import {log} from '../logger';
6
6
 
7
7
  /**
8
8
  * Determines if extensions have changed, and updates a hash the `package.json` in `appiumHome` if so.
@@ -10,40 +10,31 @@ import log from '../logger';
10
10
  * If they have, we need to sync them with the `extensions.yaml` manifest.
11
11
  *
12
12
  * _Warning: this makes a blocking call to `writeFileSync`._
13
- * @param {string} appiumHome
14
- * @returns {Promise<boolean>} `true` if `package.json` `appiumHome` changed
15
13
  */
16
- export async function packageDidChange(appiumHome) {
14
+ export async function packageDidChange(appiumHome: string): Promise<boolean> {
17
15
  const hashFilename = path.join(appiumHome, PKG_HASHFILE_RELATIVE_PATH);
18
16
 
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
17
  const hashFilenameDir = path.dirname(hashFilename);
32
18
  log.debug(`Creating hash file directory: ${hashFilenameDir}`);
33
19
  try {
34
20
  await fs.mkdirp(hashFilenameDir);
35
- } catch (err) {
21
+ } catch (err: any) {
36
22
  throw new Error(
37
23
  `Appium could not create the directory for hash file: ${hashFilenameDir}. Original error: ${err.message}`
38
24
  );
39
25
  }
40
26
 
27
+ let isChanged: boolean;
28
+ let writeHash: () => void;
29
+ let oldHash: string | undefined;
30
+ let hash: string;
41
31
  try {
42
32
  ({isChanged, writeHash, oldHash, hash} = await isPackageChanged({
43
33
  cwd: appiumHome,
44
34
  hashFilename: PKG_HASHFILE_RELATIVE_PATH,
45
35
  }));
46
36
  } catch {
37
+ // If the library fails, assume the manifest may be stale and should be refreshed.
47
38
  return true;
48
39
  }
49
40
 
@@ -53,7 +44,7 @@ export async function packageDidChange(appiumHome) {
53
44
  log.debug(
54
45
  `Updated hash of ${appiumHome}/package.json from: ${oldHash ?? '(none)'} to: ${hash}`
55
46
  );
56
- } catch (err) {
47
+ } catch (err: any) {
57
48
  throw new Error(
58
49
  `Appium could not write hash file: ${hashFilenameDir}. Original error: ${err.message}`
59
50
  );
@@ -0,0 +1,62 @@
1
+ import _ from 'lodash';
2
+ import type {PluginType} from '@appium/types';
3
+ import type {ExtManifest, ExtName, ExtRecord} from 'appium/types';
4
+ import {PLUGIN_TYPE} from '../constants';
5
+ import {log} from '../logger';
6
+ import {ExtensionConfig} from './extension-config';
7
+ import type {Manifest} from './manifest';
8
+
9
+ export class PluginConfig extends ExtensionConfig<PluginType> {
10
+ private static readonly _instances = new WeakMap<Manifest, PluginConfig>();
11
+
12
+ private constructor(manifest: Manifest) {
13
+ super(PLUGIN_TYPE, manifest);
14
+ }
15
+
16
+ static create(manifest: Manifest): PluginConfig {
17
+ const instance = new PluginConfig(manifest);
18
+ if (PluginConfig.getInstance(manifest)) {
19
+ throw new Error(
20
+ `Manifest with APPIUM_HOME ${manifest.appiumHome} already has a PluginConfig; use PluginConfig.getInstance() to retrieve it.`
21
+ );
22
+ }
23
+ PluginConfig._instances.set(manifest, instance);
24
+ return instance;
25
+ }
26
+
27
+ static getInstance(manifest: Manifest): PluginConfig | undefined {
28
+ return PluginConfig._instances.get(manifest);
29
+ }
30
+
31
+ async validate(): Promise<ExtRecord<PluginType>> {
32
+ return await super._validate(this.manifest.getExtensionData(PLUGIN_TYPE));
33
+ }
34
+
35
+ public override extensionDesc(
36
+ pluginName: ExtName<PluginType>,
37
+ {version}: ExtManifest<PluginType>
38
+ ): string {
39
+ return `${String(pluginName)}@${version}`;
40
+ }
41
+
42
+ override print(activeNames: ExtName<PluginType>[] = []): void {
43
+ const pluginNames = Object.keys(this.installedExtensions);
44
+
45
+ if (_.isEmpty(pluginNames)) {
46
+ log.info(
47
+ `No plugins have been installed. Use the "appium plugin" ` + 'command to install the one(s) you want to use.'
48
+ );
49
+ return;
50
+ }
51
+
52
+ log.info(`Available plugins:`);
53
+ for (const [pluginName, pluginData] of _.toPairs(this.installedExtensions)) {
54
+ const activeTxt = _.includes(activeNames, pluginName as ExtName<PluginType>) ? ' (ACTIVE)' : '';
55
+ log.info(` - ${this.extensionDesc(pluginName as ExtName<PluginType>, pluginData)}${activeTxt}`);
56
+ }
57
+
58
+ if (_.isEmpty(activeNames)) {
59
+ log.info('No plugins activated. Use the --use-plugins flag with names of plugins to activate');
60
+ }
61
+ }
62
+ }