npm.io
3.0.1 • Published 2 weeks ago

@boundaries/elements

Licence
MIT
Version
3.0.1
Deps
0
Vulns
0
Weekly
0
Stars
931

Build status Coverage Status Quality Gate

Renovate Last commit Last release

NPM downloads License

Mutation testing badge

@boundaries/elements

Entity descriptors and matchers for @boundaries tools, such as @boundaries/eslint-plugin.

Table of Contents

Introduction

@boundaries/elements provides a system for describing and matching entities in JavaScript and TypeScript projects. An entity is any file or dependency under analysis, described by three complementary facets:

  • Element — the architectural role (e.g., component, service, helper)
  • File — per-file metadata and categorization
  • Module — the origin of a dependency (local, external, or core)

Together, these facets let you define architectural boundaries, validate dependencies between parts of your codebase, and enforce structural rules. This package powers tools like @boundaries/eslint-plugin and uses Micromatch patterns for flexible matching.

This package does not read or analyze your codebase directly. It provides the core logic for defining and matching entities and dependencies, which can be integrated into linters, build systems, or custom tooling.

Features

  • Entity-based model — describe files and dependencies as entities with element, file, and module facets
  • Flexible pattern matching using Micromatch syntax
  • File categorization independent from elements
  • Module origin detection — distinguish local, external, and core dependencies
  • Template variables for dynamic selector matching
  • Built-in caching for optimal performance
  • Capture path fragments for advanced matching scenarios

Installation

Install the package via npm:

npm install @boundaries/elements

Quick Start

Define how your project is structured, then describe and match entities:

import { Elements } from '@boundaries/elements';

const elements = new Elements();

// Define your project structure
const matcher = elements.getMatcher({
  elements: [
    { type: "component", pattern: "src/components/*", capture: ["name"] },
    { type: "service", pattern: "src/services/*.ts", capture: ["name"] },
  ],
  files: [
    { pattern: "**/*.spec.ts", category: "test" },
    { pattern: "**/*.controller.ts", category: "controller" },
  ],
});

// Describe an entity — returns element, file, and module information
const entity = matcher.describeEntity("src/components/Button/Button.spec.ts");

console.log(entity.element.types);    // ["component"]
console.log(entity.element.captured); // { name: "Button" }
console.log(entity.file.categories);  // ["test"]
console.log(entity.module.origin);    // "local"

Match entities against selectors:

// Does this file belong to a component?
matcher.isEntityMatch("src/components/Button/index.ts", {
  element: { type: "component" },
}); // true

// Is this file a test file inside a component?
matcher.isEntityMatch("src/components/Button/Button.spec.ts", {
  element: { type: "component" },
  file: { categories: "test" },
}); // true

// Validate dependencies between entities
matcher.isDependencyMatch(
  {
    from: "src/components/Button/index.ts",
    to: "src/services/Api.ts",
    source: "../../services/Api",
    kind: "value",
    nodeKind: "ImportDeclaration",
  },
  {
    from: { element: { type: "component" } },
    to: { element: { type: "service" } },
    dependency: { kind: "value" },
  }
); // true

Core Concepts

Entities

An entity is the central abstraction in @boundaries/elements. Every file or dependency in your project is an entity, described by three facets:

Entity
├── element  — architectural role (component, service, module, ...)
├── file     — per-file metadata (categories, captured values)
└── module   — origin information (local, external, core)

When you describe an entity, you get a composite object with all three facets:

const entity = matcher.describeEntity("src/modules/auth/auth.controller.ts");
// {
//   element: { types: ["module"], path: "src/modules/auth", captured: { name: "auth" }, ... },
//   file:    { categories: ["controller"], path: "src/.../auth.controller.ts", ... },
//   module:  { origin: "local", source: null, internalPath: null }
// }
Elements

An element represents an architectural unit — a meaningful group of files in your project (components, services, modules, helpers, etc.). Elements are defined via element descriptors that map file path patterns to types.

Key characteristics:

  • A file can belong to an element (or be unknown if no descriptor matches)
  • Elements form hierarchies: a component inside a module has that module as its parent
  • An element can have multiple types (e.g., a file matching both "component" and "widget" descriptors)
Files

A file provides per-file metadata and categorization, independent from which element the file belongs to. Files are categorized via file descriptors.

For example, within a "module" element, you can distinguish between controllers, services, models, and tests — all belonging to the same element but having different file categories.

A file can match multiple file descriptors, accumulating all matched categories.

Modules

A module describes the origin of a dependency:

  • local — files within your project
  • external — external dependencies (e.g., packages from node_modules)
  • core — Node.js built-in modules (e.g., fs, path, node:crypto)

For external and core modules, the module description also includes source (the base package name, e.g., "react") and internalPath (the subpath, e.g., "hooks/useState" for "react/hooks/useState").

Usage

Configuration Options

When creating an Elements instance, you can provide configuration options that will be used as defaults for all matchers:

const elements = new Elements({
  ignorePaths: ["**/dist/**", "**/build/**", "**/node_modules/**"],
  includePaths: ["src/**/*"],
  rootPath: "/absolute/path/to/project",
  flagAsExternal: {
    unresolvableAlias: true,
    inNodeModules: true,
    outsideRootPath: true,
    customSourcePatterns: ["@myorg/*"],
  },
});

Available options:

  • ignorePaths: Micromatch pattern(s) to exclude certain paths from matching (default: none)
  • includePaths: Micromatch pattern(s) to include only specific paths (default: all paths)
  • legacyTemplates: Whether to enable legacy template syntax support (default: true, but it will be false in future releases). This allows using ${variable} syntax in templates for backward compatibility.
  • cache: Whether to enable internal caching to improve performance (default: true)
  • rootPath: Absolute path to the project root. When configured, file paths should be provided as absolute paths to allow the package to determine which files are outside the project root (default: undefined)
  • flagAsExternal: Configuration for categorizing dependencies as external or local. Multiple conditions can be specified, and dependencies will be categorized as external if ANY condition is met (OR logic). See Flagging Dependencies as External for details.

