@intentset/architecture 0.8.1 → 0.9.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.
Files changed (56) hide show
  1. package/dist/areas.d.ts +1 -1
  2. package/dist/areas.d.ts.map +1 -1
  3. package/dist/areas.js +3 -0
  4. package/dist/areas.js.map +1 -1
  5. package/dist/baseline.d.ts +96 -11
  6. package/dist/baseline.d.ts.map +1 -1
  7. package/dist/baseline.js +219 -22
  8. package/dist/baseline.js.map +1 -1
  9. package/dist/boundaries.d.ts +11 -2
  10. package/dist/boundaries.d.ts.map +1 -1
  11. package/dist/boundaries.js +175 -17
  12. package/dist/boundaries.js.map +1 -1
  13. package/dist/check.d.ts +9 -2
  14. package/dist/check.d.ts.map +1 -1
  15. package/dist/check.js +21 -6
  16. package/dist/check.js.map +1 -1
  17. package/dist/config.d.ts +10 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +32 -1
  20. package/dist/config.js.map +1 -1
  21. package/dist/extract.d.ts +36 -1
  22. package/dist/extract.d.ts.map +1 -1
  23. package/dist/extract.js +142 -22
  24. package/dist/extract.js.map +1 -1
  25. package/dist/finding.d.ts +12 -0
  26. package/dist/finding.d.ts.map +1 -1
  27. package/dist/finding.js +6 -1
  28. package/dist/finding.js.map +1 -1
  29. package/dist/imports.d.ts +16 -0
  30. package/dist/imports.d.ts.map +1 -1
  31. package/dist/imports.js +17 -2
  32. package/dist/imports.js.map +1 -1
  33. package/dist/index.d.ts +8 -7
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +6 -5
  36. package/dist/index.js.map +1 -1
  37. package/dist/layers.d.ts +6 -0
  38. package/dist/layers.d.ts.map +1 -1
  39. package/dist/layers.js +19 -0
  40. package/dist/layers.js.map +1 -1
  41. package/dist/names.d.ts +48 -0
  42. package/dist/names.d.ts.map +1 -0
  43. package/dist/names.js +194 -0
  44. package/dist/names.js.map +1 -0
  45. package/package.json +2 -2
  46. package/src/areas.ts +3 -1
  47. package/src/baseline.ts +266 -24
  48. package/src/boundaries.ts +191 -22
  49. package/src/check.ts +34 -7
  50. package/src/config.ts +44 -1
  51. package/src/extract.ts +168 -22
  52. package/src/finding.ts +19 -1
  53. package/src/imports.ts +25 -3
  54. package/src/index.ts +16 -4
  55. package/src/layers.ts +18 -0
  56. package/src/names.ts +215 -0
package/src/extract.ts CHANGED
@@ -28,6 +28,10 @@
28
28
  * Anything this module cannot read is returned as a problem, never dropped:
29
29
  * a dynamic import with a computed specifier, or a token the scanner could
30
30
  * not terminate (invariant 6).
31
+ *
32
+ * Beside each reference it records the names the reference takes, and beside
33
+ * the file the names the file exports itself, so a name taken through an
34
+ * entrypoint or a barrel can be followed to the file that defines it (VSA006).
31
35
  */
32
36
  import { LanguageVariant, SyntaxKind } from "typescript/unstable/ast";
33
37
  import { computeLineStarts, createScanner, tokenIsIdentifierOrKeyword } from "typescript/unstable/ast/scanner";
