UNPKG

counsel

Version:

the end of boilerplate. automatically bake structure, opinions, and business rules into projects

272 lines 11.2 kB
"use strict"; 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