Skip to content

Commit

Permalink
docs: add docs for barrel-less modules
Browse files Browse the repository at this point in the history
add documentation for barrel-modules and
the release notes for the upcoming v0.18
  • Loading branch information
rainerhahnekamp authored Oct 18, 2024
1 parent 38265af commit 86cf9f6
Show file tree
Hide file tree
Showing 17 changed files with 170 additions and 78 deletions.
92 changes: 39 additions & 53 deletions docs/docs/dependency-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,21 @@ displayed_sidebar: tutorialSidebar
---

## Introduction

Dependency rules determine which modules can access other modules. Since managing dependencies on a per-module basis
doesn't scale well, Sheriff utilizes tags to group modules together. Dependency rules are then defined based on these
tags.
Dependency rules determine which modules can access each other. Since managing dependencies on a per-module basis doesn't scale well, Sheriff utilizes tags to group modules together. Dependency rules are then defined based on these tags.

Each tag specifies a list of other tags it can access. To maintain clarity, it’s best practice to categorize tags into
two groups: one for defining the module's domain/scope and another for defining the module's type.

For instance, if an application includes a customer domain and a holiday domain, these would define the domain tags.

Within a domain, you might have different modules distinguished by type tags. For example, one module might contain
smart components, another might have dumb components, and another might handle logic.
A domain has different modules distinguished by type tags. For example, one module might contain smart components, another might have dumb components, and another might handle logic.

Domain tags could be `domain:customer` and `domain:holiday`. Type tags could be `type:feature` (for smart components),
`type:ui` (for dumb components), or `type:data` (for logic).

Each module should have both a domain tag and a type tag. For example, a module containing smart components for
customers would be tagged with `domain:customer` and `type:feature`. A module in the same domain but containing UI
components would be tagged with `domain:customer` and `type:ui`.
In this case, each module has both a domain tag and a type tag. For example, a module containing smart components for
customers would have `domain:customer` and `type:feature`. A module in the same domain but containing UI
components would have `domain:customer` and `type:ui`.

Dependency rules specify that a module tagged with `domain:customer` can only access modules with the same domain tag.
Additionally, a module tagged with `type:feature` can access modules tagged with `type:ui` and `type:data`.
Expand Down Expand Up @@ -121,24 +117,19 @@ flowchart LR

## Automatic Tagging

Initially, you don't need to assign tags to modules manually.

Any module that remains untagged is automatically assigned the `noTag` tag.
Sheriff automatically detects modules and assigns the `noTag` tag to them.

All files that are not part of a specific module are assigned to the `root` module and therefore receive the `root` tag.
It assigns all files that aren't part of a module to the `root` module. The root module gets the `root` tag.

However, it is essential to set up the dependency rules. Specifically, you must allow the [`root` tag](#the-root-tag) (
i.e., the root module) to access all other untagged modules.
It's essential to set up the dependency rules. Specifically, the [`root` tag](#the-root-tag) (i.e., the root module) needs to access all modules tagged with `noTag`.

This configuration is done by setting the `depRules` in the _sheriff.config.ts_ file. The `depRules` is an object
literal where each key represents the from tag and its value specifies the tags it can access.
The `depRules` property in the _sheriff.config.ts_ file defines the dependency rules. This property is an object literal where each key represents the tag of a module that wants to access another module. Its value specifies the tags it can access.

Initially, all modules can access each other. As a result, every `noTag` module can access other `noTag` modules as well
as those tagged with `root`.
Initially, all modules can access each other, meaning that every `noTag` module can access other `noTag` modules.

If you use the [CLI](./cli) to initialize Sheriff for the first time, this configuration will be set up automatically.
The initial configuration from the [CLI](./cli) includes this setup.

Here's an example configuration in _sheriff.config.ts_:
Heres an example configuration in `sheriff.config.ts`:

```typescript
import { SheriffConfig } from '@softarc/sheriff-core';
Expand All @@ -151,9 +142,9 @@ export const sheriffConfig: SheriffConfig = {
};
```

This approach is recommended for existing projects, as it allows for the incremental introduction of Sheriff.
That is also the recommendation for existing projects because it allows an incremental integration of Sheriff.

