GithubHelp home page GithubHelp logo

schreibse / sheriff Goto Github PK

View Code? Open in Web Editor NEW

This project forked from softarc-consulting/sheriff

0.0 0.0 0.0 8.08 MB

Lightweight Modularity for TypeScript Projects

License: MIT License

Shell 0.85% JavaScript 1.39% TypeScript 90.42% CSS 0.15% HTML 5.33% SCSS 1.86%

sheriff's Introduction

Sheriff

Modularity for TypeScript Projects

build status npm version

Sheriff enforces module boundaries and dependency rules in TypeScript.

It is easy to use and has zero dependencies. The only peer dependency is TypeScript itself.

Some examples are located in ./test-projects/.

1. Installation & Setup

Examples are available at https://github.com/softarc-consulting/sheriff/tree/main/test-projects

1.1. Sheriff and ESLint (recommended)

In order to get the best developer experience, we recommend to use Sheriff with the ESLint plugin.

npm install -D @softarc/sheriff-core @softarc/eslint-plugin-sheriff

In your eslintrc.json, insert the rules:

{
  "files": ["*.ts"],
  "extends": ["plugin:@softarc/sheriff/default"]
}
Angular (CLI) Example
{
  "root": true,
  "ignorePatterns": ["**/*"],
  "overrides": [
    // existing rules...
    {
      "files": ["*.ts"],
      "extends": ["plugin:@softarc/sheriff/default"],
    },
  ],
}
Angular (NX) Example
{
  "root": true,
  "ignorePatterns": ["**/*"],
  "plugins": ["@nrwl/nx"],
  "overrides": [
    // existing rules...
    {
      "files": ["*.ts"],
      "extends": ["plugin:@softarc/sheriff/default"],
    },
  ],
}

1.2. Sheriff without ESLint

You can also use Sheriff without ESLint. In this case, you have to run the Sheriff CLI manually.

npm install -D @softarc/sheriff-core

The CLI provides you with commands to list modules, check the rules and export the dependency graph in JSON format.

For more details, see the CLI.

2. Video Introduction

3. Module Boundaries

Every directory with an index.ts is a module. index.ts exports those files that should be accessible from the outside, i.e. it exposes the public API of the module.

In the screenshot below, you see an index.ts, which exposes the holidays-facade.service.ts, but encapsulates the internal.service.ts.

Screenshot 2023-06-24 at 12 24 09

Every file outside of that directory (module) now gets a linting error when it imports the internal.service.ts.

Screenshot 2023-06-24 at 12 23 32

4. Dependency Rules

Sheriff provides access rules.

To define access rules, run npx sheriff init in your project's root folder. This creates a sheriff.config.ts file, where you can define the tags and dependency rules.

The initial sheriff.config.ts doesn't have any restrictions in terms of dependency rules.

4.1. Automatic Tagging

By default, an untagged module has the tag "noTag". All files which are not part of a module are assigned to the "root" module and therefore have the tag "root".

Dependency rules operate on those tags.

Here's an example of a sheriff.config.ts file with auto-tagged modules:

import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  depRules: {
    root: 'noTag',
    noTag: ['noTag', 'root'],
  },
};

The configuration allows every module with tag "noTag" to access any other module with tag "noTag" and "root".

This is the recommendation for existing projects and allows an incremental introduction of Sheriff.

If you start from scratch, you should go with manual tagging.

To disable automatic tagging, set autoTagging to false:

import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  autoTagging: false,
  tagging: {
    // see below...
  },
};

4.2. The root Tag

Let's say we have the following directory structure:

src/app
├── main.ts
├── app.config.ts
├── app.component.ts
├── holidays
│   ├── data
│   │   ├── index.ts
│   │   ├── internal.service.ts
│   │   └── holidays-data.service.ts
│   ├── feature
│   │   ├── index.ts
│   │   └── holidays-facade.service.ts
│── core
│   ├── header.component.ts
│   ├── footer.component.ts

src/app/holidays/data and src/app/holidays/feature are modules. All other files are part of the root module which is tagged with "root". Sheriff assigns the tag "root" automatically. You cannot change it and "root" doesn't have an index.ts. By default, it is not possible to import from the root module.