Pattern Matching with rootPath: When rootPath is configured:

  • Matching patterns in element descriptors are relative to the rootPath. The package automatically converts absolute paths to relative paths internally for pattern matching.
  • With partialMatch: true (the default), patterns are evaluated right-to-left (from the end of the path), so the relativity to rootPath is typically less important. For example, a pattern like *.model.ts will match any file ending with .model.ts regardless of its location within rootPath.
  • With partialMatch: false, patterns must match the complete relative path from rootPath. Files outside rootPath maintain their absolute paths and require absolute patterns to match.
Creating a Matcher

Use the getMatcher method to create a matcher with a descriptors configuration:

const matcher = elements.getMatcher({
  elements: [
    { type: "component", pattern: "src/components/*" },
  ],
  files: [
    { category: "helper", pattern: "src/helpers/*.js" },
  ]
});

The getMatcher method accepts a DescriptorsConfig object with the following properties:

  • elements (ElementDescriptor[]): Optional array of element descriptors. If not provided, no element abstraction layer is created and only file descriptors are used.
  • files (FileDescriptor[]): Optional array of file descriptors. If not provided, only element descriptors are used to describe files.
  • elementsSingleType (boolean): When true, only the first matching descriptor's type is used at each path level. When false (default), an element can match multiple type descriptors, accumulating all matched types in the types array.

You can combine element and file descriptors:

const matcher = elements.getMatcher({
  elements: [
    { type: "module", pattern: "src/modules/*" },
  ],
  files: [
    { pattern: "**/*.controller.ts", category: "controller", capture: ["name"] },
    { pattern: "**/*.service.ts", category: "service", capture: ["name"] },
    { pattern: "**/*.model.ts", category: "model", capture: ["name"] },
  ],
});

Tip: Matchers with identical descriptors and options share the same cache instance for improved performance.

You can override the default options when creating a matcher:

const matcher = elements.getMatcher(
  { elements: [/* descriptors */] },
  {
    ignorePaths: ["**/*.test.ts"],
  }
);
Element Descriptors

Element descriptors define how files are identified and categorized into architectural elements (e.g., modules, components, services). Each descriptor is an object with the following properties:

  • pattern (string | string[]): Micromatch pattern(s) to match file paths
  • type (string): The element type to assign to matching files
  • category (string): (Deprecated) Additional categorization for the element. Use file descriptors with categories instead for more flexible per-file categorization.
  • mode ("file" | "folder" | "full"): (Deprecated) Matching mode (default: "folder")
    • "folder": Matches the first folder matching the pattern. The library will add **/* to the given pattern for matching files, because it needs to know exactly which folder has to be considered the element. So, you have to provide patterns matching the folder being the element, not the files directly.
    • "file": Matches files directly, but still matches progressively from the right. The provided pattern will not be modified.
    • "full": Matches the complete path.
  • partialMatch (boolean): Whether the pattern is allowed to match only a suffix of the file path (default: true). When true, the pattern is matched right-to-left, so it only needs to match the end of the path (components/* matches any path ending in components/<something>). This means you only need wildcards in the path segments you want to capture — intermediate segments are traversed automatically. If you also need to capture values from the beginning (left side) of the path, complement with basePattern and baseCapture. When false, the pattern is matched against the full file path from the project root, while keeping folder semantics (the element path is the matched folder prefix). It defaults to true for backward compatibility, but will most likely default to false in a future major version and eventually be removed, because requiring the full pattern is more intuitive and is already how file descriptors match. When false, the mode option has no effect.
  • basePattern (string): Additional pattern that must match from the project root. Only useful when partialMatch: true (the default): use it when you want to capture values from the beginning (left side) of the path, without having to define wildcards for every intermediate path segment between the root and the right-side pattern.
  • capture (string[]): Array of keys to capture path fragments
  • baseCapture (string[]): Array of keys to capture fragments from basePattern. Only useful together with basePattern, and therefore only when partialMatch: true. If the same key is defined in both capture and baseCapture, the value from capture takes precedence.
File Descriptors

File descriptors allow categorizing individual files independently from the element they belong to. This provides more granular control over file classification within elements. Each descriptor is an object with the following properties:

  • pattern (string | string[]): Micromatch pattern(s) to match file paths. Each pattern is matched against the full file path in a single pass (there is no right-to-left accumulation, and no option to change it). Use **/ to match files at any depth — for example **/*.spec.ts rather than *.spec.ts.
  • category (string): The category to assign to matching files (e.g., "controller", "service", "model").
  • capture (string[]): Array of keys to capture path fragments from the pattern.
const matcher = elements.getMatcher({
  elements: [
    { type: "module", pattern: "src/modules/*", capture: ["moduleName"] },
  ],
  files: [
    { pattern: "**/*.controller.ts", category: "controller", capture: ["name"] },
    { pattern: "**/*.service.ts", category: "service", capture: ["name"] },
    { pattern: "**/*.spec.ts", category: "test", capture: ["name"] },
  ],
});

A file can match multiple file descriptors, accumulating all matched categories in the categories array of the file description.

Describing Entities

The describeEntity method is the primary way to get information about a file or dependency. It returns a unified description combining all three facets:

const entity = matcher.describeEntity("src/modules/auth/auth.controller.ts");

The result is an EntityDescription object:

{
  element: ElementDescription,  // Architectural role
  file: FileDescription,        // File-level metadata
  module: ModuleDescription,    // Origin information
}

Element facet — architectural information about the element the file belongs to:

Property Type Description
types string[] | null Matched element types (e.g., ["module"]). Contains multiple types unless elementsSingleType is enabled. null for unknown/ignored.
path string | null Path representing the detected element boundary
captured object | null Captured values from descriptor patterns
parents ElementParent[] Parent element chain. Each parent has types, path, category, and captured.
fileInternalPath string | null Path of the file relative to the element it belongs to
isIgnored boolean Whether the file was excluded by ignorePaths / includePaths
isUnknown boolean Whether no descriptor matched

File facet — per-file categorization based on file descriptors:

