feat(appium): Adjust NODE_PATH so NPM could properly resolve component peer dependencies
MODIFY
packages/appium/lib/main.js
packages/appium/lib/main.js
@@ -26,7 +26,7 @@ import {loadExtensions, getActivePlugins, getActiveDrivers} from './extension';
import {DRIVER_TYPE, PLUGIN_TYPE, SERVER_SUBCOMMAND} from './constants';
import registerNode from './grid-register';
import {getDefaultsForSchema, validate} from './schema/schema';
-import {inspect} from './utils';
+import {inspect, adjustNodePath} from './utils';
const {resolveAppiumHome} = env;
@@ -172,6 +172,8 @@ function areServerCommandArgs(args) {
async function init(args) {
const appiumHome = args?.appiumHome ?? (await resolveAppiumHome());
+ await adjustNodePath();
+
const {driverConfig, pluginConfig} = await loadExtensions(appiumHome);
const parser = getParser();

MODIFY
packages/appium/lib/utils.js
packages/appium/lib/utils.js
@@ -2,6 +2,8 @@ import _ from 'lodash';
import logger from './logger';
import {processCapabilities, PROTOCOLS} from '@appium/base-driver';
import {inspect as dump} from 'util';
+import {fs} from '@appium/support';
+import path from 'path';
const W3C_APPIUM_PREFIX = 'appium';
@@ -209,6 +211,77 @@ function getPackageVersion(pkgName) {
return pkgInfo.version;
}
+/**
+ * Adjusts NODE_PATH environment variable,
+ * so drivers and plugins could load their peer dependencies.
+ * Read https://nodejs.org/api/modules.html#loading-from-the-global-folders
+ * for more details.
+ * @returns {Promise<void>}
+ */
+async function adjustNodePath() {
+ const pathParts = path.resolve(...(__filename.split(path.sep)))
+ .split(path.sep)
+ .map((x) => x === '' ? path.sep : x);
+ let nodeModulesRoot = null;
+ for (let pathItemIdx = pathParts.length - 1; pathItemIdx > 0; --pathItemIdx) {
+ const manifestPath = path.join(...(pathParts.slice(0, pathItemIdx)), 'package.json');
+ if (!await fs.exists(manifestPath)) {
+ continue;
+ }
+ try {
+ if (JSON.parse(await fs.readFile(manifestPath, 'utf8')).name === 'appium') {
+ nodeModulesRoot = path.join(...(pathParts.slice(0, pathItemIdx - 1)));
+ break;
+ }
+ } catch (ign) {}
+ }
+ if (!nodeModulesRoot) {
+ return;
+ }
+
+ const refreshRequirePaths = () => {
+ try {
+ // ! This hack allows us to avoid modification of import
+ // ! statements in client modules. It uses a private API though,
+ // ! so it could break (maybe, eventually).
+ // See https://gist.github.com/branneman/8048520#7-the-hack
+ // @ts-ignore
+ require('module').Module._initPaths();
+ return true;
+ } catch (e) {
+ logger.info(`Module init paths cannot be refreshed. Original error: ${e.message}`);
+ return false;
+ }
+ };
+
+ if (!process.env.NODE_PATH) {
+ process.env.NODE_PATH = nodeModulesRoot;
+ if (refreshRequirePaths()) {
+ logger.info(`Setting NODE_PATH to '${nodeModulesRoot}'`);
+ process.env.APPIUM_OMIT_PEER_DEPS = '1';
+ } else {
+ delete process.env.NODE_PATH;
+ }
+ return;
+ }
+
+ const nodePathParts = process.env.NODE_PATH.split(path.delimiter);
+ if (nodePathParts.includes(nodeModulesRoot)) {
+ logger.info(`NODE_PATH already includes '${nodeModulesRoot}'`);
+ process.env.APPIUM_OMIT_PEER_DEPS = '1';
+ return;
+ }
+
+ nodePathParts.push(nodeModulesRoot);
+ process.env.NODE_PATH = nodePathParts.join(path.delimiter);
+ if (refreshRequirePaths()) {
+ logger.info(`Adding '${nodeModulesRoot}' to NODE_PATH`);
+ process.env.APPIUM_OMIT_PEER_DEPS = '1';
+ } else {
+ process.env.NODE_PATH = _.without(nodePathParts, nodeModulesRoot).join(path.delimiter);
+ }
+}
+
/**
* Pulls the initial values of Appium settings from the given capabilities argument.
* Each setting item must satisfy the following format:
@@ -250,6 +323,7 @@ export {
getPackageVersion,
pullSettings,
removeAppiumPrefixes,
+ adjustNodePath,
};
/**

MODIFY
packages/appium/test/e2e/cli.e2e.spec.js
packages/appium/test/e2e/cli.e2e.spec.js
@@ -41,6 +41,22 @@ describe('CLI behavior', function () {
resolveFixture('test-driver-invalid-peer-dep/package.json')
);
+ describe('when appium is already installed', function () {
+ before(async function () {
+ appiumHome = await tempDir.openDir();
+ });
+
+ after(async function () {
+ await fs.rimraf(appiumHome);
+ });
+
+ it('should not duplicate it as peer dependencies for drivers', async function () {
+ const run = runAppiumJson(appiumHome);
+ await run([DRIVER_TYPE, INSTALL, 'appium-fake-driver', '--source', 'npm']);
+ await fs.exists(path.join(appiumHome, 'node_modules', 'appium')).should.eventually.be.false;
+ });
+ });
+
describe('when appium is a dependency of the project in the current working directory', function () {
/** @type {string} */
let hashPath;

MODIFY
packages/appium/test/unit/utils.spec.js
packages/appium/test/unit/utils.spec.js
@@ -4,12 +4,14 @@ import {
pullSettings,
removeAppiumPrefixes,
inspect,
+ adjustNodePath,
} from '../../lib/utils';
import {BASE_CAPS, W3C_CAPS} from '../helpers';
import _ from 'lodash';
import {stripColors} from '@colors/colors';
import {createSandbox} from 'sinon';
import logger from '../../lib/logger';
+import {fs} from '@appium/support';
describe('utils', function () {
describe('parseCapsForInnerDriver()', function () {
@@ -252,4 +254,25 @@ describe('utils', function () {
);
});
});
+
+ describe('adjustNodePath()', function () {
+ const prevValue = process.env.NODE_PATH;
+
+ beforeEach(function () {
+ if (process.env.NODE_PATH) {
+ delete process.env.NODE_PATH;
+ }
+ });
+
+ afterEach(function () {
+ if (prevValue) {
+ process.env.NODE_PATH = prevValue;
+ }
+ });
+
+ it('should adjust NODE_PATH', async function () {
+ await adjustNodePath();
+ (await fs.exists(process.env.NODE_PATH)).should.be.true;
+ });
+ });
});

MODIFY
packages/support/lib/npm.js
packages/support/lib/npm.js
@@ -193,20 +193,13 @@ export class NPM {
}
}
- /**
- * If we've found a `package.json` containined the `appiumCreated` property,
- * then we can do whatever we please with it, since we created it. This is
- * likely when `APPIUM_HOME` is the default (in `~/.appium`). In that case,
- * we want `--global-style` to avoid deduping, and we also do not need a
- * `package-lock.json`.
- *
- * If we _haven't_ found such a key, then this `package.json` isn't a
- * "dummy" and is controlled by the user. So we'll just add it as a dev
- * dep; whatever else it does is up to the user's npm config.
- */
- const installOpts = (await hasAppiumDependency(cwd))
- ? ['--save-dev']
- : ['--save-dev', '--save-exact', '--global-style', '--no-package-lock'];
+ const installOpts = ['--save-dev'];
+ if (!await hasAppiumDependency(cwd)) {
+ if (process.env.APPIUM_OMIT_PEER_DEPS) {
+ installOpts.push('--omit=peer');
+ }
+ installOpts.push('--save-exact', '--global-style', '--no-package-lock');
+ }
const res = await this.exec(
'install',

feat(appium): Adjust NODE_PATH so NPM could properly resolve component peer dependencies (#17325)