flowchart LR
  app.config.ts --> holidays/feature/index.ts
  holidays/feature/holidays.component.ts --> holidays/data/index.ts

  subgraph "noTag (holidays/data)"
    holidays/data/index.ts
    holidays/data/internal.service.ts
    holidays/data/holidays-data.service.ts
  end
  subgraph "noTag (holidays/feature)"
    holidays/feature/index.ts
    holidays/feature/holidays.component.ts
  end
  subgraph root
    main.ts
    app.config.ts
    app.component.ts
    core/header.component.ts
    core/footer.component.ts
  end

  style holidays/feature/index.ts stroke: #333, stroke-width: 4px
  style holidays/data/index.ts stroke: #333, stroke-width: 4px
  style root fill: #f9f9f9
Loading

4.3. Manual Tagging

The following snippet shows a configuration where four directories are assigned to a domain and to a module type:

import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  tagging: {
    'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
    'src/app/holidays/data': ['domain:holidays', 'type:data'],
    'src/app/customers/feature': ['domain:customers', 'type:feature'],
    'src/app/customers/data': ['domain:customers', 'type:data'],
  },
  depRules: {},
};

With "domain:" and "type:", we have two dimensions which allows us to define the following rules:

  1. A module can only depend on modules of the same domain
  2. A module of "type:feature" can depend on "type:data" but not the other way around
  3. "root" can depend on a module of "type:feature" and both domains.
import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  version: 1,
  tagging: {
    'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
    'src/app/holidays/data': ['domain:holidays', 'type:data'],
    'src/app/customers/feature': ['domain:customers', 'type:feature'],
    'src/app/customers/data': ['domain:customers', 'type:data'],
  },
  depRules: {
    'domain:holidays': ['domain:holidays'], // Rule 1
    'domain:customers': ['domain:customers'], // Rule 1
    'type:feature': 'type:data', // Rule 2
    root: ['type:feature', 'domain:holidays', 'domain:customers'], // Rule 3
  },
};

If those roles are violated, a linting error is thrown:

Screenshot 2023-06-13 at 17 50 41

For existing projects, you want to tag modules and define dependency rules incrementally.

If you only want to tag modules from "holidays" and leave the rest auto-tagged, you can do so:

import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  tagging: {
    'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
    'src/app/holidays/data': ['domain:holidays', 'type:data'],
  },
  depRules: {
    'domain:holidays': ['domain:holidays', 'noTag'],
    'type:feature': ['type:data', 'noTag'],
    root: ['type:feature', 'domain:holidays', 'noTag'],
    noTag: ['noTag', 'root'],
  },
};

All modules in the directory "customers" have the tag "noTag". Be aware, that every module from "domain:holidays" can now depend on any module from directory "customers" but not vice versa.

4.4. Nested Paths

Nested paths simplify the configuration. Multiple levels are allowed.

import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  tagging: {
    'src/app': {
      holidays: {
        feature: ['domain:holidays', 'type:feature'],
        data: ['domain:holidays', 'type:data'],
      },
      customers: {
        feature: ['domain:customers', 'type:feature'],
        data: ['domain:customers', 'type:data'],
      },
    },
  },
  depRules: {
    'domain:holidays': ['domain:holidays'],
    'domain:customers': ['domain:customers'],
    'type:feature': 'type:data',
    root: ['type:feature', 'domain:holidays', 'domain:customers'],
  },
};

4.5. Placeholders

Placeholders help with repeating patterns. They have the snippet <name>.

import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  tagging: {
    'src/app': {
      holidays: {
        '<type>': ['domain:holidays', 'type:<type>'],
      },
      customers: {
        '<type>': ['domain:customers', 'type:<type>'],
      },
    },
  },
  depRules: {
    'domain:holidays': ['domain:holidays'],
    'domain:customers': ['domain:customers'],
    'type:feature': 'type:data',
    root: ['type:feature', 'domain:holidays', 'domain:customers'],
  },
};

We can use placeholders on all levels. Our configuration is now more concise.

import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  version: 1,
  tagging: {
    'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
  },
  depRules: {
    'domain:holidays': ['domain:holidays'],
    'domain:customers': ['domain:customers'],
    'type:feature': 'type:data',
    root: ['type:feature', 'domain:holidays', 'domain:customers'],
  },
};

4.6. depRules Functions & Wildcards

We could use functions for depRules instead of static values. The names of the tags can include wildcards:

import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  version: 1,
  tagging: {
    'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
  },
  depRules: {
    'domain:*': ({ from, to }) => from === to,
    'type:feature': 'type:data',
    root: ['type:feature', ({ to }) => to.startsWith('domain:')],
  },
};

