From 7abc297c02b673a706f1539d0ef49aff92208616 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 27 Apr 2026 15:46:47 +0200 Subject: [PATCH 1/3] feat: Ditch bluebird, lodash and plist --- lib/helpers.ts | 3 +-- package.json | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/helpers.ts b/lib/helpers.ts index e07c2f9..0f6fd2a 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,5 +1,4 @@ import _ from 'lodash'; -import B from 'bluebird'; import {exec} from 'teen_process'; import type {TeenProcessExecResult} from 'teen_process'; import {fs, plist} from '@appium/support'; @@ -56,7 +55,7 @@ export async function findAppPaths(bundleId: string): Promise { } })(), ); - return (await B.all(results)).filter(Boolean) as string[]; + return (await Promise.all(results)).filter(Boolean) as string[]; } /** diff --git a/package.json b/package.json index 3d38659..d755d58 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,7 @@ "dependencies": { "@appium/support": "^7.0.0-rc.1", "asyncbox": "^6.0.1", - "bluebird": "^3.7.2", "lodash": "^4.17.4", - "plist": "^3.0.1", "semver": "^7.0.0", "teen_process": "^4.0.4" }, @@ -60,7 +58,6 @@ "@appium/types": "^1.0.0-rc.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", - "@types/bluebird": "^3.5.38", "@types/lodash": "^4.14.196", "@types/mocha": "^10.0.1", "@types/node": "^25.0.0", From 73d2fb78a34ee7e22b2902b88787676ce4b97b06 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 27 Apr 2026 16:28:42 +0200 Subject: [PATCH 2/3] moar --- lib/helpers.ts | 28 +++++++++-- lib/xcode.ts | 13 +++--- package.json | 2 - test/e2e/xcode-specs.js | 96 +++++++++++++++++--------------------- test/unit/helpers-specs.js | 36 ++++++++++++++ test/unit/index-specs.js | 10 +--- 6 files changed, 112 insertions(+), 73 deletions(-) create mode 100644 test/unit/helpers-specs.js diff --git a/lib/helpers.ts b/lib/helpers.ts index 0f6fd2a..49ade39 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import {exec} from 'teen_process'; import type {TeenProcessExecResult} from 'teen_process'; import {fs, plist} from '@appium/support'; @@ -6,6 +5,25 @@ import path from 'node:path'; export const XCRUN_TIMEOUT = 15000; +/** + * Memoizes function calls by caching results for serialized argument lists. + * + * @param fn The function to memoize + * @returns A memoized wrapper around the input function + */ +export function memoize( + fn: (...args: Args) => Result, +): (...args: Args) => Result { + const cache = new Map(); + return (...args: Args): Result => { + const key = JSON.stringify(args); + if (!cache.has(key)) { + cache.set(key, fn(...args)); + } + return cache.get(key) as Result; + }; +} + /** * Executes 'xcrun' command line utility * @@ -44,8 +62,12 @@ export async function findAppPaths(bundleId: string): Promise { return []; } - const matchedPaths = _.trim(stdout).split('\n').map(_.trim).filter(Boolean); - if (_.isEmpty(matchedPaths)) { + const matchedPaths = stdout + .trim() + .split('\n') + .map((p) => p.trim()) + .filter(Boolean); + if (!matchedPaths.length) { return []; } const results = matchedPaths.map((p) => diff --git a/lib/xcode.ts b/lib/xcode.ts index a03a120..c3fa6bf 100644 --- a/lib/xcode.ts +++ b/lib/xcode.ts @@ -1,10 +1,9 @@ import {fs, logger} from '@appium/support'; import path from 'node:path'; import {retry} from 'asyncbox'; -import _ from 'lodash'; import {exec} from 'teen_process'; import * as semver from 'semver'; -import {runXcrunCommand, findAppPaths, XCRUN_TIMEOUT, readXcodePlist} from './helpers'; +import {runXcrunCommand, findAppPaths, XCRUN_TIMEOUT, readXcodePlist, memoize} from './helpers'; import type {XcodeVersion} from './types'; const DEFAULT_NUMBER_OF_RETRIES = 2; @@ -22,7 +21,7 @@ const log = logger.getLogger('Xcode'); export async function getPathFromXcodeSelect(timeout: number = XCRUN_TIMEOUT): Promise { const generateErrorMessage = async (prefix: string): Promise => { const xcodePaths = await findAppPaths(XCODE_BUNDLE_ID); - if (_.isEmpty(xcodePaths)) { + if (!xcodePaths.length) { return `${prefix}. Consider installing Xcode to address this issue.`; } @@ -91,7 +90,7 @@ export async function getPathFromDeveloperDir(): Promise { * @returns Full path to Xcode Developer subfolder timeout * @throws {Error} If there was an error while retrieving the path. */ -export const getPath = _.memoize( +export const getPath = memoize( (timeout: number = XCRUN_TIMEOUT): Promise => process.env.DEVELOPER_DIR ? getPathFromDeveloperDir() : getPathFromXcodeSelect(timeout), ); @@ -189,7 +188,7 @@ export async function getMaxIOSSDKWithoutRetry(timeout: number = XCRUN_TIMEOUT): * @returns The SDK version * @throws {Error} If the SDK version number cannot be determined */ -export const getMaxIOSSDK = _.memoize(function getMaxIOSSDK( +export const getMaxIOSSDK = memoize(function getMaxIOSSDK( retries: number = DEFAULT_NUMBER_OF_RETRIES, timeout: number = XCRUN_TIMEOUT, ) { @@ -221,7 +220,7 @@ export async function getMaxTVOSSDKWithoutRetry(timeout: number = XCRUN_TIMEOUT) * @returns The SDK version * @throws {Error} If the SDK version number cannot be determined */ -export const getMaxTVOSSDK = _.memoize(async function getMaxTVOSSDK( +export const getMaxTVOSSDK = memoize(async function getMaxTVOSSDK( retries: number = DEFAULT_NUMBER_OF_RETRIES, timeout: number = XCRUN_TIMEOUT, ): Promise { @@ -254,7 +253,7 @@ async function getVersionWithoutRetry( * @returns Xcode version * @throws {Error} If there was a failure while retrieving the version */ -const getVersionMemoized = _.memoize(function getVersionMemoized( +const getVersionMemoized = memoize(function getVersionMemoized( retries: number = DEFAULT_NUMBER_OF_RETRIES, timeout: number = XCRUN_TIMEOUT, ): Promise { diff --git a/package.json b/package.json index d755d58..2824ff5 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "dependencies": { "@appium/support": "^7.0.0-rc.1", "asyncbox": "^6.0.1", - "lodash": "^4.17.4", "semver": "^7.0.0", "teen_process": "^4.0.4" }, @@ -58,7 +57,6 @@ "@appium/types": "^1.0.0-rc.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", - "@types/lodash": "^4.14.196", "@types/mocha": "^10.0.1", "@types/node": "^25.0.0", "chai": "^6.0.0", diff --git a/test/e2e/xcode-specs.js b/test/e2e/xcode-specs.js index cb0db74..4b74f5d 100644 --- a/test/e2e/xcode-specs.js +++ b/test/e2e/xcode-specs.js @@ -1,36 +1,27 @@ import * as xcode from '../../lib/xcode'; import {fs, util} from '@appium/support'; -import _ from 'lodash'; +import {expect, use} from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +use(chaiAsPromised); describe('xcode @skip-linux', function () { // on slow machines and busy CI systems these can be slow and flakey this.timeout(30000); - let chai; - let chaiAsPromised; - let should; - - before(async function () { - chai = await import('chai'); - chaiAsPromised = await import('chai-as-promised'); - - should = chai.should(); - chai.use(chaiAsPromised.default); - }); - describe('getPath', function () { it('should get the path to xcode from xcode-select', async function () { - const path = await xcode.getPathFromXcodeSelect(); - should.exist(path); - await fs.exists(path); + const xcodePath = await xcode.getPathFromXcodeSelect(); + expect(xcodePath).to.exist; + await fs.exists(xcodePath); }); it('should get the path to xcode if provided in DEVELOPER_DIR', async function () { process.env.DEVELOPER_DIR = await xcode.getPathFromXcodeSelect(); try { - const path = await xcode.getPathFromDeveloperDir(); - should.exist(path); - await fs.exists(path); + const xcodePath = await xcode.getPathFromDeveloperDir(); + expect(xcodePath).to.exist; + await fs.exists(xcodePath); } finally { delete process.env.DEVELOPER_DIR; } @@ -39,15 +30,15 @@ describe('xcode @skip-linux', function () { it('should fail if the path to xcode provided in DEVELOPER_DIR is wrong', async function () { process.env.DEVELOPER_DIR = 'yolo'; try { - await xcode.getPathFromDeveloperDir().should.eventually.be.rejected; + await expect(xcode.getPathFromDeveloperDir()).to.be.rejected; } finally { delete process.env.DEVELOPER_DIR; } }); it('should get the path to xcode', async function () { - const path = await xcode.getPath(); - path.should.eql(await xcode.getPathFromXcodeSelect()); + const xcodePath = await xcode.getPath(); + expect(xcodePath).to.eql(await xcode.getPathFromXcodeSelect()); }); }); @@ -55,10 +46,10 @@ describe('xcode @skip-linux', function () { let versionRE = /\d\.\d\.*\d*/; it('should get the version of xcode', async function () { - let version = /** @type {string} */ (await xcode.getVersion()); - should.exist(version); - _.isString(version).should.be.true; - versionRE.test(version).should.be.true; + const version = await xcode.getVersion(); + expect(version).to.exist; + expect(version).to.be.a('string'); + expect(versionRE.test(version)).to.be.true; }); it('should get the path and version again, these values are cached', async function () { @@ -66,55 +57,54 @@ describe('xcode @skip-linux', function () { await xcode.getVersion(); let before = Number(new Date()); - let path = await xcode.getPath(); + const path = await xcode.getPath(); let after = Number(new Date()); - should.exist(path); + expect(path).to.exist; await fs.exists(path); - (after - before).should.be.at.most(2); + expect(after - before).to.be.at.most(2); before = Number(new Date()); - let version = /** @type {string} */ (await xcode.getVersion()); + const version = await xcode.getVersion(); after = Number(new Date()); - should.exist(version); - _.isString(version).should.be.true; - versionRE.test(version).should.be.true; - (after - before).should.be.at.most(2); + expect(version).to.exist; + expect(version).to.be.a('string'); + expect(versionRE.test(version)).to.be.true; + expect(after - before).to.be.at.most(2); }); it('should get the parsed version', async function () { - let nonParsedVersion = await xcode.getVersion(); - let version = /** @type {import('../../lib/xcode').XcodeVersion} */ ( - await xcode.getVersion(true) - ); - should.exist(version); - _.isString(version.versionString).should.be.true; - version.versionString.should.eql(nonParsedVersion); - - parseFloat(String(version.versionFloat)).should.equal(version.versionFloat); - parseInt(String(version.major), 10).should.equal(version.major); - parseInt(String(version.minor), 10).should.equal(version.minor); + const nonParsedVersion = await xcode.getVersion(); + const version = await xcode.getVersion(true); + expect(version).to.exist; + expect(version.versionString).to.be.a('string'); + expect(version.versionString).to.eql(nonParsedVersion); + + expect(parseFloat(String(version.versionFloat))).to.equal(version.versionFloat); + expect(parseInt(String(version.major), 10)).to.equal(version.major); + expect(parseInt(String(version.minor), 10)).to.equal(version.minor); }); }); it('should get clang version', async function () { const cliVersion = await xcode.getClangVersion(); - _.isString(util.coerceVersion(/** @type {string} */ (cliVersion), true)).should.be.true; + expect(cliVersion).to.exist; + expect(util.coerceVersion(cliVersion, true)).to.be.a('string'); }); it('should get max iOS SDK version', async function () { - let version = await xcode.getMaxIOSSDK(); + const version = await xcode.getMaxIOSSDK(); - should.exist(version); - (typeof version).should.equal('string'); - (parseFloat(String(version)) - 6.1).should.be.at.least(0); + expect(version).to.exist; + expect(version).to.be.a('string'); + expect(parseFloat(String(version)) - 6.1).to.be.at.least(0); }); it('should get max tvOS SDK version', async function () { - let version = await xcode.getMaxTVOSSDK(); + const version = await xcode.getMaxTVOSSDK(); - should.exist(version); - (typeof version).should.equal('string'); + expect(version).to.exist; + expect(version).to.be.a('string'); }); }); diff --git a/test/unit/helpers-specs.js b/test/unit/helpers-specs.js new file mode 100644 index 0000000..1f4710d --- /dev/null +++ b/test/unit/helpers-specs.js @@ -0,0 +1,36 @@ +import {expect} from 'chai'; +import {memoize} from '../../lib/helpers'; + +describe('helpers', function () { + describe('memoize', function () { + it('should cache the result for identical arguments', function () { + let callCount = 0; + const add = memoize((a, b) => { + callCount += 1; + return a + b; + }); + + const result1 = add(1, 2); + const result2 = add(1, 2); + + expect(result1).to.equal(3); + expect(result2).to.equal(3); + expect(callCount).to.equal(1); + }); + + it('should not reuse cache for different arguments', function () { + let callCount = 0; + const add = memoize((a, b) => { + callCount += 1; + return a + b; + }); + + const result1 = add(1, 2); + const result2 = add(2, 3); + + expect(result1).to.equal(3); + expect(result2).to.equal(5); + expect(callCount).to.equal(2); + }); + }); +}); diff --git a/test/unit/index-specs.js b/test/unit/index-specs.js index c1b7895..e254036 100644 --- a/test/unit/index-specs.js +++ b/test/unit/index-specs.js @@ -1,14 +1,8 @@ import xcode from '../../lib/index'; +import {expect} from 'chai'; describe('index', function () { - let chai; - - before(async function () { - chai = await import('chai'); - chai.should(); - }); - it('exported objects should exist', function () { - xcode.should.exist; + expect(xcode).to.exist; }); }); From e7d68e5fca3ec531b1d774c58a310aca25140c39 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 27 Apr 2026 17:09:47 +0200 Subject: [PATCH 3/3] migrate tests --- package.json | 4 ++-- test/e2e/{xcode-specs.js => xcode-specs.ts} | 20 +++++++++---------- .../{helpers-specs.js => helpers-specs.ts} | 4 ++-- test/unit/{index-specs.js => index-specs.ts} | 2 +- tsconfig.json | 5 +++-- 5 files changed, 18 insertions(+), 17 deletions(-) rename test/e2e/{xcode-specs.js => xcode-specs.ts} (88%) rename test/unit/{helpers-specs.js => helpers-specs.ts} (88%) rename test/unit/{index-specs.js => index-specs.ts} (100%) diff --git a/package.json b/package.json index 2824ff5..bb3934b 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ "format": "prettier -w ./lib ./test", "format:check": "prettier --check ./lib ./test", "prepare": "npm run build", - "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.js\"", - "e2e-test": "mocha --exit --timeout 5m \"./test/e2e/**/*-specs.js\"" + "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.ts\"", + "e2e-test": "mocha --exit --timeout 5m \"./test/e2e/**/*-specs.ts\"" }, "prettier": { "bracketSpacing": false, diff --git a/test/e2e/xcode-specs.js b/test/e2e/xcode-specs.ts similarity index 88% rename from test/e2e/xcode-specs.js rename to test/e2e/xcode-specs.ts index 4b74f5d..12159e5 100644 --- a/test/e2e/xcode-specs.js +++ b/test/e2e/xcode-specs.ts @@ -1,7 +1,7 @@ -import * as xcode from '../../lib/xcode'; import {fs, util} from '@appium/support'; import {expect, use} from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import * as xcode from '../../lib/xcode'; use(chaiAsPromised); @@ -43,10 +43,10 @@ describe('xcode @skip-linux', function () { }); describe('getVersion', function () { - let versionRE = /\d\.\d\.*\d*/; + const versionRE = /\d\.\d\.*\d*/; it('should get the version of xcode', async function () { - const version = await xcode.getVersion(); + const version = await xcode.getVersion(false); expect(version).to.exist; expect(version).to.be.a('string'); expect(versionRE.test(version)).to.be.true; @@ -54,18 +54,18 @@ describe('xcode @skip-linux', function () { it('should get the path and version again, these values are cached', async function () { await xcode.getPath(); - await xcode.getVersion(); + await xcode.getVersion(false); let before = Number(new Date()); - const path = await xcode.getPath(); + const xcodePath = await xcode.getPath(); let after = Number(new Date()); - expect(path).to.exist; - await fs.exists(path); + expect(xcodePath).to.exist; + await fs.exists(xcodePath); expect(after - before).to.be.at.most(2); before = Number(new Date()); - const version = await xcode.getVersion(); + const version = await xcode.getVersion(false); after = Number(new Date()); expect(version).to.exist; @@ -75,7 +75,7 @@ describe('xcode @skip-linux', function () { }); it('should get the parsed version', async function () { - const nonParsedVersion = await xcode.getVersion(); + const nonParsedVersion = await xcode.getVersion(false); const version = await xcode.getVersion(true); expect(version).to.exist; expect(version.versionString).to.be.a('string'); @@ -90,7 +90,7 @@ describe('xcode @skip-linux', function () { it('should get clang version', async function () { const cliVersion = await xcode.getClangVersion(); expect(cliVersion).to.exist; - expect(util.coerceVersion(cliVersion, true)).to.be.a('string'); + expect(util.coerceVersion(cliVersion!, true)).to.be.a('string'); }); it('should get max iOS SDK version', async function () { diff --git a/test/unit/helpers-specs.js b/test/unit/helpers-specs.ts similarity index 88% rename from test/unit/helpers-specs.js rename to test/unit/helpers-specs.ts index 1f4710d..7366388 100644 --- a/test/unit/helpers-specs.js +++ b/test/unit/helpers-specs.ts @@ -5,7 +5,7 @@ describe('helpers', function () { describe('memoize', function () { it('should cache the result for identical arguments', function () { let callCount = 0; - const add = memoize((a, b) => { + const add = memoize((a: number, b: number) => { callCount += 1; return a + b; }); @@ -20,7 +20,7 @@ describe('helpers', function () { it('should not reuse cache for different arguments', function () { let callCount = 0; - const add = memoize((a, b) => { + const add = memoize((a: number, b: number) => { callCount += 1; return a + b; }); diff --git a/test/unit/index-specs.js b/test/unit/index-specs.ts similarity index 100% rename from test/unit/index-specs.js rename to test/unit/index-specs.ts index e254036..fa89766 100644 --- a/test/unit/index-specs.js +++ b/test/unit/index-specs.ts @@ -1,5 +1,5 @@ -import xcode from '../../lib/index'; import {expect} from 'chai'; +import xcode from '../../lib/index'; describe('index', function () { it('exported objects should exist', function () { diff --git a/tsconfig.json b/tsconfig.json index 0237410..3df5858 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "extends": "@appium/tsconfig/tsconfig.json", "compilerOptions": { "outDir": "build", - "types": ["node"], + "types": ["node", "mocha"], "checkJs": true }, "ts-node": { @@ -13,6 +13,7 @@ } }, "include": [ - "lib" + "lib", + "test" ] }