eslint-plugin-boundaries

repository·master·Indexed 21 days ago

https://github.com/javierbrea/eslint-plugin-boundaries

A tool for enforcing architectural constraints in JavaScript and TypeScript projects. It allows developers to define layers, file categories, and module origins to prevent illegal dependencies and maintain clean architecture. The project includes @boundaries/elements for describing and matching architectural boundaries using entities, elements, files, and modules.

Tokens
79.4K
Snippets
219
Records
337
Agent score
71%

What's inside eslint-plugin-boundaries

  1. Overview of ESLint Plugin Boundaries rules

    master

    ESLint Plugin Boundaries provides a suite of rules to enforce architectural boundaries. The plugin is centered around the canonical boundaries/dependencies rule, which restricts dependencies between defined elements. Other active rules complement this by catching unrecognized files, unknown dependencies, or dependencies on ignored files.

    To use these rules effectively, you should understand the following core concepts:

    • Classification: How to define your architecture using elements and files.
    • Selectors: How to select specific dependencies or elements.
    • Policies: How to configure rule options and custom error messages.
  2. Understand the dependency runtime description structure

    master

    When the plugin analyzes a dependency, it generates a description object containing three main parts: from, to, and dependency.

    • from: The element, file, and module the dependency comes from.
    • to: The element, file, and module the dependency points to.
    • dependency: Metadata about the import itself, including its kind (value, type, or typeof), the source, and the specifiers.
    // Example runtime description for a dependency in src/controllers/controller-a/index.js
    {
      from: {
        element: {
          types: ["controller"],
          captured: { elementName: "controller-a" },
        },
        file: { categories: null },
        module: { origin: "local" },
      },
      to: {
        element: {
          types: ["view"],
          captured: { elementName: "view-a" },
        },
        file: { categories: null },
        module: { origin: "local" },
      },
      dependency: {
        kind: "value",
        source: "@views/view-a",
        specifiers: ["ViewA"],
      },
    }
  3. How element descriptors interact with no-unknown-files

    master

    The no-unknown-files rule flags files that the plugin does not recognize. A file is only reported if it meets both of these conditions:

    1. It belongs to no known element (matches no boundaries/elements descriptor).
    2. It matches no file descriptor (boundaries/files).

    Crucially: Defining an element descriptor for a pattern makes all files matching that pattern "known". Even if those files don't match a specific file descriptor, they will not be flagged by no-unknown-files once they are covered by an element descriptor.

  4. Understand the Entity model (Element, File, and Module layers)

    master

    An entity is the fundamental unit the plugin analyzes. Every file is described by three independent, orthogonal layers. Because they are orthogonal, a single file can simultaneously belong to an element, have specific file categories, and resolve to a specific module origin.

    • Element layer: Represents the architectural unit (e.g., a folder like components/Button/). You define these using element descriptors.
    • File layer: Represents the kind of file it is (e.g., a test, a style, or a story), regardless of its architectural location. You define these using file descriptors.
    • Module layer: Describes what an import resolves to (a local file, an external package, or a Node.js built-in). This is derived automatically from the import and is not manually configured (except via specific settings like boundaries/flag-as-external).

    Use these layers to answer questions like: 'What architectural piece is this?' (Element), 'What kind of file is this?' (File), or 'Is this an external dependency?' (Module).

    // Example of how a single file might be represented across layers:
    {
      element: { type: "component", path: "components/atoms/atom-a" },
      file:    { categories: ["tsx"], path: "components/atoms/atom-a/AtomA.tsx" },
      module:  { origin: "local", source: null, internalPath: null }
    }
  5. Understand Policies Evaluation Order

    master

    Policies are evaluated sequentially. Each matching policy can override the results of previous policies. To manage complex rules, place more specific policies after more general policies so they can act as exceptions to the general rules.

    Example of overriding behavior:

    1. A general policy allows components to import helpers.
    2. A more specific policy disallows components of family atoms from importing data helpers.
    3. An even more specific policy allows atoms to import a specific sort.js helper, overriding the previous disallow rule.
    {
      default: "disallow",
      policies: [
        // 1. General allow
        {
          from: { element: { type: "component" } },
          allow: { to: { element: { type: "helper" } } }
        },
        // 2. Specific disallow (overrides 1)
        {
          from: { element: { type: "component", captured: { family: "atoms" } } },
          disallow: { to: { element: { type: "helper", captured: { family: "data" } } } },
        },
        // 3. Specific exception (overrides 2)
        {
          from: { element: { type: "component", captured: { family: "atoms" } } },
          allow: {
            to: { element: { type: "helper", captured: { family: "data" }, fileInternalPath: "sort.js" } }
          }
        }
      ]
    }
  6. How JS Boundaries works

    master

    JS Boundaries enforces architectural boundaries by analyzing the relationships between abstract elements in your project. It works by inspecting import and export statements, require calls, and dynamic import() expressions (though other AST nodes like jest.mock() can be configured).

    The workflow follows three main steps:

    1. Classification: You use descriptors in your configuration to classify files and folders into layers (elements, files, or modules).
    2. Runtime Description: For every dependency found, the plugin builds a description that captures the from side, the to side, and the dependency metadata (kind, relationship, etc.).
    3. Policy Enforcement: You define policies using selectors to allow or disallow specific combinations of these descriptions. If a dependency violates a policy, ESLint reports an error.
  7. Understand Hierarchical Elements

    master

    The plugin supports parent-child relationships between elements. After finding a match, the plugin continues searching at higher path levels for parent elements. This allows you to write policies that restrict access based on hierarchy (e.g., allowing a component to import from its parent module but not from a sibling module).

    Example Configuration:

    "boundaries/elements": [
      { type: "component", pattern: "components/*", capture: ["componentName"] },
      { type: "module", pattern: "modules/*", capture: ["moduleName"] }
    ]

    For a file at src/modules/auth/components/login-form/index.js:

    1. The plugin matches the component element (login-form).
    2. It then matches the module element (auth) as its parent.

    Parents are accessible via the element.parents array, with the nearest parent listed first.

  8. Understand Dependency Metadata

    master

    In eslint-plugin-boundaries, dependency metadata describes the nature of the relationship between two entities (from and to). While from and to identify the files involved, the dependency metadata explains what the dependency is (e.g., is it a type-only import, what is the structural relationship, or what specifiers are being imported).

    Unlike elements or files, dependency metadata is not configurable. The plugin automatically computes it from the AST node, the literal source string, and the relative position of the elements in the hierarchy.

  9. How policies match dependencies

    master

    A policy matches a dependency by combining selectors from three main sources: the importing file (from), the imported file (to), and the dependency itself (dependency). These selectors use properties from the runtime description of each entity.

    Combining Selectors

    Properties defined at the policy level and inside allow/disallow effects are combined to build a unique dependency selector. For a policy to match, all combined conditions must be met.

    Merging OR conditions (arrays)

    When using arrays to define OR conditions, the properties are merged together for each item in the array. For example, a policy can target files that are either a specific element type OR belong to a specific file category.

    Merging properties at policy and effect levels

    If the same property (e.g., from) is defined at both the policy level and inside allow/disallow, they are merged. The properties inside allow/disallow take precedence.

    Merging nested properties

    Nested properties like parent, captured, or relationship are merged when defined at both levels.

    Warning: Extending nested properties is not supported when using arrays (OR conditions) in the selectors. In such cases, the allow/disallow selector will override the policy-level selector instead of merging with it.

    // Example of combining from, to, and dependency
    {
      from: { element: { type: "component" } },
      allow: {
        to: { element: { type: "helper" } },
        dependency: { kind: "value" }
      }
    }
  10. What are Element Descriptors?

    master

    In eslint-plugin-boundaries, an element is the architectural piece a file belongs to, typically represented by a folder (e.g., components/Button/).

    Element Descriptors are the configuration objects used to recognize these architectural pieces by matching file paths against specific patterns. They form one of the three classification layers (alongside Files and Modules) used to enforce architectural boundaries.

  11. How architectural boundaries work

    master

    ESLint Plugin Boundaries works by classifying every file and dependency in your project across three dimensions: its architectural element, its file category, and its origin.

    When an import statement is encountered, the plugin checks the classification of the source file (from) against the classification of the imported module (to) based on the policies you have defined. If the interaction is not explicitly allowed (or is explicitly disallowed), ESLint triggers an error, providing real-time feedback on architectural violations.

  12. Use `pattern` and `partialMatch` for path matching

    master

    The pattern property defines how file paths are matched to elements.

    Default Behavior (partialMatch: true)

    By default, the plugin matches patterns progressively from the right side of the path. This acts like an implicit **/ prefix. For example, components/* will match src/components/Button and packages/ui/src/components/Button.

    Anchored Matching (partialMatch: false)

    Set partialMatch: false when a pattern must be anchored at the project root to distinguish between different directory trees with the same name.

    Example:

    {
      type: "component",
      pattern: "src/ui/components/*",
      partialMatch: false,
      capture: ["componentName"]
    }