Property Type Description
categories string[] | null Matched file categories (e.g., ["controller"]). null for unknown/ignored.
path string | null File path
captured object | null Captured values from file descriptor patterns
isIgnored boolean Whether the file was excluded
isUnknown boolean Whether no file descriptor matched

Module facet — origin information:

Property Type Description
origin "local" | "external" | "core" Where the dependency comes from
source string | null Base module name for external/core (e.g., "react"). null for local.
internalPath string | null Path within the module (e.g., "hooks/useState"). null for local.

Example:

const entity = matcher.describeEntity("src/modules/auth/auth.controller.ts");

console.log(entity.element.types);       // ["module"]
console.log(entity.element.path);        // "src/modules/auth"
console.log(entity.element.captured);    // { moduleName: "auth" }
console.log(entity.file.categories);     // ["controller"]
console.log(entity.file.captured);       // { name: "auth" }
console.log(entity.module.origin);       // "local"

For external dependencies, pass a source parameter:

const entity = matcher.describeEntity("react/hooks", "react/hooks");

console.log(entity.module.origin);       // "external"
console.log(entity.module.source);       // "react"
console.log(entity.module.internalPath); // "hooks"

For full details on all description properties and edge cases (ignored, unknown), see the Description Reference in the API Reference.

Describing Dependencies

The describeDependency method describes the relationship between two entities:

const dep = matcher.describeDependency({
  from: "src/components/Button/index.ts",
  to: "src/services/Api.ts",
  source: "../../services/Api",
  kind: "value",
  nodeKind: "ImportDeclaration",
  specifiers: ["ApiClient"],
});

The result contains:

{
  from: EntityDescription,      // Source entity (the file importing)
  to: EntityDescription,        // Target entity (the file being imported)
  dependency: {
    source: string,             // Raw import/export source string
    kind: "value" | "type" | "typeof",
    nodeKind: string | null,    // AST node type (e.g., "ImportDeclaration")
    specifiers: string[] | null,
    relationship: {
      from: DependencyRelationshipType | null,
      to: DependencyRelationshipType | null,
    }
  }
}

Both from and to are full entity descriptions, so you can inspect element, file, and module information for both sides:

console.log(dep.from.element.types);     // ["component"]
console.log(dep.to.element.types);       // ["service"]
console.log(dep.to.module.origin);       // "local"
console.log(dep.dependency.source);      // "../../services/Api"
console.log(dep.dependency.kind);        // "value"

The relationship field describes how the two entities relate to each other structurally. Possible values: "internal", "child", "parent", "sibling", "descendant", "ancestor", "uncle", "nephew", or null.

  • relationship.to — how to relates to from (e.g., "child" means the target is a child of the source)
  • relationship.from — the inverse perspective
Matching Entities

To check whether a file matches specific criteria, use isEntityMatch with an entity selector. An entity selector is an object with optional element, file, and module sub-selectors — all must match for the entity to match:

// Match local modules with "controller" file category
matcher.isEntityMatch("src/modules/auth/auth.controller.ts", {
  element: { type: "module" },
  file: { categories: "controller" },
  module: { origin: "local" },
}); // true

You only need to specify the sub-selectors you care about:

// Just check the element type
matcher.isEntityMatch("src/components/Button/index.ts", {
  element: { type: "component" },
}); // true

// Just check if it's an external dependency
matcher.isEntityMatch("react", {
  module: { origin: "external" },
}, { source: "react" }); // true

// Just check the file category
matcher.isEntityMatch("src/modules/auth/auth.spec.ts", {
  file: { categories: "test" },
}); // true
Entity Selector Properties

Element sub-selector — matches against element description properties:

  • type (string | string[]): Micromatch pattern(s) for the element type. Matches the first type in the types array.
  • types (string | string[] | ArrayQuery<string>): Micromatch pattern(s) for matching against all element types, or an array query object for richer matching (anyOf, allOf, noneOf, equalsTo, atIndex, hasLength).
  • path (string | string[]): Micromatch pattern(s) for the element path.
  • captured (object | object[]): Captured values selector. Object = AND logic (all keys must match). Array of objects = OR logic (any object can match).
  • parent (object | null): Selector for the first (closest) parent element. Set to null to match top-level elements with no parents. Supports type, types, path, and captured. Unchanged — still matches only parents[0].
  • parents (ArrayQuery<ParentElementSingleSelector>): Array query over the full ancestor chain. parents[0] is the closest parent; the last element is the outermost ancestor. See Array query selectors for operators.
  • fileInternalPath (string | string[]): Pattern(s) for the path of the file relative to the element.
  • isIgnored (boolean): Whether the element is ignored.
  • isUnknown (boolean): Whether no descriptor matched.

File sub-selector — matches against file description properties:

  • path (string | string[]): Micromatch pattern(s) for the file path.
  • categories (string | string[] | ArrayQuery<string>): Micromatch pattern(s) for file categories, or an array query object for richer matching (anyOf, allOf, noneOf, equalsTo, atIndex, hasLength).
  • captured (object | object[]): Captured values selector (same semantics as element).
  • isIgnored (boolean): Whether the file is ignored.
  • isUnknown (boolean): Whether no file descriptor matched.

Module sub-selector — matches against module description properties:

  • origin (string | string[]): Micromatch pattern(s) for the origin ("local", "external", or "core").
  • source (string | string[]): Micromatch pattern(s) for the base module name (e.g., "react", "fs").
  • internalPath (string | string[]): Micromatch pattern(s) for the path within the module.

All selector properties are optional and support null to match only entities with null in the corresponding description property.

For backward compatibility, entity selectors also accept flat element selectors (without the element/file/module nesting). Properties like origin, elementPath, and internalPath in flat selectors are automatically mapped to their new locations. See Legacy Selectors for details.

Array Query Selectors

The types (element), categories (file), and parents (element) selector properties accept an array query object in addition to the plain string / string-array form. An array query gives you fine-grained control over how the target array is matched.

All operators present in the same object are AND-combined. An absent operator imposes no constraint.

