@enhance-eng/archangel 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  ![Seconds to check the same 400 architecture rules on a 9,800-file TypeScript monorepo: archangel 1.9 s; dependency-cruiser, ESLint and Steiger together 67.7 s](https://raw.githubusercontent.com/enhance-labs-builds/archangel/main/docs/assets/speed-light.svg)
6
6
 
7
- Architecture tests for TypeScript, checked in milliseconds. You write down how your code is
8
- organized. Archangel tells you when the code breaks those rules, and what to do about it. It checks
7
+ Architecture tests for TypeScript, checked in milliseconds. You write a table of what each part of
8
+ your code may import. Archangel tells you when the code breaks the table, and what to do about it. It checks
9
9
  a large repository in about a second, so it can run in your editor as you type.
10
10
 
11
11
  ```sh
@@ -18,7 +18,7 @@ Archangel checks imports, import cycles, file and folder names, the files that f
18
18
  and exports. It has presets for Feature-Sliced Design and Next.js, and you can write your own
19
19
  conditions in TypeScript.
20
20
 
21
- - [Quick start](https://github.com/enhance-labs-builds/archangel/blob/main/README.md#quick-start)
21
+ - [Get started](https://github.com/enhance-labs-builds/archangel/blob/main/README.md#get-started)
22
22
  - [Rules](https://github.com/enhance-labs-builds/archangel/blob/main/docs/rules.md)
23
23
  - [Presets](https://github.com/enhance-labs-builds/archangel/blob/main/docs/presets.md)
24
24
  - [Plugins](https://github.com/enhance-labs-builds/archangel/blob/main/docs/plugins.md)
@@ -5,14 +5,32 @@
5
5
  "description": "The elements of a repository, named sets of its files, and the rules between them. Archangel checks the same fields, with every problem at once.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": [
9
- "elements"
10
- ],
11
8
  "properties": {
12
9
  "$schema": {
13
10
  "type": "string",
14
11
  "description": "This schema, for editors; archangel ignores it."
15
12
  },
13
+ "parts": {
14
+ "type": "object",
15
+ "description": "The parts of the code, by file pattern. A file is in one part at most.",
16
+ "minProperties": 1,
17
+ "additionalProperties": {
18
+ "oneOf": [
19
+ {
20
+ "type": "string",
21
+ "description": "A path pattern; `{name}` captures one instance, such as one feature."
22
+ },
23
+ {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "string"
27
+ },
28
+ "minItems": 1,
29
+ "description": "Several path patterns."
30
+ }
31
+ ]
32
+ }
33
+ },
16
34
  "elements": {
17
35
  "type": "object",
18
36
  "description": "The elements: parts of the code by file pattern, or packages, by name.",
@@ -51,6 +69,13 @@
51
69
  ]
52
70
  }
53
71
  },
72
+ "groups": {
73
+ "type": "object",
74
+ "description": "The same as `selectors`: named sets of files, which may overlap.",
75
+ "additionalProperties": {
76
+ "$ref": "#/definitions/selector"
77
+ }
78
+ },
54
79
  "selectors": {
55
80
  "type": "object",
56
81
  "description": "Named sets of files, which rules and other selectors use by name. A selector's name may not be an element's.",
@@ -58,6 +83,18 @@
58
83
  "$ref": "#/definitions/selector"
59
84
  }
60
85
  },
86
+ "externals": {
87
+ "type": "object",
88
+ "description": "Named lists of outside packages, such as `pg`, `@aws-sdk/*` or `node:fs`.",
89
+ "additionalProperties": {
90
+ "type": "array",
91
+ "items": {
92
+ "type": "string"
93
+ },
94
+ "minItems": 1,
95
+ "description": "Package names. A package is in every set that lists it."
96
+ }
97
+ },
61
98
  "rules": {
62
99
  "type": "array",
63
100
  "description": "The rules: a shorthand with `kind`, or a sentence of `files` and one condition.",
@@ -83,6 +120,10 @@
83
120
  "type": "string",
84
121
  "description": "Why the rule exists, shown with each violation."
85
122
  },
123
+ "because": {
124
+ "type": "string",
125
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
126
+ },
86
127
  "link": {
87
128
  "type": "string",
88
129
  "description": "Where to read more, shown with each violation."
@@ -129,6 +170,10 @@
129
170
  "type": "string",
130
171
  "description": "Why the rule exists, shown with each violation."
131
172
  },
173
+ "because": {
174
+ "type": "string",
175
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
176
+ },
132
177
  "link": {
133
178
  "type": "string",
134
179
  "description": "Where to read more, shown with each violation."
@@ -172,6 +217,10 @@
172
217
  "type": "string",
173
218
  "description": "Why the rule exists, shown with each violation."
174
219
  },
220
+ "because": {
221
+ "type": "string",
222
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
223
+ },
175
224
  "link": {
176
225
  "type": "string",
177
226
  "description": "Where to read more, shown with each violation."
@@ -203,6 +252,116 @@
203
252
  }
204
253
  }
205
254
  },
255
+ {
256
+ "type": "object",
257
+ "description": "An import table: for each row, what its files may import. Each row is a rule named `<table>.<row>`.",
258
+ "additionalProperties": false,
259
+ "required": [
260
+ "kind",
261
+ "name",
262
+ "rows"
263
+ ],
264
+ "properties": {
265
+ "kind": {
266
+ "const": "mayImport"
267
+ },
268
+ "name": {
269
+ "type": "string",
270
+ "description": "The table's name, which starts the name of each row. It has no colon and no slash."
271
+ },
272
+ "rows": {
273
+ "type": "object",
274
+ "description": "Each row: a part, a group or a kind, and what its files may import.",
275
+ "additionalProperties": {
276
+ "type": "array",
277
+ "items": {
278
+ "type": "string"
279
+ },
280
+ "description": "Names of rows, parts, groups and externals sets, or path patterns."
281
+ }
282
+ },
283
+ "files": {
284
+ "$ref": "#/definitions/selector",
285
+ "description": "The files whose imports the table judges. By default, the files of its rows."
286
+ },
287
+ "kinds": {
288
+ "type": "string",
289
+ "description": "The pattern that gives each file its kind, with the capture `{kind}`."
290
+ },
291
+ "within": {
292
+ "$ref": "#/definitions/side",
293
+ "description": "An area: the rows judge only imports whose target is in it."
294
+ },
295
+ "everyRowMayImport": {
296
+ "type": "array",
297
+ "items": {
298
+ "type": "string"
299
+ },
300
+ "description": "Cells that every row also has."
301
+ },
302
+ "mayNotImportItself": {
303
+ "type": "array",
304
+ "items": {
305
+ "type": "string"
306
+ },
307
+ "description": "The rows that do not permit their own name."
308
+ },
309
+ "because": {
310
+ "type": "string",
311
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
312
+ },
313
+ "reason": {
314
+ "type": "string",
315
+ "description": "Why the rule exists, shown with each violation."
316
+ },
317
+ "link": {
318
+ "type": "string",
319
+ "description": "Where to read more, shown with each violation."
320
+ },
321
+ "imports": {
322
+ "enum": [
323
+ "all",
324
+ "runtime",
325
+ "types"
326
+ ],
327
+ "description": "Which imports the rule checks: all of them, those that stay at runtime, or type imports."
328
+ },
329
+ "names": {
330
+ "type": "array",
331
+ "items": {
332
+ "type": "string"
333
+ },
334
+ "minItems": 1,
335
+ "description": "Only imports that bring in one of these names."
336
+ },
337
+ "specifier": {
338
+ "enum": [
339
+ "relative",
340
+ "parent",
341
+ "alias",
342
+ "package"
343
+ ],
344
+ "description": "Only imports whose specifier is written this way."
345
+ },
346
+ "dependency": {
347
+ "enum": [
348
+ "dev",
349
+ "undeclared"
350
+ ],
351
+ "description": "Only imports of packages that the nearest package.json lists this way."
352
+ },
353
+ "instance": {
354
+ "enum": [
355
+ "same",
356
+ "different"
357
+ ],
358
+ "description": "Only imports whose two ends compare this way, in the row's name."
359
+ },
360
+ "examples": {
361
+ "$ref": "#/definitions/examples"
362
+ }
363
+ }
364
+ },
206
365
  {
207
366
  "type": "object",
208
367
  "description": "The files may not import the other side.",
@@ -224,6 +383,10 @@
224
383
  "type": "string",
225
384
  "description": "Why the rule exists, shown with each violation."
226
385
  },
386
+ "because": {
387
+ "type": "string",
388
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
389
+ },
227
390
  "link": {
228
391
  "type": "string",
229
392
  "description": "Where to read more, shown with each violation."
@@ -342,6 +505,10 @@
342
505
  "type": "string",
343
506
  "description": "Why the rule exists, shown with each violation."
344
507
  },
508
+ "because": {
509
+ "type": "string",
510
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
511
+ },
345
512
  "link": {
346
513
  "type": "string",
347
514
  "description": "Where to read more, shown with each violation."
@@ -358,8 +525,16 @@
358
525
  "$ref": "#/definitions/examples"
359
526
  },
360
527
  "mayOnlyImport": {
361
- "$ref": "#/definitions/side",
362
- "description": "All that the files may import, besides each other."
528
+ "anyOf": [
529
+ {
530
+ "$ref": "#/definitions/side"
531
+ },
532
+ {
533
+ "type": "array",
534
+ "maxItems": 0
535
+ }
536
+ ],
537
+ "description": "All that the files may import, besides each other. An empty list needs `within`."
363
538
  },
364
539
  "names": {
365
540
  "type": "array",
@@ -392,6 +567,10 @@
392
567
  "undeclared"
393
568
  ],
394
569
  "description": "Only imports of packages that the importing file's nearest package.json lists only as development dependencies, or does not list at all."
570
+ },
571
+ "within": {
572
+ "$ref": "#/definitions/side",
573
+ "description": "The area: only imports whose other end is in it. On `mayOnlyImport` the target, and on `mayOnlyBeImportedBy` the importer."
395
574
  }
396
575
  }
397
576
  },
@@ -416,6 +595,10 @@
416
595
  "type": "string",
417
596
  "description": "Why the rule exists, shown with each violation."
418
597
  },
598
+ "because": {
599
+ "type": "string",
600
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
601
+ },
419
602
  "link": {
420
603
  "type": "string",
421
604
  "description": "Where to read more, shown with each violation."
@@ -466,6 +649,10 @@
466
649
  "undeclared"
467
650
  ],
468
651
  "description": "Only imports of packages that the importing file's nearest package.json lists only as development dependencies, or does not list at all."
652
+ },
653
+ "within": {
654
+ "$ref": "#/definitions/side",
655
+ "description": "The area: only imports whose other end is in it. On `mayOnlyImport` the target, and on `mayOnlyBeImportedBy` the importer."
469
656
  }
470
657
  }
471
658
  },
@@ -490,6 +677,10 @@
490
677
  "type": "string",
491
678
  "description": "Why the rule exists, shown with each violation."
492
679
  },
680
+ "because": {
681
+ "type": "string",
682
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
683
+ },
493
684
  "link": {
494
685
  "type": "string",
495
686
  "description": "Where to read more, shown with each violation."
@@ -544,6 +735,10 @@
544
735
  "type": "string",
545
736
  "description": "Why the rule exists, shown with each violation."
546
737
  },
738
+ "because": {
739
+ "type": "string",
740
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
741
+ },
547
742
  "link": {
548
743
  "type": "string",
549
744
  "description": "Where to read more, shown with each violation."
@@ -585,6 +780,10 @@
585
780
  "type": "string",
586
781
  "description": "Why the rule exists, shown with each violation."
587
782
  },
783
+ "because": {
784
+ "type": "string",
785
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
786
+ },
588
787
  "link": {
589
788
  "type": "string",
590
789
  "description": "Where to read more, shown with each violation."
@@ -645,6 +844,10 @@
645
844
  "type": "string",
646
845
  "description": "Why the rule exists, shown with each violation."
647
846
  },
847
+ "because": {
848
+ "type": "string",
849
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
850
+ },
648
851
  "link": {
649
852
  "type": "string",
650
853
  "description": "Where to read more, shown with each violation."
@@ -692,6 +895,10 @@
692
895
  "type": "string",
693
896
  "description": "Why the rule exists, shown with each violation."
694
897
  },
898
+ "because": {
899
+ "type": "string",
900
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
901
+ },
695
902
  "link": {
696
903
  "type": "string",
697
904
  "description": "Where to read more, shown with each violation."
@@ -739,6 +946,10 @@
739
946
  "type": "string",
740
947
  "description": "Why the rule exists, shown with each violation."
741
948
  },
949
+ "because": {
950
+ "type": "string",
951
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
952
+ },
742
953
  "link": {
743
954
  "type": "string",
744
955
  "description": "Where to read more, shown with each violation."
@@ -773,6 +984,10 @@
773
984
  "type": "string",
774
985
  "description": "Why the rule exists, shown with each violation."
775
986
  },
987
+ "because": {
988
+ "type": "string",
989
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
990
+ },
776
991
  "link": {
777
992
  "type": "string",
778
993
  "description": "Where to read more, shown with each violation."
@@ -807,6 +1022,10 @@
807
1022
  "type": "string",
808
1023
  "description": "Why the rule exists, shown with each violation."
809
1024
  },
1025
+ "because": {
1026
+ "type": "string",
1027
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
1028
+ },
810
1029
  "link": {
811
1030
  "type": "string",
812
1031
  "description": "Where to read more, shown with each violation."
@@ -851,6 +1070,10 @@
851
1070
  "type": "string",
852
1071
  "description": "Why the rule exists, shown with each violation."
853
1072
  },
1073
+ "because": {
1074
+ "type": "string",
1075
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
1076
+ },
854
1077
  "link": {
855
1078
  "type": "string",
856
1079
  "description": "Where to read more, shown with each violation."
@@ -885,6 +1108,10 @@
885
1108
  "type": "string",
886
1109
  "description": "Why the rule exists, shown with each violation."
887
1110
  },
1111
+ "because": {
1112
+ "type": "string",
1113
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
1114
+ },
888
1115
  "link": {
889
1116
  "type": "string",
890
1117
  "description": "Where to read more, shown with each violation."
@@ -923,6 +1150,10 @@
923
1150
  "type": "string",
924
1151
  "description": "Why the rule exists, shown with each violation."
925
1152
  },
1153
+ "because": {
1154
+ "type": "string",
1155
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
1156
+ },
926
1157
  "link": {
927
1158
  "type": "string",
928
1159
  "description": "Where to read more, shown with each violation."
@@ -1000,13 +1231,29 @@
1000
1231
  "accepted": {
1001
1232
  "type": "array",
1002
1233
  "items": {
1003
- "$ref": "#/definitions/exampleFiles"
1234
+ "anyOf": [
1235
+ {
1236
+ "type": "string",
1237
+ "description": "One line, such as `src/a.ts imports src/b.ts`, for which archangel writes the files."
1238
+ },
1239
+ {
1240
+ "$ref": "#/definitions/exampleFiles"
1241
+ }
1242
+ ]
1004
1243
  }
1005
1244
  },
1006
1245
  "rejected": {
1007
1246
  "type": "array",
1008
1247
  "items": {
1009
- "$ref": "#/definitions/exampleFiles"
1248
+ "anyOf": [
1249
+ {
1250
+ "type": "string",
1251
+ "description": "One line, such as `src/a.ts imports src/b.ts`, for which archangel writes the files."
1252
+ },
1253
+ {
1254
+ "$ref": "#/definitions/exampleFiles"
1255
+ }
1256
+ ]
1010
1257
  }
1011
1258
  }
1012
1259
  }
@@ -1188,5 +1435,34 @@
1188
1435
  "minLength": 1,
1189
1436
  "description": "A name: text, `*` for any text, and `{FileName}`, `{fileName}`, `{file-name}`, `{file_name}` or `{FILE_NAME}` for the file's name in each style, or the same spellings of `{FolderName}` for the name of the folder that holds it."
1190
1437
  }
1191
- }
1438
+ },
1439
+ "anyOf": [
1440
+ {
1441
+ "properties": {
1442
+ "parts": true
1443
+ },
1444
+ "required": [
1445
+ "parts"
1446
+ ]
1447
+ },
1448
+ {
1449
+ "properties": {
1450
+ "elements": true
1451
+ },
1452
+ "required": [
1453
+ "elements"
1454
+ ]
1455
+ },
1456
+ {
1457
+ "properties": {
1458
+ "rules": {
1459
+ "type": "array",
1460
+ "minItems": 1
1461
+ }
1462
+ },
1463
+ "required": [
1464
+ "rules"
1465
+ ]
1466
+ }
1467
+ ]
1192
1468
  }
package/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  // The types of an archangel config. They check, as the config is written, that every rule names a
2
- // declared element or selector, that a sentence has exactly one condition, and that an isolate
3
- // rule names an element with a capture such as `{feature}`. The names inside `selectors` are
4
- // left to archangel, because checking them here makes TypeScript infer mutable lists for them.
2
+ // declared part or group, that a sentence has exactly one condition, that a table's cells name
3
+ // something, and that an isolate rule names a part with a capture such as `{feature}`. The names
4
+ // inside `groups` are left to archangel, because checking them here makes TypeScript infer mutable
5
+ // lists for them. `parts` and `elements` are two spellings of one key, as are `groups` and
6
+ // `selectors`, and `because` and `reason`: a config uses one spelling of each.
5
7
 
6
8
  import type { AnyCondition, Plugin } from './plugin.js';
7
9
 
@@ -29,6 +31,14 @@ type Captures<S> = S extends string
29
31
  : false
30
32
  : false;
31
33
 
34
+ /** The named sets of outside packages. */
35
+ export type Externals = Readonly<Record<string, readonly string[]>>;
36
+
37
+ /** Each externals set, as the package element it reads as. */
38
+ type ExternalElements<X extends Externals> = {
39
+ readonly [K in keyof X]: { readonly packages: readonly string[] };
40
+ };
41
+
32
42
  /** The name of a declared element. */
33
43
  export type ElementName<E extends Elements> = Extract<keyof E, string>;
34
44
 
@@ -99,21 +109,35 @@ export type Side<E extends Elements, S extends Selectors> =
99
109
  /** Files by path, relative to the config folder, with their text. */
100
110
  export type ExampleFiles = Readonly<Record<string, string>>;
101
111
 
102
- /** What every rule may have. */
103
- export interface RuleOptions {
112
+ /** A one-line example, such as `'src/a.ts imports src/b.ts'`, or the files of an example. */
113
+ export type Example = string | ExampleFiles;
114
+
115
+ /** What the examples of a rule hold. */
116
+ export interface Examples {
117
+ /** Examples that the rule must accept. */
118
+ readonly accepted?: readonly Example[];
119
+ /** Examples that the rule must reject. */
120
+ readonly rejected?: readonly Example[];
121
+ }
122
+
123
+ /** Why a rule exists, shown with each violation. A rule has one of the two spellings. */
124
+ type Because =
125
+ | { readonly because?: string; readonly reason?: never }
126
+ | { readonly reason?: string; readonly because?: never };
127
+
128
+ /** What every rule may have, and its reason. */
129
+ export type RuleOptions = CommonOptions & Because;
130
+
131
+ /** What every rule may have, without its reason. */
132
+ export interface CommonOptions {
104
133
  /** The rule's name, for exceptions and the baseline; each rule gives a default. */
105
134
  readonly name?: string;
106
- /** Why the rule exists, shown with each violation. */
107
- readonly reason?: string;
108
135
  /** Where to read more, shown with each violation. */
109
136
  readonly link?: string;
110
137
  /** Which imports the rule checks: all of them, those that stay at runtime, or type imports. */
111
138
  readonly imports?: 'all' | 'runtime' | 'types';
112
- /** File sets the rule must accept and must reject, which `archangel test` checks. */
113
- readonly examples?: {
114
- readonly accepted?: readonly ExampleFiles[];
115
- readonly rejected?: readonly ExampleFiles[];
116
- };
139
+ /** Examples that the rule must accept and must reject, which `archangel test` checks. */
140
+ readonly examples?: Examples;
117
141
  }
118
142
 
119
143
  /**
@@ -197,15 +221,21 @@ type NoLimits = { readonly [K in keyof ImportLimits]?: never };
197
221
 
198
222
  type NoChains = { readonly transitively?: false; readonly stopAt?: never };
199
223
 
224
+ type NoArea = { readonly within?: never };
225
+
226
+ /** The area whose imports an import condition judges: an import outside it passes the rule. */
227
+ type Area<E extends Elements, S extends Selectors> = { readonly within?: Side<E, S> };
228
+
200
229
  /** A rule written as a sentence: `files`, and exactly one condition. */
201
- export type Sentence<E extends Elements, S extends Selectors> = Omit<RuleOptions, 'imports'> & {
230
+ export type Sentence<E extends Elements, S extends Selectors> = Omit<CommonOptions, 'imports'> & Because & {
202
231
  readonly kind?: never;
203
232
  /** The files the rule is about. */
204
233
  readonly files: Files<E, S>;
205
234
  } & (
206
- | (Only<Side<E, S>, 'mayNotImport'> & ImportLimits & NoChains & Pick<RuleOptions, 'imports'>)
235
+ | (Only<Side<E, S>, 'mayNotImport'> & ImportLimits & NoChains & NoArea & Pick<CommonOptions, 'imports'>)
207
236
  | (Only<Side<E, S>, 'mayNotImport'> &
208
- NoLimits & {
237
+ NoLimits &
238
+ NoArea & {
209
239
  /** Follow chains of imports, not only direct ones. */
210
240
  readonly transitively: true;
211
241
  /** Files where a chain ends, such as `{ directive: 'use server' }`. */
@@ -213,24 +243,25 @@ export type Sentence<E extends Elements, S extends Selectors> = Omit<RuleOptions
213
243
  /** A chain of type imports never runs, so a transitive rule checks no type imports. */
214
244
  readonly imports?: 'all' | 'runtime';
215
245
  })
216
- | (Only<Side<E, S>, 'mayOnlyImport'> & ImportLimits & NoChains & Pick<RuleOptions, 'imports'>)
246
+ | (Only<Side<E, S>, 'mayOnlyImport'> & ImportLimits & NoChains & Area<E, S> & Pick<CommonOptions, 'imports'>)
217
247
  | (Only<Side<E, S>, 'mayOnlyBeImportedBy'> &
218
248
  ImportLimits &
219
249
  NoChains &
220
- Pick<RuleOptions, 'imports'>)
221
- | (Only<Side<E, S>, 'mustBeNamed'> & NoLimits & NoChains & NoImports)
222
- | (Only<Side<E, S>, 'mustContain'> & NoLimits & NoChains & NoImports)
223
- | (Only<Side<E, S>, 'mustNotExist'> & NoLimits & NoChains & NoImports)
224
- | (Only<Side<E, S>, 'mustExport'> & NoLimits & NoChains & NoImports)
225
- | (Only<Side<E, S>, 'mayNotExport'> & NoLimits & NoChains & NoImports)
226
- | (Only<Side<E, S>, 'mustOnlyReExport'> & NoLimits & NoChains & NoImports)
227
- | (Only<Side<E, S>, 'noCycles'> & NoLimits & NoChains & Pick<RuleOptions, 'imports'>)
250
+ Area<E, S> &
251
+ Pick<CommonOptions, 'imports'>)
252
+ | (Only<Side<E, S>, 'mustBeNamed'> & NoLimits & NoChains & NoArea & NoImports)
253
+ | (Only<Side<E, S>, 'mustContain'> & NoLimits & NoChains & NoArea & NoImports)
254
+ | (Only<Side<E, S>, 'mustNotExist'> & NoLimits & NoChains & NoArea & NoImports)
255
+ | (Only<Side<E, S>, 'mustExport'> & NoLimits & NoChains & NoArea & NoImports)
256
+ | (Only<Side<E, S>, 'mayNotExport'> & NoLimits & NoChains & NoArea & NoImports)
257
+ | (Only<Side<E, S>, 'mustOnlyReExport'> & NoLimits & NoChains & NoArea & NoImports)
258
+ | (Only<Side<E, S>, 'noCycles'> & NoLimits & NoChains & NoArea & Pick<CommonOptions, 'imports'>)
228
259
  );
229
260
 
230
261
  type NoImports = { readonly imports?: never };
231
262
 
232
263
  /** A rule about folders: `folders`, and `mustBeNamed` or `mustContain`. */
233
- export type FolderSentence = Omit<RuleOptions, 'imports'> & {
264
+ export type FolderSentence = Omit<CommonOptions, 'imports'> & Because & {
234
265
  readonly kind?: never;
235
266
  readonly files?: never;
236
267
  /**
@@ -241,6 +272,7 @@ export type FolderSentence = Omit<RuleOptions, 'imports'> & {
241
272
  readonly folders: string | readonly string[];
242
273
  } & NoLimits &
243
274
  NoChains &
275
+ NoArea &
244
276
  NoImports &
245
277
  (
246
278
  | (Only<never, 'mustBeNamed'>)
@@ -249,7 +281,7 @@ export type FolderSentence = Omit<RuleOptions, 'imports'> & {
249
281
  );
250
282
 
251
283
  /** A sentence of a rule of several sentences: the rule holds its name, reason and examples. */
252
- type InnerSentence<T> = T extends unknown ? Omit<T, 'name' | 'reason' | 'link' | 'examples' | 'imports'> : never;
284
+ type InnerSentence<T> = T extends unknown ? Omit<T, 'name' | 'reason' | 'because' | 'link' | 'examples' | 'imports'> : never;
253
285
 
254
286
  /** A shorthand as one sentence of a rule of several sentences. */
255
287
  type InnerShorthand<E extends Elements> =
@@ -258,7 +290,7 @@ type InnerShorthand<E extends Elements> =
258
290
  | { readonly kind: 'entry'; readonly element: NoInfer<ElementName<E>>; readonly files: readonly string[] };
259
291
 
260
292
  /** A rule of several sentences, broken when any of them is, which needs a name. */
261
- export type SeveralSentences<E extends Elements, S extends Selectors> = Omit<RuleOptions, 'name'> & {
293
+ export type SeveralSentences<E extends Elements, S extends Selectors> = Omit<CommonOptions, 'name'> & Because & {
262
294
  readonly kind?: never;
263
295
  readonly files?: never;
264
296
  /** The rule's name, for exceptions and the baseline. */
@@ -281,9 +313,9 @@ export type ConditionName<P extends Plugins> = {
281
313
 
282
314
  /** A rule that uses a plugin's condition, whose options the condition's schema checks. */
283
315
  export type PluginRule<E extends Elements, S extends Selectors, P extends Plugins> = Omit<
284
- RuleOptions,
316
+ CommonOptions,
285
317
  'imports'
286
- > & {
318
+ > & Because & NoArea & {
287
319
  readonly kind?: never;
288
320
  /** The files whose records the condition sees. */
289
321
  readonly files: Files<E, S>;
@@ -293,7 +325,71 @@ export type PluginRule<E extends Elements, S extends Selectors, P extends Plugin
293
325
  readonly options?: unknown;
294
326
  };
295
327
 
296
- /** A rule, as an object: one of the three shorthands, a sentence, or a plugin rule. */
328
+ /** A word for a table or a row: any text with no colon and no slash. */
329
+ type NoSeparator<T extends string> = T extends `${string}:${string}` | `${string}/${string}`
330
+ ? 'the name of a table or a row has no colon or slash'
331
+ : T;
332
+
333
+ /** The name of a table. */
334
+ export type TableName<T extends string> = NoSeparator<T>;
335
+
336
+ /** What a table's rows are, before their names are checked. */
337
+ export type TableRows = Readonly<Record<string, readonly string[]>>;
338
+
339
+ /** What a cell may name: a row, a declared name, or a path pattern. */
340
+ type TableCell<Row extends string, Declared extends string> = Row | Declared | PathPattern;
341
+
342
+ /**
343
+ * The rows `R`, with each cell checked, and each row name checked against `Key`, the row names that
344
+ * the table permits.
345
+ */
346
+ type CheckedRows<R extends TableRows, Key extends string, Declared extends string> = {
347
+ readonly [Row in keyof R]: Row extends string
348
+ ? Row extends NoSeparator<Row>
349
+ ? Row extends Key
350
+ ? readonly TableCell<Extract<keyof R, string>, Declared>[]
351
+ : 'a row of a table without kinds is a declared part or group'
352
+ : NoSeparator<Row>
353
+ : never;
354
+ };
355
+
356
+ /** A name that a table can use as a row without kinds: a part or a group, not a set of packages. */
357
+ type RowName<E extends Elements, S extends Selectors> = FileElementName<E> | Extract<keyof S, string>;
358
+
359
+ /** Every name that a cell can use. */
360
+ type DeclaredName<E extends Elements, S extends Selectors> = ElementName<E> | Extract<keyof S, string>;
361
+
362
+ /** The options of a table with the rows `R`. */
363
+ export type TableOptions<E extends Elements, S extends Selectors, Row extends string> = Omit<CommonOptions, 'name'> &
364
+ Because &
365
+ ImportLimits & {
366
+ /** The files that the table judges as importers; without it, every file with a row. */
367
+ readonly files?: Files<E, S>;
368
+ /** The area whose imports the table judges, and without it, the whole repository. */
369
+ readonly within?: Side<E, S>;
370
+ /** Cells that every row has, such as a file that all code may use. */
371
+ readonly everyRowMayImport?: readonly TableCell<NoInfer<Row>, DeclaredName<E, S>>[];
372
+ /** Rows that may not import their own name, in the same instance or in another. */
373
+ readonly mayNotImportItself?: readonly NoInfer<Row>[];
374
+ };
375
+
376
+ /** A table, as an object. Archangel checks its names when the config loads. */
377
+ export type TableRule = RuleOptions &
378
+ ImportLimits & {
379
+ readonly kind: 'mayImport';
380
+ readonly name: string;
381
+ readonly rows: TableRows;
382
+ readonly kinds?: string;
383
+ readonly files?: unknown;
384
+ readonly within?: unknown;
385
+ readonly everyRowMayImport?: readonly string[];
386
+ readonly mayNotImportItself?: readonly string[];
387
+ };
388
+
389
+ /** A rule's body when its name comes first. */
390
+ type Body<T> = T extends unknown ? Omit<T, 'name'> & { readonly name?: never } : never;
391
+
392
+ /** A rule, as an object: one of the three shorthands, a table, a sentence, or a plugin rule. */
297
393
  export type Rule<E extends Elements, S extends Selectors = {}, P extends Plugins = {}> =
298
394
  | (RuleOptions & { readonly kind: 'layers'; readonly order: readonly NoInfer<ElementName<E>>[] })
299
395
  | (RuleOptions & { readonly kind: 'isolate'; readonly element: NoInfer<CapturedElement<E>> })
@@ -302,6 +398,7 @@ export type Rule<E extends Elements, S extends Selectors = {}, P extends Plugins
302
398
  readonly element: NoInfer<ElementName<E>>;
303
399
  readonly files: readonly string[];
304
400
  })
401
+ | TableRule
305
402
  | Sentence<E, S>
306
403
  | FolderSentence
307
404
  | SeveralSentences<E, S>
@@ -333,6 +430,26 @@ export interface RuleBuilders<
333
430
  files: string | readonly string[],
334
431
  options?: RuleOptions & Named<N, X>,
335
432
  ): Rule<E, S, P>;
433
+ /** A table of what each row may import, whose rows are kinds of file, parts or groups. */
434
+ mayImport<const T extends string, const R extends TableRows>(
435
+ name: TableName<T>,
436
+ rows: R & CheckedRows<R, string, DeclaredName<E, S>>,
437
+ options: TableOptions<E, S, Extract<keyof R, string>> & {
438
+ /** The pattern that gives a file its kind, such as `**\/*.{kind}.ts`. */
439
+ readonly kinds: string;
440
+ },
441
+ ): Rule<E, S, P>;
442
+ /** A table whose rows are all parts or groups. */
443
+ mayImport<const T extends string, const R extends TableRows>(
444
+ name: TableName<T>,
445
+ rows: R & CheckedRows<R, RowName<E, S>, DeclaredName<E, S>>,
446
+ options?: TableOptions<E, S, Extract<keyof R, string>> & { readonly kinds?: never },
447
+ ): Rule<E, S, P>;
448
+ /** A rule with its name first: `files`, or `folders`, and exactly one condition. */
449
+ rule<const N extends string = string>(
450
+ name: OwnRuleName<N, X>,
451
+ sentence: Body<Sentence<E, S> | FolderSentence | SeveralSentences<E, S> | PluginRule<E, S, P>>,
452
+ ): Rule<E, S, P>;
336
453
  /** A rule as a sentence: `files`, or `folders`, and exactly one condition or plugin condition. */
337
454
  rule<const N extends string = string>(
338
455
  sentence: (Sentence<E, S> | FolderSentence | SeveralSentences<E, S> | PluginRule<E, S, P>) &
@@ -351,8 +468,11 @@ export interface Preset<X extends string = string> {
351
468
  readonly package?: string;
352
469
  /** The folder the preset applies to, relative to the config folder. */
353
470
  readonly root?: string;
471
+ readonly parts?: Elements;
354
472
  readonly elements?: Elements;
473
+ readonly groups?: Selectors;
355
474
  readonly selectors?: Selectors;
475
+ readonly externals?: Externals;
356
476
  readonly plugins?: Plugins;
357
477
  /** The rules, each with a name. */
358
478
  readonly rules?: readonly unknown[] | ((builders: RuleBuilders<Elements, Selectors, Plugins>) => readonly unknown[]);
@@ -377,36 +497,65 @@ export declare function nextjs<const X extends string = 'nextjs'>(options?: {
377
497
  readonly root?: string;
378
498
  }): Preset<X>;
379
499
 
500
+ /** The parts of the code, by file pattern. A part holds files, so it has no packages. */
501
+ export type PartSpecs = Readonly<Record<string, string | readonly string[]>>;
502
+
503
+ /** One spelling of the parts of the code. */
504
+ type PartsKey<E extends Elements> =
505
+ | {
506
+ /** The parts of the code, by file pattern. Each file is in one part. */
507
+ readonly parts?: E & PartSpecs;
508
+ readonly elements?: never;
509
+ }
510
+ | {
511
+ /** The same as `parts`, which may also hold packages. */
512
+ readonly elements?: E;
513
+ readonly parts?: never;
514
+ };
515
+
516
+ /** One spelling of the named sets of files. */
517
+ type GroupsKey<S extends Selectors> =
518
+ | {
519
+ /** Named sets of files, which can cross parts, and which rules use by name. */
520
+ readonly groups?: S;
521
+ readonly selectors?: never;
522
+ }
523
+ | {
524
+ /** The same as `groups`. */
525
+ readonly selectors?: S;
526
+ readonly groups?: never;
527
+ };
528
+
380
529
  /**
381
- * An archangel config, which extends the presets named in `X` and names its own rules with the
382
- * names in `N`.
530
+ * An archangel config, which extends the presets named in `X`, names its own rules with the names
531
+ * in `N`, and names the sets of outside packages in `O`.
383
532
  */
384
- export interface Config<
533
+ export type Config<
385
534
  E extends Elements,
386
535
  S extends Selectors = {},
387
536
  P extends Plugins = {},
388
537
  X extends string = string,
389
538
  N extends string = string,
390
- > {
391
- /** The elements: parts of the code by file pattern, or packages; a config that extends a preset may have none. */
392
- readonly elements?: E;
393
- /** Named sets of files, which rules and other selectors use by name. */
394
- readonly selectors?: S;
395
- /** Lines in `.gitignore` syntax that remove files from the check. */
396
- readonly ignore?: readonly string[];
397
- /** Specifiers that may stay unresolved, each with the reason; a specifier may end in `*`. */
398
- readonly allowUnresolved?: Readonly<Record<string, string>>;
399
- /** The plugins, by the name the rules use them by, as `plugin: '<name>/<condition>'`. */
400
- readonly plugins?: P;
401
- /** The presets the config extends; the config's own rules replace preset rules of one name. */
402
- readonly extends?: readonly Preset<X>[];
403
- /** Rule names to turn off after the presets are merged, such as `fsd/insignificant-slice`. */
404
- readonly off?: readonly string[];
405
- /** The rules, or a function that builds them. */
406
- readonly rules:
407
- | readonly (Rule<E, S, P> & Named<N, X>)[]
408
- | ((builders: RuleBuilders<E, S, P, X>) => readonly (Rule<E, S, P> & Named<N, X>)[]);
409
- }
539
+ O extends Externals = {},
540
+ > = PartsKey<E> &
541
+ GroupsKey<S> & {
542
+ /** Named sets of outside packages, such as `{ driver: ['pg', '@aws-sdk/*', 'node:fs'] }`. */
543
+ readonly externals?: O;
544
+ /** Lines in `.gitignore` syntax that remove files from the check. */
545
+ readonly ignore?: readonly string[];
546
+ /** Specifiers that may stay unresolved, each with the reason; a specifier may end in `*`. */
547
+ readonly allowUnresolved?: Readonly<Record<string, string>>;
548
+ /** The plugins, by the name the rules use them by, as `plugin: '<name>/<condition>'`. */
549
+ readonly plugins?: P;
550
+ /** The presets the config extends; the config's own rules replace preset rules of one name. */
551
+ readonly extends?: readonly Preset<X>[];
552
+ /** Rule or table names to turn off after the presets are merged, such as `fsd/insignificant-slice`. */
553
+ readonly off?: readonly string[];
554
+ /** The rules, or a function that builds them. */
555
+ readonly rules?:
556
+ | readonly (Rule<E & ExternalElements<O>, S, P> & Named<N, X>)[]
557
+ | ((builders: RuleBuilders<E & ExternalElements<O>, S, P, X>) => readonly (Rule<E & ExternalElements<O>, S, P> & Named<N, X>)[]);
558
+ };
410
559
 
411
560
  /** Declares an archangel config, with types that check its names as it is written. */
412
561
  export declare function defineConfig<
@@ -415,4 +564,5 @@ export declare function defineConfig<
415
564
  const P extends Plugins = {},
416
565
  const X extends string = never,
417
566
  const N extends string = string,
418
- >(config: Config<E, S, P, X, N>): Config<E, S, P>;
567
+ const O extends Externals = {},
568
+ >(config: Config<E, S, P, X, N, O>): Config<E, S, P, string, string, O>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enhance-eng/archangel",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Make architecture verifiable. Architecture tests for TypeScript, checked in milliseconds: imports, cycles, names, folder structure and exports.",
5
5
  "keywords": [
6
6
  "architecture",
@@ -53,12 +53,21 @@
53
53
  "pluralize": "8.0.0"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@enhance-eng/archangel-darwin-arm64": "0.1.1",
57
- "@enhance-eng/archangel-darwin-x64": "0.1.1",
58
- "@enhance-eng/archangel-linux-arm64-gnu": "0.1.1",
59
- "@enhance-eng/archangel-linux-arm64-musl": "0.1.1",
60
- "@enhance-eng/archangel-linux-x64-gnu": "0.1.1",
61
- "@enhance-eng/archangel-linux-x64-musl": "0.1.1",
62
- "@enhance-eng/archangel-win32-x64": "0.1.1"
56
+ "@enhance-eng/archangel-darwin-arm64": "0.2.0",
57
+ "@enhance-eng/archangel-darwin-x64": "0.2.0",
58
+ "@enhance-eng/archangel-linux-arm64-gnu": "0.2.0",
59
+ "@enhance-eng/archangel-linux-arm64-musl": "0.2.0",
60
+ "@enhance-eng/archangel-linux-x64-gnu": "0.2.0",
61
+ "@enhance-eng/archangel-linux-x64-musl": "0.2.0",
62
+ "@enhance-eng/archangel-win32-x64": "0.2.0"
63
+ },
64
+ "tsd": {
65
+ "compilerOptions": {
66
+ "paths": {
67
+ "@enhance-eng/archangel": [
68
+ "./index.d.ts"
69
+ ]
70
+ }
71
+ }
63
72
  }
64
73
  }