appium 2.0.0-beta.25 → 2.0.0-beta.26

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 (124) hide show
  1. package/build/lib/appium-config.schema.json +278 -0
  2. package/build/lib/appium.js +45 -66
  3. package/build/lib/cli/args.js +19 -39
  4. package/build/lib/cli/driver-command.js +5 -9
  5. package/build/lib/cli/extension-command.js +73 -64
  6. package/build/lib/cli/extension.js +10 -23
  7. package/build/lib/cli/parser.js +9 -19
  8. package/build/lib/cli/plugin-command.js +5 -9
  9. package/build/lib/cli/utils.js +2 -4
  10. package/build/lib/config-file.js +2 -4
  11. package/build/lib/config.js +7 -6
  12. package/build/lib/constants.js +60 -0
  13. package/build/lib/extension/driver-config.js +190 -0
  14. package/build/lib/extension/extension-config.js +297 -0
  15. package/build/lib/extension/index.js +77 -0
  16. package/build/lib/extension/manifest.js +246 -0
  17. package/build/lib/extension/package-changed.js +68 -0
  18. package/build/lib/extension/plugin-config.js +87 -0
  19. package/build/lib/grid-register.js +2 -4
  20. package/build/lib/logger.js +2 -4
  21. package/build/lib/logsink.js +2 -4
  22. package/build/lib/main.js +40 -68
  23. package/build/lib/schema/appium-config-schema.js +2 -4
  24. package/build/lib/schema/arg-spec.js +11 -14
  25. package/build/lib/schema/cli-args.js +2 -4
  26. package/build/lib/schema/cli-transformers.js +2 -4
  27. package/build/lib/schema/index.js +2 -4
  28. package/build/lib/schema/keywords.js +2 -4
  29. package/build/lib/schema/schema.js +55 -37
  30. package/build/lib/utils.js +1 -32
  31. package/lib/appium.js +50 -68
  32. package/lib/cli/args.js +19 -23
  33. package/lib/cli/driver-command.js +10 -2
  34. package/lib/cli/extension-command.js +216 -135
  35. package/lib/cli/extension.js +7 -15
  36. package/lib/cli/parser.js +6 -14
  37. package/lib/cli/plugin-command.js +1 -2
  38. package/lib/config-file.js +3 -3
  39. package/lib/config.js +5 -4
  40. package/lib/constants.js +79 -0
  41. package/lib/extension/driver-config.js +230 -0
  42. package/lib/extension/extension-config.js +459 -0
  43. package/lib/extension/index.js +103 -0
  44. package/lib/extension/manifest.js +590 -0
  45. package/lib/extension/package-changed.js +64 -0
  46. package/lib/extension/plugin-config.js +111 -0
  47. package/lib/grid-register.js +4 -4
  48. package/lib/main.js +51 -88
  49. package/lib/schema/arg-spec.js +2 -2
  50. package/lib/schema/cli-args.js +1 -0
  51. package/lib/schema/keywords.js +1 -1
  52. package/lib/schema/schema.js +60 -28
  53. package/lib/utils.js +2 -32
  54. package/package.json +29 -21
  55. package/{postinstall.js → scripts/postinstall.js} +1 -1
  56. package/types/types.d.ts +70 -31
  57. package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
  58. package/build/check-npm-pack-files.js +0 -23
  59. package/build/commands-yml/parse.js +0 -319
  60. package/build/commands-yml/validator.js +0 -130
  61. package/build/index.js +0 -19
  62. package/build/lib/cli/npm.js +0 -220
  63. package/build/lib/driver-config.js +0 -100
  64. package/build/lib/drivers.js +0 -100
  65. package/build/lib/ext-config-io.js +0 -165
  66. package/build/lib/extension-config.js +0 -320
  67. package/build/lib/plugin-config.js +0 -69
  68. package/build/lib/plugins.js +0 -18
  69. package/build/postinstall.js +0 -90
  70. package/build/test/cli/cli-e2e-specs.js +0 -221
  71. package/build/test/cli/cli-helpers.js +0 -86
  72. package/build/test/cli/cli-specs.js +0 -71
  73. package/build/test/cli/fixtures/test-driver/package.json +0 -27
  74. package/build/test/cli/schema-args-specs.js +0 -48
  75. package/build/test/cli/schema-e2e-specs.js +0 -47
  76. package/build/test/config-e2e-specs.js +0 -112
  77. package/build/test/config-file-e2e-specs.js +0 -191
  78. package/build/test/config-file-specs.js +0 -281
  79. package/build/test/config-specs.js +0 -258
  80. package/build/test/driver-e2e-specs.js +0 -435
  81. package/build/test/driver-specs.js +0 -386
  82. package/build/test/ext-config-io-specs.js +0 -181
  83. package/build/test/extension-config-specs.js +0 -365
  84. package/build/test/fixtures/allow-feat.txt +0 -5
  85. package/build/test/fixtures/caps.json +0 -3
  86. package/build/test/fixtures/config/allow-insecure.txt +0 -3
  87. package/build/test/fixtures/config/appium.config.bad-nodeconfig.json +0 -5
  88. package/build/test/fixtures/config/appium.config.bad.json +0 -32
  89. package/build/test/fixtures/config/appium.config.ext-good.json +0 -9
  90. package/build/test/fixtures/config/appium.config.ext-unknown-props.json +0 -10
  91. package/build/test/fixtures/config/appium.config.good.js +0 -40
  92. package/build/test/fixtures/config/appium.config.good.json +0 -33
  93. package/build/test/fixtures/config/appium.config.good.yaml +0 -30
  94. package/build/test/fixtures/config/appium.config.invalid.json +0 -31
  95. package/build/test/fixtures/config/appium.config.security-array.json +0 -5
  96. package/build/test/fixtures/config/appium.config.security-delimited.json +0 -5
  97. package/build/test/fixtures/config/appium.config.security-path.json +0 -5
  98. package/build/test/fixtures/config/driver-fake.config.json +0 -8
  99. package/build/test/fixtures/config/nodeconfig.json +0 -3
  100. package/build/test/fixtures/config/plugin-fake.config.json +0 -0
  101. package/build/test/fixtures/default-args.js +0 -35
  102. package/build/test/fixtures/deny-feat.txt +0 -5
  103. package/build/test/fixtures/driver.schema.js +0 -20
  104. package/build/test/fixtures/extensions.yaml +0 -27
  105. package/build/test/fixtures/flattened-schema.js +0 -532
  106. package/build/test/fixtures/plugin.schema.js +0 -20
  107. package/build/test/fixtures/schema-with-extensions.js +0 -28
  108. package/build/test/grid-register-specs.js +0 -74
  109. package/build/test/helpers.js +0 -75
  110. package/build/test/logger-specs.js +0 -76
  111. package/build/test/npm-specs.js +0 -20
  112. package/build/test/parser-specs.js +0 -319
  113. package/build/test/plugin-e2e-specs.js +0 -316
  114. package/build/test/schema/arg-spec-specs.js +0 -70
  115. package/build/test/schema/cli-args-specs.js +0 -408
  116. package/build/test/schema/schema-specs.js +0 -407
  117. package/build/test/utils-specs.js +0 -288
  118. package/lib/cli/npm.js +0 -251
  119. package/lib/driver-config.js +0 -101
  120. package/lib/drivers.js +0 -84
  121. package/lib/ext-config-io.js +0 -287
  122. package/lib/extension-config.js +0 -366
  123. package/lib/plugin-config.js +0 -63
  124. package/lib/plugins.js +0 -13
