Skip to content

Getting started

Markdown for agents

This page takes you from an empty config to a table that finds a real problem. It takes about ten minutes. Each output on this page is what archangel 0.3.5 prints for the files on this page.

  • You need Node 22.18 or later. Node runs archangel.config.ts without a build step.
  • You need a TypeScript or JavaScript repository.

The example repository is a small shop. It has two features and some shared code:

  • package.json
  • Directorysrc/
    • Directoryfeatures/
      • Directorycart/
        • cart.ts
        • index.ts
      • Directorycheckout/
        • checkout.ts
    • Directoryshared/
      • money.ts
  1. Install the package as a development dependency:

    Terminal window
    npm install --save-dev @enhance-eng/archangel
  2. Write a starter config:

    Terminal
    $ npx archangel init --typescript
    wrote archangel.config.ts with 2 parts: features, shared
    It imports `@enhance-eng/archangel`, so install the package first: npm install --save-dev @enhance-eng/archangel
    Write what each part may import in a table, then run `archangel check`; `archangel docs may-import` shows how.

    init makes one part for each folder of source files. It also adds one rule against import cycles:

    archangel.config.ts
    import { defineConfig } from '@enhance-eng/archangel';
    export default defineConfig({
    parts: {
    features: 'src/features/**',
    shared: 'src/shared/**',
    },
    rules: ({ rule }) => [
    rule('no-cycles', {
    files: '**',
    noCycles: true,
    because: 'Files that import each other cannot change or load one at a time.',
    examples: {
    accepted: ['cycle/a.ts imports cycle/b.ts'],
    rejected: ['cycle/a.ts imports cycle/b.ts imports cycle/a.ts'],
    },
    }),
    ],
    });
  3. Check the repository:

    Terminal
    $ npx archangel check
    checked 5 files and 4 imports: no problems

    The count includes the config file. The starter config finds no problem, because it has no rule about your architecture yet.

A table is the main form of an architecture test. Each row of the table is a set of files. The list on the row is what those files may import. The table refuses each other import into the repository.

The shop has one decision: a feature does not import another feature. Code that two features need goes in shared.

  1. Replace the config with this config:

    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'],
    },
    }),
    ],
    });
    • parts gives names to the parts of the code. {feature} is a capture. It makes each folder in src/features one instance of the part feature, so cart and checkout are two instances.
    • The table app has two rows. A file in feature may import shared, and files in its own instance. A file in shared may import only shared.
    • because is your reason. Archangel shows it with each problem.
    • The rule no-react-in-shared is a rule with one condition. The next section tells you about it.
  2. Check the repository again:

    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`.

    checkout.ts imports a file inside the cart feature. The error shows the line, the rule app.feature, your reason, and what the row permits.

  3. Fix the problem. checkout.ts needs only the formatted total, so import formatMoney from shared:

    src/features/checkout/checkout.ts
    import { formatMoney } from '../../shared/money';
    export function checkoutSummary(prices: number[]): string {
    return `Total: ${formatMoney(prices.reduce((sum, price) => sum + price, 0))}`;
    }
  4. Check again:

    Terminal
    $ npx archangel check
    checked 5 files and 4 imports: no problems

A rule can be wrong. For example, a typing error in a path can make a rule find no files, and then it never finds a problem. So each rule that is not a table row must have a rejected example: code that the rule must refuse.

The rule no-react-in-shared has one example, on one line:

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'],
},
})

Run the examples:

Terminal
$ npx archangel test
pass no-react-in-shared rejected #1 src/shared/money.ts
1 examples, none failed; 0 rules without a rejected example
2 table rows need no written example; `archangel rules` shows what each covers

Archangel makes the files of each example in memory, and checks that the rule refuses them. It does not write files. A table row needs no example, because archangel rules shows what each row judged, and a table that finds no file is an error.

archangel rules shows each rule as a sentence, with the number of files and imports that it judged:

Terminal
$ npx archangel rules
app 2 rows archangel.config.ts:9
because: A feature changes alone. Code that two features need goes in shared.
app.feature: feature may import only shared, and itself in the same instance
3 files · 3 imports judged · 1 problem · 0 exceptions
app.shared: shared may import only itself
1 file · 0 imports 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
4 files · 0 problems
no-react-in-shared archangel.config.ts:16
because: Shared code also runs on the server.
files in shared may not import react
1 file · 0 imports judged · 0 problems · 0 exceptions
Not judged by any rule: 0 files, 0 imports between repository files.
Outside packages that no rule names: none.

The last two lines are important. They list the files and imports that no rule judges, and the outside packages that no rule names. A file in that list can break your architecture, and no test fails.

To see why archangel refused an import, give the file and the line:

Terminal
$ npx archangel explain src/features/checkout/checkout.ts:1
src/features/checkout/checkout.ts:1 '../cart/cart' -> src/features/cart/cart.ts
file: part feature (instance checkout)
target: part feature (instance cart)
judged by:
app.feature refuses: another instance of feature is not in its row (feature may import only shared, and itself in the same instance)
skipped by:
app.shared the file is not in shared
app.every-file-has-a-kind this rule checks files, not imports
no-react-in-shared the file is not in shared