nlu
Version:
Use this package to link your projects together for local development.
406 lines (303 loc) • 11 kB
text/typescript
;
//core
import * as util from 'util';
import * as assert from 'assert';
import * as path from 'path';
import * as cp from 'child_process';
import * as fs from 'fs';
//npm
import chalk from 'chalk';
const dashdash = require('dashdash');
import async = require('async');
import residence = require('residence');
const cwd = process.cwd();
const root = residence.findProjectRoot(cwd);
const treeify = require('treeify');
import mkdirp = require('mkdirp');
//project
import {makeFindProject} from '../../find-projects';
import {mapPaths} from '../../map-paths-with-env-vars';
import {cleanCache} from '../../cache-clean';
import log from '../../logging';
import {getIgnore, getSearchRoots} from "../../handle-options";
import options from './cmd-line-opts';
import {runNPMLink} from '../../run-link';
import {createTree} from '../../create-visual-tree';
import {getCleanMap} from '../../get-clean-final-map';
import {q} from '../../search-queue';
const npmLinkUpPkg = require('../../../package.json');
import {EVCb, NluMap, NLURunOpts} from "../../index";
import {
globalConfigFilePath,
determineIfReinstallIsNeeded,
getDevKeys,
getProdKeys,
validateConfigFile,
validateOptions, mapConfigObject
} from "../../utils";
import {NluGlobalSettingsConf} from "../../index";
//////////////////////////////////////////////////////////////////////////
process.once('exit', function (code) {
log.info('NLU is exiting with code:', code, '\n');
});
//////////////////////////////////////////////////////////////
const allowUnknown = process.argv.indexOf('--allow-unknown') > 0;
let opts: NLURunOpts, globalConf: NluGlobalSettingsConf, parser = dashdash.createParser({options, allowUnknown});
try {
opts = parser.parse(process.argv);
} catch (e) {
log.error(chalk.magenta(' => CLI parsing error:'), chalk.magentaBright.bold(e.message));
process.exit(1);
}
if (opts.help) {
let help = parser.help({includeEnv: true}).trimRight();
console.log('usage: nlu run [OPTIONS]\n'
+ 'options:\n'
+ help);
process.exit(0);
}
try{
globalConf = require(globalConfigFilePath);
}
catch(err){
log.warn('Could not load global config');
globalConf = {};
}
if(!(globalConf && typeof globalConf === 'object')){
globalConf = {};
}
if(Array.isArray(globalConf)){
globalConf = {};
}
if (!root) {
log.error('You do not appear to be within an NPM project (no package.json could be found).');
log.error(' => Your present working directory is =>', chalk.magenta.bold(cwd));
process.exit(1);
}
let pkg, conf;
try {
pkg = require(path.resolve(root + '/package.json'));
}
catch (e) {
log.error('Bizarrely, you do not seem to have a "package.json" file in the root of your project.');
log.error('Your project root is supposedly here:', chalk.magenta(root));
log.error(e.message);
process.exit(1);
}
try {
conf = require(path.resolve(root + '/.nlu.json'));
}
catch (e) {
log.error('You do not have an ".nlu.json" file in the root of your project. ' +
'You need this config file for npmlinkup to do its thing.');
log.error('Your project root is supposedly here:', chalk.magenta(root));
log.error(e.message);
process.exit(1);
}
conf.localSettings = conf.localSettings || {};
if(!(conf.localSettings && typeof conf.localSettings === 'object')){
conf.localSettings = {}
}
if(Array.isArray(conf.localSettings)){
conf.localSettings = {};
}
opts = Object.assign({},
mapConfigObject(globalConf),
mapConfigObject(conf.localSettings),
opts
);
if (!validateOptions(opts)) {
log.error(chalk.bold('Your command line arguments were invalid, try:', chalk.magentaBright('nlu run --help')));
process.exit(1);
}
if (!validateConfigFile(conf)) {
log.error('Your .nlu.json config file appears to be invalid. To override this, use --override.');
if (!opts.override) {
process.exit(1);
}
}
const mainProjectName = pkg.name;
if (!mainProjectName) {
log.error('Ummmm, your package.json file does not have a name property. Fatal.');
process.exit(1);
}
if (opts.verbosity > 0) {
log.good(`We are running the "npm-link-up" tool for your project named "${chalk.magenta(mainProjectName)}".`);
}
const productionDepsKeys = getProdKeys(pkg);
const allDepsKeys = getDevKeys(pkg);
let list = conf.list;
assert(Array.isArray(list),
'Your .nlu.json file must have a top-level "list" property that is an array of strings.');
list = list.filter(function (item: string) {
return !/###/.test(item);
});
if (list.length < 1) {
log.error(chalk.magenta(' => You do not have any dependencies listed in your .nlu.json file.'));
log.error(chalk.cyan.bold(util.inspect(conf)));
process.exit(1);
}
const searchRoots = getSearchRoots(opts, conf);
if(searchRoots.length < 1){
log.error(chalk.red('No search-roots provided.'));
log.error('You should either update your .nlu.json config to have a searchRoots array.');
log.error('Or you can use the --search-root=X option at the command line.');
log.error(chalk.bold('Conveniently, you may include environment variables in your search root strings.'));
log.error('For example, to search everything starting from $HOME, you can use --search-root=$HOME option at the command line.');
log.error('However, it is highly recommended to choose a subdirectory from $HOME, since searching that many files can take some time.');
process.exit(1);
}
const inListButNotInDeps: Array<string> = list.filter((item: string) => {
return !allDepsKeys.includes(item);
});
inListButNotInDeps.forEach(function (item) {
if (opts.verbosity > 1) {
log.warning('warning, the following item was listed in your .nlu.json file, ' +
'but is not listed in your package.json dependencies => "' + item + '".');
}
});
// we need to store a version of the list without the top level package's name
const originalList = list.slice(0);
// always push the very project's name
list.push(mainProjectName);
list = list.filter(function (item: string, index: number) {
return list.indexOf(item) === index;
});
const totalList = new Map();
list.forEach(function (l: string) {
totalList.set(l, true);
});
const ignore = getIgnore(conf, opts);
originalList.forEach(function (item: string) {
if (opts.verbosity > 0) {
log.good(`The following dep will be linked to this project => "${chalk.gray.bold(item)}".`);
}
});
const map: NluMap = {};
let cleanMap: NluMap;
if (opts.dry_run) {
log.warning(chalk.bold.gray('Because --dry-run was used, we are not actually linking projects together.'));
}
// add the main project to the map
// when we search for projects, we ignore any projects where package.json name is "mainProjectName"
map[mainProjectName] = {
name: mainProjectName,
bin: conf.bin || null,
isMainProject: true,
linkToItself: conf.linkToItself,
runInstall: conf.alwaysReinstall,
path: root,
deps: conf.list
};
async.autoInject({
readNodeModulesFolders(cb: EVCb<any>) {
const nm = path.resolve(root + '/node_modules');
const keys = opts.production ? productionDepsKeys: allDepsKeys;
determineIfReinstallIsNeeded(nm, keys, opts, (err, val) =>{
if(err){
return cb(err);
}
if(val === true){
opts.install_main = true;
}
cb(null);
});
},
ensureNodeModules(readNodeModulesFolders: any, cb: EVCb<any>) {
if (!readNodeModulesFolders) {
// no error reading node_modules dir
return process.nextTick(cb);
}
opts.install_main = true;
// we have to install, because node_modules does not exist
mkdirp(path.resolve(root + '/node_modules'), cb);
},
npmCacheClean(cb: EVCb<any>) {
if (opts.dry_run) {
return process.nextTick(cb);
}
if (!opts.clear_all_caches) {
return process.nextTick(cb);
}
log.info(`Cleaning the NPM cache.`);
cleanCache(cb);
},
mapSearchRoots(npmCacheClean: any, cb: EVCb<any>) {
opts.verbosity > 3 && log.info(`Mapping original search roots from your root project's "searchRoots" property.`);
mapPaths(searchRoots, cb);
},
findItems(mapSearchRoots: Array<string>, cb: EVCb<any>) {
let searchRoots = mapSearchRoots.slice(0);
if (opts.verbosity > 1) {
log.info('Beginning to search for NPM projects on your filesystem.');
}
if (opts.verbosity > 3) {
log.info('NPM-Link-Up will be searching these roots for relevant projects:');
log.info(chalk.magenta(util.inspect(searchRoots)));
}
if (opts.verbosity > 2) {
log.warning('Note that NPM-Link-Up may come across a project of yours that needs to search in directories');
log.warning('not covered by your original search roots, and these new directories will be searched as well.');
}
const status = {searching: true};
const findProject = makeFindProject(mainProjectName, totalList, map, ignore, opts, status);
searchRoots.forEach(function (sr) {
q.push(function (cb) {
findProject(sr, cb);
});
});
if (q.idle()) {
return process.nextTick(cb, new Error('For some reason, no paths/items went onto the search queue.'));
}
let first = true;
q.error = q.drain = function (err?: any) {
if (err) {
status.searching = false;
log.error(chalk.magenta('There was a search queue processing error.'));
}
if (first) {
q.kill();
cb(err, {actuallyRan: true});
}
first = false;
};
},
runUtility(findItems: void, cb: EVCb<any>) {
try {
cleanMap = getCleanMap(mainProjectName, map);
}
catch (err) {
return process.nextTick(cb, err);
}
if (opts.dry_run) {
return process.nextTick(cb);
}
log.good('Beginning to actually link projects together...');
runNPMLink(cleanMap, opts, cb);
}
},
function (err: any, results: object) {
if (err) {
log.error(err.stack || err);
return process.exit(1);
}
if ((results as any).runUtility) {
// if runUtility is defined on results, then we actually ran the tool
log.good(chalk.green.underline('NPM-Link-Up run was successful. All done.'));
}
const treeObj = createTree(cleanMap, mainProjectName, originalList, opts);
const treeString = treeify.asTree(treeObj, true);
const formattedStr = String(treeString).split('\n').map(function (line) {
return '\t' + line;
});
if (opts.verbosity > 1) {
log.good(chalk.cyan.bold('NPM-Link-Up results as a visual:'), '\n');
console.log('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^');
console.log(chalk.white(formattedStr.join('\n')));
console.log('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^');
}
setTimeout(function () {
process.exit(0);
}, 100);
});