diff --git a/docs/docs/dependency-rules.md b/docs/docs/dependency-rules.md index dbf9b09..7be7b7e 100644 --- a/docs/docs/dependency-rules.md +++ b/docs/docs/dependency-rules.md @@ -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`. @@ -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_: +Here’s an example configuration in `sheriff.config.ts`: ```typescript import { SheriffConfig } from '@softarc/sheriff-core'; @@ -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. --- @@ -164,7 +155,7 @@ import { SheriffConfig } from '@softarc/sheriff-core'; export const sheriffConfig: SheriffConfig = { autoTagging: false, - tagging: { + modules: { // see below... }, }; @@ -172,7 +163,7 @@ export const sheriffConfig: SheriffConfig = { ## The `root` Tag -Consider the following directory structure: +Given the following project structure:
 src/app
@@ -192,10 +183,11 @@ src/app
 │   ├── footer.component.ts
 
-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 @@ -226,8 +218,9 @@ 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: @@ -235,7 +228,7 @@ The following snippet demonstrates a configuration where four directories are as 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'], @@ -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. @@ -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'], @@ -276,16 +268,13 @@ If these rules are violated, a linting error will be triggered: Screenshot 2023-06-13 at 17 50 41 -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'], }, @@ -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 @@ -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'], @@ -332,13 +320,13 @@ export const sheriffConfig: SheriffConfig = { ## Placeholders -Placeholders help with repeating patterns. They have the snippet ``. +Placeholders help with repeating patterns. They have the syntax ``, where `name` is the placeholder name. ```typescript import { SheriffConfig } from '@softarc/sheriff-core'; export const sheriffConfig: SheriffConfig = { - tagging: { + modules: { 'src/app': { holidays: { '': ['domain:holidays', 'type:'], @@ -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:'], }, depRules: { @@ -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:'], }, depRules: { @@ -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:'], }, depRules: { diff --git a/docs/docs/integration.md b/docs/docs/integration.md index b84747b..b76c9fb 100644 --- a/docs/docs/integration.md +++ b/docs/docs/integration.md @@ -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 @@ -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: { @@ -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`. diff --git a/docs/docs/introduction.md b/docs/docs/introduction.md index 9db9923..25964fc 100644 --- a/docs/docs/introduction.md +++ b/docs/docs/introduction.md @@ -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. diff --git a/docs/docs/module_boundaries.md b/docs/docs/module_boundaries.md index 71258f5..1c513a7 100644 --- a/docs/docs/module_boundaries.md +++ b/docs/docs/module_boundaries.md @@ -3,15 +3,89 @@ sidebar_position: 3 title: Module Boundaries displayed_sidebar: tutorialSidebar --- +There are two types of modules: **Barrel Modules**, which include an `index.ts` file in their root folder, and **Barrel-less Modules**. Barrel-less modules require a configuration file, while barrel modules do not. -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. +The recommendation is for barrel-less modules because they optimize tree-shaking. -In the screenshot below, you see an _index.ts_, which exposes the _holidays-facade.service.ts_, but encapsulates the -_internal.service.ts_. +A project allows both module types. However, if a module, configured as barrel-less, contains a barrel file, it becomes a barrel module. -Screenshot 2023-06-24 at 12 24 09 +## Barrel-less Modules -Every file outside of that directory (module) now gets a linting error when it imports the _internal.service.ts_. +Barrel-less modules have a subdirectory `internal`. All files in that subdirectory `internal` are encapsulated, i.e. other modules cannot access them. -Screenshot 2023-06-24 at 12 23 32 +The configuration file `sheriff.config.ts` defines these modules. The CLI command `npx sheriff init` generates the configuration automatically with the following content: + +```typescript +export const config: SheriffConfig = { + modules: {}, + depRules: { + root: 'noTag', + noTag: 'noTag', + }, +}; +``` + +The `depRules` can stay as they are. They allow all modules to access each other. + +The `modules` object defines the modules. The key is a directory path relative to the project root. The value is a string or an array of strings, defining the tags of the module. + +For example, the current project has the directories _db_ and _web_. `modules` defines them as follows: + +```typescript +export const config: SheriffConfig = { + modules: { + db: 'noTag', + web: 'noTag' + }, + enableBarrelLess: true, // <-- this is important + depRules: { + root: "noTag", + noTag: "noTag" + } +}; +``` + +Again, the value `noTag` means that there is no restriction on which modules can access each other. + +The `web` module has a dependency on `db`. The file `fetcher.ts` in `web` imports `db.ts` from `db`. This is a valid import because `db.ts` is not located in the `internal` directory of `db`. + +Therefore, ESLint does not show any errors. + +Valid Import + +However, if `fetcher.ts` accesses `credentials.ts` from `db`, ESLint will show an error. This results in an encapsulation violation because `credentials.ts` is located in the `internal` directory of `db`. + +Invalid Import + +## Barrel Modules + +Barrel modules have an `index.ts` in their root folder. Sheriff detects them automatically, even if `modules` in `sheriff.config.ts` doesn't define them. + +The `index.ts` file exports the files that other modules can access. The files that are not exported are encapsulated. + +Since Sheriff detects them automatically, no configuration file is necessary. However, if a `sheriff.config.ts` exists, the initial content from the CLI is enough. + +```typescript +export const config: SheriffConfig = { + depRules: { + root: 'noTag', + noTag: 'noTag', + }, +}; +``` + +The screenshot below shows the same example with `db` and `web` as barrel modules. `db` has an `index.ts` that exports `db.ts`. `credential.ts` is not in an `internal` folder but is still encapsulated because it is not exported. + + + +`fetcher.ts` accessing `db.ts` causes therefore no error. + + + +An access to the non-exported `credential.ts` causes an error. + + + +--- + +It is also possible to disable the automatic module detection. For more information, see [Dependency Rules](./dependency-rules.md#automatic-tagging). diff --git a/docs/docs/release-notes/0.18.md b/docs/docs/release-notes/0.18.md new file mode 100644 index 0000000..8001813 --- /dev/null +++ b/docs/docs/release-notes/0.18.md @@ -0,0 +1,24 @@ +# 0.18 + +## Barrel-less Modules +This version introduces barrel-less modules, which are disabled by default. To enable them, set `enableBarrelLess: true` in `sheriff.config.ts`. + +Barrel-less modules are not automatically set; they need to be defined in `sheriff.config.ts`. Unlike the previous barrel modules, barrel-less modules don't have an `index.ts` file. Instead, all files located in the `internal` subdirectory are encapsulated and not accessible from other modules. + +Barrel modules are still supported and detected automatically. Access to barrel-less modules is done directly, without an `index.ts` file. + +For more information, see the [Module Boundaries](../module_boundaries.md). + +## Renaming `tagging` to `modules` and Deprecation of the `tagging` Property + +With barrel modules, the `tagging` property was used to assign tags to modules. If a module wasn't tagged, it was automatically assigned the `noTag` tag. + +For barrel-less modules, the `tagging` property defines the modules. This means future module-based settings will also be handled through `tagging`. + +To clarify that `tagging` is the actual definition of the modules, the `tagging` property has been deprecated and renamed to `modules`. + +The `tagging` property still works, but we recommend renaming it to `modules`. No other changes are required. + +## Renaming of "Deep Import" to "Encapsulation Violation" + +Since in barrel-less modules, every access is a deep import but can still be valid, the ESLint rule "Deep Import" has been renamed to "Encapsulation Violation". The "Deep Import" rule still works but is deprecated. diff --git a/docs/docs/roadmap.md b/docs/docs/roadmap.md index a63deb2..ddd3a92 100644 --- a/docs/docs/roadmap.md +++ b/docs/docs/roadmap.md @@ -9,13 +9,13 @@ displayed_sidebar: roadmapSidebar In order to reach version 1, we plan to add following features -- ESLint flat config -- Barrel-less modules: It should be possible to define encapsulation without an _index.ts_. This is because barrel files cause a problem for any code-splitting/tree-shaking process. We plan to provide following alternatives to the barrel file: +- ✅ ESLint flat config +- ✅ Barrel-less modules: It should be possible to define encapsulation without an _index.ts_. This is because barrel files cause a problem for any code-splitting/tree-shaking process. We plan to provide following alternatives to the barrel file: - _internal_ folder - folder/files with `_` prefix - decorators @private/@public -- optional cache: For large applications we require a cache together with a background process that watches the filesystem and updates the cache. In v1, the cache will be disabled by default. In later versions, it will become enabled. -- Angular schematic: For Angular application, there will be a migration available that allows to update Sheriff via `ng update`, and install it via `ng add @softarc/ng-sheriff`. +- ☑️ optional cache: For large applications we require a cache together with a background process that watches the filesystem and updates the cache. In v1, the cache will be disabled by default. In later versions, it will become enabled. +- ☑️ Angular schematic: For Angular application, there will be a migration available that allows to update Sheriff via `ng update`, and install it via `ng add @softarc/ng-sheriff`. ## Future plans diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 7ea3095..9d7cf68 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -74,7 +74,7 @@ const config: Config = { label: 'Docs', }, { - to: '/docs/release-notes/0.17', + to: '/docs/release-notes/0.18', label: 'Release Notes', position: 'left', }, diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 9fdd3b3..15a3e07 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -13,7 +13,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { // By default, Docusaurus generates a sidebar from the docs folder structure tutorialSidebar: ['introduction', 'installation', 'module_boundaries', 'dependency-rules', 'cli', 'integration'], - releaseNotesSidebar: [{type: 'category', label: 'Release Notes', items: ['release-notes/0.17', 'release-notes/0.16']}], + releaseNotesSidebar: [{type: 'category', label: 'Release Notes', items: ['release-notes/0.18', 'release-notes/0.17', 'release-notes/0.16']}], roadmapSidebar: ['roadmap'], }; diff --git a/docs/static/img/module-boundaries-1.png b/docs/static/img/module-boundaries-1.png deleted file mode 100644 index 2494eb1..0000000 Binary files a/docs/static/img/module-boundaries-1.png and /dev/null differ diff --git a/docs/static/img/module-boundaries-2.png b/docs/static/img/module-boundaries-2.png deleted file mode 100644 index 265ee0d..0000000 Binary files a/docs/static/img/module-boundaries-2.png and /dev/null differ diff --git a/docs/static/img/module-boundaries-barrel-file.png b/docs/static/img/module-boundaries-barrel-file.png new file mode 100644 index 0000000..9f4ea3a Binary files /dev/null and b/docs/static/img/module-boundaries-barrel-file.png differ diff --git a/docs/static/img/module-boundaries-barrel-invalid.png b/docs/static/img/module-boundaries-barrel-invalid.png new file mode 100644 index 0000000..fec27d6 Binary files /dev/null and b/docs/static/img/module-boundaries-barrel-invalid.png differ diff --git a/docs/static/img/module-boundaries-barrel-less-invalid.png b/docs/static/img/module-boundaries-barrel-less-invalid.png new file mode 100644 index 0000000..c84c2bd Binary files /dev/null and b/docs/static/img/module-boundaries-barrel-less-invalid.png differ diff --git a/docs/static/img/module-boundaries-barrel-less-valid.png b/docs/static/img/module-boundaries-barrel-less-valid.png new file mode 100644 index 0000000..93d702a Binary files /dev/null and b/docs/static/img/module-boundaries-barrel-less-valid.png differ diff --git a/docs/static/img/module-boundaries-barrel-valid.png b/docs/static/img/module-boundaries-barrel-valid.png new file mode 100644 index 0000000..331cecc Binary files /dev/null and b/docs/static/img/module-boundaries-barrel-valid.png differ diff --git a/packages/core/src/lib/cli/init.ts b/packages/core/src/lib/cli/init.ts index 8d8d221..eaac8b3 100644 --- a/packages/core/src/lib/cli/init.ts +++ b/packages/core/src/lib/cli/init.ts @@ -22,7 +22,7 @@ import { SheriffConfig } from '@softarc/sheriff-core'; */ export const config: SheriffConfig = { - tagging: {}, // apply tags to your modules + modules: {}, // apply tags to your modules depRules: { // root is a virtual module, which contains all files not being part // of any module, e.g. application shell, main.ts, etc. diff --git a/packages/core/src/lib/cli/tests/__snapshots__/init.spec.ts.snap b/packages/core/src/lib/cli/tests/__snapshots__/init.spec.ts.snap index db182c8..4e2164a 100644 --- a/packages/core/src/lib/cli/tests/__snapshots__/init.spec.ts.snap +++ b/packages/core/src/lib/cli/tests/__snapshots__/init.spec.ts.snap @@ -10,7 +10,7 @@ exports[`init > should create the file > sheriff.config 1`] = ` */ export const config: SheriffConfig = { - tagging: {}, // apply tags to your modules + modules: {}, // apply tags to your modules depRules: { // root is a virtual module, which contains all files not being part // of any module, e.g. application shell, main.ts, etc.