appium 3.2.1 → 3.3.0
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/build/lib/cli/args.d.ts +16 -12
- package/build/lib/cli/args.d.ts.map +1 -1
- package/build/lib/cli/args.js +15 -35
- package/build/lib/cli/args.js.map +1 -1
- package/build/lib/cli/driver-command.d.ts +51 -93
- package/build/lib/cli/driver-command.d.ts.map +1 -1
- package/build/lib/cli/driver-command.js +11 -66
- package/build/lib/cli/driver-command.js.map +1 -1
- package/build/lib/cli/extension-command.d.ts +211 -415
- package/build/lib/cli/extension-command.d.ts.map +1 -1
- package/build/lib/cli/extension-command.js +384 -653
- package/build/lib/cli/extension-command.js.map +1 -1
- package/build/lib/cli/extension.d.ts +11 -16
- package/build/lib/cli/extension.d.ts.map +1 -1
- package/build/lib/cli/extension.js +10 -28
- package/build/lib/cli/extension.js.map +1 -1
- package/build/lib/cli/parser.d.ts +40 -69
- package/build/lib/cli/parser.d.ts.map +1 -1
- package/build/lib/cli/parser.js +24 -59
- package/build/lib/cli/parser.js.map +1 -1
- package/build/lib/cli/plugin-command.d.ts +50 -90
- package/build/lib/cli/plugin-command.d.ts.map +1 -1
- package/build/lib/cli/plugin-command.js +11 -63
- package/build/lib/cli/plugin-command.js.map +1 -1
- package/build/lib/cli/setup-command.d.ts +21 -26
- package/build/lib/cli/setup-command.d.ts.map +1 -1
- package/build/lib/cli/setup-command.js +13 -55
- package/build/lib/cli/setup-command.js.map +1 -1
- package/build/lib/cli/utils.d.ts +27 -29
- package/build/lib/cli/utils.d.ts.map +1 -1
- package/build/lib/cli/utils.js +29 -31
- package/build/lib/cli/utils.js.map +1 -1
- package/build/lib/config-file.d.ts +24 -67
- package/build/lib/config-file.d.ts.map +1 -1
- package/build/lib/config-file.js +56 -115
- package/build/lib/config-file.js.map +1 -1
- package/build/lib/config.d.ts +42 -44
- package/build/lib/config.d.ts.map +1 -1
- package/build/lib/config.js +75 -107
- package/build/lib/config.js.map +1 -1
- package/build/lib/constants.d.ts +23 -23
- package/build/lib/constants.d.ts.map +1 -1
- package/build/lib/constants.js +10 -15
- package/build/lib/constants.js.map +1 -1
- package/build/lib/doctor/doctor.d.ts +40 -57
- package/build/lib/doctor/doctor.d.ts.map +1 -1
- package/build/lib/doctor/doctor.js +29 -60
- package/build/lib/doctor/doctor.js.map +1 -1
- package/build/lib/grid-register.d.ts +32 -7
- package/build/lib/grid-register.d.ts.map +1 -1
- package/build/lib/grid-register.js +84 -48
- package/build/lib/grid-register.js.map +1 -1
- package/build/lib/logsink.d.ts +13 -22
- package/build/lib/logsink.d.ts.map +1 -1
- package/build/lib/logsink.js +48 -103
- package/build/lib/logsink.js.map +1 -1
- package/build/lib/main.js +1 -1
- package/build/lib/main.js.map +1 -1
- package/build/lib/schema/arg-spec.d.ts +32 -107
- package/build/lib/schema/arg-spec.d.ts.map +1 -1
- package/build/lib/schema/arg-spec.js +11 -107
- package/build/lib/schema/arg-spec.js.map +1 -1
- package/build/lib/schema/cli-args.d.ts +3 -15
- package/build/lib/schema/cli-args.d.ts.map +1 -1
- package/build/lib/schema/cli-args.js +15 -105
- package/build/lib/schema/cli-args.js.map +1 -1
- package/build/lib/schema/cli-transformers.d.ts +15 -12
- package/build/lib/schema/cli-transformers.d.ts.map +1 -1
- package/build/lib/schema/cli-transformers.js +15 -45
- package/build/lib/schema/cli-transformers.js.map +1 -1
- package/build/lib/schema/index.d.ts +2 -2
- package/build/lib/schema/index.d.ts.map +1 -1
- package/build/lib/schema/index.js.map +1 -1
- package/build/lib/schema/keywords.d.ts +12 -20
- package/build/lib/schema/keywords.d.ts.map +1 -1
- package/build/lib/schema/keywords.js +6 -51
- package/build/lib/schema/keywords.js.map +1 -1
- package/build/lib/schema/schema.d.ts +106 -231
- package/build/lib/schema/schema.d.ts.map +1 -1
- package/build/lib/schema/schema.js +75 -345
- package/build/lib/schema/schema.js.map +1 -1
- package/build/lib/utils.d.ts +59 -238
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +55 -207
- package/build/lib/utils.js.map +1 -1
- package/lib/cli/{args.js → args.ts} +40 -51
- package/lib/cli/driver-command.ts +122 -0
- package/lib/cli/{extension-command.js → extension-command.ts} +610 -689
- package/lib/cli/extension.ts +65 -0
- package/lib/cli/{parser.js → parser.ts} +48 -71
- package/lib/cli/plugin-command.ts +117 -0
- package/lib/cli/{setup-command.js → setup-command.ts} +57 -72
- package/lib/cli/utils.ts +97 -0
- package/lib/config-file.ts +212 -0
- package/lib/{config.js → config.ts} +129 -141
- package/lib/{constants.js → constants.ts} +30 -41
- package/lib/doctor/{doctor.js → doctor.ts} +81 -91
- package/lib/grid-register.ts +250 -0
- package/lib/{logsink.js → logsink.ts} +91 -137
- package/lib/main.js +1 -1
- package/lib/schema/arg-spec.ts +131 -0
- package/lib/schema/cli-args.ts +171 -0
- package/lib/schema/cli-transformers.ts +83 -0
- package/lib/schema/keywords.ts +96 -0
- package/lib/schema/schema.ts +449 -0
- package/lib/utils.ts +404 -0
- package/package.json +19 -20
- package/tsconfig.json +1 -1
- package/build/package.json +0 -99
- package/lib/cli/driver-command.js +0 -174
- package/lib/cli/extension.js +0 -74
- package/lib/cli/plugin-command.js +0 -164
- package/lib/cli/utils.js +0 -91
- package/lib/config-file.js +0 -228
- package/lib/grid-register.js +0 -146
- package/lib/schema/arg-spec.js +0 -229
- package/lib/schema/cli-args.js +0 -254
- package/lib/schema/cli-transformers.js +0 -113
- package/lib/schema/keywords.js +0 -136
- package/lib/schema/schema.js +0 -725
- package/lib/utils.js +0 -512
- /package/lib/schema/{index.js → index.ts} +0 -0
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import B from 'bluebird';
|
|
2
2
|
import _ from 'lodash';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import type {AppiumLogger, ExtensionType, IDoctorCheck} from '@appium/types';
|
|
5
|
+
import type {
|
|
6
|
+
ExtInstallReceipt as AppiumExtInstallReceipt,
|
|
7
|
+
ExtManifest as AppiumExtManifest,
|
|
8
|
+
ExtMetadata as AppiumExtMetadata,
|
|
9
|
+
ExtPackageJson as AppiumExtPackageJson,
|
|
10
|
+
ExtRecord as AppiumExtRecord,
|
|
11
|
+
InstallType,
|
|
12
|
+
} from 'appium/types';
|
|
13
|
+
import type {PackageJson} from 'type-fest';
|
|
14
|
+
import type {ExtensionConfig as BaseExtensionConfig} from '../extension/extension-config';
|
|
4
15
|
import {npm, util, env, console, fs, system} from '@appium/support';
|
|
5
16
|
import {spinWith, RingBuffer} from './utils';
|
|
6
17
|
import {
|
|
@@ -25,24 +36,73 @@ const MAX_CONCURRENT_REPO_FETCHES = 5;
|
|
|
25
36
|
class NotUpdatableError extends Error {}
|
|
26
37
|
class NoUpdatesAvailableError extends Error {}
|
|
27
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Options for the {@linkcode ExtensionCliCommand} constructor
|
|
41
|
+
*/
|
|
42
|
+
export type ExtensionCommandOptions<ExtType extends ExtensionType = ExtensionType> = {
|
|
43
|
+
config: ExtensionConfig<ExtType>;
|
|
44
|
+
json: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ExtensionConfig<ExtType extends ExtensionType = ExtensionType> = BaseExtensionConfig<ExtType>;
|
|
48
|
+
export type ExtRecord<ExtType extends ExtensionType = ExtensionType> = AppiumExtRecord<ExtType>;
|
|
49
|
+
export type ExtMetadata<ExtType extends ExtensionType = ExtensionType> = AppiumExtMetadata<ExtType>;
|
|
50
|
+
export type ExtManifest<ExtType extends ExtensionType = ExtensionType> = AppiumExtManifest<ExtType>;
|
|
51
|
+
export type ExtPackageJson<ExtType extends ExtensionType = ExtensionType> = AppiumExtPackageJson<ExtType>;
|
|
52
|
+
export type ExtInstallReceipt<ExtType extends ExtensionType = ExtensionType> =
|
|
53
|
+
AppiumExtInstallReceipt<ExtType>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extra stuff about extensions; used indirectly by {@linkcode ExtensionCliCommand.list}.
|
|
57
|
+
*/
|
|
58
|
+
export type ExtensionListMetadata = {
|
|
59
|
+
installed: boolean;
|
|
60
|
+
upToDate?: boolean;
|
|
61
|
+
updateVersion?: string | null;
|
|
62
|
+
unsafeUpdateVersion?: string | null;
|
|
63
|
+
updateError?: string;
|
|
64
|
+
devMode?: boolean;
|
|
65
|
+
repositoryUrl?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Possible return value for {@linkcode ExtensionCliCommand.list}
|
|
70
|
+
*/
|
|
71
|
+
export type ExtensionListData<ExtType extends ExtensionType = ExtensionType> = Partial<
|
|
72
|
+
ExtManifest<ExtType>
|
|
73
|
+
> &
|
|
74
|
+
Partial<ExtensionListMetadata>;
|
|
75
|
+
|
|
76
|
+
export type InstalledExtensionListData<ExtType extends ExtensionType = ExtensionType> = ExtManifest<ExtType> &
|
|
77
|
+
ExtensionListMetadata;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Return value of {@linkcode ExtensionCliCommand.list}.
|
|
81
|
+
*/
|
|
82
|
+
export type ExtensionList<ExtType extends ExtensionType = ExtensionType> = Record<
|
|
83
|
+
string,
|
|
84
|
+
ExtensionListData<ExtType>
|
|
85
|
+
>;
|
|
86
|
+
|
|
28
87
|
/**
|
|
29
88
|
* Omits `driverName`/`pluginName` props from the receipt to make a {@linkcode ExtManifest}
|
|
30
|
-
* @template {ExtensionType} ExtType
|
|
31
|
-
* @param {ExtInstallReceipt<ExtType>} receipt
|
|
32
|
-
* @returns {ExtManifest<ExtType>}
|
|
33
89
|
*/
|
|
34
|
-
function receiptToManifest(
|
|
35
|
-
|
|
90
|
+
function receiptToManifest<ExtType extends ExtensionType>(
|
|
91
|
+
receipt: ExtInstallReceipt<ExtType>
|
|
92
|
+
): ExtManifest<ExtType> {
|
|
93
|
+
return _.omit(receipt, 'driverName', 'pluginName') as ExtManifest<ExtType>;
|
|
36
94
|
}
|
|
37
95
|
|
|
38
96
|
/**
|
|
39
97
|
* Fetches the remote extension version requirements
|
|
40
98
|
*
|
|
41
|
-
* @param
|
|
42
|
-
* @param
|
|
43
|
-
* @returns {Promise<[string, string|null]>}
|
|
99
|
+
* @param pkgName Extension name
|
|
100
|
+
* @param [pkgVer] Extension version (if not provided then the latest is assumed)
|
|
44
101
|
*/
|
|
45
|
-
async function getRemoteExtensionVersionReq(
|
|
102
|
+
async function getRemoteExtensionVersionReq(
|
|
103
|
+
pkgName: string,
|
|
104
|
+
pkgVer?: string
|
|
105
|
+
): Promise<[string, string | null]> {
|
|
46
106
|
const allDeps = await npm.getPackageInfo(
|
|
47
107
|
`${pkgName}${pkgVer ? `@${pkgVer}` : ``}`,
|
|
48
108
|
['peerDependencies', 'dependencies']
|
|
@@ -52,33 +112,29 @@ async function getRemoteExtensionVersionReq(pkgName, pkgVer) {
|
|
|
52
112
|
return [npmPackage.version, requiredVersionPair ? requiredVersionPair[1] : null];
|
|
53
113
|
}
|
|
54
114
|
|
|
55
|
-
|
|
56
|
-
* @template {ExtensionType} ExtType
|
|
57
|
-
*/
|
|
58
|
-
class ExtensionCliCommand {
|
|
115
|
+
abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType> {
|
|
59
116
|
/**
|
|
60
117
|
* This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
|
|
61
|
-
* @type {ExtensionConfig<ExtType>}
|
|
62
118
|
*/
|
|
63
|
-
config
|
|
119
|
+
protected readonly config: ExtensionConfig<ExtType>;
|
|
64
120
|
|
|
65
121
|
/**
|
|
66
122
|
* {@linkcode Record} of official plugins or drivers.
|
|
67
|
-
* @type {KnownExtensions<ExtType>}
|
|
68
123
|
*/
|
|
69
|
-
knownExtensions
|
|
124
|
+
protected knownExtensions: Record<string, string>;
|
|
70
125
|
|
|
71
126
|
/**
|
|
72
127
|
* If `true`, command output has been requested as JSON.
|
|
73
|
-
* @type {boolean}
|
|
74
128
|
*/
|
|
75
|
-
isJsonOutput;
|
|
129
|
+
protected readonly isJsonOutput: boolean;
|
|
130
|
+
protected readonly log: any;
|
|
76
131
|
|
|
77
132
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
133
|
+
* Creates an extension command instance.
|
|
134
|
+
*
|
|
135
|
+
* @param opts - constructor options containing extension config and JSON mode
|
|
80
136
|
*/
|
|
81
|
-
constructor({config, json}) {
|
|
137
|
+
constructor({config, json}: ExtensionCommandOptions<ExtType>) {
|
|
82
138
|
this.config = config;
|
|
83
139
|
this.log = new console.CliConsole({jsonMode: json});
|
|
84
140
|
this.isJsonOutput = Boolean(json);
|
|
@@ -87,32 +143,17 @@ class ExtensionCliCommand {
|
|
|
87
143
|
/**
|
|
88
144
|
* `driver` or `plugin`, depending on the `ExtensionConfig`.
|
|
89
145
|
*/
|
|
90
|
-
get type() {
|
|
146
|
+
get type(): ExtensionType {
|
|
91
147
|
return this.config.extensionType;
|
|
92
148
|
}
|
|
93
149
|
|
|
94
150
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* For TS to understand that a function throws an exception, it must actually throw an exception--
|
|
98
|
-
* in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
|
|
99
|
-
* nor is something like `@returns {never}` which does not imply a thrown exception.
|
|
100
|
-
*
|
|
101
|
-
* @param {string} message
|
|
102
|
-
* @protected
|
|
103
|
-
* @throws {Error}
|
|
104
|
-
*/
|
|
105
|
-
_createFatalError(message) {
|
|
106
|
-
return new Error(this.log.decorate(message, 'error'));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Take a CLI parse and run an extension command based on its type
|
|
151
|
+
* Executes an extension subcommand from parsed CLI args.
|
|
111
152
|
*
|
|
112
|
-
* @param
|
|
113
|
-
* @
|
|
153
|
+
* @param args - parsed CLI argument object
|
|
154
|
+
* @returns result of the executed extension subcommand
|
|
114
155
|
*/
|
|
115
|
-
async execute(args) {
|
|
156
|
+
async execute(args: Record<string, any>): Promise<unknown> {
|
|
116
157
|
const cmd = args[`${this.type}Command`];
|
|
117
158
|
if (!_.isFunction(this[cmd])) {
|
|
118
159
|
throw this._createFatalError(`Cannot handle ${this.type} command ${cmd}`);
|
|
@@ -122,13 +163,12 @@ class ExtensionCliCommand {
|
|
|
122
163
|
}
|
|
123
164
|
|
|
124
165
|
/**
|
|
125
|
-
*
|
|
166
|
+
* Lists available/installed extensions and optional update metadata.
|
|
126
167
|
*
|
|
127
|
-
* @
|
|
128
|
-
* @
|
|
129
|
-
* @return {Promise<ExtensionList<ExtType>>} map of extension names to extension data
|
|
168
|
+
* @param opts - list command options
|
|
169
|
+
* @returns map of extension names to list data
|
|
130
170
|
*/
|
|
131
|
-
async list({showInstalled, showUpdates, verbose = false}) {
|
|
171
|
+
async list({showInstalled, showUpdates, verbose = false}: ListOptions): Promise<ExtensionList> {
|
|
132
172
|
const listData = this._buildListData(showInstalled);
|
|
133
173
|
|
|
134
174
|
const lsMsg =
|
|
@@ -151,294 +191,36 @@ class ExtensionCliCommand {
|
|
|
151
191
|
}
|
|
152
192
|
|
|
153
193
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* @param {boolean} showInstalled
|
|
158
|
-
* @returns {ExtensionList<ExtType>}
|
|
159
|
-
* @private
|
|
160
|
-
*/
|
|
161
|
-
_buildListData(showInstalled) {
|
|
162
|
-
const installedNames = Object.keys(this.config.installedExtensions);
|
|
163
|
-
const knownNames = Object.keys(this.knownExtensions);
|
|
164
|
-
return [...installedNames, ...knownNames].reduce((acc, name) => {
|
|
165
|
-
if (!acc[name]) {
|
|
166
|
-
if (installedNames.includes(name)) {
|
|
167
|
-
acc[name] = {
|
|
168
|
-
.../** @type {Partial<ExtManifest<ExtType>>} */ (this.config.installedExtensions[name]),
|
|
169
|
-
installed: true,
|
|
170
|
-
};
|
|
171
|
-
} else if (!showInstalled) {
|
|
172
|
-
acc[name] = /** @type {ExtensionListData<ExtType>} */ ({
|
|
173
|
-
pkgName: this.knownExtensions[name],
|
|
174
|
-
installed: false,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return acc;
|
|
179
|
-
}, /** @type {ExtensionList<ExtType>} */ ({}));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Check for available updates for installed extensions
|
|
184
|
-
*
|
|
185
|
-
* @template {ExtensionType} ExtType
|
|
186
|
-
* @param {ExtensionList<ExtType>} listData
|
|
187
|
-
* @param {boolean} showUpdates
|
|
188
|
-
* @param {string} lsMsg
|
|
189
|
-
* @returns {Promise<void>}
|
|
190
|
-
* @private
|
|
191
|
-
*/
|
|
192
|
-
async _checkForUpdates(listData, showUpdates, lsMsg) {
|
|
193
|
-
await spinWith(this.isJsonOutput, lsMsg, async () => {
|
|
194
|
-
// We'd like to still show lsMsg even if showUpdates is false
|
|
195
|
-
if (!showUpdates) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Filter to only extensions that need update checks (installed npm packages)
|
|
200
|
-
const extensionsToCheck = _.toPairs(listData).filter(
|
|
201
|
-
([, data]) => data.installed && data.installType === INSTALL_TYPE_NPM
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
await B.map(
|
|
205
|
-
extensionsToCheck,
|
|
206
|
-
async ([ext, data]) => {
|
|
207
|
-
try {
|
|
208
|
-
const updates = await this.checkForExtensionUpdate(ext);
|
|
209
|
-
data.updateVersion = updates.safeUpdate;
|
|
210
|
-
data.unsafeUpdateVersion = updates.unsafeUpdate;
|
|
211
|
-
data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
|
|
212
|
-
} catch (e) {
|
|
213
|
-
data.updateError = e.message;
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
{concurrency: MAX_CONCURRENT_REPO_FETCHES}
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Add repository URLs to list data for all extensions
|
|
223
|
-
*
|
|
224
|
-
* @template {ExtensionType} ExtType
|
|
225
|
-
* @param {ExtensionList<ExtType>} listData
|
|
226
|
-
* @returns {Promise<void>}
|
|
227
|
-
* @private
|
|
228
|
-
*/
|
|
229
|
-
async _addRepositoryUrlsToListData(listData) {
|
|
230
|
-
await spinWith(this.isJsonOutput, 'Fetching repository information', async () => {
|
|
231
|
-
await B.map(
|
|
232
|
-
_.values(listData),
|
|
233
|
-
async (data) => {
|
|
234
|
-
const repoUrl = await this._getRepositoryUrl(data);
|
|
235
|
-
if (repoUrl) {
|
|
236
|
-
data.repositoryUrl = repoUrl;
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
{concurrency: MAX_CONCURRENT_REPO_FETCHES}
|
|
240
|
-
);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Display normal formatted output
|
|
246
|
-
*
|
|
247
|
-
* @template {ExtensionType} ExtType
|
|
248
|
-
* @param {ExtensionList<ExtType>} listData
|
|
249
|
-
* @param {boolean} showUpdates
|
|
250
|
-
* @returns {Promise<ExtensionList<ExtType>>}
|
|
251
|
-
* @private
|
|
252
|
-
*/
|
|
253
|
-
async _displayNormalListOutput(listData, showUpdates) {
|
|
254
|
-
for (const [name, data] of _.toPairs(listData)) {
|
|
255
|
-
const line = await this._formatExtensionLine(name, data, showUpdates);
|
|
256
|
-
this.log.log(line);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return listData;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Format a single extension line for display
|
|
264
|
-
*
|
|
265
|
-
* @template {ExtensionType} ExtType
|
|
266
|
-
* @param {string} name
|
|
267
|
-
* @param {ExtensionListData<ExtType>} data
|
|
268
|
-
* @param {boolean} showUpdates
|
|
269
|
-
* @returns {Promise<string>}
|
|
270
|
-
* @private
|
|
271
|
-
*/
|
|
272
|
-
async _formatExtensionLine(name, data, showUpdates) {
|
|
273
|
-
if (data.installed) {
|
|
274
|
-
const installTxt = this._formatInstallText(/** @type {InstalledExtensionListData<ExtType>} */ (data));
|
|
275
|
-
const updateTxt = showUpdates ? this._formatUpdateText(/** @type {InstalledExtensionListData<ExtType>} */ (data)) : '';
|
|
276
|
-
return `- ${name.yellow}${installTxt}${updateTxt}`;
|
|
277
|
-
}
|
|
278
|
-
const installTxt = ' [not installed]'.grey;
|
|
279
|
-
return `- ${name.yellow}${installTxt}`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Format installation status text
|
|
284
|
-
*
|
|
285
|
-
* @template {ExtensionType} ExtType
|
|
286
|
-
* @param {InstalledExtensionListData<ExtType>} data
|
|
287
|
-
* @returns {string}
|
|
288
|
-
* @private
|
|
289
|
-
*/
|
|
290
|
-
_formatInstallText(data) {
|
|
291
|
-
const {installType, installSpec, version} = data;
|
|
292
|
-
let typeTxt;
|
|
293
|
-
switch (installType) {
|
|
294
|
-
case INSTALL_TYPE_GIT:
|
|
295
|
-
case INSTALL_TYPE_GITHUB:
|
|
296
|
-
typeTxt = `(cloned from ${installSpec})`.yellow;
|
|
297
|
-
break;
|
|
298
|
-
case INSTALL_TYPE_LOCAL:
|
|
299
|
-
typeTxt = `(linked from ${installSpec})`.magenta;
|
|
300
|
-
break;
|
|
301
|
-
case INSTALL_TYPE_DEV:
|
|
302
|
-
typeTxt = '(dev mode)';
|
|
303
|
-
break;
|
|
304
|
-
default:
|
|
305
|
-
typeTxt = '(npm)';
|
|
306
|
-
}
|
|
307
|
-
return `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Format update information text
|
|
312
|
-
*
|
|
313
|
-
* @template {ExtensionType} ExtType
|
|
314
|
-
* @param {InstalledExtensionListData<ExtType>} data
|
|
315
|
-
* @returns {string}
|
|
316
|
-
* @private
|
|
317
|
-
*/
|
|
318
|
-
_formatUpdateText(data) {
|
|
319
|
-
const {updateVersion, unsafeUpdateVersion, upToDate, updateError} = data;
|
|
320
|
-
if (updateError) {
|
|
321
|
-
return ` [Cannot check for updates: ${updateError}]`.red;
|
|
322
|
-
}
|
|
323
|
-
let txt = '';
|
|
324
|
-
if (updateVersion) {
|
|
325
|
-
txt += ` [${updateVersion} available]`.magenta;
|
|
326
|
-
}
|
|
327
|
-
if (upToDate) {
|
|
328
|
-
txt += ` [Up to date]`.green;
|
|
329
|
-
}
|
|
330
|
-
if (unsafeUpdateVersion) {
|
|
331
|
-
txt += ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
|
|
332
|
-
}
|
|
333
|
-
return txt;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Get repository URL from package data
|
|
338
|
-
*
|
|
339
|
-
* @template {ExtensionType} ExtType
|
|
340
|
-
* @param {ExtensionListData<ExtType>} data
|
|
341
|
-
* @returns {Promise<string|null>}
|
|
342
|
-
* @private
|
|
343
|
-
*/
|
|
344
|
-
async _getRepositoryUrl(data) {
|
|
345
|
-
if (data.installed && data.installPath) {
|
|
346
|
-
return await this._getRepositoryUrlFromInstalled(
|
|
347
|
-
/** @type {InstalledExtensionListData<ExtType>} */ (data)
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
if (data.pkgName && !data.installed) {
|
|
351
|
-
return await this._getRepositoryUrlFromNpm(data.pkgName);
|
|
352
|
-
}
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Get repository URL from installed extension's package.json
|
|
194
|
+
* For any `package.json` fields which a particular type of extension requires, validate the
|
|
195
|
+
* presence and form of those fields on the `package.json` data, throwing an error if anything is
|
|
196
|
+
* amiss.
|
|
358
197
|
*
|
|
359
|
-
* @
|
|
360
|
-
* @param
|
|
361
|
-
* @returns {Promise<string|null>}
|
|
362
|
-
* @private
|
|
198
|
+
* @param extMetadata - the data in the "appium" field of `package.json` for an extension
|
|
199
|
+
* @param installSpec - Extension name/spec
|
|
363
200
|
*/
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
369
|
-
if (pkg.repository) {
|
|
370
|
-
if (typeof pkg.repository === 'string') {
|
|
371
|
-
return pkg.repository;
|
|
372
|
-
}
|
|
373
|
-
if (pkg.repository.url) {
|
|
374
|
-
return pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
} catch {
|
|
379
|
-
// Ignore errors reading package.json
|
|
380
|
-
}
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
201
|
+
protected abstract validateExtensionFields(
|
|
202
|
+
extMetadata: ExtMetadata<ExtType>,
|
|
203
|
+
installSpec: string
|
|
204
|
+
): void;
|
|
383
205
|
|
|
384
206
|
/**
|
|
385
|
-
*
|
|
207
|
+
* Logs a message and returns an {@linkcode Error} to throw.
|
|
386
208
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*/
|
|
391
|
-
async _getRepositoryUrlFromNpm(pkgName) {
|
|
392
|
-
try {
|
|
393
|
-
const repoInfo = await npm.getPackageInfo(pkgName, ['repository']);
|
|
394
|
-
// When requesting only 'repository', npm.getPackageInfo returns the repository object directly
|
|
395
|
-
if (repoInfo) {
|
|
396
|
-
if (typeof repoInfo === 'string') {
|
|
397
|
-
return repoInfo;
|
|
398
|
-
}
|
|
399
|
-
if (repoInfo.url) {
|
|
400
|
-
return repoInfo.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
} catch {
|
|
404
|
-
// Ignore errors fetching from npm
|
|
405
|
-
}
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Checks whether the given extension is compatible with the currently installed server
|
|
209
|
+
* For TS to understand that a function throws an exception, it must actually throw an exception--
|
|
210
|
+
* in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
|
|
211
|
+
* nor is something like a `never` return annotation, which does not imply a thrown exception.
|
|
411
212
|
*
|
|
412
|
-
* @
|
|
413
|
-
* @returns {Promise<void>}
|
|
213
|
+
* @throws {Error}
|
|
414
214
|
*/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
await spinWith(this.isJsonOutput, `Checking if '${pkgName}' is compatible`, async () => {
|
|
421
|
-
const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(pkgName, pkgVer);
|
|
422
|
-
if (serverVersion && extVersionRequirement && !semver.satisfies(serverVersion, extVersionRequirement)) {
|
|
423
|
-
throw this._createFatalError(
|
|
424
|
-
`'${installSpec}' cannot be installed because the server version it requires (${extVersionRequirement}) ` +
|
|
425
|
-
`does not meet the currently installed one (${serverVersion}). Please install ` +
|
|
426
|
-
`a compatible server version first.`
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
});
|
|
215
|
+
protected _createFatalError(message: string): Error {
|
|
216
|
+
return new Error(this.log.decorate(message, 'error'));
|
|
430
217
|
}
|
|
431
218
|
|
|
432
219
|
/**
|
|
433
|
-
*
|
|
220
|
+
* Build the initial list data structure from installed and known extensions
|
|
434
221
|
*
|
|
435
|
-
* @param {InstallOpts} opts
|
|
436
|
-
* @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
|
|
437
222
|
*/
|
|
438
|
-
async _install({installSpec, installType, packageName}) {
|
|
439
|
-
/** @type {ExtInstallReceipt<ExtType>} */
|
|
440
|
-
let receipt;
|
|
441
|
-
|
|
223
|
+
protected async _install({installSpec, installType, packageName}: InstallOpts): Promise<Record<string, any>> {
|
|
442
224
|
if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
|
|
443
225
|
throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
|
|
444
226
|
}
|
|
@@ -447,16 +229,12 @@ class ExtensionCliCommand {
|
|
|
447
229
|
throw this._createFatalError(`When using --source=${installType}, must also use --package`);
|
|
448
230
|
}
|
|
449
231
|
|
|
450
|
-
|
|
451
|
-
* @type {InstallViaNpmArgs}
|
|
452
|
-
*/
|
|
453
|
-
let installViaNpmOpts;
|
|
232
|
+
let installViaNpmOpts: InstallViaNpmArgs;
|
|
454
233
|
|
|
455
234
|
/**
|
|
456
235
|
* The probable (?) name of the extension derived from the install spec.
|
|
457
236
|
*
|
|
458
237
|
* If using a local install type, this will remain empty.
|
|
459
|
-
* @type {string}
|
|
460
238
|
*/
|
|
461
239
|
let probableExtName = '';
|
|
462
240
|
|
|
@@ -471,9 +249,9 @@ class ExtensionCliCommand {
|
|
|
471
249
|
installViaNpmOpts = {
|
|
472
250
|
installSpec,
|
|
473
251
|
installType,
|
|
474
|
-
pkgName:
|
|
252
|
+
pkgName: packageName as string,
|
|
475
253
|
};
|
|
476
|
-
probableExtName =
|
|
254
|
+
probableExtName = packageName as string;
|
|
477
255
|
} else if (installType === INSTALL_TYPE_GIT) {
|
|
478
256
|
// git urls can have '.git' at the end, but this is not necessary and would complicate the
|
|
479
257
|
// way we download and name directories, so we can just remove it
|
|
@@ -481,11 +259,12 @@ class ExtensionCliCommand {
|
|
|
481
259
|
installViaNpmOpts = {
|
|
482
260
|
installSpec,
|
|
483
261
|
installType,
|
|
484
|
-
pkgName:
|
|
262
|
+
pkgName: packageName as string,
|
|
485
263
|
};
|
|
486
|
-
probableExtName =
|
|
264
|
+
probableExtName = packageName as string;
|
|
487
265
|
} else {
|
|
488
|
-
let pkgName
|
|
266
|
+
let pkgName: string;
|
|
267
|
+
let pkgVer: string | undefined;
|
|
489
268
|
if (installType === INSTALL_TYPE_LOCAL) {
|
|
490
269
|
pkgName = path.isAbsolute(installSpec) ? installSpec : path.resolve(installSpec);
|
|
491
270
|
} else {
|
|
@@ -494,7 +273,7 @@ class ExtensionCliCommand {
|
|
|
494
273
|
// extensions installed via npm can include versions or tags after the '@'
|
|
495
274
|
// sign, so check for that. We also need to be careful that package names themselves can
|
|
496
275
|
// contain the '@' symbol, as in `npm install @appium/fake-driver@1.2.0`
|
|
497
|
-
let name;
|
|
276
|
+
let name: string;
|
|
498
277
|
const splits = installSpec.split('@');
|
|
499
278
|
if (installSpec.startsWith('@')) {
|
|
500
279
|
// this is the case where we have an npm org included in the package name
|
|
@@ -540,12 +319,11 @@ class ExtensionCliCommand {
|
|
|
540
319
|
|
|
541
320
|
await this._checkInstallCompatibility(installViaNpmOpts);
|
|
542
321
|
|
|
543
|
-
receipt = await this.installViaNpm(installViaNpmOpts);
|
|
322
|
+
const receipt = await this.installViaNpm(installViaNpmOpts);
|
|
544
323
|
|
|
545
324
|
// this _should_ be the same as `probablyExtName` as the one derived above unless
|
|
546
325
|
// install type is local.
|
|
547
|
-
|
|
548
|
-
const extName = receipt[/** @type {string} */ (`${this.type}Name`)];
|
|
326
|
+
const extName = receipt[`${this.type}Name`];
|
|
549
327
|
|
|
550
328
|
// check _a second time_ with the more-accurate extName
|
|
551
329
|
if (this.config.isInstalled(extName)) {
|
|
@@ -558,12 +336,11 @@ class ExtensionCliCommand {
|
|
|
558
336
|
|
|
559
337
|
// this field does not exist as such in the manifest (it's used as a property name instead)
|
|
560
338
|
// so that's why it's being removed here.
|
|
561
|
-
/** @type {ExtManifest<ExtType>} */
|
|
562
339
|
const extManifest = receiptToManifest(receipt);
|
|
563
340
|
|
|
564
341
|
const [errors, warnings] = await B.all([
|
|
565
|
-
this.config.getProblems(extName, extManifest),
|
|
566
|
-
this.config.getWarnings(extName, extManifest),
|
|
342
|
+
this.config.getProblems(extName, extManifest as any),
|
|
343
|
+
this.config.getWarnings(extName, extManifest as any),
|
|
567
344
|
]);
|
|
568
345
|
const errorMap = new Map([[extName, errors]]);
|
|
569
346
|
const warningMap = new Map([[extName, warnings]]);
|
|
@@ -581,7 +358,7 @@ class ExtensionCliCommand {
|
|
|
581
358
|
this.log.warn(warningSummaries.join('\n'));
|
|
582
359
|
}
|
|
583
360
|
|
|
584
|
-
await this.config.addExtension(extName, extManifest);
|
|
361
|
+
await this.config.addExtension(extName, extManifest as any);
|
|
585
362
|
|
|
586
363
|
// update the hash if we've changed the local `package.json`
|
|
587
364
|
if (await env.hasAppiumDependency(this.config.appiumHome)) {
|
|
@@ -589,140 +366,19 @@ class ExtensionCliCommand {
|
|
|
589
366
|
}
|
|
590
367
|
|
|
591
368
|
// log info for the user
|
|
592
|
-
this.log.info(
|
|
369
|
+
this.log.info(
|
|
370
|
+
this.getPostInstallText({extName, extData: receipt as unknown as ExtInstallReceipt<ExtType>})
|
|
371
|
+
);
|
|
593
372
|
|
|
594
373
|
return this.config.installedExtensions;
|
|
595
374
|
}
|
|
596
375
|
|
|
597
376
|
/**
|
|
598
|
-
*
|
|
377
|
+
* Get the text which should be displayed to the user after an extension has been installed. This
|
|
378
|
+
* is designed to be overridden by drivers/plugins with their own particular text.
|
|
599
379
|
*
|
|
600
|
-
* @param {InstallViaNpmArgs} args
|
|
601
|
-
* @returns {Promise<ExtInstallReceipt<ExtType>>}
|
|
602
380
|
*/
|
|
603
|
-
|
|
604
|
-
const installMsg = `Installing '${installSpec}'`;
|
|
605
|
-
const validateMsg = `Validating '${installSpec}'`;
|
|
606
|
-
|
|
607
|
-
// the string used for installation is either <name>@<ver> in the case of a standard NPM
|
|
608
|
-
// package, or whatever the user sent in otherwise.
|
|
609
|
-
const installStr = installType === INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
|
|
610
|
-
const appiumHome = this.config.appiumHome;
|
|
611
|
-
try {
|
|
612
|
-
const {pkg, installPath} = await spinWith(
|
|
613
|
-
this.isJsonOutput,
|
|
614
|
-
installMsg,
|
|
615
|
-
async () => await npm.installPackage(appiumHome, installStr, {pkgName, installType})
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
await spinWith(this.isJsonOutput, validateMsg, async () => {
|
|
619
|
-
this.validatePackageJson(pkg, installSpec);
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
return this.getInstallationReceipt({
|
|
623
|
-
pkg,
|
|
624
|
-
installPath,
|
|
625
|
-
installType,
|
|
626
|
-
installSpec,
|
|
627
|
-
});
|
|
628
|
-
} catch (err) {
|
|
629
|
-
throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Get the text which should be displayed to the user after an extension has been installed. This
|
|
635
|
-
* is designed to be overridden by drivers/plugins with their own particular text.
|
|
636
|
-
*
|
|
637
|
-
* @param {ExtensionArgs} args
|
|
638
|
-
* @returns {string}
|
|
639
|
-
*/
|
|
640
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
641
|
-
getPostInstallText(args) {
|
|
642
|
-
throw this._createFatalError('Must be implemented in final class');
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Once a package is installed on-disk, this gathers some necessary metadata for validation.
|
|
647
|
-
*
|
|
648
|
-
* @param {GetInstallationReceiptOpts<ExtType>} opts
|
|
649
|
-
* @returns {ExtInstallReceipt<ExtType>}
|
|
650
|
-
*/
|
|
651
|
-
getInstallationReceipt({pkg, installPath, installType, installSpec}) {
|
|
652
|
-
const {appium, name, version, peerDependencies} = pkg;
|
|
653
|
-
|
|
654
|
-
const strVersion = /** @type {string} */ (version);
|
|
655
|
-
/** @type {import('appium/types').InternalMetadata} */
|
|
656
|
-
const internal = {
|
|
657
|
-
pkgName: /** @type {string} */ (name),
|
|
658
|
-
version: strVersion,
|
|
659
|
-
installType,
|
|
660
|
-
installSpec,
|
|
661
|
-
installPath,
|
|
662
|
-
appiumVersion: peerDependencies?.appium,
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
/** @type {ExtMetadata<ExtType>} */
|
|
666
|
-
const extMetadata = appium;
|
|
667
|
-
|
|
668
|
-
return {
|
|
669
|
-
...internal,
|
|
670
|
-
...extMetadata,
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Validates the _required_ root fields of an extension's `package.json` file.
|
|
676
|
-
*
|
|
677
|
-
* These required fields are:
|
|
678
|
-
* - `name`
|
|
679
|
-
* - `version`
|
|
680
|
-
* - `appium`
|
|
681
|
-
* @param {import('type-fest').PackageJson} pkg - `package.json` of extension
|
|
682
|
-
* @param {string} installSpec - Extension name/spec
|
|
683
|
-
* @throws {ReferenceError} If `package.json` has a missing or invalid field
|
|
684
|
-
* @returns {pkg is ExtPackageJson<ExtType>}
|
|
685
|
-
*/
|
|
686
|
-
validatePackageJson(pkg, installSpec) {
|
|
687
|
-
const {appium, name, version} = /** @type {ExtPackageJson<ExtType>} */ (pkg);
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
*
|
|
691
|
-
* @param {string} field
|
|
692
|
-
* @returns {ReferenceError}
|
|
693
|
-
*/
|
|
694
|
-
const createMissingFieldError = (field) =>
|
|
695
|
-
new ReferenceError(
|
|
696
|
-
`${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
|
|
697
|
-
);
|
|
698
|
-
|
|
699
|
-
if (!name) {
|
|
700
|
-
throw createMissingFieldError('name');
|
|
701
|
-
}
|
|
702
|
-
if (!version) {
|
|
703
|
-
throw createMissingFieldError('version');
|
|
704
|
-
}
|
|
705
|
-
if (!appium) {
|
|
706
|
-
throw createMissingFieldError('appium');
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
this.validateExtensionFields(appium, installSpec);
|
|
710
|
-
|
|
711
|
-
return true;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* For any `package.json` fields which a particular type of extension requires, validate the
|
|
716
|
-
* presence and form of those fields on the `package.json` data, throwing an error if anything is
|
|
717
|
-
* amiss.
|
|
718
|
-
*
|
|
719
|
-
* @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
|
|
720
|
-
* @param {string} installSpec - Extension name/spec
|
|
721
|
-
*/
|
|
722
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
723
|
-
validateExtensionFields(extMetadata, installSpec) {
|
|
724
|
-
throw this._createFatalError('Must be implemented in final class');
|
|
725
|
-
}
|
|
381
|
+
protected abstract getPostInstallText(args: ExtensionArgs<ExtType>): PostInstallText;
|
|
726
382
|
|
|
727
383
|
/**
|
|
728
384
|
* Uninstall an extension.
|
|
@@ -731,10 +387,9 @@ class ExtensionCliCommand {
|
|
|
731
387
|
*
|
|
732
388
|
* Will only remove the extension from the manifest if it has been successfully removed.
|
|
733
389
|
*
|
|
734
|
-
* @
|
|
735
|
-
* @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data (without the extension just uninstalled)
|
|
390
|
+
* @return map of all installed extension names to extension data (without the extension just uninstalled)
|
|
736
391
|
*/
|
|
737
|
-
async _uninstall({installSpec}) {
|
|
392
|
+
protected async _uninstall({installSpec}: UninstallOpts): Promise<Record<string, any>> {
|
|
738
393
|
if (!this.config.isInstalled(installSpec)) {
|
|
739
394
|
throw this._createFatalError(
|
|
740
395
|
`Can't uninstall ${this.type} '${installSpec}'; it is not installed`
|
|
@@ -757,10 +412,8 @@ class ExtensionCliCommand {
|
|
|
757
412
|
/**
|
|
758
413
|
* Attempt to update one or more drivers using NPM
|
|
759
414
|
*
|
|
760
|
-
* @param {ExtensionUpdateOpts} updateSpec
|
|
761
|
-
* @return {Promise<ExtensionUpdateResult>}
|
|
762
415
|
*/
|
|
763
|
-
async _update({installSpec, unsafe}) {
|
|
416
|
+
protected async _update({installSpec, unsafe}: ExtensionUpdateOpts): Promise<ExtensionUpdateResult> {
|
|
764
417
|
const shouldUpdateAll = installSpec === UPDATE_ALL;
|
|
765
418
|
// if we're specifically requesting an update for an extension, make sure it's installed
|
|
766
419
|
if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
|
|
@@ -773,13 +426,11 @@ class ExtensionCliCommand {
|
|
|
773
426
|
: [installSpec];
|
|
774
427
|
|
|
775
428
|
// 'errors' will have ext names as keys and error objects as values
|
|
776
|
-
|
|
777
|
-
const errors = {};
|
|
429
|
+
const errors: Record<string, Error> = {};
|
|
778
430
|
|
|
779
431
|
// 'updates' will have ext names as keys and update objects as values, where an update
|
|
780
432
|
// object is of the form {from: versionString, to: versionString}
|
|
781
|
-
|
|
782
|
-
const updates = {};
|
|
433
|
+
const updates: Record<string, UpdateReport> = {};
|
|
783
434
|
|
|
784
435
|
for (const e of extsToUpdate) {
|
|
785
436
|
try {
|
|
@@ -807,6 +458,9 @@ class ExtensionCliCommand {
|
|
|
807
458
|
);
|
|
808
459
|
}
|
|
809
460
|
const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
|
|
461
|
+
if (!updateVer) {
|
|
462
|
+
throw new NoUpdatesAvailableError();
|
|
463
|
+
}
|
|
810
464
|
await spinWith(
|
|
811
465
|
this.isJsonOutput,
|
|
812
466
|
`Updating ${this.type} '${e}' from ${update.current} to ${updateVer}`,
|
|
@@ -850,15 +504,13 @@ class ExtensionCliCommand {
|
|
|
850
504
|
* Given an extension name, figure out what its highest possible version upgrade is, and also the
|
|
851
505
|
* highest possible safe upgrade.
|
|
852
506
|
*
|
|
853
|
-
* @param
|
|
854
|
-
* @return {Promise<PossibleUpdates>}
|
|
507
|
+
* @param ext - name of extension
|
|
855
508
|
*/
|
|
856
|
-
async checkForExtensionUpdate(ext) {
|
|
509
|
+
protected async checkForExtensionUpdate(ext: string): Promise<PossibleUpdates> {
|
|
857
510
|
// TODO decide how we want to handle beta versions?
|
|
858
511
|
// this is a helper method, 'ext' is assumed to already be installed here, and of the npm
|
|
859
512
|
// install type
|
|
860
513
|
const {version, pkgName} = this.config.installedExtensions[ext];
|
|
861
|
-
/** @type {string?} */
|
|
862
514
|
let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
|
|
863
515
|
let safeUpdate = await npm.getLatestSafeUpgradeVersion(
|
|
864
516
|
this.config.appiumHome,
|
|
@@ -881,53 +533,14 @@ class ExtensionCliCommand {
|
|
|
881
533
|
return {current: version, safeUpdate, unsafeUpdate};
|
|
882
534
|
}
|
|
883
535
|
|
|
884
|
-
/**
|
|
885
|
-
* Actually update an extension installed by NPM, using the NPM cli. And update the installation
|
|
886
|
-
* manifest.
|
|
887
|
-
*
|
|
888
|
-
* @param {string} installSpec - name of extension to update
|
|
889
|
-
* @param {string} version - version string identifier to update extension to
|
|
890
|
-
* @returns {Promise<void>}
|
|
891
|
-
*/
|
|
892
|
-
async updateExtension(installSpec, version) {
|
|
893
|
-
const {pkgName, installType} = this.config.installedExtensions[installSpec];
|
|
894
|
-
const extData = await this.installViaNpm({
|
|
895
|
-
installSpec,
|
|
896
|
-
installType,
|
|
897
|
-
pkgName,
|
|
898
|
-
pkgVer: version,
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
delete extData[/** @type {string} */ (`${this.type}Name`)];
|
|
902
|
-
await this.config.updateExtension(installSpec, extData);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Just wraps {@linkcode child_process.spawn} with some default options
|
|
907
|
-
*
|
|
908
|
-
* @param {string} cwd - CWD
|
|
909
|
-
* @param {string} script - Path to script
|
|
910
|
-
* @param {string[]} args - Extra args for script
|
|
911
|
-
* @param {import('child_process').SpawnOptions} opts - Options
|
|
912
|
-
* @returns {import('node:child_process').ChildProcess}
|
|
913
|
-
*/
|
|
914
|
-
_runUnbuffered(cwd, script, args = [], opts = {}) {
|
|
915
|
-
return spawn(process.execPath, [script, ...args], {
|
|
916
|
-
cwd,
|
|
917
|
-
stdio: 'inherit',
|
|
918
|
-
...opts,
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
|
|
922
536
|
/**
|
|
923
537
|
* Runs doctor checks for the given extension.
|
|
924
538
|
*
|
|
925
|
-
* @
|
|
926
|
-
* @returns {Promise<number>} The amount of Doctor checks that were
|
|
539
|
+
* @returns The amount of Doctor checks that were
|
|
927
540
|
* successfully loaded and executed for the given extension
|
|
928
541
|
* @throws {Error} If any of the mandatory Doctor checks fails.
|
|
929
542
|
*/
|
|
930
|
-
async _doctor({installSpec}) {
|
|
543
|
+
protected async _doctor({installSpec}: DoctorOptions): Promise<number> {
|
|
931
544
|
if (!this.config.isInstalled(installSpec)) {
|
|
932
545
|
throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
|
|
933
546
|
}
|
|
@@ -939,7 +552,7 @@ class ExtensionCliCommand {
|
|
|
939
552
|
`No package.json could be found for "${installSpec}" ${this.type}`
|
|
940
553
|
);
|
|
941
554
|
}
|
|
942
|
-
let doctorSpec;
|
|
555
|
+
let doctorSpec: {checks: string[]} | undefined;
|
|
943
556
|
try {
|
|
944
557
|
doctorSpec = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')).appium?.doctor;
|
|
945
558
|
} catch (e) {
|
|
@@ -957,7 +570,8 @@ class ExtensionCliCommand {
|
|
|
957
570
|
`containing the 'checks' key with the array of script paths`
|
|
958
571
|
);
|
|
959
572
|
}
|
|
960
|
-
const paths = doctorSpec.checks
|
|
573
|
+
const paths: string[] = doctorSpec.checks
|
|
574
|
+
.map((p) => {
|
|
961
575
|
const scriptPath = path.resolve(moduleRoot, p);
|
|
962
576
|
if (!path.normalize(scriptPath).startsWith(path.normalize(moduleRoot))) {
|
|
963
577
|
this.log.error(
|
|
@@ -967,9 +581,9 @@ class ExtensionCliCommand {
|
|
|
967
581
|
return null;
|
|
968
582
|
}
|
|
969
583
|
return scriptPath;
|
|
970
|
-
})
|
|
971
|
-
|
|
972
|
-
const loadChecksPromises = [];
|
|
584
|
+
})
|
|
585
|
+
.filter((p): p is string => Boolean(p));
|
|
586
|
+
const loadChecksPromises: Promise<unknown>[] = [];
|
|
973
587
|
for (const p of paths) {
|
|
974
588
|
const promise = (async () => {
|
|
975
589
|
// https://github.com/nodejs/node/issues/31710
|
|
@@ -982,12 +596,11 @@ class ExtensionCliCommand {
|
|
|
982
596
|
})();
|
|
983
597
|
loadChecksPromises.push(promise);
|
|
984
598
|
}
|
|
985
|
-
const isDoctorCheck = (
|
|
599
|
+
const isDoctorCheck = (x) =>
|
|
986
600
|
['diagnose', 'fix', 'hasAutofix', 'isOptional'].every((method) => _.isFunction(x?.[method]));
|
|
987
|
-
|
|
988
|
-
const checks = _.flatMap((await B.all(loadChecksPromises)).filter(Boolean).map(_.toPairs))
|
|
601
|
+
const checks: IDoctorCheck[] = _.flatMap((await B.all(loadChecksPromises)).filter(Boolean).map(_.toPairs))
|
|
989
602
|
.map(([, value]) => value)
|
|
990
|
-
.filter(isDoctorCheck);
|
|
603
|
+
.filter(isDoctorCheck) as IDoctorCheck[];
|
|
991
604
|
if (_.isEmpty(checks)) {
|
|
992
605
|
this.log.info(`The ${this.type} "${installSpec}" exports no valid doctor checks`);
|
|
993
606
|
return 0;
|
|
@@ -1011,10 +624,13 @@ class ExtensionCliCommand {
|
|
|
1011
624
|
* `scripts` field is not a plain object, or if the `scriptName` is
|
|
1012
625
|
* not found within `scripts` object.
|
|
1013
626
|
*
|
|
1014
|
-
* @param {RunOptions} opts
|
|
1015
|
-
* @return {Promise<RunOutput>}
|
|
1016
627
|
*/
|
|
1017
|
-
async _run({
|
|
628
|
+
protected async _run({
|
|
629
|
+
installSpec,
|
|
630
|
+
scriptName,
|
|
631
|
+
extraArgs = [],
|
|
632
|
+
bufferOutput = false,
|
|
633
|
+
}: RunOptions): Promise<RunOutput> {
|
|
1018
634
|
if (!this.config.isInstalled(installSpec)) {
|
|
1019
635
|
throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
|
|
1020
636
|
}
|
|
@@ -1038,7 +654,7 @@ class ExtensionCliCommand {
|
|
|
1038
654
|
}
|
|
1039
655
|
|
|
1040
656
|
if (!scriptName) {
|
|
1041
|
-
const allScripts = _.toPairs(extScripts);
|
|
657
|
+
const allScripts = _.toPairs(extScripts as Record<string, string>);
|
|
1042
658
|
const root = this.config.getInstallPath(installSpec);
|
|
1043
659
|
const existingScripts = await B.filter(
|
|
1044
660
|
allScripts,
|
|
@@ -1055,7 +671,7 @@ class ExtensionCliCommand {
|
|
|
1055
671
|
return {};
|
|
1056
672
|
}
|
|
1057
673
|
|
|
1058
|
-
if (!(scriptName in
|
|
674
|
+
if (!(scriptName in extScripts)) {
|
|
1059
675
|
throw this._createFatalError(
|
|
1060
676
|
`The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
|
|
1061
677
|
);
|
|
@@ -1118,22 +734,426 @@ class ExtensionCliCommand {
|
|
|
1118
734
|
throw this._createFatalError(message);
|
|
1119
735
|
}
|
|
1120
736
|
}
|
|
737
|
+
|
|
738
|
+
private _buildListData(showInstalled: boolean): ExtensionList {
|
|
739
|
+
const installedNames = Object.keys(this.config.installedExtensions);
|
|
740
|
+
const knownNames = Object.keys(this.knownExtensions);
|
|
741
|
+
return [...installedNames, ...knownNames].reduce((acc, name) => {
|
|
742
|
+
if (!acc[name]) {
|
|
743
|
+
if (installedNames.includes(name)) {
|
|
744
|
+
acc[name] = {
|
|
745
|
+
...this.config.installedExtensions[name],
|
|
746
|
+
installed: true,
|
|
747
|
+
};
|
|
748
|
+
} else if (!showInstalled) {
|
|
749
|
+
acc[name] = {
|
|
750
|
+
pkgName: this.knownExtensions[name],
|
|
751
|
+
installed: false,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return acc;
|
|
756
|
+
}, {});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Install an extension via NPM
|
|
761
|
+
*
|
|
762
|
+
*/
|
|
763
|
+
private async installViaNpm({
|
|
764
|
+
installSpec,
|
|
765
|
+
pkgName,
|
|
766
|
+
pkgVer,
|
|
767
|
+
installType,
|
|
768
|
+
}: InstallViaNpmArgs): Promise<ExtInstallReceipt<ExtType>> {
|
|
769
|
+
const installMsg = `Installing '${installSpec}'`;
|
|
770
|
+
const validateMsg = `Validating '${installSpec}'`;
|
|
771
|
+
|
|
772
|
+
// the string used for installation is either <name>@<ver> in the case of a standard NPM
|
|
773
|
+
// package, or whatever the user sent in otherwise.
|
|
774
|
+
const installStr = installType === INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
|
|
775
|
+
const appiumHome = this.config.appiumHome;
|
|
776
|
+
try {
|
|
777
|
+
const {pkg, installPath} = await spinWith(
|
|
778
|
+
this.isJsonOutput,
|
|
779
|
+
installMsg,
|
|
780
|
+
async () => await npm.installPackage(appiumHome, installStr, {pkgName, installType})
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
const validatedPkg = await spinWith(this.isJsonOutput, validateMsg, async () =>
|
|
784
|
+
this.validatePackageJson(pkg, installSpec)
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
return this.getInstallationReceipt({
|
|
788
|
+
pkg: validatedPkg,
|
|
789
|
+
installPath,
|
|
790
|
+
installType,
|
|
791
|
+
installSpec,
|
|
792
|
+
});
|
|
793
|
+
} catch (err) {
|
|
794
|
+
throw this._createFatalError(`Encountered an error when installing package: ${err.message}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Actually update an extension installed by NPM, using the NPM cli. And update the installation
|
|
801
|
+
* manifest.
|
|
802
|
+
*
|
|
803
|
+
* @param installSpec - name of extension to update
|
|
804
|
+
* @param version - version string identifier to update extension to
|
|
805
|
+
*/
|
|
806
|
+
private async updateExtension(installSpec: string, version: string): Promise<void> {
|
|
807
|
+
const {pkgName, installType} = this.config.installedExtensions[installSpec];
|
|
808
|
+
const extData = await this.installViaNpm({
|
|
809
|
+
installSpec,
|
|
810
|
+
installType,
|
|
811
|
+
pkgName,
|
|
812
|
+
pkgVer: version,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
delete extData[`${this.type}Name`];
|
|
816
|
+
await this.config.updateExtension(installSpec, extData as any);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Just wraps {@linkcode child_process.spawn} with some default options
|
|
821
|
+
*
|
|
822
|
+
* @param cwd - CWD
|
|
823
|
+
* @param script - Path to script
|
|
824
|
+
* @param args - Extra args for script
|
|
825
|
+
* @param opts - Options
|
|
826
|
+
*/
|
|
827
|
+
private _runUnbuffered(
|
|
828
|
+
cwd: string,
|
|
829
|
+
script: string,
|
|
830
|
+
args: string[] = [],
|
|
831
|
+
opts: Record<string, any> = {}
|
|
832
|
+
) {
|
|
833
|
+
return spawn(process.execPath, [script, ...args], {
|
|
834
|
+
cwd,
|
|
835
|
+
stdio: 'inherit',
|
|
836
|
+
...opts,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Once a package is installed on-disk, this gathers some necessary metadata for validation.
|
|
842
|
+
*
|
|
843
|
+
*/
|
|
844
|
+
private getInstallationReceipt({
|
|
845
|
+
pkg,
|
|
846
|
+
installPath,
|
|
847
|
+
installType,
|
|
848
|
+
installSpec,
|
|
849
|
+
}: GetInstallationReceiptOpts<ExtType>): ExtInstallReceipt<ExtType> {
|
|
850
|
+
const {appium, name, version, peerDependencies} = pkg;
|
|
851
|
+
|
|
852
|
+
const strVersion = version;
|
|
853
|
+
const internal = {
|
|
854
|
+
pkgName: name,
|
|
855
|
+
version: strVersion,
|
|
856
|
+
installType,
|
|
857
|
+
installSpec,
|
|
858
|
+
installPath,
|
|
859
|
+
appiumVersion: peerDependencies?.appium,
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const extMetadata = appium;
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
...internal,
|
|
866
|
+
...extMetadata,
|
|
867
|
+
} as unknown as ExtInstallReceipt<ExtType>;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Validates the _required_ root fields of an extension's `package.json` file.
|
|
872
|
+
*
|
|
873
|
+
* These required fields are:
|
|
874
|
+
* - `name`
|
|
875
|
+
* - `version`
|
|
876
|
+
* - `appium`
|
|
877
|
+
* @param pkg - `package.json` of extension
|
|
878
|
+
* @param installSpec - Extension name/spec
|
|
879
|
+
* @throws {ReferenceError} If `package.json` has a missing or invalid field
|
|
880
|
+
*/
|
|
881
|
+
private validatePackageJson(pkg: PackageJson, installSpec: string): ExtPackageJson<ExtType> {
|
|
882
|
+
const {appium, name, version} = pkg;
|
|
883
|
+
|
|
884
|
+
const createMissingFieldError = (field: string): ReferenceError =>
|
|
885
|
+
new ReferenceError(
|
|
886
|
+
`${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
if (!name) {
|
|
890
|
+
throw createMissingFieldError('name');
|
|
891
|
+
}
|
|
892
|
+
if (!version) {
|
|
893
|
+
throw createMissingFieldError('version');
|
|
894
|
+
}
|
|
895
|
+
if (!appium) {
|
|
896
|
+
throw createMissingFieldError('appium');
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
this.validateExtensionFields(appium as unknown as ExtMetadata<ExtType>, installSpec);
|
|
900
|
+
|
|
901
|
+
return pkg as unknown as ExtPackageJson<ExtType>;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Check for available updates for installed extensions
|
|
906
|
+
*
|
|
907
|
+
*/
|
|
908
|
+
private async _checkForUpdates(
|
|
909
|
+
listData: ExtensionList,
|
|
910
|
+
showUpdates: boolean,
|
|
911
|
+
lsMsg: string
|
|
912
|
+
): Promise<void> {
|
|
913
|
+
await spinWith(this.isJsonOutput, lsMsg, async () => {
|
|
914
|
+
// We'd like to still show lsMsg even if showUpdates is false
|
|
915
|
+
if (!showUpdates) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Filter to only extensions that need update checks (installed npm packages)
|
|
920
|
+
const extensionsToCheck = _.toPairs(listData as Record<string, any>).filter(
|
|
921
|
+
([, data]) => data.installed && data.installType === INSTALL_TYPE_NPM
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
await B.map(
|
|
925
|
+
extensionsToCheck,
|
|
926
|
+
async ([ext, data]) => {
|
|
927
|
+
try {
|
|
928
|
+
const updates = await this.checkForExtensionUpdate(ext);
|
|
929
|
+
data.updateVersion = updates.safeUpdate;
|
|
930
|
+
data.unsafeUpdateVersion = updates.unsafeUpdate;
|
|
931
|
+
data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
|
|
932
|
+
} catch (e) {
|
|
933
|
+
data.updateError = (e as Error).message;
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
{concurrency: MAX_CONCURRENT_REPO_FETCHES}
|
|
937
|
+
);
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Add repository URLs to list data for all extensions
|
|
943
|
+
*
|
|
944
|
+
*/
|
|
945
|
+
private async _addRepositoryUrlsToListData(listData: ExtensionList): Promise<void> {
|
|
946
|
+
await spinWith(this.isJsonOutput, 'Fetching repository information', async () => {
|
|
947
|
+
await B.map(
|
|
948
|
+
_.values(listData),
|
|
949
|
+
async (data) => {
|
|
950
|
+
const repoUrl = await this._getRepositoryUrl(data);
|
|
951
|
+
if (repoUrl) {
|
|
952
|
+
data.repositoryUrl = repoUrl;
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
{concurrency: MAX_CONCURRENT_REPO_FETCHES}
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Display normal formatted output
|
|
962
|
+
*
|
|
963
|
+
*/
|
|
964
|
+
private async _displayNormalListOutput(
|
|
965
|
+
listData: ExtensionList,
|
|
966
|
+
showUpdates: boolean
|
|
967
|
+
): Promise<ExtensionList> {
|
|
968
|
+
for (const [name, data] of _.toPairs(listData)) {
|
|
969
|
+
const line = await this._formatExtensionLine(name, data, showUpdates);
|
|
970
|
+
this.log.log(line);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return listData;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Format a single extension line for display
|
|
978
|
+
*
|
|
979
|
+
*/
|
|
980
|
+
private async _formatExtensionLine(
|
|
981
|
+
name: string,
|
|
982
|
+
data: ExtensionListData,
|
|
983
|
+
showUpdates: boolean
|
|
984
|
+
): Promise<string> {
|
|
985
|
+
if (data.installed) {
|
|
986
|
+
const installTxt = this._formatInstallText(data);
|
|
987
|
+
const updateTxt = showUpdates ? this._formatUpdateText(data) : '';
|
|
988
|
+
return `- ${name.yellow}${installTxt}${updateTxt}`;
|
|
989
|
+
}
|
|
990
|
+
const installTxt = ' [not installed]'.grey;
|
|
991
|
+
return `- ${name.yellow}${installTxt}`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Format installation status text
|
|
996
|
+
*
|
|
997
|
+
*/
|
|
998
|
+
private _formatInstallText(data: ExtensionListData): string {
|
|
999
|
+
const {installType, installSpec, version} = data;
|
|
1000
|
+
let typeTxt;
|
|
1001
|
+
switch (installType) {
|
|
1002
|
+
case INSTALL_TYPE_GIT:
|
|
1003
|
+
case INSTALL_TYPE_GITHUB:
|
|
1004
|
+
typeTxt = `(cloned from ${installSpec})`.yellow;
|
|
1005
|
+
break;
|
|
1006
|
+
case INSTALL_TYPE_LOCAL:
|
|
1007
|
+
typeTxt = `(linked from ${installSpec})`.magenta;
|
|
1008
|
+
break;
|
|
1009
|
+
case INSTALL_TYPE_DEV:
|
|
1010
|
+
typeTxt = '(dev mode)';
|
|
1011
|
+
break;
|
|
1012
|
+
default:
|
|
1013
|
+
typeTxt = '(npm)';
|
|
1014
|
+
}
|
|
1015
|
+
return `@${String(version).yellow} ${('[installed ' + typeTxt + ']').green}`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Format update information text
|
|
1020
|
+
*
|
|
1021
|
+
*/
|
|
1022
|
+
private _formatUpdateText(data: ExtensionListData): string {
|
|
1023
|
+
const {updateVersion, unsafeUpdateVersion, upToDate, updateError} = data;
|
|
1024
|
+
if (updateError) {
|
|
1025
|
+
return ` [Cannot check for updates: ${updateError}]`.red;
|
|
1026
|
+
}
|
|
1027
|
+
let txt = '';
|
|
1028
|
+
if (updateVersion) {
|
|
1029
|
+
txt += ` [${updateVersion} available]`.magenta;
|
|
1030
|
+
}
|
|
1031
|
+
if (upToDate) {
|
|
1032
|
+
txt += ` [Up to date]`.green;
|
|
1033
|
+
}
|
|
1034
|
+
if (unsafeUpdateVersion) {
|
|
1035
|
+
txt += ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
|
|
1036
|
+
}
|
|
1037
|
+
return txt;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Get repository URL from package data
|
|
1042
|
+
*
|
|
1043
|
+
*/
|
|
1044
|
+
private async _getRepositoryUrl(data: ExtensionListData): Promise<string | null> {
|
|
1045
|
+
if (data.installed && data.installPath) {
|
|
1046
|
+
return await this._getRepositoryUrlFromInstalled(
|
|
1047
|
+
data
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
if (data.pkgName && !data.installed) {
|
|
1051
|
+
return await this._getRepositoryUrlFromNpm(data.pkgName);
|
|
1052
|
+
}
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Get repository URL from installed extension's package.json
|
|
1058
|
+
*
|
|
1059
|
+
*/
|
|
1060
|
+
private async _getRepositoryUrlFromInstalled(data: ExtensionListData): Promise<string | null> {
|
|
1061
|
+
try {
|
|
1062
|
+
const pkgJsonPath = path.join(String(data.installPath), 'package.json');
|
|
1063
|
+
if (await fs.exists(pkgJsonPath)) {
|
|
1064
|
+
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
1065
|
+
if (pkg.repository) {
|
|
1066
|
+
if (typeof pkg.repository === 'string') {
|
|
1067
|
+
return pkg.repository;
|
|
1068
|
+
}
|
|
1069
|
+
if (pkg.repository.url) {
|
|
1070
|
+
return pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
} catch {
|
|
1075
|
+
// Ignore errors reading package.json
|
|
1076
|
+
}
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Get repository URL from npm for a package name
|
|
1082
|
+
*
|
|
1083
|
+
*/
|
|
1084
|
+
private async _getRepositoryUrlFromNpm(pkgName: string): Promise<string | null> {
|
|
1085
|
+
try {
|
|
1086
|
+
const repoInfo = await npm.getPackageInfo(pkgName, ['repository']);
|
|
1087
|
+
// When requesting only 'repository', npm.getPackageInfo returns the repository object directly
|
|
1088
|
+
if (repoInfo) {
|
|
1089
|
+
if (typeof repoInfo === 'string') {
|
|
1090
|
+
return repoInfo;
|
|
1091
|
+
}
|
|
1092
|
+
if (repoInfo.url) {
|
|
1093
|
+
return repoInfo.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
} catch {
|
|
1097
|
+
// Ignore errors fetching from npm
|
|
1098
|
+
}
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Checks whether the given extension is compatible with the currently installed server
|
|
1104
|
+
*
|
|
1105
|
+
*/
|
|
1106
|
+
private async _checkInstallCompatibility({
|
|
1107
|
+
installSpec,
|
|
1108
|
+
pkgName,
|
|
1109
|
+
pkgVer,
|
|
1110
|
+
installType,
|
|
1111
|
+
}: InstallViaNpmArgs): Promise<void> {
|
|
1112
|
+
if (INSTALL_TYPE_NPM !== installType) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
await spinWith(this.isJsonOutput, `Checking if '${pkgName}' is compatible`, async () => {
|
|
1117
|
+
const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(pkgName, pkgVer);
|
|
1118
|
+
if (serverVersion && extVersionRequirement && !semver.satisfies(serverVersion, extVersionRequirement)) {
|
|
1119
|
+
throw this._createFatalError(
|
|
1120
|
+
`'${installSpec}' cannot be installed because the server version it requires (${extVersionRequirement}) ` +
|
|
1121
|
+
`does not meet the currently installed one (${serverVersion}). Please install ` +
|
|
1122
|
+
`a compatible server version first.`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1121
1127
|
}
|
|
1122
1128
|
|
|
1123
1129
|
/**
|
|
1124
1130
|
* This is needed to ensure proper module resolution for installed extensions,
|
|
1125
1131
|
* especially ESM ones.
|
|
1126
1132
|
*
|
|
1127
|
-
* @param
|
|
1128
|
-
* @param
|
|
1129
|
-
* @param
|
|
1133
|
+
* @param driverConfig - active driver extension config
|
|
1134
|
+
* @param pluginConfig - active plugin extension config
|
|
1135
|
+
* @param logger - logger instance used for non-fatal symlink errors
|
|
1136
|
+
* @returns resolves when symlink injection has completed for all extensions
|
|
1130
1137
|
*/
|
|
1131
|
-
export async function injectAppiumSymlinks(
|
|
1132
|
-
|
|
1138
|
+
export async function injectAppiumSymlinks(
|
|
1139
|
+
driverConfig: ExtensionConfig<any>,
|
|
1140
|
+
pluginConfig: ExtensionConfig<any>,
|
|
1141
|
+
logger: AppiumLogger
|
|
1142
|
+
): Promise<void> {
|
|
1143
|
+
const isNpmInstalledExtension = (
|
|
1144
|
+
details: InstalledExtensionLike
|
|
1145
|
+
): details is InstalledExtensionLike & {installType: typeof INSTALL_TYPE_NPM; installPath: string} =>
|
|
1146
|
+
details.installType === INSTALL_TYPE_NPM && Boolean(details.installPath);
|
|
1147
|
+
|
|
1148
|
+
const installedExtensions = [
|
|
1133
1149
|
...Object.values(driverConfig.installedExtensions || {}),
|
|
1134
|
-
...Object.values(pluginConfig.installedExtensions || {})
|
|
1135
|
-
]
|
|
1136
|
-
|
|
1150
|
+
...Object.values(pluginConfig.installedExtensions || {}),
|
|
1151
|
+
] as InstalledExtensionLike[];
|
|
1152
|
+
|
|
1153
|
+
const installPaths = _.compact(installedExtensions
|
|
1154
|
+
.filter((details): details is InstalledExtensionLike => Boolean(details))
|
|
1155
|
+
.filter(isNpmInstalledExtension)
|
|
1156
|
+
.map((details) => details.installPath));
|
|
1137
1157
|
// After the extension is installed, we try to inject the appium module symlink
|
|
1138
1158
|
// into the extension's node_modules folder if it is not there yet.
|
|
1139
1159
|
// We also inject the symlink into other installed extensions' node_modules folders
|
|
@@ -1148,12 +1168,10 @@ export async function injectAppiumSymlinks(driverConfig, pluginConfig, logger) {
|
|
|
1148
1168
|
* This is needed to ensure proper module resolution for installed extensions,
|
|
1149
1169
|
* especially ESM ones.
|
|
1150
1170
|
*
|
|
1151
|
-
* @param
|
|
1152
|
-
* @param {import('@appium/types').AppiumLogger} logger
|
|
1153
|
-
* @returns {Promise<void>}
|
|
1171
|
+
* @param dstFolder The destination folder where the symlink should be created
|
|
1154
1172
|
*/
|
|
1155
|
-
async function injectAppiumSymlink(dstFolder, logger) {
|
|
1156
|
-
let appiumModuleRoot;
|
|
1173
|
+
async function injectAppiumSymlink(dstFolder: string, logger: AppiumLogger): Promise<void> {
|
|
1174
|
+
let appiumModuleRoot = '';
|
|
1157
1175
|
try {
|
|
1158
1176
|
appiumModuleRoot = getAppiumModuleRoot();
|
|
1159
1177
|
const symlinkPath = path.join(dstFolder, path.basename(appiumModuleRoot));
|
|
@@ -1169,187 +1187,90 @@ async function injectAppiumSymlink(dstFolder, logger) {
|
|
|
1169
1187
|
}
|
|
1170
1188
|
}
|
|
1171
1189
|
|
|
1172
|
-
export default ExtensionCliCommand;
|
|
1173
|
-
export {ExtensionCliCommand as ExtensionCommand};
|
|
1174
|
-
|
|
1175
|
-
/**
|
|
1176
|
-
* Options for the {@linkcode ExtensionCliCommand} constructor
|
|
1177
|
-
* @template {ExtensionType} ExtType
|
|
1178
|
-
* @typedef ExtensionCommandOptions
|
|
1179
|
-
* @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
|
|
1180
|
-
* @property {boolean} json - whether the output of this command should be JSON or text
|
|
1181
|
-
*/
|
|
1182
|
-
|
|
1183
|
-
/**
|
|
1184
|
-
* Extra stuff about extensions; used indirectly by {@linkcode ExtensionCliCommand.list}.
|
|
1185
|
-
*
|
|
1186
|
-
* @typedef ExtensionListMetadata
|
|
1187
|
-
* @property {boolean} installed - If `true`, the extension is installed
|
|
1188
|
-
* @property {boolean} upToDate - If the extension is installed and the latest
|
|
1189
|
-
* @property {string|null} updateVersion - If the extension is installed, the version it can be updated to
|
|
1190
|
-
* @property {string|null} unsafeUpdateVersion - Same as above, but a major version bump
|
|
1191
|
-
* @property {string} [updateError] - Update check error message (if present)
|
|
1192
|
-
* @property {boolean} [devMode] - If Appium is run from an extension's working copy
|
|
1193
|
-
* @property {string} [repositoryUrl] - Repository URL for the extension (if available)
|
|
1194
|
-
*/
|
|
1195
|
-
|
|
1196
|
-
/**
|
|
1197
|
-
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
|
1198
|
-
* @typedef {import('@appium/types').DriverType} DriverType
|
|
1199
|
-
* @typedef {import('@appium/types').PluginType} PluginType
|
|
1200
|
-
*/
|
|
1201
|
-
|
|
1202
|
-
/**
|
|
1203
|
-
* @template {ExtensionType} ExtType
|
|
1204
|
-
* @typedef {import('appium/types').ExtRecord<ExtType>} ExtRecord
|
|
1205
|
-
*/
|
|
1206
|
-
|
|
1207
|
-
/**
|
|
1208
|
-
* @template {ExtensionType} ExtType
|
|
1209
|
-
* @typedef {import('../extension/extension-config').ExtensionConfig<ExtType>} ExtensionConfig
|
|
1210
|
-
*/
|
|
1211
|
-
|
|
1212
|
-
/**
|
|
1213
|
-
* @template {ExtensionType} ExtType
|
|
1214
|
-
* @typedef {import('appium/types').ExtMetadata<ExtType>} ExtMetadata
|
|
1215
|
-
*/
|
|
1216
|
-
|
|
1217
|
-
/**
|
|
1218
|
-
* @template {ExtensionType} ExtType
|
|
1219
|
-
* @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
|
|
1220
|
-
*/
|
|
1221
|
-
|
|
1222
|
-
/**
|
|
1223
|
-
* @template {ExtensionType} ExtType
|
|
1224
|
-
* @typedef {import('appium/types').ExtPackageJson<ExtType>} ExtPackageJson
|
|
1225
|
-
*/
|
|
1226
|
-
|
|
1227
|
-
/**
|
|
1228
|
-
* @template {ExtensionType} ExtType
|
|
1229
|
-
* @typedef {import('appium/types').ExtInstallReceipt<ExtType>} ExtInstallReceipt
|
|
1230
|
-
*/
|
|
1231
|
-
|
|
1232
|
-
/**
|
|
1233
|
-
* Possible return value for {@linkcode ExtensionCliCommand.list}
|
|
1234
|
-
* @template {ExtensionType} ExtType
|
|
1235
|
-
* @typedef {Partial<ExtManifest<ExtType>> & Partial<ExtensionListMetadata>} ExtensionListData
|
|
1236
|
-
*/
|
|
1237
|
-
|
|
1238
|
-
/**
|
|
1239
|
-
* @template {ExtensionType} ExtType
|
|
1240
|
-
* @typedef {ExtManifest<ExtType> & ExtensionListMetadata} InstalledExtensionListData
|
|
1241
|
-
*/
|
|
1242
|
-
|
|
1243
|
-
/**
|
|
1244
|
-
* Return value of {@linkcode ExtensionCliCommand.list}.
|
|
1245
|
-
* @template {ExtensionType} ExtType
|
|
1246
|
-
* @typedef {Record<string,ExtensionListData<ExtType>>} ExtensionList
|
|
1247
|
-
*/
|
|
1248
|
-
|
|
1249
1190
|
/**
|
|
1250
1191
|
* Options for {@linkcode ExtensionCliCommand._run}.
|
|
1251
|
-
* @typedef RunOptions
|
|
1252
|
-
* @property {string} installSpec - name of the extension to run a script from
|
|
1253
|
-
* @property {string} [scriptName] - name of the script to run. If not provided
|
|
1254
|
-
* then all available script names will be printed
|
|
1255
|
-
* @property {string[]} [extraArgs] - arguments to pass to the script
|
|
1256
|
-
* @property {boolean} [bufferOutput] - if true, will buffer the output of the script and return it
|
|
1257
1192
|
*/
|
|
1193
|
+
type RunOptions = {
|
|
1194
|
+
installSpec: string;
|
|
1195
|
+
scriptName?: string;
|
|
1196
|
+
extraArgs?: string[];
|
|
1197
|
+
bufferOutput?: boolean;
|
|
1198
|
+
};
|
|
1258
1199
|
|
|
1259
1200
|
/**
|
|
1260
1201
|
* Options for {@linkcode ExtensionCliCommand.doctor}.
|
|
1261
|
-
* @typedef DoctorOptions
|
|
1262
|
-
* @property {string} installSpec - name of the extension to run doctor checks for
|
|
1263
1202
|
*/
|
|
1203
|
+
type DoctorOptions = {installSpec: string};
|
|
1264
1204
|
|
|
1265
1205
|
/**
|
|
1266
1206
|
* Return value of {@linkcode ExtensionCliCommand._run}
|
|
1267
|
-
*
|
|
1268
|
-
* @typedef RunOutput
|
|
1269
|
-
* @property {string[]} [output] - script output if `bufferOutput` was `true` in {@linkcode RunOptions}
|
|
1270
1207
|
*/
|
|
1208
|
+
export type RunOutput = {output?: string[]};
|
|
1271
1209
|
|
|
1272
1210
|
/**
|
|
1273
|
-
*
|
|
1274
|
-
* @typedef ExtensionUpdateOpts
|
|
1275
|
-
* @property {string} installSpec - the name of the extension to update
|
|
1276
|
-
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
|
|
1211
|
+
* Return type of {@linkcode ExtensionCliCommand.getPostInstallText}.
|
|
1277
1212
|
*/
|
|
1213
|
+
export type PostInstallText = string;
|
|
1278
1214
|
|
|
1279
1215
|
/**
|
|
1280
|
-
*
|
|
1281
|
-
* @typedef ExtensionUpdateResult
|
|
1282
|
-
* @property {Record<string,Error>} errors - map of ext names to error objects
|
|
1283
|
-
* @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
|
|
1216
|
+
* Options for {@linkcode ExtensionCliCommand._update}.
|
|
1284
1217
|
*/
|
|
1218
|
+
type ExtensionUpdateOpts = {installSpec: string; unsafe: boolean};
|
|
1285
1219
|
|
|
1286
1220
|
/**
|
|
1287
1221
|
* Part of result of {@linkcode ExtensionCliCommand._update}.
|
|
1288
|
-
* @typedef UpdateReport
|
|
1289
|
-
* @property {string} from - version the extension was updated from
|
|
1290
|
-
* @property {string} to - version the extension was updated to
|
|
1291
1222
|
*/
|
|
1223
|
+
type UpdateReport = {from: string; to: string | null};
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Return value of {@linkcode ExtensionCliCommand._update}.
|
|
1227
|
+
*/
|
|
1228
|
+
export type ExtensionUpdateResult = {errors: Record<string, Error>; updates: Record<string, UpdateReport>};
|
|
1292
1229
|
|
|
1293
1230
|
/**
|
|
1294
1231
|
* Options for {@linkcode ExtensionCliCommand._uninstall}.
|
|
1295
|
-
* @typedef UninstallOpts
|
|
1296
|
-
* @property {string} installSpec - the name or spec of an extension to uninstall
|
|
1297
1232
|
*/
|
|
1233
|
+
type UninstallOpts = {installSpec: string};
|
|
1298
1234
|
|
|
1299
1235
|
/**
|
|
1300
1236
|
* Used by {@linkcode ExtensionCliCommand.getPostInstallText}
|
|
1301
|
-
* @typedef ExtensionArgs
|
|
1302
|
-
* @property {string} extName - the name of an extension
|
|
1303
|
-
* @property {object} extData - the data for an installed extension
|
|
1304
1237
|
*/
|
|
1238
|
+
export type ExtensionArgs<ExtType extends ExtensionType = ExtensionType> = {
|
|
1239
|
+
extName: string;
|
|
1240
|
+
extData: ExtInstallReceipt<ExtType>;
|
|
1241
|
+
};
|
|
1305
1242
|
|
|
1306
1243
|
/**
|
|
1307
1244
|
* Options for {@linkcode ExtensionCliCommand.installViaNpm}
|
|
1308
|
-
* @typedef InstallViaNpmArgs
|
|
1309
|
-
* @property {string} installSpec - the name or spec of an extension to install
|
|
1310
|
-
* @property {string} pkgName - the NPM package name of the extension
|
|
1311
|
-
* @property {import('appium/types').InstallType} installType - type of install
|
|
1312
|
-
* @property {string} [pkgVer] - the specific version of the NPM package
|
|
1313
1245
|
*/
|
|
1246
|
+
type InstallViaNpmArgs = {
|
|
1247
|
+
installSpec: string;
|
|
1248
|
+
pkgName: string;
|
|
1249
|
+
installType: InstallType;
|
|
1250
|
+
pkgVer?: string;
|
|
1251
|
+
};
|
|
1314
1252
|
|
|
1315
1253
|
/**
|
|
1316
1254
|
* Object returned by {@linkcode ExtensionCliCommand.checkForExtensionUpdate}
|
|
1317
|
-
* @typedef PossibleUpdates
|
|
1318
|
-
* @property {string} current - current version
|
|
1319
|
-
* @property {string?} safeUpdate - version we can safely update to if it exists, or null
|
|
1320
|
-
* @property {string?} unsafeUpdate - version we can unsafely update to if it exists, or null
|
|
1321
1255
|
*/
|
|
1256
|
+
type PossibleUpdates = {current: string; safeUpdate: string | null; unsafeUpdate: string | null};
|
|
1322
1257
|
|
|
1323
1258
|
/**
|
|
1324
1259
|
* Options for {@linkcode ExtensionCliCommand._install}
|
|
1325
|
-
* @typedef InstallOpts
|
|
1326
|
-
* @property {string} installSpec - the name or spec of an extension to install
|
|
1327
|
-
* @property {InstallType} installType - how to install this extension. One of the INSTALL_TYPES
|
|
1328
|
-
* @property {string} [packageName] - for git/github installs, the extension node package name
|
|
1329
1260
|
*/
|
|
1261
|
+
type InstallOpts = {installSpec: string; installType: InstallType; packageName?: string};
|
|
1330
1262
|
|
|
1331
|
-
|
|
1332
|
-
* @template {ExtensionType} ExtType
|
|
1333
|
-
* @typedef {ExtType extends DriverType ? typeof import('../constants').KNOWN_DRIVERS : ExtType extends PluginType ? typeof import('../constants').KNOWN_PLUGINS : never} KnownExtensions
|
|
1334
|
-
*/
|
|
1263
|
+
type ListOptions = {showInstalled: boolean; showUpdates: boolean; verbose?: boolean};
|
|
1335
1264
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1265
|
+
type GetInstallationReceiptOpts<ExtType extends ExtensionType = ExtensionType> = {
|
|
1266
|
+
installPath: string;
|
|
1267
|
+
installSpec: string;
|
|
1268
|
+
pkg: ExtPackageJson<ExtType>;
|
|
1269
|
+
installType: InstallType;
|
|
1270
|
+
};
|
|
1342
1271
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
* @property {string} installPath
|
|
1348
|
-
* @property {string} installSpec
|
|
1349
|
-
* @property {ExtPackageJson<ExtType>} pkg
|
|
1350
|
-
* @property {InstallType} installType
|
|
1351
|
-
*/
|
|
1272
|
+
type InstalledExtensionLike = {installType?: InstallType; installPath?: string};
|
|
1273
|
+
|
|
1274
|
+
export default ExtensionCliCommand;
|
|
1275
|
+
export {ExtensionCliCommand as ExtensionCommand};
|
|
1352
1276
|
|
|
1353
|
-
/**
|
|
1354
|
-
* @typedef {import('appium/types').InstallType} InstallType
|
|
1355
|
-
*/
|