Operator Shape Matches when
anyOf (string | { expand: string })[] At least one array element matches at least one of the matchers. Empty operand never matches.
allOf (string | { expand: string })[] For every matcher, at least one array element matches it. Empty operand vacuously matches.
noneOf (string | { expand: string })[] No array element matches any of the matchers. Empty operand always matches.
equalsTo string[] Array length equals N and array[i] matches matcher[i] (ordered, exact-length).
atIndex { index: number; matches: TMatcher | TMatcher[] } Resolves the index (negative counts from end), then that element matches matches. When matches is an array, OR semantics apply — the element must satisfy at least one of the matchers. Out-of-range never matches.
hasLength number The array length is exactly this value.

Edge cases:

  • When the target array is null (unknown/ignored element or file), the entire query returns false.
  • An empty query object {} returns true for any non-null array (no constraints).
  • For equalsTo, order matters: ["a", "b"] does not match ["b", "a"].
  • Negative atIndex.index counts from the end: -1 = last element (outermost ancestor for parents).
  • All string matchers are micromatch patterns and are rendered as Handlebars templates before matching, exactly like all other selector values.

TMatcher per property:

  • types (element) / categories (file): string (a micromatch pattern) or { expand: string } (see below)
  • parents: ParentElementSingleSelector — an object supporting type, types (accepts StringArrayQuery), path, category, and captured
{ expand } items in anyOf / allOf / noneOf

For element.types, file.categories, and parent.types, each item in anyOf, allOf, and noneOf can be a { expand: "{{ path }}" } object instead of a plain string. The path is resolved against the template data at match time and the result is spread in place as additional string matchers.

This is useful when you need to build the operand list dynamically from the other side's property — for example, to match elements that share (or do not share) types with a given element.

Resolution:

  • Path resolves to a string array → each element becomes a separate matcher.
  • Path resolves to a scalar → a single matcher.
  • Path resolves to null/undefined, or the value is not a single {{ }} expression → no matchers (empty-operand rules apply: empty noneOf always passes; empty anyOf never matches).
// element.types: to element must not share any type with the from element
matcher.isEntityMatch(toFilePath, {
  element: {
    types: { noneOf: [{ expand: "{{ from.element.types }}" }] },
  },
}, { extraTemplateData: { from: { element: fromDescription } } });

// element.types: mixed static + dynamic — exclude "legacy" and all of from's types
{
  element: {
    types: { noneOf: ["legacy", { expand: "{{ from.element.types }}" }] },
  },
}

// file.categories: to file must share at least one category with the from file
{
  file: {
    categories: { anyOf: [{ expand: "{{ from.file.categories }}" }] },
  },
}

Examples:

// element.types: require at least one of these types
matcher.isEntityMatch(filePath, {
  element: { types: { anyOf: ["component", "widget"] } },
});

// element.types: forbid certain types
matcher.isEntityMatch(filePath, {
  element: { types: { noneOf: ["ignored", "legacy"] } },
});

// file.categories: require both categories to be present
matcher.isFileMatch(filePath, {
  categories: { allOf: ["react", "test"] },
});

// file.categories: file has exactly one category
matcher.isFileMatch(filePath, {
  categories: { hasLength: 1 },
});

// element.parents: top-level element (no parents)
matcher.isEntityMatch(filePath, {
  element: { parents: { hasLength: 0 } },
});

// element.parents: closest parent (index 0) is a module
matcher.isEntityMatch(filePath, {
  element: { parents: { atIndex: { index: 0, matches: { type: "module" } } } },
});

// element.parents: outermost ancestor (index -1) is an app
matcher.isEntityMatch(filePath, {
  element: { parents: { atIndex: { index: -1, matches: { type: "app" } } } },
});

// element.parents: closest parent is a module OR an app (OR via array)
matcher.isEntityMatch(filePath, {
  element: {
    parents: {
      atIndex: { index: 0, matches: [{ type: "module" }, { type: "app" }] },
    },
  },
});

// file.categories: first category is "components" or "ui"
matcher.isFileMatch(filePath, {
  categories: { atIndex: { index: 0, matches: ["components", "ui"] } },
});

// element.parents: ancestor chain has exactly two levels in order
matcher.isEntityMatch(filePath, {
  element: {
    parents: { equalsTo: [{ type: "module" }, { type: "app" }] },
  },
});
Matching Dependencies

To check whether a dependency matches specific criteria, use isDependencyMatch. It takes the dependency properties and a dependency selector:

matcher.isDependencyMatch(
  {
    from: "src/components/Button/index.ts",
    to: "src/services/Api.ts",
    source: "../../services/Api",
    kind: "value",
    nodeKind: "ImportDeclaration",
  },
  {
    from: { element: { type: "component" } },
    to: { element: { type: "service" } },
    dependency: { kind: "value" },
  }
); // true

Dependency properties (input):

  • from (string): Source file path
  • to (string): Target file path
  • source (string): Import/export source as written in code
  • kind (string): Import kind ("type", "value", "typeof")
  • nodeKind (string): AST node kind
  • specifiers (string[]): Imported/exported names

Dependency selector:

  • from (entity selector | entity selector[]): Entity selector(s) for the source entity
  • to (entity selector | entity selector[]): Entity selector(s) for the target entity
  • dependency (object | object[]): Selector(s) for dependency metadata. When an array is provided, the metadata matches if any selector matches (OR logic). Properties:
    • kind (string | string[]): Micromatch pattern(s) for the dependency kind
    • source (string | string[]): Pattern(s) to match the import/export source
    • specifiers (string | string[]): Pattern(s) for import/export specifiers
    • nodeKind (string | string[]): Pattern(s) for the AST node type
    • relationship (object): Relationship selectors from both perspectives:
      • from (string | string[]): Relationship from the source perspective
      • to (string | string[]): Relationship from the target perspective (e.g., "internal", "child", "parent", "sibling", "descendant", "ancestor", "uncle", "nephew")

Important: All properties within a single selector must match for it to be considered a match (AND logic). Use arrays of selectors for OR logic.

