@enhance-eng/archangel 0.1.1 → 0.3.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."
@@ -504,9 +695,12 @@
504
695
  },
505
696
  {
506
697
  "$ref": "#/definitions/namePattern"
698
+ },
699
+ {
700
+ "$ref": "#/definitions/nameList"
507
701
  }
508
702
  ],
509
- "description": "A style that the name, up to the first dot, must be in, or a name pattern that the full name, without the last extension, must match."
703
+ "description": "A style that the name, up to the first dot, must be in, or a name pattern that the full name, without the last extension, must match, or a list of them, of which one must match."
510
704
  }
511
705
  }
512
706
  },
@@ -544,6 +738,10 @@
544
738
  "type": "string",
545
739
  "description": "Why the rule exists, shown with each violation."
546
740
  },
741
+ "because": {
742
+ "type": "string",
743
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
744
+ },
547
745
  "link": {
548
746
  "type": "string",
549
747
  "description": "Where to read more, shown with each violation."
@@ -558,9 +756,12 @@
558
756
  },
559
757
  {
560
758
  "$ref": "#/definitions/namePattern"
759
+ },
760
+ {
761
+ "$ref": "#/definitions/nameList"
561
762
  }
562
763
  ],
563
- "description": "A style that each folder's name must be in, or a name pattern that it must match, which may use `{FolderName}` for the folder that holds it."
764
+ "description": "A style that each folder's name must be in, or a name pattern that it must match, which may use `{FolderName}` for the folder that holds it, or a list of them, of which one must match."
564
765
  }
565
766
  }
566
767
  },
@@ -585,6 +786,10 @@
585
786
  "type": "string",
586
787
  "description": "Why the rule exists, shown with each violation."
587
788
  },
789
+ "because": {
790
+ "type": "string",
791
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
792
+ },
588
793
  "link": {
589
794
  "type": "string",
590
795
  "description": "Where to read more, shown with each violation."
@@ -645,6 +850,10 @@
645
850
  "type": "string",
646
851
  "description": "Why the rule exists, shown with each violation."
647
852
  },
853
+ "because": {
854
+ "type": "string",
855
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
856
+ },
648
857
  "link": {
649
858
  "type": "string",
650
859
  "description": "Where to read more, shown with each violation."
@@ -692,6 +901,10 @@
692
901
  "type": "string",
693
902
  "description": "Why the rule exists, shown with each violation."
694
903
  },
904
+ "because": {
905
+ "type": "string",
906
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
907
+ },
695
908
  "link": {
696
909
  "type": "string",
697
910
  "description": "Where to read more, shown with each violation."
@@ -739,6 +952,10 @@
739
952
  "type": "string",
740
953
  "description": "Why the rule exists, shown with each violation."
741
954
  },
955
+ "because": {
956
+ "type": "string",
957
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
958
+ },
742
959
  "link": {
743
960
  "type": "string",
744
961
  "description": "Where to read more, shown with each violation."
@@ -773,6 +990,10 @@
773
990
  "type": "string",
774
991
  "description": "Why the rule exists, shown with each violation."
775
992
  },
993
+ "because": {
994
+ "type": "string",
995
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
996
+ },
776
997
  "link": {
777
998
  "type": "string",
778
999
  "description": "Where to read more, shown with each violation."
@@ -807,6 +1028,10 @@
807
1028
  "type": "string",
808
1029
  "description": "Why the rule exists, shown with each violation."
809
1030
  },
1031
+ "because": {
1032
+ "type": "string",
1033
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
1034
+ },
810
1035
  "link": {
811
1036
  "type": "string",
812
1037
  "description": "Where to read more, shown with each violation."
@@ -851,6 +1076,10 @@
851
1076
  "type": "string",
852
1077
  "description": "Why the rule exists, shown with each violation."
853
1078
  },
1079
+ "because": {
1080
+ "type": "string",
1081
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
1082
+ },
854
1083
  "link": {
855
1084
  "type": "string",
856
1085
  "description": "Where to read more, shown with each violation."
@@ -885,6 +1114,10 @@
885
1114
  "type": "string",
886
1115
  "description": "Why the rule exists, shown with each violation."
887
1116
  },
1117
+ "because": {
1118
+ "type": "string",
1119
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
1120
+ },
888
1121
  "link": {
889
1122
  "type": "string",
890
1123
  "description": "Where to read more, shown with each violation."
@@ -923,6 +1156,10 @@
923
1156
  "type": "string",
924
1157
  "description": "Why the rule exists, shown with each violation."
925
1158
  },
