Skip to content

Migrate from eslint-plugin-boundaries

Markdown for agents

This guide moves an eslint-plugin-boundaries setup to archangel. The two tools have the same idea: give names to areas of the code, then say which area may import which. In archangel, the names are parts, and the main rule is a table.

eslint.config.js
import boundaries from 'eslint-plugin-boundaries';
export default [
{
plugins: { boundaries },
settings: {
'boundaries/elements': [
{ type: 'app', pattern: 'src/app/**', mode: 'full' },
{ type: 'feature', pattern: 'src/features/*', capture: ['featureName'] },
{ type: 'shared', pattern: 'src/shared/**', mode: 'full' },
],
},
rules: {
'boundaries/element-types': ['error', {
default: 'disallow',
rules: [
{ from: 'app', allow: ['feature', 'shared'] },
{ from: 'feature', allow: ['shared', ['feature', { featureName: '${from.featureName}' }]] },
{ from: 'shared', allow: ['shared'] },
],
}],
'boundaries/entry-point': ['error', {
default: 'disallow',
rules: [{ target: ['feature'], allow: 'index.ts' }],
}],
},
},
];
eslint-plugin-boundaries archangel
An element in boundaries/elements A part in parts.
pattern with mode: 'full' A path pattern that ends in /**, from the config folder.
pattern: 'src/features/*' with capture: ['featureName'] 'src/features/{featureName}/**'. The capture goes in the pattern.
boundaries/element-types with default: 'disallow' A table. Each from is a row, and its allow list is the row’s list.
allow of the same element with ${from.featureName} Nothing. A row may import its own part in the same instance by default.
allow of another instance of the same element Add the part to its own row, for example feature: ['shared', 'feature'].
boundaries/entry-point entry('feature', 'index.ts').
boundaries/no-private entry, or isolate for instances that may not import each other at all.
boundaries/external A cell that names an externals set, or a rule with mayNotImport: { packages: [...] }.
boundaries/no-unknown-files A rule with mustNotExist on the files that are in no part.
archangel.config.ts
import { defineConfig } from '@enhance-eng/archangel';
export default defineConfig({
parts: {
app: 'src/app/**',
feature: 'src/features/{featureName}/**',
shared: 'src/shared/**',
},
rules: ({ entry, mayImport }) => [
mayImport('element-types', {
app: ['feature', 'shared'],
feature: ['shared'],
shared: [],
}),
entry('feature', 'index.ts', {
name: 'entry-point',
examples: {
rejected: ['src/app/main.ts imports src/features/login/form.ts'],
},
}),
],
});
  • Each element type is a part. The capture {featureName} makes each folder in src/features one instance.
  • The table element-types keeps the name of the old rule. Its rows are named element-types.app, element-types.feature and element-types.shared.
  • The entry shorthand takes the name entry-point, with a rejected example.
  1. Prove the rules:

    Terminal
    $ npx archangel test
    pass entry-point rejected #1 src/app/main.ts, src/features/login/form.ts
    1 examples, none failed; 0 rules without a rejected example
    3 table rows need no written example; `archangel rules` shows what each covers
  2. Check the repository:

    Terminal
    $ npx archangel check
    error[entry]: "../features/cart/cart-total" reaches inside "cart" of "feature" past its entry files
    --> src/app/main.ts:1:27
    |
    1 | import { cartTotal } from '../features/cart/cart-total';
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ import it through "../features/cart"
    |
    = note: rule: entry-point
    error[may-import]: feature may not import another instance of feature, through "../cart": feature may import only shared, and itself in the same instance
    --> src/features/checkout/index.ts:1:27
    |
    1 | import { cartTotal } from '../cart';
    | ^^^^^^^^^ feature may import only shared, and itself in the same instance
    |
    = help: remove the import, or move the code to a file whose row permits the import
    = note: rule: element-types.feature
    checked 6 files and 6 imports: 2 problems
    For what a code means, run `archangel docs entry` or `archangel docs may-import`.

    app/main.ts reaches past the entry file of cart, and checkout imports another feature. Compare these problems with what ESLint reports before you remove the plugin.

  3. Read what each row judged:

    Terminal
    $ npx archangel rules
    element-types 3 rows archangel.config.ts:10
    element-types.app: app may import only feature, shared, and itself
    1 file · 2 imports judged · 0 problems · 0 exceptions
    element-types.feature: feature may import only shared, and itself in the same instance
    3 files · 3 imports judged · 1 problem · 0 exceptions
    element-types.shared: shared may import only itself
    1 file · 0 imports judged · 0 problems · 0 exceptions
    element-types.every-file-has-a-kind: a file in no row is not checked here; add `files` to check it
    5 files · 0 problems
    entry-point archangel.config.ts:16
    files in feature may be imported from outside their instance only through its entry files index.ts
    3 files · 4 imports judged · 1 problem · 0 exceptions
    Not judged by any rule: 0 files, 0 imports between repository files.
    Outside packages that no rule names: none.
  • One run checks all files. ESLint runs the plugin on each file, and the plugin resolves the imports of that file. archangel check reads the repository one time, and it can also check cycles and chains of imports.
  • Each file is in one part at most. boundaries takes the first element that matches a file. In archangel, a file that two parts find is the error part-overlap. Make the patterns disjoint, or make one of them a group.
  • An element that finds no file is an error. In archangel, this is element-empty. It usually shows a pattern with a typing error.
  • A table refuses imports into the repository only. A table judges a package only when a cell names an externals set that holds it. See Tables, rows and cells.