/eslint-plugin-boundaries

Eslint plugin checking architecture boundaries between elements

Primary LanguageJavaScriptMIT LicenseMIT

Build status Coverage Status Quality Gate

NPM dependencies Renovate Last commit Last release

NPM downloads License

eslint-plugin-boundaries

In words of Robert C. Martin, "Software architecture is the art of drawing lines that I call boundaries. Those boundaries separate software elements from one another, and restrict those on one side from knowing about those on the other." (*acknowledgements)

This plugin ensures that your architecture boundaries are respected by the elements in your project checking the folders and files structure and the import statements (Read the main rules overview chapter for better comprehension.). It is not a replacement for eslint-plugin-import, on the contrary, the combination of both plugins is recommended.

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's devDependencies:

npm install --save-dev eslint eslint-plugin-boundaries

eslint-plugin-boundaries does not install ESLint for you. You must install it yourself.

Then, in your .eslintrc.json file:

{
  "plugins": ["boundaries"],
  "extends": ["plugin:boundaries/recommended"],
  "settings": {
    "boundaries/types": ["helpers", "models", "views", "controllers"],
  }
}

Each architecture should define its own element types to make the plugin work properly. Otherwise, you will receive a warning and rules won't be applied. Read the configuration chapter for further info.

Main rules overview

Private elements

This rule ensures that elements can't require other element's children. So, when an element B is children of A, B becomes a "private" element of A, and only A can use it. (Also other descendants of A could use B if "allowUncles" option is enabled in the rule).

Read the docs of the boundaries/no-private rule for further info.

Entry point

This rule ensures that elements can't import another file from other element than the defined entry point for that type (index.js by default)

Read the docs of the boundaries/entry-point rule for further info.

Allowed element types

This rule ensures that dependencies between your project element types are allowed.

Examples of usage:

  • Define types in your project as "models", "views" and "controllers". Then ensure that "views" and "models" can be required only by "controllers", and "controllers" will never be used by "views" or "models".
  • Define types in your project as "components", "views", "layouts", "pages", "helpers". Then ensure that "components" can only require "helpers", that "views" can only require "components" or "helpers", that "layouts" can only require "views", "components" or "helpers", and that "pages" can require any other element type.

Read the docs of the boundaries/allowed-types rule for further info.

Forbidden external modules

External dependencies used by each type of element in your project can be checked using this rule. For example, you can define that "helpers" can't import react, or "components" can't import react-router-dom.

Read the docs of the boundaries/no-external rule for further info.

Requisites

The plugin needs your files and folders to be named and organized in a way that it can recognize the type of an element, and its position in an elements hierarchy (And this is not necessarily bad, because if the plugin can recognize them, an human developer will do it easily too, and your project will become more organized before even running eslint 😉).

So there are basic rules that have to be followed to make it work:

  • Folder names: Elements of a certain type must be always allocated under a folder which name should be the type itself (models/[x], controllers/[y], helpers/[z], etc.).
  • Folders hierarchy: Type folders can also be created inside elements folders. Then these children elements will become "private" ones of the element containing them, and the "private elements" rule will be applied (controllers/[x]/models/[y], controllers/[x]/views/[z], etc.).
  • Unique entry point for each element: Ideally, each element should expose an unique file as "entry point". By default, the plugin considers this file being the index.js one, and will not allow to import any other element file (using only element folder names in imports statements is the recommended way to work when using this plugin), but this behavior can be configured. You can define a different main file pattern or even a different file pattern for each element type. You could also change the configuration to allow importing any element file, but this is strongly not recommended, as it breaks the motivation of the plugin itself.

The plugin won't apply rules to an element when it does not recognize its type, but you can force all elements in your project to follow these patterns enabling the boundaries/prefer-recognized-types rule.

Rules

Configuration

Global settings

Each architecture should define its own element types to make the plugin work properly. Otherwise, you will receive a warning and rules won't be applied.

In your .eslintrc.json file:

{
  "settings": {
    "boundaries/types": ["helpers", "components", "views", "layouts", "pages", "app"],
    "boundaries/ignore": ["src/**/*.spec.js", "src/**/*.test.js"],
    "boundaries/alias": {
      "components": "src/components",
      "helpers": "src/helpers"
    }
  }
}
  • boundaries/types: Folder names containing an specific type of elements. _In the example above, the plugin will recognize all children folders of a folder named helpers" as elements of type "helpers" (helpers/[x], components/foo/helpers/[y], etc.).
  • boundaries/ignore: Files matching these glob expressions will be ignored by the plugin.
  • boundaries/alias: If you are using alias in the project (e.g. webpack resolve alias or babel-plugin-module-resolver), you'll have to provide those alias also to the plugin configuration.

NOTE: The plugin uses globule under the hood to do file names and folders matching.

Rules configuration

Some rules require extra configuration, and it has to be defined in each specific "rule" property of the .eslintrc.json file. For example, allowed types relationships has to be provided for the boundaries/allowed-types rule. Rules requiring extra configuration will print a warning in case they are enabled without the needed extra config. Please refer to the docs of each rule for further info.

Predefined configurations

This plugin is distributed with two different predefined configurations: "recommended" and "strict".

Recommended

We recommend to use this setting until you are familiarized with the folders structure required by this plugin and its configuration, or if you are applying the plugin to an already existing project. Rules boundaries/prefer-recognized-types, boundaries/no-import-not-recognized-types and boundaries/no-import-ignored are disabled, so it allows to have parts of the project non-compliant with your element types, allowing refactor the code progressively.

{
  "extends": ["plugin:boundaries/recommended"]
}

Strict

All rules are enabled, so all elements in the project will be compliant with your architecture boundaries. 😃

{
  "extends": ["plugin:boundaries/strict"]
}

Custom configuration

Settings and specific rules can be configured separately following the eslint configuration docs.

Acknowledgements

* Quote from Robert C. Martin's book "Clean Architecture: A Craftsman's Guide to Software Structure and Design".

Contributing

Contributors are welcome. Please read the contributing guidelines and code of conduct.

License

MIT, see LICENSE for details.