1159
+ "because": {
1160
+ "type": "string",
1161
+ "description": "The same as `reason`: why the rule exists. A rule has one of the two."
1162
+ },
926
1163
  "link": {
927
1164
  "type": "string",
928
1165
  "description": "Where to read more, shown with each violation."
@@ -1000,13 +1237,29 @@
1000
1237
  "accepted": {
1001
1238
  "type": "array",
1002
1239
  "items": {
1003
- "$ref": "#/definitions/exampleFiles"
1240
+ "anyOf": [
1241
+ {
1242
+ "type": "string",
1243
+ "description": "One line, such as `src/a.ts imports src/b.ts`, for which archangel writes the files."
1244
+ },
1245
+ {
1246
+ "$ref": "#/definitions/exampleFiles"
1247
+ }
1248
+ ]
1004
1249
  }
1005
1250
  },
1006
1251
  "rejected": {
1007
1252
  "type": "array",
1008
1253
  "items": {
1009
- "$ref": "#/definitions/exampleFiles"
1254
+ "anyOf": [
1255
+ {
1256
+ "type": "string",
1257
+ "description": "One line, such as `src/a.ts imports src/b.ts`, for which archangel writes the files."
1258
+ },
1259
+ {
1260
+ "$ref": "#/definitions/exampleFiles"
1261
+ }
1262
+ ]
1010
1263
  }
1011
1264
  }
1012
1265
  }
@@ -1186,7 +1439,51 @@
1186
1439
  "namePattern": {
1187
1440
  "type": "string",
1188
1441
  "minLength": 1,
1189
- "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."
1442
+ "description": "A name: text, `*` for any text or no 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."
1443
+ },
1444
+ "nameList": {
1445
+ "type": "array",
1446
+ "minItems": 1,
1447
+ "items": {
1448
+ "anyOf": [
1449
+ {
1450
+ "$ref": "#/definitions/style"
1451
+ },
1452
+ {
1453
+ "$ref": "#/definitions/namePattern"
1454
+ }
1455
+ ]
1456
+ },
1457
+ "description": "The names that a `mustBeNamed` rule permits. One entry must match."
1458
+ }
1459
+ },
1460
+ "anyOf": [
1461
+ {
1462
+ "properties": {
1463
+ "parts": true
1464
+ },
1465
+ "required": [
1466
+ "parts"
1467
+ ]
1468
+ },
1469
+ {
1470
+ "properties": {
1471
+ "elements": true
1472
+ },
1473
+ "required": [
1474
+ "elements"
1475
+ ]
1476
+ },
1477
+ {
1478
+ "properties": {
1479
+ "rules": {
1480
+ "type": "array",
1481
+ "minItems": 1
1482
+ }
1483
+ },
1484
+ "required": [
1485
+ "rules"
1486
+ ]
1190
1487
  }
1191
- }
1488
+ ]
1192
1489
  }
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
  /**
@@ -153,12 +177,15 @@ export interface ImportLimits {
153
177
  export type NameStyle = 'kebab-case' | 'camelCase' | 'PascalCase' | 'snake_case' | 'SCREAMING_SNAKE_CASE';
154
178
 
155
179
  /**
156
- * A name: text, `*` for any text, and `{FileName}`, `{fileName}`, `{file-name}`, `{file_name}` or
180
+ * A name: text, `*` for any text or no text, and `{FileName}`, `{fileName}`, `{file-name}`, `{file_name}` or
157
181
  * `{FILE_NAME}` for the file's name in each style, or the same spellings of `{FolderName}` for the
158
182
  * name of the folder that holds it.
159
183
  */
160
184
  export type NamePattern = string;
161
185
 
