Reference: Config file: Where archangel finds its config, the two formats, and each top-level key.
# Config file
> Where archangel finds its config, the two formats, and each top-level key.
The config holds the names of the parts of your code and the rules about them. This page lists each top-level key. Each rule form has its own page.
## Where archangel finds the config
Archangel looks in the current folder, and then in each folder above it. In each folder, it looks for these names, in this order:
1. `archangel.config.ts`
2. `archangel.config.mts`
3. `archangel.config.js`
4. `archangel.config.mjs`
5. `archangel.config.json`
It uses the first file that it finds. `--config ` replaces this search.
The folder of the config is the **config folder**. Archangel reads the source files below it. Each pattern in the config starts at the config folder, and each path in a report is relative to it. Archangel does not add `**/` before a pattern.
When archangel finds no config, it stops with [`config-missing`](/archangel/reference/diagnostics/config-missing/) and exit code 2.
## TypeScript or JSON
| | `archangel.config.ts` (and `.mts`, `.js`, `.mjs`) | `archangel.config.json` |
| ------------------------ | ------------------------------------------------------- | ------------------------------------------- |
| Needs Node | Yes, 22.18 or later. Node runs it without a build step. | No. |
| Type checks as you write | Yes, with `defineConfig`. | Completion and checks from the JSON schema. |
| `rules` | A list, or a function that gets the rule builders. | A list of rule objects. |
| Presets and plugins | Yes. | No, because they are code. |
Archangel runs a TypeScript or JavaScript config one time for each load. It uses the default export. When `rules` is a function, archangel calls it one time with the rule builders: `rule`, `mayImport`, `layers`, `isolate` and `entry`.
`defineConfig` changes nothing when the config runs. It gives the types. With it, the TypeScript compiler finds some errors before archangel runs. For example, it finds a rule that names a part that does not exist, and a table cell that names nothing.
A JSON config holds the same content. Each rule is the object that you give to `rule(...)`, and a table or a shorthand has a `kind`. Add `$schema` to get completion in your editor:
archangel.config.json
```json
{
"$schema": "./node_modules/@enhance-eng/archangel/archangel.schema.json",
"parts": {
"feature": "src/features/{feature}/**",
"shared": "src/shared/**"
},
"rules": [
{
"kind": "mayImport",
"name": "app",
"rows": { "feature": ["shared"], "shared": [] },
"because": "Shared code never depends on a feature."
}
]
}
```
Note
The count of files in `archangel check` includes a TypeScript config, because it is a source file. A JSON config is not a source file. So one repository can show one more file with a TypeScript config.
## The keys
| Key | Type | What it does |
| ------------------------------------- | ---------------- | ----------------------------------------------------------- |
| [`parts`](#parts) | object | Named areas of your code. Each file is in one part at most. |
| [`groups`](#groups) | object | Named sets of files. A file can be in many groups. |
| [`externals`](#externals) | object | Named sets of outside packages. |
| [`rules`](#rules) | list or function | The tables and the rules. |
| [`extends`](#extends) | list | Presets, such as `fsd(...)` and `nextjs(...)`. |
| [`off`](#off) | list of names | Rules and table rows to turn off. |
| [`plugins`](#plugins) | object | Plugins, by the name that rules use. |
| [`ignore`](#ignore) | list of lines | Files that archangel does not read. |
| [`allowUnresolved`](#allowunresolved) | object | Imports that may stay unresolved, each with a reason. |
| `$schema` | string | JSON only. The path of the JSON schema. |
Archangel refuses a key that it does not know, with [`config-invalid`](/archangel/reference/diagnostics/config-invalid/). The message names the nearest correct key.
A config needs at least one part, one rule or one preset. A config with none of these checks nothing, so archangel refuses it.
### `parts`
A part is a named area of your code. Its value is a path pattern or a list of path patterns.
```ts
parts: {
feature: 'src/features/{feature}/**',
shared: 'src/shared/**',
app: ['src/app/**', 'src/main.ts'],
},
```
* A name in braces, such as `{feature}`, is a **capture**. It makes each folder one **instance** of the part. Read [Captures and instances](/archangel/concepts/captures-and-instances/).
* Each file is in one of your parts at most. When two parts find one file, archangel shows [`part-overlap`](/archangel/reference/diagnostics/part-overlap/).
* A part that finds no file is the error [`element-empty`](/archangel/reference/diagnostics/element-empty/).
* A part holds files, not packages. Write packages in `externals`.
### `groups`
A group is a named set of files. A file can be in many groups, so a group can cut across parts. Its value is any [set of files](/archangel/reference/sets-of-files/).
```ts
groups: {
test: '**/*.test.ts',
client: { directive: 'use client' },
},
```
A group that finds no file is the error [`selector-empty`](/archangel/reference/diagnostics/selector-empty/).
### `externals`
An externals set is a named list of outside packages.
```ts
externals: {
driver: ['pg', '@aws-sdk/*', 'node:fs'],
},
```
| You write | It matches |
| ------------ | ------------------------------------------------------------------ |
| `pg` | the package `pg` and each of its subpaths, such as `pg/lib/client` |
| `@aws-sdk/*` | each package in the scope `@aws-sdk` |
| `*` | each package |
| `node:fs` | the Node built-in module `fs` |
Archangel refuses these names, because they can never match:
* a Node built-in module without `node:`, such as `fs`. Write `node:fs`.
* a name with a subpath, such as `pg/lib`.
* a name that starts with `@types/`.
* the name of a package in your repository. An import of a workspace package resolves to its files, so use a part.
An import counts as the package that it resolves to. Use an externals set in a table cell, or on the other side of a rule. On the other side of a rule, you can also write the packages directly: `{ packages: ['react'] }`.
### `rules`
The tables and the rules. In TypeScript, write a function that gets the rule builders:
```ts
rules: ({ mayImport, rule, layers, isolate, entry }) => [
isolate('feature'),
rule('no-react-in-shared', {
files: 'shared',
mayNotImport: { packages: ['react'] },
examples: { rejected: ['src/shared/a.ts imports react'] },
}),
],
```
You can also write a list of rule objects, as in JSON. Each rule has a name, and two rules cannot have one name ([`rule-name-duplicate`](/archangel/reference/diagnostics/rule-name-duplicate/)).
| Form | Page |
| ---------------------------------------- | ---------------------------------------------- |
| `mayImport(name, rows, options)` | [Tables](/archangel/reference/tables/) |
| `rule(name, { files, })` | [Conditions](/archangel/reference/conditions/) |
| `rule(name, { folders, })` | [Conditions](/archangel/reference/conditions/) |
| `rule(name, { sentences: [...] })` | [Conditions](/archangel/reference/conditions/) |
| `rule(name, { files, plugin, options })` | [Plugin API](/archangel/reference/plugin-api/) |
| `layers`, `isolate`, `entry` | [Shorthands](/archangel/reference/shorthands/) |
### `extends`
A list of presets. Each preset adds parts, groups, rules and plugins, with its name as a prefix.
```ts
extends: [fsd({ root: 'src' }), nextjs()],
```
Read [Presets](/archangel/reference/presets/).
### `off`
A list of rule names and table row names to turn off. Archangel applies `off` after it merges the presets.
```ts
off: ['fsd/insignificant-slice', 'app.every-file-has-a-kind'],
```
A name in `off` must be the name of a rule. When it is not, archangel stops and shows the nearest correct name. `archangel rules` lists the rules that `off` removes.
### `plugins`
The plugins that the config loads, by the name that rules use:
```ts
plugins: { team },
```
A rule then uses a condition of the plugin as `plugin: 'team/max-imports'`. Read [Plugin API](/archangel/reference/plugin-api/).
### `ignore`
Lines in `.gitignore` syntax. Archangel does not read a file that a line matches.
```ts
ignore: ['src/generated/', '**/*.stories.tsx'],
```
A line with no `/` except at its end matches at any depth, as in `.gitignore`: `'public/'` leaves out every folder named `public`. Write `'/public/'` for the folder at the top only. When a part or a group matches no file because of a line, its error names the line.
Archangel already skips each file that a `.gitignore` file in the repository ignores. It uses only the `.gitignore` files of the repository. It does not use your global Git ignore file, `.git/info/exclude` or `.ignore` files, so each machine gets the same files. A line that is not valid is [`ignore-line-invalid`](/archangel/reference/diagnostics/ignore-line-invalid/).
Archangel reads each file below the config folder with the extension `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs` or `.cjs`, in hidden folders too. Declaration files (`.d.ts`) are source files.
### `allowUnresolved`
Specifiers that may stay unresolved, each with a reason. A specifier can end in `*`.
```ts
allowUnresolved: {
'virtual:*': 'Vite makes these modules when it builds.',
'./generated/routes': 'The build writes this file before it compiles.',
},
```
An import that archangel cannot resolve is the error [`import-unresolved`](/archangel/reference/diagnostics/import-unresolved/), because archangel cannot judge it. A specifier in `allowUnresolved` gives no error, and the summary counts these imports.
A type import, such as `import type { Point } from 'geojson'`, of a package that only `@types` installs resolves to that package, as TypeScript resolves it, so it needs no entry here.
An entry that names a package also permits a tsconfig whose `extends` names that package when it is not installed, such as a sub-project that installs its own packages. `'@tsconfig/node20': 'the worker installs its own packages'` stops the [`resolution-file-invalid`](/archangel/reference/diagnostics/resolution-file-invalid/) error for it. The files below that tsconfig are still checked.
## How archangel resolves imports
Archangel resolves each import as TypeScript does:
* It uses the nearest `tsconfig.json` of the importing file, with its `extends`, `baseUrl` and `paths`.
* It uses the `exports` and `imports` fields of `package.json`.
* For a specifier that ends in `.js`, `.jsx`, `.mjs` or `.cjs`, it looks for the TypeScript file, then the declaration file, then the JavaScript file.
* It follows a symbolic link to the real file. So an import of a workspace package resolves to the files of that package.
Archangel finds import statements, `export ... from`, dynamic `import()` and `require()` with a literal specifier, `import x = require()`, import types such as `import('./x').T`, and triple-slash references.
An `import type` is a type import. With `verbatimModuleSyntax` in your tsconfig, `import { type A } from './a'` stays in the compiled code, so it is a runtime import.
## Older spellings
A config from archangel 0.1 still loads. A config uses one spelling of each key. Archangel refuses two spellings in one config. These spellings stop at 1.0.
| Spelling | Older spelling |
| ------------------------- | ------------------------------- |
| `parts` | `elements` |
| `groups` | `selectors` |
| `because` | `reason` |
| `rule('', { ... })` | `rule({ name: '', ... })` |
In 0.1, `elements` could hold a package set, `{ packages: [...] }`. Write it as an entry in `externals` now. On the other side of a rule, `{ packages: [...] }` is not an older spelling, and it stays.
The words in some messages still come from 0.1. For example, a message can say “element” for a part, and “selector” for a group.