// Components can only import from services via type imports
matcher.isDependencyMatch(
  { from: "src/components/Button/index.ts", to: "src/services/Api.ts", source: "../../services/Api", kind: "type", nodeKind: "ImportDeclaration" },
  {
    from: { element: { type: "component" } },
    to: { element: { type: "service" } },
    dependency: { kind: "type" },
  }
); // true

// Match dependencies from external modules
matcher.isDependencyMatch(
  { from: "src/index.ts", to: "react", source: "react", kind: "value", nodeKind: "ImportDeclaration" },
  {
    to: { module: { origin: "external", source: "react*" } },
  }
); // true
Template Variables

Selectors support template variables using Handlebars syntax ({{ variableName }}). Templates are resolved at match time using the data available in the matching context.

Available Template Data

For entity matching:

  • Properties of the entity under match are available in the element object (types, captured, path, etc.)

For dependency matching:

  • from: Properties of the source entity (with element, file, module sub-objects)
  • to: Properties of the target entity (with element, file, module sub-objects)
  • dependency: Dependency metadata (kind, nodeKind, specifiers, source, relationship, etc.)
Template Examples
const matcher = elements.getMatcher({
  elements: [
    {
      type: "component",
      pattern: "src/modules/*/components/*",
      partialMatch: false,
      capture: ["module", "componentName"]
    }
  ],
});

// Match components from specific module using template
matcher.isElementMatch(
  "src/modules/auth/components/login-form/LoginForm.tsx",
  { 
    type: "component",
    captured: { module: "{{ element.captured.module }}" }
  },
);

// Using captured array for OR logic
matcher.isElementMatch(
  "src/modules/auth/components/login-form/LoginForm.tsx",
  { 
    type: "component",
    captured: [
      { module: "auth" },
      { module: "user", componentName: "user-profile" },
    ]
  },
);

// Using templates in dependency selectors
matcher.isDependencyMatch(
  {
    from: "src/components/Button.tsx",
    to: "src/services/Api.ts",
    source: "../services/Api",
    kind: "type",
    nodeKind: "ImportDeclaration",
    specifiers: ["calculateSum", "calculateAvg"],
  },
  {
    from: { element: { type: "{{ from.element.types.[0] }}" } },
    to: { element: { path: "{{ to.element.path }}" } },
    dependency: {
      specifiers: "{{ lookup dependency.specifiers 0 }}",
      kind: "{{ dependency.kind }}",
    },
  }
);
Using Extra Template Data

You can provide additional template data using the extraTemplateData option in MatcherOptions:

matcher.isElementMatch(
  "src/components/UserProfile.tsx",
  { type: "{{ componentType }}" },
  {
    extraTemplateData: { componentType: "component" }
  }
);
Flagging Dependencies as External

The flagAsExternal configuration allows you to control how dependencies are categorized as external or local. This is especially useful in monorepo setups where you may want to treat inter-package dependencies as external even though they're within the same repository.

Multiple conditions can be specified, and dependencies will be flagged as external if ANY condition is met (OR logic).

Available Options
  • unresolvableAlias (boolean, default: true): Non-relative imports whose path cannot be resolved are categorized as external

    const elements = new Elements({
      flagAsExternal: { unresolvableAlias: true },
    });
    const matcher = elements.getMatcher({ elements: [/* descriptors */] });
    
    // describeDependency({ from, to, source, kind }):
    // to: null, source: 'unresolved-module' -> to.module.origin: 'external'
    // to: '/project/src/Button.ts', source: './Button' -> to.module.origin: 'local'
  • inNodeModules (boolean, default: true): Non-relative paths that include node_modules in the resolved path are categorized as external

    const elements = new Elements({
      flagAsExternal: { inNodeModules: true },
    });
    const matcher = elements.getMatcher({ elements: [/* descriptors */] });
    
    // describeDependency({ from, to, source, kind }):
    // to: '/project/node_modules/react/index.js', source: 'react' -> to.module.origin: 'external'
    // to: '/project/src/utils.ts', source: './utils' -> to.module.origin: 'local'
  • outsideRootPath (boolean, default: false): Dependencies whose resolved path is outside the configured rootPath are categorized as external. This is particularly useful in monorepo setups.

    Important: This option requires rootPath to be configured. When using this option, all file paths must be absolute and include the rootPath prefix for files within the project.

    const elements = new Elements({
      rootPath: '/monorepo/packages/app',
      flagAsExternal: { outsideRootPath: true },
    });
    const matcher = elements.getMatcher({ elements: [/* descriptors */] });
    
    // describeDependency({ from, to, source, kind }):
    // to: '/monorepo/packages/shared/index.ts', source: '@myorg/shared' -> to.module.origin: 'external'
    // to: '/monorepo/packages/app/src/utils/helper.ts', source: './utils/helper' -> to.module.origin: 'local'
  • customSourcePatterns (string[], default: []): Array of micromatch patterns that, when matching the import/export source string, categorize the dependency as external

    const elements = new Elements({
      flagAsExternal: { customSourcePatterns: ['@myorg/*', '~/**'] },
    });
    const matcher = elements.getMatcher({ elements: [/* descriptors */] });
    
    // describeDependency({ from, to, source, kind }):
    // source: '@myorg/shared' -> to.module.origin: 'external' (matches '@myorg/*')
    // source: '~/utils/helper' -> to.module.origin: 'external' (matches '~/**')
    // source: '@other/package' -> to.module.origin: 'local' (no match, unless inNodeModules is true or other conditions met)
Path Requirements with rootPath

When rootPath is configured, the package needs absolute paths to correctly determine which files are outside the project root, but matching patterns must remain relative to rootPath, especially when partialMatch: false (because the default partialMatch: true matches progressively from the right, so it is typically less affected by relativity).

const elements = new Elements({
  rootPath: '/project/packages/app',
  flagAsExternal: {
    outsideRootPath: true,
  },
});

