@boundaries/elements
Entity descriptors and matchers for
@boundariestools, such as@boundaries/eslint-plugin.
Table of Contents
- Introduction
- Features
- Installation
- Quick Start
- Core Concepts
- Usage
- API Reference
- Legacy Selectors
- Migration from v2
- Contributing
- License
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 projectexternal— external dependencies (e.g., packages fromnode_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 befalsein 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: WhenrootPathis 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 torootPathis typically less important. For example, a pattern like*.model.tswill match any file ending with.model.tsregardless of its location withinrootPath.- With
partialMatch: false, patterns must match the complete relative path fromrootPath. Files outsiderootPathmaintain 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): Whentrue, only the first matching descriptor's type is used at each path level. Whenfalse(default), an element can match multiple type descriptors, accumulating all matched types in thetypesarray.
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 pathstype(string): The element type to assign to matching filescategory(string): (Deprecated) Additional categorization for the element. Use file descriptors withcategoriesinstead 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). Whentrue, the pattern is matched right-to-left, so it only needs to match the end of the path (components/*matches any path ending incomponents/<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 withbasePatternandbaseCapture. Whenfalse, the pattern is matched against the full file path from the project root, while keeping folder semantics (the elementpathis the matched folder prefix). It defaults totruefor backward compatibility, but will most likely default tofalsein a future major version and eventually be removed, because requiring the full pattern is more intuitive and is already how file descriptors match. Whenfalse, themodeoption has no effect.basePattern(string): Additional pattern that must match from the project root. Only useful whenpartialMatch: 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-sidepattern.capture(string[]): Array of keys to capture path fragmentsbaseCapture(string[]): Array of keys to capture fragments frombasePattern. Only useful together withbasePattern, and therefore only whenpartialMatch: true. If the same key is defined in bothcaptureandbaseCapture, the value fromcapturetakes 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.tsrather 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
categoriesarray 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— howtorelates tofrom(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 thetypesarray.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 tonullto match top-level elements with no parents. Supportstype,types,path, andcaptured. Unchanged — still matches onlyparents[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
nullto match only entities withnullin the corresponding description property.
For backward compatibility, entity selectors also accept flat element selectors (without the
element/file/modulenesting). Properties likeorigin,elementPath, andinternalPathin 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 returnsfalse. - An empty query object
{}returnstruefor any non-null array (no constraints). - For
equalsTo, order matters:["a", "b"]does not match["b", "a"]. - Negative
atIndex.indexcounts from the end:-1= last element (outermost ancestor forparents). - 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 supportingtype,types(acceptsStringArrayQuery),path,category, andcaptured
{ 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: emptynoneOfalways passes; emptyanyOfnever 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 pathto(string): Target file pathsource(string): Import/export source as written in codekind(string): Import kind ("type","value","typeof")nodeKind(string): AST node kindspecifiers(string[]): Imported/exported names
Dependency selector:
from(entity selector | entity selector[]): Entity selector(s) for the source entityto(entity selector | entity selector[]): Entity selector(s) for the target entitydependency(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 kindsource(string | string[]): Pattern(s) to match the import/export sourcespecifiers(string | string[]): Pattern(s) for import/export specifiersnodeKind(string | string[]): Pattern(s) for the AST node typerelationship(object): Relationship selectors from both perspectives:from(string | string[]): Relationship from the source perspectiveto(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
elementobject (types, captured, path, etc.)
For dependency matching:
from: Properties of the source entity (withelement,file,modulesub-objects)to: Properties of the target entity (withelement,file,modulesub-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 externalconst 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 includenode_modulesin the resolved path are categorized as externalconst 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 configuredrootPathare categorized as external. This is particularly useful in monorepo setups.Important: This option requires
rootPathto be configured. When using this option, all file paths must be absolute and include therootPathprefix 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 externalconst 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 usingrootPath- 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 optionalelements,files, andelementsSingleTypeproperties.options:ConfigOptions— Optional. Configuration options for the matcher. Overrides the defaults set in theElementsinstance.
- 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 optionalsourcefor 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 matchedelement,file, andmodulesub-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 withfrom,to,source,kind,nodeKind, andspecifiersproperties.
- 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 withfrom,to,source,kind,nodeKind, andspecifiersproperties.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, ordependencyproperties, 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, ordependencyproperties, 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 optionalsource.
- 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
typeorcategorykey, 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.