@goodbones/browser 0.1.0-beta.1
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/LICENSE +21 -0
- package/build/dts/index.d.ts +12 -0
- package/build/dts/index.d.ts.map +1 -0
- package/build/dts/main.d.ts +3 -0
- package/build/dts/main.d.ts.map +1 -0
- package/build/dts/model/atlas.d.ts +157 -0
- package/build/dts/model/atlas.d.ts.map +1 -0
- package/build/dts/model/campaigns.d.ts +141 -0
- package/build/dts/model/campaigns.d.ts.map +1 -0
- package/build/dts/server/collect.d.ts +16 -0
- package/build/dts/server/collect.d.ts.map +1 -0
- package/build/dts/server/compose.d.ts +13 -0
- package/build/dts/server/compose.d.ts.map +1 -0
- package/build/dts/server/export.d.ts +3 -0
- package/build/dts/server/export.d.ts.map +1 -0
- package/build/dts/server/handler.d.ts +6 -0
- package/build/dts/server/handler.d.ts.map +1 -0
- package/build/dts/server/serve.d.ts +15 -0
- package/build/dts/server/serve.d.ts.map +1 -0
- package/build/dts/server/site.d.ts +2 -0
- package/build/dts/server/site.d.ts.map +1 -0
- package/build/dts/server/source.d.ts +19 -0
- package/build/dts/server/source.d.ts.map +1 -0
- package/build/dts/server/vite-plugin.d.ts +8 -0
- package/build/dts/server/vite-plugin.d.ts.map +1 -0
- package/build/dts/server/watch.d.ts +10 -0
- package/build/dts/server/watch.d.ts.map +1 -0
- package/build/esm/index.js +14 -0
- package/build/esm/index.js.map +1 -0
- package/build/esm/main.js +134 -0
- package/build/esm/main.js.map +1 -0
- package/build/esm/model/atlas.js +431 -0
- package/build/esm/model/atlas.js.map +1 -0
- package/build/esm/model/campaigns.js +181 -0
- package/build/esm/model/campaigns.js.map +1 -0
- package/build/esm/server/collect.js +83 -0
- package/build/esm/server/collect.js.map +1 -0
- package/build/esm/server/compose.js +50 -0
- package/build/esm/server/compose.js.map +1 -0
- package/build/esm/server/export.js +19 -0
- package/build/esm/server/export.js.map +1 -0
- package/build/esm/server/handler.js +67 -0
- package/build/esm/server/handler.js.map +1 -0
- package/build/esm/server/serve.js +80 -0
- package/build/esm/server/serve.js.map +1 -0
- package/build/esm/server/site.js +6 -0
- package/build/esm/server/site.js.map +1 -0
- package/build/esm/server/source.js +67 -0
- package/build/esm/server/source.js.map +1 -0
- package/build/esm/server/vite-plugin.js +21 -0
- package/build/esm/server/vite-plugin.js.map +1 -0
- package/build/esm/server/watch.js +56 -0
- package/build/esm/server/watch.js.map +1 -0
- package/build/site/_astro/Browser.yxldcnQ3.js +3 -0
- package/build/site/_astro/client.Buyw3Q1S.js +9 -0
- package/build/site/_astro/index.DQrWs1cb.css +1 -0
- package/build/site/_astro/react.DJY1zw8Z.js +1 -0
- package/build/site/favicon.svg +1 -0
- package/build/site/index.html +8 -0
- package/package.json +78 -0
- package/src/app/components/Browser.tsx +153 -0
- package/src/app/components/architecture/ArchitectureBrowser.tsx +240 -0
- package/src/app/components/architecture/Detail.tsx +655 -0
- package/src/app/components/architecture/ManifestPane.tsx +178 -0
- package/src/app/components/architecture/Tree.tsx +205 -0
- package/src/app/components/architecture/rows.ts +181 -0
- package/src/app/components/architecture/selection.ts +6 -0
- package/src/app/components/campaigns/CampaignBrowser.tsx +146 -0
- package/src/app/components/campaigns/Detail.tsx +653 -0
- package/src/app/components/campaigns/Ladder.tsx +326 -0
- package/src/app/components/campaigns/pick.ts +7 -0
- package/src/app/components/lib/data.ts +132 -0
- package/src/app/pages/index.astro +34 -0
- package/src/app/public/favicon.svg +1 -0
- package/src/app/styles/theme.css +993 -0
- package/src/index.ts +37 -0
- package/src/main.ts +155 -0
- package/src/model/atlas.ts +751 -0
- package/src/model/campaigns.ts +376 -0
- package/src/server/collect.ts +132 -0
- package/src/server/compose.ts +79 -0
- package/src/server/export.ts +24 -0
- package/src/server/handler.ts +77 -0
- package/src/server/serve.ts +100 -0
- package/src/server/site.ts +6 -0
- package/src/server/source.ts +89 -0
- package/src/server/vite-plugin.ts +32 -0
- package/src/server/watch.ts +65 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import type { Atlas, AtlasFile, AtlasFolder, AtlasNode, EdgeStatus } from "../../../model/atlas.js";
|
|
2
|
+
import type { Selection } from "./selection.js";
|
|
3
|
+
|
|
4
|
+
// What the manifest says about the thing under the pointer, in the CLI's
|
|
5
|
+
// words: what it may import and why each import passed or did not, who
|
|
6
|
+
// imports it, what it owes, and every violation on it. With nothing under
|
|
7
|
+
// the pointer, the repository as a whole.
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
readonly atlas: Atlas;
|
|
11
|
+
readonly selected: Selection | null;
|
|
12
|
+
readonly hovered: Selection | null;
|
|
13
|
+
readonly onSelect: (selection: Selection | null) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const same = (a: Selection | null, b: Selection | null): boolean =>
|
|
17
|
+
a !== null &&
|
|
18
|
+
b !== null &&
|
|
19
|
+
a.kind === b.kind &&
|
|
20
|
+
(a.kind === "node" ? a.id === (b as { id: string }).id : a.path === (b as { path: string }).path);
|
|
21
|
+
|
|
22
|
+
export const Detail = ({ atlas, hovered, onSelect, selected }: Props): React.JSX.Element => {
|
|
23
|
+
const shown = hovered ?? selected;
|
|
24
|
+
const pinned = shown !== null && same(shown, selected);
|
|
25
|
+
return (
|
|
26
|
+
<div className="detail">
|
|
27
|
+
{shown === null ? (
|
|
28
|
+
<Overview atlas={atlas} onSelect={onSelect} />
|
|
29
|
+
) : (
|
|
30
|
+
<>
|
|
31
|
+
<div className="pane-head">
|
|
32
|
+
<span className="eyebrow">{shown.kind}</span>
|
|
33
|
+
{pinned ? (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
className="small"
|
|
37
|
+
onClick={() => {
|
|
38
|
+
onSelect(null);
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
unpin
|
|
42
|
+
</button>
|
|
43
|
+
) : (
|
|
44
|
+
<span className="hint">click to pin</span>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
{shown.kind === "file" ? (
|
|
48
|
+
<FileDetail atlas={atlas} path={shown.path} onSelect={onSelect} />
|
|
49
|
+
) : shown.kind === "folder" ? (
|
|
50
|
+
<FolderDetail atlas={atlas} path={shown.path} onSelect={onSelect} />
|
|
51
|
+
) : (
|
|
52
|
+
<NodeDetail atlas={atlas} id={shown.id} onSelect={onSelect} />
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const Overview = ({
|
|
61
|
+
atlas,
|
|
62
|
+
onSelect,
|
|
63
|
+
}: {
|
|
64
|
+
atlas: Atlas;
|
|
65
|
+
onSelect: Props["onSelect"];
|
|
66
|
+
}): React.JSX.Element => {
|
|
67
|
+
const refused = atlas.violations.filter((one) => !one.baselined);
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<div className="pane-head">
|
|
71
|
+
<span className="eyebrow">repository</span>
|
|
72
|
+
</div>
|
|
73
|
+
<h2 className="mono">{atlas.name}</h2>
|
|
74
|
+
<p className="muted">
|
|
75
|
+
{atlas.files.filter((file) => file.walked).length} files under {atlas.roots.join(", ")},{" "}
|
|
76
|
+
{atlas.edges.length} edges, {atlas.manifest.nodes.length} nodes in {atlas.manifest.path}
|
|
77
|
+
{atlas.manifest.files.length > 1
|
|
78
|
+
? ` and ${String(atlas.manifest.files.length - 1)} included file(s)`
|
|
79
|
+
: ""}
|
|
80
|
+
.
|
|
81
|
+
</p>
|
|
82
|
+
<dl className="facts">
|
|
83
|
+
<dt>violations</dt>
|
|
84
|
+
<dd className={refused.length > 0 ? "bad" : "good"}>
|
|
85
|
+
{refused.length}
|
|
86
|
+
{atlas.violations.length > refused.length
|
|
87
|
+
? ` (+${String(atlas.violations.length - refused.length)} baselined)`
|
|
88
|
+
: ""}
|
|
89
|
+
</dd>
|
|
90
|
+
<dt>unresolved</dt>
|
|
91
|
+
<dd className={atlas.unresolved.length > 0 ? "bad" : "good"}>{atlas.unresolved.length}</dd>
|
|
92
|
+
<dt>cycles</dt>
|
|
93
|
+
<dd className={atlas.cycles > 0 ? "bad" : "good"}>{atlas.cycles}</dd>
|
|
94
|
+
</dl>
|
|
95
|
+
<h3>How to read it</h3>
|
|
96
|
+
<p className="muted">
|
|
97
|
+
Hover a file: its imports come forward in the accent, its importers in ink, a refused edge
|
|
98
|
+
in red, and the node that governs it lights up in the manifest. Hover a line of the
|
|
99
|
+
manifest: every file that node governs lights up in the tree. Click either to keep it.
|
|
100
|
+
</p>
|
|
101
|
+
<h3>Nodes</h3>
|
|
102
|
+
<ul className="list">
|
|
103
|
+
{atlas.manifest.nodes
|
|
104
|
+
.filter((node) => node.depth === 0)
|
|
105
|
+
.map((node) => (
|
|
106
|
+
<li key={node.id}>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
className="link mono"
|
|
110
|
+
onClick={() => {
|
|
111
|
+
onSelect({ kind: "node", id: node.id });
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{node.path}
|
|
115
|
+
</button>
|
|
116
|
+
<span className="muted">
|
|
117
|
+
{" "}
|
|
118
|
+
{node.files === 0 ? "" : `${String(node.files)} files`}
|
|
119
|
+
</span>
|
|
120
|
+
</li>
|
|
121
|
+
))}
|
|
122
|
+
</ul>
|
|
123
|
+
{atlas.manifest.rules.length > 0 ? (
|
|
124
|
+
<>
|
|
125
|
+
<h3>Repository-wide rules</h3>
|
|
126
|
+
<ul className="list">
|
|
127
|
+
{atlas.manifest.rules.map((rule) => (
|
|
128
|
+
<li key={`${rule.family}:${rule.name}`} title={rule.message}>
|
|
129
|
+
<span className="family">{rule.family}</span>{" "}
|
|
130
|
+
<span className="mono">{rule.name}</span>
|
|
131
|
+
</li>
|
|
132
|
+
))}
|
|
133
|
+
</ul>
|
|
134
|
+
</>
|
|
135
|
+
) : null}
|
|
136
|
+
{refused.length > 0 ? (
|
|
137
|
+
<>
|
|
138
|
+
<h3>Violations</h3>
|
|
139
|
+
<ul className="list">
|
|
140
|
+
{refused.slice(0, 40).map((one) => (
|
|
141
|
+
<li key={one.fingerprint} className="violation">
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className="link mono"
|
|
145
|
+
onClick={() => {
|
|
146
|
+
onSelect({ kind: "file", path: one.file });
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{one.file}
|
|
150
|
+
</button>
|
|
151
|
+
<div className="muted small">{one.message}</div>
|
|
152
|
+
</li>
|
|
153
|
+
))}
|
|
154
|
+
{refused.length > 40 ? <li className="muted">… {refused.length - 40} more</li> : null}
|
|
155
|
+
</ul>
|
|
156
|
+
</>
|
|
157
|
+
) : null}
|
|
158
|
+
</>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const statusWord = (status: EdgeStatus): string =>
|
|
163
|
+
status === "allowed" ? "allowed" : status === "refused" ? "refused" : "no allowlist";
|
|
164
|
+
|
|
165
|
+
const NodeCrumbs = ({
|
|
166
|
+
atlas,
|
|
167
|
+
chain,
|
|
168
|
+
onSelect,
|
|
169
|
+
}: {
|
|
170
|
+
atlas: Atlas;
|
|
171
|
+
chain: ReadonlyArray<string>;
|
|
172
|
+
onSelect: Props["onSelect"];
|
|
173
|
+
}): React.JSX.Element => (
|
|
174
|
+
<div className="crumbs">
|
|
175
|
+
{chain.map((id, index) => {
|
|
176
|
+
const node = atlas.manifest.nodes.find((one) => one.id === id);
|
|
177
|
+
return (
|
|
178
|
+
<span key={id}>
|
|
179
|
+
{index > 0 ? <span className="muted"> › </span> : null}
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
className="link mono"
|
|
183
|
+
onClick={() => {
|
|
184
|
+
onSelect({ kind: "node", id });
|
|
185
|
+
}}
|
|
186
|
+
title={node?.message ?? id}
|
|
187
|
+
>
|
|
188
|
+
{node?.key ?? id}
|
|
189
|
+
</button>
|
|
190
|
+
</span>
|
|
191
|
+
);
|
|
192
|
+
})}
|
|
193
|
+
{chain.length === 0 ? <span className="muted">governed by no node</span> : null}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const FileDetail = ({
|
|
198
|
+
atlas,
|
|
199
|
+
onSelect,
|
|
200
|
+
path,
|
|
201
|
+
}: {
|
|
202
|
+
atlas: Atlas;
|
|
203
|
+
path: string;
|
|
204
|
+
onSelect: Props["onSelect"];
|
|
205
|
+
}): React.JSX.Element | null => {
|
|
206
|
+
const file: AtlasFile | undefined = atlas.files.find((one) => one.path === path);
|
|
207
|
+
if (file === undefined) return null;
|
|
208
|
+
const node = file.node === null ? null : atlas.manifest.nodes.find((one) => one.id === file.node);
|
|
209
|
+
const out = atlas.edges.filter((edge) => edge.from === path);
|
|
210
|
+
const into = atlas.edges.filter((edge) => edge.to === path);
|
|
211
|
+
const violations = atlas.violations.filter((one) => one.file === path);
|
|
212
|
+
const unresolved = atlas.unresolved.filter((one) => one.file === path);
|
|
213
|
+
// The allowlist in force: every allowance down the chain, nearest last.
|
|
214
|
+
const allowances = file.chain
|
|
215
|
+
.map((id) => atlas.manifest.nodes.find((one) => one.id === id))
|
|
216
|
+
.filter((one): one is AtlasNode => one !== undefined && one.imports !== null);
|
|
217
|
+
return (
|
|
218
|
+
<>
|
|
219
|
+
<h2 className="mono path">{file.path}</h2>
|
|
220
|
+
{!file.walked ? (
|
|
221
|
+
<p className="muted">Outside the walk: reached from it, governed by nothing.</p>
|
|
222
|
+
) : null}
|
|
223
|
+
<NodeCrumbs atlas={atlas} chain={file.chain} onSelect={onSelect} />
|
|
224
|
+
{node?.message !== null && node?.message !== undefined ? (
|
|
225
|
+
<p className="message">{node.message}</p>
|
|
226
|
+
) : null}
|
|
227
|
+
|
|
228
|
+
<h3>May import</h3>
|
|
229
|
+
{allowances.length === 0 ? (
|
|
230
|
+
<p className="muted">anything — no node above this file states an allowlist</p>
|
|
231
|
+
) : (
|
|
232
|
+
allowances.map((one) => (
|
|
233
|
+
<div key={one.id} className="allowlist">
|
|
234
|
+
<div className="muted small">
|
|
235
|
+
from <span className="mono">{one.key}</span>
|
|
236
|
+
{one.imports?.reset === true ? " (reset)" : ""}
|
|
237
|
+
{one.imports?.unrestricted === true ? " (unrestricted)" : ""}
|
|
238
|
+
</div>
|
|
239
|
+
<ul className="chips">
|
|
240
|
+
{(one.imports?.allowances ?? []).map((allowance) => (
|
|
241
|
+
<li
|
|
242
|
+
key={`${allowance.kind}:${allowance.entry}`}
|
|
243
|
+
className={`chip ${allowance.kind} ${allowance.used ? "" : "slack"}`}
|
|
244
|
+
title={
|
|
245
|
+
allowance.used
|
|
246
|
+
? allowance.kind === "external"
|
|
247
|
+
? "a package"
|
|
248
|
+
: allowance.expanded
|
|
249
|
+
: "slack: no import uses this entry"
|
|
250
|
+
}
|
|
251
|
+
>
|
|
252
|
+
{allowance.entry}
|
|
253
|
+
</li>
|
|
254
|
+
))}
|
|
255
|
+
{(one.imports?.deny ?? []).map((denial) => (
|
|
256
|
+
<li key={denial.message} className="chip deny" title={denial.message}>
|
|
257
|
+
deny {denial.match.join(" | ")}
|
|
258
|
+
</li>
|
|
259
|
+
))}
|
|
260
|
+
</ul>
|
|
261
|
+
</div>
|
|
262
|
+
))
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
<h3>Imports</h3>
|
|
266
|
+
{out.length + file.externals.length + file.builtins.length === 0 ? (
|
|
267
|
+
<p className="muted">nothing</p>
|
|
268
|
+
) : (
|
|
269
|
+
<ul className="list">
|
|
270
|
+
{out.map((edge) => (
|
|
271
|
+
<li key={edge.to} className={`edge-row ${edge.status}`}>
|
|
272
|
+
<span className={`verdict ${edge.status}`}>
|
|
273
|
+
{edge.status === "refused" ? "✗" : "✓"}
|
|
274
|
+
</span>
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
className="link mono"
|
|
278
|
+
onClick={() => {
|
|
279
|
+
onSelect({ kind: "file", path: edge.to });
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
{edge.to}
|
|
283
|
+
</button>
|
|
284
|
+
<div className="muted small">
|
|
285
|
+
{edge.status === "refused"
|
|
286
|
+
? edge.refusedBy.map((one) => one.message).join(" ")
|
|
287
|
+
: edge.admittedBy === null
|
|
288
|
+
? statusWord(edge.status)
|
|
289
|
+
: `admitted by ${edge.admittedBy.entry} on ${edge.admittedBy.node}${edge.admittedBy.fragment === null ? "" : ` (via ${edge.admittedBy.fragment})`}`}
|
|
290
|
+
</div>
|
|
291
|
+
</li>
|
|
292
|
+
))}
|
|
293
|
+
{file.externals.map((external) => (
|
|
294
|
+
<li key={external.package} className={`edge-row ${external.status}`}>
|
|
295
|
+
<span className={`verdict ${external.status}`}>
|
|
296
|
+
{external.status === "refused" ? "✗" : "✓"}
|
|
297
|
+
</span>
|
|
298
|
+
<span className="mono">{external.package}</span>
|
|
299
|
+
<span className="muted small"> external · {external.specifiers.join(", ")}</span>
|
|
300
|
+
<div className="muted small">
|
|
301
|
+
{external.admittedBy === null
|
|
302
|
+
? statusWord(external.status)
|
|
303
|
+
: `admitted by ${external.admittedBy.entry} on ${external.admittedBy.node}`}
|
|
304
|
+
</div>
|
|
305
|
+
</li>
|
|
306
|
+
))}
|
|
307
|
+
{file.builtins.map((builtin) => (
|
|
308
|
+
<li key={builtin} className="edge-row">
|
|
309
|
+
<span className="verdict allowed">·</span>
|
|
310
|
+
<span className="mono">{builtin}</span>
|
|
311
|
+
<span className="muted small"> builtin</span>
|
|
312
|
+
</li>
|
|
313
|
+
))}
|
|
314
|
+
{unresolved.map((one) => (
|
|
315
|
+
<li key={one.specifier} className="edge-row refused">
|
|
316
|
+
<span className="verdict refused">?</span>
|
|
317
|
+
<span className="mono">{one.specifier}</span>
|
|
318
|
+
<div className="muted small">unresolved: {one.detail}</div>
|
|
319
|
+
</li>
|
|
320
|
+
))}
|
|
321
|
+
</ul>
|
|
322
|
+
)}
|
|
323
|
+
|
|
324
|
+
<h3>Imported by</h3>
|
|
325
|
+
{into.length === 0 ? (
|
|
326
|
+
<p className="muted">nothing in the walk</p>
|
|
327
|
+
) : (
|
|
328
|
+
<ul className="list">
|
|
329
|
+
{into.map((edge) => (
|
|
330
|
+
<li key={edge.from} className={`edge-row ${edge.status}`}>
|
|
331
|
+
<span className={`verdict ${edge.status}`}>
|
|
332
|
+
{edge.status === "refused" ? "✗" : "✓"}
|
|
333
|
+
</span>
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
className="link mono"
|
|
337
|
+
onClick={() => {
|
|
338
|
+
onSelect({ kind: "file", path: edge.from });
|
|
339
|
+
}}
|
|
340
|
+
>
|
|
341
|
+
{edge.from}
|
|
342
|
+
</button>
|
|
343
|
+
</li>
|
|
344
|
+
))}
|
|
345
|
+
</ul>
|
|
346
|
+
)}
|
|
347
|
+
|
|
348
|
+
{file.requires.length > 0 ? (
|
|
349
|
+
<>
|
|
350
|
+
<h3>Owes</h3>
|
|
351
|
+
<ul className="list">
|
|
352
|
+
{file.requires.map((owed) => (
|
|
353
|
+
<li key={owed.sibling} className={owed.present ? "" : "bad"}>
|
|
354
|
+
<span className={`verdict ${owed.present ? "allowed" : "refused"}`}>
|
|
355
|
+
{owed.present ? "✓" : "✗"}
|
|
356
|
+
</span>
|
|
357
|
+
<span className="mono">{owed.sibling}</span>
|
|
358
|
+
<span className="muted small"> {owed.rule}</span>
|
|
359
|
+
</li>
|
|
360
|
+
))}
|
|
361
|
+
</ul>
|
|
362
|
+
</>
|
|
363
|
+
) : null}
|
|
364
|
+
|
|
365
|
+
{file.importRules.length > 0 ? (
|
|
366
|
+
<>
|
|
367
|
+
<h3>Import rules selecting it</h3>
|
|
368
|
+
<ul className="chips">
|
|
369
|
+
{file.importRules.map((rule) => (
|
|
370
|
+
<li key={rule} className="chip rule mono">
|
|
371
|
+
{rule}
|
|
372
|
+
</li>
|
|
373
|
+
))}
|
|
374
|
+
</ul>
|
|
375
|
+
</>
|
|
376
|
+
) : null}
|
|
377
|
+
|
|
378
|
+
<h3>Violations</h3>
|
|
379
|
+
{violations.length === 0 ? (
|
|
380
|
+
<p className="good">none</p>
|
|
381
|
+
) : (
|
|
382
|
+
<ul className="list">
|
|
383
|
+
{violations.map((one) => (
|
|
384
|
+
<li key={one.fingerprint} className={`violation ${one.baselined ? "baselined" : ""}`}>
|
|
385
|
+
<span className="family">{one.kind}</span> {one.message}
|
|
386
|
+
{one.baselined ? <span className="muted small"> (baselined)</span> : null}
|
|
387
|
+
</li>
|
|
388
|
+
))}
|
|
389
|
+
</ul>
|
|
390
|
+
)}
|
|
391
|
+
</>
|
|
392
|
+
);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const FolderDetail = ({
|
|
396
|
+
atlas,
|
|
397
|
+
onSelect,
|
|
398
|
+
path,
|
|
399
|
+
}: {
|
|
400
|
+
atlas: Atlas;
|
|
401
|
+
path: string;
|
|
402
|
+
onSelect: Props["onSelect"];
|
|
403
|
+
}): React.JSX.Element | null => {
|
|
404
|
+
const folder: AtlasFolder | undefined = atlas.folders.find((one) => one.path === path);
|
|
405
|
+
if (folder === undefined) return null;
|
|
406
|
+
const within = atlas.files.filter((file) => file.path.startsWith(`${path}/`));
|
|
407
|
+
const chain =
|
|
408
|
+
within[0]?.chain.filter((id) => {
|
|
409
|
+
const node = atlas.manifest.nodes.find((one) => one.id === id);
|
|
410
|
+
return node?.folder === true;
|
|
411
|
+
}) ?? (folder.node === null ? [] : [folder.node]);
|
|
412
|
+
const violations = atlas.violations.filter((one) => one.file.startsWith(`${path}/`));
|
|
413
|
+
const out = atlas.edges.filter(
|
|
414
|
+
(edge) => edge.from.startsWith(`${path}/`) && !edge.to.startsWith(`${path}/`),
|
|
415
|
+
);
|
|
416
|
+
const into = atlas.edges.filter(
|
|
417
|
+
(edge) => edge.to.startsWith(`${path}/`) && !edge.from.startsWith(`${path}/`),
|
|
418
|
+
);
|
|
419
|
+
const declared = folder.declares
|
|
420
|
+
.map((id) => atlas.manifest.nodes.find((one) => one.id === id))
|
|
421
|
+
.filter((one): one is AtlasNode => one !== undefined);
|
|
422
|
+
return (
|
|
423
|
+
<>
|
|
424
|
+
<h2 className="mono path">{folder.path}/</h2>
|
|
425
|
+
<NodeCrumbs atlas={atlas} chain={chain} onSelect={onSelect} />
|
|
426
|
+
{declared.map((node) => (
|
|
427
|
+
<div key={node.id} className="declared">
|
|
428
|
+
<div className="muted small">
|
|
429
|
+
declares node <span className="mono">{node.path}</span>
|
|
430
|
+
{node.position === null ? "" : ` · ${node.position.file}:${String(node.position.line)}`}
|
|
431
|
+
</div>
|
|
432
|
+
{node.message !== null ? <p className="message">{node.message}</p> : null}
|
|
433
|
+
</div>
|
|
434
|
+
))}
|
|
435
|
+
<dl className="facts">
|
|
436
|
+
<dt>files</dt>
|
|
437
|
+
<dd>{within.length}</dd>
|
|
438
|
+
<dt>edges out</dt>
|
|
439
|
+
<dd>{out.length}</dd>
|
|
440
|
+
<dt>edges in</dt>
|
|
441
|
+
<dd>{into.length}</dd>
|
|
442
|
+
<dt>violations</dt>
|
|
443
|
+
<dd className={violations.length > 0 ? "bad" : "good"}>{violations.length}</dd>
|
|
444
|
+
</dl>
|
|
445
|
+
<h3>Reaches</h3>
|
|
446
|
+
<ul className="list">
|
|
447
|
+
{[...new Set(out.map((edge) => folderOf(edge.to)))].sort().map((target) => (
|
|
448
|
+
<li key={target}>
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
className="link mono"
|
|
452
|
+
onClick={() => {
|
|
453
|
+
onSelect({ kind: "folder", path: target });
|
|
454
|
+
}}
|
|
455
|
+
>
|
|
456
|
+
{target}/
|
|
457
|
+
</button>
|
|
458
|
+
</li>
|
|
459
|
+
))}
|
|
460
|
+
{out.length === 0 ? <li className="muted">nothing outside itself</li> : null}
|
|
461
|
+
</ul>
|
|
462
|
+
<h3>Reached from</h3>
|
|
463
|
+
<ul className="list">
|
|
464
|
+
{[...new Set(into.map((edge) => folderOf(edge.from)))].sort().map((source) => (
|
|
465
|
+
<li key={source}>
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
className="link mono"
|
|
469
|
+
onClick={() => {
|
|
470
|
+
onSelect({ kind: "folder", path: source });
|
|
471
|
+
}}
|
|
472
|
+
>
|
|
473
|
+
{source}/
|
|
474
|
+
</button>
|
|
475
|
+
</li>
|
|
476
|
+
))}
|
|
477
|
+
{into.length === 0 ? <li className="muted">nothing outside itself</li> : null}
|
|
478
|
+
</ul>
|
|
479
|
+
</>
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const folderOf = (file: string): string => file.slice(0, Math.max(0, file.lastIndexOf("/")));
|
|
484
|
+
|
|
485
|
+
const NodeDetail = ({
|
|
486
|
+
atlas,
|
|
487
|
+
id,
|
|
488
|
+
onSelect,
|
|
489
|
+
}: {
|
|
490
|
+
atlas: Atlas;
|
|
491
|
+
id: string;
|
|
492
|
+
onSelect: Props["onSelect"];
|
|
493
|
+
}): React.JSX.Element | null => {
|
|
494
|
+
const node = atlas.manifest.nodes.find((one) => one.id === id);
|
|
495
|
+
if (node === undefined) return null;
|
|
496
|
+
const chain: Array<string> = [];
|
|
497
|
+
let cursor: AtlasNode | undefined = node;
|
|
498
|
+
while (cursor !== undefined) {
|
|
499
|
+
chain.unshift(cursor.id);
|
|
500
|
+
cursor =
|
|
501
|
+
cursor.parent === null
|
|
502
|
+
? undefined
|
|
503
|
+
: atlas.manifest.nodes.find((one) => one.id === cursor?.parent);
|
|
504
|
+
}
|
|
505
|
+
const governed = atlas.files.filter((file) => file.chain.includes(id));
|
|
506
|
+
const violations = atlas.violations.filter((one) => node.rules.includes(one.rule));
|
|
507
|
+
return (
|
|
508
|
+
<>
|
|
509
|
+
<h2 className="mono path">{node.path}</h2>
|
|
510
|
+
<NodeCrumbs atlas={atlas} chain={chain} onSelect={onSelect} />
|
|
511
|
+
<dl className="facts">
|
|
512
|
+
<dt>id</dt>
|
|
513
|
+
<dd className="mono">{node.id}</dd>
|
|
514
|
+
<dt>kind</dt>
|
|
515
|
+
<dd>
|
|
516
|
+
{node.folder ? `folder, layout ${node.layout}` : "file"}
|
|
517
|
+
{node.partial ? ", partial" : ""}
|
|
518
|
+
</dd>
|
|
519
|
+
<dt>written at</dt>
|
|
520
|
+
<dd className="mono">
|
|
521
|
+
{node.position === null ? "—" : `${node.position.file}:${String(node.position.line)}`}
|
|
522
|
+
</dd>
|
|
523
|
+
<dt>governs</dt>
|
|
524
|
+
<dd>{governed.length} files</dd>
|
|
525
|
+
</dl>
|
|
526
|
+
{node.message !== null ? <p className="message">{node.message}</p> : null}
|
|
527
|
+
{node.imports !== null ? (
|
|
528
|
+
<>
|
|
529
|
+
<h3>imports</h3>
|
|
530
|
+
{node.imports.message !== null ? (
|
|
531
|
+
<p className="muted small">{node.imports.message}</p>
|
|
532
|
+
) : null}
|
|
533
|
+
<ul className="chips">
|
|
534
|
+
{node.imports.allowances.map((allowance) => (
|
|
535
|
+
<li
|
|
536
|
+
key={`${allowance.kind}:${allowance.entry}`}
|
|
537
|
+
className={`chip ${allowance.kind} ${allowance.used ? "" : "slack"}`}
|
|
538
|
+
title={allowance.used ? allowance.expanded : "slack: no import uses this entry"}
|
|
539
|
+
>
|
|
540
|
+
{allowance.entry}
|
|
541
|
+
</li>
|
|
542
|
+
))}
|
|
543
|
+
{node.imports.deny.map((denial) => (
|
|
544
|
+
<li key={denial.message} className="chip deny" title={denial.message}>
|
|
545
|
+
deny {denial.match.join(" | ")}
|
|
546
|
+
</li>
|
|
547
|
+
))}
|
|
548
|
+
</ul>
|
|
549
|
+
{node.imports.reset ? <p className="muted small">reset: inherits no allowance</p> : null}
|
|
550
|
+
{node.imports.unrestricted ? (
|
|
551
|
+
<p className="muted small">unrestricted: no allowlist yet</p>
|
|
552
|
+
) : null}
|
|
553
|
+
</>
|
|
554
|
+
) : null}
|
|
555
|
+
{node.importedBy !== null ? (
|
|
556
|
+
<>
|
|
557
|
+
<h3>importedBy</h3>
|
|
558
|
+
<p className="muted small">{node.importedBy.message}</p>
|
|
559
|
+
<ul className="chips">
|
|
560
|
+
{node.importedBy.allow.map((entry) => (
|
|
561
|
+
<li key={entry} className="chip allow">
|
|
562
|
+
{entry}
|
|
563
|
+
</li>
|
|
564
|
+
))}
|
|
565
|
+
</ul>
|
|
566
|
+
</>
|
|
567
|
+
) : null}
|
|
568
|
+
{node.members.length > 0 ? (
|
|
569
|
+
<>
|
|
570
|
+
<h3>members</h3>
|
|
571
|
+
<ul className="list">
|
|
572
|
+
{node.members.map((rule, index) => (
|
|
573
|
+
<li key={index}>
|
|
574
|
+
<span className="family">{rule.subject}</span> {rule.message}
|
|
575
|
+
</li>
|
|
576
|
+
))}
|
|
577
|
+
</ul>
|
|
578
|
+
</>
|
|
579
|
+
) : null}
|
|
580
|
+
{node.surface.length > 0 ? (
|
|
581
|
+
<>
|
|
582
|
+
<h3>surface</h3>
|
|
583
|
+
<ul className="list">
|
|
584
|
+
{node.surface.map((rule, index) => (
|
|
585
|
+
<li key={index}>{rule.message}</li>
|
|
586
|
+
))}
|
|
587
|
+
</ul>
|
|
588
|
+
</>
|
|
589
|
+
) : null}
|
|
590
|
+
{node.requires.length > 0 ? (
|
|
591
|
+
<>
|
|
592
|
+
<h3>requires</h3>
|
|
593
|
+
<ul className="chips">
|
|
594
|
+
{node.requires.map((one) => (
|
|
595
|
+
<li key={one} className="chip">
|
|
596
|
+
{one}
|
|
597
|
+
</li>
|
|
598
|
+
))}
|
|
599
|
+
</ul>
|
|
600
|
+
</>
|
|
601
|
+
) : null}
|
|
602
|
+
<h3>Rules lowered from it</h3>
|
|
603
|
+
<ul className="chips">
|
|
604
|
+
{node.rules.map((rule) => (
|
|
605
|
+
<li key={rule} className="chip rule mono">
|
|
606
|
+
{rule}
|
|
607
|
+
</li>
|
|
608
|
+
))}
|
|
609
|
+
{node.rules.length === 0 ? <li className="muted">none</li> : null}
|
|
610
|
+
</ul>
|
|
611
|
+
<h3>Violations of its rules</h3>
|
|
612
|
+
{violations.length === 0 ? (
|
|
613
|
+
<p className="good">none</p>
|
|
614
|
+
) : (
|
|
615
|
+
<ul className="list">
|
|
616
|
+
{violations.map((one) => (
|
|
617
|
+
<li key={one.fingerprint} className="violation">
|
|
618
|
+
<button
|
|
619
|
+
type="button"
|
|
620
|
+
className="link mono"
|
|
621
|
+
onClick={() => {
|
|
622
|
+
onSelect({ kind: "file", path: one.file });
|
|
623
|
+
}}
|
|
624
|
+
>
|
|
625
|
+
{one.file}
|
|
626
|
+
</button>
|
|
627
|
+
<div className="muted small">{one.message}</div>
|
|
628
|
+
</li>
|
|
629
|
+
))}
|
|
630
|
+
</ul>
|
|
631
|
+
)}
|
|
632
|
+
{governed.length > 0 ? (
|
|
633
|
+
<>
|
|
634
|
+
<h3>Files</h3>
|
|
635
|
+
<ul className="list compact">
|
|
636
|
+
{governed.slice(0, 60).map((file) => (
|
|
637
|
+
<li key={file.path}>
|
|
638
|
+
<button
|
|
639
|
+
type="button"
|
|
640
|
+
className="link mono"
|
|
641
|
+
onClick={() => {
|
|
642
|
+
onSelect({ kind: "file", path: file.path });
|
|
643
|
+
}}
|
|
644
|
+
>
|
|
645
|
+
{file.path}
|
|
646
|
+
</button>
|
|
647
|
+
</li>
|
|
648
|
+
))}
|
|
649
|
+
{governed.length > 60 ? <li className="muted">… {governed.length - 60} more</li> : null}
|
|
650
|
+
</ul>
|
|
651
|
+
</>
|
|
652
|
+
) : null}
|
|
653
|
+
</>
|
|
654
|
+
);
|
|
655
|
+
};
|