Write a plugin
Markdown for agentsThis guide writes a plugin with two conditions, uses them in rules, and proves them with examples. Use a plugin when the built-in conditions cannot say what you want.
Before you start
Section titled “Before you start”- Read Getting started.
- Your config is
archangel.config.ts. A JSON config cannot load a plugin, because a plugin is code.
How a plugin works
Section titled “How a plugin works”- 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.
- A rule names a condition and some files.
- Archangel calls the condition’s
checkfunction with the part of the map that the rule’s files cover.checkgives 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
Section titled “Write the plugin”-
Write the conditions in
plugins/team.ts:plugins/team.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 nameThe name that a rule uses after the plugin name, for example team/max-imports.descriptionOne sentence that says what the condition requires. readsThe facts that checkneeds:'imports','importers'or'exports'. Archangel sends only these.optionsSchemaA JSON Schema for the optionsof a rule. Archangel refuses a rule whose options do not match it. It is optional.checkThe function. Archangel calls it as check(view, options).max-importsreadsimports, and refuses a file that imports more thanmaxother files.no-orphansreadsimporters, and refuses a file that no other file imports. -
Load the plugin in the config, and use each condition in a rule:
archangel.config.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 nameteam.plugin: 'team/max-imports'names the condition.optionsgoes tocheckwithout changes.
Prove and run the plugin
Section titled “Prove and run the plugin”-
Run the examples:
Terminal $ npx archangel testpass small-files accepted #1 src/features/a/x.ts, src/features/a/y.tspass small-files rejected #1 src/features/a/w.ts, src/features/a/x.ts, src/features/a/y.ts, src/features/a/z.tspass no-orphans rejected #1 src/shared/unused.ts3 examples, none failed; 0 rules without a rejected exampleA 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.
-
Check the repository:
Terminal $ npx archangel checkerror[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 problemsFor what a code means, run `archangel docs plugin`.src/features/cart/index.tshas four imports.src/shared/dates.tsis a file that nothing imports. -
See what the rules judged:
Terminal $ npx archangel rulessmall-files archangel.config.ts:11because: A file with many imports does too many things.files in feature must meet the plugin condition team/max-imports4 files · 1 problemno-orphans archangel.config.ts:27because: Shared code that nothing imports is dead code.files in shared must meet the plugin condition team/no-orphans2 files · 1 problemFiles that no rule judges: 1plugins/team.tsImports between repository files that no rule judges: 4src/features/cart/index.ts:1 imports src/shared/money.tssrc/features/cart/index.ts:2 imports src/features/cart/discount.tssrc/features/cart/index.ts:3 imports src/features/cart/tax.tssrc/features/cart/index.ts:4 imports src/features/cart/total.tsOutside packages that no rule names: @enhance-eng/archangel (1 import).A plugin rule judges files, not imports. So
archangel ruleslists the imports of the cart as not judged. The plugin file itself is a source file, and no rule judges it.
What check receives
Section titled “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 lists each field.
What a problem can say
Section titled “What a problem can say”A problem names a file of the view, or a folder that holds a file of the view:
{ 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:
{ 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
Section titled “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.
Publish a plugin
Section titled “Publish a plugin”- Name the package
archangel-plugin-<name>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.
Next steps
Section titled “Next steps”- Plugin API.
- Migrate from dependency-cruiser. The condition
no-orphanson this page does the work of the dependency-cruiser rule of the same name.