Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/cdaringe/counsel

the end of boilerplate. automatically bake structure, opinions, and biz rules into projects.
https://github.com/cdaringe/counsel

automation management nodejs opinions project rule

Last synced: 2 days ago
JSON representation

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

Awesome Lists containing this project

README

        




# counsel

[![CircleCI](https://circleci.com/gh/cdaringe/counsel.svg?style=svg)](https://circleci.com/gh/cdaringe/counsel) ![](https://img.shields.io/badge/standardjs-%E2%9C%93-brightgreen.svg) [![TypeScript package](https://img.shields.io/badge/language-typescript-blue.svg)](https://www.typescriptlang.org)

the end of boilerplate. bake structure, opinions, and rules into projects. see the [documentation site](https://cdaringe.github.io/counsel/).

it's similar to the popular [yeoman/yo](http://yeoman.io/) 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`:

```sh
$ 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`

![](./img/demo-apply.svg)

- `npx counsel check`

![](./img/demo-check-fail.svg)

`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 `Rule`s. 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:

```typescript
// .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

`Rule`s are basic interfaces with:

1. a [`name`](#rulename)
1. an optional [`plan`](#ruleplan) function
1. an optional [`check`](#rulecheck) function
1. an optional list of [`dependencies`](#ruledependencies)
1. an optional list of [`devDependencies`](#ruledevdependencies)

in a nut-shell, _that's it_. counsel is a small set of functions that run these
`Rule`s against your project.

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

```ts
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:

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

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

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

### rule.check

check recieves a [TaskPayload](#taskpayload) as is responsible for ensuring
that a rule is enforced. we've already seen a few examples of check functions:

- [asserting that a keyword was added](#rule)
- [asserting that a readme file exists](##counselfile)

check functions should:

- be synchronous, or return a promise
- `throw` (or reject) `Error`s 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:

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

by using [semver](https://www.npmjs.com/package/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](https://github.com/cdaringe/counsel/blob/master/src/interfaces.ts).

### 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](https://github.com/cdaringe/counsel/blob/7537c31c3cce4bdaaaae18718b53cf9719bb29fb/src/interfaces.ts#L67) ([1](https://github.com/cdaringe/counsel/blob/7537c31c3cce4bdaaaae18718b53cf9719bb29fb/src/interfaces.ts#L28), [2](https://github.com/cdaringe/counsel/blob/7537c31c3cce4bdaaaae18718b53cf9719bb29fb/src/interfaces.ts#L46)).

## batteries

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

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

#### copy

- [copy](https://github.com/cdaringe/counsel/blob/master/src/rules/copy.ts) - copies files or folders into a project

```ts
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

- [filename-format](https://github.com/cdaringe/counsel/blob/master/src/rules/filename-format.ts) - enforces a filename-format convention

```ts
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](https://github.com/cdaringe/counsel/blob/master/src/rules/githook.ts) - installs githook support via [husky](https://www.npmjs.com/package/husky) into a project

```ts
import { rules } from 'counsel'
const { create } = rules.githook

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

#### readme

- [readme](https://github.com/cdaringe/counsel/blob/master/src/rules/readme.ts) - enforces that a project has a readme file

```ts
import { rules } from 'counsel'
const { rule } = rules.readme
```

#### script

- [script](https://github.com/cdaringe/counsel/blob/master/src/rules/script.ts) - installs a npm script to a project

```ts
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

- node library example ruleset
- see it used [in this project, here](https://github.com/cdaringe/counsel/blob/master/.counsel.ts)

## similar works

- [FormidableLabs/builder](https://github.com/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](https://github.com/margdking)