Skip to content

A rule has some files and one condition. Read it as a sentence: “These files may not import React.”

rule('no-react-in-shared', { files: 'shared', mayNotImport: { packages: ['react'] }, examples: { rejected: ['src/shared/a.ts imports react'] } })
Form What it is
rule('<name>', { files: ..., <condition>: ... }) A rule about files, with its name first.
rule({ name: '<name>', files: ..., <condition>: ... }) The same rule, with the name as an option.
rule('<name>', { folders: ..., <condition>: ... }) A rule about folders.
rule('<name>', { sentences: [...] }) One name for a group of conditions.
rule('<name>', { files: ..., plugin: ..., options: ... }) A rule with a plugin condition. Read Plugin API.

A rule without a name gets a name from its condition, for example isolate(feature). Give each rule a name, because you use it in off, in exception comments and in the baseline.

A rule’s own name cannot have a /. Only the names of a preset have a prefix, such as fsd/public-api. To replace a preset rule, write a rule with the same name.

Each rule that is not a table row needs a rejected example. Read Examples.

Condition Value Archangel reports
mayNotImport the other side an import from the files into the other side
mayOnlyImport the other side an import from the files into something that is not on the other side
mayOnlyBeImportedBy the other side an import into the files from a file that is not on the other side
noCycles true files that import each other in a cycle

The other side is a set of files, packages, or a list of these.

rule('ui-not-to-db', { files: 'ui', mayNotImport: 'db', examples: { rejected: ['src/ui/a.ts imports src/db/a.ts'] } })

Add transitively: true to find chains of imports. Read Import options.

rule('views-import-little', { files: 'view', mayOnlyImport: ['shared', { packages: ['react'] }], examples: { rejected: ['src/view/a.ts imports src/db/a.ts'] } })
  • The rule always permits its own files. When files is one part with a capture, it permits only the files of the same instance. To permit each instance, add the part to the list.
  • Without within, the rule judges each import, packages too.
  • An empty list is a config error, unless the rule has within. With within, an empty list refuses each import into the area, except into the rule’s own files.
rule('db-behind-api', { files: 'db', mayOnlyBeImportedBy: 'api', examples: { rejected: ['src/view/a.ts imports src/db/a.ts'] } })

The rule permits imports from its own files. An import from a file that is in no part is a problem too.

rule('no-cycles', { files: 'src/**', noCycles: true, examples: { rejected: ['src/a.ts imports src/b.ts imports src/a.ts'] } })
  • The rule reports each group of files that import each other. The report lists each file of the group and shows the shortest cycle.
  • It counts only runtime imports by default. The compiler removes an import type, so a cycle of type imports cannot break when the code runs. To count type imports too, add imports: 'all'.
Condition Value Archangel reports
mustNotExist true each file, or folder, that the rule finds
mustBeNamed a style, a name pattern, or a list of these a name that does not match
mustContain a path pattern, or a list of patterns an instance or a folder that has no matching file
rule('every-file-in-a-part', { files: { all: ['src/**', { not: { element: '*' } }] }, mustNotExist: true, examples: { rejected: ['src/stray.ts'] } })

This rule reports each file in src that is in no part.

rule('component-names', { files: 'src/view/**/*.tsx', mustBeNamed: 'PascalCase', examples: { rejected: ['src/view/user-avatar.tsx'] } })
rule('ui-file-names', { files: 'src/ui/**/*.tsx', mustBeNamed: ['*.view', '*.container'], examples: { rejected: ['src/ui/list.tsx'] } })

A file has two names:

File Name Full name
button.view.tsx button button.view
UserCard.tsx UserCard UserCard

The name is the text up to the first dot. The full name is the text without the last extension.

  • A style checks the name: kebab-case, camelCase, PascalCase, snake_case or SCREAMING_SNAKE_CASE.
  • A name pattern checks the full name. In a name pattern, * is any text or no text. *.view matches cart.view.tsx. It does not match view.tsx or cart.view.extra.tsx.
  • A name pattern can use the name of the file, or of the folder that holds it, in each style:
Spelling Example for src/user-profile/avatar-image.tsx
{FileName} AvatarImage
{fileName} avatarImage
{file-name} avatar-image
{file_name} avatar_image
{FILE_NAME} AVATAR_IMAGE
{FolderName}, {folderName}, {folder-name}, {folder_name}, {FOLDER_NAME} UserProfile, userProfile, user-profile, user_profile, USER_PROFILE
rule('test-names', { files: 'src/**/*.test.ts', mustBeNamed: '{folder-name}.test', examples: { rejected: ['src/cart/total.test.ts'] } })

A list passes when one entry matches. A list with one entry gives the same result and the same message as the single value. An empty list is a config error. Each entry follows the rules of a single value.

rule('feature-index', { files: 'feature', mustContain: ['index.ts', 'index.tsx'], examples: { rejected: ['src/features/search/model.ts'] } })
  • With files, the files must be one part with a capture. The pattern starts at the root of each instance. Other files give rule-needs-capture.
  • With folders, the pattern starts at each folder.
  • In a list, one match is sufficient.
Condition Value Archangel reports
mustExport a name pattern a file that exports no name that matches
mayNotExport 'default', 'star' or a name pattern each export of that type
mustOnlyReExport true a file with code of its own
rule('view-exports', { files: 'src/view/**/*.tsx', mustExport: '{FileName}', examples: { rejected: [{ 'src/view/Avatar.tsx': 'export function Picture() {}' }] } })

Re-exports count, and so do the names that an export * chain brings in.

rule('no-default-exports', { files: 'src/**', mayNotExport: 'default', examples: { rejected: [{ 'src/a.ts': 'export default 1;' }] } })
  • 'default' refuses a default export.
  • 'star' refuses export * and export * as.
  • A name pattern refuses each named export that matches it.

Archangel reports one problem for each such export, on its line.

rule('index-only-re-exports', { files: 'src/**/index.ts', mustOnlyReExport: true, examples: { rejected: [{ 'src/shared/index.ts': 'export const a = 1;' }] } })

A file passes when each top-level statement is an import, an export ... from, a directive, or an export { ... } of names that the file imported. A type declaration counts as code of the file.

Write folders instead of files. A folder rule can use mustBeNamed, mustContain or mustNotExist.

rule('folder-names', { folders: 'src/**', mustBeNamed: 'kebab-case', examples: { rejected: ['src/UserProfile/a.ts'] } })
rule('shared-entry', { folders: ['src/shared/*', '!src/shared/{ui,lib}'], mustContain: ['index.ts', 'index.tsx'], examples: { rejected: ['src/shared/money/format.ts'] } })
  • A pattern finds the folders below its fixed part. So src/** is each folder inside src, not src itself.
  • A pattern that starts with ! removes folders, as in .gitignore.
  • Archangel checks each folder one time, however many files it holds.
  • For mustBeNamed on folders, an entry cannot use the file name.

Use sentences to give one name to a group of conditions. The rule fails when one of them fails.

rule('no-ui-in-app', {
sentences: [
{ folders: 'src/app/ui', mustNotExist: true },
{ files: 'src/app/ui.*', mustNotExist: true },
],
examples: { rejected: ['src/app/ui.ts'] },
})

A sentence holds files or folders, one condition, and the options of that condition. The rule holds name, because, link, examples and imports. A sentence can also be a shorthand, written with its kind, such as { kind: 'isolate', element: 'feature' }.

In a JSON config, each rule is the object that you give to rule(...), with name as a field:

{ "name": "no-react-in-shared", "files": "shared", "mayNotImport": { "packages": ["react"] } }