Migrate from eslint-plugin-boundaries
Markdown for agentsThis 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.
The config before
Section titled “The config before”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' }], }], }, },];Map each setting
Section titled “Map each setting”| 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. |
The config after
Section titled “The config after”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 insrc/featuresone instance. - The table
element-typeskeeps the name of the old rule. Its rows are namedelement-types.app,element-types.featureandelement-types.shared. - The
entryshorthand takes the nameentry-point, with a rejected example.
Run it
Section titled “Run it”-
Prove the rules:
Terminal $ npx archangel testpass entry-point rejected #1 src/app/main.ts, src/features/login/form.ts1 examples, none failed; 0 rules without a rejected example3 table rows need no written example; `archangel rules` shows what each covers -
Check the repository:
Terminal $ npx archangel checkerror[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-pointerror[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.featurechecked 6 files and 6 imports: 2 problemsFor what a code means, run `archangel docs entry` or `archangel docs may-import`.app/main.tsreaches past the entry file ofcart, andcheckoutimports another feature. Compare these problems with what ESLint reports before you remove the plugin. -
Read what each row judged:
Terminal $ npx archangel ruleselement-types 3 rows archangel.config.ts:10element-types.app: app may import only feature, shared, and itself1 file · 2 imports judged · 0 problems · 0 exceptionselement-types.feature: feature may import only shared, and itself in the same instance3 files · 3 imports judged · 1 problem · 0 exceptionselement-types.shared: shared may import only itself1 file · 0 imports judged · 0 problems · 0 exceptionselement-types.every-file-has-a-kind: a file in no row is not checked here; add `files` to check it5 files · 0 problemsentry-point archangel.config.ts:16files in feature may be imported from outside their instance only through its entry files index.ts3 files · 4 imports judged · 1 problem · 0 exceptionsNot judged by any rule: 0 files, 0 imports between repository files.Outside packages that no rule names: none.
Results that change
Section titled “Results that change”- One run checks all files. ESLint runs the plugin on each file, and the plugin resolves the
imports of that file.
archangel checkreads 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.