counsel
Version:
the end of boilerplate. automatically bake structure, opinions, and business rules into projects
272 lines • 11.2 kB
JavaScript
;
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const errors_1 = require("./errors");
const logger_1 = require("./logger");
const dependencies_1 = require("./dependencies");
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const project = __importStar(require("./project"));
const rules = __importStar(require("./rules")); // eslint-disable-line no-unused-vars (compile time only import)
exports.rules = rules;
const bluebird_1 = __importDefault(require("bluebird"));
const execa_1 = __importDefault(require("execa"));
const Prompt = require('prompt-checkbox');
/**
* sugar function for calling plan + migrate
*/
exports.apply = async (opts) => {
const { ctx: { assumeYes, logger } } = opts;
logger.verbose('counsel::apply::start');
logger.info(logger_1.withEmoji('planning project migration', '🗓'));
let migrations = await exports.plan(opts);
const rules = exports.toRulesWithPlans(opts.rules);
const report = rules
.map((rule, i) => ({ rule, migration: migrations[i] }))
.reduce((agg, curr) => {
const { rule: { dependencies = [], devDependencies = [], name }, migration } = curr;
return Object.assign({}, agg, { [name]: {
dependencies: dependencies.map(dependencies_1.dependencyToString).join(','),
devDependencies: devDependencies.map(dependencies_1.dependencyToString).join(','),
migration: migration ? 'READY' : 'NONE'
} });
}, {});
if (migrations.length && process.stdout.isTTY && !assumeYes) {
logger.info('migration plan:');
console.table(report);
}
if (migrations.length && !assumeYes) {
const prompt = new Prompt({
name: 'rules',
message: 'select the rules to be applied:',
radio: true,
choices: Object.keys(report),
default: Object.keys(report)
});
var choices = await prompt.run();
// select migrations by plucking corresponding indexes where rule names match
migrations = rules
.map((rule, i) => {
if (choices.includes(rule.name))
return migrations[i];
return null;
})
.filter(Boolean);
await prompt.ui.close();
prompt.end(false);
}
logger.info(logger_1.withEmoji('migrating project', '🔄'));
const res = await exports.migrate(Object.assign({}, opts, { migrations }));
logger.verbose('counsel::apply::finish');
return res;
};
exports.plan = async (opts) => {
opts.ctx.logger.verbose('counsel::plan::start');
const res = await bluebird_1.default.map(exports.toRulesWithPlans(opts.rules), rule => Promise.resolve(rule.plan({ ctx: opts.ctx, rule, fs, path })));
opts.ctx.logger.verbose('counsel::plan::finish');
return res.filter(Boolean);
};
/**
* Migrate project by means of executing provided Migrations, generally
* administering project-level side-effects
*/
exports.migrate = async (opts) => {
const { migrations, ctx, rules } = opts;
opts.ctx.logger.verbose('counsel::migrate::start');
if (!migrations.length) {
opts.ctx.logger.info('counsel::migrate: no migrations');
}
const res = await bluebird_1.default.map(migrations.filter(Boolean), migration => Promise.resolve(migration()));
const [dependencies, devDependencies] = rules.reduce(([dependencies, devDependencies], rule) => [
[...dependencies, ...(rule.dependencies || [])],
[...devDependencies, ...(rule.devDependencies || [])]
], [[], []]);
await exports.migrateDependencies({ dependencies, ctx, development: false });
await exports.migrateDependencies({
dependencies: devDependencies,
ctx,
development: true
});
if (exports.isTargetPackageDirty(ctx))
await exports.writeTargetPackage(ctx);
opts.ctx.logger.verbose('counsel::migrate::finish');
return res;
};
exports.check = async (opts) => {
opts.ctx.logger.verbose('counsel::check::start');
const res = await bluebird_1.default.map(opts.rules.filter(rule => rule.check), rule => Promise.resolve(rule.check({ ctx: opts.ctx, rule, fs, path })).catch(({ message, stack }) => {
throw new errors_1.CheckError({ message, stack, rule });
}));
opts.ctx.logger.verbose('counsel::check::finish');
return res;
};
// support!
exports.createDefaultContext = async (targetProjectDirname, options) => {
options = options || {};
const { logLevel } = options;
const targetProjectPackageJsonFilename = path.resolve(targetProjectDirname, 'package.json');
const targetProjectPackageJson = await fs.readJSON(targetProjectPackageJsonFilename);
const ctx = {
packageJsonPristine: Object.assign({}, targetProjectPackageJson),
ctxKey: 'counsel',
logger: logger_1.createLogger({ logLevel }),
packageJson: targetProjectPackageJson,
packageJsonFilename: targetProjectPackageJsonFilename,
projectDirname: targetProjectDirname
};
return ctx;
};
exports.getLocalctxFilename = async (targetProjectDirname) => {
for (const ext of ['ts', 'js']) {
const basename = `.counsel.${ext}`;
const localctxFilename = path.join(targetProjectDirname || process.cwd(), basename);
const exists = await fs
.lstat(localctxFilename)
.then(() => true)
.catch(() => false);
if (exists)
return localctxFilename;
}
return null;
};
exports.importLocalctx = async (targetProjectDirname) => {
const ctxFilename = await exports.getLocalctxFilename(targetProjectDirname);
if (!ctxFilename)
return null;
if (ctxFilename.match(/\.ts$/)) {
const transpileModule = require('typescript').transpileModule;
const tsSettingsContents = (await fs.readFile(ctxFilename)).toString();
const commonJsCtx = transpileModule(tsSettingsContents, {});
try {
const nodeEval = require('node-eval');
const mod = nodeEval(commonJsCtx.outputText, path.join(targetProjectDirname, '.counsel.js'));
return mod;
}
catch (err) {
if (err.message && err.message.match(/Please pass/)) {
throw new errors_1.CounselError([
'failed to load configuration file. are all of your dependencies',
'installed?\n',
`original error:\n\t${err.message}`
].join(' '));
}
throw err;
}
}
return require(ctxFilename);
};
exports.init = async (logger) => {
const basename = '.counsel.ts';
const ctxFilename = await exports.getLocalctxFilename();
if (ctxFilename) {
return logger.warn(`ctx file already exists: ${ctxFilename}`);
}
const contents = await fs.readFile(path.join(__dirname, '.counsel-template.ts.template'));
await fs.writeFile(path.join(process.cwd(), basename), contents);
logger.info(logger_1.withEmoji(`ctx file ${basename} created successfully`, '⚙️'));
};
exports.getPristineDependencies = (ctx) => ctx.packageJsonPristine.dependencies || {};
exports.getPristineDevDependencies = (ctx) => ctx.packageJsonPristine.devDependencies || {};
exports.loadProjectSettings = async (targetProjectDirname, defaultctx, cli) => {
const localctx = await exports.importLocalctx(targetProjectDirname);
if (!localctx)
return { ctx: defaultctx, rules: [] };
const create = localctx.create || localctx;
if (typeof create !== 'function') {
throw new Error('local counsel ctx must export create function');
}
return Promise.resolve(create({ ctx: defaultctx, rules: [], cli }));
};
/**
* run `npm install` (or similar) for dev or std deps
* @returns {boolean} indicator if packages were installed
*/
exports.installDependencies = async (opts) => {
const isDev = !!opts.development;
let bin = opts.packageManager;
let isYarnPackage;
if (!bin) {
isYarnPackage = await project.isYarn(opts.ctx.projectDirname);
bin = isYarnPackage ? 'yarn' : 'npm';
}
const installCmd = isYarnPackage ? 'add' : 'install';
const toInstall = opts.dependencies;
let flag = '';
if (isDev)
flag = isYarnPackage ? '--dev' : '--save-dev';
if (!toInstall.length)
return false;
opts.ctx.logger.info(`installing ${isDev ? 'development' : ''} dependencies: ${toInstall
.map(dependencies_1.dependencyToString)
.map(dependencyString => dependencyString.replace('@*', ''))
.join(', ')}`);
try {
const args = [
installCmd,
flag,
...toInstall
.map(dependencies_1.dependencyToString)
.map(dependencyString => dependencyString.replace('@*', ''))
].filter(Boolean);
opts.ctx.logger.verbose(`${bin} ${args.join(' ')}`);
await execa_1.default(bin, args, {
cwd: opts.ctx.projectDirname,
stdio: (process.env.NODE_ENV || '').match(/test/) ? 'ignore' : 'inherit'
});
}
catch (err) {
if (err)
return opts.ctx.logger.error(err);
}
return true;
};
/**
* Determines if the target project's package.json has changed (during rule application)
*/
exports.isTargetPackageDirty = (ctx) => {
const pkg1 = JSON.stringify(ctx.packageJsonPristine);
const pkg2 = JSON.stringify(ctx.packageJson);
const res = pkg1 !== pkg2;
ctx.logger.verbose(`counsel::isTargetPackageDirty?: ${res}`);
return res;
};
/**
* write the project package.json if it's dirty
*/
exports.writeTargetPackage = async (ctx) => {
return fs.writeFile(ctx.packageJsonFilename, JSON.stringify(ctx.packageJson, null, 2));
};
/**
* migrate deps required by rule set
*/
exports.migrateDependencies = async (opts) => {
const { dependencies, development, ctx } = opts;
const toInstall = dependencies_1.filterToInstallPackages(dependencies_1.fromNameVersionObject(development
? exports.getPristineDevDependencies(ctx)
: exports.getPristineDependencies(ctx)), dependencies, ctx.logger);
const isPackageDirty = await exports.installDependencies({
ctx,
dependencies: toInstall,
development
});
// istanbul ignore else
if (isPackageDirty) {
const tPkg = await fs.readJSON(ctx.packageJsonFilename);
ctx.packageJson.dependencies = tPkg.dependencies;
}
};
exports.toRulesWithPlans = (rules) => rules.filter(rule => rule.plan);
__export(require("./errors"));
//# sourceMappingURL=counsel.js.map