// Matching patterns are relative to rootPath
const matcher = elements.getMatcher({
  elements: [
    { type: 'component', pattern: 'src/**/*.ts', partialMatch: false }, // Relative to /project/packages/app
  ],
});

// Using absolute file paths with relative patterns
const dep = matcher.describeDependency({
  from: '/project/packages/app/src/index.ts',      // absolute file path
  to: '/project/packages/shared/index.ts',         // absolute file path
  source: '@myorg/shared',
  kind: 'value',
});
// Result: dep.to.module.origin === 'external' (outside rootPath)
// Note: Pattern 'src/**/*.ts' matches because the package converts
// absolute paths to relative internally for pattern matching

Key Points:

  • File paths in API calls (from, to, filePath) must be absolute when using rootPath
  • Matching patterns in element descriptors stay relative to rootPath
  • The package handles the conversion internally
  • When not using rootPath, the package continues to work with relative paths as before, maintaining backward compatibility.

API Reference

Class: Elements
Constructor

Creates a new Elements instance with optional default configuration.

new Elements(options?: ConfigurationOptions);
getMatcher

Creates a new matcher instance.

  • Parameters:
    • descriptors: DescriptorsConfig — A descriptors configuration object with optional elements, files, and elementsSingleType properties.
    • options: ConfigOptions — Optional. Configuration options for the matcher. Overrides the defaults set in the Elements instance.
  • Returns: Matcher
const matcher = elements.getMatcher({
  elements: [
    { type: "component", pattern: "src/components/*" },
  ],
  files: [
    { pattern: "**/*.spec.ts", category: "test" },
  ],
});
clearCache

Clears all cached matcher instances and shared caches.

elements.clearCache();
serializeCache

Serializes all cached matcher instances and shared caches to a plain object.

const cache = elements.serializeCache();
setCacheFromSerialized

Sets the cached matcher instances and shared caches from a serialized object.

const cache = elements.serializeCache();
elements.setCacheFromSerialized(cache);
Matcher Instance Methods
Entity Methods
describeEntity

Returns a detailed entity description combining element, file, and module information.

  • Parameters:
    • filePath: string — Optional. The path of the file to describe.
    • source: string — Optional. The dependency source string for determining module origin.
  • Returns: EntityDescription
const entity = matcher.describeEntity("src/components/Button.tsx");
isEntityMatch

Checks if a given path matches an entity selector.

  • Parameters:
    • filePath: string — The file path to check.
    • selector: EntitySelector — Entity selector or array of entity selectors.
    • options: EntityMatcherOptions — Optional. Includes optional source for module origin resolution.
  • Returns: boolean
matcher.isEntityMatch("src/modules/auth/auth.controller.ts", {
  element: { type: "module" },
  file: { categories: "controller" },
});
getEntitySelectorMatching

Returns the first matching entity selector result, or null.

  • Parameters:
    • filePath: string — The file path to check.
    • selector: EntitySelector — Entity selector to match against.
    • options: EntityMatcherOptions — Optional.
  • Returns: EntitySingleSelectorMatchResult | null — Contains matched element, file, and module sub-selectors.
const result = matcher.getEntitySelectorMatching("src/modules/auth/auth.controller.ts", {
  element: { type: "module" },
  file: { categories: "controller" },
});
getEntitySelectorMatchingDescription

Matches an entity description (from describeEntity) against entity selectors.

  • Parameters:
    • description: EntityDescription — The entity description to match.
    • selector: EntitySelector — The entity selector to match against.
    • options: MatcherOptions — Optional.
  • Returns: EntitySingleSelectorMatchResult | null
const entity = matcher.describeEntity("src/modules/auth/auth.controller.ts");
const result = matcher.getEntitySelectorMatchingDescription(entity, [{
  element: { type: "module" },
  file: { categories: "controller" },
}]);
Dependency Methods
describeDependency

Returns a detailed dependency description.

  • Parameters:
    • dependency: Object with from, to, source, kind, nodeKind, and specifiers properties.
  • Returns: DependencyDescription
const dep = matcher.describeDependency({
  from: "src/components/Button.tsx",
  to: "src/services/Api.ts",
  source: "../services/Api",
  kind: "type",
  nodeKind: "ImportDeclaration",
});
isDependencyMatch

Checks if dependency properties match a dependency selector.

  • Parameters:
    • dependency: Object with from, to, source, kind, nodeKind, and specifiers properties.
    • selector: DependencySelector — Dependency selector or array of selectors.
  • Returns: boolean
matcher.isDependencyMatch(
  { from: "src/components/Button.tsx", to: "src/services/Api.ts", source: "../services/Api", kind: "type", nodeKind: "ImportDeclaration" },
  {
    from: { element: { type: "component" } },
    to: { element: { type: "service" } },
    dependency: { nodeKind: "Import*" },
  }
);
getDependencySelectorMatching

Returns the dependency selector matching result (from, to, dependency, isMatch).

When arrays of selectors are provided in the from, to, or dependency properties, the method returns the first matching selector from each group.

  • Parameters:
    • dependency: Dependency properties.
    • selector: DependencySelector.
  • Returns: DependencySelectorMatchResult
const result = matcher.getDependencySelectorMatching(
  { from: "src/components/Button.tsx", to: "src/services/Api.ts", source: "../services/Api", kind: "type" },
  { to: { element: { type: "service" } }, dependency: { kind: "type" } }
);
getDependencySelectorMatchingDescription

Matches a dependency description (from describeDependency) against dependency selectors.

When arrays of selectors are provided in the from, to, or dependency properties, the method returns the first matching selector from each group.

  • Parameters:
    • description: DependencyDescription — Dependency description to match.
    • selector: DependencySelector — Array of dependency selectors.
  • Returns: DependencySelectorMatchResult
const dep = matcher.describeDependency({ from: "src/components/Button.tsx", to: "src/services/Api.ts", source: "../services/Api", kind: "type", nodeKind: "ImportDeclaration" });
const result = matcher.getDependencySelectorMatchingDescription(dep, [{
  to: { element: { type: "service" } },
  dependency: { kind: "type" },
}]);
Element Methods

