@fusion.dev/helix-diagnostics 0.135.0 → 0.137.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 +138 -12
- package/dist/analyze.d.ts +33 -3
- package/dist/analyze.js +50 -2
- package/dist/config.d.ts +71 -0
- package/dist/config.js +126 -0
- package/dist/console.d.ts +28 -5
- package/dist/console.js +60 -21
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +42 -0
- package/dist/index.d.ts +46 -10
- package/dist/index.js +68 -8
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.js +12 -1
- package/dist/report.d.ts +194 -2
- package/dist/report.js +17 -1
- package/dist/rules.d.ts +34 -0
- package/dist/rules.js +76 -0
- package/dist/usage.d.ts +50 -0
- package/dist/usage.js +147 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a finding is about, who fixes it, and which finding it is from one run
|
|
3
|
+
* to the next.
|
|
4
|
+
*
|
|
5
|
+
* **The fingerprint is what turns findings into issues.** Without it, a
|
|
6
|
+
* dashboard can count findings per run and cannot say whether the one in this
|
|
7
|
+
* run is the one from last week, fixed, or back again.
|
|
8
|
+
*/
|
|
9
|
+
import type { Published } from './manifest.js';
|
|
10
|
+
import type { Identified, Raw } from './report.js';
|
|
11
|
+
/**
|
|
12
|
+
* A selector with what a build changes taken out, so the same rule in the
|
|
13
|
+
* next build reads the same.
|
|
14
|
+
*
|
|
15
|
+
* **Hashed class names**, which CSS Modules write and every bundler hashes its
|
|
16
|
+
* own way:
|
|
17
|
+
*
|
|
18
|
+
* - Next.js: `trace-graph-module__lFhy0q__traceFrame`, to `trace-graph-module__traceFrame`.
|
|
19
|
+
* - webpack's default: `card__title___3xYz1`, to `card__title`.
|
|
20
|
+
* - Vite's default: `_title_1a2b3_7`, to `_title`.
|
|
21
|
+
*
|
|
22
|
+
* webpack's is taken out first: its `___` would otherwise read as the end of
|
|
23
|
+
* a Next.js hash. And spacing, which a minifier rewrites.
|
|
24
|
+
*/
|
|
25
|
+
export declare const normalize: (subject: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* A finding's identity across runs: its kind, its tag, and its normalized
|
|
28
|
+
* subject. **The stylesheet is left out**, since a bundler puts a content
|
|
29
|
+
* hash in a file's name, so the same mistake in two stylesheets is one issue
|
|
30
|
+
* seen twice, which is how a reader thinks of it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const fingerprintOf: (finding: Pick<Raw, 'kind' | 'subject' | 'tag'>) => string;
|
|
33
|
+
/** A finding with its category, owner, family, and fingerprint. */
|
|
34
|
+
export declare const identify: (finding: Raw, tags: Map<string, Published>) => Identified;
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const CATEGORY = {
|
|
2
|
+
'dead-hook': 'styling',
|
|
3
|
+
'dead-part': 'styling',
|
|
4
|
+
'duplicate-registration': 'integrity',
|
|
5
|
+
'manifest-skew': 'integrity',
|
|
6
|
+
'unforwarded-part': 'styling',
|
|
7
|
+
'unknown-tag': 'integrity',
|
|
8
|
+
};
|
|
9
|
+
const OWNER = {
|
|
10
|
+
'dead-hook': 'consumer',
|
|
11
|
+
'dead-part': 'consumer',
|
|
12
|
+
'duplicate-registration': 'consumer',
|
|
13
|
+
'manifest-skew': 'consumer',
|
|
14
|
+
'unforwarded-part': 'helix',
|
|
15
|
+
'unknown-tag': 'consumer',
|
|
16
|
+
};
|
|
17
|
+
export const normalize = (subject) => subject
|
|
18
|
+
.replace(/___[A-Za-z0-9_-]{5}\b/g, '')
|
|
19
|
+
.replace(/__[A-Za-z0-9_-]{6}__/g, '__')
|
|
20
|
+
.replace(/\b(_[A-Za-z][A-Za-z0-9-]*)_[a-z0-9]{5}_\d+\b/g, '$1')
|
|
21
|
+
.replace(/\s+/g, ' ')
|
|
22
|
+
.trim();
|
|
23
|
+
const hash = (text) => {
|
|
24
|
+
let one = 0xdeadbeef;
|
|
25
|
+
let two = 0x41c6ce57;
|
|
26
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
27
|
+
const code = text.charCodeAt(at);
|
|
28
|
+
one = Math.imul(one ^ code, 2654435761);
|
|
29
|
+
two = Math.imul(two ^ code, 1597334677);
|
|
30
|
+
}
|
|
31
|
+
one = Math.imul(one ^ (one >>> 16), 2246822507) ^ Math.imul(two ^ (two >>> 13), 3266489909);
|
|
32
|
+
two = Math.imul(two ^ (two >>> 16), 2246822507) ^ Math.imul(one ^ (one >>> 13), 3266489909);
|
|
33
|
+
return (4294967296 * (2097151 & two) + (one >>> 0)).toString(16).padStart(14, '0');
|
|
34
|
+
};
|
|
35
|
+
export const fingerprintOf = (finding) => hash(`${finding.kind}\u0000${finding.tag}\u0000${normalize(finding.subject)}`);
|
|
36
|
+
export const identify = (finding, tags) => ({
|
|
37
|
+
...finding,
|
|
38
|
+
category: CATEGORY[finding.kind],
|
|
39
|
+
family: tags.get(finding.tag)?.family,
|
|
40
|
+
fingerprint: fingerprintOf(finding),
|
|
41
|
+
owner: OWNER[finding.kind],
|
|
42
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -20,23 +20,33 @@
|
|
|
20
20
|
* report(diagnose({ tiers: [{ manifest: styled, name: '@fusion.dev/helix' }] }))
|
|
21
21
|
* ```
|
|
22
22
|
*
|
|
23
|
-
* **
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* readers we were not. Findings go where the caller puts them.
|
|
23
|
+
* **Nothing here sends anything.** `diagnose` returns a report and `report`
|
|
24
|
+
* prints it. Where else a report goes is a sink's decision, and every sink
|
|
25
|
+
* that leaves the page is one the caller adds.
|
|
27
26
|
*
|
|
28
27
|
* **Development only.** Import it from a dev-only entry: it walks every
|
|
29
28
|
* stylesheet in the page and has no business in a production bundle.
|
|
30
29
|
*/
|
|
31
30
|
export { collect } from './collect.js';
|
|
31
|
+
export { type Config, DEFAULTS, pagePattern } from './config.js';
|
|
32
32
|
export { report } from './console.js';
|
|
33
|
+
export { fingerprintOf, normalize } from './identity.js';
|
|
33
34
|
export type { ManifestSource, Published, Tier } from './manifest.js';
|
|
34
35
|
export { index, nearest } from './manifest.js';
|
|
35
|
-
export type { Coverage, Finding, Origin, Report, Unreadable } from './report.js';
|
|
36
|
+
export type { Category, Configuration, Coverage, Environment, Finding, FindingCategory, Kind, Layer, Origin, Owner, Report, Severity, Unreadable, Usage, } from './report.js';
|
|
37
|
+
export { CATEGORIES, KINDS } from './report.js';
|
|
38
|
+
export { type Override, type Rules, SHIPPED_RULES } from './rules.js';
|
|
39
|
+
export { type Watcher, watch } from './usage.js';
|
|
40
|
+
export { VERSION } from './version.js';
|
|
41
|
+
import { type Config } from './config.js';
|
|
36
42
|
import type { Tier } from './manifest.js';
|
|
37
|
-
import type { Report } from './report.js';
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
import type { Environment, Report } from './report.js';
|
|
44
|
+
import { type Watcher } from './usage.js';
|
|
45
|
+
/**
|
|
46
|
+
* What to look at, what to compare it against, and the settings for this
|
|
47
|
+
* call, which win over the project's file field by field.
|
|
48
|
+
*/
|
|
49
|
+
export interface Options extends Config {
|
|
40
50
|
/**
|
|
41
51
|
* `duplicateRegistrations` from `@fusion.dev/helix-core`, when it is installed.
|
|
42
52
|
*
|
|
@@ -48,6 +58,17 @@ export interface Options {
|
|
|
48
58
|
* from outside and cannot name the losing class.
|
|
49
59
|
*/
|
|
50
60
|
duplicates?: () => string[];
|
|
61
|
+
/**
|
|
62
|
+
* Where this run happens, as the project declares it. **Never inferred**:
|
|
63
|
+
* a bundler's `development` condition misfires both ways, and a run whose
|
|
64
|
+
* environment nobody declared is never sent.
|
|
65
|
+
*/
|
|
66
|
+
environment?: Environment | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The project's own file, `helix-diagnostics.json`, parsed. The build reads
|
|
69
|
+
* the file, since the page cannot: import the JSON and pass it here.
|
|
70
|
+
*/
|
|
71
|
+
file?: Config | undefined;
|
|
51
72
|
/**
|
|
52
73
|
* The document to scan. Defaults to the ambient one.
|
|
53
74
|
*
|
|
@@ -55,6 +76,12 @@ export interface Options {
|
|
|
55
76
|
* an iframe it owns can scan that too.
|
|
56
77
|
*/
|
|
57
78
|
from?: Document;
|
|
79
|
+
/**
|
|
80
|
+
* The page's path, for a router that knows its own pattern, such as
|
|
81
|
+
* `/products/[id]`. Otherwise read from the location and made a pattern, as
|
|
82
|
+
* `detail.pages` says.
|
|
83
|
+
*/
|
|
84
|
+
page?: string | undefined;
|
|
58
85
|
/**
|
|
59
86
|
* The manifests to check against, one per installed tier.
|
|
60
87
|
*
|
|
@@ -63,9 +90,14 @@ export interface Options {
|
|
|
63
90
|
* invisible to either manifest alone.
|
|
64
91
|
*/
|
|
65
92
|
tiers: Tier[];
|
|
93
|
+
/**
|
|
94
|
+
* A watcher from `watch()`, so usage counts every element that rendered
|
|
95
|
+
* since it began. Without one, usage counts the elements in the page now.
|
|
96
|
+
*/
|
|
97
|
+
watcher?: Watcher | undefined;
|
|
66
98
|
}
|
|
67
99
|
/**
|
|
68
|
-
* One
|
|
100
|
+
* One run over the page.
|
|
69
101
|
*
|
|
70
102
|
* **It returns coverage and findings together and offers no way to have one
|
|
71
103
|
* without the other.** A collector that cannot read cross-origin sheets reports
|
|
@@ -75,5 +107,9 @@ export interface Options {
|
|
|
75
107
|
*
|
|
76
108
|
* Cheap enough to call on a route change. There is no observer and no polling
|
|
77
109
|
* loop: sheets that arrive later are found by calling it again.
|
|
110
|
+
*
|
|
111
|
+
* **A manifest from another version is refused**, with one finding that says
|
|
112
|
+
* so in place of every other: by version where both sides carry one, and
|
|
113
|
+
* otherwise by a part the manifest does not list rendering in the page.
|
|
78
114
|
*/
|
|
79
|
-
export declare const diagnose: (
|
|
115
|
+
export declare const diagnose: (options: Options) => Report;
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,81 @@
|
|
|
1
1
|
export { collect } from './collect.js';
|
|
2
|
+
export { DEFAULTS, pagePattern } from './config.js';
|
|
2
3
|
export { report } from './console.js';
|
|
4
|
+
export { fingerprintOf, normalize } from './identity.js';
|
|
3
5
|
export { index, nearest } from './manifest.js';
|
|
4
|
-
|
|
6
|
+
export { CATEGORIES, KINDS } from './report.js';
|
|
7
|
+
export { SHIPPED_RULES } from './rules.js';
|
|
8
|
+
export { watch } from './usage.js';
|
|
9
|
+
export { VERSION } from './version.js';
|
|
10
|
+
import { analyze, registrations, skew, versionSkew } from './analyze.js';
|
|
5
11
|
import { collect } from './collect.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
12
|
+
import { pagePattern, resolveConfig } from './config.js';
|
|
13
|
+
import { identify } from './identity.js';
|
|
14
|
+
import { index, versionsOf } from './manifest.js';
|
|
15
|
+
import { applyRules, overridesOf } from './rules.js';
|
|
16
|
+
import { listed, loadedVersions, styled, walk } from './usage.js';
|
|
17
|
+
import { VERSION } from './version.js';
|
|
18
|
+
const runId = () => globalThis.crypto?.randomUUID?.() ??
|
|
19
|
+
`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
|
|
20
|
+
export const diagnose = (options) => {
|
|
21
|
+
const started = performance.now();
|
|
22
|
+
const at = new Date().toISOString();
|
|
23
|
+
const { duplicates, environment, file, from, page, tiers, watcher, ...call } = options;
|
|
8
24
|
if (tiers.length === 0) {
|
|
9
25
|
throw new Error('diagnose() needs at least one manifest. Without one every part name is unknown and every finding would be noise.');
|
|
10
26
|
}
|
|
27
|
+
const resolved = resolveConfig(file, call);
|
|
28
|
+
const { detail } = resolved.configuration;
|
|
11
29
|
const document_ = from ?? globalThis.document;
|
|
12
|
-
const { answered, coverage, rules } = collect(document_);
|
|
30
|
+
const { answered, coverage, rules: located } = collect(document_);
|
|
13
31
|
const tags = index(tiers);
|
|
32
|
+
const counted = watcher ? watcher.counts() : walk(document_);
|
|
33
|
+
const seen = loadedVersions();
|
|
34
|
+
const manifests = versionsOf(tiers);
|
|
35
|
+
const byVersion = versionSkew(manifests, seen);
|
|
36
|
+
const known = seen.length > 0 && Object.keys(manifests).length > 0;
|
|
37
|
+
const skewed = applyRules((byVersion.length > 0 ? byVersion : known ? [] : skew(tags, document_)).map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
38
|
+
const judged = skewed.findings.length > 0
|
|
39
|
+
? { findings: [], silenced: [] }
|
|
40
|
+
: applyRules([
|
|
41
|
+
...analyze(located, tags, document_, answered),
|
|
42
|
+
...registrations(document_, duplicates),
|
|
43
|
+
].map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
44
|
+
const path = page ?? globalThis.location?.pathname;
|
|
45
|
+
const shownPage = path === undefined || detail.pages === 'none'
|
|
46
|
+
? undefined
|
|
47
|
+
: detail.pages === 'patterns'
|
|
48
|
+
? pagePattern(path)
|
|
49
|
+
: path;
|
|
14
50
|
return {
|
|
51
|
+
configuration: {
|
|
52
|
+
...resolved.configuration,
|
|
53
|
+
overrides: overridesOf(file?.rules, call.rules),
|
|
54
|
+
silenced: [...skewed.silenced, ...judged.silenced],
|
|
55
|
+
},
|
|
15
56
|
coverage,
|
|
16
|
-
findings: [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
57
|
+
findings: [...skewed.findings, ...judged.findings],
|
|
58
|
+
project: resolved.project,
|
|
59
|
+
run: {
|
|
60
|
+
at,
|
|
61
|
+
duration: Math.round(performance.now() - started),
|
|
62
|
+
id: runId(),
|
|
63
|
+
source: 'page',
|
|
64
|
+
...(environment === undefined ? {} : { environment }),
|
|
65
|
+
...(shownPage === undefined ? {} : { page: shownPage }),
|
|
66
|
+
},
|
|
67
|
+
schema: 1,
|
|
68
|
+
usage: {
|
|
69
|
+
...listed(counted, tags),
|
|
70
|
+
...styled(located, tags, answered, detail.hookValues),
|
|
71
|
+
counted: watcher ? 'watched' : 'walked',
|
|
72
|
+
},
|
|
73
|
+
versions: { collector: VERSION, helix: seen, manifests, rules: VERSION },
|
|
74
|
+
withheld: Object.entries(resolved.send)
|
|
75
|
+
.filter(([, sent]) => !sent.on)
|
|
76
|
+
.map(([category, sent]) => ({
|
|
77
|
+
category: category,
|
|
78
|
+
decidedBy: sent.decidedBy,
|
|
79
|
+
})),
|
|
20
80
|
};
|
|
21
81
|
};
|
package/dist/manifest.d.ts
CHANGED
|
@@ -16,7 +16,18 @@
|
|
|
16
16
|
*/
|
|
17
17
|
/** The subset of the manifest schema this reads. Nothing else is touched. */
|
|
18
18
|
export interface ManifestSource {
|
|
19
|
+
/**
|
|
20
|
+
* The package and version the manifest describes, which `helix manifest
|
|
21
|
+
* build` writes from the package's `package.json`. Absent from a manifest
|
|
22
|
+
* built before 0.137.
|
|
23
|
+
*/
|
|
24
|
+
helix?: {
|
|
25
|
+
package?: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
};
|
|
19
28
|
modules?: Array<{
|
|
29
|
+
/** Where the module is, which names the family: `src/button/root/button-root.ts`. */
|
|
30
|
+
path?: string;
|
|
20
31
|
declarations?: Array<{
|
|
21
32
|
cssParts?: Array<{
|
|
22
33
|
name: string;
|
|
@@ -41,6 +52,8 @@ export interface Tier {
|
|
|
41
52
|
* scope, and `analyze.ts` needs both in the same file.
|
|
42
53
|
*/
|
|
43
54
|
export interface Published {
|
|
55
|
+
/** The family, from the module's path: `button` for `src/button/root/button-root.ts`. */
|
|
56
|
+
family?: string | undefined;
|
|
44
57
|
hooks: Map<string, Set<string>>;
|
|
45
58
|
parts: Map<string, Set<string>>;
|
|
46
59
|
}
|
|
@@ -52,6 +65,8 @@ export interface Published {
|
|
|
52
65
|
* styled one did not forward it", and that sentence needs the names.
|
|
53
66
|
*/
|
|
54
67
|
export declare const index: (tiers: Tier[]) => Map<string, Published>;
|
|
68
|
+
/** The version each manifest describes, by package, where it says. */
|
|
69
|
+
export declare const versionsOf: (tiers: Tier[]) => Record<string, string>;
|
|
55
70
|
/**
|
|
56
71
|
* The published name closest to a wrong one, or nothing when none is close.
|
|
57
72
|
*
|
package/dist/manifest.js
CHANGED
|
@@ -2,6 +2,7 @@ export const index = (tiers) => {
|
|
|
2
2
|
const tags = new Map();
|
|
3
3
|
for (const tier of tiers) {
|
|
4
4
|
for (const module of tier.manifest.modules ?? []) {
|
|
5
|
+
const family = /^src\/([^/]+)\//.exec(module.path ?? '')?.[1];
|
|
5
6
|
for (const declaration of module.declarations ?? []) {
|
|
6
7
|
const tag = declaration.tagName;
|
|
7
8
|
if (!tag) {
|
|
@@ -9,7 +10,7 @@ export const index = (tiers) => {
|
|
|
9
10
|
}
|
|
10
11
|
let element = tags.get(tag);
|
|
11
12
|
if (!element) {
|
|
12
|
-
element = { hooks: new Map(), parts: new Map() };
|
|
13
|
+
element = { family, hooks: new Map(), parts: new Map() };
|
|
13
14
|
tags.set(tag, element);
|
|
14
15
|
}
|
|
15
16
|
for (const [field, into] of [
|
|
@@ -27,6 +28,16 @@ export const index = (tiers) => {
|
|
|
27
28
|
}
|
|
28
29
|
return tags;
|
|
29
30
|
};
|
|
31
|
+
export const versionsOf = (tiers) => {
|
|
32
|
+
const versions = {};
|
|
33
|
+
for (const tier of tiers) {
|
|
34
|
+
const version = tier.manifest.helix?.version;
|
|
35
|
+
if (version) {
|
|
36
|
+
versions[tier.manifest.helix?.package ?? tier.name] = version;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return versions;
|
|
40
|
+
};
|
|
30
41
|
export const nearest = (wrong, candidates) => {
|
|
31
42
|
let best;
|
|
32
43
|
let distance = Math.floor(wrong.length / 3);
|
package/dist/report.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface Origin {
|
|
|
38
38
|
* rather than a finding.
|
|
39
39
|
*/
|
|
40
40
|
export interface Finding {
|
|
41
|
+
/**
|
|
42
|
+
* What the fault is about: `styling` for parts and hooks, `markup` for
|
|
43
|
+
* attributes and slots, `integrity` for the page and the library
|
|
44
|
+
* disagreeing, `guideline` for advice. A dashboard groups by it.
|
|
45
|
+
*/
|
|
46
|
+
category: FindingCategory;
|
|
41
47
|
/**
|
|
42
48
|
* What the collector believes the author meant, when it can tell.
|
|
43
49
|
*
|
|
@@ -48,15 +54,63 @@ export interface Finding {
|
|
|
48
54
|
* catch.
|
|
49
55
|
*/
|
|
50
56
|
expected?: string | undefined;
|
|
57
|
+
/** The family the tag belongs to, from the manifest's module path. */
|
|
58
|
+
family?: string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* The finding's identity across runs: the same fault in the next run has
|
|
61
|
+
* the same fingerprint, which is what makes fixed and regressed computable.
|
|
62
|
+
* See `fingerprintOf`.
|
|
63
|
+
*/
|
|
64
|
+
fingerprint: string;
|
|
51
65
|
/** Machine-readable fault class, stable across versions. */
|
|
52
|
-
kind:
|
|
66
|
+
kind: Kind;
|
|
53
67
|
/** One sentence, addressed to whoever wrote the CSS. */
|
|
54
68
|
message: string;
|
|
55
69
|
/** Where it was read. Absent only for faults that are not in a stylesheet. */
|
|
56
70
|
origin?: Origin;
|
|
71
|
+
/**
|
|
72
|
+
* Who fixes it: `consumer`, the page's authors, or `helix`, the library,
|
|
73
|
+
* as for a part the styled tier never forwarded.
|
|
74
|
+
*/
|
|
75
|
+
owner: Owner;
|
|
76
|
+
/** How serious it is, after the shipped rules and any overrides. */
|
|
77
|
+
severity: Severity;
|
|
78
|
+
/**
|
|
79
|
+
* Which layer set `severity`: the rules this package ships, the project's
|
|
80
|
+
* file, or the argument to `diagnose`. With two layers able to override,
|
|
81
|
+
* a severity alone cannot say where it came from.
|
|
82
|
+
*/
|
|
83
|
+
decidedBy: Layer;
|
|
57
84
|
/** The selector the fault sits in, or the tag for a runtime fault. */
|
|
58
85
|
subject: string;
|
|
86
|
+
/** The Helix tag the finding is about, which a scoped override matches on. */
|
|
87
|
+
tag: string;
|
|
59
88
|
}
|
|
89
|
+
/** Every fault class, stable across versions. */
|
|
90
|
+
export declare const KINDS: readonly ['dead-hook', 'dead-part', 'duplicate-registration', 'manifest-skew', 'unforwarded-part', 'unknown-tag'];
|
|
91
|
+
/** A fault class. */
|
|
92
|
+
export type Kind = (typeof KINDS)[number];
|
|
93
|
+
/** How serious a finding is. */
|
|
94
|
+
export type Severity = 'error' | 'info' | 'warn';
|
|
95
|
+
/** Where a finding's severity was decided. */
|
|
96
|
+
export type Layer = 'argument' | 'file' | 'shipped';
|
|
97
|
+
/** What a fault is about. */
|
|
98
|
+
export type FindingCategory = 'guideline' | 'integrity' | 'markup' | 'styling';
|
|
99
|
+
/** Who fixes a fault. */
|
|
100
|
+
export type Owner = 'consumer' | 'helix';
|
|
101
|
+
/** A finding as a check first writes it. */
|
|
102
|
+
export type Raw = Omit<Finding, 'category' | 'decidedBy' | 'family' | 'fingerprint' | 'owner' | 'severity'>;
|
|
103
|
+
/** A finding with its identity, before the rules have given it a severity. */
|
|
104
|
+
export type Identified = Omit<Finding, 'decidedBy' | 'severity'>;
|
|
105
|
+
/**
|
|
106
|
+
* The kinds of telemetry a run can hold, each its own shape. A project turns
|
|
107
|
+
* each on or off for sending; see `Config.send`.
|
|
108
|
+
*/
|
|
109
|
+
export declare const CATEGORIES: readonly ['assist', 'configuration', 'coverage', 'findings', 'performance', 'runtime', 'usage'];
|
|
110
|
+
/** A kind of telemetry. */
|
|
111
|
+
export type Category = (typeof CATEGORIES)[number];
|
|
112
|
+
/** Where a run was declared to happen. Undeclared is never sent. */
|
|
113
|
+
export type Environment = 'ci' | 'development' | 'production';
|
|
60
114
|
/** A sheet the collector could not read, and the reason. */
|
|
61
115
|
export interface Unreadable {
|
|
62
116
|
reason: 'cross-origin' | 'detached' | 'threw';
|
|
@@ -81,8 +135,146 @@ export interface Coverage {
|
|
|
81
135
|
/** Every sheet that could not be read, with its reason. */
|
|
82
136
|
unreadable: Unreadable[];
|
|
83
137
|
}
|
|
84
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* What a page uses of Helix, counted by tag.
|
|
140
|
+
*
|
|
141
|
+
* Plain objects of counts, so a sink can write the report as JSON and read it
|
|
142
|
+
* back unchanged.
|
|
143
|
+
*/
|
|
144
|
+
export interface Usage {
|
|
145
|
+
/**
|
|
146
|
+
* Each axis set, by tag, axis, and value: `emphasis`, `material`,
|
|
147
|
+
* `separation`, and `tone`. An axis left unset is not counted.
|
|
148
|
+
*/
|
|
149
|
+
axes: Record<string, Record<string, Record<string, number>>>;
|
|
150
|
+
/**
|
|
151
|
+
* The Helix elements other Helix elements render inside their own shadow
|
|
152
|
+
* roots, such as the select a pagination size draws. Kept apart from
|
|
153
|
+
* `tags`, which counts what the page put there.
|
|
154
|
+
*/
|
|
155
|
+
composed: Record<string, number>;
|
|
156
|
+
/**
|
|
157
|
+
* How the elements were counted. `watched`: every element that rendered
|
|
158
|
+
* since `watch()` began, including the ones that have gone, such as a
|
|
159
|
+
* closed dialog. `walked`: the elements in the page at the moment of the
|
|
160
|
+
* run, which misses those.
|
|
161
|
+
*/
|
|
162
|
+
counted: 'walked' | 'watched';
|
|
163
|
+
/** Each hook the page's stylesheets set, by tag, counted per declaration. */
|
|
164
|
+
hooks: Record<string, Record<string, number>>;
|
|
165
|
+
/**
|
|
166
|
+
* The values each hook is set to, by tag, hook, and value, where the
|
|
167
|
+
* project allows it (`detail.hookValues`). A hook set to one value across
|
|
168
|
+
* projects is a token Helix should have shipped. Values over 200
|
|
169
|
+
* characters are cut there.
|
|
170
|
+
*/
|
|
171
|
+
hookValues: Record<string, Record<string, Record<string, number>>>;
|
|
172
|
+
/** Each part the page's stylesheets style, by tag, counted per rule. */
|
|
173
|
+
parts: Record<string, Record<string, number>>;
|
|
174
|
+
/** Each named slot filled, by tag. */
|
|
175
|
+
slots: Record<string, Record<string, number>>;
|
|
176
|
+
/** Each Helix element the page put there, by tag. */
|
|
177
|
+
tags: Record<string, number>;
|
|
178
|
+
/**
|
|
179
|
+
* Elements named `helix-*` that no manifest passed in lists, by tag: a
|
|
180
|
+
* page's own element under the prefix, or a family whose manifest was not
|
|
181
|
+
* passed. Kept out of the other counts, which are Helix's.
|
|
182
|
+
*/
|
|
183
|
+
unlisted: Record<string, number>;
|
|
184
|
+
}
|
|
185
|
+
/** The settings a run was made under, and what they turned off. */
|
|
186
|
+
export interface Configuration {
|
|
187
|
+
/** The lowest level each category prints at in the console, or `off`. */
|
|
188
|
+
console: {
|
|
189
|
+
coverage: 'info' | 'off';
|
|
190
|
+
findings: Severity | 'off';
|
|
191
|
+
usage: 'debug' | 'off';
|
|
192
|
+
};
|
|
193
|
+
/** How much detail goes with what is sent. */
|
|
194
|
+
detail: {
|
|
195
|
+
evidence: 'full' | 'none' | 'selector';
|
|
196
|
+
hookValues: boolean;
|
|
197
|
+
pages: 'full' | 'none' | 'patterns';
|
|
198
|
+
};
|
|
199
|
+
/** Where sending happens, and production's sample rate. */
|
|
200
|
+
environments: {
|
|
201
|
+
ci: boolean;
|
|
202
|
+
development: boolean;
|
|
203
|
+
production: false | {
|
|
204
|
+
sample: number;
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
/** Each override in effect, from the file or the call. */
|
|
208
|
+
overrides: Array<{
|
|
209
|
+
kind: Kind;
|
|
210
|
+
layer: Exclude<Layer, 'shipped'>;
|
|
211
|
+
severity: Severity | 'off';
|
|
212
|
+
tags?: string[];
|
|
213
|
+
}>;
|
|
214
|
+
/**
|
|
215
|
+
* How many findings each override turned off, by kind and layer. A finding
|
|
216
|
+
* turned off is still a fault, and a report that hid the count would read
|
|
217
|
+
* as cleaner than the page is.
|
|
218
|
+
*/
|
|
219
|
+
silenced: Array<{
|
|
220
|
+
count: number;
|
|
221
|
+
kind: Kind;
|
|
222
|
+
layer: Exclude<Layer, 'shipped'>;
|
|
223
|
+
}>;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* One run: what was found, what is used, how much could be seen, and the
|
|
227
|
+
* settings it was made under, with everything a dashboard needs to place it.
|
|
228
|
+
*
|
|
229
|
+
* **The same shape from every source and to every sink**, versioned by
|
|
230
|
+
* `schema`, so a run written by CI today is read by the dashboard next year.
|
|
231
|
+
*/
|
|
85
232
|
export interface Report {
|
|
233
|
+
/** The shape's version. Raised when a field changes meaning or goes. */
|
|
234
|
+
schema: 1;
|
|
235
|
+
configuration: Configuration;
|
|
86
236
|
coverage: Coverage;
|
|
87
237
|
findings: Finding[];
|
|
238
|
+
/**
|
|
239
|
+
* The project's own name and labels, as its configuration gives them. The
|
|
240
|
+
* key is never written into a report.
|
|
241
|
+
*/
|
|
242
|
+
project: {
|
|
243
|
+
name?: string;
|
|
244
|
+
tags: string[];
|
|
245
|
+
};
|
|
246
|
+
run: {
|
|
247
|
+
/** When the run began, as an ISO 8601 time. */
|
|
248
|
+
at: string;
|
|
249
|
+
/** How long the run took, in milliseconds. */
|
|
250
|
+
duration: number;
|
|
251
|
+
/** Where it was declared to run. Absent where nobody declared it. */
|
|
252
|
+
environment?: Environment;
|
|
253
|
+
/** A new identity per run, so a run sent twice is stored once. */
|
|
254
|
+
id: string;
|
|
255
|
+
/** The page, as `detail.pages` allows: a pattern, a path, or absent. */
|
|
256
|
+
page?: string;
|
|
257
|
+
/** Where the run was made: in the page, or from the code. */
|
|
258
|
+
source: 'code' | 'page';
|
|
259
|
+
};
|
|
260
|
+
usage: Usage;
|
|
261
|
+
versions: {
|
|
262
|
+
/** This package's version, which collected the run. */
|
|
263
|
+
collector: string;
|
|
264
|
+
/** The Helix versions loaded on the page, one per copy of core: two means two copies. */
|
|
265
|
+
helix: string[];
|
|
266
|
+
/** The version each manifest passed in describes, by package. */
|
|
267
|
+
manifests: Record<string, string>;
|
|
268
|
+
/** The shipped rules' version, which is the collector's. */
|
|
269
|
+
rules: string;
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* The categories the project turned off for sending, and which place
|
|
273
|
+
* turned each off, so an empty category reads as withheld and never as
|
|
274
|
+
* nothing there.
|
|
275
|
+
*/
|
|
276
|
+
withheld: Array<{
|
|
277
|
+
category: Category;
|
|
278
|
+
decidedBy: Layer;
|
|
279
|
+
}>;
|
|
88
280
|
}
|
package/dist/report.js
CHANGED
|
@@ -1 +1,17 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const KINDS = [
|
|
2
|
+
'dead-hook',
|
|
3
|
+
'dead-part',
|
|
4
|
+
'duplicate-registration',
|
|
5
|
+
'manifest-skew',
|
|
6
|
+
'unforwarded-part',
|
|
7
|
+
'unknown-tag',
|
|
8
|
+
];
|
|
9
|
+
export const CATEGORIES = [
|
|
10
|
+
'assist',
|
|
11
|
+
'configuration',
|
|
12
|
+
'coverage',
|
|
13
|
+
'findings',
|
|
14
|
+
'performance',
|
|
15
|
+
'runtime',
|
|
16
|
+
'usage',
|
|
17
|
+
];
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Configuration, type Finding, type Identified, type Kind, type Layer, type Severity } from './report.js';
|
|
2
|
+
/** One override: a severity, `off`, or either for some tags only. */
|
|
3
|
+
export type Override = Severity | 'off' | {
|
|
4
|
+
/** The severity, or `off`. */
|
|
5
|
+
severity: Severity | 'off';
|
|
6
|
+
/** The tags it applies to. Every other tag keeps the layer below. */
|
|
7
|
+
tags: string[];
|
|
8
|
+
};
|
|
9
|
+
/** Overrides by kind. A list holds several, each for different tags. */
|
|
10
|
+
export type Rules = Partial<Record<Kind, Override | Override[]>>;
|
|
11
|
+
/**
|
|
12
|
+
* The shipped severities.
|
|
13
|
+
*
|
|
14
|
+
* `unforwarded-part` is a warning because the fault is in Helix: the part
|
|
15
|
+
* exists on the other tier, and the styled tier never forwarded it.
|
|
16
|
+
* Everything else means the page's CSS does nothing where it was written, or
|
|
17
|
+
* that the manifest cannot be trusted, and is an error.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SHIPPED_RULES: Readonly<Record<Kind, Severity>>;
|
|
20
|
+
/** Throws on an override that would silently do nothing. */
|
|
21
|
+
export declare const checkRules: (rules: Rules | undefined, layer: Exclude<Layer, 'shipped'>) => void;
|
|
22
|
+
/**
|
|
23
|
+
* The findings with their severities, and how many each override turned off,
|
|
24
|
+
* by kind and layer.
|
|
25
|
+
*
|
|
26
|
+
* The shipped rule first, then the file, then the argument, each later layer
|
|
27
|
+
* replacing what it speaks to.
|
|
28
|
+
*/
|
|
29
|
+
export declare const applyRules: (findings: Identified[], file: Rules | undefined, argument: Rules | undefined) => {
|
|
30
|
+
findings: Finding[];
|
|
31
|
+
silenced: Configuration['silenced'];
|
|
32
|
+
};
|
|
33
|
+
/** Every override in effect, flattened, for the report's configuration. */
|
|
34
|
+
export declare const overridesOf: (file: Rules | undefined, argument: Rules | undefined) => Configuration['overrides'];
|