If you are starting a new project, you can skip this step and proceed directly to [manual tagging](#manual-tagging).
For green-field projects, the [manual tagging](#manual-tagging) is the better option.

---

Expand All @@ -164,15 +155,15 @@ import { SheriffConfig } from '@softarc/sheriff-core';

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

## The `root` Tag

Consider the following directory structure:
Given the following project structure:

<pre>
src/app
Expand All @@ -192,10 +183,11 @@ src/app
│ ├── footer.component.ts
</pre>

The directories _src/app/holidays/data_ and _src/app/holidays/feature_ are considered modules. All other files are part
of the root module, which is automatically tagged with `root` by Sheriff. This tag cannot be changed, and the root
module does not include an _index.ts_ file. [By default](./integration), importing from the root module is not
permitted.
The directories `src/app/holidays/data` and `src/app/holidays/feature` are barrel modules. All other files are part of the root module, which is automatically tagged with `root` by Sheriff

This tagging of the `root` module cannot be changed. With disabled barrel-less mode (`enableBarrelLess: false`), which is the default, no module can access the root module.

The property `excludeRoot` can disable this behavior. [By default](./integration). The best option, though, is to enable barrel-less mode which makes `root` a barrel-less module.

```mermaid
flowchart LR
Expand Down Expand Up @@ -226,16 +218,17 @@ flowchart LR

## Manual Tagging

To assign tags manually, you need to provide a `tagging` object in the _sheriff.config.ts_ file. The keys in this object
represent the module directories, and the corresponding values are the tags assigned to those modules.
The `modules` property in the `sheriff.config.ts` defines barrel-less modules but also assigns tags to modules, regardless if barrel or barrel-less.

The keys of `modules` represent the module directories, and the corresponding values are the tags assigned to those modules.

The following snippet demonstrates a configuration where four directories are assigned both a domain and a module type:

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

export const sheriffConfig: SheriffConfig = {
tagging: {
modules: {
'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
'src/app/holidays/data': ['domain:holidays', 'type:data'],
'src/app/customers/feature': ['domain:customers', 'type:feature'],
Expand All @@ -245,9 +238,9 @@ export const sheriffConfig: SheriffConfig = {
};
```

By using `domain:_` and `type:_`, we establish two dimensions that allow us to define the following rules:
By using `domain:_` and `type:_` define two dimension for the whole project. The following rules should apply

1. A module can only depend on other modules within the same domain.
1. A module can only depend on other modules of the same domain.
2. A module tagged as `type:feature` can depend on `type:data`, but the reverse is not allowed.
3. The `root` module can depend on modules tagged as `type:feature`. Since the root module only has the `root` tag,
there is no need to include domain tags.
Expand All @@ -256,8 +249,7 @@ By using `domain:_` and `type:_`, we establish two dimensions that allow us to d
import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
modules: {
'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
'src/app/holidays/data': ['domain:holidays', 'type:data'],
'src/app/customers/feature': ['domain:customers', 'type:feature'],
Expand All @@ -276,16 +268,13 @@ If these rules are violated, a linting error will be triggered:

<img width="1512" alt="Screenshot 2023-06-13 at 17 50 41" src="/img/dependency-rules-1.png"></img>

For existing projects, it is recommended to tag modules and define dependency rules incrementally.

If you prefer to only tag modules within the "holidays" directory and leave the rest of the modules auto-tagged, you can
do so:
If only the modules within the director "holidays" should get tags, and the other modules should be auto-tagged, i.e. `noTag`, the configuration would look like this:

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

export const sheriffConfig: SheriffConfig = {
tagging: {
modules: {
'src/app/holidays/feature': ['domain:holidays', 'type:feature'],
'src/app/holidays/data': ['domain:holidays', 'type:data'],
},
Expand All @@ -298,8 +287,7 @@ export const sheriffConfig: SheriffConfig = {
};
```

All modules in the "customers" directory are assigned the `noTag` tag. Be aware that this setup allows any module from
`domain:holidays` to depend on modules within the "customers" directory, but the reverse is not permitted.
Note: This setup allows any module from `domain:holidays` to depend on modules within the `customers` directory, but the reverse is not permitted.

## Nested Paths

Expand All @@ -309,7 +297,7 @@ Nested paths simplify the configuration. Multiple levels are allowed.
import { SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
tagging: {
modules: {
'src/app': {
holidays: {
feature: ['domain:holidays', 'type:feature'],
Expand All @@ -332,13 +320,13 @@ export const sheriffConfig: SheriffConfig = {

## Placeholders

Placeholders help with repeating patterns. They have the snippet `<name>`.
Placeholders help with repeating patterns. They have the syntax `<name>`, where `name` is the placeholder name.

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

export const sheriffConfig: SheriffConfig = {
tagging: {
modules: {
'src/app': {
holidays: {
'<type>': ['domain:holidays', 'type:<type>'],
Expand All @@ -357,13 +345,13 @@ export const sheriffConfig: SheriffConfig = {
};
```

We can use placeholders on all levels. Our configuration is now more concise.
Placeholders are available on all levels. The configuration could therefore further be improved.

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

export const sheriffConfig: SheriffConfig = {
tagging: {
modules: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
Expand All @@ -377,14 +365,13 @@ export const sheriffConfig: SheriffConfig = {

## `depRules` Functions & Wildcards

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

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

export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
modules: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
Expand All @@ -401,8 +388,7 @@ or use `sameTag`, which is a pre-defined function.
import { sameTag, SheriffConfig } from '@softarc/sheriff-core';

export const sheriffConfig: SheriffConfig = {
version: 1,
tagging: {
modules: {
'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
},
depRules: {
Expand Down
22 changes: 14 additions & 8 deletions docs/docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ displayed_sidebar: tutorialSidebar
It is usually not possible to modularize an existing codebase at once. Instead, we have to integrate Sheriff
incrementally.

Next to [automatic tagging](./dependency-rules#automatic-tagging), we introduce manual tagged modules step by step.
Next to [automatic tagging](./dependency-rules#automatic-tagging), we introduce 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".
## With barrel-less modules

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:
The recommended approach is start with only one module. For example `holidays/feature`. Encapsulated files of that modules need to be moved to the `internals` folder. If `holidays/feature` is barrel-less, it can access `root`, given the dependency rules allow access to tag `root`.

By default, barrel-less modules are disabled. They have to be enabled in `sheriff.config.ts` via `enableBarrelLess: true`.

## Without barrel-less modules

If Sheriff only supports barrel modules, then the integration would still progress module by module. `holidays/feature` gets an `index.ts` and defines its exposed files. Since `root` would be barrel-less, `holidays/feature` cannot access it.

```mermaid
flowchart LR
Expand All @@ -39,12 +43,12 @@ flowchart LR
style app.config.ts fill:lightgreen
```

We can disable the deep import checks for the **root** module by setting `excludeRoot` in _sheriff.config.ts_ to `true`:
There is a special property for this use case: `excludeRoot`. Once set to `true`, all modules can access all files in the root module.

```typescript
export const config: SheriffConfig = {
excludeRoot: true, // <-- set this
tagging: {
modules: {
'src/shared': 'shared',
},
depRules: {
Expand Down Expand Up @@ -80,4 +84,6 @@ flowchart LR
style app.config.ts fill:lightgreen
```

Once all files from "root" import form **shared's** _index.ts_, create another module and do the same.
---

Please note that the `excludeRoot` property only makes sense with `enableBarrelLess: false`.
6 changes: 4 additions & 2 deletions docs/docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ title: Introduction
displayed_sidebar: tutorialSidebar
---

Sheriff enforces module boundaries and dependency rules in TypeScript.
**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.
- **[Module boundaries](./module_boundaries.md)** ensure that files within a module are encapsulated, preventing access from outside the module. Modules are defined either via a `sheriff.config.ts` file or by the presence of a barrel file, like `index.ts`.

- **[Dependency rules](./dependency-rules.md)** allow you to specify which modules can depend on one another, enforcing a clear structure throughout your project. Like module boundaries, these rules are defined in the `sheriff.config.ts` file.

Sheriff has **zero external dependencies**, with TypeScript as its only peer dependency.
Loading

0 comments on commit 86cf9f6

Please sign in to comment.