@intentset/architecture 0.8.0 → 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/names.ts ADDED
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Where a name an import takes is defined (VSA006, profile §3, TS004). An
3
+ * entrypoint or a barrel passes names on from other files, and the layer
4
+ * matrix is about the file that defines a name, not the one an import happens
5
+ * to name. So each name is followed through `export ... from`, through
6
+ * `export { a }` of an imported binding, and into `export *`, to the file
7
+ * that declares it.
8
+ *
9
+ * A name a file neither re-exports nor declares, where no `export *` of the
10
+ * file provides it either, is taken as defined in that file: TypeScript
11
+ * refuses a name a module does not export, so this only happens where this
12
+ * reader missed a declaration shape, and it leaves the edge judged as before.
13
+ * A name that may come from a package's `export *` or from an export this
14
+ * reader cannot name (`export =`, `module.exports`) is untraced, and the
15
+ * caller lists it as not checked (invariant 6).
16
+ */
17
+ import type { LocalExports } from "./extract.ts";
18
+ import type { ImportEdge } from "./imports.ts";
19
+ import { compareStrings } from "./patterns.ts";
20
+
21
+ /** Where one name was found: in a file, in a package, or nowhere this reader can say. */
22
+ export type Origin =
23
+ | { kind: "file"; file: string; through: string[] }
24
+ | { kind: "package"; specifier: string; through: string[] }
25
+ | { kind: "untraced"; why: string; through: string[] };
26
+
27
+ export interface Definition {
28
+ /** The name as the module the edge targets exports it. */
29
+ name: string;
30
+ typeOnly: boolean;
31
+ /** `through` lists the files that passed the name on, the edge's target first; empty when the target declares it. */
32
+ origin: Origin;
33
+ }
34
+
35
+ interface FileTable {
36
+ /** Exported name to the edge and the name it is taken from there. */
37
+ reexports: Map<string, { edge: ImportEdge; name: string }>;
38
+ /** `export * from`, in source order. */
39
+ wildcards: ImportEdge[];
40
+ declared: Set<string>;
41
+ opaque: boolean;
42
+ }
43
+
44
+ /** How many re-exports deep a name is followed before it is called untraced. */
45
+ const MAX_DEPTH = 64;
46
+
47
+ export class NameIndex {
48
+ readonly #tables = new Map<string, FileTable>();
49
+ readonly #all = new Map<string, string[] | null>();
50
+
51
+ constructor(edges: readonly ImportEdge[], exports: ReadonlyMap<string, LocalExports>) {
52
+ const imported = new Map<string, Map<string, { edge: ImportEdge; name: string }>>();
53
+ for (const edge of edges) {
54
+ if (edge.dynamic) continue;
55
+ const table = this.#table(edge.from, exports);
56
+ for (const taken of edge.names) {
57
+ if (edge.reexport) {
58
+ if (taken.name === "*" && taken.as === "*") table.wildcards.push(edge);
59
+ else table.reexports.set(taken.as, { edge, name: taken.name });
60
+ } else if (taken.as !== "") {
61
+ const bindings = imported.get(edge.from) ?? new Map();
62
+ bindings.set(taken.as, { edge, name: taken.name });
63
+ imported.set(edge.from, bindings);
64
+ }
65
+ }
66
+ }
67
+ // `export { a }` and `export default a` of an imported binding pass the import on.
68
+ for (const [path, local] of exports) {
69
+ const table = this.#table(path, exports);
70
+ for (const binding of local.bindings) {
71
+ const source = imported.get(path)?.get(binding.name);
72
+ if (source !== undefined) table.reexports.set(binding.as, source);
73
+ else table.declared.add(binding.as);
74
+ }
75
+ }
76
+ }
77
+
78
+ #table(path: string, exports: ReadonlyMap<string, LocalExports>): FileTable {
79
+ let table = this.#tables.get(path);
80
+ if (table === undefined) {
81
+ const local = exports.get(path);
82
+ table = {
83
+ reexports: new Map(),
84
+ wildcards: [],
85
+ declared: new Set(local?.declared ?? []),
86
+ opaque: local?.opaque ?? false,
87
+ };
88
+ this.#tables.set(path, table);
89
+ }
90
+ return table;
91
+ }
92
+
93
+ /** Where each name `edge` takes is defined, sorted by name. A name of `*` is expanded to every name the target exports. */
94
+ definitions(edge: ImportEdge): Definition[] {
95
+ if (edge.to === null || edge.names.length === 0) return [];
96
+ const to = edge.to;
97
+ const out = new Map<string, Definition>();
98
+ for (const taken of edge.names) {
99
+ const names = taken.name === "*" ? this.#everyName(to, new Set()) : [taken.name];
100
+ if (names === null) {
101
+ out.set("*", {
102
+ name: "*",
103
+ typeOnly: taken.typeOnly,
104
+ origin: { kind: "untraced", why: "it exports names this reader cannot list", through: [to] },
105
+ });
106
+ continue;
107
+ }
108
+ for (const name of names) {
109
+ if (taken.name === "*" && taken.as === "*" && name === "default") continue;
110
+ const typeOnly = taken.typeOnly || edge.typeOnly;
111
+ const known = out.get(name);
112
+ if (known !== undefined && (!known.typeOnly || typeOnly)) continue;
113
+ out.set(name, { name, typeOnly, origin: this.trace(to, name) });
114
+ }
115
+ }
116
+ return [...out.values()].sort((a, b) => compareStrings(a.name, b.name));
117
+ }
118
+
119
+ /** Follow one name exported by `file` to the file that defines it. */
120
+ trace(file: string, name: string): Origin {
121
+ const through: string[] = [];
122
+ const seen = new Set<string>();
123
+ let at = file;
124
+ let current = name;
125
+ for (let depth = 0; depth < MAX_DEPTH; depth++) {
126
+ const key = `${at}\u0000${current}`;
127
+ if (seen.has(key)) return { kind: "untraced", why: "its re-exports form a cycle", through };
128
+ seen.add(key);
129
+ const table = this.#tables.get(at);
130
+ if (table === undefined) return { kind: "file", file: at, through };
131
+ const next = table.reexports.get(current);
132
+ if (next !== undefined) {
133
+ through.push(at);
134
+ if (next.edge.to === null) return { kind: "package", specifier: next.edge.specifier, through };
135
+ if (next.name === "*") return { kind: "file", file: next.edge.to, through };
136
+ at = next.edge.to;
137
+ current = next.name;
138
+ continue;
139
+ }
140
+ if (table.declared.has(current)) return { kind: "file", file: at, through };
141
+ if (current !== "default") {
142
+ const provider = table.wildcards.find(
143
+ (edge) => edge.to !== null && this.#provides(edge.to, current, new Set()),
144
+ );
145
+ if (provider !== undefined) {
146
+ through.push(at);
147
+ at = provider.to as string;
148
+ continue;
149
+ }
150
+ for (const edge of table.wildcards) {
151
+ const hidden = edge.to === null ? { package: edge.specifier } : this.#unlisted(edge.to, new Set());
152
+ if (hidden === null) continue;
153
+ through.push(at);
154
+ return "package" in hidden
155
+ ? { kind: "package", specifier: hidden.package, through }
156
+ : { kind: "untraced", why: `${hidden.file} exports names this reader cannot list`, through };
157
+ }
158
+ }
159
+ if (table.opaque) return { kind: "untraced", why: `${at} exports names this reader cannot list`, through };
160
+ return { kind: "file", file: at, through };
161
+ }
162
+ return { kind: "untraced", why: `it is re-exported more than ${MAX_DEPTH} times`, through };
163
+ }
164
+
165
+ /** Does `file` export `name`, itself or through `export *`? */
166
+ #provides(file: string, name: string, seen: Set<string>): boolean {
167
+ if (seen.has(file)) return false;
168
+ seen.add(file);
169
+ const table = this.#tables.get(file);
170
+ if (table === undefined) return false;
171
+ if (table.reexports.has(name) || table.declared.has(name)) return true;
172
+ return table.wildcards.some((edge) => edge.to !== null && this.#provides(edge.to, name, seen));
173
+ }
174
+
175
+ /** The first package or unreadable file `export *` reaches from `file`, which may hold a name no file here declares. */
176
+ #unlisted(file: string, seen: Set<string>): { package: string } | { file: string } | null {
177
+ if (seen.has(file)) return null;
178
+ seen.add(file);
179
+ const table = this.#tables.get(file);
180
+ if (table === undefined) return null;
181
+ if (table.opaque) return { file };
182
+ for (const edge of table.wildcards) {
183
+ const hidden = edge.to === null ? { package: edge.specifier } : this.#unlisted(edge.to, seen);
184
+ if (hidden !== null) return hidden;
185
+ }
186
+ return null;
187
+ }
188
+
189
+ /**
190
+ * Every name `file` exports that some file here defines, sorted, or null when an export cannot be listed. A
191
+ * package's `export *` adds nothing: what a package defines is in no slice's layer.
192
+ */
193
+ #everyName(file: string, seen: Set<string>): string[] | null {
194
+ const cached = this.#all.get(file);
195
+ if (cached !== undefined) return cached;
196
+ if (seen.has(file)) return [];
197
+ seen.add(file);
198
+ const table = this.#tables.get(file);
199
+ if (table === undefined) return [];
200
+ if (table.opaque) return this.#remember(file, null);
201
+ const names = new Set([...table.reexports.keys(), ...table.declared]);
202
+ for (const edge of table.wildcards) {
203
+ if (edge.to === null) continue;
204
+ const more = this.#everyName(edge.to, seen);
205
+ if (more === null) return this.#remember(file, null);
206
+ for (const name of more) if (name !== "default") names.add(name);
207
+ }
208
+ return this.#remember(file, [...names].sort(compareStrings));
209
+ }
210
+
211
+ #remember(file: string, names: string[] | null): string[] | null {
212
+ this.#all.set(file, names);
213
+ return names;
214
+ }
215
+ }