@@ -0,0 +1,590 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Module containing {@link Manifest} which handles reading & writing of extension config files.
5
+ */
6
+
7
+ import { env, fs } from '@appium/support';
8
+ import _ from 'lodash';
9
+ import path from 'path';
10
+ import YAML from 'yaml';
11
+ import { 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
+
16
+ /**
17
+ * Default depth to search in directory tree for whatever it is we're looking for.
18
+ *
19
+ * It's 4 because smaller numbers didn't work.
20
+ */
21
+ const DEFAULT_SEARCH_DEPTH = 4;
22
+
23
+ /**
24
+ * Default options for {@link findExtensions}.
25
+ * @type {Readonly<import('klaw').Options>}
26
+ */
27
+ const DEFAULT_FIND_EXTENSIONS_OPTS = Object.freeze({
28
+ depthLimit: DEFAULT_SEARCH_DEPTH,
29
+ /* istanbul ignore next */
30
+ filter: (filepath) => !path.basename(filepath).startsWith('.'),
31
+ });
32
+
33
+ /**
34
+ * Current configuration schema revision!
35
+ */
36
+ const CONFIG_SCHEMA_REV = 2;
37
+
38
+ /**
39
+ * The name of the prop (`drivers`) used in `extensions.yaml` for drivers.
40
+ * @type {`${typeof DRIVER_TYPE}s`}
41
+ */
42
+ const CONFIG_DATA_DRIVER_KEY = `${DRIVER_TYPE}s`;
43
+
44
+ /**
45
+ * The name of the prop (`plugins`) used in `extensions.yaml` for plugins.
46
+ * @type {`${typeof PLUGIN_TYPE}s`}
47
+ */
48
+ const CONFIG_DATA_PLUGIN_KEY = `${PLUGIN_TYPE}s`;
49
+
50
+ /**
51
+ * @type {Readonly<ManifestData>}
52
+ */
53
+ const INITIAL_MANIFEST_DATA = Object.freeze({
54
+ [CONFIG_DATA_DRIVER_KEY]: Object.freeze({}),
55
+ [CONFIG_DATA_PLUGIN_KEY]: Object.freeze({}),
56
+ schemaRev: CONFIG_SCHEMA_REV,
57
+ });
58
+
59
+ /**
60
+ * Given a `package.json` return `true` if it represents an Appium Extension (either a driver or plugin).
61
+ *
62
+ * The `package.json` must have an `appium` property which is an object.
63
+ * @param {any} value
64
+ * @returns {value is ExtensionPackageJson<ExtensionType>}
65
+ */
66
+ function isExtension (value) {
67
+ return (
68
+ _.isPlainObject(value) &&
69
+ _.isPlainObject(value.appium) &&
70
+ _.isString(value.name) &&
71
+ _.isString(value.version)
72
+ );
73
+ }
74
+ /**
75
+ * Given a `package.json`, return `true` if it represents an Appium Driver.
76
+ *
77
+ * To be considered a driver, a `package.json` must have a fields
78
+ * `appium.driverName`, `appium.automationName` and `appium.platformNames`.
79
+ * @param {any} value - Value to test
80
+ * @returns {value is ExtensionPackageJson<DriverType>}
81
+ */
82
+ function isDriver (value) {
83
+ return (
84
+ isExtension(value) &&
85
+ _.isString(_.get(value, 'appium.driverName')) &&
86
+ _.isString(_.get(value, 'appium.automationName')) &&
87
+ _.isArray(_.get(value, 'appium.platformNames'))
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Given a `package.json`, return `true` if it represents an Appium Plugin.
93
+ *
94
+ * To be considered a plugin, a `package.json` must have an `appium.pluginName` field.
95
+ * @param {any} value - Value to test
96
+ * @returns {value is ExtensionPackageJson<PluginType>}
97
+ */
98
+ function isPlugin (value) {
99
+ return isExtension(value) && _.isString(_.get(value, 'appium.pluginName'));
100
+ }
101
+
102
+ /**
103
+ * Handles reading & writing of extension config files.
104
+ *
105
+ * Only one instance of this class exists per value of `APPIUM_HOME`.
106
+ */
107
+ export class Manifest {
108
+ /**
109
+ * The entire contents of a parsed YAML extension config file.
110
+ *
111
+ * Contains proxies for automatic persistence on disk
112
+ * @type {ManifestData}
113
+ * @private
114
+ */
115
+ _data;
116
+
117
+ /**
118
+ * Path to `APPIUM_HOME`.
119
+ * @private
120
+ * @type {Readonly<string>}
121
+ */
122
+ _appiumHome;
123
+
124
+ /**
125
+ * Path to `extensions.yaml`
126
+ * @type {string}
127
+ * Not set until {@link Manifest.read} is called.
128
+ */
129
+ _manifestPath;
130
+
131
+ /**
132
+ * Helps avoid writing multiple times.
133
+ *
134
+ * If this is `undefined`, calling {@link Manifest.write} will cause it to be
135
+ * set to a `Promise`. When the call to `write()` is complete, the `Promise`
136
+ * will resolve and then this value will be set to `undefined`. Concurrent calls
137
+ * made while this value is a `Promise` will return the `Promise` itself.
138
+ * @private
139
+ * @type {Promise<boolean>|undefined}
140
+ */
141
+ _writing;
142
+
143
+ /**
144
+ * Helps avoid reading multiple times.
145
+ *
146
+ * If this is `undefined`, calling {@link Manifest.read} will cause it to be
147
+ * set to a `Promise`. When the call to `read()` is complete, the `Promise`
148
+ * will resolve and then this value will be set to `undefined`. Concurrent calls
149
+ * made while this value is a `Promise` will return the `Promise` itself.
150
+ * @private
151
+ * @type {Promise<void>|undefined}
152
+ */
153
+ _reading;
154
+
155
+ /**
156
+ * Sets internal data to a fresh clone of {@link INITIAL_MANIFEST_DATA}
157
+ *
158
+ * Use {@link Manifest.getInstance} instead.
159
+ * @param {string} appiumHome
160
+ * @private
161
+ */
162
+ constructor (appiumHome) {
163
+ this._appiumHome = appiumHome;
164
+ this._data = _.cloneDeep(INITIAL_MANIFEST_DATA);
165
+ }
166
+
167
+ /**
168
+ * Returns a new or existing {@link Manifest} instance, based on the value of `appiumHome`.
169
+ *
170
+ * Maintains one instance per value of `appiumHome`.
171
+ * @param {string} appiumHome - Path to `APPIUM_HOME`
172
+ * @returns {Manifest}
173
+ */
174
+ static getInstance = _.memoize(function _getInstance (
175
+ appiumHome,
176
+ ) {
177
+ return new Manifest(appiumHome);
178
+ });
179
+
180
+ /**
181
+ * Searches `APPIUM_HOME` for installed extensions and adds them to the manifest.
182
+ * @param {SyncWithInstalledExtensionsOpts} opts
183
+ * @returns {Promise<boolean>} `true` if any extensions were added, `false` otherwise.
184
+ */
185
+ async syncWithInstalledExtensions ({depthLimit = DEFAULT_SEARCH_DEPTH} = {}) {
186
+ const walkOpts = _.defaults({depthLimit}, DEFAULT_FIND_EXTENSIONS_OPTS);
187
+ // this could be parallelized, but we can't use fs.walk as an async iterator
188
+ let didChange = false;
189
+ for await (const {stats, path: filepath} of fs.walk(
190
+ this._appiumHome,
191
+ walkOpts,
192
+ )) {
193
+ if (filepath !== this._appiumHome && stats.isDirectory()) {
194
+ try {
195
+ const pkg = await env.readPackageInDir(filepath);
196
+ if (pkg && isExtension(pkg)) {
197
+ // it's possible that this extension already exists in the manifest,
198
+ // so only update `didChange` if it's new.
199
+ const added = this.addExtensionFromPackage(
200
+ pkg,
201
+ path.join(filepath, 'package.json'),
202
+ );
203
+ didChange = didChange || added;
204
+ }
205
+ } catch {}
206
+ }
207
+ }
208
+ return didChange;
209
+ }
210
+
211
+ /**
212
+ * Returns `true` if driver with name `name` is registered.
213
+ * @param {string} name - Driver name
214
+ * @returns {boolean}
215
+ */
216
+ hasDriver (name) {
217
+ return Boolean(this._data.drivers[name]);
218
+ }
219
+
220
+ /**
221
+ * Returns `true` if plugin with name `name` is registered.
222
+ * @param {string} name - Plugin name
223
+ * @returns {boolean}
224
+ */
225
+ hasPlugin (name) {
226
+ return Boolean(this._data.plugins[name]);
227
+ }
228
+
229
+ /**
230
+ * Given a path to a `package.json`, add it as either a driver or plugin to the manifest.
231
+ *
232
+ * Will _not_ overwrite existing entries.
233
+ * @template {ExtensionType} ExtType
234
+ * @param {ExtensionPackageJson<ExtType>} pkgJson
235
+ * @param {string} pkgPath
236
+ * @returns {boolean} - `true` upon success, `false` if the extension is already registered.
237
+ */
238
+ addExtensionFromPackage (pkgJson, pkgPath) {
239
+ /**
240
+ * @type {InternalData}
241
+ */
242
+ const internal = {
243
+ pkgName: pkgJson.name,
244
+ version: pkgJson.version,
245
+ installType: INSTALL_TYPE_NPM,
246
+ installSpec: `${pkgJson.name}@${pkgJson.version}`,
247
+ };
248
+
249
+ if (isDriver(pkgJson)) {
250
+ if (!this.hasDriver(pkgJson.appium.driverName)) {
251
+ this.addExtension(DRIVER_TYPE, pkgJson.appium.driverName, {
252
+ ..._.omit(pkgJson.appium, 'driverName'),
253
+ ...internal
254
+ });
255
+ return true;
256
+ }
257
+ return false;
258
+ } else if (isPlugin(pkgJson)) {
259
+ if (!this.hasPlugin(pkgJson.appium.pluginName)) {
260
+ this.addExtension(PLUGIN_TYPE, pkgJson.appium.pluginName, {
261
+ ..._.omit(pkgJson.appium, 'pluginName'),
262
+ ...internal,
263
+ });
264
+ return true;
265
+ }
266
+ return false;
267
+ } else {
268
+ throw new TypeError(
269
+ `The extension in ${path.dirname(
270
+ pkgPath,
271
+ )} is neither a valid driver nor a valid plugin.`,
272
+ );
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Adds an extension to the manifest as was installed by the `appium` CLI. The
278
+ * `extData`, `extType`, and `extName` have already been determined.
279
+ *
280
+ * See {@link Manifest.addExtensionFromPackage} for adding an extension from an on-disk package.
281
+ * @template {ExtensionType} ExtType
282
+ * @param {ExtType} extType - `driver` or `plugin`
283
+ * @param {string} extName - Name of extension
284
+ * @param {ExtData<ExtType>} extData - Extension metadata
285
+ * @returns {void}
286
+ */
287
+ addExtension (extType, extName, extData) {
288
+ this._data[`${extType}s`][extName] = extData;
289
+ }
290
+
291
+ /**
292
+ * Returns the APPIUM_HOME path
293
+ */
294
+ get appiumHome () {
295
+ return this._appiumHome;
296
+ }
297
+
298
+ /**
299
+ * Returns the path to the manifest file
300
+ */
301
+ get manifestPath () {
302
+ return this._manifestPath;
303
+ }
304
+
305
+ /**
306
+ * Returns extension data for a particular type.
307
+ *
308
+ * @template {ExtensionType} ExtType
309
+ * @param {ExtType} extType
310
+ * @returns {ExtRecord<ExtType>}
311
+ */
312
+ getExtensionData (extType) {
313
+ return this._data[/** @type {string} */ (`${extType}s`)];
314
+ }
315
+
316
+ /**
317
+ * Reads manifest from disk and _overwrites_ the internal data.
318
+ *
319
+ * If the manifest does not exist on disk, an {@link INITIAL_MANIFEST_DATA "empty"} manifest file will be created.
320
+ *
321
+ * 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.
322
+ *
323
+ * Only one read operation should happen at a time. This is controlled via the {@link Manifest._reading} property.
324
+ * @returns {Promise<ManifestData>} The data
325
+ */
326
+ async read () {
327
+ if (this._reading) {
328
+ await this._reading;
329
+ return this._data;
330
+ }
331
+
332
+ this._reading = (async () => {
333
+ /** @type {ManifestData} */
334
+ let data;
335
+ let isNewFile = false;
336
+ await this._setManifestPath();
337
+ try {
338
+ log.debug(`Reading ${this._manifestPath}...`);
339
+ const yaml = await fs.readFile(this._manifestPath, 'utf8');
340
+ data = YAML.parse(yaml);
341
+ } catch (err) {
342
+ if (err.code === 'ENOENT') {
343
+ data = _.cloneDeep(INITIAL_MANIFEST_DATA);
344
+ isNewFile = true;
345
+ } else {
346
+ if (this._manifestPath) {
347
+ throw new Error(
348
+ `Appium had trouble loading the extension installation ` +
349
+ `cache file (${this._manifestPath}). It may be invalid YAML. Specific error: ${err.message}`,
350
+ );
351
+ } else {
352
+ throw new Error(
353
+ `Appium encountered an unknown problem. Specific error: ${err.message}`,
354
+ );
355
+ }
356
+ }
357
+ }
358
+
359
+ this._data = data;
360
+ let installedExtensionsChanged = false;
361
+ if (
362
+ await env.hasAppiumDependency(this.appiumHome) &&
363
+ (await packageDidChange(this.appiumHome))
364
+ ) {
365
+ installedExtensionsChanged = await this.syncWithInstalledExtensions();
366
+ }
367
+
368
+ if (isNewFile || installedExtensionsChanged) {
369
+ await this.write();
370
+ }
371
+ })();
372
+ try {
373
+ await this._reading;
374
+ return this._data;
375
+ } finally {
376
+ this._reading = undefined;
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Ensures {@link Manifest._manifestPath} is set.
382
+ *
383
+ * Creates the directory if necessary.
384
+ * @private
385
+ * @returns {Promise<string>}
386
+ */
387
+ async _setManifestPath () {
388
+ if (!this._manifestPath) {
389
+ this._manifestPath = await env.resolveManifestPath(this._appiumHome);
390
+
391
+ /* istanbul ignore if */
392
+ if (path.relative(this._appiumHome, this._manifestPath).startsWith('.')) {
393
+ throw new Error(
394
+ `Mismatch between location of APPIUM_HOME and manifest file. APPIUM_HOME: ${this.appiumHome}, manifest file: ${this._manifestPath}`,
395
+ );
396
+ }
397
+ }
398
+
399
+ return this._manifestPath;
400
+ }
401
+
402
+ /**
403
+ * Writes the data if it need s writing.
404
+ *
405
+ * If the `schemaRev` prop needs updating, the file will be written.
406
+ *
407
+ * @todo If this becomes too much of a bottleneck, throttle it.
408
+ * @returns {Promise<boolean>} Whether the data was written
409
+ */
410
+ async write () {
411
+ if (this._writing) {
412
+ return this._writing;
413
+ }
414
+ this._writing = (async () => {
415
+ await this._setManifestPath();
416
+ try {
417
+ await fs.mkdirp(path.dirname(this._manifestPath));
418
+ } catch (err) {
419
+ throw new Error(
420
+ `Appium could not create the directory for the manifest file: ${path.dirname(
421
+ this._manifestPath,
422
+ )}. Original error: ${err.message}`,
423
+ );
424
+ }
425
+ try {
426
+ await fs.writeFile(
427
+ this._manifestPath,
428
+ YAML.stringify(this._data),
429
+ 'utf8',
430
+ );
431
+ return true;
432
+ } catch (err) {
433
+ throw new Error(
434
+ `Appium could not write to manifest at ${this._manifestPath} using APPIUM_HOME ${this._appiumHome}. ` +
435
+ `Please ensure it is writable. Original error: ${err.message}`,
436
+ );
437
+ }
438
+ })();
439
+ try {
440
+ return await this._writing;
441
+ } finally {
442
+ this._writing = undefined;
443
+ }
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Either `driver` or `plugin` rn
449
+ * @typedef {typeof DRIVER_TYPE | typeof PLUGIN_TYPE} ExtensionType
450
+ */
451
+
452
+ /**
453
+ * Represents an entire YAML manifest (`extensions.yaml`)
454
+ * @typedef ManifestData
455
+ * @property {ExtRecord<DriverType>} drivers - Record of drivers, keyed by name
456
+ * @property {ExtRecord<PluginType>} plugins - Record of plugins, keyed by name
457
+ * @property {number} [schemaRev] - The schema revision of the manifest
458
+ */
459
+
460
+ /**
461
+ * Combination of external + internal extension data with `driverName`/`pluginName` removed (it becomes a key in an {@link ExtRecord} object).
462
+ * @template {ExtensionType} ExtType
463
+ * @typedef {(Omit<ExternalData<ExtType>, ExtType extends DriverType ? 'driverName' : 'pluginName'>) & InternalData & CommonData} ExtensionManifest
464
+ */
465
+
466
+ /**
467
+ * Manifest extension data which is _not_ provided in `package.json`. It may be derived
468
+ * (e.g., `installSpec`) or copied from elsewhere in a `package.json` (e.g.,
469
+ * `version`).
470
+ * @typedef InternalData
471
+ * @property {string} pkgName - Name of package (e.g., `appium-xcuitest-driver`)
472
+ * @property {string} version - Version of package
473
+ * @property {import('./extension-config').InstallType} installType - Install type
474
+ * @property {string} installSpec - Whatever the user typed as the extension to install. May be derived from `package.json`
475
+ */
476
+
477
+ /**
478
+ * Convert external (`package.json`) extension data into manifest data
479
+ * @typedef {ExtensionManifest<DriverType>} ManifestDriverData
480
+ */
481
+
482
+ /**
483
+ * Convert external (`package.json`) extension data into manifest data
484
+ * @typedef {ExtensionManifest<PluginType>} ManifestPluginData
485
+ */
486
+
487
+ /**
488
+ * Data points shared by all Appium extensions
489
+ * @typedef CommonData
490
+ * @property {string} mainClass - Name of main class in the extension
491
+ * @property {Record<string,string>} [scripts] - Collection of scripts which an extension may run
492
+ * @property {string | (import('ajv').SchemaObject & {[key: number]: never})} [schema] - Argument schema object
493
+ */
494
+
495
+ /**
496
+ * Driver-specific manifest data.
497
+ * @typedef DriverData
498
+ * @property {string} automationName - Automation engine to use
499
+ * @property {string[]} platformNames - Platforms to run on
500
+ * @property {string} driverName - Name of driver (_not_ the same as the package name, probably)
501
+ */
502
+
503
+ /**
504
+ * Plugin-specific manifest data.
505
+ * @typedef PluginData
506
+ * @property {string} pluginName - Name of plugin (_not_ the same as the package name, probably)
507
+ */
508
+
509
+ /**
510
+ * Generic type to refer to either {@link DriverData} or {@link PluginData}
511
+ * @template {ExtensionType} ExtType
512
+ * @typedef {CommonData & (ExtType extends DriverType ? DriverData : PluginData)} ExternalData
513
+ */
514
+
515
+ /**
516
+ * Main class/constructor of third-party plugin
517
+ *
518
+ * Referenced by {@link CommonData.mainClass}
519
+ * @typedef { {pluginName: string} & import('type-fest').Class<unknown> & ExtClassStaticMembers} PluginClass
520
+ */
521
+
522
+ /**
523
+ * Main class/constructor of third-party driver
524
+ *
525
+ * Referenced by {@link CommonData.mainClass}
526
+ * @typedef { {driverName: string} & import('type-fest').Class<unknown> & ExtClassStaticMembers } DriverClass
527
+ */
528
+
529
+ /**
530
+ * @typedef ExtClassStaticMembers
531
+ * @property {UpdateServerFn} [updateServer]
532
+ * @property {import('@appium/base-driver').MethodMap} [newMethodMap]
533
+ */
534
+
535
+ /**
536
+ * @callback UpdateServerFn
537
+ * @param {import('express').Express} app - Express app
538
+ * @param {import('http').Server} httpServer - HTTP server
539
+ * @returns {import('type-fest').Promisable<void>}
540
+ */
541
+ /**
542
+ * Generic type for an object keyed by extension name, with values of type {@link ExtData}
543
+ * @template {ExtensionType} ExtType
544
+ * @typedef {Record<string,ExtData<ExtType>>} ExtRecord
545
+ */
546
+
547
+ /**
548
+ * Generic type to refer to the data in an {@link ExtRecord}; this is the data for each extension in `extensions.yaml`.
549
+ * @template {ExtensionType} ExtType
550
+ * @typedef {ExtensionManifest<ExtType>} ExtData
551
+ */
552
+
553
+ /**
554
+ * Like {@link ExtData} except it _for sure_ has a `schema` property.
555
+ * @template {ExtensionType} ExtType
556
+ * @typedef {(ExtensionManifest<ExtType>) & {schema: import('ajv').SchemaObject|string} } ExtDataWithSchema
557
+ */
558
+
559
+ /**
560
+ * Generic type to refer to the main class constructor of an extension
561
+ * @template {ExtensionType} ExtType
562
+ * @typedef {ExtType extends DriverType ? DriverClass : PluginClass} ExtClass
563
+ */
564
+
565
+ /**
566
+ * Generic type for the key of an {@link ExtRecord} which corresponds to an extension name.
567
+ * @template {ExtensionType} ExtType
568
+ * @typedef {keyof ExtRecord<ExtType>} ExtName
569
+ */
570
+
571
+ /**
572
+ * Type of the string referring to a driver (typically as a key or type string)
573
+ * @typedef {typeof import('../constants').DRIVER_TYPE} DriverType
574
+ */
575
+
576
+ /**
577
+ * Type of the string referring to a plugin (typically as a key or type string)
578
+ * @typedef {typeof import('../constants').PLUGIN_TYPE} PluginType
579
+ */
580
+
581
+ /**
582
+ * A `package.json` containing extension data.
583
+ * @template {ExtensionType} ExtType
584
+ * @typedef {import('type-fest').SetRequired<import('type-fest').PackageJson, 'name' | 'version'> & {appium: ExternalData<ExtType>} } ExtensionPackageJson
585
+ */
586
+
587
+ /**
588
+ * @typedef SyncWithInstalledExtensionsOpts
589
+ * @property {number} [depthLimit] - Maximum depth to recurse into subdirectories
590
+ */
@@ -0,0 +1,64 @@
1
+ // @ts-check
2
+
3
+ import { fs } from '@appium/support';
4
+ import { isPackageChanged } from 'package-changed';
5
+ import path from 'path';
6
+ import { PKG_HASHFILE_RELATIVE_PATH } from '../constants';
7
+ import log from '../logger';
8
+
9
+ /**
10
+ * Determines if extensions have changed, and updates a hash the `package.json` in `appiumHome` if so.
11
+ *
12
+ * If they have, we need to sync them with the `extensions.yaml` manifest.
13
+ *
14
+ * _Warning: this makes a blocking call to `writeFileSync`._
15
+ * @param {string} appiumHome
16
+ * @returns {Promise<boolean>} `true` if `package.json` `appiumHome` changed
17
+ */
18
+ export async function packageDidChange (appiumHome) {
19
+ const hashFilename = path.join(appiumHome, PKG_HASHFILE_RELATIVE_PATH);
20
+
21
+ // XXX: the types in `package-changed` seem to be wrong.
22
+
23
+ /** @type {boolean} */
24
+ let isChanged;
25
+ /** @type {() => void} */
26
+ let writeHash;
27
+ /** @type {string} */
28
+ let hash;
29
+ /** @type {string|undefined} */
30
+ let oldHash;
31
+
32
+ // first mkdirp the target dir.
33
+ const hashFilenameDir = path.dirname(hashFilename);
34
+ log.debug(`Creating hash file directory: ${hashFilenameDir}`);
35
+ try {
36
+ await fs.mkdirp(hashFilenameDir);
37
+ } catch (err) {
38
+ throw new Error(
39
+ `Appium could not create the directory for hash file: ${hashFilenameDir}. Original error: ${err.message}`,
40
+ );
41
+ }
42
+
43
+ try {
44
+ ({isChanged, writeHash, oldHash, hash} = await isPackageChanged({
45
+ cwd: appiumHome,
46
+ hashFilename: PKG_HASHFILE_RELATIVE_PATH,
47
+ }));
48
+ } catch {
49
+ return true;
50
+ }
51
+
52
+ if (isChanged) {
53
+ try {
54
+ writeHash();
55
+ log.debug(`Updated hash of ${appiumHome}/package.json from: ${oldHash ?? '(none)'} to: ${hash}`);
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
+ }