186
+ /** One name that a `mustBeNamed` rule permits: a style or a name pattern. */
187
+ export type NameEntry = NameStyle | (NamePattern & {});
188
+
162
189
  type Conditions<Other> = {
163
190
  /** The files may not import the other side. */
164
191
  readonly mayNotImport: Other;
@@ -168,9 +195,9 @@ type Conditions<Other> = {
168
195
  readonly mayOnlyBeImportedBy: Other;
169
196
  /**
170
197
  * The name, up to the first dot, is in a style, or the full name, without the last extension,
171
- * matches a name pattern.
198
+ * matches a name pattern. In a list, one entry must match.
172
199
  */
173
- readonly mustBeNamed: NameStyle | (NamePattern & {});
200
+ readonly mustBeNamed: NameEntry | readonly [NameEntry, ...NameEntry[]];
174
201
  /**
175
202
  * Each instance of `files`, an element with a capture, has a file that matches the pattern, or
176
203
  * one of the patterns.
@@ -197,15 +224,21 @@ type NoLimits = { readonly [K in keyof ImportLimits]?: never };
197
224
 
198
225
  type NoChains = { readonly transitively?: false; readonly stopAt?: never };
199
226
 
227
+ type NoArea = { readonly within?: never };
228
+
229
+ /** The area whose imports an import condition judges: an import outside it passes the rule. */
230
+ type Area<E extends Elements, S extends Selectors> = { readonly within?: Side<E, S> };
231
+
200
232
  /** A rule written as a sentence: `files`, and exactly one condition. */
201
- export type Sentence<E extends Elements, S extends Selectors> = Omit<RuleOptions, 'imports'> & {
233
+ export type Sentence<E extends Elements, S extends Selectors> = Omit<CommonOptions, 'imports'> & Because & {
202
234
  readonly kind?: never;
203
235
  /** The files the rule is about. */
204
236
  readonly files: Files<E, S>;
205
237
  } & (
206
- | (Only<Side<E, S>, 'mayNotImport'> & ImportLimits & NoChains & Pick<RuleOptions, 'imports'>)
238
+ | (Only<Side<E, S>, 'mayNotImport'> & ImportLimits & NoChains & NoArea & Pick<CommonOptions, 'imports'>)
207
239
  | (Only<Side<E, S>, 'mayNotImport'> &
208
- NoLimits & {
240
+ NoLimits &
241
+ NoArea & {
209
242
  /** Follow chains of imports, not only direct ones. */
210
243
  readonly transitively: true;
211
244
  /** Files where a chain ends, such as `{ directive: 'use server' }`. */
@@ -213,24 +246,25 @@ export type Sentence<E extends Elements, S extends Selectors> = Omit<RuleOptions
213
246
  /** A chain of type imports never runs, so a transitive rule checks no type imports. */
214
247
  readonly imports?: 'all' | 'runtime';
215
248
  })
216
- | (Only<Side<E, S>, 'mayOnlyImport'> & ImportLimits & NoChains & Pick<RuleOptions, 'imports'>)
249
+ | (Only<Side<E, S>, 'mayOnlyImport'> & ImportLimits & NoChains & Area<E, S> & Pick<CommonOptions, 'imports'>)
217
250
  | (Only<Side<E, S>, 'mayOnlyBeImportedBy'> &
218
251
  ImportLimits &
219
252
  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'>)
253
+ Area<E, S> &
254
+ Pick<CommonOptions, 'imports'>)
255
+ | (Only<Side<E, S>, 'mustBeNamed'> & NoLimits & NoChains & NoArea & NoImports)
256
+ | (Only<Side<E, S>, 'mustContain'> & NoLimits & NoChains & NoArea & NoImports)
257
+ | (Only<Side<E, S>, 'mustNotExist'> & NoLimits & NoChains & NoArea & NoImports)
258
+ | (Only<Side<E, S>, 'mustExport'> & NoLimits & NoChains & NoArea & NoImports)
259
+ | (Only<Side<E, S>, 'mayNotExport'> & NoLimits & NoChains & NoArea & NoImports)
260
+ | (Only<Side<E, S>, 'mustOnlyReExport'> & NoLimits & NoChains & NoArea & NoImports)
261
+ | (Only<Side<E, S>, 'noCycles'> & NoLimits & NoChains & NoArea & Pick<CommonOptions, 'imports'>)
228
262
  );
229
263
 
230
264
  type NoImports = { readonly imports?: never };
231
265
 
232
266
  /** A rule about folders: `folders`, and `mustBeNamed` or `mustContain`. */
233
- export type FolderSentence = Omit<RuleOptions, 'imports'> & {
267
+ export type FolderSentence = Omit<CommonOptions, 'imports'> & Because & {
234
268
  readonly kind?: never;
235
269
  readonly files?: never;
236
270
  /**
@@ -241,6 +275,7 @@ export type FolderSentence = Omit<RuleOptions, 'imports'> & {
241
275
  readonly folders: string | readonly string[];
242
276
  } & NoLimits &
243
277
  NoChains &
278
+ NoArea &
244
279
  NoImports &
245
280
  (
246
281
  | (Only<never, 'mustBeNamed'>)
@@ -249,7 +284,7 @@ export type FolderSentence = Omit<RuleOptions, 'imports'> & {
249
284
  );
250
285
 
251
286
  /** 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;
287
+ type InnerSentence<T> = T extends unknown ? Omit<T, 'name' | 'reason' | 'because' | 'link' | 'examples' | 'imports'> : never;
253
288
 
254
289
  /** A shorthand as one sentence of a rule of several sentences. */
255
290
  type InnerShorthand<E extends Elements> =
@@ -258,7 +293,7 @@ type InnerShorthand<E extends Elements> =
258
293
  | { readonly kind: 'entry'; readonly element: NoInfer<ElementName<E>>; readonly files: readonly string[] };
259
294
 
260
295
  /** 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'> & {
296
+ export type SeveralSentences<E extends Elements, S extends Selectors> = Omit<CommonOptions, 'name'> & Because & {
262
297
  readonly kind?: never;
263
298
  readonly files?: never;
264
299
  /** The rule's name, for exceptions and the baseline. */
@@ -279,21 +314,95 @@ export type ConditionName<P extends Plugins> = {
279
314
  : never;
280
315
  }[Extract<keyof P, string>];
281
316
 
317
+ /** The conditions of the `fsd` preset, which a config that extends it can use with its own files. */
318
+ export type FsdConditionName = `fsd/${
319
+ | 'typo-in-layer-name'
320
+ | 'ambiguous-slice-names'
321
+ | 'inconsistent-naming'
322
+ | 'insignificant-slice'
323
+ | 'repetitive-naming'
324
+ | 'excessive-slicing'
325
+ | 'shared-lib-grouping'}`;
326
+
282
327
  /** A rule that uses a plugin's condition, whose options the condition's schema checks. */
283
328
  export type PluginRule<E extends Elements, S extends Selectors, P extends Plugins> = Omit<
284
- RuleOptions,
329
+ CommonOptions,
285
330
  'imports'
286
- > & {
331
+ > & Because & NoArea & {
287
332
  readonly kind?: never;
288
333
  /** The files whose records the condition sees. */
289
334
  readonly files: Files<E, S>;
290
335
  /** The condition, as `<plugin>/<condition>`. */
291
- readonly plugin: NoInfer<ConditionName<P>>;
336
+ readonly plugin: NoInfer<ConditionName<P>> | FsdConditionName;
292
337
  /** The options, which the condition's JSON Schema checks when the config loads. */
293
338
  readonly options?: unknown;
294
339
  };
295
340
 
296
- /** A rule, as an object: one of the three shorthands, a sentence, or a plugin rule. */
341
+ /** A word for a table or a row: any text with no colon and no slash. */
342
+ type NoSeparator<T extends string> = T extends `${string}:${string}` | `${string}/${string}`
343
+ ? 'the name of a table or a row has no colon or slash'
344
+ : T;
345
+
346
+ /** The name of a table. */
347
+ export type TableName<T extends string> = NoSeparator<T>;
348
+
349
+ /** What a table's rows are, before their names are checked. */
350
+ export type TableRows = Readonly<Record<string, readonly string[]>>;
351
+
352
+ /** What a cell may name: a row, a declared name, or a path pattern. */
353
+ type TableCell<Row extends string, Declared extends string> = Row | Declared | PathPattern;
354
+
355
+ /**
356
+ * The rows `R`, with each cell checked, and each row name checked against `Key`, the row names that
357
+ * the table permits.
358
+ */
359
+ type CheckedRows<R extends TableRows, Key extends string, Declared extends string> = {
360
+ readonly [Row in keyof R]: Row extends string
361
+ ? Row extends NoSeparator<Row>
362
+ ? Row extends Key
363
+ ? readonly TableCell<Extract<keyof R, string>, Declared>[]
364
+ : 'a row of a table without kinds is a declared part or group'
365
+ : NoSeparator<Row>
366
+ : never;
367
+ };
368
+
369
+ /** A name that a table can use as a row without kinds: a part or a group, not a set of packages. */
370
+ type RowName<E extends Elements, S extends Selectors> = FileElementName<E> | Extract<keyof S, string>;
371
+
372
+ /** Every name that a cell can use. */
373
+ type DeclaredName<E extends Elements, S extends Selectors> = ElementName<E> | Extract<keyof S, string>;
374
+
375
+ /** The options of a table with the rows `R`. */
376
+ export type TableOptions<E extends Elements, S extends Selectors, Row extends string> = Omit<CommonOptions, 'name'> &
377
+ Because &
378
+ ImportLimits & {
379
+ /** The files that the table judges as importers; without it, every file with a row. */
380
+ readonly files?: Files<E, S>;
381
+ /** The area whose imports the table judges, and without it, the whole repository. */
382
+ readonly within?: Side<E, S>;
383
+ /** Cells that every row has, such as a file that all code may use. */
384
+ readonly everyRowMayImport?: readonly TableCell<NoInfer<Row>, DeclaredName<E, S>>[];
385
+ /** Rows that may not import their own name, in the same instance or in another. */
386
+ readonly mayNotImportItself?: readonly NoInfer<Row>[];
387
+ };
388
+
389
+ /** A table, as an object. Archangel checks its names when the config loads. */
390
+ export type TableRule = RuleOptions &
391
+ ImportLimits & {
392
+ readonly kind: 'mayImport';
393
+ readonly name: string;
394
+ readonly rows: TableRows;
395
+ readonly kinds?: string;
396
+ readonly files?: unknown;
397
+ readonly within?: unknown;
398
+ readonly everyRowMayImport?: readonly string[];
399
+ readonly mayNotImportItself?: readonly string[];
400
+ };
401
+
402
+ /** A rule's body when its name comes first. */
403
+ type Body<T> = T extends unknown ? Omit<T, 'name'> & { readonly name?: never } : never;
404
+
405
+ /** A rule, as an object: one of the three shorthands, a table, a sentence, or a plugin rule. */
297
406
  export type Rule<E extends Elements, S extends Selectors = {}, P extends Plugins = {}> =
298
407
  | (RuleOptions & { readonly kind: 'layers'; readonly order: readonly NoInfer<ElementName<E>>[] })
299
408
  | (RuleOptions & { readonly kind: 'isolate'; readonly element: NoInfer<CapturedElement<E>> })
@@ -302,6 +411,7 @@ export type Rule<E extends Elements, S extends Selectors = {}, P extends Plugins
302
411
  readonly element: NoInfer<ElementName<E>>;
303
412
  readonly files: readonly string[];
304
413
  })
414
+ | TableRule
305
415
  | Sentence<E, S>
306
416
  | FolderSentence
307
417
  | SeveralSentences<E, S>
@@ -333,6 +443,26 @@ export interface RuleBuilders<
333
443
  files: string | readonly string[],
334
444
  options?: RuleOptions & Named<N, X>,
335
445
  ): Rule<E, S, P>;
446
+ /** A table of what each row may import, whose rows are kinds of file, parts or groups. */
447
+ mayImport<const T extends string, const R extends TableRows>(
448
+ name: TableName<T>,
449
+ rows: R & CheckedRows<R, string, DeclaredName<E, S>>,
450
+ options: TableOptions<E, S, Extract<keyof R, string>> & {
451
+ /** The pattern that gives a file its kind, such as `**\/*.{kind}.ts`. */
452
+ readonly kinds: string;
453
+ },
454
+ ): Rule<E, S, P>;
455
+ /** A table whose rows are all parts or groups. */
456
+ mayImport<const T extends string, const R extends TableRows>(
457
+ name: TableName<T>,
458
+ rows: R & CheckedRows<R, RowName<E, S>, DeclaredName<E, S>>,
459
+ options?: TableOptions<E, S, Extract<keyof R, string>> & { readonly kinds?: never },
460
+ ): Rule<E, S, P>;
461
+ /** A rule with its name first: `files`, or `folders`, and exactly one condition. */
462
+ rule<const N extends string = string>(
463
+ name: OwnRuleName<N, X>,
464
+ sentence: Body<Sentence<E, S> | FolderSentence | SeveralSentences<E, S> | PluginRule<E, S, P>>,
465
+ ): Rule<E, S, P>;
336
466
  /** A rule as a sentence: `files`, or `folders`, and exactly one condition or plugin condition. */
337
467
  rule<const N extends string = string>(
338
468
  sentence: (Sentence<E, S> | FolderSentence | SeveralSentences<E, S> | PluginRule<E, S, P>) &
@@ -351,8 +481,11 @@ export interface Preset<X extends string = string> {
351
481
  readonly package?: string;
352
482
  /** The folder the preset applies to, relative to the config folder. */
353
483
  readonly root?: string;
484
+ readonly parts?: Elements;
354
485
  readonly elements?: Elements;
486
+ readonly groups?: Selectors;
355
487
  readonly selectors?: Selectors;
488
+ readonly externals?: Externals;
356
489
  readonly plugins?: Plugins;
357
490
  /** The rules, each with a name. */
358
491
  readonly rules?: readonly unknown[] | ((builders: RuleBuilders<Elements, Selectors, Plugins>) => readonly unknown[]);
@@ -377,36 +510,65 @@ export declare function nextjs<const X extends string = 'nextjs'>(options?: {
377
510
  readonly root?: string;
378
511
  }): Preset<X>;
379
512
 
513
+ /** The parts of the code, by file pattern. A part holds files, so it has no packages. */
514
+ export type PartSpecs = Readonly<Record<string, string | readonly string[]>>;
515
+
516
+ /** One spelling of the parts of the code. */
517
+ type PartsKey<E extends Elements> =
518
+ | {
519
+ /** The parts of the code, by file pattern. Each file is in one part. */
520
+ readonly parts?: E & PartSpecs;
521
+ readonly elements?: never;
522
+ }
523
+ | {
524
+ /** The same as `parts`, which may also hold packages. */
525
+ readonly elements?: E;
526
+ readonly parts?: never;
527
+ };
528
+
529
+ /** One spelling of the named sets of files. */
530
+ type GroupsKey<S extends Selectors> =
531
+ | {
532
+ /** Named sets of files, which can cross parts, and which rules use by name. */
533
+ readonly groups?: S;
534
+ readonly selectors?: never;
535
+ }
536
+ | {
537
+ /** The same as `groups`. */
538
+ readonly selectors?: S;
539
+ readonly groups?: never;
540
+ };
541
+
380
542
  /**
381
- * An archangel config, which extends the presets named in `X` and names its own rules with the
382
- * names in `N`.
543
+ * An archangel config, which extends the presets named in `X`, names its own rules with the names
544
+ * in `N`, and names the sets of outside packages in `O`.
383
545
  */
384
- export interface Config<
546
+ export type Config<
385
547
  E extends Elements,
386
548
  S extends Selectors = {},
387
549
  P extends Plugins = {},
388
550
  X extends string = string,
389
551
  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
- }
552
+ O extends Externals = {},
553
+ > = PartsKey<E> &
554
+ GroupsKey<S> & {
555
+ /** Named sets of outside packages, such as `{ driver: ['pg', '@aws-sdk/*', 'node:fs'] }`. */
556
+ readonly externals?: O;
557
+ /** Lines in `.gitignore` syntax that remove files from the check. */
558
+ readonly ignore?: readonly string[];
559
+ /** Specifiers that may stay unresolved, each with the reason; a specifier may end in `*`. */
560
+ readonly allowUnresolved?: Readonly<Record<string, string>>;
561
+ /** The plugins, by the name the rules use them by, as `plugin: '<name>/<condition>'`. */
562
+ readonly plugins?: P;
563
+ /** The presets the config extends; the config's own rules replace preset rules of one name. */
564
+ readonly extends?: readonly Preset<X>[];
565
+ /** Rule or table names to turn off after the presets are merged, such as `fsd/insignificant-slice`. */
566
+ readonly off?: readonly string[];
567
+ /** The rules, or a function that builds them. */
568
+ readonly rules?:
569
+ | readonly (Rule<E & ExternalElements<O>, S, P> & Named<N, X>)[]
570
+ | ((builders: RuleBuilders<E & ExternalElements<O>, S, P, X>) => readonly (Rule<E & ExternalElements<O>, S, P> & Named<N, X>)[]);
571
+ };
410
572
 
411
573
  /** Declares an archangel config, with types that check its names as it is written. */
412
574
  export declare function defineConfig<
@@ -415,4 +577,5 @@ export declare function defineConfig<
415
577
  const P extends Plugins = {},
416
578
  const X extends string = never,
417
579
  const N extends string = string,
418
- >(config: Config<E, S, P, X, N>): Config<E, S, P>;
580
+ const O extends Externals = {},
581
+ >(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.3.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.3.0",
57
+ "@enhance-eng/archangel-darwin-x64": "0.3.0",
58
+ "@enhance-eng/archangel-linux-arm64-gnu": "0.3.0",
59
+ "@enhance-eng/archangel-linux-arm64-musl": "0.3.0",
60
+ "@enhance-eng/archangel-linux-x64-gnu": "0.3.0",
61
+ "@enhance-eng/archangel-linux-x64-musl": "0.3.0",
62
+ "@enhance-eng/archangel-win32-x64": "0.3.0"
63
+ },
64
+ "tsd": {
65
+ "compilerOptions": {
66
+ "paths": {
67
+ "@enhance-eng/archangel": [
68
+ "./index.d.ts"
69
+ ]
70
+ }
71
+ }
63
72
  }
64
73
  }