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