@fusion.dev/helix-diagnostics 0.136.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 +67 -20
- package/dist/analyze.d.ts +7 -0
- package/dist/analyze.js +10 -0
- package/dist/config.d.ts +71 -0
- package/dist/config.js +126 -0
- package/dist/console.d.ts +8 -1
- package/dist/console.js +37 -15
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +42 -0
- package/dist/index.d.ts +29 -19
- package/dist/index.js +59 -16
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.js +12 -1
- package/dist/report.d.ts +131 -9
- package/dist/report.js +9 -0
- package/dist/rules.d.ts +7 -7
- package/dist/rules.js +14 -4
- package/dist/usage.d.ts +8 -2
- package/dist/usage.js +18 -2
- package/dist/version.d.ts +9 -0
- package/dist/version.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,21 +43,36 @@ diagnostic cannot drift from what is true.
|
|
|
43
43
|
|
|
44
44
|
## What a report holds
|
|
45
45
|
|
|
46
|
+
A report is one run, in one shape for every sink, versioned by `schema`:
|
|
47
|
+
|
|
48
|
+
- **`run`**: a new `id`, when it began (`at`), how long it took, where it ran
|
|
49
|
+
(`environment`, as you declare it), and the `page`, as a pattern by default:
|
|
50
|
+
`/orders/1042` reads `/orders/:id`.
|
|
51
|
+
- **`project`**: the name and tags your configuration gives. The key is never
|
|
52
|
+
written into a report.
|
|
53
|
+
- **`versions`**: the collector's and the rules', the Helix versions loaded on
|
|
54
|
+
the page (two means two copies), and the version each manifest describes.
|
|
46
55
|
- **`coverage`**: how many stylesheets and shadow roots were read, and which
|
|
47
56
|
sheets could not be. A cross-origin stylesheet throws on `.cssRules`, so a run
|
|
48
57
|
that read three sheets of nineteen returns the same empty findings as one that
|
|
49
58
|
read all nineteen. Read `coverage` before you believe `findings`.
|
|
50
|
-
- **`findings`**: each with its kind, a severity, the
|
|
51
|
-
severity, the tag
|
|
52
|
-
|
|
59
|
+
- **`findings`**: each with its kind, a severity, the place that set the
|
|
60
|
+
severity, the tag and its family, a `category` (`styling`, `markup`,
|
|
61
|
+
`integrity`, or `guideline`), an `owner` (`consumer`, or `helix` for a fault
|
|
62
|
+
in the library), a `fingerprint` that stays the same from one run to the next
|
|
63
|
+
for the same fault, and the rule's own text as evidence.
|
|
53
64
|
- **`usage`**: the Helix elements on the page by tag, the axes set on them
|
|
54
65
|
(`emphasis`, `material`, `separation`, and `tone`), the named slots filled,
|
|
55
66
|
the elements Helix renders inside other Helix elements, and the parts and
|
|
56
|
-
hooks your stylesheets use
|
|
57
|
-
rendered since `watch()` began,
|
|
58
|
-
closed dialog. Without one it counts
|
|
59
|
-
of the run. Elements named `helix-*`
|
|
60
|
-
own, are counted apart as `unlisted`.
|
|
67
|
+
hooks your stylesheets use, with the values the hooks are set to. With a
|
|
68
|
+
watcher it counts every element that rendered since `watch()` began,
|
|
69
|
+
including ones since removed, such as a closed dialog. Without one it counts
|
|
70
|
+
the elements on the page at the moment of the run. Elements named `helix-*`
|
|
71
|
+
that no manifest lists, such as your own, are counted apart as `unlisted`.
|
|
72
|
+
- **`configuration`**: the settings the run was made under, each override in
|
|
73
|
+
effect, and how many findings each override turned off, by kind.
|
|
74
|
+
- **`withheld`**: the categories your configuration turned off for sending,
|
|
75
|
+
and which place turned each off.
|
|
61
76
|
|
|
62
77
|
## What the console shows
|
|
63
78
|
|
|
@@ -86,31 +101,63 @@ works on it:
|
|
|
86
101
|
| `duplicate-registration` | error | Two copies of Helix are on the page |
|
|
87
102
|
| `manifest-skew` | error | An element renders a part its manifest does not list, so the manifest is from another version. It replaces every other finding, since each would be judged against the wrong version. Turned off, the page is judged as usual |
|
|
88
103
|
|
|
89
|
-
##
|
|
104
|
+
## Configure it
|
|
90
105
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
`off`, or either for some tags only:
|
|
106
|
+
Every setting can be given in three places, and a later place wins, field by
|
|
107
|
+
field: the defaults this package ships, your project's file, and the call.
|
|
108
|
+
The page cannot read files, so import your file and pass it as `file`:
|
|
95
109
|
|
|
96
110
|
```js
|
|
97
|
-
import
|
|
111
|
+
import project from './helix-diagnostics.json' with { type: 'json' }
|
|
98
112
|
|
|
99
113
|
diagnose({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
'unforwarded-part': 'off',
|
|
104
|
-
},
|
|
114
|
+
environment: 'development',
|
|
115
|
+
file: project,
|
|
116
|
+
rules: { 'unforwarded-part': 'off' },
|
|
105
117
|
tiers,
|
|
106
118
|
})
|
|
107
119
|
```
|
|
108
120
|
|
|
109
|
-
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"project": { "name": "storefront", "tags": ["web"] },
|
|
124
|
+
"rules": { "dead-hook": { "severity": "warn", "tags": ["helix-card"] } },
|
|
125
|
+
"console": { "findings": "warn", "usage": "off" },
|
|
126
|
+
"send": { "performance": false },
|
|
127
|
+
"detail": { "evidence": "full", "hookValues": true, "pages": "patterns" },
|
|
128
|
+
"environments": { "development": true, "ci": true, "production": false }
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Setting | Takes | Default |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `rules.<kind>` | `error`, `warn`, `info`, `off`, or `{ severity, tags }` | see Finding kinds |
|
|
135
|
+
| `console.findings` | `error`, `warn`, `info`, or `off`: the lowest severity printed | `info` |
|
|
136
|
+
| `console.coverage` | `info` or `off` | `info` |
|
|
137
|
+
| `console.usage` | `debug` or `off` | `debug` |
|
|
138
|
+
| `send.<category>` | `true` or `false`, for `findings`, `usage`, `coverage`, `configuration`, `runtime`, `performance`, and `assist` | all on but `performance` |
|
|
139
|
+
| `detail.evidence` | `full`, `selector`, or `none` | `full` |
|
|
140
|
+
| `detail.hookValues` | `true` or `false` | `true` |
|
|
141
|
+
| `detail.pages` | `patterns`, `full` (the path, never the query), or `none` | `patterns` |
|
|
142
|
+
| `environments.<name>` | `true` or `false`; `production` takes `{ sample }` from 0 to 1 | development and CI on |
|
|
143
|
+
| `project` | `key`, `name`, and `tags` | none |
|
|
144
|
+
|
|
145
|
+
`send`, `detail`, and `environments` decide what a sending sink sends, and
|
|
146
|
+
nothing here sends yet: they are recorded in every report, so a dashboard
|
|
147
|
+
knows what a project withheld. A setting or a value that does not exist
|
|
148
|
+
throws, and names the closest that does.
|
|
110
149
|
|
|
111
150
|
Development only. It walks every stylesheet in the page, and it sends nothing
|
|
112
151
|
anywhere.
|
|
113
152
|
|
|
153
|
+
## Versions
|
|
154
|
+
|
|
155
|
+
`@fusion.dev/helix-core` lists its version on the page as it loads, and every
|
|
156
|
+
manifest built from 0.137 names the package and version it describes. Where
|
|
157
|
+
both sides say, a manifest from another version is one `manifest-skew`
|
|
158
|
+
finding in place of all others. Where either is silent, the check falls back
|
|
159
|
+
to a part rendering that the manifest does not list.
|
|
160
|
+
|
|
114
161
|
[Documentation](https://fusion.dev/helix)
|
|
115
162
|
|
|
116
163
|
MIT
|
package/dist/analyze.d.ts
CHANGED
|
@@ -68,3 +68,10 @@ export declare const registrations: (from?: Document, known?: () => string[]) =>
|
|
|
68
68
|
* findings that are each the same mistake are worse than one that names it.
|
|
69
69
|
*/
|
|
70
70
|
export declare const skew: (tags: Map<string, Published>, from: Document) => Raw[];
|
|
71
|
+
/**
|
|
72
|
+
* A manifest from another version, read from the numbers where both sides
|
|
73
|
+
* have one: the version a manifest describes, against the versions of core
|
|
74
|
+
* loaded on the page. Definite where `skew` above infers, and the check
|
|
75
|
+
* `diagnose` makes first.
|
|
76
|
+
*/
|
|
77
|
+
export declare const versionSkew: (manifests: Record<string, string>, seen: string[]) => Raw[];
|
package/dist/analyze.js
CHANGED
|
@@ -207,3 +207,13 @@ export const skew = (tags, from) => {
|
|
|
207
207
|
visit(from, 0);
|
|
208
208
|
return findings;
|
|
209
209
|
};
|
|
210
|
+
export const versionSkew = (manifests, seen) => seen.length === 0
|
|
211
|
+
? []
|
|
212
|
+
: Object.entries(manifests)
|
|
213
|
+
.filter(([, version]) => !seen.includes(version))
|
|
214
|
+
.map(([name, version]) => ({
|
|
215
|
+
kind: 'manifest-skew',
|
|
216
|
+
message: `The manifest passed in for \`${name}\` describes ${version}, and the page runs Helix ${seen.join(' and ')}. Every other finding would be judged against the wrong version, so none are reported. Pass the \`custom-elements.json\` from the installed packages.`,
|
|
217
|
+
subject: `${name}@${version}`,
|
|
218
|
+
tag: '',
|
|
219
|
+
}));
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type Category, type Configuration, type Layer, type Report } from './report.js';
|
|
2
|
+
import { type Rules } from './rules.js';
|
|
3
|
+
/** Everything a project can set. Every field is optional; see `DEFAULTS`. */
|
|
4
|
+
export interface Config {
|
|
5
|
+
/** The lowest level each category prints at in the console, or `off`. */
|
|
6
|
+
console?: Partial<Configuration['console']>;
|
|
7
|
+
/** How much detail goes with what is sent. */
|
|
8
|
+
detail?: Partial<Configuration['detail']>;
|
|
9
|
+
/** Where sending happens. Production takes `{ sample }`, from 0 to 1. */
|
|
10
|
+
environments?: Partial<Configuration['environments']>;
|
|
11
|
+
/**
|
|
12
|
+
* The project: its key, which opts it in to sending, and its own name and
|
|
13
|
+
* labels. `tags` are free, and `fusion` marks Fusion's own projects.
|
|
14
|
+
*/
|
|
15
|
+
project?: {
|
|
16
|
+
key?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
tags?: string[];
|
|
19
|
+
};
|
|
20
|
+
/** Severities by kind; see `Rules`. */
|
|
21
|
+
rules?: Rules;
|
|
22
|
+
/** Each category, sent or withheld. Nothing is sent without a key. */
|
|
23
|
+
send?: Partial<Record<Category, boolean>>;
|
|
24
|
+
}
|
|
25
|
+
/** The shipped defaults. */
|
|
26
|
+
export declare const DEFAULTS: {
|
|
27
|
+
readonly console: {
|
|
28
|
+
readonly coverage: 'info';
|
|
29
|
+
readonly findings: 'info';
|
|
30
|
+
readonly usage: 'debug';
|
|
31
|
+
};
|
|
32
|
+
readonly detail: {
|
|
33
|
+
readonly evidence: 'full';
|
|
34
|
+
readonly hookValues: true;
|
|
35
|
+
readonly pages: 'patterns';
|
|
36
|
+
};
|
|
37
|
+
readonly environments: {
|
|
38
|
+
readonly ci: true;
|
|
39
|
+
readonly development: true;
|
|
40
|
+
readonly production: false;
|
|
41
|
+
};
|
|
42
|
+
readonly send: {
|
|
43
|
+
readonly assist: true;
|
|
44
|
+
readonly configuration: true;
|
|
45
|
+
readonly coverage: true;
|
|
46
|
+
readonly findings: true;
|
|
47
|
+
readonly performance: false;
|
|
48
|
+
readonly runtime: true;
|
|
49
|
+
readonly usage: true;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/** Throws on a setting that names nothing, or a value it cannot take. */
|
|
53
|
+
export declare const checkConfig: (config: Config | undefined, layer: Exclude<Layer, 'shipped'>) => void;
|
|
54
|
+
/** The settings in effect, from the defaults, the file, and the call. */
|
|
55
|
+
export interface Resolved {
|
|
56
|
+
configuration: Omit<Configuration, 'overrides' | 'silenced'>;
|
|
57
|
+
project: Report['project'];
|
|
58
|
+
/** Each category's sending, and which place decided it. */
|
|
59
|
+
send: Record<Category, {
|
|
60
|
+
decidedBy: Layer;
|
|
61
|
+
on: boolean;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
/** The three places, merged, with a later one winning field by field. */
|
|
65
|
+
export declare const resolveConfig: (file: Config | undefined, call: Config | undefined) => Resolved;
|
|
66
|
+
/**
|
|
67
|
+
* A page's path as a pattern: each segment that names one record, a number,
|
|
68
|
+
* a UUID, or a long run of hex or mixed letters and digits, becomes `:id`.
|
|
69
|
+
* `/orders/1042/items/9f8e7d6c5b4a` reads `/orders/:id/items/:id`.
|
|
70
|
+
*/
|
|
71
|
+
export declare const pagePattern: (path: string) => string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { nearest } from './manifest.js';
|
|
2
|
+
import { CATEGORIES } from './report.js';
|
|
3
|
+
import { checkRules } from './rules.js';
|
|
4
|
+
export const DEFAULTS = {
|
|
5
|
+
console: { coverage: 'info', findings: 'info', usage: 'debug' },
|
|
6
|
+
detail: { evidence: 'full', hookValues: true, pages: 'patterns' },
|
|
7
|
+
environments: { ci: true, development: true, production: false },
|
|
8
|
+
send: {
|
|
9
|
+
assist: true,
|
|
10
|
+
configuration: true,
|
|
11
|
+
coverage: true,
|
|
12
|
+
findings: true,
|
|
13
|
+
performance: false,
|
|
14
|
+
runtime: true,
|
|
15
|
+
usage: true,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
const ALLOWED = {
|
|
19
|
+
console: {
|
|
20
|
+
coverage: ['info', 'off'],
|
|
21
|
+
findings: ['error', 'warn', 'info', 'off'],
|
|
22
|
+
usage: ['debug', 'off'],
|
|
23
|
+
},
|
|
24
|
+
detail: {
|
|
25
|
+
evidence: ['full', 'selector', 'none'],
|
|
26
|
+
hookValues: [true, false],
|
|
27
|
+
pages: ['patterns', 'full', 'none'],
|
|
28
|
+
},
|
|
29
|
+
environments: { ci: [true, false], development: [true, false], production: 'sample' },
|
|
30
|
+
project: { key: 'string', name: 'string', tags: 'tags' },
|
|
31
|
+
send: Object.fromEntries(CATEGORIES.map((category) => [category, [true, false]])),
|
|
32
|
+
};
|
|
33
|
+
const shown = (value) => typeof value === 'string' ? `\`${value}\`` : String(value);
|
|
34
|
+
export const checkConfig = (config, layer) => {
|
|
35
|
+
if (config === undefined) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (typeof config !== 'object' || config === null || Array.isArray(config)) {
|
|
39
|
+
throw new Error(`The ${layer} is not a configuration object.`);
|
|
40
|
+
}
|
|
41
|
+
for (const [section, value] of Object.entries(config)) {
|
|
42
|
+
if (section === 'rules') {
|
|
43
|
+
checkRules(value, layer);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const allowed = ALLOWED[section];
|
|
47
|
+
if (!allowed) {
|
|
48
|
+
const close = nearest(section, [...Object.keys(ALLOWED), 'rules']);
|
|
49
|
+
throw new Error(`The ${layer} sets \`${section}\`, which is no setting.` +
|
|
50
|
+
(close ? ` The closest is \`${close}\`.` : ''));
|
|
51
|
+
}
|
|
52
|
+
for (const [name, one] of Object.entries(value)) {
|
|
53
|
+
const takes = allowed[name];
|
|
54
|
+
if (takes === undefined) {
|
|
55
|
+
const close = nearest(name, Object.keys(allowed));
|
|
56
|
+
throw new Error(`The ${layer} sets \`${section}.${name}\`, which is no setting.` +
|
|
57
|
+
(close ? ` The closest is \`${section}.${close}\`.` : ''));
|
|
58
|
+
}
|
|
59
|
+
const fits = takes === 'string'
|
|
60
|
+
? typeof one === 'string'
|
|
61
|
+
: takes === 'tags'
|
|
62
|
+
? Array.isArray(one) && one.every((tag) => typeof tag === 'string')
|
|
63
|
+
: takes === 'sample'
|
|
64
|
+
? one === false ||
|
|
65
|
+
(typeof one === 'object' &&
|
|
66
|
+
one !== null &&
|
|
67
|
+
typeof one.sample === 'number' &&
|
|
68
|
+
one.sample >= 0 &&
|
|
69
|
+
one.sample <= 1)
|
|
70
|
+
: takes.includes(one);
|
|
71
|
+
if (!fits) {
|
|
72
|
+
const expected = takes === 'string'
|
|
73
|
+
? 'a string'
|
|
74
|
+
: takes === 'tags'
|
|
75
|
+
? 'a list of strings'
|
|
76
|
+
: takes === 'sample'
|
|
77
|
+
? '`false`, or `{ sample }` from 0 to 1'
|
|
78
|
+
: takes.map(shown).join(', ');
|
|
79
|
+
throw new Error(`The ${layer} sets \`${section}.${name}\` to \`${JSON.stringify(one)}\`. It takes ${expected}.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
export const resolveConfig = (file, call) => {
|
|
85
|
+
checkConfig(file, 'file');
|
|
86
|
+
checkConfig(call, 'argument');
|
|
87
|
+
const merged = (section, base) => ({
|
|
88
|
+
...base,
|
|
89
|
+
...(file?.[section] ?? {}),
|
|
90
|
+
...(call?.[section] ?? {}),
|
|
91
|
+
});
|
|
92
|
+
const send = Object.fromEntries(CATEGORIES.map((category) => {
|
|
93
|
+
const fromCall = call?.send?.[category];
|
|
94
|
+
const fromFile = file?.send?.[category];
|
|
95
|
+
return [
|
|
96
|
+
category,
|
|
97
|
+
fromCall !== undefined
|
|
98
|
+
? { decidedBy: 'argument', on: fromCall }
|
|
99
|
+
: fromFile !== undefined
|
|
100
|
+
? { decidedBy: 'file', on: fromFile }
|
|
101
|
+
: { decidedBy: 'shipped', on: DEFAULTS.send[category] },
|
|
102
|
+
];
|
|
103
|
+
}));
|
|
104
|
+
const project = { ...(file?.project ?? {}), ...(call?.project ?? {}) };
|
|
105
|
+
return {
|
|
106
|
+
configuration: {
|
|
107
|
+
console: merged('console', { ...DEFAULTS.console }),
|
|
108
|
+
detail: merged('detail', { ...DEFAULTS.detail }),
|
|
109
|
+
environments: merged('environments', { ...DEFAULTS.environments }),
|
|
110
|
+
},
|
|
111
|
+
project: {
|
|
112
|
+
...(project.name === undefined ? {} : { name: project.name }),
|
|
113
|
+
tags: project.tags ?? [],
|
|
114
|
+
},
|
|
115
|
+
send,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
export const pagePattern = (path) => path
|
|
119
|
+
.split('/')
|
|
120
|
+
.map((segment) => /^\d+$/.test(segment) ||
|
|
121
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment) ||
|
|
122
|
+
/^[0-9a-f]{8,}$/i.test(segment) ||
|
|
123
|
+
(segment.length >= 12 && /\d/.test(segment) && /^[A-Za-z0-9_-]+$/.test(segment))
|
|
124
|
+
? ':id'
|
|
125
|
+
: segment)
|
|
126
|
+
.join('/');
|
package/dist/console.d.ts
CHANGED
|
@@ -27,5 +27,12 @@
|
|
|
27
27
|
import type { Report } from './report.js';
|
|
28
28
|
/** The console methods this sink calls. A test hands in its own. */
|
|
29
29
|
export type ConsoleSink = Pick<Console, 'debug' | 'error' | 'group' | 'groupCollapsed' | 'groupEnd' | 'info' | 'warn'>;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Writes a report and hands it back, so a caller can chain a second sink.
|
|
32
|
+
*
|
|
33
|
+
* **Each category prints as the project's `console` setting says**: findings
|
|
34
|
+
* at or above a severity, coverage and usage on or off. The heading counts
|
|
35
|
+
* every finding whatever is printed under it, and the ones the rules turned
|
|
36
|
+
* off, so a quiet console never reads as a clean page.
|
|
37
|
+
*/
|
|
31
38
|
export declare const report: (result: Report, to?: ConsoleSink) => Report;
|
package/dist/console.js
CHANGED
|
@@ -7,41 +7,63 @@ const entry = (finding) => {
|
|
|
7
7
|
return lines.join('\n');
|
|
8
8
|
};
|
|
9
9
|
const counted = (count, noun) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
10
|
+
const RANK = { error: 3, info: 1, warn: 2 };
|
|
10
11
|
export const report = (result, to = console) => {
|
|
11
|
-
const { coverage, findings } = result;
|
|
12
|
+
const { configuration, coverage, findings } = result;
|
|
13
|
+
const levels = configuration.console;
|
|
12
14
|
const bySeverity = (severity) => findings.filter((finding) => finding.severity === severity).length;
|
|
13
15
|
const errors = bySeverity('error');
|
|
14
16
|
const warnings = bySeverity('warn');
|
|
17
|
+
const off = configuration.silenced.reduce((total, one) => total + one.count, 0);
|
|
15
18
|
const missed = coverage.sheets - coverage.read;
|
|
16
|
-
const
|
|
19
|
+
const counts = [
|
|
20
|
+
counted(errors, 'error'),
|
|
21
|
+
counted(warnings, 'warning'),
|
|
22
|
+
`${bySeverity('info')} info`,
|
|
23
|
+
...(off > 0 ? [`${off} turned off`] : []),
|
|
24
|
+
];
|
|
25
|
+
const heading = findings.length === 0 && off === 0
|
|
17
26
|
? 'helix diagnostics: no findings'
|
|
18
|
-
: `helix diagnostics: ${
|
|
27
|
+
: `helix diagnostics: ${counts.join(', ')}`;
|
|
19
28
|
if (errors + warnings > 0 || missed > 0) {
|
|
20
29
|
to.group(heading);
|
|
21
30
|
}
|
|
22
31
|
else {
|
|
23
32
|
to.groupCollapsed(heading);
|
|
24
33
|
}
|
|
25
|
-
|
|
34
|
+
if (levels.coverage !== 'off') {
|
|
35
|
+
to.info(`read ${coverage.read} of ${counted(coverage.sheets, 'stylesheet')} across ${counted(coverage.roots, 'shadow root')}` +
|
|
36
|
+
(result.run.page ? ` on ${result.run.page}` : ''));
|
|
37
|
+
}
|
|
26
38
|
if (missed > 0) {
|
|
27
39
|
to.warn([
|
|
28
40
|
`${counted(missed, 'stylesheet')} could not be read, so ${missed === 1 ? 'it was' : 'they were'} not checked:`,
|
|
29
41
|
...coverage.unreadable.map((sheet) => `${sheet.sheet} (${sheet.reason})`),
|
|
30
42
|
].join('\n'));
|
|
31
43
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
if (off > 0 && levels.coverage !== 'off') {
|
|
45
|
+
to.info(configuration.silenced
|
|
46
|
+
.map((one) => `${counted(one.count, one.kind)} turned off by the ${one.layer}`)
|
|
47
|
+
.join('\n'));
|
|
48
|
+
}
|
|
49
|
+
if (levels.findings !== 'off') {
|
|
50
|
+
const floor = RANK[levels.findings];
|
|
51
|
+
for (const finding of findings) {
|
|
52
|
+
if (RANK[finding.severity] >= floor) {
|
|
53
|
+
to[finding.severity](entry(finding));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
35
56
|
}
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
if (levels.usage !== 'off') {
|
|
58
|
+
const { counted: how, hooks, parts, tags, unlisted } = result.usage;
|
|
59
|
+
const sum = (counts) => Object.values(counts).reduce((total, one) => total + Object.keys(one).length, 0);
|
|
60
|
+
const elements = Object.values(tags).reduce((total, one) => total + one, 0);
|
|
61
|
+
const others = Object.keys(unlisted).length;
|
|
62
|
+
const versions = result.versions.helix;
|
|
63
|
+
to.debug(`usage (${how}): ${counted(Object.keys(tags).length, 'tag')} in ${counted(elements, 'element')}, ${counted(sum(parts), 'part')} styled, ${counted(sum(hooks), 'hook')} set` +
|
|
64
|
+
(others > 0 ? `, and ${counted(others, 'helix- tag')} no manifest lists` : '') +
|
|
65
|
+
(versions.length > 0 ? `; Helix ${versions.join(' and ')}` : ''));
|
|
38
66
|
}
|
|
39
|
-
const { counted: how, hooks, parts, tags, unlisted } = result.usage;
|
|
40
|
-
const sum = (counts) => Object.values(counts).reduce((total, one) => total + Object.keys(one).length, 0);
|
|
41
|
-
const elements = Object.values(tags).reduce((total, one) => total + one, 0);
|
|
42
|
-
const others = Object.keys(unlisted).length;
|
|
43
|
-
to.debug(`usage (${how}): ${counted(Object.keys(tags).length, 'tag')} in ${counted(elements, 'element')}, ${counted(sum(parts), 'part')} styled, ${counted(sum(hooks), 'hook')} set` +
|
|
44
|
-
(others > 0 ? `, and ${counted(others, 'helix- tag')} no manifest lists` : ''));
|
|
45
67
|
to.groupEnd();
|
|
46
68
|
return result;
|
|
47
69
|
};
|
|
@@ -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
|
@@ -28,19 +28,25 @@
|
|
|
28
28
|
* stylesheet in the page and has no business in a production bundle.
|
|
29
29
|
*/
|
|
30
30
|
export { collect } from './collect.js';
|
|
31
|
+
export { type Config, DEFAULTS, pagePattern } from './config.js';
|
|
31
32
|
export { report } from './console.js';
|
|
33
|
+
export { fingerprintOf, normalize } from './identity.js';
|
|
32
34
|
export type { ManifestSource, Published, Tier } from './manifest.js';
|
|
33
35
|
export { index, nearest } from './manifest.js';
|
|
34
|
-
export type { Coverage, Finding, Kind, Layer, Origin, Report, Severity, Unreadable, Usage, } from './report.js';
|
|
35
|
-
export { KINDS } 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';
|
|
36
38
|
export { type Override, type Rules, SHIPPED_RULES } from './rules.js';
|
|
37
39
|
export { type Watcher, watch } from './usage.js';
|
|
40
|
+
export { VERSION } from './version.js';
|
|
41
|
+
import { type Config } from './config.js';
|
|
38
42
|
import type { Tier } from './manifest.js';
|
|
39
|
-
import type { Report } from './report.js';
|
|
40
|
-
import { type Rules } from './rules.js';
|
|
43
|
+
import type { Environment, Report } from './report.js';
|
|
41
44
|
import { type Watcher } from './usage.js';
|
|
42
|
-
/**
|
|
43
|
-
|
|
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 {
|
|
44
50
|
/**
|
|
45
51
|
* `duplicateRegistrations` from `@fusion.dev/helix-core`, when it is installed.
|
|
46
52
|
*
|
|
@@ -53,10 +59,16 @@ export interface Options {
|
|
|
53
59
|
*/
|
|
54
60
|
duplicates?: () => string[];
|
|
55
61
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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.
|
|
58
65
|
*/
|
|
59
|
-
|
|
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;
|
|
60
72
|
/**
|
|
61
73
|
* The document to scan. Defaults to the ambient one.
|
|
62
74
|
*
|
|
@@ -65,14 +77,11 @@ export interface Options {
|
|
|
65
77
|
*/
|
|
66
78
|
from?: Document;
|
|
67
79
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* ```ts
|
|
72
|
-
* diagnose({ tiers, rules: { 'unforwarded-part': 'off', 'dead-hook': { severity: 'warn', tags: ['helix-card'] } } })
|
|
73
|
-
* ```
|
|
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.
|
|
74
83
|
*/
|
|
75
|
-
|
|
84
|
+
page?: string | undefined;
|
|
76
85
|
/**
|
|
77
86
|
* The manifests to check against, one per installed tier.
|
|
78
87
|
*
|
|
@@ -88,7 +97,7 @@ export interface Options {
|
|
|
88
97
|
watcher?: Watcher | undefined;
|
|
89
98
|
}
|
|
90
99
|
/**
|
|
91
|
-
* One
|
|
100
|
+
* One run over the page.
|
|
92
101
|
*
|
|
93
102
|
* **It returns coverage and findings together and offers no way to have one
|
|
94
103
|
* without the other.** A collector that cannot read cross-origin sheets reports
|
|
@@ -100,6 +109,7 @@ export interface Options {
|
|
|
100
109
|
* loop: sheets that arrive later are found by calling it again.
|
|
101
110
|
*
|
|
102
111
|
* **A manifest from another version is refused**, with one finding that says
|
|
103
|
-
* so in place of every other:
|
|
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.
|
|
104
114
|
*/
|
|
105
|
-
export declare const diagnose: (
|
|
115
|
+
export declare const diagnose: (options: Options) => Report;
|
package/dist/index.js
CHANGED
|
@@ -1,38 +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
|
-
export { KINDS } from './report.js';
|
|
6
|
+
export { CATEGORIES, KINDS } from './report.js';
|
|
5
7
|
export { SHIPPED_RULES } from './rules.js';
|
|
6
8
|
export { watch } from './usage.js';
|
|
7
|
-
|
|
9
|
+
export { VERSION } from './version.js';
|
|
10
|
+
import { analyze, registrations, skew, versionSkew } from './analyze.js';
|
|
8
11
|
import { collect } from './collect.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
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;
|
|
13
24
|
if (tiers.length === 0) {
|
|
14
25
|
throw new Error('diagnose() needs at least one manifest. Without one every part name is unknown and every finding would be noise.');
|
|
15
26
|
}
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
const resolved = resolveConfig(file, call);
|
|
28
|
+
const { detail } = resolved.configuration;
|
|
18
29
|
const document_ = from ?? globalThis.document;
|
|
19
30
|
const { answered, coverage, rules: located } = collect(document_);
|
|
20
31
|
const tags = index(tiers);
|
|
21
|
-
const
|
|
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);
|
|
22
38
|
const judged = skewed.findings.length > 0
|
|
23
|
-
? { findings: [], silenced:
|
|
24
|
-
: applyRules([
|
|
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;
|
|
25
50
|
return {
|
|
51
|
+
configuration: {
|
|
52
|
+
...resolved.configuration,
|
|
53
|
+
overrides: overridesOf(file?.rules, call.rules),
|
|
54
|
+
silenced: [...skewed.silenced, ...judged.silenced],
|
|
55
|
+
},
|
|
26
56
|
coverage,
|
|
27
57
|
findings: [...skewed.findings, ...judged.findings],
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 }),
|
|
31
66
|
},
|
|
67
|
+
schema: 1,
|
|
32
68
|
usage: {
|
|
33
|
-
...listed(
|
|
34
|
-
...styled(located, tags, answered),
|
|
69
|
+
...listed(counted, tags),
|
|
70
|
+
...styled(located, tags, answered, detail.hookValues),
|
|
35
71
|
counted: watcher ? 'watched' : 'walked',
|
|
36
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
|
+
})),
|
|
37
80
|
};
|
|
38
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,12 +54,25 @@ 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
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;
|
|
57
76
|
/** How serious it is, after the shipped rules and any overrides. */
|
|
58
77
|
severity: Severity;
|
|
59
78
|
/**
|
|
@@ -75,8 +94,23 @@ export type Kind = (typeof KINDS)[number];
|
|
|
75
94
|
export type Severity = 'error' | 'info' | 'warn';
|
|
76
95
|
/** Where a finding's severity was decided. */
|
|
77
96
|
export type Layer = 'argument' | 'file' | 'shipped';
|
|
78
|
-
/**
|
|
79
|
-
export type
|
|
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';
|
|
80
114
|
/** A sheet the collector could not read, and the reason. */
|
|
81
115
|
export interface Unreadable {
|
|
82
116
|
reason: 'cross-origin' | 'detached' | 'threw';
|
|
@@ -128,6 +162,13 @@ export interface Usage {
|
|
|
128
162
|
counted: 'walked' | 'watched';
|
|
129
163
|
/** Each hook the page's stylesheets set, by tag, counted per declaration. */
|
|
130
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>>>;
|
|
131
172
|
/** Each part the page's stylesheets style, by tag, counted per rule. */
|
|
132
173
|
parts: Record<string, Record<string, number>>;
|
|
133
174
|
/** Each named slot filled, by tag. */
|
|
@@ -141,18 +182,99 @@ export interface Usage {
|
|
|
141
182
|
*/
|
|
142
183
|
unlisted: Record<string, number>;
|
|
143
184
|
}
|
|
144
|
-
/** The
|
|
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
|
+
*/
|
|
145
232
|
export interface Report {
|
|
233
|
+
/** The shape's version. Raised when a field changes meaning or goes. */
|
|
234
|
+
schema: 1;
|
|
235
|
+
configuration: Configuration;
|
|
146
236
|
coverage: Coverage;
|
|
147
237
|
findings: Finding[];
|
|
148
238
|
/**
|
|
149
|
-
*
|
|
150
|
-
* is
|
|
151
|
-
* than the page is.
|
|
239
|
+
* The project's own name and labels, as its configuration gives them. The
|
|
240
|
+
* key is never written into a report.
|
|
152
241
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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';
|
|
156
259
|
};
|
|
157
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
|
+
}>;
|
|
158
280
|
}
|
package/dist/report.js
CHANGED
package/dist/rules.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Finding, type
|
|
1
|
+
import { type Configuration, type Finding, type Identified, type Kind, type Layer, type Severity } from './report.js';
|
|
2
2
|
/** One override: a severity, `off`, or either for some tags only. */
|
|
3
3
|
export type Override = Severity | 'off' | {
|
|
4
4
|
/** The severity, or `off`. */
|
|
@@ -20,15 +20,15 @@ export declare const SHIPPED_RULES: Readonly<Record<Kind, Severity>>;
|
|
|
20
20
|
/** Throws on an override that would silently do nothing. */
|
|
21
21
|
export declare const checkRules: (rules: Rules | undefined, layer: Exclude<Layer, 'shipped'>) => void;
|
|
22
22
|
/**
|
|
23
|
-
* The findings with their severities, and how many each
|
|
23
|
+
* The findings with their severities, and how many each override turned off,
|
|
24
|
+
* by kind and layer.
|
|
24
25
|
*
|
|
25
26
|
* The shipped rule first, then the file, then the argument, each later layer
|
|
26
27
|
* replacing what it speaks to.
|
|
27
28
|
*/
|
|
28
|
-
export declare const applyRules: (findings:
|
|
29
|
+
export declare const applyRules: (findings: Identified[], file: Rules | undefined, argument: Rules | undefined) => {
|
|
29
30
|
findings: Finding[];
|
|
30
|
-
silenced:
|
|
31
|
-
argument: number;
|
|
32
|
-
file: number;
|
|
33
|
-
};
|
|
31
|
+
silenced: Configuration['silenced'];
|
|
34
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'];
|
package/dist/rules.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { nearest } from './manifest.js';
|
|
2
|
-
import { KINDS } from './report.js';
|
|
2
|
+
import { KINDS, } from './report.js';
|
|
3
3
|
export const SHIPPED_RULES = {
|
|
4
4
|
'dead-hook': 'error',
|
|
5
5
|
'dead-part': 'error',
|
|
@@ -41,7 +41,7 @@ const said = (rules, finding) => {
|
|
|
41
41
|
return typeof plain === 'string' ? plain : undefined;
|
|
42
42
|
};
|
|
43
43
|
export const applyRules = (findings, file, argument) => {
|
|
44
|
-
const
|
|
44
|
+
const off = new Map();
|
|
45
45
|
const kept = [];
|
|
46
46
|
for (const finding of findings) {
|
|
47
47
|
let severity = SHIPPED_RULES[finding.kind];
|
|
@@ -57,10 +57,20 @@ export const applyRules = (findings, file, argument) => {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
if (severity === 'off') {
|
|
60
|
-
|
|
60
|
+
const layer = decidedBy;
|
|
61
|
+
const key = `${finding.kind} ${layer}`;
|
|
62
|
+
const entry = off.get(key) ?? { count: 0, kind: finding.kind, layer };
|
|
63
|
+
entry.count += 1;
|
|
64
|
+
off.set(key, entry);
|
|
61
65
|
continue;
|
|
62
66
|
}
|
|
63
67
|
kept.push({ ...finding, decidedBy, severity });
|
|
64
68
|
}
|
|
65
|
-
return { findings: kept, silenced };
|
|
69
|
+
return { findings: kept, silenced: [...off.values()] };
|
|
66
70
|
};
|
|
71
|
+
export const overridesOf = (file, argument) => [
|
|
72
|
+
['file', file],
|
|
73
|
+
['argument', argument],
|
|
74
|
+
].flatMap(([layer, rules]) => Object.entries(rules ?? {}).flatMap(([kind, value]) => (Array.isArray(value) ? value : [value]).map((one) => typeof one === 'string'
|
|
75
|
+
? { kind: kind, layer, severity: one }
|
|
76
|
+
: { kind: kind, layer, severity: one.severity, tags: one.tags })));
|
package/dist/usage.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import type { Published } from './manifest.js';
|
|
|
3
3
|
import type { Usage } from './report.js';
|
|
4
4
|
/** The element counts, before the stylesheets add theirs. */
|
|
5
5
|
type Counted = Pick<Usage, 'axes' | 'composed' | 'slots' | 'tags'>;
|
|
6
|
+
/**
|
|
7
|
+
* The Helix versions loaded on the page: every copy of core adds its own to
|
|
8
|
+
* a list under a registered symbol as it loads. Empty for a core older than
|
|
9
|
+
* 0.137, which lists nothing.
|
|
10
|
+
*/
|
|
11
|
+
export declare const loadedVersions: () => string[];
|
|
6
12
|
/** The elements in the page now, counted once each. */
|
|
7
13
|
export declare const walk: (from: Document) => Counted;
|
|
8
14
|
/**
|
|
@@ -15,7 +21,7 @@ export declare const listed: (counted: Counted, tags: Map<string, Published>) =>
|
|
|
15
21
|
/** A running count of what renders, from `watch()`. */
|
|
16
22
|
export interface Watcher {
|
|
17
23
|
/** The counts so far. A copy: counting goes on after it is read. */
|
|
18
|
-
counts():
|
|
24
|
+
counts(): Counted;
|
|
19
25
|
/** Stops counting, and gives the slot back to whatever held it before. */
|
|
20
26
|
stop(): void;
|
|
21
27
|
}
|
|
@@ -40,5 +46,5 @@ export declare const watch: (from?: Document) => Watcher;
|
|
|
40
46
|
* the element's own styles say they exist. A dead one is a finding and is not
|
|
41
47
|
* counted as use.
|
|
42
48
|
*/
|
|
43
|
-
export declare const styled: (rules: Located[], tags: Map<string, Published>, answered: Answered) => Pick<Usage, 'hooks' | 'parts'>;
|
|
49
|
+
export declare const styled: (rules: Located[], tags: Map<string, Published>, answered: Answered, values?: boolean) => Pick<Usage, 'hookValues' | 'hooks' | 'parts'>;
|
|
44
50
|
export {};
|
package/dist/usage.js
CHANGED
|
@@ -13,6 +13,10 @@ const under = (into, key, make) => {
|
|
|
13
13
|
return found;
|
|
14
14
|
};
|
|
15
15
|
const empty = () => ({ axes: {}, composed: {}, slots: {}, tags: {} });
|
|
16
|
+
export const loadedVersions = () => {
|
|
17
|
+
const listed = globalThis[Symbol.for('fusion.dev/helix/versions')];
|
|
18
|
+
return Array.isArray(listed) ? listed.filter((one) => typeof one === 'string').sort() : [];
|
|
19
|
+
};
|
|
16
20
|
const tally = (counted, element) => {
|
|
17
21
|
const tag = element.localName;
|
|
18
22
|
if (!tag.startsWith('helix-')) {
|
|
@@ -104,8 +108,12 @@ export const watch = (from = document) => {
|
|
|
104
108
|
},
|
|
105
109
|
};
|
|
106
110
|
};
|
|
107
|
-
export const styled = (rules, tags, answered) => {
|
|
108
|
-
const counted = {
|
|
111
|
+
export const styled = (rules, tags, answered, values = true) => {
|
|
112
|
+
const counted = {
|
|
113
|
+
hooks: {},
|
|
114
|
+
hookValues: {},
|
|
115
|
+
parts: {},
|
|
116
|
+
};
|
|
109
117
|
for (const located of rules) {
|
|
110
118
|
const selector = located.rule.selectorText;
|
|
111
119
|
if (!selector?.includes('helix-')) {
|
|
@@ -123,6 +131,14 @@ export const styled = (rules, tags, answered) => {
|
|
|
123
131
|
if (property.startsWith('--') &&
|
|
124
132
|
(tags.get(tag)?.hooks.has(property) || answered.get(tag)?.has(property))) {
|
|
125
133
|
bump(under(counted.hooks, tag, () => ({})), property);
|
|
134
|
+
if (values) {
|
|
135
|
+
const value = (located.rule.style?.getPropertyValue?.(property) ?? '')
|
|
136
|
+
.trim()
|
|
137
|
+
.slice(0, 200);
|
|
138
|
+
if (value !== '') {
|
|
139
|
+
bump(under(under(counted.hookValues, tag, () => ({})), property, () => ({})), value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
126
142
|
}
|
|
127
143
|
}
|
|
128
144
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The version of the package this file ships in.
|
|
3
|
+
*
|
|
4
|
+
* **Written by `fusion/scripts/version.mjs`** wherever it writes the package's
|
|
5
|
+
* `package.json`, and stamped back into the repository beside it after a
|
|
6
|
+
* release, so the two never disagree; a spec beside this holds them to it.
|
|
7
|
+
* Nothing else in the running code knows which release it is.
|
|
8
|
+
*/
|
|
9
|
+
export declare const VERSION = "0.137.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = '0.137.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-diagnostics",
|
|
3
3
|
"description": "Finds the Helix mistakes that are valid CSS: dead parts, misnamed hooks, two copies of the library.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.137.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@fusion.dev/helix-testing": "0.
|
|
32
|
+
"@fusion.dev/helix-testing": "0.137.0",
|
|
33
33
|
"@types/bun": "^1.3.0"
|
|
34
34
|
},
|
|
35
35
|
"homepage": "https://fusion.dev",
|