Git Product home page Git Product logo

counsel's Introduction

counsel

CircleCI TypeScript package

the end of boilerplate. bake structure, opinions, and rules into projects. see the documentation site.

it's similar to the popular yeoman/yo package, but manages projects programmatically versus using boilerplate.

counsel is for project maintainers. counsel makes sense for people who are developing many projects. counsel doesn't always make sense for teams or maintainers working on just a single project or two.

install

yarn add --dev counsel

alternatively, npm install --save-dev counsel

usage

conventional usage is to add a .counsel.ts file to your project root dirname.

you can have counsel insert a generic .counsel.ts file for you using --init:

$ counsel --init
info: ⚙️ config file .counsel.ts created successfully

alternatively, as shown next, we can bootstrap our own counsel.ts file.

once a project has a counsel file, run various counsel commands:

  • npx counsel apply

  • npx counsel check

npx counsel --help is also there to help!

concepts

counsel has only one major concept to understand--the Rule. counsel can apply rules and check that rules are enforced. counsel rules are specified using a .counsel.ts file, hereby "counsel file." let's look at counsel files and rules next.

counsel file

the counsel file declares and exports Rules. the only expectation is that it exports a function named create with following signature:

ContextWithRules => ContextWithRules

let's create a basic rule that enforces that the project has a readme file:

// .counsel.ts
export const assertReadmeExists: Rule = {
  name: 'assert-readme-exists',
  check: async ({ fs, path, ctx: { projectDirname } }) => {
    const filename = path.resolve(projectDirname, 'readme.md')
    const isReadable = await fs.lstat(filename).catch(() => false)
    if (!isReadable) throw new Error('readme.md file missing')
  }
}

// export your rules via a `create` function
export function create (opts: ContextWithRules) =>
  ({ ...opts, rules: [assertReadmeExists] })

create, import, and use as many rules as desired. rules can be used for all sorts of reasons. sky is the limit.

rule

Rules are basic interfaces with:

  1. a name
  2. an optional plan function
  3. an optional check function
  4. an optional list of dependencies
  5. an optional list of devDependencies

in a nut-shell, that's it. counsel is a small set of functions that run these Rules against your project.

here's a simple rule that exercises some of the rule api:

export const exampleRule: Rule = {
  name: 'example-rule',
  plan: ({ ctx }) => {
    console.log(
      `planning to add keyword 'example' to pkg: ${ctx.packageJson.name}`
    )
    return () => {
      ctx.packageJson.keywords = ctx.packageJson.keywords || []
      ctx.packageJson.keywords.push('example')
    }
  },
  check: async ({ ctx: { packageJson } }) => {
    const keywords = packageJson.keywords || []
    console.log(`existing keywords: ${keywords.join(' ')}`)
    const keywordExists = keywords.find(val => val === 'example')
    if (!keywordExists) throw new Error("'example' keyword missing")
  },
  devDependencies: [{ name: 'debug', range: '*' }]
}

rule.name

every rule requires a name. it must always be a string.

rule.plan

a plan returns a function or null, which we call a Migration. a Migration is responsible for changing the project in some way. rather than mutating the project upfront, all changes to a project are encouraged to happen in the Migration. this gives the user an opporitunity to opt-out of rules in counsel's interactive mode.

for example, here's a simplified version of counsel's baked in copy rule:

export interface CopyRule {
  src: string
  dest: string
}
const plan = (opts: TaskPayload<CopyRule>) =>
  () => fs.copy(opts.rule.src, opts.rule.dest)

the () => fs.copy(...) matches the Migration type, so it should be set! plan receives a TaskPayload as input, covered later.

export type Migration =
  null // return null when there is nothing to migrate
  | (() => void | Promise<void>) // otherwise, migrate in a returned function

rule.check

check recieves a TaskPayload as is responsible for ensuring that a rule is enforced. we've already seen a few examples of check functions:

check functions should:

  • be synchronous, or return a promise
  • throw (or reject) Errors when a violation is detected
  • tend to be lenient

on the topic of leniency, consider counsel's baked in ScriptRule. if you wanted a rule to provide a default npm script named test, where the test command was node test/index.js, consider if the project added a timeout flag, such as "test": "node test/index.js --timeout 10s".

it would be a bad user experience to throw if the script did not strictly equal node test/index.js. adding a simple flag is likely something that rule implementer would be OK with. more imporantly, the core intent of the rule is likely to assert that the user has written tests. a better check implementation would be to ensure that a test script is present, and is truthy (i.e. runs some test script). enforcing rules at any given granularity is something that needs to be worked through with rule makers and their teams. be weary of agitating consumers by implementing overly strict checks.

rule.dependencies

rules can request dependencies & devDependencies to be installed. dependencies are always requested in a range format:

const installRule: Rule = {
  name: 'install-koa',
  dependencies: [
    { name: 'koa', range: '^2' }
  ],
  devDependencies: [
    { name: 'node-fetch': range: '*' }
  ]
}

by using semver ranges, you can pin dependencies with moderate precision or flexibility.

typings

it is worth brief mention that the majority of counsel's interfaces/typings are packed nicely into a < 100 LOC file here, for your viewing.

TaskPayload

plan and check receive a task payload as input. the payload is rich with data and async functions to help plan and check. check out the typings in the source code (1, 2).

batteries

counsel exports a handful of common and helpful rules. batteries included!

see counsel.rules, or src/rules to see a handful. at the time of writing, these default rules include:

copy

  • copy - copies files or folders into a project
import { rules } from 'counsel'
const { plan } = rules.copy
const rule: CopyRule = {
  name: 'copy-markdown-file-test',
  src: path.resolve(__dirname, 'readme-template.md'),
  dest: path.resolve(ctx.projectDirname, 'readme.md'),
  plan
}

filename-format

import { kebabCase } from 'lodash'
import { rules } from 'counsel'
const { check } = rules.filenameFormat

const rule: FilenameFormatRule = {
  name: 'test-filename-rule',
  filenameFormatExtensions: ['js'],
  filenameFormatExclude: ['coffee'],
  filenameFormatFunction: kebabCase,
  check
}
// test-file.js // ok
// functional-module.js // ok
// SomeFile // not ok

githook

  • githook - installs githook support via husky into a project
import { rules } from 'counsel'
const { create } = rules.githook

const rule: GitHooksRule = create({
  name: 'lint-on-commit',
  hooks: {
    'pre-commit': 'yarn lint'
  }
})

readme

  • readme - enforces that a project has a readme file
import { rules } from 'counsel'
const { rule } = rules.readme

script

  • script - installs a npm script to a project
import { rules } from 'counsel'
const { create } = rules.script
const rule: criptRule = create({
  name: 'add-test-script-rule',
  scriptName: 'test',
  scriptCommand: 'tape test/blah.js'
})

examples

similar works

  • FormidableLabs/builder
    • counsel is very similar to builder, but counsel doesn't need to be yet-another-task-runner. you can npx counsel apply, never fully install it, and reap many of it's benefits.
    • builder also claims flexibility and an anti-"buy the farm" attitude. in practice, we've observed the opposite. feel free to try both! :)

logo credit

margdking

counsel's People

Contributors

cdaringe avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar

counsel's Issues

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.