Guides: Write a plugin: Write your own condition in TypeScript, use it in a rule, prove it with examples, and publish it. # Write a plugin > Write your own condition in TypeScript, use it in a rule, prove it with examples, and publish it. This guide writes a plugin with two conditions, uses them in rules, and proves them with examples. Use a plugin when the [built-in conditions](/archangel/reference/conditions/) cannot say what you want. ## Before you start * Read [Getting started](/archangel/start/getting-started/). * Your config is `archangel.config.ts`. A JSON config cannot load a plugin, because a plugin is code. ## How a plugin works 1. Archangel makes a map of your code. For each file, the map has its path, its part, its groups, what it imports, what imports it, and what it exports. 2. A rule names a condition and some files. 3. Archangel calls the condition’s `check` function with the part of the map that the rule’s files cover. `check` gives back a list of problems. The plugin runs in the Node process that loads your config, not in archangel’s own process. ## Write the plugin 1. Write the conditions in `plugins/team.ts`: plugins/team.ts ```ts import { defineCondition, definePlugin } from '@enhance-eng/archangel'; const maxImports = defineCondition({ name: 'max-imports', description: 'A file imports at most `max` other files.', reads: ['imports'], optionsSchema: { type: 'object', properties: { max: { type: 'integer', minimum: 1 } }, required: ['max'], }, check(view, options: { max: number }) { return view.files.flatMap((file) => { // A package is not another file of the repository, so only imports of files count. const files = file.imports.filter((each) => each.file !== null).length; return files > options.max ? [{ file: file.path, message: `${file.path} imports ${files} files, and the limit is ${options.max}` }] : []; }); }, }); const noOrphans = defineCondition({ name: 'no-orphans', description: 'Another file imports each file.', reads: ['importers'], check(view) { return view.files .filter((file) => file.importers.length === 0) .map((file) => ({ file: file.path, message: `no file imports ${file.path}; delete it or import it` })); }, }); export default definePlugin({ name: 'team', schemaVersion: 1, conditions: [maxImports, noOrphans] }); ``` A condition has these fields: | Field | What it is | | --------------- | ------------------------------------------------------------------------------------------------------------------ | | `name` | The name that a rule uses after the plugin name, for example `team/max-imports`. | | `description` | One sentence that says what the condition requires. | | `reads` | The facts that `check` needs: `'imports'`, `'importers'` or `'exports'`. Archangel sends only these. | | `optionsSchema` | A JSON Schema for the `options` of a rule. Archangel refuses a rule whose options do not match it. It is optional. | | `check` | The function. Archangel calls it as `check(view, options)`. | `max-imports` reads `imports`, and refuses a file that imports more than `max` other files. `no-orphans` reads `importers`, and refuses a file that no other file imports. 2. Load the plugin in the config, and use each condition in a rule: archangel.config.ts ```ts import { defineConfig } from '@enhance-eng/archangel'; import team from './plugins/team.ts'; export default defineConfig({ parts: { feature: 'src/features/{feature}/**', shared: 'src/shared/**', }, plugins: { team }, rules: ({ rule }) => [ rule('small-files', { files: 'feature', plugin: 'team/max-imports', options: { max: 2 }, because: 'A file with many imports does too many things.', examples: { accepted: [{ 'src/features/a/x.ts': "import './y';", 'src/features/a/y.ts': '' }], rejected: [{ 'src/features/a/x.ts': "import './y';\nimport './z';\nimport './w';", 'src/features/a/y.ts': '', 'src/features/a/z.ts': '', 'src/features/a/w.ts': '', }], }, }), rule('no-orphans', { files: 'shared', plugin: 'team/no-orphans', because: 'Shared code that nothing imports is dead code.', examples: { rejected: [{ 'src/shared/unused.ts': 'export const unused = 1;' }], }, }), ], }); ``` * `plugins: { team }` loads the plugin with the name `team`. * `plugin: 'team/max-imports'` names the condition. * `options` goes to `check` without changes. ## Prove and run the plugin 1. Run the examples: Terminal ```ansi $ npx archangel test pass small-files accepted #1 src/features/a/x.ts, src/features/a/y.ts pass small-files rejected #1 src/features/a/w.ts, src/features/a/x.ts, src/features/a/y.ts, src/features/a/z.ts pass no-orphans rejected #1 src/shared/unused.ts 3 examples, none failed; 0 rules without a rejected example ``` A one-line example does not work for a plugin condition, because archangel cannot know which text of a file the condition reads. Write the files of each example yourself, as in the config above. 2. Check the repository: Terminal ```ansi $ npx archangel check error[plugin]: src/features/cart/index.ts imports 4 files, and the limit is 2 --> src/features/cart/index.ts = note: rule: small-files = note: because: A file with many imports does too many things. error[plugin]: no file imports src/shared/dates.ts; delete it or import it --> src/shared/dates.ts = note: rule: no-orphans = note: because: Shared code that nothing imports is dead code. checked 8 files and 7 imports: 2 problems For what a code means, run `archangel docs plugin`. ``` `src/features/cart/index.ts` has four imports. `src/shared/dates.ts` is a file that nothing imports. 3. See what the rules judged: Terminal ```ansi $ npx archangel rules small-files archangel.config.ts:11 because: A file with many imports does too many things. files in feature must meet the plugin condition team/max-imports 4 files · 1 problem no-orphans archangel.config.ts:27 because: Shared code that nothing imports is dead code. files in shared must meet the plugin condition team/no-orphans 2 files · 1 problem Files that no rule judges: 1 plugins/team.ts Imports between repository files that no rule judges: 4 src/features/cart/index.ts:1 imports src/shared/money.ts src/features/cart/index.ts:2 imports src/features/cart/discount.ts src/features/cart/index.ts:3 imports src/features/cart/tax.ts src/features/cart/index.ts:4 imports src/features/cart/total.ts Outside packages that no rule names: @enhance-eng/archangel (1 import). ``` A plugin rule judges files, not imports. So `archangel rules` lists the imports of the cart as not judged. The plugin file itself is a source file, and no rule judges it. ## What `check` receives `view.files` has one entry for each file of the rule’s `files`, sorted by path. Each entry has these fields: | Field | What it is | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `path` | The path from the config folder. | | `element` | The part of the file, or `null`. | | `captures` | The values of the part’s captures, for example `{ "feature": "cart" }`. | | `selectors` | The groups that hold the file. | | `imports` | Only when the condition reads `'imports'`. Each import has `specifier`, `occurrence`, `line`, `column`, `kind` (`'runtime'` or `'type'`), `names`, `file` and `package`. | | `importers` | Only when the condition reads `'importers'`. The paths of the files that import this file. | | `exports` | Only when the condition reads `'exports'`. It has `names`, `default` and `star`. | The TypeScript types are in `@enhance-eng/archangel`: `View`, `FileRecord`, `ImportRecord`, `ExportsRecord` and `Violation`. [Plugin API](/archangel/reference/plugin-api/) lists each field. ## What a problem can say A problem names a file of the view, or a folder that holds a file of the view: ```ts { file: 'src/features/cart/index.ts', message: 'src/features/cart/index.ts imports 4 files, and the limit is 2' } ``` A problem can also name an import. Then an exception comment above that import can permit it: ```ts { file: 'src/a.ts', import: { specifier: './b', occurrence: 0 }, message: 'a.ts may not import b.ts' } ``` `check` can also give back a promise of a list. ## When a plugin fails A plugin rule never passes because its code did not run. Archangel shows `plugin-failed`, and the check fails, when: * a condition throws an error; * a condition reports a file that is not in its view; * the plugin process stops. Each line that a plugin prints goes to standard error, or to the editor’s log, with the name of the plugin. In the editor The language server shows archangel’s own problems at once. It shows the problems of a plugin when the plugin answers. ## Publish a plugin * Name the package `archangel-plugin-` or `@scope/archangel-plugin`. * A new field in the view does not change `schemaVersion`. A plugin reads only the fields that it knows, so a new release of archangel does not break it. * To give a set of rules with the plugin, publish a preset next to it. A preset is a function that returns part of a config. Read [Presets](/archangel/reference/presets/). ## Next steps * [Plugin API](/archangel/reference/plugin-api/). * [Migrate from dependency-cruiser](/archangel/guides/from-dependency-cruiser/). The condition `no-orphans` on this page does the work of the dependency-cruiser rule of the same name.