diff --git a/packages/cli-tools/package.json b/packages/cli-tools/package.json index 909cd8ed7..d45ed88d4 100644 --- a/packages/cli-tools/package.json +++ b/packages/cli-tools/package.json @@ -9,6 +9,7 @@ "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", + "find-up": "^5.0.0", "lodash": "^4.17.15", "mime": "^2.4.1", "node-fetch": "^2.6.0", @@ -21,7 +22,7 @@ "@react-native-community/cli-types": "^9.0.0-alpha.0", "@types/lodash": "^4.14.149", "@types/mime": "^2.0.1", - "@types/node": "^17.0.35", + "@types/node": "^12.0.0", "@types/node-fetch": "^2.5.5" }, "files": [ diff --git a/packages/cli-tools/src/__tests__/resolveNodeModuleDir.test.ts b/packages/cli-tools/src/__tests__/resolveNodeModuleDir.test.ts new file mode 100644 index 000000000..3024af155 --- /dev/null +++ b/packages/cli-tools/src/__tests__/resolveNodeModuleDir.test.ts @@ -0,0 +1,24 @@ +import {getTempDirectory, writeFiles} from '../../../../jest/helpers'; +import resolveNodeModuleDir from '../resolveNodeModuleDir'; +import path from 'path'; + +const DIR = getTempDirectory('resolve_node_module_dir_test'); + +describe('resolveNodeModuleDir', () => { + it('throws an error when node module directory does not exist', () => { + expect(() => + resolveNodeModuleDir(DIR, 'non-existing-package'), + ).toThrowError( + 'Node module directory for package non-existing-package was not found', + ); + }); + + it('returns resolved directory', () => { + writeFiles(DIR, { + 'node_modules/test-package/package.json': '{}', + }); + expect(resolveNodeModuleDir(DIR, 'test-package')).toEqual( + path.join(DIR, 'node_modules/test-package'), + ); + }); +}); diff --git a/packages/cli-tools/src/findPackageDependencyDir.ts b/packages/cli-tools/src/findPackageDependencyDir.ts new file mode 100644 index 000000000..f8e02d0d0 --- /dev/null +++ b/packages/cli-tools/src/findPackageDependencyDir.ts @@ -0,0 +1,102 @@ +/** + * Source vendored from: + * https://github.com/microsoft/rnx-kit/blob/f37adca5161eba66fc27de25d48f72973fff9e8e/packages/tools-node/src/package.ts#L213-L234 + */ +import findUp from 'find-up'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Pick the value for each `key` property from `obj` and return each one in a new object. + * If `names` are given, use them in the new object, instead of `keys`. + * + * If any `key` was not found or its value was `undefined`, nothing will be picked for that key. + * + * @param obj Object to pick from + * @param keys Keys to pick + * @param names Optional names to use in the output object + * @returns A new object containing a each `name` property and the picked value, or `undefined` if no keys were picked. + */ +export function pickValues( + obj: T, + keys: (keyof T)[], + names?: string[], +): Record | undefined { + const finalNames = names ?? keys; + const results: Record = {}; + + let pickedValue = false; + for (let index = 0; index < keys.length; ++index) { + const value = obj[keys[index]]; + if (typeof value !== 'undefined') { + results[finalNames[index].toString()] = value; + pickedValue = true; + } + } + + return pickedValue ? results : undefined; +} + +/** + * Components of a package reference. + */ +export type PackageRef = { + scope?: string; + name: string; +}; + +/** + * Options which control how package dependecies are located. + */ +export type FindPackageDependencyOptions = { + /** + * Optional starting directory for the search. Defaults to `process.cwd()`. + */ + startDir?: string; + + /** + * Optional flag controlling whether symlinks can be found. Defaults to `true`. + * When `false`, and the package dependency directory is a symlink, it will not + * be found. + */ + allowSymlinks?: boolean; + + /** + * Optional flag controlling whether to resolve symlinks. Defaults to `false`. + * Note that this flag has no effect if `allowSymlinks` is `false`. + */ + resolveSymlinks?: boolean; +}; + +/** + * Find the package dependency's directory, starting from the given directory + * and moving outward, through all parent directories. + * + * Package dependencies exist under 'node_modules/[`scope`]/[`name`]'. + * + * @param ref Package dependency reference + * @param options Options which control the search + * @returns Path to the package dependency's directory, or `undefined` if not found. + */ +export function findPackageDependencyDir( + ref: string | PackageRef, + options?: FindPackageDependencyOptions, +): string | undefined { + const pkgName = + typeof ref === 'string' ? ref : path.join(ref.scope ?? '', ref.name); + const packageDir = findUp.sync(path.join('node_modules', pkgName), { + ...pickValues( + options ?? {}, + ['startDir', 'allowSymlinks'], + ['cwd', 'allowSymlinks'], + ), + type: 'directory', + }); + if (!packageDir || !options?.resolveSymlinks) { + return packageDir; + } + + return fs.lstatSync(packageDir).isSymbolicLink() + ? path.resolve(path.dirname(packageDir), fs.readlinkSync(packageDir)) + : packageDir; +} diff --git a/packages/cli-tools/src/resolveNodeModuleDir.ts b/packages/cli-tools/src/resolveNodeModuleDir.ts index 7a03950cd..f7e5c06a3 100644 --- a/packages/cli-tools/src/resolveNodeModuleDir.ts +++ b/packages/cli-tools/src/resolveNodeModuleDir.ts @@ -1,4 +1,5 @@ -import path from 'path'; +import {findPackageDependencyDir} from './findPackageDependencyDir'; +import {CLIError} from './errors'; /** * Finds a path inside `node_modules` @@ -7,9 +8,14 @@ export default function resolveNodeModuleDir( root: string, packageName: string, ): string { - return path.dirname( - require.resolve(path.join(packageName, 'package.json'), { - paths: [root], - }), - ); + const packageDependencyDirectory = findPackageDependencyDir(packageName, { + startDir: root, + }); + if (packageDependencyDirectory === undefined) { + throw new CLIError( + `Node module directory for package ${packageName} was not found`, + ); + } else { + return packageDependencyDirectory; + } } diff --git a/yarn.lock b/yarn.lock index c09bd875c..d87c113aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2623,7 +2623,7 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@^12.0.0", "@types/node@^17.0.35": +"@types/node@*", "@types/node@^12.0.0": version "12.20.47" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.47.tgz#ca9237d51f2a2557419688511dab1c8daf475188" integrity sha512-BzcaRsnFuznzOItW1WpQrDHM7plAa7GIDMZ6b5pnMbkqEtM/6WCOhvZar39oeMQP79gwvFUWjjptE7/KGcNqFg== @@ -5644,6 +5644,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -7856,6 +7864,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -9213,6 +9228,13 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -9234,6 +9256,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map-series@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" @@ -12528,3 +12557,8 @@ yargs@^16.2.0: string-width "^4.2.0" y18n "^5.0.5" yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==