@stag-build/phonebook 0.1.3 → 0.1.4
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 +9 -1
- package/dist/cli.js +22 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +21 -2
- package/dist/commands/init.js +33 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/engines/android.d.ts +54 -0
- package/dist/engines/android.js +149 -5
- package/dist/engines/android.js.map +1 -1
- package/dist/engines/ios.d.ts +53 -4
- package/dist/engines/ios.js +121 -25
- package/dist/engines/ios.js.map +1 -1
- package/dist/errors.js +12 -1
- package/dist/errors.js.map +1 -1
- package/dist/ios/snapshotTestClass.d.ts +44 -0
- package/dist/ios/snapshotTestClass.js +115 -0
- package/dist/ios/snapshotTestClass.js.map +1 -1
- package/dist/ios/unrendered.d.ts +38 -0
- package/dist/ios/unrendered.js +92 -0
- package/dist/ios/unrendered.js.map +1 -0
- package/dist/mcp/server.d.ts +28 -0
- package/dist/mcp/server.js +232 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/scan/android.js +142 -16
- package/dist/scan/android.js.map +1 -1
- package/dist/scan/gaps.d.ts +52 -0
- package/dist/scan/gaps.js +169 -0
- package/dist/scan/gaps.js.map +1 -0
- package/dist/scan/ios.d.ts +44 -0
- package/dist/scan/ios.js +445 -41
- package/dist/scan/ios.js.map +1 -1
- package/dist/scan/scope.d.ts +30 -0
- package/dist/scan/scope.js +208 -0
- package/dist/scan/scope.js.map +1 -0
- package/dist/scan/stats.d.ts +12 -0
- package/dist/scan/stats.js +24 -0
- package/dist/scan/stats.js.map +1 -0
- package/dist/scan/types.d.ts +87 -1
- package/package.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { realpath } from 'node:fs/promises';
|
|
4
|
+
import { relative, resolve } from 'node:path';
|
|
5
|
+
import { computeStats } from './stats.js';
|
|
6
|
+
import { componentsNamedIn } from './ios.js';
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
/**
|
|
9
|
+
* Narrow a finished report to the components declared in `paths`.
|
|
10
|
+
*
|
|
11
|
+
* The scan itself is never narrowed, and that distinction is the whole design.
|
|
12
|
+
* Which types are enums, what a project's preview helper supplies, which
|
|
13
|
+
* locales ship — all of it is a fact about the project, not about one file, and
|
|
14
|
+
* a scan that walked only the changed files would get every one of them wrong.
|
|
15
|
+
* So the walk stays whole and the report is filtered afterwards, which costs a
|
|
16
|
+
* few hundred milliseconds and buys an answer that is still correct.
|
|
17
|
+
*
|
|
18
|
+
* What it saves is the reader. A run that touched five components was handed
|
|
19
|
+
* five hundred gaps, of which ten were its own; the rest are a backlog nobody
|
|
20
|
+
* asked it about, and an agent cannot tell which is which.
|
|
21
|
+
*/
|
|
22
|
+
export function scopeReport(report, paths) {
|
|
23
|
+
if (paths.length === 0)
|
|
24
|
+
return report;
|
|
25
|
+
const wanted = paths.map((p) => p.replace(/^\.\//, '').replace(/\/+$/, ''));
|
|
26
|
+
const matches = (file) => wanted.some((p) => file === p || file.startsWith(`${p}/`));
|
|
27
|
+
const components = report.components.filter((c) => matches(c.file));
|
|
28
|
+
const orphanPreviews = report.orphanPreviews.filter((p) => matches(p.file));
|
|
29
|
+
const changed = new Set(components.map((c) => c.name));
|
|
30
|
+
const previewsOfWhatChanged = previewsRendering(report, changed, matches);
|
|
31
|
+
const uncoveredUsesOfWhatChanged = uncoveredUses(report, changed, matches);
|
|
32
|
+
return {
|
|
33
|
+
...report,
|
|
34
|
+
components,
|
|
35
|
+
orphanPreviews,
|
|
36
|
+
stats: computeStats(components, orphanPreviews),
|
|
37
|
+
scope: {
|
|
38
|
+
paths: wanted,
|
|
39
|
+
componentsScanned: report.components.length,
|
|
40
|
+
...(previewsOfWhatChanged.length > 0 ? { previewsOfWhatChanged } : {}),
|
|
41
|
+
...(uncoveredUsesOfWhatChanged.length > 0 ? { uncoveredUsesOfWhatChanged } : {}),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Previews outside the scope that put something inside it on screen.
|
|
47
|
+
*
|
|
48
|
+
* A component is rarely previewed only where it is declared: edit a row and the
|
|
49
|
+
* preview that shows it may live in the list's file, which the filter drops. The
|
|
50
|
+
* agent can read these and decide — they are as likely to be fine as not.
|
|
51
|
+
*
|
|
52
|
+
* Reached through the component graph rather than the preview's own text,
|
|
53
|
+
* because a preview usually names one view and shows a dozen. IceCubes made
|
|
54
|
+
* the point: changing `StatusRowDetailView` matched no preview at all, because
|
|
55
|
+
* the one preview that renders it says `StatusRowView(` and nothing else — the
|
|
56
|
+
* detail view is three levels down its body. Matching the text alone answers a
|
|
57
|
+
* question nobody asked, which is which previews mention a name.
|
|
58
|
+
*/
|
|
59
|
+
function previewsRendering(report, changed, inScope) {
|
|
60
|
+
const found = [];
|
|
61
|
+
const previews = [...report.components.flatMap((c) => c.previews), ...report.orphanPreviews];
|
|
62
|
+
const known = new Set(report.components.map((c) => c.name));
|
|
63
|
+
// Guarded children are left to the ask list below. A preview that reaches
|
|
64
|
+
// something only through an `if` has not been shown to render it, and saying
|
|
65
|
+
// both here and there would be two answers to one question.
|
|
66
|
+
const uses = new Map(report.components.map((c) => {
|
|
67
|
+
const guarded = new Set((c.conditionalUses ?? []).map((u) => u.name));
|
|
68
|
+
return [c.name, (c.uses ?? []).filter((n) => !guarded.has(n))];
|
|
69
|
+
}));
|
|
70
|
+
for (const preview of previews) {
|
|
71
|
+
if (inScope(preview.file))
|
|
72
|
+
continue;
|
|
73
|
+
const renders = reachedFrom(componentsNamedIn(preview.annotationText ?? '', known), uses, changed);
|
|
74
|
+
if (renders.length === 0)
|
|
75
|
+
continue;
|
|
76
|
+
found.push({ name: preview.displayName ?? preview.name, file: preview.file, line: preview.line, renders });
|
|
77
|
+
}
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Which of `targets` are reachable from `seeds` by following `uses`.
|
|
82
|
+
*
|
|
83
|
+
* A view tree, walked breadth-first. `seen` is what keeps a cycle — two views
|
|
84
|
+
* that render each other under different conditions, which SwiftUI allows —
|
|
85
|
+
* from being an infinite descent.
|
|
86
|
+
*/
|
|
87
|
+
function reachedFrom(seeds, uses, targets) {
|
|
88
|
+
const hit = new Set();
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const queue = [...seeds];
|
|
91
|
+
while (queue.length > 0) {
|
|
92
|
+
const name = queue.shift();
|
|
93
|
+
if (seen.has(name))
|
|
94
|
+
continue;
|
|
95
|
+
seen.add(name);
|
|
96
|
+
if (targets.has(name))
|
|
97
|
+
hit.add(name);
|
|
98
|
+
queue.push(...(uses.get(name) ?? []));
|
|
99
|
+
}
|
|
100
|
+
return [...hit];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Views outside the scope that render something inside it and have no preview
|
|
104
|
+
* of their own.
|
|
105
|
+
*
|
|
106
|
+
* Two ways to end up here, and the second is the one that took two runs against
|
|
107
|
+
* IceCubes to see. A view with no preview at all shows the change nowhere. A
|
|
108
|
+
* view that renders it inside an `if` has a preview that may show nothing of it
|
|
109
|
+
* either — StatusRowView is previewed as a timeline row, and the detail view it
|
|
110
|
+
* was changed to render only appears when focused. "Has a preview" answered the
|
|
111
|
+
* wrong question, and answered it yes.
|
|
112
|
+
*
|
|
113
|
+
* Whether either context deserves a preview is a judgment about the product,
|
|
114
|
+
* which is why this is something to ask the designer rather than a gap to
|
|
115
|
+
* close.
|
|
116
|
+
*
|
|
117
|
+
* Direct users only, unlike the list above, and the asymmetry is the point. A
|
|
118
|
+
* fact can be long and still be worth reading; a question cannot. Follow the
|
|
119
|
+
* graph from a component every screen shows and the ask list becomes every
|
|
120
|
+
* screen in the app, which is not something anyone can answer.
|
|
121
|
+
*/
|
|
122
|
+
function uncoveredUses(report, changed, inScope) {
|
|
123
|
+
const found = [];
|
|
124
|
+
for (const component of report.components) {
|
|
125
|
+
if (inScope(component.file))
|
|
126
|
+
continue;
|
|
127
|
+
const uses = (component.uses ?? []).filter((name) => changed.has(name));
|
|
128
|
+
if (uses.length === 0)
|
|
129
|
+
continue;
|
|
130
|
+
if (component.previews.length === 0) {
|
|
131
|
+
found.push({ component: component.name, file: component.file, line: component.line, uses, reason: 'no-preview' });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// It has a preview. That only settles the question for what it shows
|
|
135
|
+
// unconditionally.
|
|
136
|
+
const guarded = (component.conditionalUses ?? []).filter((u) => changed.has(u.name));
|
|
137
|
+
if (guarded.length === 0)
|
|
138
|
+
continue;
|
|
139
|
+
// The first guarded render carries the line and the condition. Listing one
|
|
140
|
+
// condition per child would make the question longer than the answer, and
|
|
141
|
+
// the reader opens the file either way.
|
|
142
|
+
found.push({
|
|
143
|
+
component: component.name,
|
|
144
|
+
file: component.file,
|
|
145
|
+
line: guarded[0].line,
|
|
146
|
+
uses: guarded.map((u) => u.name),
|
|
147
|
+
reason: 'conditional',
|
|
148
|
+
guard: guarded[0].guard,
|
|
149
|
+
previews: component.previews.map((p) => ({
|
|
150
|
+
name: p.displayName ?? p.name,
|
|
151
|
+
file: p.file,
|
|
152
|
+
line: p.line,
|
|
153
|
+
})),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return found;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Files with uncommitted changes, relative to the project directory.
|
|
160
|
+
*
|
|
161
|
+
* `git status --porcelain` reports staged, unstaged and untracked in one pass,
|
|
162
|
+
* which is what an agent's turn leaves behind: it edits and adds, it does not
|
|
163
|
+
* commit. Paths come back relative to the repository root, so they are rebased
|
|
164
|
+
* onto the project directory — the two differ whenever phonebook.config.json
|
|
165
|
+
* sits in a subdirectory of a larger repository.
|
|
166
|
+
*
|
|
167
|
+
* A directory that is not a repository, or a git that is not installed, is not
|
|
168
|
+
* an error here. It means the caller cannot scope by change and gets the whole
|
|
169
|
+
* project, which is the behaviour they had before asking.
|
|
170
|
+
*/
|
|
171
|
+
export async function changedFiles(projectDir) {
|
|
172
|
+
let root;
|
|
173
|
+
let status;
|
|
174
|
+
let base;
|
|
175
|
+
try {
|
|
176
|
+
root = (await run('git', ['rev-parse', '--show-toplevel'], { cwd: projectDir })).stdout.trim();
|
|
177
|
+
status = (await run('git', ['status', '--porcelain'], { cwd: projectDir })).stdout;
|
|
178
|
+
// git answers in real paths. The caller's directory may be reached through
|
|
179
|
+
// a symlink — every macOS temp directory is — and comparing the two
|
|
180
|
+
// unresolved puts the whole repository outside itself.
|
|
181
|
+
base = await realpath(projectDir);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const files = new Set();
|
|
187
|
+
for (const line of status.split('\n')) {
|
|
188
|
+
if (line.trim() === '')
|
|
189
|
+
continue;
|
|
190
|
+
// "XY path", or "XY old -> new" for a rename: the new name is the one that
|
|
191
|
+
// still exists to be scanned.
|
|
192
|
+
let path = line.slice(3);
|
|
193
|
+
const arrow = path.indexOf(' -> ');
|
|
194
|
+
if (arrow !== -1)
|
|
195
|
+
path = path.slice(arrow + 4);
|
|
196
|
+
path = path.trim().replace(/^"|"$/g, '');
|
|
197
|
+
if (path === '')
|
|
198
|
+
continue;
|
|
199
|
+
const fromProject = relative(base, resolve(root, path));
|
|
200
|
+
// Outside the project directory: another module of the same repository,
|
|
201
|
+
// which this scan never walked and cannot report on.
|
|
202
|
+
if (fromProject.startsWith('..'))
|
|
203
|
+
continue;
|
|
204
|
+
files.add(fromProject);
|
|
205
|
+
}
|
|
206
|
+
return [...files];
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/scan/scope.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ScannedComponent, ScannedPreview, CoverageReport } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Totals over a set of components and the previews that could not be matched
|
|
4
|
+
* to one.
|
|
5
|
+
*
|
|
6
|
+
* Shared by both scanners and by `scopeReport`, which needs the same numbers
|
|
7
|
+
* over a subset. The self-preview rule is Android's — a composable annotated
|
|
8
|
+
* with `@Preview` is its own preview — and it costs nothing on iOS, where
|
|
9
|
+
* `#Preview` is always a separate top-level block and the count is therefore
|
|
10
|
+
* always zero.
|
|
11
|
+
*/
|
|
12
|
+
export declare function computeStats(components: ScannedComponent[], orphanPreviews: ScannedPreview[]): CoverageReport['stats'];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Totals over a set of components and the previews that could not be matched
|
|
3
|
+
* to one.
|
|
4
|
+
*
|
|
5
|
+
* Shared by both scanners and by `scopeReport`, which needs the same numbers
|
|
6
|
+
* over a subset. The self-preview rule is Android's — a composable annotated
|
|
7
|
+
* with `@Preview` is its own preview — and it costs nothing on iOS, where
|
|
8
|
+
* `#Preview` is always a separate top-level block and the count is therefore
|
|
9
|
+
* always zero.
|
|
10
|
+
*/
|
|
11
|
+
export function computeStats(components, orphanPreviews) {
|
|
12
|
+
const allPreviews = [...components.flatMap((c) => c.previews), ...orphanPreviews];
|
|
13
|
+
return {
|
|
14
|
+
components: components.length,
|
|
15
|
+
withPreview: components.filter((c) => c.previews.length > 0).length,
|
|
16
|
+
withDarkPreview: components.filter((c) => c.previews.some((p) => p.dark)).length,
|
|
17
|
+
totalPreviews: components.reduce((sum, c) => sum + c.previews.length, 0) + orphanPreviews.length,
|
|
18
|
+
hintCount: allPreviews.reduce((sum, p) => sum + (p.hints?.length ?? 0), 0),
|
|
19
|
+
gapCount: components.reduce((sum, c) => sum + (c.gaps?.length ?? 0), 0),
|
|
20
|
+
componentsWithGaps: components.filter((c) => (c.gaps ?? []).some((g) => g.severity === 'warning')).length,
|
|
21
|
+
selfPreviewed: components.filter((c) => c.previews.some((p) => p.name === c.name && p.line === c.line)).length,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/scan/stats.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,UAA8B,EAC9B,cAAgC;IAEhC,MAAM,WAAW,GAAG,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,GAAG,cAAc,CAAC,CAAC;IAClF,OAAO;QACL,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,WAAW,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM;QACnE,eAAe,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QAChF,aAAa,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,cAAc,CAAC,MAAM;QAChG,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1E,QAAQ,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;QACvE,kBAAkB,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAC1C,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CACrD,CAAC,MAAM;QACR,aAAa,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACrC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAC/D,CAAC,MAAM;KACT,CAAC;AACJ,CAAC"}
|
package/dist/scan/types.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* can find components with missing previews/states and add them as code.
|
|
5
5
|
*/
|
|
6
6
|
import type { PreviewHint } from './hints.js';
|
|
7
|
-
|
|
7
|
+
import type { ComponentProperty, CoverageGap } from './gaps.js';
|
|
8
|
+
export type { PreviewHint, ComponentProperty, CoverageGap };
|
|
8
9
|
export interface ScannedPreview {
|
|
9
10
|
/** Preview function name (Android) or #Preview display name / "unnamed" (iOS) */
|
|
10
11
|
name: string;
|
|
@@ -25,6 +26,22 @@ export interface ScannedPreview {
|
|
|
25
26
|
* annotation doesn't declare. See src/scan/hints.ts. */
|
|
26
27
|
hints?: PreviewHint[];
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* A component rendered only behind a condition, and enough about the render to
|
|
31
|
+
* go and look at it.
|
|
32
|
+
*
|
|
33
|
+
* The condition text is the payload. "Renders it inside an if" names a shape;
|
|
34
|
+
* `if isFocused, !isCompact` names the state a preview would have to set, which
|
|
35
|
+
* is the difference between a question and a chore.
|
|
36
|
+
*/
|
|
37
|
+
export interface ConditionalUse {
|
|
38
|
+
/** The component rendered behind the condition. */
|
|
39
|
+
name: string;
|
|
40
|
+
/** The condition as written, whitespace collapsed. */
|
|
41
|
+
guard: string;
|
|
42
|
+
/** Line of the guarded render, in the file that declares the parent. */
|
|
43
|
+
line: number;
|
|
44
|
+
}
|
|
28
45
|
export interface ScannedComponent {
|
|
29
46
|
/** Composable function name (Android) or View struct name (iOS) */
|
|
30
47
|
name: string;
|
|
@@ -32,12 +49,78 @@ export interface ScannedComponent {
|
|
|
32
49
|
line: number;
|
|
33
50
|
/** Previews matched to this component (same file + name-prefix heuristic) */
|
|
34
51
|
previews: ScannedPreview[];
|
|
52
|
+
/** Stored properties (iOS) or composable parameters (Android). What the
|
|
53
|
+
* component's states are derived from. */
|
|
54
|
+
properties?: ComponentProperty[];
|
|
55
|
+
/** Types read via `@Environment(X.self)` (iOS). An unsatisfied one traps at
|
|
56
|
+
* render, so a preview that omits it is broken rather than incomplete. */
|
|
57
|
+
environmentTypes?: string[];
|
|
58
|
+
/** Other discovered components this one renders in its body (iOS). What makes
|
|
59
|
+
* it possible to say who else is affected when a component changes. */
|
|
60
|
+
uses?: string[];
|
|
61
|
+
/**
|
|
62
|
+
* The subset of `uses` this component renders only inside an `if`, `switch`
|
|
63
|
+
* or `guard`. Its own preview may never reach these.
|
|
64
|
+
*/
|
|
65
|
+
conditionalUses?: ConditionalUse[];
|
|
66
|
+
/** Previews this component should have and doesn't. See src/scan/gaps.ts. */
|
|
67
|
+
gaps?: CoverageGap[];
|
|
35
68
|
}
|
|
36
69
|
export interface CoverageReport {
|
|
37
70
|
platform: 'android' | 'ios';
|
|
38
71
|
components: ScannedComponent[];
|
|
39
72
|
/** Previews that could not be matched to any discovered component */
|
|
40
73
|
orphanPreviews: ScannedPreview[];
|
|
74
|
+
/** Locales the project ships beyond its development language. Empty when the
|
|
75
|
+
* project is not localized, which keeps the localization gap silent there. */
|
|
76
|
+
extraLocales: string[];
|
|
77
|
+
/** Present when the report was narrowed to a subset of the project's files.
|
|
78
|
+
* The scan still walked everything; only this report is filtered. */
|
|
79
|
+
scope?: {
|
|
80
|
+
paths: string[];
|
|
81
|
+
/** Components the scan found, before the filter. */
|
|
82
|
+
componentsScanned: number;
|
|
83
|
+
/**
|
|
84
|
+
* Previews outside the scope that put a component inside it on screen,
|
|
85
|
+
* directly or through the views they render. Editing a row changes what
|
|
86
|
+
* these show, and the filter would otherwise hide them. A fact to act on,
|
|
87
|
+
* not a defect.
|
|
88
|
+
*/
|
|
89
|
+
previewsOfWhatChanged?: {
|
|
90
|
+
name: string;
|
|
91
|
+
file: string;
|
|
92
|
+
line: number;
|
|
93
|
+
/** In-scope components this preview reaches. Its own text may name none of them. */
|
|
94
|
+
renders: string[];
|
|
95
|
+
}[];
|
|
96
|
+
/**
|
|
97
|
+
* Views outside the scope that render a component inside it directly and
|
|
98
|
+
* have no preview of their own, so nothing shows the change in that
|
|
99
|
+
* context. Whether that context is worth covering is a judgment about the
|
|
100
|
+
* product, which is why this is a question for the designer rather than a
|
|
101
|
+
* gap — and why it stops at one hop where `previewsOfWhatChanged` does not.
|
|
102
|
+
*/
|
|
103
|
+
uncoveredUsesOfWhatChanged?: {
|
|
104
|
+
component: string;
|
|
105
|
+
file: string;
|
|
106
|
+
/** The guarded render when `reason` is `conditional`, so the line points at
|
|
107
|
+
* the branch in question; the component's declaration otherwise. */
|
|
108
|
+
line: number;
|
|
109
|
+
/** In-scope components it renders. */
|
|
110
|
+
uses: string[];
|
|
111
|
+
/** Why nothing shows the change here. */
|
|
112
|
+
reason: 'no-preview' | 'conditional';
|
|
113
|
+
/** `conditional` only: the condition a preview would have to satisfy. */
|
|
114
|
+
guard?: string;
|
|
115
|
+
/** `conditional` only: the previews that exist and do not enter the branch.
|
|
116
|
+
* Naming them is what makes this answerable — the reader opens one. */
|
|
117
|
+
previews?: {
|
|
118
|
+
name: string;
|
|
119
|
+
file: string;
|
|
120
|
+
line: number;
|
|
121
|
+
}[];
|
|
122
|
+
}[];
|
|
123
|
+
};
|
|
41
124
|
/** Totals for a quick summary */
|
|
42
125
|
stats: {
|
|
43
126
|
components: number;
|
|
@@ -45,6 +128,9 @@ export interface CoverageReport {
|
|
|
45
128
|
withDarkPreview: number;
|
|
46
129
|
totalPreviews: number;
|
|
47
130
|
hintCount: number;
|
|
131
|
+
gapCount: number;
|
|
132
|
+
/** Components with at least one warning-severity gap. */
|
|
133
|
+
componentsWithGaps: number;
|
|
48
134
|
/** Components whose only preview is themselves (the @Preview-on-the-composable
|
|
49
135
|
* pattern for screen-level composables with default parameters). */
|
|
50
136
|
selfPreviewed: number;
|
package/package.json
CHANGED