UNPKG

7.35 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3// @ts-check
4
5/* eslint-disable no-console, promise/prefer-await-to-then */
6
7/**
8 * This script is intended to be run as a `postinstall` lifecycle script,
9 * and will automatically install extensions if requested by the user.
10 *
11 * If the current working directory is within a project which has `appium`
12 * as a dependency, this script does nothing; extensions must be managed
13 * via `npm` or another package manager.
14 *
15 * If `CI=1` is in the environment, this script will exit with a non-zero
16 * code upon failure (which will typically break a build). Otherwise, it
17 * will always exit with code 0, even if errors occur.
18 *
19 * @module
20 * @example
21 * `npm install -g appium --drivers=uiautomator2,xcuitest --plugins=images`
22 */
23
24const B = require('bluebird');
25const path = require('node:path');
26const {realpath} = require('node:fs/promises');
27
28B.config({
29 cancellation: true,
30});
31
32/** @type {typeof import('../lib/cli/extension').runExtensionCommand} */
33let runExtensionCommand;
34/** @type {typeof import('../lib/constants').DRIVER_TYPE} */
35let DRIVER_TYPE;
36/** @type {typeof import('../lib/constants').PLUGIN_TYPE} */
37let PLUGIN_TYPE;
38/** @type {typeof import('../lib/extension').loadExtensions} */
39let loadExtensions;
40
41const _ = require('lodash');
42const wrap = _.partial(
43 require('wrap-ansi'),
44 _,
45 process.stderr.columns ?? process.stdout.columns ?? 80
46);
47const ora = require('ora');
48
49/** @type {typeof import('@appium/support').env} */
50let env;
51/** @type {typeof import('@appium/support').util} */
52let util;
53/** @type {typeof import('@appium/support').logger} */
54let logger;
55
56function log(message) {
57 console.error(wrap(`[Appium] ${message}`));
58}
59
60/**
61 * This is a naive attempt at determining whether or not we are in a dev environment; in other
62 * words, is `postinstall` being run from within the `appium` monorepo?
63 *
64 * When we're in the monorepo, `npm_config_local_prefix` will be set to the root of the monorepo root
65 * dir when running this lifecycle script from an `npm install` in the monorepo root.
66 *
67 * `realpath` is necessary due to macOS omitting `/private` from paths
68 */
69async function isDevEnvironment() {
70 return (
71 process.env.npm_config_local_prefix &&
72 path.join(process.env.npm_config_local_prefix, 'packages', 'appium') ===
73 (await realpath(path.join(__dirname, '..')))
74 );
75}
76
77/**
78 * Setup / check environment if we should do anything here
79 * @returns {Promise<boolean>} `true` if Appium is built and ready to go
80 */
81async function init() {
82 if (await isDevEnvironment()) {
83 log('Dev environment likely; skipping automatic installation of extensions');
84 return false;
85 }
86 try {
87 ({env, util, logger} = require('@appium/support'));
88 // @ts-ignore
89 ({runExtensionCommand} = require('../build/lib/cli/extension'));
90 ({DRIVER_TYPE, PLUGIN_TYPE} = require('../build/lib/constants'));
91 // @ts-ignore
92 ({loadExtensions} = require('../build/lib/extension'));
93 logger.getLogger('Appium').level = 'error';
94
95 // if we're doing `npm install -g appium` then we will assume we don't have a local appium.
96 if (!process.env.npm_config_global && (await env.hasAppiumDependency(process.cwd()))) {
97 log(`Found local Appium installation; skipping automatic installation of extensions.`);
98 return false;
99 }
100 return true;
101 } catch {
102 log('Dev environment likely; skipping automatic installation of extensions');
103 return false;
104 }
105}
106
107async function main() {
108 if (!(await init())) {
109 return;
110 }
111
112 const driverEnv = process.env.npm_config_drivers;
113 const pluginEnv = process.env.npm_config_plugins;
114
115 const spinner = ora({
116 text: 'Looking for extensions to automatically install...',
117 prefixText: '[Appium]',
118 }).start();
119
120 if (!driverEnv && !pluginEnv) {
121 spinner.succeed(
122 wrap(`No drivers or plugins to automatically install.
123 If desired, provide arguments with comma-separated values "--drivers=<known_driver>[,known_driver...]" and/or "--plugins=<known_plugin>[,known_plugin...]" to the "npm install appium" command. The specified extensions will be installed automatically with Appium. Note: to see the list of known extensions, run "appium <driver|plugin> list".`)
124 );
125 return;
126 }
127
128 /**
129 * @type {[[typeof DRIVER_TYPE, string?], [typeof PLUGIN_TYPE, string?]]}
130 */
131 const specs = [
132 [DRIVER_TYPE, driverEnv],
133 [PLUGIN_TYPE, pluginEnv],
134 ];
135
136 spinner.start('Resolving Appium home directory...');
137 const appiumHome = await env.resolveAppiumHome();
138 spinner.succeed(`Found Appium home: ${appiumHome}`);
139
140 spinner.start('Loading extension data...');
141 const {driverConfig, pluginConfig} = await loadExtensions(appiumHome);
142 spinner.succeed('Loaded extension data.');
143
144 const installedStats = {[DRIVER_TYPE]: 0, [PLUGIN_TYPE]: 0};
145 for (const [type, extEnv] of specs) {
146 if (extEnv) {
147 for await (let ext of extEnv.split(',')) {
148 ext = ext.trim();
149 try {
150 await checkAndInstallExtension({
151 runExtensionCommand,
152 appiumHome,
153 type,
154 ext,
155 driverConfig,
156 pluginConfig,
157 spinner,
158 });
159 installedStats[type]++;
160 } catch (e) {
161 spinner.fail(`Could not install ${type} "${ext}": ${e.message}`);
162 if (process.env.CI) {
163 process.exitCode = 1;
164 }
165 return;
166 }
167 }
168 }
169 }
170 spinner.succeed(
171 `Done. ${installedStats[DRIVER_TYPE]} ${util.pluralize(
172 'driver',
173 installedStats[DRIVER_TYPE]
174 )} and ${installedStats[PLUGIN_TYPE]} ${util.pluralize(
175 'plugin',
176 installedStats[PLUGIN_TYPE]
177 )} are installed.`
178 );
179}
180
181/**
182 * @privateRemarks the two `@ts-ignore` directives here are because I have no idea what's wrong with
183 * the types and don't want to spend more time on it. regardless, it seems to work for now.
184 * @param {CheckAndInstallExtensionsOpts} opts
185 */
186async function checkAndInstallExtension({
187 runExtensionCommand,
188 appiumHome,
189 type,
190 ext,
191 driverConfig,
192 pluginConfig,
193 spinner,
194}) {
195 const extList = await runExtensionCommand(
196 // @ts-ignore
197 {
198 appiumHome,
199 subcommand: type,
200 [`${type}Command`]: 'list',
201 showInstalled: true,
202 suppressOutput: true,
203 },
204 type === DRIVER_TYPE ? driverConfig : pluginConfig
205 );
206 if (extList[ext]) {
207 spinner.info(`The ${type} "${ext}" is already installed.`);
208 return;
209 }
210 spinner.start(`Installing ${type} "${ext}"...`);
211 await runExtensionCommand(
212 // @ts-ignore
213 {
214 subcommand: type,
215 appiumHome,
216 [`${type}Command`]: 'install',
217 suppressOutput: true,
218 [type]: ext,
219 },
220 type === DRIVER_TYPE ? driverConfig : pluginConfig
221 );
222 spinner.succeed(`Installed ${type} "${ext}".`);
223}
224
225if (require.main === module) {
226 main().catch((e) => {
227 log(e);
228 process.exitCode = 1;
229 });
230}
231
232module.exports = main;
233
234/**
235 * @typedef CheckAndInstallExtensionsOpts
236 * @property {typeof runExtensionCommand} runExtensionCommand
237 * @property {string} appiumHome
238 * @property {DRIVER_TYPE | PLUGIN_TYPE} type
239 * @property {string} ext
240 * @property {import('../lib/extension/driver-config').DriverConfig} driverConfig
241 * @property {import('../lib/extension/plugin-config').PluginConfig} pluginConfig
242 * @property {import('ora').Ora} spinner
243 */