appium 2.0.0-beta.5 → 2.0.0-beta.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -63
- package/build/lib/appium.d.ts +226 -0
- package/build/lib/appium.d.ts.map +1 -0
- package/build/lib/appium.js +694 -439
- package/build/lib/appium.js.map +1 -0
- package/build/lib/cli/args.d.ts +17 -0
- package/build/lib/cli/args.d.ts.map +1 -0
- package/build/lib/cli/args.js +253 -319
- package/build/lib/cli/args.js.map +1 -0
- package/build/lib/cli/driver-command.d.ts +102 -0
- package/build/lib/cli/driver-command.d.ts.map +1 -0
- package/build/lib/cli/driver-command.js +126 -81
- package/build/lib/cli/driver-command.js.map +1 -0
- package/build/lib/cli/extension-command.d.ts +385 -0
- package/build/lib/cli/extension-command.d.ts.map +1 -0
- package/build/lib/cli/extension-command.js +731 -383
- package/build/lib/cli/extension-command.js.map +1 -0
- package/build/lib/cli/extension.d.ts +23 -0
- package/build/lib/cli/extension.d.ts.map +1 -0
- package/build/lib/cli/extension.js +71 -54
- package/build/lib/cli/extension.js.map +1 -0
- package/build/lib/cli/parser.d.ts +84 -0
- package/build/lib/cli/parser.d.ts.map +1 -0
- package/build/lib/cli/parser.js +240 -128
- package/build/lib/cli/parser.js.map +1 -0
- package/build/lib/cli/plugin-command.d.ts +99 -0
- package/build/lib/cli/plugin-command.d.ts.map +1 -0
- package/build/lib/cli/plugin-command.js +120 -81
- package/build/lib/cli/plugin-command.js.map +1 -0
- package/build/lib/cli/utils.d.ts +29 -0
- package/build/lib/cli/utils.d.ts.map +1 -0
- package/build/lib/cli/utils.js +72 -51
- package/build/lib/cli/utils.js.map +1 -0
- package/build/lib/config-file.d.ts +100 -0
- package/build/lib/config-file.d.ts.map +1 -0
- package/build/lib/config-file.js +207 -0
- package/build/lib/config-file.js.map +1 -0
- package/build/lib/config.d.ts +49 -0
- package/build/lib/config.d.ts.map +1 -0
- package/build/lib/config.js +265 -201
- package/build/lib/config.js.map +1 -0
- package/build/lib/constants.d.ts +49 -0
- package/build/lib/constants.d.ts.map +1 -0
- package/build/lib/constants.js +66 -0
- package/build/lib/constants.js.map +1 -0
- package/build/lib/extension/driver-config.d.ts +82 -0
- package/build/lib/extension/driver-config.d.ts.map +1 -0
- package/build/lib/extension/driver-config.js +212 -0
- package/build/lib/extension/driver-config.js.map +1 -0
- package/build/lib/extension/extension-config.d.ts +249 -0
- package/build/lib/extension/extension-config.d.ts.map +1 -0
- package/build/lib/extension/extension-config.js +581 -0
- package/build/lib/extension/extension-config.js.map +1 -0
- package/build/lib/extension/index.d.ts +48 -0
- package/build/lib/extension/index.d.ts.map +1 -0
- package/build/lib/extension/index.js +105 -0
- package/build/lib/extension/index.js.map +1 -0
- package/build/lib/extension/manifest-migrations.d.ts +27 -0
- package/build/lib/extension/manifest-migrations.d.ts.map +1 -0
- package/build/lib/extension/manifest-migrations.js +118 -0
- package/build/lib/extension/manifest-migrations.js.map +1 -0
- package/build/lib/extension/manifest.d.ts +145 -0
- package/build/lib/extension/manifest.d.ts.map +1 -0
- package/build/lib/extension/manifest.js +521 -0
- package/build/lib/extension/manifest.js.map +1 -0
- package/build/lib/extension/package-changed.d.ts +11 -0
- package/build/lib/extension/package-changed.d.ts.map +1 -0
- package/build/lib/extension/package-changed.js +62 -0
- package/build/lib/extension/package-changed.js.map +1 -0
- package/build/lib/extension/plugin-config.d.ts +56 -0
- package/build/lib/extension/plugin-config.d.ts.map +1 -0
- package/build/lib/extension/plugin-config.js +102 -0
- package/build/lib/extension/plugin-config.js.map +1 -0
- package/build/lib/grid-register.d.ts +10 -0
- package/build/lib/grid-register.d.ts.map +1 -0
- package/build/lib/grid-register.js +122 -144
- package/build/lib/grid-register.js.map +1 -0
- package/build/lib/logger.d.ts +3 -0
- package/build/lib/logger.d.ts.map +1 -0
- package/build/lib/logger.js +5 -17
- package/build/lib/logger.js.map +1 -0
- package/build/lib/logsink.d.ts +4 -0
- package/build/lib/logsink.d.ts.map +1 -0
- package/build/lib/logsink.js +190 -187
- package/build/lib/logsink.js.map +1 -0
- package/build/lib/main.d.ts +62 -0
- package/build/lib/main.d.ts.map +1 -0
- package/build/lib/main.js +348 -228
- package/build/lib/main.js.map +1 -0
- package/build/lib/schema/arg-spec.d.ts +143 -0
- package/build/lib/schema/arg-spec.d.ts.map +1 -0
- package/build/lib/schema/arg-spec.js +164 -0
- package/build/lib/schema/arg-spec.js.map +1 -0
- package/build/lib/schema/cli-args.d.ts +19 -0
- package/build/lib/schema/cli-args.d.ts.map +1 -0
- package/build/lib/schema/cli-args.js +217 -0
- package/build/lib/schema/cli-args.js.map +1 -0
- package/build/lib/schema/cli-transformers.d.ts +5 -0
- package/build/lib/schema/cli-transformers.d.ts.map +1 -0
- package/build/lib/schema/cli-transformers.js +124 -0
- package/build/lib/schema/cli-transformers.js.map +1 -0
- package/build/lib/schema/index.d.ts +3 -0
- package/build/lib/schema/index.d.ts.map +1 -0
- package/build/lib/schema/index.js +19 -0
- package/build/lib/schema/index.js.map +1 -0
- package/build/lib/schema/keywords.d.ts +24 -0
- package/build/lib/schema/keywords.d.ts.map +1 -0
- package/build/lib/schema/keywords.js +128 -0
- package/build/lib/schema/keywords.js.map +1 -0
- package/build/lib/schema/schema.d.ts +259 -0
- package/build/lib/schema/schema.d.ts.map +1 -0
- package/build/lib/schema/schema.js +615 -0
- package/build/lib/schema/schema.js.map +1 -0
- package/build/lib/utils.d.ts +121 -0
- package/build/lib/utils.d.ts.map +1 -0
- package/build/lib/utils.js +351 -273
- package/build/lib/utils.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/build/types/cli.d.ts +134 -0
- package/build/types/cli.d.ts.map +1 -0
- package/build/types/cli.js +3 -0
- package/build/types/cli.js.map +1 -0
- package/build/types/index.d.ts +15 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +19 -0
- package/build/types/index.js.map +1 -0
- package/build/types/manifest/base.d.ts +135 -0
- package/build/types/manifest/base.d.ts.map +1 -0
- package/build/types/manifest/base.js +3 -0
- package/build/types/manifest/base.js.map +1 -0
- package/build/types/manifest/index.d.ts +19 -0
- package/build/types/manifest/index.d.ts.map +1 -0
- package/build/types/manifest/index.js +40 -0
- package/build/types/manifest/index.js.map +1 -0
- package/build/types/manifest/v3.d.ts +139 -0
- package/build/types/manifest/v3.d.ts.map +1 -0
- package/build/types/manifest/v3.js +3 -0
- package/build/types/manifest/v3.js.map +1 -0
- package/driver.d.ts +1 -0
- package/driver.js +14 -0
- package/index.js +11 -0
- package/lib/appium.js +535 -186
- package/lib/cli/args.js +267 -422
- package/lib/cli/driver-command.js +126 -23
- package/lib/cli/extension-command.js +679 -271
- package/lib/cli/extension.js +48 -17
- package/lib/cli/parser.js +263 -83
- package/lib/cli/plugin-command.js +115 -20
- package/lib/cli/utils.js +24 -10
- package/lib/config-file.js +220 -0
- package/lib/config.js +244 -110
- package/lib/constants.js +71 -0
- package/lib/extension/driver-config.js +250 -0
- package/lib/extension/extension-config.js +682 -0
- package/lib/extension/index.js +116 -0
- package/lib/extension/manifest-migrations.js +120 -0
- package/lib/extension/manifest.js +573 -0
- package/lib/extension/package-changed.js +64 -0
- package/lib/extension/plugin-config.js +112 -0
- package/lib/grid-register.js +49 -35
- package/lib/logger.js +1 -2
- package/lib/logsink.js +64 -38
- package/lib/main.js +321 -101
- package/lib/schema/arg-spec.js +229 -0
- package/lib/schema/cli-args.js +238 -0
- package/lib/schema/cli-transformers.js +119 -0
- package/lib/schema/index.js +2 -0
- package/lib/schema/keywords.js +136 -0
- package/lib/schema/schema.js +722 -0
- package/lib/utils.js +291 -167
- package/package.json +83 -84
- package/plugin.d.ts +1 -0
- package/plugin.js +13 -0
- package/scripts/autoinstall-extensions.js +237 -0
- package/support.d.ts +1 -0
- package/support.js +13 -0
- package/tsconfig.json +25 -0
- package/types/cli.ts +189 -0
- package/types/index.ts +20 -0
- package/types/manifest/README.md +30 -0
- package/types/manifest/base.ts +158 -0
- package/types/manifest/index.ts +27 -0
- package/types/manifest/v3.ts +161 -0
- package/CHANGELOG.md +0 -3515
- package/bin/ios-webkit-debug-proxy-launcher.js +0 -71
- package/build/lib/cli/npm.js +0 -208
- package/build/lib/cli/parser-helpers.js +0 -82
- package/build/lib/driver-config.js +0 -77
- package/build/lib/drivers.js +0 -96
- package/build/lib/extension-config.js +0 -253
- package/build/lib/plugin-config.js +0 -59
- package/build/lib/plugins.js +0 -14
- package/lib/cli/npm.js +0 -184
- package/lib/cli/parser-helpers.js +0 -79
- package/lib/driver-config.js +0 -46
- package/lib/drivers.js +0 -81
- package/lib/extension-config.js +0 -209
- package/lib/plugin-config.js +0 -34
- package/lib/plugins.js +0 -10
|
@@ -1,82 +1,131 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
-
|
|
2
|
+
import B from 'bluebird';
|
|
3
3
|
import _ from 'lodash';
|
|
4
|
-
import NPM from './npm';
|
|
5
4
|
import path from 'path';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
5
|
+
import {npm, util, env, console} from '@appium/support';
|
|
6
|
+
import {spinWith, RingBuffer} from './utils';
|
|
7
|
+
import {SubProcess} from 'teen_process';
|
|
8
|
+
import {
|
|
9
|
+
INSTALL_TYPE_NPM,
|
|
10
|
+
INSTALL_TYPE_GIT,
|
|
11
|
+
INSTALL_TYPE_GITHUB,
|
|
12
|
+
INSTALL_TYPE_LOCAL,
|
|
13
|
+
} from '../extension/extension-config';
|
|
14
|
+
import {packageDidChange} from '../extension/package-changed';
|
|
10
15
|
|
|
11
16
|
const UPDATE_ALL = 'installed';
|
|
12
17
|
|
|
13
18
|
class NotUpdatableError extends Error {}
|
|
14
19
|
class NoUpdatesAvailableError extends Error {}
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Omits `driverName`/`pluginName` props from the receipt to make a {@linkcode ExtManifest}
|
|
23
|
+
* @template {ExtensionType} ExtType
|
|
24
|
+
* @param {ExtInstallReceipt<ExtType>} receipt
|
|
25
|
+
* @returns {ExtManifest<ExtType>}
|
|
26
|
+
*/
|
|
27
|
+
function receiptToManifest(receipt) {
|
|
28
|
+
return /** @type {ExtManifest<ExtType>} */ (_.omit(receipt, 'driverName', 'pluginName'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @template {ExtensionType} ExtType
|
|
33
|
+
*/
|
|
34
|
+
class ExtensionCommand {
|
|
35
|
+
/**
|
|
36
|
+
* This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
|
|
37
|
+
* @type {ExtensionConfig<ExtType>}
|
|
38
|
+
*/
|
|
39
|
+
config;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* {@linkcode Record} of official plugins or drivers.
|
|
43
|
+
* @type {KnownExtensions<ExtType>}
|
|
44
|
+
*/
|
|
45
|
+
knownExtensions;
|
|
17
46
|
|
|
18
47
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
21
|
-
* @property {boolean} json - whether the output of this command should be JSON or text
|
|
22
|
-
* @property {string} type - DRIVER_TYPE or PLUGIN_TYPE
|
|
48
|
+
* If `true`, command output has been requested as JSON.
|
|
49
|
+
* @type {boolean}
|
|
23
50
|
*/
|
|
51
|
+
isJsonOutput;
|
|
24
52
|
|
|
25
53
|
/**
|
|
26
54
|
* Build an ExtensionCommand
|
|
27
|
-
*
|
|
28
|
-
* @param {ExtensionCommandConstructor} opts
|
|
29
|
-
* @return {ExtensionCommand}
|
|
55
|
+
* @param {ExtensionCommandOptions<ExtType>} opts
|
|
30
56
|
*/
|
|
31
|
-
constructor
|
|
57
|
+
constructor({config, json}) {
|
|
32
58
|
this.config = config;
|
|
33
|
-
this.
|
|
34
|
-
this.isJsonOutput = json;
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
this.log = new console.CliConsole({jsonMode: json});
|
|
60
|
+
this.isJsonOutput = Boolean(json);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `driver` or `plugin`, depending on the `ExtensionConfig`.
|
|
65
|
+
*/
|
|
66
|
+
get type() {
|
|
67
|
+
return this.config.extensionType;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Logs a message and returns an {@linkcode Error} to throw.
|
|
72
|
+
*
|
|
73
|
+
* For TS to understand that a function throws an exception, it must actually throw an exception--
|
|
74
|
+
* in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
|
|
75
|
+
* nor is something like `@returns {never}` which does not imply a thrown exception.
|
|
76
|
+
* @param {string} message
|
|
77
|
+
* @protected
|
|
78
|
+
* @throws {Error}
|
|
79
|
+
*/
|
|
80
|
+
_createFatalError(message) {
|
|
81
|
+
return new Error(this.log.decorate(message, 'error'));
|
|
37
82
|
}
|
|
38
83
|
|
|
39
84
|
/**
|
|
40
85
|
* Take a CLI parse and run an extension command based on its type
|
|
41
86
|
*
|
|
42
87
|
* @param {object} args - a key/value object with CLI flags and values
|
|
43
|
-
* @return {object} the result of the specific command which is executed
|
|
88
|
+
* @return {Promise<object>} the result of the specific command which is executed
|
|
44
89
|
*/
|
|
45
|
-
async execute
|
|
90
|
+
async execute(args) {
|
|
46
91
|
const cmd = args[`${this.type}Command`];
|
|
47
|
-
if (!_.isFunction(
|
|
48
|
-
throw
|
|
92
|
+
if (!_.isFunction(this[cmd])) {
|
|
93
|
+
throw this._createFatalError(`Cannot handle ${this.type} command ${cmd}`);
|
|
49
94
|
}
|
|
50
95
|
const executeCmd = this[cmd].bind(this);
|
|
51
96
|
return await executeCmd(args);
|
|
52
97
|
}
|
|
53
98
|
|
|
54
|
-
/**
|
|
55
|
-
* @typedef {Object} ListArgs
|
|
56
|
-
* @property {boolean} showInstalled - whether should show only installed extensions
|
|
57
|
-
* @property {boolean} showUpdates - whether should show available updates
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
99
|
/**
|
|
61
100
|
* List extensions
|
|
62
101
|
*
|
|
63
|
-
* @param {
|
|
64
|
-
* @return {
|
|
102
|
+
* @param {ListOptions} opts
|
|
103
|
+
* @return {Promise<ExtensionListData>} map of extension names to extension data
|
|
65
104
|
*/
|
|
66
|
-
async list
|
|
105
|
+
async list({showInstalled, showUpdates}) {
|
|
67
106
|
const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
|
|
68
107
|
const installedNames = Object.keys(this.config.installedExtensions);
|
|
69
108
|
const knownNames = Object.keys(this.knownExtensions);
|
|
70
|
-
const exts = [...installedNames, ...knownNames].reduce(
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
109
|
+
const exts = [...installedNames, ...knownNames].reduce(
|
|
110
|
+
(acc, name) => {
|
|
111
|
+
if (!acc[name]) {
|
|
112
|
+
if (installedNames.includes(name)) {
|
|
113
|
+
acc[name] = {
|
|
114
|
+
...this.config.installedExtensions[name],
|
|
115
|
+
installed: true,
|
|
116
|
+
};
|
|
117
|
+
} else if (!showInstalled) {
|
|
118
|
+
acc[name] = {pkgName: this.knownExtensions[name], installed: false};
|
|
119
|
+
}
|
|
76
120
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
121
|
+
return acc;
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
|
|
125
|
+
* {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
|
|
126
|
+
* @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
|
|
127
|
+
*/ ({})
|
|
128
|
+
);
|
|
80
129
|
|
|
81
130
|
// if we want to show whether updates are available, put that behind a spinner
|
|
82
131
|
await spinWith(this.isJsonOutput, lsMsg, async () => {
|
|
@@ -84,306 +133,417 @@ export default class ExtensionCommand {
|
|
|
84
133
|
return;
|
|
85
134
|
}
|
|
86
135
|
for (const [ext, data] of _.toPairs(exts)) {
|
|
87
|
-
|
|
88
|
-
if (!installed || installType !== INSTALL_TYPE_NPM) {
|
|
136
|
+
if (!data.installed || data.installType !== INSTALL_TYPE_NPM) {
|
|
89
137
|
// don't need to check for updates on exts that aren't installed
|
|
90
138
|
// also don't need to check for updates on non-npm exts
|
|
91
139
|
continue;
|
|
92
140
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
try {
|
|
142
|
+
const updates = await this.checkForExtensionUpdate(ext);
|
|
143
|
+
data.updateVersion = updates.safeUpdate;
|
|
144
|
+
data.unsafeUpdateVersion = updates.unsafeUpdate;
|
|
145
|
+
data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
data.updateError = e.message;
|
|
148
|
+
}
|
|
97
149
|
}
|
|
98
150
|
});
|
|
99
151
|
|
|
152
|
+
const listData = /** @type {ExtensionListData} */ (exts);
|
|
153
|
+
|
|
100
154
|
// if we're just getting the data, short circuit return here since we don't need to do any
|
|
101
155
|
// formatting logic
|
|
102
156
|
if (this.isJsonOutput) {
|
|
103
|
-
return
|
|
157
|
+
return listData;
|
|
104
158
|
}
|
|
105
159
|
|
|
106
|
-
for (const [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
160
|
+
for (const [name, data] of _.toPairs(listData)) {
|
|
161
|
+
let installTxt = ' [not installed]'.grey;
|
|
162
|
+
let updateTxt = '';
|
|
163
|
+
let upToDateTxt = '';
|
|
164
|
+
let unsafeUpdateTxt = '';
|
|
165
|
+
if (data.installed) {
|
|
166
|
+
const {
|
|
167
|
+
installType,
|
|
168
|
+
installSpec,
|
|
169
|
+
updateVersion,
|
|
170
|
+
unsafeUpdateVersion,
|
|
171
|
+
version,
|
|
172
|
+
upToDate,
|
|
173
|
+
updateError,
|
|
174
|
+
} = data;
|
|
175
|
+
let typeTxt;
|
|
176
|
+
switch (installType) {
|
|
177
|
+
case INSTALL_TYPE_GIT:
|
|
178
|
+
case INSTALL_TYPE_GITHUB:
|
|
179
|
+
typeTxt = `(cloned from ${installSpec})`.yellow;
|
|
180
|
+
break;
|
|
181
|
+
case INSTALL_TYPE_LOCAL:
|
|
182
|
+
typeTxt = `(linked from ${installSpec})`.magenta;
|
|
183
|
+
break;
|
|
184
|
+
default:
|
|
185
|
+
typeTxt = '(NPM)';
|
|
186
|
+
}
|
|
187
|
+
installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
|
|
188
|
+
|
|
189
|
+
if (showUpdates) {
|
|
190
|
+
if (updateError) {
|
|
191
|
+
updateTxt = ` [Cannot check for updates: ${updateError}]`.red;
|
|
192
|
+
} else {
|
|
193
|
+
if (updateVersion) {
|
|
194
|
+
updateTxt = ` [${updateVersion} available]`.magenta;
|
|
195
|
+
}
|
|
196
|
+
if (upToDate) {
|
|
197
|
+
upToDateTxt = ` [Up to date]`.green;
|
|
198
|
+
}
|
|
199
|
+
if (unsafeUpdateVersion) {
|
|
200
|
+
unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
121
204
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
' [not installed]'.grey;
|
|
125
|
-
const updateTxt = showUpdates && updateVersion ?
|
|
126
|
-
` [${updateVersion} available]`.magenta :
|
|
127
|
-
'';
|
|
128
|
-
const upToDateTxt = showUpdates && upToDate ?
|
|
129
|
-
` [Up to date]`.green :
|
|
130
|
-
'';
|
|
131
|
-
const unsafeUpdateTxt = showUpdates && unsafeUpdateVersion ?
|
|
132
|
-
` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan :
|
|
133
|
-
'';
|
|
134
|
-
|
|
135
|
-
console.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
|
|
205
|
+
|
|
206
|
+
this.log.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
|
|
136
207
|
}
|
|
137
208
|
|
|
138
|
-
return
|
|
209
|
+
return listData;
|
|
139
210
|
}
|
|
140
211
|
|
|
141
|
-
/**
|
|
142
|
-
* @typedef {Object} InstallArgs
|
|
143
|
-
* @property {string} ext - the name or spec of an extension to install
|
|
144
|
-
* @property {string} installType - how to install this extension. One of the INSTALL_TYPES
|
|
145
|
-
* @property {string} [packageName] - for git/github installs, the extension node package name
|
|
146
|
-
*/
|
|
147
|
-
|
|
148
212
|
/**
|
|
149
213
|
* Install an extension
|
|
150
214
|
*
|
|
151
|
-
* @param {
|
|
152
|
-
* @return {
|
|
215
|
+
* @param {InstallOpts} opts
|
|
216
|
+
* @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
|
|
153
217
|
*/
|
|
154
|
-
async
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
let extData;
|
|
158
|
-
let installSpec = ext;
|
|
218
|
+
async _install({installSpec, installType, packageName}) {
|
|
219
|
+
/** @type {ExtInstallReceipt<ExtType>} */
|
|
220
|
+
let receipt;
|
|
159
221
|
|
|
160
222
|
if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
|
|
161
|
-
throw
|
|
223
|
+
throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
|
|
162
224
|
}
|
|
163
225
|
|
|
164
226
|
if (!packageName && [INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB].includes(installType)) {
|
|
165
|
-
throw
|
|
227
|
+
throw this._createFatalError(`When using --source=${installType}, must also use --package`);
|
|
166
228
|
}
|
|
167
229
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
230
|
+
/**
|
|
231
|
+
* @type {InstallViaNpmArgs}
|
|
232
|
+
*/
|
|
233
|
+
let installViaNpmOpts;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The probable (?) name of the extension derived from the install spec.
|
|
237
|
+
*
|
|
238
|
+
* If using a local install type, this will remain empty.
|
|
239
|
+
* @type {string}
|
|
240
|
+
*/
|
|
241
|
+
let probableExtName = '';
|
|
242
|
+
|
|
243
|
+
// depending on `installType`, build the options to pass into `installViaNpm`
|
|
244
|
+
if (installType === INSTALL_TYPE_GITHUB) {
|
|
176
245
|
if (installSpec.split('/').length !== 2) {
|
|
177
|
-
throw
|
|
178
|
-
|
|
246
|
+
throw this._createFatalError(
|
|
247
|
+
`Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
|
|
248
|
+
'it should be of the form <org>/<repo>'
|
|
249
|
+
);
|
|
179
250
|
}
|
|
180
|
-
|
|
251
|
+
installViaNpmOpts = {
|
|
252
|
+
installSpec,
|
|
253
|
+
installType,
|
|
254
|
+
pkgName: /** @type {string} */ (packageName),
|
|
255
|
+
};
|
|
256
|
+
probableExtName = installSpec;
|
|
181
257
|
} else if (installType === INSTALL_TYPE_GIT) {
|
|
182
258
|
// git urls can have '.git' at the end, but this is not necessary and would complicate the
|
|
183
259
|
// way we download and name directories, so we can just remove it
|
|
184
260
|
installSpec = installSpec.replace(/\.git$/, '');
|
|
185
|
-
|
|
261
|
+
installViaNpmOpts = {
|
|
262
|
+
installSpec,
|
|
263
|
+
installType,
|
|
264
|
+
pkgName: /** @type {string} */ (packageName),
|
|
265
|
+
};
|
|
266
|
+
probableExtName = installSpec;
|
|
186
267
|
} else {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// sign, so check for that. We also need to be careful that package names themselves can
|
|
191
|
-
// contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
|
|
192
|
-
let name, pkgVer;
|
|
193
|
-
const splits = installSpec.split('@');
|
|
194
|
-
if (installSpec[0] === '@') {
|
|
195
|
-
// this is the case where we have an npm org included in the package name
|
|
196
|
-
[name, pkgVer] = [`@${splits[1]}`, splits[2]];
|
|
268
|
+
let pkgName, pkgVer;
|
|
269
|
+
if (installType === INSTALL_TYPE_LOCAL) {
|
|
270
|
+
pkgName = path.isAbsolute(installSpec) ? installSpec : path.resolve(installSpec);
|
|
197
271
|
} else {
|
|
198
|
-
// this
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
272
|
+
// at this point we have either an npm package or an appium verified extension
|
|
273
|
+
// name or a local path. both of which will be installed via npm.
|
|
274
|
+
// extensions installed via npm can include versions or tags after the '@'
|
|
275
|
+
// sign, so check for that. We also need to be careful that package names themselves can
|
|
276
|
+
// contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
|
|
277
|
+
let name;
|
|
278
|
+
const splits = installSpec.split('@');
|
|
279
|
+
if (installSpec[0] === '@') {
|
|
280
|
+
// this is the case where we have an npm org included in the package name
|
|
281
|
+
[name, pkgVer] = [`@${splits[1]}`, splits[2]];
|
|
282
|
+
} else {
|
|
283
|
+
// this is the case without an npm org
|
|
284
|
+
[name, pkgVer] = splits;
|
|
285
|
+
}
|
|
202
286
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
287
|
+
if (installType === INSTALL_TYPE_NPM) {
|
|
288
|
+
// if we're installing a named package from npm, we don't need to check
|
|
289
|
+
// against the appium extension list; just use the installSpec as is
|
|
290
|
+
pkgName = name;
|
|
291
|
+
} else {
|
|
292
|
+
// if we're installing a named appium driver (like 'xcuitest') we need to
|
|
293
|
+
// dereference the actual npm package ('appiupm-xcuitest-driver'), so
|
|
294
|
+
// check it exists and get the correct package
|
|
295
|
+
const knownNames = Object.keys(this.knownExtensions);
|
|
296
|
+
if (!_.includes(knownNames, name)) {
|
|
297
|
+
const msg =
|
|
298
|
+
`Could not resolve ${this.type}; are you sure it's in the list ` +
|
|
299
|
+
`of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
|
|
300
|
+
throw this._createFatalError(msg);
|
|
301
|
+
}
|
|
302
|
+
probableExtName = name;
|
|
303
|
+
pkgName = this.knownExtensions[name];
|
|
304
|
+
// given that we'll use the install type in the driver json, store it as
|
|
305
|
+
// 'npm' now
|
|
306
|
+
installType = INSTALL_TYPE_NPM;
|
|
216
307
|
}
|
|
217
|
-
pkgName = this.knownExtensions[name];
|
|
218
|
-
// given that we'll use the install type in the driver json, store it as
|
|
219
|
-
// 'npm' now
|
|
220
|
-
installType = INSTALL_TYPE_NPM;
|
|
221
308
|
}
|
|
309
|
+
installViaNpmOpts = {installSpec, pkgName, pkgVer, installType};
|
|
310
|
+
}
|
|
222
311
|
|
|
223
|
-
|
|
312
|
+
// fail fast here if we can
|
|
313
|
+
if (probableExtName && this.config.isInstalled(probableExtName)) {
|
|
314
|
+
throw this._createFatalError(
|
|
315
|
+
`A ${this.type} named "${probableExtName}" is already installed. ` +
|
|
316
|
+
`Did you mean to update? Run "appium ${this.type} update". See ` +
|
|
317
|
+
`installed ${this.type}s with "appium ${this.type} list --installed".`
|
|
318
|
+
);
|
|
224
319
|
}
|
|
225
320
|
|
|
226
|
-
|
|
227
|
-
delete extData[`${this.type}Name`];
|
|
321
|
+
receipt = await this.installViaNpm(installViaNpmOpts);
|
|
228
322
|
|
|
323
|
+
// this _should_ be the same as `probablyExtName` as the one derived above unless
|
|
324
|
+
// install type is local.
|
|
325
|
+
/** @type {string} */
|
|
326
|
+
const extName = receipt[/** @type {string} */ (`${this.type}Name`)];
|
|
327
|
+
|
|
328
|
+
// check _a second time_ with the more-accurate extName
|
|
229
329
|
if (this.config.isInstalled(extName)) {
|
|
230
|
-
throw
|
|
231
|
-
|
|
232
|
-
|
|
330
|
+
throw this._createFatalError(
|
|
331
|
+
`A ${this.type} named "${extName}" is already installed. ` +
|
|
332
|
+
`Did you mean to update? Run "appium ${this.type} update". See ` +
|
|
333
|
+
`installed ${this.type}s with "appium ${this.type} list --installed".`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// this field does not exist as such in the manifest (it's used as a property name instead)
|
|
338
|
+
// so that's why it's being removed here.
|
|
339
|
+
/** @type {ExtManifest<ExtType>} */
|
|
340
|
+
const extManifest = receiptToManifest(receipt);
|
|
341
|
+
|
|
342
|
+
const [errors, warnings] = await B.all([
|
|
343
|
+
this.config.getProblems(extName, extManifest),
|
|
344
|
+
this.config.getWarnings(extName, extManifest),
|
|
345
|
+
]);
|
|
346
|
+
const errorMap = new Map([[extName, errors]]);
|
|
347
|
+
const warningMap = new Map([[extName, warnings]]);
|
|
348
|
+
const {errorSummaries, warningSummaries} = this.config.getValidationResultSummaries(
|
|
349
|
+
errorMap,
|
|
350
|
+
warningMap
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (!_.isEmpty(errorSummaries)) {
|
|
354
|
+
throw this._createFatalError(errorSummaries.join('\n'));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// note that we won't show any warnings if there were errors.
|
|
358
|
+
if (!_.isEmpty(warningSummaries)) {
|
|
359
|
+
this.log.warn(warningSummaries.join('\n'));
|
|
233
360
|
}
|
|
234
361
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
362
|
+
await this.config.addExtension(extName, extManifest);
|
|
363
|
+
|
|
364
|
+
// update the hash if we've changed the local `package.json`
|
|
365
|
+
if (await env.hasAppiumDependency(this.config.appiumHome)) {
|
|
366
|
+
await packageDidChange(this.config.appiumHome);
|
|
367
|
+
}
|
|
238
368
|
|
|
239
369
|
// log info for the user
|
|
240
|
-
log(this.
|
|
370
|
+
this.log.info(this.getPostInstallText({extName, extData: receipt}));
|
|
241
371
|
|
|
242
372
|
return this.config.installedExtensions;
|
|
243
373
|
}
|
|
244
374
|
|
|
245
|
-
/**
|
|
246
|
-
* @typedef {Object} InstallViaNpmArgs
|
|
247
|
-
* @property {string} ext - the name or spec of an extension to install
|
|
248
|
-
* @property {string} pkgName - the NPM package name of the extension
|
|
249
|
-
* @property {string} [pkgVer] - the specific version of the NPM package
|
|
250
|
-
*/
|
|
251
|
-
|
|
252
375
|
/**
|
|
253
376
|
* Install an extension via NPM
|
|
254
377
|
*
|
|
255
378
|
* @param {InstallViaNpmArgs} args
|
|
379
|
+
* @returns {Promise<ExtInstallReceipt<ExtType>>}
|
|
256
380
|
*/
|
|
257
|
-
async installViaNpm
|
|
381
|
+
async installViaNpm({installSpec, pkgName, pkgVer, installType}) {
|
|
258
382
|
const npmSpec = `${pkgName}${pkgVer ? '@' + pkgVer : ''}`;
|
|
259
|
-
const specMsg = npmSpec ===
|
|
260
|
-
const msg = `Installing '${
|
|
383
|
+
const specMsg = npmSpec === installSpec ? '' : ` using NPM install spec '${npmSpec}'`;
|
|
384
|
+
const msg = `Installing '${installSpec}'${specMsg}`;
|
|
261
385
|
try {
|
|
262
|
-
const
|
|
263
|
-
await
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return
|
|
386
|
+
const {pkg, path} = await spinWith(this.isJsonOutput, msg, async () => {
|
|
387
|
+
const {pkg, installPath: path} = await npm.installPackage(this.config.appiumHome, pkgName, {
|
|
388
|
+
pkgVer,
|
|
389
|
+
installType,
|
|
390
|
+
});
|
|
391
|
+
this.validatePackageJson(pkg, installSpec);
|
|
392
|
+
return {pkg, path};
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return this.getInstallationReceipt({
|
|
396
|
+
pkg,
|
|
397
|
+
installPath: path,
|
|
398
|
+
installType,
|
|
399
|
+
installSpec,
|
|
400
|
+
});
|
|
272
401
|
} catch (err) {
|
|
273
|
-
throw
|
|
402
|
+
throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
|
|
274
403
|
}
|
|
275
404
|
}
|
|
276
405
|
|
|
277
|
-
/**
|
|
278
|
-
* @typedef {Object} ExtensionArgs
|
|
279
|
-
* @property {string} extName - the name of an extension
|
|
280
|
-
* @property {object} extData - the data for an installed extension
|
|
281
|
-
*/
|
|
282
|
-
|
|
283
406
|
/**
|
|
284
407
|
* Get the text which should be displayed to the user after an extension has been installed. This
|
|
285
408
|
* is designed to be overridden by drivers/plugins with their own particular text.
|
|
286
409
|
*
|
|
287
410
|
* @param {ExtensionArgs} args
|
|
411
|
+
* @returns {string}
|
|
288
412
|
*/
|
|
289
|
-
|
|
290
|
-
|
|
413
|
+
// eslint-disable-next-line no-unused-vars
|
|
414
|
+
getPostInstallText(args) {
|
|
415
|
+
throw this._createFatalError('Must be implemented in final class');
|
|
291
416
|
}
|
|
292
417
|
|
|
293
418
|
/**
|
|
294
|
-
*
|
|
295
|
-
* 'appium' field in the JSON data. We need this information to e.g. determine which class to
|
|
296
|
-
* load as the main driver class, or to be able to detect incompatibilities between driver and
|
|
297
|
-
* appium versions.
|
|
419
|
+
* Once a package is installed on-disk, this gathers some necessary metadata for validation.
|
|
298
420
|
*
|
|
299
|
-
* @param {
|
|
300
|
-
*
|
|
421
|
+
* @param {GetInstallationReceiptOpts<ExtType>} opts
|
|
422
|
+
* @returns {ExtInstallReceipt<ExtType>}
|
|
301
423
|
*/
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
424
|
+
getInstallationReceipt({pkg, installPath, installType, installSpec}) {
|
|
425
|
+
const {appium, name, version, peerDependencies} = pkg;
|
|
426
|
+
|
|
427
|
+
/** @type {import('appium/types').InternalMetadata} */
|
|
428
|
+
const internal = {
|
|
429
|
+
pkgName: name,
|
|
430
|
+
version,
|
|
431
|
+
installType,
|
|
432
|
+
installSpec,
|
|
433
|
+
installPath,
|
|
434
|
+
appiumVersion: peerDependencies?.appium,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/** @type {ExtMetadata<ExtType>} */
|
|
438
|
+
const extMetadata = appium;
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
...internal,
|
|
442
|
+
...extMetadata,
|
|
443
|
+
};
|
|
311
444
|
}
|
|
312
445
|
|
|
313
446
|
/**
|
|
314
|
-
*
|
|
315
|
-
* presence and form of those fields on the package.json data, throwing an error if anything is
|
|
316
|
-
* amiss.
|
|
447
|
+
* Validates the _required_ root fields of an extension's `package.json` file.
|
|
317
448
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
449
|
+
* These required fields are:
|
|
450
|
+
* - `name`
|
|
451
|
+
* - `version`
|
|
452
|
+
* - `appium`
|
|
453
|
+
* @param {import('type-fest').PackageJson} pkg - `package.json` of extension
|
|
454
|
+
* @param {string} installSpec - Extension name/spec
|
|
455
|
+
* @throws {ReferenceError} If `package.json` has a missing or invalid field
|
|
456
|
+
* @returns {pkg is ExtPackageJson<ExtType>}
|
|
320
457
|
*/
|
|
321
|
-
|
|
322
|
-
|
|
458
|
+
validatePackageJson(pkg, installSpec) {
|
|
459
|
+
const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkg);
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
*
|
|
463
|
+
* @param {string} field
|
|
464
|
+
* @returns {ReferenceError}
|
|
465
|
+
*/
|
|
466
|
+
const createMissingFieldError = (field) =>
|
|
467
|
+
new ReferenceError(
|
|
468
|
+
`${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (!name) {
|
|
472
|
+
throw createMissingFieldError('name');
|
|
473
|
+
}
|
|
474
|
+
if (!version) {
|
|
475
|
+
throw createMissingFieldError('version');
|
|
476
|
+
}
|
|
477
|
+
if (!appium) {
|
|
478
|
+
throw createMissingFieldError('appium');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.validateExtensionFields(appium, installSpec);
|
|
482
|
+
|
|
483
|
+
return true;
|
|
323
484
|
}
|
|
324
485
|
|
|
325
486
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
487
|
+
* For any `package.json` fields which a particular type of extension requires, validate the
|
|
488
|
+
* presence and form of those fields on the `package.json` data, throwing an error if anything is
|
|
489
|
+
* amiss.
|
|
490
|
+
*
|
|
491
|
+
* @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
|
|
492
|
+
* @param {string} installSpec - Extension name/spec
|
|
328
493
|
*/
|
|
494
|
+
// eslint-disable-next-line no-unused-vars
|
|
495
|
+
validateExtensionFields(extMetadata, installSpec) {
|
|
496
|
+
throw this._createFatalError('Must be implemented in final class');
|
|
497
|
+
}
|
|
329
498
|
|
|
330
499
|
/**
|
|
331
|
-
* Uninstall an extension
|
|
500
|
+
* Uninstall an extension.
|
|
501
|
+
*
|
|
502
|
+
* First tries to do this via `npm uninstall`, but if that fails, just `rm -rf`'s the extension dir.
|
|
332
503
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
504
|
+
* Will only remove the extension from the manifest if it has been successfully removed.
|
|
505
|
+
*
|
|
506
|
+
* @param {UninstallOpts} opts
|
|
507
|
+
* @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data (without the extension just uninstalled)
|
|
335
508
|
*/
|
|
336
|
-
async
|
|
337
|
-
if (!this.config.isInstalled(
|
|
338
|
-
throw
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
await fs.rimraf(this.config.getInstallPath(ext));
|
|
342
|
-
} finally {
|
|
343
|
-
await this.config.removeExtension(ext);
|
|
509
|
+
async _uninstall({installSpec}) {
|
|
510
|
+
if (!this.config.isInstalled(installSpec)) {
|
|
511
|
+
throw this._createFatalError(
|
|
512
|
+
`Can't uninstall ${this.type} '${installSpec}'; it is not installed`
|
|
513
|
+
);
|
|
344
514
|
}
|
|
345
|
-
|
|
515
|
+
const pkgName = this.config.installedExtensions[installSpec].pkgName;
|
|
516
|
+
await npm.uninstallPackage(this.config.appiumHome, pkgName);
|
|
517
|
+
await this.config.removeExtension(installSpec);
|
|
518
|
+
this.log.ok(`Successfully uninstalled ${this.type} '${installSpec}'`.green);
|
|
346
519
|
return this.config.installedExtensions;
|
|
347
520
|
}
|
|
348
521
|
|
|
349
|
-
/**
|
|
350
|
-
* @typedef {Object} ExtensionUpdateOpts
|
|
351
|
-
* @property {string} ext - the name of the extension to update
|
|
352
|
-
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision
|
|
353
|
-
* boundaries
|
|
354
|
-
*/
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* @typedef {Object} UpdateReport
|
|
358
|
-
* @property {string} from - version updated from
|
|
359
|
-
* @property {string} to - version updated to
|
|
360
|
-
*/
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* @typedef {Object} ExtensionUpdateResult
|
|
364
|
-
* @property {Object} errors - map of ext names to error objects
|
|
365
|
-
* @property {Object} updates - map of ext names to {@link UpdateReport}s
|
|
366
|
-
*/
|
|
367
|
-
|
|
368
522
|
/**
|
|
369
523
|
* Attempt to update one or more drivers using NPM
|
|
370
524
|
*
|
|
371
525
|
* @param {ExtensionUpdateOpts} updateSpec
|
|
372
|
-
* @return {ExtensionUpdateResult}
|
|
526
|
+
* @return {Promise<ExtensionUpdateResult>}
|
|
373
527
|
*/
|
|
374
|
-
async
|
|
375
|
-
const shouldUpdateAll =
|
|
528
|
+
async _update({installSpec, unsafe}) {
|
|
529
|
+
const shouldUpdateAll = installSpec === UPDATE_ALL;
|
|
376
530
|
// if we're specifically requesting an update for an extension, make sure it's installed
|
|
377
|
-
if (!shouldUpdateAll && !this.config.isInstalled(
|
|
378
|
-
throw
|
|
531
|
+
if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
|
|
532
|
+
throw this._createFatalError(
|
|
533
|
+
`The ${this.type} "${installSpec}" was not installed, so can't be updated`
|
|
534
|
+
);
|
|
379
535
|
}
|
|
380
|
-
const extsToUpdate = shouldUpdateAll
|
|
536
|
+
const extsToUpdate = shouldUpdateAll
|
|
537
|
+
? Object.keys(this.config.installedExtensions)
|
|
538
|
+
: [installSpec];
|
|
381
539
|
|
|
382
540
|
// 'errors' will have ext names as keys and error objects as values
|
|
541
|
+
/** @type {Record<string,Error>} */
|
|
383
542
|
const errors = {};
|
|
384
543
|
|
|
385
544
|
// 'updates' will have ext names as keys and update objects as values, where an update
|
|
386
545
|
// object is of the form {from: versionString, to: versionString}
|
|
546
|
+
/** @type {Record<string,UpdateReport>} */
|
|
387
547
|
const updates = {};
|
|
388
548
|
|
|
389
549
|
for (const e of extsToUpdate) {
|
|
@@ -393,17 +553,23 @@ export default class ExtensionCommand {
|
|
|
393
553
|
throw new NotUpdatableError();
|
|
394
554
|
}
|
|
395
555
|
});
|
|
396
|
-
const update = await spinWith(
|
|
397
|
-
|
|
398
|
-
if
|
|
399
|
-
|
|
556
|
+
const update = await spinWith(
|
|
557
|
+
this.isJsonOutput,
|
|
558
|
+
`Checking if ${this.type} '${e}' needs an update`,
|
|
559
|
+
async () => {
|
|
560
|
+
const update = await this.checkForExtensionUpdate(e);
|
|
561
|
+
if (!(update.safeUpdate || update.unsafeUpdate)) {
|
|
562
|
+
throw new NoUpdatesAvailableError();
|
|
563
|
+
}
|
|
564
|
+
return update;
|
|
400
565
|
}
|
|
401
|
-
|
|
402
|
-
});
|
|
566
|
+
);
|
|
403
567
|
if (!unsafe && !update.safeUpdate) {
|
|
404
|
-
throw
|
|
405
|
-
|
|
406
|
-
|
|
568
|
+
throw this._createFatalError(
|
|
569
|
+
`The ${this.type} '${e}' has a major revision update ` +
|
|
570
|
+
`(${update.current} => ${update.unsafeUpdate}), which could include ` +
|
|
571
|
+
`breaking changes. If you want to apply this update, re-run with --unsafe`
|
|
572
|
+
);
|
|
407
573
|
}
|
|
408
574
|
const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
|
|
409
575
|
await spinWith(
|
|
@@ -417,47 +583,47 @@ export default class ExtensionCommand {
|
|
|
417
583
|
}
|
|
418
584
|
}
|
|
419
585
|
|
|
420
|
-
log(
|
|
586
|
+
this.log.info('Update report:');
|
|
587
|
+
|
|
421
588
|
for (const [e, update] of _.toPairs(updates)) {
|
|
422
|
-
log(
|
|
589
|
+
this.log.ok(` - ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
|
|
423
590
|
}
|
|
591
|
+
|
|
424
592
|
for (const [e, err] of _.toPairs(errors)) {
|
|
425
593
|
if (err instanceof NotUpdatableError) {
|
|
426
|
-
log(
|
|
427
|
-
|
|
594
|
+
this.log.warn(
|
|
595
|
+
` - '${e}' was not installed via npm, so we could not check ` + `for updates`.yellow
|
|
596
|
+
);
|
|
428
597
|
} else if (err instanceof NoUpdatesAvailableError) {
|
|
429
|
-
log(
|
|
598
|
+
this.log.info(` - '${e}' had no updates available`.yellow);
|
|
430
599
|
} else {
|
|
431
600
|
// otherwise, make it pop with red!
|
|
432
|
-
log(
|
|
601
|
+
this.log.error(` - '${e}' failed to update: ${err}`.red);
|
|
433
602
|
}
|
|
434
603
|
}
|
|
435
|
-
|
|
436
604
|
return {updates, errors};
|
|
437
605
|
}
|
|
438
606
|
|
|
439
|
-
/**
|
|
440
|
-
* @typedef PossibleUpdates
|
|
441
|
-
* @property {string} current - current version
|
|
442
|
-
* @property {string|null} safeUpdate - version we can safely update to if it exists, or null
|
|
443
|
-
* @property {string|null} unsafeUpdate - version we can unsafely update to if it exists, or null
|
|
444
|
-
*/
|
|
445
|
-
|
|
446
607
|
/**
|
|
447
608
|
* Given an extension name, figure out what its highest possible version upgrade is, and also the
|
|
448
609
|
* highest possible safe upgrade.
|
|
449
610
|
*
|
|
450
611
|
* @param {string} ext - name of extension
|
|
451
|
-
* @return {PossibleUpdates}
|
|
612
|
+
* @return {Promise<PossibleUpdates>}
|
|
452
613
|
*/
|
|
453
|
-
async checkForExtensionUpdate
|
|
614
|
+
async checkForExtensionUpdate(ext) {
|
|
454
615
|
// TODO decide how we want to handle beta versions?
|
|
455
616
|
// this is a helper method, 'ext' is assumed to already be installed here, and of the npm
|
|
456
617
|
// install type
|
|
457
618
|
const {version, pkgName} = this.config.installedExtensions[ext];
|
|
458
|
-
|
|
459
|
-
let
|
|
460
|
-
|
|
619
|
+
/** @type {string?} */
|
|
620
|
+
let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
|
|
621
|
+
let safeUpdate = await npm.getLatestSafeUpgradeVersion(
|
|
622
|
+
this.config.appiumHome,
|
|
623
|
+
pkgName,
|
|
624
|
+
version
|
|
625
|
+
);
|
|
626
|
+
if (unsafeUpdate !== null && !util.compareVersions(unsafeUpdate, '>', version)) {
|
|
461
627
|
// the latest version is not greater than the current version, so there's no possible update
|
|
462
628
|
unsafeUpdate = null;
|
|
463
629
|
safeUpdate = null;
|
|
@@ -477,13 +643,255 @@ export default class ExtensionCommand {
|
|
|
477
643
|
* Actually update an extension installed by NPM, using the NPM cli. And update the installation
|
|
478
644
|
* manifest.
|
|
479
645
|
*
|
|
480
|
-
* @param {string}
|
|
646
|
+
* @param {string} installSpec - name of extension to update
|
|
481
647
|
* @param {string} version - version string identifier to update extension to
|
|
648
|
+
* @returns {Promise<void>}
|
|
649
|
+
*/
|
|
650
|
+
async updateExtension(installSpec, version) {
|
|
651
|
+
const {pkgName, installType} = this.config.installedExtensions[installSpec];
|
|
652
|
+
const extData = await this.installViaNpm({
|
|
653
|
+
installSpec,
|
|
654
|
+
installType,
|
|
655
|
+
pkgName,
|
|
656
|
+
pkgVer: version,
|
|
657
|
+
});
|
|
658
|
+
delete extData[/** @type {string} */ (`${this.type}Name`)];
|
|
659
|
+
await this.config.updateExtension(installSpec, extData);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Runs a script cached inside the "scripts" field under "appium"
|
|
664
|
+
* inside of the driver/plugins "package.json" file. Will throw
|
|
665
|
+
* an error if the driver/plugin does not contain a "scripts" field
|
|
666
|
+
* underneath the "appium" field in its package.json, if the
|
|
667
|
+
* "scripts" field is not a plain object, or if the scriptName is
|
|
668
|
+
* not found within "scripts" object.
|
|
669
|
+
*
|
|
670
|
+
* @param {RunOptions} opts
|
|
671
|
+
* @return {Promise<RunOutput>}
|
|
482
672
|
*/
|
|
483
|
-
async
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
673
|
+
async _run({installSpec, scriptName, extraArgs = []}) {
|
|
674
|
+
if (!this.config.isInstalled(installSpec)) {
|
|
675
|
+
throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const extConfig = this.config.installedExtensions[installSpec];
|
|
679
|
+
|
|
680
|
+
// note: TS cannot understand that _.has() is a type guard
|
|
681
|
+
if (!('scripts' in extConfig)) {
|
|
682
|
+
throw this._createFatalError(
|
|
683
|
+
`The ${this.type} named '${installSpec}' does not contain the ` +
|
|
684
|
+
`"scripts" field underneath the "appium" field in its package.json`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const extScripts = extConfig.scripts;
|
|
689
|
+
|
|
690
|
+
if (!extScripts || !_.isPlainObject(extScripts)) {
|
|
691
|
+
throw this._createFatalError(
|
|
692
|
+
`The ${this.type} named '${installSpec}' "scripts" field must be a plain object`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (!(scriptName in extScripts)) {
|
|
697
|
+
throw this._createFatalError(
|
|
698
|
+
`The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const runner = new SubProcess(process.execPath, [extScripts[scriptName], ...extraArgs], {
|
|
703
|
+
cwd: this.config.getInstallPath(installSpec),
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const output = new RingBuffer(50);
|
|
707
|
+
|
|
708
|
+
runner.on('stream-line', (line) => {
|
|
709
|
+
output.enqueue(line);
|
|
710
|
+
this.log.log(line);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
await runner.start(0);
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
await runner.join();
|
|
717
|
+
this.log.ok(`${scriptName} successfully ran`.green);
|
|
718
|
+
return {output: output.getBuff()};
|
|
719
|
+
} catch (err) {
|
|
720
|
+
this.log.error(`Encountered an error when running '${scriptName}': ${err.message}`.red);
|
|
721
|
+
return {error: err.message, output: output.getBuff()};
|
|
722
|
+
}
|
|
488
723
|
}
|
|
489
724
|
}
|
|
725
|
+
|
|
726
|
+
export default ExtensionCommand;
|
|
727
|
+
export {ExtensionCommand};
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Options for the {@linkcode ExtensionCommand} constructor
|
|
731
|
+
* @template {ExtensionType} ExtType
|
|
732
|
+
* @typedef ExtensionCommandOptions
|
|
733
|
+
* @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
|
|
734
|
+
* @property {boolean} json - whether the output of this command should be JSON or text
|
|
735
|
+
*/
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Extra stuff about extensions; used indirectly by {@linkcode ExtensionCommand.list}.
|
|
739
|
+
*
|
|
740
|
+
* @typedef ExtensionMetadata
|
|
741
|
+
* @property {boolean} installed - If `true`, the extension is installed
|
|
742
|
+
* @property {string?} updateVersion - If the extension is installed, the version it can be updated to
|
|
743
|
+
* @property {string?} unsafeUpdateVersion - Same as above, but a major version bump
|
|
744
|
+
* @property {boolean} upToDate - If the extension is installed and the latest
|
|
745
|
+
* @property {string?} updateError - Update check error message (if present)
|
|
746
|
+
*/
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
|
750
|
+
* @typedef {import('@appium/types').DriverType} DriverType
|
|
751
|
+
* @typedef {import('@appium/types').PluginType} PluginType
|
|
752
|
+
*/
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* @template {ExtensionType} ExtType
|
|
756
|
+
* @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
|
|
757
|
+
*/
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* @template {ExtensionType} ExtType
|
|
761
|
+
* @typedef {import('../extension/extension-config').ExtensionConfig<ExtType>} ExtensionConfig
|
|
762
|
+
*/
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* @template {ExtensionType} ExtType
|
|
766
|
+
* @typedef {import('appium/types').ExtMetadata<ExtType>} ExtMetadata
|
|
767
|
+
*/
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* @template {ExtensionType} ExtType
|
|
771
|
+
* @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
|
|
772
|
+
*/
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* @template {ExtensionType} ExtType
|
|
776
|
+
* @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
|
|
777
|
+
*/
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* @template {ExtensionType} ExtType
|
|
781
|
+
* @typedef {import('appium/types').ExtInstallReceipt<ExtType>} ExtInstallReceipt
|
|
782
|
+
*/
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Possible return value for {@linkcode ExtensionCommand.list}
|
|
786
|
+
* @typedef {Partial<InstalledExtensionListData> & {pkgName: string, installed: false}} UninstalledExtensionListData
|
|
787
|
+
*/
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Possible return value for {@linkcode ExtensionCommand.list}
|
|
791
|
+
* @typedef {import('appium/types').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
|
|
792
|
+
*/
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Return value of {@linkcode ExtensionCommand.list}.
|
|
796
|
+
* @typedef {Record<string,InstalledExtensionListData|UninstalledExtensionListData>} ExtensionListData
|
|
797
|
+
*/
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Options for {@linkcode ExtensionCommand._run}.
|
|
801
|
+
* @typedef RunOptions
|
|
802
|
+
* @property {string} installSpec - name of the extension to run a script from
|
|
803
|
+
* @property {string} scriptName - name of the script to run
|
|
804
|
+
* @property {string[]} [extraArgs] - arguments to pass to the script
|
|
805
|
+
*/
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Return value of {@linkcode ExtensionCommand._run}
|
|
809
|
+
*
|
|
810
|
+
* @typedef RunOutput
|
|
811
|
+
* @property {string} [error] - error message if script ran unsuccessfully, otherwise undefined
|
|
812
|
+
* @property {string[]} output - script output
|
|
813
|
+
*/
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Options for {@linkcode ExtensionCommand._update}.
|
|
817
|
+
* @typedef ExtensionUpdateOpts
|
|
818
|
+
* @property {string} installSpec - the name of the extension to update
|
|
819
|
+
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
|
|
820
|
+
*/
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Return value of {@linkcode ExtensionCommand._update}.
|
|
824
|
+
* @typedef ExtensionUpdateResult
|
|
825
|
+
* @property {Record<string,Error>} errors - map of ext names to error objects
|
|
826
|
+
* @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
|
|
827
|
+
*/
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Part of result of {@linkcode ExtensionCommand._update}.
|
|
831
|
+
* @typedef UpdateReport
|
|
832
|
+
* @property {string} from - version the extension was updated from
|
|
833
|
+
* @property {string} to - version the extension was updated to
|
|
834
|
+
*/
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Options for {@linkcode ExtensionCommand._uninstall}.
|
|
838
|
+
* @typedef UninstallOpts
|
|
839
|
+
* @property {string} installSpec - the name or spec of an extension to uninstall
|
|
840
|
+
*/
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Used by {@linkcode ExtensionCommand.getPostInstallText}
|
|
844
|
+
* @typedef ExtensionArgs
|
|
845
|
+
* @property {string} extName - the name of an extension
|
|
846
|
+
* @property {object} extData - the data for an installed extension
|
|
847
|
+
*/
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Options for {@linkcode ExtensionCommand.installViaNpm}
|
|
851
|
+
* @typedef InstallViaNpmArgs
|
|
852
|
+
* @property {string} installSpec - the name or spec of an extension to install
|
|
853
|
+
* @property {string} pkgName - the NPM package name of the extension
|
|
854
|
+
* @property {import('appium/types').InstallType} installType - type of install
|
|
855
|
+
* @property {string} [pkgVer] - the specific version of the NPM package
|
|
856
|
+
*/
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Object returned by {@linkcode ExtensionCommand.checkForExtensionUpdate}
|
|
860
|
+
* @typedef PossibleUpdates
|
|
861
|
+
* @property {string} current - current version
|
|
862
|
+
* @property {string?} safeUpdate - version we can safely update to if it exists, or null
|
|
863
|
+
* @property {string?} unsafeUpdate - version we can unsafely update to if it exists, or null
|
|
864
|
+
*/
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Options for {@linkcode ExtensionCommand._install}
|
|
868
|
+
* @typedef InstallOpts
|
|
869
|
+
* @property {string} installSpec - the name or spec of an extension to install
|
|
870
|
+
* @property {InstallType} installType - how to install this extension. One of the INSTALL_TYPES
|
|
871
|
+
* @property {string} [packageName] - for git/github installs, the extension node package name
|
|
872
|
+
*/
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* @template {ExtensionType} ExtType
|
|
876
|
+
* @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
|
|
877
|
+
*/
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* @typedef ListOptions
|
|
881
|
+
* @property {boolean} showInstalled - whether should show only installed extensions
|
|
882
|
+
* @property {boolean} showUpdates - whether should show available updates
|
|
883
|
+
*/
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Opts for {@linkcode ExtensionCommand.getInstallationReceipt}
|
|
887
|
+
* @template {ExtensionType} ExtType
|
|
888
|
+
* @typedef GetInstallationReceiptOpts
|
|
889
|
+
* @property {string} installPath
|
|
890
|
+
* @property {string} installSpec
|
|
891
|
+
* @property {ExtPackageJson<ExtType>} pkg
|
|
892
|
+
* @property {InstallType} installType
|
|
893
|
+
*/
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* @typedef {import('appium/types').InstallType} InstallType
|
|
897
|
+
*/
|