@boundaries/elements 1.2.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,8 +19,13 @@
19
19
  - [Usage](#usage)
20
20
  - [Configuration Options](#configuration-options)
21
21
  - [Creating a matcher](#creating-a-matcher)
22
- - [Element Descriptors](#element-descriptors)
22
+ - [Element Descriptors](#element-descriptors)
23
+ - [Descriptions API](#descriptions-api)
24
+ - [Element Description](#element-description)
25
+ - [Dependency Description](#dependency-description)
23
26
  - [Selectors](#selectors)
27
+ - [Element Selectors](#element-selectors)
28
+ - [Dependency Selectors](#dependency-selectors)
24
29
  - [Template Variables](#template-variables)
25
30
  - [Using Matchers](#using-matchers)
26
31
  - [Element Matching](#element-matching)
@@ -91,12 +96,12 @@ const matcher = elements.getMatcher([
91
96
  ]);
92
97
 
93
98
  // Match an element
94
- const isComponent = matcher.isMatch("src/components/Button.tsx", {
99
+ const isComponent = matcher.isElementMatch("src/components/Button.tsx", {
95
100
  type: "component"
96
101
  }); // true
97
102
 
98
103
  // Match a dependency
99
- const isValidDependency = matcher.isMatch(
104
+ const isValidDependency = matcher.isDependencyMatch(
100
105
  {
101
106
  from: "src/components/Button.tsx",
102
107
  to: "src/services/Api.ts",
@@ -107,6 +112,7 @@ const isValidDependency = matcher.isMatch(
107
112
  {
108
113
  from: { category: "react" },
109
114
  to: { type: "service" },
115
+ dependency: { nodeKind: "ImportDeclaration" },
110
116
  }
111
117
  ); // true
112
118
  ```
@@ -194,17 +200,98 @@ Element descriptors define how files are identified and categorized. Each descri
194
200
  - **`capture`** (`string[]`): Array of keys to capture path fragments
195
201
  - **`baseCapture`** (`string[]`): Array of keys to capture fragments from `basePattern`. If the same key is defined in both `capture` and `baseCapture`, the value from `capture` takes precedence.
196
202
 
203
+ ### Descriptions API
204
+
205
+ The matcher can also return normalized runtime descriptions. These descriptions are the canonical API used by `@boundaries/eslint-plugin` and are useful for debugging, reporting, and custom tooling.
206
+
207
+ > [!IMPORTANT]
208
+ > This section describes the **output API** of `describeElement` / `describeDependency`, which is different from the **input API** used by `isDependencyMatch`.
209
+
210
+ #### Element Description
211
+
212
+ `matcher.describeElement(filePath)` returns an object with normalized element metadata.
213
+
214
+ Common fields:
215
+
216
+ - `path`: Absolute or relative file path used in the matcher call
217
+ - `type`: Matched element type, or `null` if unknown
218
+ - `category`: Matched element category, or `null`
219
+ - `captured`: Captured values map from descriptor patterns, or `null`
220
+ - `elementPath`: Path representing the detected element boundary, or `null`
221
+ - `internalPath`: Path of the file relative to `elementPath`, or `null`
222
+ - `origin`: One of `"local" | "external" | "core"`
223
+ - `isIgnored`: Whether the file was excluded by `ignorePaths` / `includePaths`
224
+ - `isUnknown`: Whether no descriptor matched
225
+
226
+ Additional fields for local known elements:
227
+
228
+ - `parents`: Parent element chain, or `null`
229
+
230
+ #### Dependency Description
231
+
232
+ `matcher.describeDependency(options)` returns:
233
+
234
+ ```ts
235
+ {
236
+ from: ElementDescription,
237
+ to: ElementDescription,
238
+ dependency: {
239
+ source: string,
240
+ module: string | null,
241
+ kind: "value" | "type" | "typeof",
242
+ nodeKind: string | null,
243
+ specifiers: string[] | null,
244
+ relationship: {
245
+ from: "internal" | "child" | "descendant" | "sibling" | "parent" | "uncle" | "nephew" | "ancestor" | null,
246
+ to: "internal" | "child" | "descendant" | "sibling" | "parent" | "uncle" | "nephew" | "ancestor" | null,
247
+ }
248
+ }
249
+ }
250
+ ```
251
+
252
+ Notes:
253
+
254
+ - `dependency.source` is the raw import/export source string from code.
255
+ - `dependency.module` is the normalized module base for external/core dependencies.
256
+ - `dependency.relationship.to` describes how `to` relates to `from`.
257
+ - `dependency.relationship.from` is the inverse perspective.
258
+ - For unknown/ignored scenarios, some values can be `null`.
259
+
260
+ Example:
261
+
262
+ ```ts
263
+ const description = matcher.describeDependency({
264
+ from: "src/components/Button.tsx",
265
+ to: "src/services/Api.ts",
266
+ source: "../services/Api",
267
+ kind: "value",
268
+ nodeKind: "ImportDeclaration",
269
+ specifiers: ["ApiClient"],
270
+ });
271
+
272
+ console.log(description.dependency.source); // "../services/Api"
273
+ console.log(description.dependency.kind); // "value"
274
+ console.log(description.dependency.relationship); // { from: ..., to: ... }
275
+ ```
276
+
197
277
  ### Selectors
198
278
 
199
279
  Selectors are used to match elements and dependencies against specific criteria. They are objects where each property represents a matching condition.
200
280
 
201
- #### Element Properties
281
+ #### Element Selectors
202
282
 
203
- All element selectors support the following properties:
283
+ When matching elements, you can use element selectors that specify conditions on the element's properties.
284
+
285
+ Element selectors support the following properties:
204
286
 
205
287
  - **`type`** (`string | string[]`): Micromatch pattern(s) for the element type/s
206
288
  - **`category`** (`string | string[]`): Micromatch pattern(s) for the element category/categories
207
- - **`captured`** (`object`): Object with keys matching captured values. Each key can be a string or an array of strings representing micromatch patterns.
289
+ - **`captured`** (`object | object[]`): Captured values selector. When provided as an object, all keys must match (AND logic). When provided as an array of objects, the element matches if any of the objects matches all keys (OR logic). Each key in the objects can be a string or an array of strings representing micromatch patterns.
290
+ - **`parent`** (`object` | `null`): Selector for the first parent in the element description (`parents[0]`). Supported properties are:
291
+ - **`type`** (`string | string[]`): Micromatch pattern(s) for parent type
292
+ - **`category`** (`string | string[]`): Micromatch pattern(s) for parent category
293
+ - **`elementPath`** (`string | string[]`): Micromatch pattern(s) for parent element path
294
+ - **`captured`** (`object | object[]`): Parent captured values selector. Uses the same semantics as `captured` in the root selector (object = AND, array = OR)
208
295
  - **`origin`** (`"local" | "external" | "core"`): Element origin
209
296
  - `local`: Files within the project
210
297
  - `external`: External dependencies (e.g., `node_modules`)
@@ -215,24 +302,33 @@ All element selectors support the following properties:
215
302
  - **`isIgnored`** (`boolean`): Whether the element is ignored
216
303
  - **`isUnknown`** (`boolean`): Whether the element type is unknown (i.e., doesn't match any descriptor)
217
304
 
218
- #### Dependency Properties
219
-
220
- When matching dependencies, the `to` selector can additionally use:
221
-
222
- - **`kind`** (`string | string[]`): Micromatch pattern(s) for the dependency kind
223
- - **`relationship`** (`string | string[]`): Element relationship. Micromatch pattern(s) for the relationship between source and target elements:
224
- - `internal`: Both files belong to the same element
225
- - `child`: Target is a child of source
226
- - `parent`: Target is a parent of source
227
- - `sibling`: Elements share the same parent
228
- - `uncle`: Target is a sibling of a source ancestor
229
- - `nephew`: Target is a child of a source sibling
230
- - `descendant`: Target is a descendant of source
231
- - `ancestor`: Target is an ancestor of source
232
- - **`specifiers`** (`string | string[]`): Pattern(s) for import/export specifiers (e.g., named imports)
233
- - **`nodeKind`** (`string | string[]`): Pattern(s) for the AST node type causing the dependency (e.g., `"ImportDeclaration"`)
234
- - **`source`** (`string | string[]`): Pattern(s) to match the source of the dependency. (e.g., the import path).
235
- - **`baseSource`** (`string | string[]`): Pattern(s) for the base module name for external imports.
305
+
306
+ > [!NOTE]
307
+ > All properties in the selector are optional. You can also use `null` values in selector to match only elements with `null` values in the corresponding properties. In the case of `parent`, setting it to `null` will match elements that have no parents (i.e., top-level elements). If `parent` is an object, it will only match elements that have at least one parent, and the first parent (`parents[0]`) matches the specified conditions.
308
+
309
+ #### Dependency Selectors
310
+
311
+ When matching dependencies, you can use dependency selectors that specify conditions on the source and target elements, as well as the dependency metadata.
312
+
313
+ - **`from`** (`element selector | element selector[]`): [Selector(s)](#element-selectors) for the source element
314
+ - **`to`** (`element selector | element selector[]`): [Selector(s)](#element-selectors) for the target element
315
+ - **`dependency`** (`object | object[]`): Selector(s) for dependency metadata. When an array is provided, the dependency metadata matches if any selector in the array matches (OR logic). Supported selector properties:
316
+ - **`kind`** (`string | string[]`): Micromatch pattern(s) for the dependency kind
317
+ - **`relationship`** (`object`): Relationship selectors from both perspectives:
318
+ - **`from`** (`string | string[]`): Relationship from the perspective of `from`
319
+ - **`to`** (`string | string[]`): Relationship from the perspective of `to`
320
+ - `internal`: Both files belong to the same element
321
+ - `child`: Target is a child of source
322
+ - `parent`: Target is a parent of source
323
+ - `sibling`: Elements share the same parent
324
+ - `uncle`: Target is a sibling of a source ancestor
325
+ - `nephew`: Target is a child of a source sibling
326
+ - `descendant`: Target is a descendant of source
327
+ - `ancestor`: Target is an ancestor of source
328
+ - **`specifiers`** (`string | string[]`): Pattern(s) for import/export specifiers (e.g., named imports)
329
+ - **`nodeKind`** (`string | string[]`): Pattern(s) for the AST node type causing the dependency (e.g., `"ImportDeclaration"`)
330
+ - **`source`** (`string | string[]`): Pattern(s) to match the source of the dependency (e.g., the import path)
331
+ - **`module`** (`string | string[]`): Pattern(s) for the base module name for external or core dependencies.
236
332
 
237
333
  > **⚠️ Important:** All properties in a selector must match for the selector to be considered a match (AND logic). Use multiple selectors for OR logic.
238
334
 
@@ -243,7 +339,7 @@ When matching dependencies, the `to` selector can additionally use:
243
339
  Selectors support template variables using [Handlebars syntax](https://handlebarsjs.com/) (`{{ variableName }}`). Templates are resolved at match time using:
244
340
 
245
341
  - **Element properties** (`type`, `category`, `captured`, etc.)
246
- - **Dependency properties** (`from`, `to`)
342
+ - **Dependency properties** (`from`, `to`, `dependency`)
247
343
 
248
344
  #### Available Template Data
249
345
 
@@ -254,7 +350,8 @@ When matching, the following data is automatically available:
254
350
 
255
351
  **For dependency matching:**
256
352
  - `from`: Properties of the dependency source element
257
- - `to`: Properties of the dependency target element, and properties of the dependency itself (source, kind, nodeKind, specifiers, etc.)
353
+ - `to`: Properties of the dependency target element
354
+ - `dependency`: Dependency metadata (`kind`, `nodeKind`, `specifiers`, `source`, `module`, `relationship`, etc.)
258
355
 
259
356
  #### Template Examples
260
357
 
@@ -270,7 +367,7 @@ const matcher = elements.getMatcher([
270
367
  ]);
271
368
 
272
369
  // Match components from specific module using template
273
- const isAuthComponent = matcher.isMatch(
370
+ const isAuthComponent = matcher.isElementMatch(
274
371
  "src/modules/auth/LoginForm.component.tsx",
275
372
  {
276
373
  type: "component",
@@ -278,8 +375,20 @@ const isAuthComponent = matcher.isMatch(
278
375
  },
279
376
  );
280
377
 
378
+ // Using captured array for OR logic
379
+ const isAuthOrUserComponent = matcher.isElementMatch(
380
+ "src/modules/auth/LoginForm.component.tsx",
381
+ {
382
+ type: "component",
383
+ captured: [
384
+ { module: "auth" }, // Matches if module is "auth"
385
+ { module: "user", fileName: "UserProfile" } // OR if module is "user" and fileName is "UserProfile"
386
+ ]
387
+ },
388
+ );
389
+
281
390
  // Using templates in dependency selectors
282
- const isDependencyMatch = matcher.isMatch(
391
+ const isDependencyMatch = matcher.isDependencyMatch(
283
392
  {
284
393
  from: "src/components/Button.tsx",
285
394
  to: "src/services/Api.ts",
@@ -290,7 +399,11 @@ const isDependencyMatch = matcher.isMatch(
290
399
  },
291
400
  {
292
401
  from: { type: "{{ from.type }}", captured: { fileName: "{{ from.captured.fileName }}" } },
293
- to: { path: "{{ to.path }}", specifiers: "{{ lookup to.specifiers 0 }}", kind: "{{ to.kind }}" },
402
+ to: { path: "{{ to.path }}" },
403
+ dependency: {
404
+ specifiers: "{{ lookup dependency.specifiers 0 }}",
405
+ kind: "{{ dependency.kind }}",
406
+ },
294
407
  }
295
408
  );
296
409
  ```
@@ -301,7 +414,7 @@ You can provide additional template data using the `extraTemplateData` option in
301
414
 
302
415
  ```ts
303
416
  // Using templates in selectors
304
- const isMatch = matcher.isMatch(
417
+ const isMatch = matcher.isElementMatch(
305
418
  "src/components/UserProfile.tsx",
306
419
  { type: "{{ componentType }}" },
307
420
  {
@@ -316,18 +429,18 @@ You can use element selectors with a created matcher to check if a given path co
316
429
 
317
430
  #### Element Matching
318
431
 
319
- To match an element, use the `isMatch` method of the matcher, providing the file path and an element selector.
432
+ To match an element, use the `isElementMatch` method of the matcher, providing the file path and an element selector.
320
433
 
321
434
  ```ts
322
- const isElementMatch = matcher.isMatch("src/components/Button.tsx", { type: "component" });
435
+ const isElementMatch = matcher.isElementMatch("src/components/Button.tsx", { type: "component" });
323
436
  ```
324
437
 
325
438
  > [!TIP]
326
- > You can also provide an array of selectors to the `isMatch` method. In this case, the method will return `true` if the element matches any of the provided selectors (OR logic).
439
+ > You can also provide an array of selectors to the `isElementMatch` method. In this case, the method will return `true` if the element matches any of the provided selectors (OR logic).
327
440
 
328
441
  #### Dependency Matching
329
442
 
330
- To match a dependency, use the `isMatch` method of the matcher, providing the properties of the dependency and a dependency selector.
443
+ To match a dependency, use the `isDependencyMatch` method of the matcher, providing the properties of the dependency and a dependency selector.
331
444
 
332
445
  **Dependency object properties:**
333
446
 
@@ -341,10 +454,11 @@ To match a dependency, use the `isMatch` method of the matcher, providing the pr
341
454
  **Dependency selector:**
342
455
 
343
456
  - **`from`**: Element selector(s) for the source file
344
- - **`to`**: Dependency selector(s) for the target file
457
+ - **`to`**: Element selector(s) for the target file
458
+ - **`dependency`**: Dependency metadata selector(s)
345
459
 
346
460
  ```ts
347
- const isDependencyMatch = matcher.isMatch(
461
+ const isDependencyMatch = matcher.isDependencyMatch(
348
462
  { // Dependency properties
349
463
  from: "src/components/Button.tsx",
350
464
  to: "src/services/Api.ts",
@@ -354,13 +468,17 @@ const isDependencyMatch = matcher.isMatch(
354
468
  },
355
469
  {
356
470
  from: { category: "react" }, // Dependency source selector/s
357
- to: { type: "service", nodeKind: "Import*" }, // Dependency target selector/s
471
+ to: { type: "service" }, // Dependency target selector/s
472
+ dependency: [
473
+ { nodeKind: "Import*" },
474
+ { source: "@services/*" },
475
+ ], // Dependency metadata selector/s (OR logic)
358
476
  }
359
477
  );
360
478
  ```
361
479
 
362
480
  > [!TIP]
363
- > You can also provide an array of selectors both to the `from` and `to` properties of the dependency selector. In this case, the method will return `true` if the dependency matches any combination of the provided selectors (OR logic).
481
+ > You can also provide an array of selectors to `from`, `to` and `dependency`. The matcher will return `true` when all provided selector groups match.
364
482
 
365
483
  ### Flagging Dependencies as External
366
484
 
@@ -534,14 +652,20 @@ elements.setCacheFromSerialized(cache);
534
652
 
535
653
  ### Matcher Instance Methods
536
654
 
537
- #### `isMatch`
655
+ #### `isElementMatch`
538
656
 
539
- Checks if a given path matches the specified element or dependency selector.
657
+ Checks if a given path matches an element selector.
540
658
 
541
659
  ```ts
542
- const isElementMatch = matcher.isMatch("src/components/Button.tsx", [{ type: "component" }]);
660
+ const isElementMatch = matcher.isElementMatch("src/components/Button.tsx", [{ type: "component" }]);
661
+ ```
662
+
663
+ #### `isDependencyMatch`
543
664
 
544
- const isDependencyMatch = matcher.isMatch(
665
+ Checks if dependency properties match a dependency selector.
666
+
667
+ ```ts
668
+ const isDependencyMatch = matcher.isDependencyMatch(
545
669
  {
546
670
  from: "src/components/Button.tsx",
547
671
  to: "src/services/Api.ts",
@@ -551,30 +675,41 @@ const isDependencyMatch = matcher.isMatch(
551
675
  },
552
676
  {
553
677
  from: [{ category: "react" }],
554
- to: { type: "service", nodeKind: "Import*" },
678
+ to: { type: "service" },
679
+ dependency: { nodeKind: "Import*" },
555
680
  }
556
681
  );
557
682
  ```
558
683
 
559
- - __Parameters__:
560
- - `path`:
561
- - `string` The path to check when using an [element selector](#selectors).
562
- - `DependencyProperties` The [properties of the dependency](#dependency-matching) to check when using a [dependency selector](#selectors).
563
- - `selector`: `ElementSelector | DependencySelector` The [selector](#selectors) to match against. It can be either an element selector (for path matching) or a dependency selector (for dependency matching).
564
- - If `path` is a string, `selector` should be an [`ElementSelector`](#selectors) or an array of `ElementSelector`.
565
- - If `path` are dependency properties, `selector` should be a [`DependencySelector`](#selectors) or an array of `DependencySelector`.
566
- - `options`: `MatcherOptions` Optional. Additional options for matching:
567
- - `extraTemplateData`: `object` Optional. Extra data to pass to selector templates. When using [template variables](#template-variables) in selectors, this data will be available for rendering.
568
-
569
- #### `getSelectorMatching`
684
+ #### `getElementSelectorMatching`
570
685
 
571
- Returns the first matching selector or `null`.
686
+ Returns the first matching element selector or `null`.
572
687
 
573
688
  ```ts
574
- const matchingSelector = matcher.getSelectorMatching("src/components/Button.tsx", [{ type: "component" }]);
689
+ const matchingSelector = matcher.getElementSelectorMatching("src/components/Button.tsx", [{ type: "component" }]);
575
690
  ```
576
691
 
577
- Parameters are the same as `isMatch`, but instead of returning a boolean, it returns the first matching selector or `null` if none match.
692
+ #### `getDependencySelectorMatching`
693
+
694
+ Returns the dependency selector matching result (`from`, `to`, `dependency`, `isMatch`).
695
+
696
+ > [!NOTE]
697
+ > This method provides detailed information about which part of the selector matched or didn't match. When arrays of selectors are provided in the `from`, `to` or `dependency` properties, the method will return the first selector that matches on each side, so the returned `from`, `to` and `dependency` will be the matching selector from each group.
698
+
699
+ ```ts
700
+ const matchingSelector = matcher.getDependencySelectorMatching(
701
+ {
702
+ from: "src/components/Button.tsx",
703
+ to: "src/services/Api.ts",
704
+ source: "../services/Api",
705
+ kind: "type",
706
+ },
707
+ {
708
+ to: { type: "service" },
709
+ dependency: { kind: "type" },
710
+ }
711
+ );
712
+ ```
578
713
 
579
714
  #### `describeElement`
580
715
 
@@ -586,6 +721,7 @@ const elementDescription = matcher.describeElement("src/components/Button.tsx");
586
721
 
587
722
  - __Parameters__:
588
723
  - `path`: `string` The path of the element to describe.
724
+ - __Returns__: [Element Description](#element-description).
589
725
 
590
726
  #### `describeDependency`
591
727
 
@@ -603,14 +739,37 @@ const dependencyDescription = matcher.describeDependency({
603
739
 
604
740
  - __Parameters__:
605
741
  - `dependency`: The [properties of the dependency to describe](#dependency-matching).
742
+ - __Returns__: [Dependency Description](#dependency-description).
743
+
744
+ #### `getElementSelectorMatchingDescription`
606
745
 
607
- #### `getSelectorMatchingDescription`
746
+ Matches an element description against element selectors. As first argument, it should receive the result of `describeElement`.
608
747
 
609
- Matches a description against selectors. As first argument, it should receive the result of `describeElement` or `describeDependency`.
748
+ As second argument, it should receive an array of element selectors. The method will return the first selector that matches the description or `null` if no selector matches.
610
749
 
611
750
  ```ts
612
751
  const elementDescription = matcher.describeElement("src/components/Button.tsx");
613
- const matchingSelector = matcher.getSelectorMatchingDescription(elementDescription, [{ type: "component" }]);
752
+ const matchingSelector = matcher.getElementSelectorMatchingDescription(elementDescription, [{ type: "component" }]);
753
+ ```
754
+
755
+ #### `getDependencySelectorMatchingDescription`
756
+
757
+ Matches a dependency description against dependency selectors. As first argument, it should receive the result of `describeDependency`.
758
+
759
+ As second argument, it should receive an array of dependency selectors. The method will return the first selector that matches the description or `null` if no selector matches.
760
+
761
+ > [!NOTE]
762
+ > This method provides detailed information about which part of the selector matched or didn't match. When arrays of selectors are provided in the `from`, `to` or `dependency` properties in a dependency selector, the method will return the first selector that matches on each side, so the returned `from`, `to` and `dependency` will be the matching selector from each group.
763
+
764
+ ```ts
765
+ const dependencyDescription = matcher.describeDependency({
766
+ from: "src/components/Button.tsx",
767
+ to: "src/services/Api.ts",
768
+ source: "../services/Api",
769
+ kind: "type",
770
+ nodeKind: "ImportDeclaration",
771
+ });
772
+ const matchingSelector = matcher.getDependencySelectorMatchingDescription(dependencyDescription, [{ to: { type: "service" }, dependency: { kind: "type" } }]);
614
773
  ```
615
774
 
616
775
  #### `clearCache`
@@ -655,18 +814,18 @@ Selectors can be defined as either a string or an array of strings representing
655
814
 
656
815
  ```ts
657
816
  // Legacy selector using a string
658
- const isElementMatch = matcher.isMatch("src/components/Button.tsx", "component");
817
+ const isElementMatch = matcher.isElementMatch("src/components/Button.tsx", "component");
659
818
  // Legacy selector using an array of strings
660
- const isElementMatch = matcher.isMatch("src/components/Button.tsx", ["component", "service"]);
819
+ const isElementMatch = matcher.isElementMatch("src/components/Button.tsx", ["component", "service"]);
661
820
  ```
662
821
 
663
822
  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:
664
823
 
665
824
  ```ts
666
825
  // Legacy selector with captured values
667
- const isElementMatch = matcher.isMatch(
826
+ const isElementMatch = matcher.isElementMatch(
668
827
  "src/modules/auth/LoginForm.component.tsx",
669
- ["component", { module: "auth" }]
828
+ ["component", { foo: "auth" }]
670
829
  );
671
830
  ```
672
831