curated-linter
Version:
Creates curated linters, like standard
312 lines (206 loc) • 11.6 kB
Markdown
[](https://travis-ci.org/mightyiam/curated-linter)
[](https://github.com/feross/standard)
[](https://codecov.io/gh/mightyiam/curated-linter)
# curated-linter (WIP)
Makes curated ESLint linters, like [standard](http://standardjs.com/)
## Work in progress
Not all documented features are implemented.
Feel free to contribute.
## Table of contents
<!-- toc -->
- [What is a curated linter?](#what-is-a-curated-linter)
- [Example](#example)
* [`package.json`:](#packagejson)
* [`index.js`](#indexjs)
- [API](#api)
* [`new CuratedLinter(getConfig)`](#new-curatedlintergetconfig)
- [`config`](#config)
* [`name`](#name)
* [`bugs`](#bugs)
* [`packageJson`](#packagejson)
* [`gitIgnore`](#gitignore)
* [`ignore`](#ignore)
* [`cwd`](#cwd)
* [`curatedExtensions`](#curatedextensions)
* [`CLIEngineOptions`](#cliengineoptions)
* [`formatter`](#formatter)
* [`defaultFiles`](#defaultfiles)
- [End user API](#end-user-api)
* [`#lintText(text, config)`](#%23linttexttext-config)
* [`#lintFiles(files, config)`](#%23lintfilesfiles-config)
* [The ESLint results object](#the-eslint-results-object)
- [End user CLI](#end-user-cli)
- [End user configuration via `package.json`](#end-user-configuration-via-packagejson)
- [End user overrides](#end-user-overrides)
* [Example of protected `config` using proxies](#example-of-protected-config-using-proxies)
- [Curated extensions](#curated-extensions)
<!-- tocstop -->
## What is a curated linter?
- a linter based on [ESLint’s `CLIEngine`](http://eslint.org/docs/developer-guide/nodejs-api#cliengine)
- probably has a name (e.g. "standard")
- probably has curated ESLint [configuration](http://eslint.org/docs/user-guide/configuring) and [rules](http://eslint.org/docs/rules/)
- the extent to which the end-user is able to override and/or extend the curated configuration [is configurable](#end-user-overrides)
- may or may not allow [using an official, curated, set of extensions](#curated-extensions)
- may have either or both of a [(promise) API](#api) and a [CLI](#cli)
- CLI may report using a custom [ESLint formatter](http://eslint.org/docs/user-guide/formatters/)
- and more features, that are not in ESLint
## Example
In this example we create our curated linter as an npm package.
### `package.json`:
```json
{
"name": "nofoobar",
"main": "index.js",
"bin": { "nofoobar": "index.js" }
}
```
Yes, the CLI is the same module as the API.
### `index.js`
For instantiation, we do not provide an `config` object—we provide a function that will return a new `config` object with each call—a getter.
```js
#!/usr/bin/env node
const CuratedLinter = require('curated-linter')
// `config` getter function
const getConfig = () => ({
name: 'nofoobar', // CLI name and `package.json` config key
packageJson: true, // read end user config from `package.json`
gitIgnore: true, // (also) ignore what git ignores
formatter: (results, config) => { // customize your CLI error output
return `${config.name}: Nope. Do not use \`foo\` or \`bar\``
// or… something more real than this
},
CLIEngineOptions: { // options for ESLint’s `CLIEngine`
ignore: false,
useEslintrc: false,
rules: { // http://eslint.org/docs/rules/
'id-blacklist': ['foo', 'bar'] // http://eslint.org/docs/rules/id-blacklist
// your usage may include all your favorite rules!
}
// for more `CLIEngine` options: http://eslint.org/docs/developer-guide/nodejs-api#cliengine
}
// not all curated-linter configuration options are used in this example
})
const noFoobar = new CuratedLinter(getConfig)
// The API is ready. Now just export it!
module.exports = noFoobar
```
This example hopefully provided you with a basic understanding. Read below for the API and some awesome features.
## API
### `new CuratedLinter(getConfig)`
> Constructs a curated linter
This is the main export of this package.
`getConfig` must be a function that returns a new [`config`](#config) object with each call.
## `config`
> Contains all of the configuration, policy and behavior of a desired curated linter
The `config` object is provided to the curated linter instance firstly via the `getConfig` [constructor](#new-curatedlintergetconfig) argument.
It can also be provided to the [`lintText`](#linttexttext-config) and [`lintFiles`](#lintfilesfiles-config) methods, for the sake of [possibly allowing the end users to override or extend the curated configuration](#end-user-overrides).
Following are all of the possible properties of `config`:
### `name`
> Machine name of the curated linter
Must be provided if the CLI feature is to be used.
### `bugs`
> URL where the end-user should report bugs
Must be provided if the CLI feature is to be used.
### `packageJson`
> Whether to allow [end user configuration via `package.json`](#end-user-configuration-via-packagejson)
### `gitIgnore`
> Whether to (also) ignore according to `.gitignore`
This determines whether, in addition to any other ignore configuration, to ignore files that are ignored by a possibly existing `.gitignore`.
The `.gitignore` that may be read is the one that may be at the same directory as the `package.json`.
### `ignore`
> List of [glob](https://www.npmjs.com/package/glob#glob-primer) file patterns to ignore
### `cwd`
> Relative file paths will be resolved to this
Defaults to `process.cwd()`.
### `curatedExtensions`
> List of [official curated extensions](#curated-extensions)
### `CLIEngineOptions`
> Will be passed to [`CLIEngine`](http://eslint.org/docs/developer-guide/nodejs-api#cliengine)
This is where you may define your rules, plugins, etc.
Tip: if you can’t find a certain property on this interface, take a look at the `baseConfig` property.
**Caveat**: For the sake of the [`config.gitIgnore`](#gitignore) feature, CuratedLinter does its own file deglobbing and serves ESLint absolute file paths. Therefore, instead of using `config.CLIEngineOptions.ignore(*)` [options](http://eslint.org/docs/developer-guide/nodejs-api#cliengine), it is recommended that you set `config.CLIEngineOptions.ignore` to `false` and use [`config.ignore`](#ignore), instead.
### `formatter`
> Formats the CLI error messages
If this is a string, it must represent a [built-in ESLint formatter](http://eslint.org/docs/user-guide/formatters/).
Otherwise, it must be a function. It will be called as `formatter(results, config)`. Except for the `config` argument, the signature is identical to [ESLint formatters](http://eslint.org/docs/developer-guide/working-with-custom-formatters).
The `config` provided will be post [user overrides](#user-overrides).
### `defaultFiles`
> Files to lint by default
Whether via the CLI or via `#lintFiles`, if no files provided, these will be linted.
An array of [glob](https://www.npmjs.com/package/glob#glob-primer)s.
## End user API
### `#lintText(text, config)`
> Lints provided text
- `text`: text (source code) to lint
- [`config`](#config)
- returns a promise of the [ESLint results object](#the-eslint-results-object)
### `#lintFiles(files, config)`
> Lints files
- `files`: array of [glob](https://www.npmjs.com/package/glob#glob-primer)s of files to lint
- [`config`](#config)
- returns a promise of the [ESLint results object](#the-eslint-results-object)
### The ESLint results object
> Resolved value of promises returned by `#lintFiles` and `#lintText`
Documentation of the ESLint results object can be found in [ESLint’s `#executeOnFiles` documentation](http://eslint.org/docs/developer-guide/nodejs-api#executeonfiles).
## End user CLI
To provide end users with a CLI, the [`config.name`](#name) property must be provided.
Also, there must be a [`package.json` `bin` property](https://docs.npmjs.com/files/package.json#bin), like so:
```json
{
"name": "nofoobar",
"bin": {
"nofoobar": "index.js"
}
}
```
The value of the property under `bin` (here `"index.js"`) must be *a module where `CuratedLinter` is instantiated*. No further method calling required. Thus your [main export](https://docs.npmjs.com/files/package.json#main) and your `bin` can be the same module.
To allow this unified `main`/`bin` "magic" <!-- add link to code -->, the property under the `bin` property *must be identical* to your [`config.name`](#name).
The module must start with the Node.js shebang (`#!/usr/bin/env node`).
## End user configuration via `package.json`
If [`config.packageJson`](#packagejson-1) is `true`, then a config from a *certain property* of the end user’s `package.json` will be read and applied as explained in [user overrides](#user-overrides). That *certain property* is [`config.name`](#name).
For example if `config.name === 'nofoobar'`:
```json
{
"name": "the-users-package",
"nofoobar": {
"ignore": ["**/*.test.js"]
}
}
```
## End user overrides
This is about implementing a policy regarding *whether*, *what* and *how* the end user of the curated linter is allowed to override or extend, the curated `config`.
You may implement whatever policy you like, by having your `getConfig` return a version of `config` that is protected using proxies.
Whether from [end user `config` in `package.json`](#end-user-configuration-via-packagejson) and/or from the end user passing `config` to `lintText` or `lintFiles`, those user `config`s will be merged into the `config` that your `getConfig` returned, using [deep assignment](https://www.npmjs.com/package/deep-assign). This means that, if your `getConfig` provides an unprotected `config`, the user will be able to override any property in that tree.
Overrides occur on method invocation (the CLI also uses those methods). On each such invocation, your curated `config` is retrieved by calling your `getConfig`. Thus, overrides applied on one method call will *not* persist on the instance.
### Example of protected `config` using proxies
The only allowed override is that the `id-blacklist` rule can be *added more identifiers*:
```js
const CuratedLinter = require('curated-linter')
const frozen = {
set: () => false
}
const append = {
set: (target, property, value) => {
target.push(value) // convert setting into appending
return true
}
}
const getConfig = () => (new Proxy({
CLIEngineOptions: new Proxy({
name: 'nofoobar',
rules: new Proxy({
'id-blacklist': new Proxy([
'foo',
'bar'
], append)
}, frozen)
}, frozen)
}, frozen))
const noFooBar = new CuratedLinter(getConfig)
module.exports = noFooBar
```
As you can see, using proxies, it is possible to implement any override/extension policy.
## Curated extensions
This feature allows official, curated extensions to be automatically used if the end user has any of them installed.
An official, curated extension is a separate [ESLint sharable configuration package](http://eslint.org/docs/developer-guide/shareable-configs).
Each member of a provided [`config.curatedExtensions`](#curatedextensions) array is expected to be a name of an ESLint shareable configuration. The `eslint-config-` prefix may be omitted. Each such package, *if the end user has it installed*, will be pushed to the end of the [`config.CLIEngineOptions`](#cliengineoptions)`.baseConfig.extends` array (will be created if `undefined` and will be made into an array if `false`).