These methods operate on the element facet only. They are useful when you only need element-level information without file or module context.

describeElement

Returns a detailed description of the element a file belongs to.

  • Parameters:
    • filePath: string — The path of the file to describe.
  • Returns: ElementDescription
const element = matcher.describeElement("src/components/Button.tsx");
console.log(element.types);    // ["component"]
console.log(element.captured); // { name: "Button" }
console.log(element.parents);  // []
isElementMatch

Checks if a given path matches an element selector.

  • Parameters:
    • filePath: string — The file path to check.
    • selector: ElementSelector — Element selector or array of element selectors.
  • Returns: boolean
matcher.isElementMatch("src/components/Button.tsx", [{ type: "component" }]);
getElementSelectorMatching

Returns the first matching element selector, or null.

  • Parameters:
    • filePath: string — The file path to check.
    • selector: ElementSelector — Element selector to match against.
  • Returns: ElementSingleSelector | null
const result = matcher.getElementSelectorMatching("src/components/Button.tsx", [{ type: "component" }]);
getElementSelectorMatchingDescription

Matches an element description (from describeElement) against element selectors.

  • Parameters:
    • description: ElementDescription — The element description to match.
    • selector: ElementSelector — Array of element selectors.
  • Returns: ElementSingleSelector | null
const element = matcher.describeElement("src/components/Button.tsx");
const result = matcher.getElementSelectorMatchingDescription(element, [{ type: "component" }]);
Module Methods

These methods operate on the module facet only. They are useful for checking the origin of a dependency independently.

describeModule

Returns the module origin description for a file or dependency.

  • Parameters:
    • filePath: string — Optional. The path of the file.
    • source: string — Optional. The dependency source string.
  • Returns: ModuleDescription
const mod = matcher.describeModule("react/hooks", "react/hooks");
console.log(mod.origin);       // "external"
console.log(mod.source);       // "react"
console.log(mod.internalPath); // "hooks"
isModuleMatch

Checks if a given path matches a module selector.

  • Parameters:
    • filePath: string — The file path to check.
    • selector: ModuleSelector — Module selector to match against.
    • options: EntityMatcherOptions — Optional. Includes optional source.
  • Returns: boolean
matcher.isModuleMatch("react", { origin: "external" }, { source: "react" });
matcher.isModuleMatch("node:fs", { origin: "core" }, { source: "node:fs" });
getModuleSelectorMatching

Returns the first matching module selector, or null.

  • Parameters:
    • filePath: string — The file path to check.
    • selector: ModuleSelector — Module selector to match against.
    • options: EntityMatcherOptions — Optional.
  • Returns: ModuleSingleSelector | null
const result = matcher.getModuleSelectorMatching("react", { origin: "external" }, { source: "react" });
getModuleSelectorMatchingDescription

Matches a module description (from describeModule) against module selectors.

  • Parameters:
    • description: ModuleDescription — The module description to match.
    • selector: ModuleSelector — Module selector to match against.
    • options: EntityMatcherOptions — Optional.
  • Returns: ModuleSingleSelector | null
const mod = matcher.describeModule("react", "react");
const result = matcher.getModuleSelectorMatchingDescription(mod, { origin: "external" });
File Methods

These methods operate on the file facet only. They are useful when you only need file-level information without element or module context.

describeFile

Returns a detailed description of a file given its path.

  • Parameters:
    • filePath: string — The path of the file to describe.
  • Returns: FileDescription
const file = matcher.describeFile("src/modules/auth/auth.controller.ts");
console.log(file.categories); // ["controller"]
console.log(file.captured);   // { name: "auth" }
console.log(file.isUnknown);  // false
isFileMatch

Checks if a given path matches a file selector.

  • Parameters:
    • filePath: string — The file path to check.
    • selector: FileSelector — File selector or array of file selectors.
    • options: MatcherOptions — Optional.
  • Returns: boolean
matcher.isFileMatch("src/modules/auth/auth.spec.ts", { categories: "test" }); // true
getFileSelectorMatching

Returns the first matching file selector, or null.

  • Parameters:
    • filePath: string — The file path to check.
    • selector: FileSelector — File selector to match against.
    • options: MatcherOptions — Optional.
  • Returns: FileSingleSelector | null
const result = matcher.getFileSelectorMatching("src/modules/auth/auth.spec.ts", [{ categories: "test" }]);
getFileSelectorMatchingDescription

Matches a file description (from describeFile) against file selectors.

  • Parameters:
    • description: FileDescription — The file description to match.
    • selector: FileSelector — File selector to match against.
    • options: MatcherOptions — Optional.
  • Returns: FileSingleSelector | null
const file = matcher.describeFile("src/modules/auth/auth.spec.ts");
const result = matcher.getFileSelectorMatchingDescription(file, [{ categories: "test" }]);
Cache Methods
clearCache

Clears the matcher's internal cache.

matcher.clearCache();

This only clears the internal cache for this matcher instance. Shared cache for micromatch results, regex and captures is not affected. You can clear all caches using Elements.clearCache().

serializeCache

Serializes the matcher's cache.

const cache = matcher.serializeCache();
setCacheFromSerialized

Restores the matcher's cache from a serialized object.

const cache = matcher.serializeCache();
matcher.clearCache();
matcher.setCacheFromSerialized(cache);
Selector Reference

This section provides a complete reference of all selector properties.

