@fusion.dev/helix-diagnostics 0.137.0 → 0.139.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 +172 -36
- package/dist/analyze.d.ts +25 -4
- package/dist/analyze.js +268 -50
- package/dist/ci.d.ts +18 -0
- package/dist/ci.js +30 -0
- package/dist/collect.d.ts +20 -1
- package/dist/collect.js +32 -17
- package/dist/config.d.ts +17 -0
- package/dist/config.js +30 -11
- package/dist/console.d.ts +9 -1
- package/dist/console.js +13 -3
- package/dist/diagnose.d.ts +75 -0
- package/dist/diagnose.js +76 -0
- package/dist/identity.js +4 -0
- package/dist/index.d.ts +5 -77
- package/dist/index.js +4 -73
- package/dist/init.d.ts +87 -0
- package/dist/init.js +326 -0
- package/dist/manifest.d.ts +65 -4
- package/dist/manifest.js +92 -27
- package/dist/overlay.d.ts +12 -0
- package/dist/overlay.js +278 -0
- package/dist/page.d.ts +25 -0
- package/dist/page.js +21 -0
- package/dist/report.d.ts +8 -1
- package/dist/report.js +2 -0
- package/dist/rules.js +2 -0
- package/dist/schedule.d.ts +57 -0
- package/dist/schedule.js +84 -0
- package/dist/usage.d.ts +10 -2
- package/dist/usage.js +42 -30
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -8,38 +8,140 @@ part a composed component never forwarded, two copies of the library on one page
|
|
|
8
8
|
npm install --save-dev @fusion.dev/helix-diagnostics
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Start it in your app
|
|
12
|
+
|
|
13
|
+
Call `initDiagnostics()` once, at your app's entry, before the app mounts:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { initDiagnostics } from '@fusion.dev/helix-diagnostics'
|
|
17
|
+
|
|
18
|
+
initDiagnostics({ environment: import.meta.env.MODE })
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It needs nothing from a framework, so the call is the same wherever Helix runs:
|
|
22
|
+
|
|
23
|
+
- `main.ts` in Vite, Vue, Svelte, or Solid;
|
|
24
|
+
- before `bootstrapApplication` in Angular, inside `ngZone.runOutsideAngular`
|
|
25
|
+
so its callbacks do not run change detection;
|
|
26
|
+
- `instrumentation-client.ts` in Next.js;
|
|
27
|
+
- a `<script type="module">` on a page with no bundler.
|
|
28
|
+
|
|
29
|
+
On a server, with no `document`, it does nothing, so it is safe in code that
|
|
30
|
+
server-side rendering also runs. A second call returns the first call's handle,
|
|
31
|
+
so React's strict mode and hot module replacement start nothing twice.
|
|
32
|
+
|
|
33
|
+
**What it does from there:**
|
|
34
|
+
|
|
35
|
+
- **Counts from the start**, every Helix element that renders, including ones
|
|
36
|
+
that go, such as a closed dialog.
|
|
37
|
+
- **Loads the lint data for the exact Helix version on the page**, from a public
|
|
38
|
+
npm CDN, about 18 KB compressed for each tier. Offline, or on a locked-down
|
|
39
|
+
network, pass it yourself:
|
|
40
|
+
`tiers: [{ lint }]` with `import lint from '@fusion.dev/helix/lint.json'`.
|
|
41
|
+
- **Runs itself**: after the page loads, after a navigation, and after the page
|
|
42
|
+
changes, once changes stop for a second, in an idle moment, and never sooner
|
|
43
|
+
than two seconds after the last run, nor sooner than twenty times its
|
|
44
|
+
duration. A page that never stops changing still runs within ten seconds.
|
|
45
|
+
Nothing runs in a hidden tab, or when nothing changed.
|
|
46
|
+
- **Prints each finding once per page**, in DevTools, at the levels your
|
|
47
|
+
`console` setting gives. The heading still counts every finding.
|
|
48
|
+
- **Shows the findings on the page**, in development. A badge in the corner
|
|
49
|
+
counts them and opens a list of each finding with its evidence. Hover over a
|
|
50
|
+
finding, or focus it, to outline the element it is about. The badge shows
|
|
51
|
+
only while the page has findings.
|
|
52
|
+
- **Hands each run to your app**: the `onReport` option, `subscribe()` on the
|
|
53
|
+
handle it returns, and a `helix-diagnostics` event on `document`.
|
|
54
|
+
|
|
55
|
+
**It never runs in production by accident.** With no `environment`, it runs only
|
|
56
|
+
on a local host: `localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, and `*.test`.
|
|
57
|
+
Production runs only with `environment: 'production'` and
|
|
58
|
+
`environments: { production: { sample } }`, and there each run goes to your app
|
|
59
|
+
alone. A visitor sees nothing in the console or on the page.
|
|
60
|
+
|
|
61
|
+
**It never breaks your app.** It never throws. A setting that names nothing is
|
|
62
|
+
printed with the closest real one, and the handle does nothing. A run that
|
|
63
|
+
throws is printed, and three failures in a row stop it. A subscriber that
|
|
64
|
+
throws stops neither the others nor the console.
|
|
65
|
+
|
|
66
|
+
| Option | What it is |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `environment` | `development`, `ci`, or `production`, as your build says it |
|
|
69
|
+
| `tiers` | Lint data or manifests to check against, where you pass them yourself |
|
|
70
|
+
| `cdn` | Where lint data loads from; `false` loads nothing |
|
|
71
|
+
| `page` | A function returning the page's path as your router names it, such as `/products/[id]` |
|
|
72
|
+
| `onReport` | Called with each run |
|
|
73
|
+
| `duplicates` | `duplicateRegistrations` from `@fusion.dev/helix-core` |
|
|
74
|
+
| `file` | Your project's `helix-diagnostics.json`, imported |
|
|
75
|
+
| Any setting below | `rules`, `console`, `sinks`, `send`, `detail`, `environments`, `project` |
|
|
76
|
+
|
|
77
|
+
The handle has `active`, `latest`, `run()`, `subscribe()`, and `stop()`. An
|
|
78
|
+
end-to-end test reaches it at `globalThis[Symbol.for('fusion.dev/helix/diagnostics')]`
|
|
79
|
+
to assert that a page has no findings.
|
|
80
|
+
|
|
81
|
+
The badge is one element, `helix-diagnostics-overlay`, with its own shadow root
|
|
82
|
+
and its own styles. Diagnostics leaves it out of everything it counts and reads.
|
|
83
|
+
It is never on a page in CI, so your screenshots never hold it. Turn it off in
|
|
84
|
+
development with `sinks: { overlay: false }`.
|
|
85
|
+
|
|
86
|
+
## Print the findings in your terminal
|
|
87
|
+
|
|
88
|
+
A page cannot write to your terminal, so the Helix CLI has a command that
|
|
89
|
+
receives each run beside your dev server. Install the CLI, then start the
|
|
90
|
+
command in the project's directory:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npm install --save-dev @fusion.dev/helix-cli
|
|
94
|
+
npx helix diagnostics
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then turn the relay on where you call `initDiagnostics()`:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
initDiagnostics({ environment: import.meta.env.MODE, sinks: { relay: true } })
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Each run that differs from the last one for its page is posted to
|
|
104
|
+
`http://127.0.0.1:7417`, and the command prints each finding the first time a
|
|
105
|
+
page has it. For a relay on another port, such as one started with
|
|
106
|
+
`--port 7500`, pass its address: `sinks: { relay: 'http://127.0.0.1:7500' }`.
|
|
107
|
+
The address is always on a local host.
|
|
108
|
+
|
|
109
|
+
- **It is off by default.** A browser prints a refused connection in DevTools
|
|
110
|
+
on every page load where nothing listens. With the relay on and the command
|
|
111
|
+
not running, one warning is printed in the console, and nothing is sent.
|
|
112
|
+
- **It serves your settings.** Where you pass no `file`, the page loads
|
|
113
|
+
`helix-diagnostics.json` from the directory the command runs in, so you do
|
|
114
|
+
not need to import it. Pass `--config <path>` for a file elsewhere. A file
|
|
115
|
+
that does not parse is not served.
|
|
116
|
+
- **It writes the CI file.** With `--out reports/helix-diagnostics.json`, it
|
|
117
|
+
keeps the latest run of every page there, in the shape `ciSink` writes.
|
|
118
|
+
- **It takes runs from pages on a local host only**: `localhost`, `127.0.0.1`,
|
|
119
|
+
`[::1]`, `*.localhost`, and `*.test`. It listens on `127.0.0.1`, so nothing
|
|
120
|
+
else on your network reaches it, and production pages never send to it.
|
|
121
|
+
|
|
122
|
+
## Run it once, directly
|
|
123
|
+
|
|
124
|
+
`diagnose()` is one run, which `initDiagnostics()` makes for you. Call it
|
|
125
|
+
yourself in a test, a script, or anywhere you want a single report:
|
|
126
|
+
|
|
11
127
|
```js
|
|
12
|
-
import { diagnose, report
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Before the components load, so every element that renders is counted.
|
|
18
|
-
const watcher = watch()
|
|
19
|
-
|
|
20
|
-
// Later, once the page has rendered, and again after a route change.
|
|
21
|
-
report(
|
|
22
|
-
diagnose({
|
|
23
|
-
duplicates: duplicateRegistrations,
|
|
24
|
-
tiers: [
|
|
25
|
-
{ manifest: styled, name: '@fusion.dev/helix' },
|
|
26
|
-
{ manifest: headless, name: '@fusion.dev/helix-headless' },
|
|
27
|
-
],
|
|
28
|
-
watcher,
|
|
29
|
-
}),
|
|
30
|
-
)
|
|
128
|
+
import { diagnose, report } from '@fusion.dev/helix-diagnostics'
|
|
129
|
+
import styled from '@fusion.dev/helix/lint.json' with { type: 'json' }
|
|
130
|
+
import headless from '@fusion.dev/helix-headless/lint.json' with { type: 'json' }
|
|
131
|
+
|
|
132
|
+
report(diagnose({ tiers: [{ lint: styled }, { lint: headless }] }))
|
|
31
133
|
```
|
|
32
134
|
|
|
33
|
-
`diagnose` needs at least one
|
|
34
|
-
|
|
35
|
-
tiers when both are installed, since
|
|
36
|
-
|
|
37
|
-
sink; the `Report` it returns is the
|
|
38
|
-
else as well.
|
|
135
|
+
`diagnose` needs at least one tier and says so instead of defaulting: with none,
|
|
136
|
+
every part name is unknown and every finding would be noise. It takes lint data
|
|
137
|
+
or a full `custom-elements.json`. Pass both tiers when both are installed, since
|
|
138
|
+
a part the headless tier publishes and the styled tier never forwarded is only
|
|
139
|
+
visible with both. `report` is the console sink; the `Report` it returns is the
|
|
140
|
+
same object, so you can send it somewhere else as well.
|
|
39
141
|
|
|
40
|
-
None of these produces an error anywhere. They are detectable only against
|
|
41
|
-
|
|
42
|
-
|
|
142
|
+
None of these faults produces an error anywhere. They are detectable only against
|
|
143
|
+
the manifest, which every Helix package publishes, so the diagnostic cannot
|
|
144
|
+
drift from what is true.
|
|
43
145
|
|
|
44
146
|
## What a report holds
|
|
45
147
|
|
|
@@ -95,11 +197,24 @@ works on it:
|
|
|
95
197
|
| Kind | Severity | Means |
|
|
96
198
|
| --- | --- | --- |
|
|
97
199
|
| `dead-part` | error | A `::part()` names a part the element does not publish |
|
|
98
|
-
| `dead-hook` | error | A
|
|
200
|
+
| `dead-hook` | error | A hook is set where nothing answers it, or a name close to a hook is set that nothing reads |
|
|
201
|
+
| `unknown-attribute` | error | A Helix element carries an attribute it does not take |
|
|
202
|
+
| `invalid-value` | error | An attribute with a closed set of values carries one outside it, so the element falls back to its default |
|
|
99
203
|
| `unforwarded-part` | warn | The headless tier publishes the part and the styled tier does not forward it, which is a gap in Helix |
|
|
100
204
|
| `unknown-tag` | error | A rule names a tag no manifest lists |
|
|
101
205
|
| `duplicate-registration` | error | Two copies of Helix are on the page |
|
|
102
|
-
| `manifest-skew` | error |
|
|
206
|
+
| `manifest-skew` | error | The manifest is from another version than the page runs. It replaces every other finding, since each would be judged against the wrong version. Turned off, the page is judged as usual |
|
|
207
|
+
|
|
208
|
+
**Hooks are checked wherever they are set**: on a Helix tag, a class, `:root`,
|
|
209
|
+
or any other selector. A hook on a selector naming no Helix tag is judged only
|
|
210
|
+
on what the page holds now. It is a finding when the selector matches elements
|
|
211
|
+
and none of them is, or holds, an element that answers it, while one that does
|
|
212
|
+
is on the page. A selector matching nothing yet is left alone, since it may be
|
|
213
|
+
for another route.
|
|
214
|
+
|
|
215
|
+
**Attributes are checked on the elements the page wrote.** The global HTML
|
|
216
|
+
attributes, `aria-*`, `data-*`, and the prefixes frameworks write are never
|
|
217
|
+
findings. The markup Helix renders inside its own components is Helix's.
|
|
103
218
|
|
|
104
219
|
## Configure it
|
|
105
220
|
|
|
@@ -135,6 +250,8 @@ diagnose({
|
|
|
135
250
|
| `console.findings` | `error`, `warn`, `info`, or `off`: the lowest severity printed | `info` |
|
|
136
251
|
| `console.coverage` | `info` or `off` | `info` |
|
|
137
252
|
| `console.usage` | `debug` or `off` | `debug` |
|
|
253
|
+
| `sinks.overlay` | `true` or `false`, for the badge in development | `true` |
|
|
254
|
+
| `sinks.relay` | `true`, `false`, or an address on a local host, such as `http://127.0.0.1:7417` | `false` |
|
|
138
255
|
| `send.<category>` | `true` or `false`, for `findings`, `usage`, `coverage`, `configuration`, `runtime`, `performance`, and `assist` | all on but `performance` |
|
|
139
256
|
| `detail.evidence` | `full`, `selector`, or `none` | `full` |
|
|
140
257
|
| `detail.hookValues` | `true` or `false` | `true` |
|
|
@@ -142,13 +259,32 @@ diagnose({
|
|
|
142
259
|
| `environments.<name>` | `true` or `false`; `production` takes `{ sample }` from 0 to 1 | development and CI on |
|
|
143
260
|
| `project` | `key`, `name`, and `tags` | none |
|
|
144
261
|
|
|
145
|
-
`send`, `detail`, and `environments` decide what
|
|
146
|
-
|
|
147
|
-
knows what a project withheld.
|
|
148
|
-
|
|
262
|
+
`send`, `detail`, and `environments` decide what the diagnostics service is
|
|
263
|
+
sent, and the service is not built yet. They are recorded in every report, so a
|
|
264
|
+
dashboard knows what a project withheld. The relay runs on your own machine and
|
|
265
|
+
receives each run whole. A setting or a value that does not exist throws, and
|
|
266
|
+
names the closest that does.
|
|
267
|
+
|
|
268
|
+
**What the page requests**: the lint data, from `cdn`, once per page load, and
|
|
269
|
+
the relay, where `sinks.relay` is on. Nothing else leaves the page.
|
|
270
|
+
|
|
271
|
+
## In CI
|
|
272
|
+
|
|
273
|
+
`@fusion.dev/helix-diagnostics/ci` gathers the runs of a CI job into one JSON
|
|
274
|
+
file, in the shape every sink shares, and says whether the job should fail:
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
import { ciSink } from '@fusion.dev/helix-diagnostics/ci'
|
|
278
|
+
|
|
279
|
+
// `runs` are the reports `diagnose` returned in each page your job opened.
|
|
280
|
+
const { code, summary } = ciSink(runs, { file: 'reports/helix-diagnostics.json' })
|
|
281
|
+
console.log(summary)
|
|
282
|
+
process.exitCode = code
|
|
283
|
+
```
|
|
149
284
|
|
|
150
|
-
|
|
151
|
-
|
|
285
|
+
A fault found on many pages is one line, with the pages it was on. `error`
|
|
286
|
+
findings fail the job by default; pass `failOn: 'warn'` to fail on warnings
|
|
287
|
+
too, or `'never'` to report without failing.
|
|
152
288
|
|
|
153
289
|
## Versions
|
|
154
290
|
|
package/dist/analyze.d.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* consumer's own `--my-app-gap` out of the findings without a list of
|
|
17
17
|
* exemptions, and what lets a finding say which tag it is wrong for.
|
|
18
18
|
*/
|
|
19
|
-
import type { Answered, Located } from './collect.js';
|
|
19
|
+
import type { Answered, Located, Readers } from './collect.js';
|
|
20
20
|
import { type Published } from './manifest.js';
|
|
21
|
+
import { type Page } from './page.js';
|
|
21
22
|
import type { Raw } from './report.js';
|
|
22
23
|
/** Every `helix-*` tag a selector mentions, in source order. */
|
|
23
24
|
export declare const targets: (selector: string) => string[];
|
|
@@ -26,6 +27,16 @@ export declare const parts: (selector: string) => Array<{
|
|
|
26
27
|
names: string[];
|
|
27
28
|
tag: string;
|
|
28
29
|
}>;
|
|
30
|
+
/**
|
|
31
|
+
* A selector made matchable now: pseudo-elements and the pseudo-classes of a
|
|
32
|
+
* moment (hover, focus) taken off, since a hook set on `.card:hover` reaches
|
|
33
|
+
* what `.card` matches. `null` where it cannot be judged from the page, as
|
|
34
|
+
* for `:host`, which only a component's own root matches.
|
|
35
|
+
*
|
|
36
|
+
* The longer pseudo-classes come first, so `:focus-visible` is taken whole
|
|
37
|
+
* and never read as `:focus` followed by `-visible`.
|
|
38
|
+
*/
|
|
39
|
+
export declare const matchable: (selector: string) => string | null;
|
|
29
40
|
/**
|
|
30
41
|
* The findings in one page's rules.
|
|
31
42
|
*
|
|
@@ -33,7 +44,7 @@ export declare const parts: (selector: string) => Array<{
|
|
|
33
44
|
* unforwarded part is phrased: the part exists, another tier publishes it, and
|
|
34
45
|
* the one being styled did not forward it.
|
|
35
46
|
*/
|
|
36
|
-
export declare const analyze: (rules: Located[], tags: Map<string, Published>, from?: Document, answered?: Answered) => Raw[];
|
|
47
|
+
export declare const analyze: (rules: Located[], tags: Map<string, Published>, from?: Document, answered?: Answered, used?: Set<string>, readers?: Readers) => Raw[];
|
|
37
48
|
/**
|
|
38
49
|
* Two copies of Helix on one page, which is not a CSS fault and is the most
|
|
39
50
|
* expensive one on the list.
|
|
@@ -51,7 +62,7 @@ export declare const analyze: (rules: Located[], tags: Map<string, Published>, f
|
|
|
51
62
|
* version old enough to predate the warning. That case reports what it can see,
|
|
52
63
|
* which is that a live element is not an instance of its own registered class.
|
|
53
64
|
*/
|
|
54
|
-
export declare const registrations: (from?: Document, known?: () => string[]) => Raw[];
|
|
65
|
+
export declare const registrations: (from?: Document, known?: () => string[], page?: Page) => Raw[];
|
|
55
66
|
/**
|
|
56
67
|
* Evidence that the manifest is from another version of Helix than the page
|
|
57
68
|
* runs: an element rendering a part its manifest entry does not list.
|
|
@@ -67,7 +78,7 @@ export declare const registrations: (from?: Document, known?: () => string[]) =>
|
|
|
67
78
|
* between versions reads as dead in every rule that styles it, and a hundred
|
|
68
79
|
* findings that are each the same mistake are worse than one that names it.
|
|
69
80
|
*/
|
|
70
|
-
export declare const skew: (tags: Map<string, Published>, from: Document) => Raw[];
|
|
81
|
+
export declare const skew: (tags: Map<string, Published>, from: Document, page?: Page) => Raw[];
|
|
71
82
|
/**
|
|
72
83
|
* A manifest from another version, read from the numbers where both sides
|
|
73
84
|
* have one: the version a manifest describes, against the versions of core
|
|
@@ -75,3 +86,13 @@ export declare const skew: (tags: Map<string, Published>, from: Document) => Raw
|
|
|
75
86
|
* `diagnose` makes first.
|
|
76
87
|
*/
|
|
77
88
|
export declare const versionSkew: (manifests: Record<string, string>, seen: string[]) => Raw[];
|
|
89
|
+
/**
|
|
90
|
+
* The attributes on the page's Helix elements that do nothing: a name the
|
|
91
|
+
* element does not take, and a value outside the ones a closed set allows.
|
|
92
|
+
* Both are valid HTML and silent, since the element falls back to its default.
|
|
93
|
+
*
|
|
94
|
+
* **Only what the page wrote.** An element Helix renders inside another is
|
|
95
|
+
* Helix's own markup. One finding per tag, attribute, and value, however many
|
|
96
|
+
* elements carry it.
|
|
97
|
+
*/
|
|
98
|
+
export declare const markup: (tags: Map<string, Published>, from: Document, page?: Page) => Raw[];
|