Sheriff: Modularity for TypeScript Projects

repository·main·Indexed 43 days ago

https://github.com/softarc-consulting/sheriff

A tool for enforcing module boundaries and dependency rules in TypeScript projects to maintain architectural integrity. It prevents unauthorized imports, enforces public APIs via index.ts files, and provides a CLI for initializing configurations, verifying rules, and exporting dependency graphs. Integration is available via @softarc/sheriff-core and @softarc/eslint-plugin-sheriff.

Tokens
21.9K
Snippets
90
Records
115
Agent score
54%

What's inside Sheriff

  1. Overview of Sheriff features

    main

    Sheriff is a tool for enforcing module boundaries and dependency rules in TypeScript projects. It helps maintain a clean codebase through several mechanisms:

    • Module Boundaries: Enforces public APIs by requiring access through index.ts files.
    • Dependency Rules: Controls how different parts of your application can access one another.
    • Module Tagging: Supports both automatic and manual tagging of modules to apply specific dependency rules.
    • CLI Tooling: Provides a command-line interface for initializing configurations, verifying rules, listing modules, and exporting dependency graphs.
  2. Enforce module boundaries and dependency rules with Sheriff

    main

    Sheriff is a tool designed to enforce module boundaries and dependency rules in TypeScript projects. It helps maintain a clean architecture by ensuring files within a module are encapsulated and that dependencies between modules follow a predefined structure.

    Core Concepts

    • Module Boundaries: Prevents files from outside a module from accessing its internal implementation. Modules are identified in two ways:

      • By a sheriff.config.ts configuration file.
      • By the presence of a barrel file (e.g., index.ts).
    • Dependency Rules: Defines which modules are allowed to depend on other modules. These rules are configured within the sheriff.config.ts file.

    Requirements

    Sheriff has zero external dependencies. Its only peer dependency is TypeScript.

  3. Enforce module boundaries in Next.js with Sheriff

    main

    Sheriff enforces module boundaries in Next.js applications through two primary mechanisms:

    1. Editor Intellisense: Provides immediate feedback in your IDE if you attempt to import from a restricted module.
    2. Linting: Running pnpm lint (or your package manager's equivalent) will catch any violations of the defined module boundaries.

    In this specific implementation, the boundaries are structured as follows:

    • app directory: Contains Next.js App Router pages and layouts. It focuses on page composition by importing from shell and shared.
    • Feature modules (e.g., shell): Encapsulate business logic. They can import from shared/ui.
    • shared/ui: A shared layer containing UI components (like IconLink) that can be imported by both app and feature modules.
  4. Configure the SheriffConfig structure

    main

    Sheriff configuration is defined in a sheriff.config.ts file using the SheriffConfig type from @softarc/sheriff-core. The configuration object is exported as a constant named config.

    import { SheriffConfig } from '@softarc/sheriff-core';
    
    export const config: SheriffConfig = {
      // Your configuration options here
    };
  5. Use wildcards and functions in `depRules`

    main

    In your SheriffConfig, the depRules object can accept functions instead of just static strings. This allows for dynamic dependency validation. Additionally, tag names can include wildcards (*) to match multiple tags.

    When a tag name contains a wildcard, the rule function receives a from and to parameter representing the tags being compared. This is useful for enforcing that a module can only depend on itself or other modules with specific properties.

    import { SheriffConfig } from '@softarc/sheriff-core';
    
    export const sheriffConfig: SheriffConfig = {
      modules: {
        'src/app/<domain>/<type>': ['domain:<domain>', 'type:<type>'],
      },
      depRules: {
        // Using a wildcard and a function to ensure a domain only depends on itself
        'domain:*': ({ from, to }) => from === to,
        'type:feature': 'type:data',
        root: ['type:feature'],
      },
    };
  6. How dependency rules and tags work

    main

    Sheriff manages module dependencies using tags. Instead of defining rules for every individual module, you group modules into tags (e.g., domain:customer and type:feature).

    The Access Logic: When a module attempts to access another, Sheriff retrieves the tags for both. Access is granted if any tag from the source module is permitted to access any tag of the target module.

    Best Practices for Tagging: It is recommended to use two dimensions for tags:

    1. Domain/Scope tags: Define the business area (e.g., domain:customer, domain:holiday).
    2. Type tags: Define the technical role (e.g., type:feature for smart components, type:ui for dumb components, type:data for logic).

    Example: A smart component in the customer domain would have both domain:customer and type:feature tags.

  7. Configure Entry Files and Entry Points

    main

    Sheriff requires a starting point to traverse your project's imports. You can define this in sheriff.config.ts using either entryFile or entryPoints.

    Entry File

    A single file (e.g., src/main.ts) that serves as the starting point. If set in the config, you can omit it from CLI commands.

    Entry Points

    Useful for monorepos or workspaces with multiple applications. You can define multiple named entry files. When using the CLI with entry points, provide them as a comma-separated list.

    Note: You cannot specify both entryFile and entryPoints in the configuration; doing so will cause an error. CLI arguments always take precedence over configuration.

    export const config: SheriffConfig = {
      // Option 1: Single entry file
      entryFile: './src/main.ts',
    
      // Option 2: Multiple named entry points
      entryPoints: {
        'app-web': './apps/web/src/main.ts',
        'app-mobile': './apps/mobile/src/main.ts',
        'app-admin': './apps/admin/src/main.ts',
      },
    };
  8. Understand the `root` tag and module access

    main

    The root tag is automatically assigned by Sheriff to files that are not part of any defined module (e.g., main.ts, app.config.ts).

    Key behaviors:

    • The root tag assignment cannot be changed.
    • With enableBarrelLess: false (the default), no module can access the root module.
    • You can use the excludeRoot property to disable this restriction, though enabling barrel-less mode is generally preferred.
  9. How Sheriff processes files and modules

    main

    Sheriff operates through a three-step pipeline to analyze project structure and enforce boundaries:

    1. File Graph: The traverseFilesystem function takes an entry file and returns a graph of all files required to run that entry file. At this stage, the graph is of type UnassignedFileInfo (a graph without module assignments).
    2. Modules: The createModules function takes the FileInfo and detects existing modules and their dependencies. The resulting type is ModuleInfo.
    3. Merging: The FileGraph and Modules are merged to produce the final FileInfo type, which contains complete information about both files and modules.

    The entry point for this entire process is the init function.