Element Selector
Property Type Description
type string | string[] Micromatch pattern(s) for element type. Matches the first type in types.
types string | string[] Micromatch pattern(s) for matching against all element types.
path string | string[] Micromatch pattern(s) for the element path.
captured object | object[] Captured values selector. Object = AND, array = OR.
parent object | null Selector for the first parent. null matches elements with no parents. Supports type, types, path, captured, category (deprecated).
fileInternalPath string | string[] Pattern(s) for the file path relative to the element.
category string | string[] (Deprecated) Use file selector categories instead.
isIgnored boolean Whether the element is ignored.
isUnknown boolean Whether the element is unknown.
File Selector
Property Type Description
path string | string[] Micromatch pattern(s) for the file path.
categories string | string[] Micromatch pattern(s) for file categories.
captured object | object[] Captured values selector.
isIgnored boolean Whether the file is ignored.
isUnknown boolean Whether the file is unknown.
Module Selector
Property Type Description
origin string | string[] Micromatch pattern(s) for origin ("local", "external", "core").
source string | string[] Micromatch pattern(s) for the base module name.
internalPath string | string[] Micromatch pattern(s) for the path within the module.
Entity Selector
Property Type Description
element ElementSelector Element sub-selector.
file FileSelector File sub-selector.
module ModuleSelector Module sub-selector.
Dependency Selector
Property Type Description
from EntitySelector | EntitySelector[] Entity selector(s) for the source entity.
to EntitySelector | EntitySelector[] Entity selector(s) for the target entity.
dependency DependencyInfoSelector | DependencyInfoSelector[] Dependency metadata selector(s). OR logic when array.

Dependency info selector properties:

Property Type Description
kind string | string[] Micromatch pattern(s) for dependency kind.
source string | string[] Pattern(s) for the import/export source.
specifiers string | string[] Pattern(s) for import/export specifiers.
nodeKind string | string[] Pattern(s) for the AST node type.
relationship object from and to relationship selectors (see Matching Dependencies).
Description Reference

This section provides a complete reference of all description types returned by describe* methods.

Element Description

Returned by describeElement(filePath).

Property Type Description
types string[] | null Matched element types. Multiple unless elementsSingleType is enabled.
path string | null Detected element boundary path.
captured object | null Captured values from descriptor patterns.
parents ElementParent[] Parent element chain (each has types, path, category, captured).
fileInternalPath string | null File path relative to the element.
category string | null (Deprecated) Element category. Use file categories.
filePath string | null (Deprecated) Full file path. Use file description path.
isIgnored boolean Whether excluded by ignorePaths/includePaths.
isUnknown boolean Whether no descriptor matched.
File Description

Returned by describeFile(filePath) or describeEntity(filePath).file.

Property Type Description
path string | null File path.
categories string[] | null Matched file categories.
captured object | null Captured values from file descriptor patterns.
isIgnored boolean Whether excluded.
isUnknown boolean Whether no file descriptor matched.
Module Description

Returned by describeModule(filePath?, source?) or describeEntity(filePath, source?).module.

Property Type Description
origin "local" | "external" | "core" Module origin.
source string | null Base module name for external/core. null for local.
internalPath string | null Subpath within the module. null for local.
Entity Description

Returned by describeEntity(filePath, source?).

Property Type Description
element ElementDescription Element facet.
file FileDescription File facet.
module ModuleDescription Module facet.
Dependency Description

Returned by describeDependency(options).

Property Type Description
from EntityDescription Source entity.
to EntityDescription Target entity.
dependency DependencyInfoDescription Dependency metadata (source, kind, nodeKind, specifiers, relationship).

Legacy Selectors

Legacy selectors are defined using a different syntax and are provided for backward compatibility. However, this syntax is deprecated and will be removed in a future release. It is recommended to migrate to the new selector syntax.

Selectors can be defined as either a string or an array of strings representing the element type(s):

// Legacy selector using a string
const isElementMatch = matcher.isElementMatch("src/components/Button.tsx", "component");
// Legacy selector using an array of strings
const isElementMatch = matcher.isElementMatch("src/components/Button.tsx", ["component", "service"]);

They can also be defined as an array where the first element is the type and the second element is an object containing captured values:

// Legacy selector with captured values
const isElementMatch = matcher.isElementMatch(
  "src/modules/auth/LoginForm.component.tsx",
  ["component", { foo: "auth" }]
);

Warning: Avoid mixing legacy selectors with the new selector syntax in the same project, as this can lead to ambiguity. In particular, if you define a top-level array selector with two elements and the second one is an object containing a type or category key, it will be interpreted as legacy options rather than two separate selectors.

Migration from v2

This section describes how to migrate from v2 to the latest version. The main changes are:

1. Wrap descriptor array in DescriptorsConfig

The getMatcher method now accepts a DescriptorsConfig object instead of a flat array:

- const matcher = elements.getMatcher([
-   { type: "component", pattern: "src/components/*" },
- ]);
+ const matcher = elements.getMatcher({
+   elements: [
+     { type: "component", pattern: "src/components/*" },
+   ],
+ });
2. Update element description access: type -> types

Element descriptions now use types (array) instead of type (string):

  const desc = matcher.describeElement("src/components/Button.tsx");
- console.log(desc.type);        // "component"
+ console.log(desc.types);       // ["component"]
+ console.log(desc.types[0]);    // "component"
3. Update dependency description access: flat -> entity structure

Dependency descriptions now use EntityDescription objects for from and to:

  const dep = matcher.describeDependency({ from, to, source, kind });
- console.log(dep.from.type);     // "component"
- console.log(dep.to.origin);     // "local"
+ console.log(dep.from.element.types[0]);  // "component"
+ console.log(dep.to.module.origin);       // "local"
4. Move origin/internalPath to module selectors

Properties that described module origin have moved from element selectors to module selectors (within entity selectors):

  // Element selector (old)
- { type: "component", origin: "local" }
  // Entity selector (new)
+ { element: { type: "component" }, module: { origin: "local" } }

For backward compatibility, the old flat selector format is still accepted and automatically converted. However, it is deprecated and will be removed in a future version.

5. Replace dependency.module with to.module.source

The module property in dependency metadata selectors has been removed. Use to.module.source instead:

  // Old: matching by module name in dependency metadata
- { to: { type: "service" }, dependency: { module: "react" } }
  // New: matching by module source in entity selector
+ { to: { module: { source: "react" } } }
6. Regenerate serialized cache

The serialized cache format has changed. If you persist cache between runs, you need to regenerate it. Old caches are not compatible with the new format.

Contributing

Contributors are welcome. Please read the contributing guidelines and code of conduct.

License

MIT, see LICENSE for details.

Keywords