@@ -44,6 +48,37 @@ export interface RawImport {
44
48
  reexport: boolean;
45
49
  /** `export * from "x"` or `export * as ns from "x"`. */
46
50
  wildcard: boolean;
51
+ /**
52
+ * The names it takes from the module, in source order. `name` is the name
53
+ * in that module (`default` for a default import), `as` the local binding
54
+ * or, for a re-export, the name exported here. A name of `*` takes every
55
+ * name the module exports: a namespace import, `import X = require("x")`,
56
+ * `export * as ns from "x"`, a dynamic import or require (with `as` empty),
57
+ * and `export * from "x"` (with `as` `*`, every name but `default` under
58
+ * its own). A side-effect import takes none.
59
+ */
60
+ names: TakenName[];
61
+ }
62
+
63
+ export interface TakenName {
64
+ name: string;
65
+ as: string;
66
+ /** `import type`, `export type`, or the specifier itself marked `type`. */
67
+ typeOnly: boolean;
68
+ }
69
+
70
+ /** What a file exports of its own, rather than from another module by `export ... from`. */
71
+ export interface LocalExports {
72
+ /** Names declared and exported here: `export function f`, `export const a`, `export default ...`. */
73
+ declared: string[];
74
+ /**
75
+ * `export { a as b }` with no module, and `export default a`: `name` is the
76
+ * local binding, `as` the exported name. Whether the binding was imported,
77
+ * which makes it a re-export, is for the reader of the file's imports.
78
+ */
79
+ bindings: TakenName[];
80
+ /** An export this reader cannot name: `export =`, `module.exports`, a destructured declaration. */
81
+ opaque: boolean;
47
82
  }
48
83
 
