Rules and conditions
Markdown for agentsEach test in a config is a rule. A rule has a set of files and one condition: what those files must do, or must not do. You can read each rule as a sentence:
rule('no-react-in-shared', { files: 'shared', mayNotImport: { packages: ['react'] } })“Files in shared may not import react.”
The forms of a rule
Section titled “The forms of a rule”| Form | Write it as | Use it for |
|---|---|---|
| Table | mayImport(name, rows, options) |
What each set of files may import. Each row is a rule. |
| Rule | rule(name, { files, condition }) |
One decision about a set of files. |
| Folder rule | rule(name, { folders, condition }) |
Names, contents or presence of folders. |
| Rule of several sentences | rule(name, { sentences }) |
One decision that needs more than one condition. |
| Shorthand | layers(parts), isolate(part), entry(part, files) |
Common import rules, in one line. |
| Plugin rule | rule(name, { files, plugin, options }) |
A condition that you write in TypeScript. |
This config has a shorthand of each type, a rule that follows chains of imports, and a rule of two sentences:
import { defineConfig } from '@enhance-eng/archangel';
export default defineConfig({ parts: { app: 'src/app/**', feature: 'src/features/{feature}/**', shared: 'src/shared/**', }, rules: ({ entry, layers, rule }) => [ layers(['app', 'feature', 'shared'], { because: 'Code depends only on the layers below it.', examples: { rejected: ['src/shared/a.ts imports src/app/main.ts'] }, }),
entry('feature', 'index.ts', { examples: { rejected: ['src/app/main.ts imports src/features/cart/cart.ts'] }, }),
rule('no-react-in-shared', { files: 'shared', mayNotImport: { packages: ['react'] }, transitively: true, because: 'Shared code also runs on the server.', examples: { rejected: ['src/shared/a.ts imports src/shared/b.ts imports react'] }, }),
rule('no-utils', { sentences: [ { folders: 'src/**/utils', mustNotExist: true }, { files: 'src/**/utils.ts', mustNotExist: true }, ], because: 'A name says what the code does, not that it is a utility.', examples: { rejected: ['src/shared/utils.ts'] }, }), ],});archangel rules writes each rule as the sentences that it checks. Read this output to see what
your config says:
$ npx archangel ruleslayers(app, feature, shared) archangel.config.ts:10 because: Code depends only on the layers below it. files in feature may not import app files in shared may not import app, feature 3 files · 3 imports judged · 0 problems · 0 exceptions
entry(feature) archangel.config.ts:15 files in feature may be imported from outside their instance only through its entry files index.ts 2 files · 2 imports judged · 0 problems · 0 exceptions
no-react-in-shared archangel.config.ts:19 because: Shared code also runs on the server. files in shared may not import react, directly or through other files 1 file · 0 imports judged · 0 problems · 0 exceptions
no-utils archangel.config.ts:27 because: A name says what the code does, not that it is a utility. no folder may be in src/**/utils files in src/**/utils.ts must not exist 0 folders · 0 problems
Files that no rule judges: 1 src/app/main.tsOutside packages that no rule names: none.layers(...)becomes one sentence for each part after the first.- A shorthand without a
namegets a name from the rule itself, such aslayers(app, feature, shared). no-utilsfinds 0 folders in this repository, because no folder is namedutils. That is correct for a rule that says “must not exist”. Archangel shows the count, so that you can see it.
The conditions
Section titled “The conditions”A rule has exactly one condition. The conditions are in three groups.
| Group | Conditions | A rule reports |
|---|---|---|
| Imports | mayNotImport, mayOnlyImport, mayOnlyBeImportedBy, noCycles |
An import, or a cycle of imports. |
| Structure | mustNotExist, mustBeNamed, mustContain |
A file or folder that exists, has the wrong name, or lacks a file. |
| Exports | mustExport, mayNotExport, mustOnlyReExport |
A file that exports the wrong names, or has code of its own. |
Each condition is in the conditions reference, with an example.
Make an import condition narrower
Section titled “Make an import condition narrower”An import condition can apply to some imports only. Add an option such as names,
specifier, instance, dependency or within:
rule('no-effects-in-view', { files: 'view', mayNotImport: { packages: ['react'] }, names: ['useEffect'], examples: { rejected: [{ 'src/view/a.ts': "import { useEffect } from 'react';" }] },})“Files in view may not import useEffect from react.” The
import options reference lists each option.
Follow chains of imports
Section titled “Follow chains of imports”A rule with transitively: true refuses a chain of imports, not only a direct import. In the
config above, no-react-in-shared refuses shared/a.ts → shared/b.ts → react. The report shows the
shortest chain. stopAt ends a chain at some files. A transitive rule counts only runtime imports,
because a chain of type imports never runs.
Options for each rule
Section titled “Options for each rule”| Option | What it does |
|---|---|
name |
The name in each report, exception and baseline entry. Write it first in rule('name', { ... }). |
because |
Your reason. Each report shows it. Tell the next reader why the rule exists. |
link |
A link that each report shows. |
imports |
'all', 'runtime', 'static' or 'types': which imports the rule counts. |
examples |
Code that the rule must accept, and code that it must reject. |
A mustNotExist rule passes when it finds nothing, so its files or folders may match nothing.
no-utils above uses both: a folder named utils, and a file named utils.ts.
One rule, one decision
Section titled “One rule, one decision”Write one rule for each decision, and give it a because. A problem then names the decision that
the code broke, and the reason for it. A rule with a reason is easy to keep, change or delete. A
rule without a reason is hard to question.