npm-link-extra
Version:
npm or yarn link common modules between a project and a monorepo/multi-packages dir
307 lines (279 loc) • 8.41 kB
JavaScript
/* eslint-disable no-console */
const execa = require('execa');
const fs = require('fs');
const path = require('path');
const { readPkgJson, logPkgsMsg, checkForLink, debugLogging } = require('./utils');
const cwd = process.cwd();
// eslint-disable-next-line import/no-dynamic-require
const { dependencies, devDependencies } = require(path.resolve(`${cwd}/package.json`));
const allDeps = Object.assign({}, devDependencies, dependencies);
let installCmd = 'npm install';
const hasYarnLock = fs.existsSync('yarn.lock');
const installedNpmClient = hasYarnLock ? 'yarn' : 'npm';
const npmClient = process.env.NLX_NPM_CLIENT ? process.env.NLX_NPM_CLIENT : installedNpmClient;
const isUsingYarn = npmClient === 'yarn';
if (isUsingYarn) {
console.log('yarn.lock file detected :: using yarn as npm client for reinstalls');
installCmd = 'yarn --ignore-scripts';
}
console.log(
`Using ${npmClient} for operations. You can override this by setting an env var of NLX_NPM_CLIENT as "npm" or "yarn".`,
);
const packageHash = {};
const packageKeys = Object.keys(allDeps);
packageKeys.forEach(function addKeyToHash(key) {
packageHash[key] = {
isLinked: checkForLink(key),
};
});
/**
* getDirectories returns all directories in a given path
* @param {String} pathTo relative path to monorepo or directory with node modules
* @param {Object} opts options to call the function with
* @return {Array} array of directories
*/
const getDirectories = (pathTo, opts) => {
const { ignorePackages } = opts || {};
if (!pathTo) {
throw Error(
'Need a RELATIVE path to the directory with packages you want to link, eg: (../../my-monorepo/packages)',
);
}
return fs
.readdirSync(pathTo)
.filter(item => {
if (ignorePackages && ignorePackages.includes(item)) {
return false;
}
// we want to make sure we don't pick up any . or .DS_Store etc
return item[0] !== '.';
})
.map(item => {
return `${pathTo}/${item}`;
})
.filter(item => {
// make sure to return only dirs
return fs.statSync(item).isDirectory();
});
};
/**
* getPackages returs all packages
* @param {Array} dirs array of directories
* @return {Array} array of packages & the relative path to them
*/
const getPackages = dirs =>
dirs
.map(dir => {
const pkg = readPkgJson(dir) || {};
pkg.dir = dir;
return pkg;
})
.filter(pkg => {
return pkg.name;
});
// Execa functions
// reinstall after unlinking since unlink deletes the symlink
function reInstall() {
if (isUsingYarn) {
return execa.shellSync(`${installCmd} --force`, { stdio: 'inherit' });
}
return execa.shellSync(installCmd, { stdio: 'inherit' });
}
/**
* getSharedDepDirs selects an array of shared and linked packages
* @param {Array} pkgs An array of dependencies
* @param {Object} hash A hash map of our deps
* @return {Array} A filtered list of shared pkg dirs
*/
function getSharedDepDirs(pkgs, hash) {
return pkgs
.map(({ name, dir }) => {
if (hash[name]) {
return dir;
}
return false;
})
.filter(Boolean);
}
/**
* getSharedLinked selects an array of shared and linked packages
* @param {Array} pkgs An array of dependencies
* @param {Object} hash A hash map of our deps
* @return {Array} A filtered list of packages
*/
function getSharedLinked(pkgs, hash) {
return pkgs
.map(name => {
const module = hash[name];
if (module && module.isLinked) {
return name;
}
return false;
})
.filter(Boolean);
}
/**
* getSharedDeps will show any shared dependencies between project & target dir
* @param {Array} pkgs An array of dependencies
* @param {Object} hash A hash map of our deps
* @return {Array} Array of shared dep names
*/
function getSharedDeps(pkgs, hash) {
return pkgs
.map(({ name }) => {
if (hash[name]) {
return name;
}
return false;
})
.filter(Boolean);
}
/**
* getLinkedDeps returns a list of linked dependencies
* @param {Array} pkgs An array of dependencies
* @return {Array} Array of linked dep names
*/
function getLinkedDeps(pkgs) {
return pkgs
.map(name => {
const isLinked = checkForLink(name);
return isLinked ? name : false;
})
.filter(Boolean);
}
// show all linked dependencies in your node_modules
function showLinkedDeps({ ignorePackages }) {
if (ignorePackages && ignorePackages.length) {
logPkgsMsg('Ignored', ignorePackages);
}
const linkedDeps = getLinkedDeps(packageKeys, packageHash);
if (linkedDeps.length) {
logPkgsMsg('Linked', linkedDeps);
} else {
console.log('No linked dependencies found');
}
}
function createLinks(pkgs, opts) {
if (pkgs.length === 0) {
console.log('Done.');
return showLinkedDeps(opts);
}
const toLink = [];
const ignore = pkgs
.map(pkg => {
const { name } = pkg;
if (checkForLink(name)) {
debugLogging(`${name} is already linked.`);
return name;
}
return null;
})
.filter(Boolean);
while (pkgs.length && pkgs[0]) {
const { name, dir } = pkgs[0];
if (ignore.includes(name)) {
debugLogging(`Ignoring already linked: ${name}.`);
pkgs.splice(0, 1);
debugLogging('Make recursive call');
return createLinks(pkgs, opts);
}
const cmd = isUsingYarn ? `${npmClient} link` : `${npmClient} link ${dir}`;
const cmdDir = isUsingYarn ? dir : cwd;
debugLogging(`Linking ${name} with "${cmd}" from ${dir}`);
execa.shellSync(cmd, {
cwd: cmdDir,
stdio: 'inherit',
});
toLink.push(name);
pkgs.splice(0, 1);
}
execa.shellSync(`${npmClient} link ${toLink.join(' ')}`, {
cwd,
stdio: 'inherit',
});
return console.log('Done linking modules with yarn.');
}
// link packages
function linkPackages(pathToPkgs) {
const pkgs = pathToPkgs.join(' ');
return execa.shell(`${npmClient} link ${pkgs}`).then(() => console.log('Succesfully linked'));
}
// unlink
function unlinkPackages(pkgs) {
if (npmClient === 'yarn') {
const toLink = [];
while (pkgs.length && pkgs[0]) {
const { name, dir } = pkgs[0];
const cmd = `${npmClient} unlink`;
console.log(`Unlinking ${name} with "${cmd}" at ${dir}`);
try {
toLink.push(name);
pkgs.splice(0, 1);
} catch (err) {
debugLogging(err);
// error(err);
}
}
execa.shellSync(`${npmClient} unlink ${toLink.join(' ')}`, {
cwd,
stdio: 'inherit',
});
return console.log('Done unlinking packages with yarn.');
}
execa.shellSync(`${npmClient} unlink ${getSharedDeps(pkgs, packageHash).join(' ')}`, {
stdio: 'inherit',
});
return console.log('Done unlinking packages.');
}
// link common dependencies between project & given directory/monorepo
function linkIfExists(pkgs, opts) {
const sharedDepsDirs = getSharedDepDirs(pkgs, packageHash);
const numOfLinked = sharedDepsDirs.length;
debugLogging(`Linking ${numOfLinked} packages`);
if (numOfLinked) {
logPkgsMsg('Linking', sharedDepsDirs);
if (isUsingYarn) {
return createLinks(pkgs, opts);
}
return linkPackages(sharedDepsDirs);
}
return console.log('No shared dependencies found');
}
// unlink common dependencies between project & given directory/monorepo
function unlinkIfLinked(pkgs) {
const sharedDeps = getSharedLinked(packageKeys, packageHash);
const numOfLinked = sharedDeps.length;
if (numOfLinked) {
debugLogging(`Unlinking ${numOfLinked} packages`);
unlinkPackages(pkgs);
console.log(
'Reinstalling for your convenience. You can cancel if needed and reinstall or re-link.',
);
reInstall();
} else {
console.log('No shared linked dependencies found');
}
}
// show common dependencies between project & given directory/monorepo
function showSharedDeps(packages) {
const sharedDepNames = getSharedDeps(packages, packageHash);
if (sharedDepNames.length) {
logPkgsMsg('Shared', sharedDepNames);
} else {
console.log('No shared dependencies found');
}
}
module.exports = {
// selectors
getPackages,
getDirectories,
getLinkedDeps,
getSharedDeps,
getSharedLinked,
getSharedDepDirs,
// commands
linkIfExists,
unlinkIfLinked,
showSharedDeps,
showLinkedDeps,
};