49
84
  export interface ExtractProblem {
@@ -54,6 +89,12 @@ export interface ExtractProblem {
54
89
  export interface Extraction {
55
90
  imports: RawImport[];
56
91
  problems: ExtractProblem[];
92
+ exports: LocalExports;
93
+ /**
94
+ * The line of the file's first JSX element or fragment, or null. Under the automatic runtime a file that renders
95
+ * needs no import, so this is the only sign of it (TS008).
96
+ */
97
+ jsx: number | null;
57
98
  }
58
99
 
59
100
  interface Token {
@@ -348,10 +389,14 @@ function describe(kind: SyntaxKind): string {
348
389
  }
349
390
  }
350
391
 
351
- /** Extract every module reference from one file. */
392
+ /** Words that open a declaration after `export default` rather than name a binding. */
393
+ const DECLARATION_WORDS = new Set(["function", "class", "async", "abstract", "interface", "enum", "namespace"]);
394
+
395
+ /** Extract every module reference from one file, with the names each takes and the names the file exports itself. */
352
396
  export function extractImports(path: string, text: string): Extraction {
353
397
  const { tokens, problems } = tokenize(path, text);
354
398
  const imports: RawImport[] = [];
399
+ const exports: LocalExports = { declared: [], bindings: [], opaque: false };
355
400
  const consumed = new Set<number>();
356
401
 
357
402
  const word = (i: number, text?: string): boolean => {
@@ -365,6 +410,8 @@ export function extractImports(path: string, text: string): Extraction {
365
410
  const add = (i: number, fields: Omit<RawImport, "specifier" | "line">) => {
366
411
  imports.push({ specifier: tokens[i].value, line: tokens[i].line, ...fields });
367
412
  };
413
+ /** A name as written in a specifier list: an identifier, a keyword such as `default`, or a string. */
414
+ const nameAt = (i: number): string | null => (word(i) ? tokens[i].text : isString(i) ? tokens[i].value : null);
368
415
  /** Index of the token closing the bracket opened at i, or tokens.length. */
369
416
  const closing = (i: number, open: SyntaxKind, close: SyntaxKind): number => {
370
417
  let depth = 0;
@@ -374,27 +421,82 @@ export function extractImports(path: string, text: string): Extraction {
374
421
  }
375
422
  return tokens.length;
376
423
  };
424
+ /**
425
+ * The specifiers between braces, `a`, `type B`, `c as d`, `"e-f" as g`:
426
+ * `name` is the first name, `as` the second or the first again.
427
+ */
428
+ const specifiers = (open: number, close: number, typeOnly: boolean): TakenName[] => {
429
+ const out: TakenName[] = [];
430
+ let k = open + 1;
431
+ while (k < close) {
432
+ let end = k;
433
+ while (end < close && !is(end, SyntaxKind.CommaToken)) end++;
434
+ let at = k;
435
+ let typed = typeOnly;
436
+ if (word(at, "type") && end - at > 1 && !(word(at + 1, "as") && end - at === 3)) {
437
+ typed = true;
438
+ at++;
439
+ }
440
+ const name = nameAt(at);
441
+ if (name !== null) {
442
+ const alias = word(at + 1, "as") && at + 2 < end ? nameAt(at + 2) : null;
443
+ out.push({ name, as: alias ?? name, typeOnly: typed });
444
+ }
445
+ k = end + 1;
446
+ }
447
+ return out;
448
+ };
377
449
  /** Named elements between braces: true when every one is written `type X`. */
378
450
  const allTyped = (open: number, close: number): boolean => {
379
- let count = 0;
380
- let typed = 0;
381
- let start = true;
382
- for (let k = open + 1; k < close; k++) {
383
- if (is(k, SyntaxKind.CommaToken)) {
384
- start = true;
385
- continue;
386
- }
387
- if (start) {
388
- count++;
389
- if (word(k, "type") && word(k + 1) && !word(k + 1, "as")) typed++;
390
- start = false;
451
+ const list = specifiers(open, close, false);
452
+ return list.length > 0 && list.every((item) => item.typeOnly);
453
+ };
454
+ /** The bindings of `import d, * as ns, { a } from`, written between `start` and the `from` at `end`. */
455
+ const importBindings = (start: number, end: number, typeOnly: boolean): TakenName[] => {
456
+ const out: TakenName[] = [];
457
+ for (let k = start; k < end; k++) {
458
+ if (is(k, SyntaxKind.OpenBraceToken)) {
459
+ const close = closing(k, SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken);
460
+ out.push(...specifiers(k, close, typeOnly));
461
+ k = close;
462
+ } else if (is(k, SyntaxKind.AsteriskToken) && word(k + 1, "as") && word(k + 2)) {
463
+ out.push({ name: "*", as: tokens[k + 2].text, typeOnly });
464
+ k += 2;
465
+ } else if (word(k)) {
466
+ out.push({ name: "default", as: tokens[k].text, typeOnly });
391
467
  }
392
468
  }
393
- return count > 0 && typed === count;
469
+ return out;
394
470
  };
395
471
  /** A call such as `import(x)` or `require(x)` that is really a method declaration `import(x) { ... }`. */
396
472
  const isMethod = (open: number): boolean =>
397
473
  is(closing(open, SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken) + 1, SyntaxKind.OpenBraceToken);
474
+ const every = (typeOnly: boolean): TakenName[] => [{ name: "*", as: "", typeOnly }];
475
+
476
+ /** `export <declaration>` with its name at i, after `export` and any `declare`. Returns false when it is not one. */
477
+ const declaration = (i: number): boolean => {
478
+ let k = i;
479
+ for (;;) {
480
+ if (word(k, "declare") || word(k, "async") || word(k, "abstract")) k++;
481
+ else if (word(k, "const") && word(k + 1, "enum")) k++;
482
+ else break;
483
+ }
484
+ if (word(k, "function")) {
485
+ k++;
486
+ if (is(k, SyntaxKind.AsteriskToken)) k++;
487
+ } else if (["class", "interface", "enum", "namespace", "module", "import"].some((kind) => word(k, kind))) k++;
488
+ else if (word(k, "type") && word(k + 1) && !is(k + 1, SyntaxKind.OpenBraceToken)) k++;
489
+ else if (word(k, "const") || word(k, "let") || word(k, "var") || word(k, "using")) {
490
+ k++;
491
+ if (is(k, SyntaxKind.OpenBraceToken) || is(k, SyntaxKind.OpenBracketToken)) {
492
+ exports.opaque = true;
493
+ return true;
494
+ }
495
+ } else return false;
496
+ if (!word(k)) return false;
497
+ exports.declared.push(tokens[k].text);
498
+ return true;
499
+ };
398
500
 
399
501
  for (let i = 0; i < tokens.length; i++) {
400
502
  if (consumed.has(i) || afterDot(i)) continue;
@@ -402,7 +504,8 @@ export function extractImports(path: string, text: string): Extraction {
402
504
  if (word(i, "import") && tokens[i].kind === SyntaxKind.ImportKeyword) {
403
505
  if (is(i + 1, SyntaxKind.OpenParenToken)) {
404
506
  if (isLiteral(i + 2) && (is(i + 3, SyntaxKind.CloseParenToken) || is(i + 3, SyntaxKind.CommaToken))) {
405
- add(i + 2, { typeOnly: word(i - 1, "typeof"), dynamic: true, reexport: false, wildcard: false });
507
+ const typeOnly = word(i - 1, "typeof");
508
+ add(i + 2, { typeOnly, dynamic: true, reexport: false, wildcard: false, names: every(typeOnly) });
406
509
  } else if (!is(i + 2, SyntaxKind.CloseParenToken) && !isMethod(i + 1)) {
407
510
  problems.push({
408
511
  line: tokens[i].line,
@@ -432,12 +535,13 @@ export function extractImports(path: string, text: string): Extraction {
432
535
  j++;
433
536
  }
434
537
  if (isString(j)) {
435
- add(j, { typeOnly, dynamic: false, reexport: false, wildcard: false });
538
+ add(j, { typeOnly, dynamic: false, reexport: false, wildcard: false, names: [] });
436
539
  continue;
437
540
  }
438
541
  if (word(j) && is(j + 1, SyntaxKind.EqualsToken)) {
439
542
  if (word(j + 2, "require") && is(j + 3, SyntaxKind.OpenParenToken) && isString(j + 4)) {
440
- add(j + 4, { typeOnly, dynamic: false, reexport: false, wildcard: false });
543
+ const names = [{ name: "*", as: tokens[j].text, typeOnly }];
544
+ add(j + 4, { typeOnly, dynamic: false, reexport: false, wildcard: false, names });
441
545
  consumed.add(j + 2);
442
546
  }
443
547
  continue;
@@ -454,7 +558,8 @@ export function extractImports(path: string, text: string): Extraction {
454
558
  }
455
559
  if (word(k, "from") && isString(k + 1)) {
456
560
  const typed = typeOnly || (typedNamed && !hasPlainBinding);
457
- add(k + 1, { typeOnly: typed, dynamic: false, reexport: false, wildcard: false });
561
+ const names = importBindings(j, k, typeOnly);
562
+ add(k + 1, { typeOnly: typed, dynamic: false, reexport: false, wildcard: false, names });
458
563
  i = k + 1;
459
564
  break;
460
565
  }
@@ -475,26 +580,62 @@ export function extractImports(path: string, text: string): Extraction {
475
580
  }
476
581
  if (is(j, SyntaxKind.AsteriskToken)) {
477
582
  let k = j + 1;
583
+ const as = word(k, "as") && nameAt(k + 1) !== null ? (nameAt(k + 1) as string) : "*";
478
584
  if (word(k, "as")) k += 2;
479
585
  if (word(k, "from") && isString(k + 1)) {
480
- add(k + 1, { typeOnly, dynamic: false, reexport: true, wildcard: true });
586
+ const names = [{ name: "*", as, typeOnly }];
587
+ add(k + 1, { typeOnly, dynamic: false, reexport: true, wildcard: true, names });
481
588
  i = k + 1;
482
589
  }
483
590
  continue;
484
591
  }
485
592
  if (is(j, SyntaxKind.OpenBraceToken)) {
486
593
  const close = closing(j, SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken);
594
+ const names = specifiers(j, close, typeOnly);
487
595
  if (word(close + 1, "from") && isString(close + 2)) {
488
- add(close + 2, { typeOnly: typeOnly || allTyped(j, close), dynamic: false, reexport: true, wildcard: false });
596
+ add(close + 2, {
597
+ typeOnly: typeOnly || allTyped(j, close),
598
+ dynamic: false,
599
+ reexport: true,
600
+ wildcard: false,
601
+ names,
602
+ });
489
603
  i = close + 2;
604
+ } else {
605
+ exports.bindings.push(...names);
606
+ i = close;
490
607
  }
608
+ continue;
609
+ }
610
+ if (is(j, SyntaxKind.EqualsToken)) {
611
+ exports.opaque = true;
612
+ continue;
613
+ }
614
+ if (word(j, "default")) {
615
+ // `export default name;` exports a binding, which may be an import; anything else is declared here.
616
+ const named =
617
+ word(j + 1) &&
618
+ !DECLARATION_WORDS.has(tokens[j + 1].text) &&
619
+ (j + 2 >= tokens.length || is(j + 2, SyntaxKind.SemicolonToken) || tokens[j + 2].line > tokens[j + 1].line);
620
+ if (named) exports.bindings.push({ name: tokens[j + 1].text, as: "default", typeOnly: false });
621
+ else exports.declared.push("default");
622
+ continue;
491
623
  }
624
+ if (!word(j, "as")) declaration(j);
625
+ continue;
626
+ }
627
+
628
+ if (
629
+ (word(i, "module") && is(i + 1, SyntaxKind.DotToken) && word(i + 2, "exports")) ||
630
+ (word(i, "exports") && (is(i + 1, SyntaxKind.DotToken) || is(i + 1, SyntaxKind.OpenBracketToken)))
631
+ ) {
632
+ exports.opaque = true;
492
633
  continue;
493
634
  }
494
635
 
495
636
  if (word(i, "require") && is(i + 1, SyntaxKind.OpenParenToken) && !word(i - 1, "function")) {
496
637
  if (isLiteral(i + 2) && is(i + 3, SyntaxKind.CloseParenToken)) {
497
- add(i + 2, { typeOnly: false, dynamic: true, reexport: false, wildcard: false });
638
+ add(i + 2, { typeOnly: false, dynamic: true, reexport: false, wildcard: false, names: every(false) });
498
639
  } else if (!is(i + 2, SyntaxKind.CloseParenToken) && !isMethod(i + 1)) {
499
640
  problems.push({
500
641
  line: tokens[i].line,
@@ -504,7 +645,12 @@ export function extractImports(path: string, text: string): Extraction {
504
645
  }
505
646
  }
506
647
 
507
- return { imports, problems };
648
+ // A nested element is pushed before the one around it, so the first is the lowest line, not the first token.
649
+ let jsx: number | null = null;
650
+ for (const token of tokens) {
651
+ if (token.kind === SyntaxKind.JsxElement && (jsx === null || token.line < jsx)) jsx = token.line;
652
+ }
653
+ return { imports, problems, exports, jsx };
508
654
  }
509
655
 
510
656
  /** What an Amplify schema declares at its top level (profile §9, AMP009). */
package/src/finding.ts CHANGED
@@ -19,6 +19,18 @@ export interface Finding {
19
19
  paths: string[];
20
20
  /** File-level and slice-level edges the finding concerns. */
21
21
  edges: SubjectEdge[];
22
+ /** For a finding about one import: what the baseline holds it by, one name at a time (VSA §9). */
23
+ taken?: Taken;
24
+ }
25
+
26
+ /** One import, as a baseline identifies it: how it imports, what it targets and the names it takes. */
27
+ export interface Taken {
28
+ /** `import`, `re-export` or `dynamic`. */
29
+ kind: string;
30
+ /** The file it resolves to, or the package specifier. */
31
+ to: string;
32
+ /** `name`, `type name` for a type-only one, `*` for every name; empty for a side-effect import. */
33
+ names: string[];
22
34
  }
23
35
 
24
36
  export interface FindingFields {
@@ -32,6 +44,7 @@ export interface FindingFields {
32
44
  remediation: string;
33
45
  paths?: string[];
34
46
  edges?: SubjectEdge[];
47
+ taken?: Taken;
35
48
  }
36
49
 
37
50
  /** One diagnostic, members in contract order, optional members present only when known. */
@@ -49,7 +62,12 @@ export function finding(fields: FindingFields): Finding {
49
62
  };
50
63
  const paths = new Set(fields.paths ?? []);
51
64
  if (fields.path !== null) paths.add(fields.path);
52
- return { diagnostic, paths: [...paths].sort(), edges: fields.edges ?? [] };
65
+ return {
66
+ diagnostic,
67
+ paths: [...paths].sort(),
68
+ edges: fields.edges ?? [],
69
+ ...(fields.taken !== undefined ? { taken: fields.taken } : {}),
70
+ };
53
71
  }
54
72
 
55
73
  /** "1 file" / "3 files". */
package/src/imports.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  * for a package outside the repository, the specifier alone. What could not
5
5
  * be read or resolved is returned beside the edges, never folded into them.
6
6
  */
7
- import { extractImports } from "./extract.ts";
7
+ import { extractImports, type LocalExports, type TakenName } from "./extract.ts";
8
+ import type { Taken } from "./finding.ts";
8
9
  import { isCode } from "./paths.ts";
9
10
  import { compareStrings } from "./patterns.ts";
10
11
  import type { Resolver, Via } from "./resolve.ts";
@@ -24,6 +25,8 @@ export interface ImportEdge {
24
25
  via: Via;
25
26
  /** Resolved through a `*` alias, an exports pattern or a package without exports (TS006). */
26
27
  wildcardAlias: boolean;
28
+ /** The names it takes from the target (`RawImport.names`). */
29
+ names: TakenName[];
27
30
  }
28
31
 
29
32
  export interface UnresolvedImport {
@@ -36,15 +39,23 @@ export interface UnresolvedImport {
36
39
  export interface ImportGraph {
37
40
  edges: ImportEdge[];
38
41
  unresolved: UnresolvedImport[];
42
+ /** What each code file exports of its own, by path. */
43
+ exports: Map<string, LocalExports>;
44
+ /** The line of the first JSX element in each file that renders one, by path. */
45
+ jsx: Map<string, number>;
39
46
  }
40
47
 
41
48
  /** Build the graph over every code file in `files` (already filtered of ignored paths). */
42
49
  export function buildImportGraph(files: ReadonlyMap<string, string>, resolver: Resolver): ImportGraph {
43
50
  const edges: ImportEdge[] = [];
44
51
  const unresolved: UnresolvedImport[] = [];
52
+ const exported = new Map<string, LocalExports>();
53
+ const rendered = new Map<string, number>();
45
54
  for (const path of [...files.keys()].sort(compareStrings)) {
46
55
  if (!isCode(path)) continue;
47
- const { imports, problems } = extractImports(path, files.get(path) as string);
56
+ const { imports, problems, exports, jsx } = extractImports(path, files.get(path) as string);
57
+ exported.set(path, exports);
58
+ if (jsx !== null) rendered.set(path, jsx);
48
59
  for (const problem of problems) {
49
60
  unresolved.push({ path, line: problem.line, specifier: null, message: problem.message });
50
61
  }
@@ -70,8 +81,19 @@ export function buildImportGraph(files: ReadonlyMap<string, string>, resolver: R
70
81
  wildcardExport: raw.wildcard,
71
82
  via: resolution.via,
72
83
  wildcardAlias: resolution.wildcard,
84
+ names: raw.names,
73
85
  });
74
86
  }
75
87
  }
76
- return { edges, unresolved };
88
+ return { edges, unresolved, exports: exported, jsx: rendered };
89
+ }
90
+
91
+ /** The import as a baseline identifies it (VSA §9): one entry per name it takes. */
92
+ export function takenBy(edge: ImportEdge): Taken {
93
+ const names = edge.names.map((taken) => `${taken.typeOnly || edge.typeOnly ? "type " : ""}${taken.name}`);
94
+ return {
95
+ kind: edge.reexport ? "re-export" : edge.dynamic ? "dynamic" : "import",
96
+ to: edge.to ?? edge.specifier,
97
+ names: [...new Set(names)].sort(compareStrings),
98
+ };
77
99
  }
package/src/index.ts CHANGED
@@ -7,14 +7,22 @@
7
7
  export { AREA_REVIEW_REQUIRED, areaOf, backendAreaOf, checkAreas, isAreaSchema } from "./areas.ts";
8
8
  export {
9
9
  applyBaseline,
10
+ type BaselineComparison,
10
11
  type BaselineEntry,
12
+ baselineKeys,
13
+ type BaselineSubject,
14
+ compareBaselines,
15
+ describeEntry,
16
+ FINGERPRINT_V2,
11
17
  fingerprint,
12
18
  type Mode,
13
19
  parseBaseline,
14
20
  plainMessage,
21
+ STALE_ENTRY_CODE,
22
+ staleEntryDiagnostics,
15
23
  writeBaseline,
16
24
  } from "./baseline.ts";
17
- export { type BoundaryResult, checkBoundaries, checkUnclassified, type SliceEdge } from "./boundaries.ts";
25
+ export { type BoundaryResult, checkBoundaries, checkJsx, checkUnclassified, type SliceEdge } from "./boundaries.ts";
18
26
  export {
19
27
  type ArchitectureResult,
20
28
  type ArchitectureSummary,
@@ -32,6 +40,7 @@ export {
32
40
  MAX_AREAS,
33
41
  readArchitectureConfig,
34
42
  resolveConfig,
43
+ UI_FRAMEWORKS,
35
44
  } from "./config.ts";
36
45
  export { checkDependencies, shortestCycle, stronglyConnected } from "./dependencies.ts";
37
46
  export {
@@ -46,6 +55,7 @@ export {
46
55
  export {
47
56
  type Extraction,
48
57
  extractImports,
58
+ type LocalExports,
49
59
  extractSchema,
50
60
  findCalls,
51
61
  type RawImport,
@@ -54,10 +64,12 @@ export {
54
64
  type SchemaExtraction,
55
65
  type SchemaKind,
56
66
  type SchemaReference,
67
+ type TakenName,
57
68
  } from "./extract.ts";
58
- export type { Finding, SubjectEdge } from "./finding.ts";
59
- export { buildImportGraph, type ImportEdge, type ImportGraph, type UnresolvedImport } from "./imports.ts";
60
- export { inSeam, isPure, layerAllows, layerOf, purePatterns, seamPatterns } from "./layers.ts";
69
+ export type { Finding, SubjectEdge, Taken } from "./finding.ts";
70
+ export { buildImportGraph, type ImportEdge, type ImportGraph, takenBy, type UnresolvedImport } from "./imports.ts";
71
+ export { type Definition, NameIndex, type Origin } from "./names.ts";
72
+ export { fileLayer, inSeam, isPure, layerAllows, layerOf, purePatterns, seamPatterns } from "./layers.ts";
61
73
  export { checkOwnership } from "./ownership.ts";
62
74
  export { type ArchitectureSubjectSource, type SubjectSourceOptions, subjectSource } from "./subject-source.ts";
63
75
  export { expandClaims, matchAny, matchPattern, validatePattern } from "./patterns.ts";
package/src/layers.ts CHANGED
@@ -30,6 +30,24 @@ export function layerOf(slice: SliceInfo, path: string): LayerOf {
30
30
  return { layer: null, reason: matches.length === 0 ? "none" : "ambiguous", candidates: matches };
31
31
  }
32
32
 
33
+ /** The reference layout's folders for the layers a slice that declares none still has (profile §1). */
34
+ const FALLBACK_LAYERS: [string, string][] = [
35
+ ["external", "client"],
36
+ ["policy", "domain/policies"],
37
+ ["model", "domain/models"],
38
+ ];
39
+
40
+ /**
41
+ * The layer a file is in for the package and JSX rules (TS007, TS008): the one layer it is declared in, or, for a
42
+ * slice that declares no layers, its client/, domain/policies/ or domain/models/ folder under the entrypoint
43
+ * directory above it. Null when it is in none, or in more than one.
44
+ */
45
+ export function fileLayer(slice: SliceInfo, path: string): string | null {
46
+ if (Object.keys(slice.meta.layers).length > 0) return layerOf(slice, path).layer;
47
+ for (const [layer, folder] of FALLBACK_LAYERS) if (matchAny(under(slice, [folder], path), path)) return layer;
48
+ return null;
49
+ }
50
+
33
51
  /** The fallback folders under the root a path belongs to, or under every root when no path is given. */
34
52
  function under(slice: SliceInfo, folders: string[], path?: string): string[] {
35
53
  const roots = path === undefined ? slice.roots : [rootOf(slice, path)].filter((root) => root !== null);