Migrate from dependency-cruiser
Markdown for agentsThis guide moves the rules of a .dependency-cruiser.cjs file to archangel. Each rule keeps its
name, so a team can find it again. At the end, archangel checks the same decisions, with examples
that prove each rule.
Before you start
Section titled “Before you start”- Install archangel:
npm install --save-dev @enhance-eng/archangel. - Keep dependency-cruiser until archangel gives the results that you expect.
The config before
Section titled “The config before”This repository has three forbidden rules:
module.exports = { forbidden: [ { name: 'no-circular', severity: 'error', from: {}, to: { circular: true }, }, { name: 'ui-not-to-db', severity: 'error', comment: 'The UI reads data through the api folder.', from: { path: '^src/ui' }, to: { path: '^src/db' }, }, { name: 'not-to-dev-dep', severity: 'error', from: { path: '^src', pathNot: '\\.test\\.ts$' }, to: { dependencyTypes: ['npm-dev'] }, }, ],};Map each rule
Section titled “Map each rule”| dependency-cruiser | archangel |
|---|---|
from: { path } |
The rule’s files: a part, a group or a path pattern. |
to: { path } |
mayNotImport, with a part, a group or a path pattern. |
pathNot |
{ all: [..., { not: ... }] } in files or on the other side. |
to: { circular: true } |
noCycles: true. Add imports: 'all' to count type imports too. |
to: { dependencyTypes: ['npm-dev'] } |
mayNotImport: { packages: ['*'] } with dependency: 'dev'. |
to: { dependencyTypes: ['npm-no-pkg'] } |
mayNotImport: { packages: ['*'] } with dependency: 'undeclared'. |
allowed with allowedSeverity |
A table: each row lists what it may import, and the table refuses the rest. |
comment |
because. Archangel shows it with each problem. |
severity: 'warn' |
No equivalent. A rule is on or off. |
no-orphans |
No built-in condition. Write a plugin condition that reads importers. |
The config after
Section titled “The config after”import { defineConfig } from '@enhance-eng/archangel';
export default defineConfig({ parts: { ui: 'src/ui/**', api: 'src/api/**', db: 'src/db/**', }, groups: { tests: '**/*.test.ts', }, rules: ({ rule }) => [ rule('no-circular', { files: 'src/**', noCycles: true, imports: 'all', examples: { rejected: ['src/a.ts imports src/b.ts imports src/a.ts'] }, }),
rule('ui-not-to-db', { files: 'ui', mayNotImport: 'db', because: 'The UI reads data through the api folder.', examples: { rejected: ['src/ui/list.ts imports src/db/client.ts'] }, }),
rule('not-to-dev-dep', { files: { all: ['src/**', { not: 'tests' }] }, mayNotImport: { packages: ['*'] }, dependency: 'dev', examples: { rejected: [{ 'src/api/a.ts': "import 'vitest';" }], }, }), ],});- The paths become parts:
ui,apianddb. Many rules can use a part, and a part that finds no file is an error. pathNot: '\\.test\\.ts$'becomes the grouptests, andfilestakes all ofsrcexcept that group.- Each rule has a rejected example.
archangel testfails if a rule cannot refuse its example.
Run it
Section titled “Run it”-
Prove the rules:
Terminal $ npx archangel testpass no-circular rejected #1 src/a.ts, src/b.tspass ui-not-to-db rejected #1 src/db/client.ts, src/ui/list.tspass not-to-dev-dep rejected #1 src/api/a.ts3 examples, none failed; 0 rules without a rejected example -
Check the repository:
Terminal $ npx archangel checkerror[no-cycles]: 2 files import each other in a cycle: src/api/note.ts → src/api/notes.ts → src/api/note.ts--> src/api/note.ts:1:32= help: remove one import of the cycle, or move what both files need into a file that neither of them imports= note: rule: no-circularerror[may-not-import]: src/ui/note-list.ts may not import "../db/client", because files in "ui" may not import "db"--> src/ui/note-list.ts:2:22|2 | import { pool } from '../db/client';| ^^^^^^^^^^^^^^ remove the import, or move the code to files that may import it|= note: rule: ui-not-to-db= note: because: The UI reads data through the api folder.error[may-not-import]: src/ui/note-list.ts may not import "vitest", because files in "all of (src/**, not tests)" may not import any package that package.json lists only under devDependencies--> src/ui/note-list.ts:3:20|3 | import { vi } from 'vitest';| ^^^^^^^^ remove the import, or move the code to files that may import it|= note: rule: not-to-dev-depchecked 6 files and 10 imports: 3 problemsFor what a code means, run `archangel docs no-cycles` or `archangel docs may-not-import`. -
Compare each problem with the output of dependency-cruiser. The next section lists the results that can change.
-
See what the rules cover:
Terminal $ npx archangel rulesno-circular archangel.config.ts:13files in src/** may not import each other in a cycle5 files · 1 problemui-not-to-db archangel.config.ts:20because: The UI reads data through the api folder.files in ui may not import db1 file · 3 imports judged · 1 problem · 0 exceptionsnot-to-dev-dep archangel.config.ts:27files in all of (src/**, not tests) may not import * that the nearest package.json lists only for development4 files · 1 import 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”- The cycle above is made of type imports.
notes.tsandnote.tsimport each other withimport type. dependency-cruiser withtsPreCompilationDepsreports cycles of type imports. Archangel’snoCyclescounts only runtime imports by default, because the compiler removes a type import. The config above addsimports: 'all'to keep the result of dependency-cruiser. Remove it to report only the cycles that exist when the code runs. - An import that does not resolve is an error. Archangel reports
import-unresolvedfor an import with no file and no installed package. dependency-cruiser does not stop on it. So expect some new errors when you move: each one is an import or a path that was wrong before. For an import that a tool makes at build time, add it toallowUnresolvedin the config, with the reason. - A rule that finds no files is an error. A path in dependency-cruiser that matches nothing
makes a rule that never fires, and the run passes. In archangel, a part, a group or a pattern
that matches nothing gives
selector-emptyorelement-empty. - One table can replace many forbidden rules. A set of
forbiddenrules that each refuse one pair of folders says, in effect, what each folder may import. A table says that directly, and it also refuses an import into a folder that no rule named.
Next steps
Section titled “Next steps”- Turn the forbidden rules about folders into a table: Layer a backend.
- Start on code that already breaks the rules: Exceptions and baselines.