Skip to content
archangel.config.ts
mayImport('app', {
  feature: ['shared'],
  shared: [],
})
npx archangel check
error[may-import]: feature may not import
another instance of feature
 --> src/features/checkout/checkout.ts:1:27
  = note: rule: app.feature
checked 5 files and 4 imports: 1 problem

Make architecture verifiable.

Architecture tests for TypeScript. Write each decision about your code as a test. Archangel checks it in milliseconds, in your editor, in CI and for your coding agents.

A decision such as “features do not import each other” is easy to write in a document. It is also easy to break with one line in a large change. A reviewer sees the diff, not the graph of imports.

Archangel makes the decision a test. Give names to the parts of your code. Then write a table: each row is a part, and the list on the row is what that part may import.

archangel.config.ts
import { defineConfig } from '@enhance-eng/archangel';
export default defineConfig({
parts: {
feature: 'src/features/{feature}/**',
shared: 'src/shared/**',
},
rules: ({ mayImport, rule }) => [
mayImport('app', {
feature: ['shared'],
shared: [],
}, {
because: 'A feature changes alone. Code that two features need goes in shared.',
}),
rule('no-react-in-shared', {
files: 'shared',
mayNotImport: { packages: ['react'] },
because: 'Shared code also runs on the server.',
examples: {
rejected: ['src/shared/money.ts imports react'],
},
}),
],
});

The checkout feature imports a file inside the cart feature. Archangel shows where, why, and what to do:

Terminal
$ npx archangel check
error[may-import]: feature may not import another instance of feature, through "../cart/cart": feature may import only shared, and itself in the same instance
--> src/features/checkout/checkout.ts:1:27
|
1 | import { cartTotal } from '../cart/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: app.feature
= note: because: A feature changes alone. Code that two features need goes in shared.
checked 5 files and 4 imports: 1 problem
For what a code means, run `archangel docs may-import`.

Imports

Which code may import which code, in a table. Direct imports, chains of imports, the names that an import brings in, and packages that package.json does not list.

Cycles

Files that import each other. Runtime imports count. Type imports count only when you say so.

Structure

Where files must be, which files a folder must contain, and which files must not exist.

Names and exports

The style of file and folder names, lists of permitted names, and what a file must export.

A rule with a wrong path finds no files. It then shows “no problems”, the same as a rule that works. Archangel does not let this occur in silence:

  • A part, a group or a table that finds no file is an error.
  • Each rule that is not a table row has an example of code that it must reject. archangel test fails when the rule does not reject it.
  • archangel rules shows each rule as a sentence, with the files and imports that it judged, and the files that no rule judges.
Seconds to check the same 400 architecture rules on a 9,800-file TypeScript monorepo: archangel 1.9 s; dependency-cruiser, ESLint and Steiger together 67.7 s Seconds to check the same 400 architecture rules on a 9,800-file TypeScript monorepo: archangel 1.9 s; dependency-cruiser, ESLint and Steiger together 67.7 s

A Rust core reads each file one time. The chart shows the time to check the same 400 rules on a 9,800-file monorepo, on a laptop. Without archangel, these rules need three tools. The editor shows the result of a change in about 13 milliseconds.