or

import { sameTag, SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
  version: 1,
  tagging: {
    'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
  },
  depRules: {
    'domain:*': [sameTag, 'shared'],
    'type:feature': 'type:data',
    root: ['type:feature', ({ to }) => to.startsWith('domain:')],
  },
};

5. CLI

The core package (@softarc/sheriff-core) comes with a CLI to initialize the configuration file, list modules, check the rules and export the dependency graph in JSON format.

5.1. init

Run npx sheriff init to create a sheriff.config.ts. Its configuration runs with automatic tagging, meaning no dependency rules are in place, and it only checks for the module boundaries.

5.2. verify [main.ts]

Run npx sheriff verify main.ts to check if your project violates any of your rules. main.ts is the entry file where Sheriff should traverse the imports.

Depending on your project, you will likely have a different entry file. For example, with an Angular CLI-based project, it would be npx sheriff verify src/main.ts.

You can omit the entry file if you set a value to the property entryFile in the sheriff.config.ts.

In that case, you only run npx sheriff verify.

5.3. list [main.ts]

Run npx sheriff list main.ts to print out all your modules along their tags. As explained above, you can alternatively use the entryFile property in sheriff.config.ts.

5.4. export [main.ts]

Run npx sheriff export main.ts > export.json and the dependency graph will be stored in export.json in JSON format. The dependency graph starts from the entry file and includes all reachable files. For every file, it will include the assigned module as well as the tags.

6. Integrating Sheriff into large Projects via excludeRoot

It is usually not possible to modularize an existing codebase at once. Instead, we have to integrate Sheriff incrementally.

Next to automatic tagging, we introduce manual tagged modules step by step.

The recommended approach is start with only one module. For example holidays/feature. All files from the outside have to import from the module's index.ts, and it has the tags "type:feature".

It is very likely that holidays/feature depends on files in the "root" module. Since "root" doesn't have an index.ts, no other module can depend on it:

flowchart LR
  holidays/feature/holidays.component.ts -- fails -->holidays/data/holidays-data.service.ts
  app.config.ts -- succeeds -->holidays/feature/holidays.component.ts
  subgraph root
    holidays/data/holidays-data.service.ts
    app.config.ts
    main.ts
    app.component.ts
    core/header.component.ts
    core/footer.component.ts
    holidays/data/internal.service.ts
  end
  subgraph "type:feature (holidays/feature)"
    holidays/feature/index.ts
    holidays/feature/holidays.component.ts
  end


  style holidays/feature/index.ts stroke: #333, stroke-width: 4px
  style root fill: #f9f9f9
  style holidays/data/holidays-data.service.ts fill:coral
  style app.config.ts fill:lightgreen
Loading

We can disable the deep import checks for the root module by setting excludeRoot in sheriff.config.ts to true:

export const config: SheriffConfig = {
  excludeRoot: true, // <-- set this
  tagging: {
    'src/shared': 'shared',
  },
  depRules: {
    root: 'noTag',
    noTag: ['noTag', 'root'],
    shared: anyTag,
  },
};
flowchart LR
  holidays/feature/holidays.component.ts  --> holidays/data/holidays-data.service.ts
  app.config.ts --> holidays/feature/holidays.component.ts
  subgraph root
    holidays/data/holidays-data.service.ts
    app.config.ts
    main.ts
    app.component.ts
    core/header.component.ts
    core/footer.component.ts
    holidays/data/internal.service.ts
  end
  subgraph "type:feature (holidays/feature)"
    holidays/feature/index.ts
    holidays/feature/holidays.component.ts
  end


  style holidays/feature/index.ts stroke: #333, stroke-width: 4px
  style root fill: #f9f9f9
  style holidays/data/holidays-data.service.ts fill:lightgreen
  style app.config.ts fill:lightgreen
Loading

Once all files from "root" import form shared's index.ts, create another module and do the same.

7. Planned Features

For feature requests, please add an issue at https://github.com/softarc-consulting/sheriff.

  • Editor
  • Angular Schematic
  • Feature Shell: It shouldn't be necessary to create a feature subdirectory for a domain, since feature has access to everything
  • Dependency rules for node_modules
  • Find cyclic dependencies
  • Find unused files
  • TestCoverage 100%
  • UI for Configuration
  • Migration from Nx (automatic)
  • Cache

sheriff's People

Contributors

rainerhahnekamp avatar fynnfeldpausch avatar chwoerz avatar

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.