Skip to content

Parts, groups and externals

Markdown for agents

Before you write a test, you give names to sets of code. A rule then says “these files may not import those files”, and each name in the sentence is one of these sets. Archangel has three types of names:

Name What it holds How many per file
Part Files, found by a path pattern One part at most
Group Files, found by a pattern, a directive or a combination Any number
Externals set Outside packages, by package name Not about files

This config uses all three:

archangel.config.ts
import { defineConfig } from '@enhance-eng/archangel';
export default defineConfig({
parts: {
feature: 'src/features/{feature}/**',
shared: 'src/shared/**',
},
groups: {
tests: '**/*.test.ts',
client: { directive: 'use client' },
},
externals: {
browser: ['react', 'react-dom'],
},
rules: ({ mayImport, rule }) => [
mayImport('app', {
feature: ['shared', 'browser'],
shared: [],
}),
rule('tests-stay-out', {
files: { not: 'tests' },
mayNotImport: 'tests',
because: 'Code that ships never needs a test file.',
examples: { rejected: ['src/shared/a.ts imports src/shared/a.test.ts'] },
}),
],
});

A part is a named area of your code. Archangel gives each file one part at most, so a part answers the question “where does this file live?”. Parts are the names that most tables and rules use.

  • A part is a path pattern, or a list of path patterns. Paths start at the config folder.
  • A file that no part finds is in no part. That is not an error. To make it an error, write a rule with mustNotExist on the files that are in no part.
  • If two parts find one file, archangel shows the error part-overlap. It cannot know which part judges the file. Narrow one pattern with not, or make one of the two a group.
  • If a part finds no file, archangel shows the error element-empty. A part that finds nothing usually has a typing error in its pattern.

A part can have a capture, such as {feature}. A capture makes each folder one instance of the part. Captures and instances tells you more.

A group is a named set of files that can cross parts. Use a group for files that have something in common, but live in many places: tests, stories, client components, generated code.

  • A file can be in many groups, and in a part too.
  • A group can be a path pattern, a list, a directive such as { directive: 'use client' }, or a combination with all, any and not. Sets of files lists each form.
  • A group that finds no file is the error selector-empty.

An externals set is a named set of outside packages. Use one in a table cell, so that a row may import those packages. On the other side of a rule, you can use the name, or write the packages in place with { packages: ['react'] }.

  • A name matches the package and each of its subpaths. react matches react/jsx-runtime.
  • @aws-sdk/* matches each package in a scope. * matches each package.
  • Write a Node built-in module with node:, for example node:fs. Archangel refuses fs.
  • Archangel refuses a name with a subpath, a name that starts with @types/, and the name of a package in your repository.

archangel explain shows the part, the instance and the groups of one file:

Terminal
$ npx archangel explain src/features/cart/cart-button.tsx
src/features/cart/cart-button.tsx
part: "feature", instance "cart"
groups: client
rules that apply: app.feature, tests-stay-out
app.feature: feature may import only shared, browser, and itself in the same instance (judges its imports)
tests-stay-out: files in not tests may not import tests (judges its imports)
imports:
line 2 'react' -> package react ("browser")
line 3 '../../shared/money' -> src/shared/money.ts ("shared")
imported by: none

The file is in the part feature, in the instance cart, and in the group client, because it starts with 'use client'. The import of react shows its externals set, browser.

The same file is in the table row feature, and the rule tests-stay-out judges it, because the file is not in the group tests. A group is the right tool for this rule. A test file lives next to the code that it tests, so a part cannot hold the test files.

Terminal
$ npx archangel rules
app 2 rows archangel.config.ts:16
app.feature: feature may import only shared, browser, and itself in the same instance
1 file · 2 imports judged · 0 problems · 0 exceptions
app.shared: shared may import only itself
2 files · 1 import judged · 0 problems · 0 exceptions
app.every-file-has-a-kind: a file in no row is not checked here; add `files` to check it
3 files · 0 problems
tests-stay-out archangel.config.ts:21
because: Code that ships never needs a test file.
files in not tests may not import tests
3 files · 3 imports judged · 0 problems · 0 exceptions
Not judged by any rule: 0 files, 0 imports between repository files.
Outside packages that no rule names: vitest (1 import).

The last line lists vitest. No rule names it, so no rule can refuse an import of it. A table judges a package only when a cell names an externals set that holds it. Tables, rows and cells tells you why.

  • Use a part for an area that has its own name and that many tests use: an app, a layer, a feature, a package.
  • Use a group for files that cross parts: tests, stories, client components.
  • Use a kind when one pattern gives the names, for example each *.entity.ts file. A kind needs no declaration, but it has a meaning only in its table. Kinds tells you more.
  • Use an externals set for packages that a table row may import.