@oclif/core
Version:
base library for oclif CLIs
186 lines (185 loc) • 7.59 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.debug = debug;
exports.findRoot = findRoot;
const node_path_1 = require("node:path");
const logger_1 = require("../logger");
const fs_1 = require("./fs");
function debug(...scope) {
return (formatter, ...args) => (0, logger_1.getLogger)(['find-root', ...scope].join(':')).debug(formatter, ...args);
}
// essentially just "cd .."
function* up(from) {
while ((0, node_path_1.dirname)(from) !== from) {
yield from;
from = (0, node_path_1.dirname)(from);
}
yield from;
}
/**
* Return the plugin root directory from a given file. This will `cd` up the file system until it finds
* a package.json and then return the dirname of that path.
*
* Example: node_modules/@oclif/plugin-version/dist/index.js -> node_modules/@oclif/plugin-version
*/
async function findPluginRoot(root, name) {
// If we know the plugin name then we just need to traverse the file
// system until we find the directory that matches the plugin name.
debug(name ?? 'root-plugin')(`Finding root starting at ${root}`);
if (name) {
for (const next of up(root)) {
if (next.endsWith((0, node_path_1.basename)(name))) {
debug(name)('Found root based on plugin name!');
return next;
}
}
}
// If there's no plugin name (typically just the root plugin), then we need
// to traverse the file system until we find a directory with a package.json
for (const next of up(root)) {
// Skip the bin directory
if ((0, node_path_1.basename)((0, node_path_1.dirname)(next)) === 'bin' &&
['dev', 'dev.cmd', 'dev.js', 'run', 'run.cmd', 'run.js'].includes((0, node_path_1.basename)(next))) {
continue;
}
try {
const cur = (0, node_path_1.join)(next, 'package.json');
debug(name ?? 'root-plugin')(`Checking ${cur}`);
if (await (0, fs_1.safeReadJson)(cur)) {
debug(name ?? 'root-plugin')('Found root by traversing up from starting point!');
return (0, node_path_1.dirname)(cur);
}
}
catch { }
}
}
/**
* Find plugin root directory for plugins installed into node_modules that don't have a `main` or `export`.
* This will go up directories until it finds a directory with the plugin installed into it.
*
* See https://github.com/oclif/config/pull/289#issuecomment-983904051
*/
async function findRootLegacy(name, root) {
debug(name ?? 'root-plugin')('Finding root using legacy method');
for (const next of up(root)) {
let cur;
if (name) {
cur = (0, node_path_1.join)(next, 'node_modules', name, 'package.json');
if (await (0, fs_1.safeReadJson)(cur))
return (0, node_path_1.dirname)(cur);
const pkg = await (0, fs_1.safeReadJson)((0, node_path_1.join)(next, 'package.json'));
if (pkg?.name === name)
return next;
}
else {
cur = (0, node_path_1.join)(next, 'package.json');
if (await (0, fs_1.safeReadJson)(cur))
return (0, node_path_1.dirname)(cur);
}
}
}
let pnp;
/**
* The pnpapi module is only available if running in a pnp environment. Because of that
* we have to require it from the plugin.
*
* Solution taken from here: https://github.com/yarnpkg/berry/issues/1467#issuecomment-642869600
*/
function maybeRequirePnpApi(root) {
if (pnp)
return pnp;
try {
// eslint-disable-next-line n/no-missing-require
pnp = require(require.resolve('pnpapi', { paths: [root] }));
return pnp;
}
catch { }
}
const getKey = (locator) => JSON.stringify(locator);
const isPeerDependency = (pkg, parentPkg, name) => getKey(pkg?.packageDependencies.get(name)) === getKey(parentPkg?.packageDependencies.get(name));
/**
* Traverse PnP dependency tree to find plugin root directory.
*
* Implementation adapted from https://yarnpkg.com/advanced/pnpapi#traversing-the-dependency-tree
*/
function findPnpRoot(name, root) {
maybeRequirePnpApi(root);
if (!pnp)
return;
debug(name)('Finding root for using pnp method');
const seen = new Set();
const traverseDependencyTree = (locator, parentPkg) => {
// Prevent infinite recursion when A depends on B which depends on A
const key = getKey(locator);
if (seen.has(key))
return;
const pkg = pnp.getPackageInformation(locator);
if (locator.name === name) {
return pkg.packageLocation;
}
seen.add(key);
for (const [name, referencish] of pkg.packageDependencies) {
// Unmet peer dependencies
if (referencish === null)
continue;
// Avoid iterating on peer dependencies - very expensive
if (parentPkg !== null && isPeerDependency(pkg, parentPkg, name))
continue;
const childLocator = pnp.getLocator(name, referencish);
const foundSomething = traverseDependencyTree(childLocator, pkg);
if (foundSomething)
return foundSomething;
}
// Important: This `delete` here causes the traversal to go over nodes even
// if they have already been traversed in another branch. If you don't need
// that, remove this line for a hefty speed increase.
seen.delete(key);
};
// Iterate on each workspace
for (const locator of pnp.getDependencyTreeRoots()) {
const foundSomething = traverseDependencyTree(locator);
if (foundSomething)
return foundSomething;
}
}
/**
* Returns the root directory of the plugin.
*
* It first attempts to use require.resolve to find the plugin root.
* If that returns a path, it will `cd` up the file system until if finds the package.json for the plugin
* Example: node_modules/@oclif/plugin-version/dist/index.js -> node_modules/@oclif/plugin-version
*
* If require.resolve throws an error, it will attempt to find the plugin root by traversing the file system.
* If we're in a PnP environment (determined by process.versions.pnp), it will use the pnpapi module to
* traverse the dependency tree. Otherwise, it will traverse the node_modules until it finds a package.json
* with a matching name.
*
* If no path is found, undefined is returned which will eventually result in a thrown Error from Plugin.
*/
async function findRoot(name, root) {
if (name) {
debug(name)(`Finding root using ${root}`);
let pkgPath;
try {
pkgPath = require.resolve(name, { paths: [root] });
debug(name)(`Found starting point with require.resolve`);
}
catch {
debug(name)(`require.resolve could not find plugin starting point`);
}
if (pkgPath) {
const found = await findPluginRoot((0, node_path_1.dirname)(pkgPath), name);
if (found) {
debug(name)(`Found root at ${found}`);
return found;
}
}
const found = process.versions.pnp ? findPnpRoot(name, root) : await findRootLegacy(name, root);
debug(name)(found ? `Found root at ${found}` : 'No root found!');
return found;
}
debug('root-plugin')(`Finding root plugin using ${root}`);
const found = await findPluginRoot(root);
debug('root-plugin')(found ? `Found root at ${found}` : 'No root found!');
return found;
}