@fusion.dev/helix-diagnostics 0.138.0 → 0.140.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 +210 -34
- package/dist/analyze.d.ts +14 -3
- package/dist/analyze.js +76 -86
- package/dist/ci.js +14 -0
- package/dist/collect.d.ts +2 -1
- package/dist/collect.js +3 -15
- package/dist/config.d.ts +24 -0
- package/dist/config.js +42 -12
- package/dist/console.d.ts +11 -1
- package/dist/console.js +46 -5
- package/dist/diagnose.d.ts +75 -0
- package/dist/diagnose.js +93 -0
- package/dist/identity.d.ts +6 -0
- package/dist/identity.js +1 -0
- package/dist/index.d.ts +5 -77
- package/dist/index.js +4 -74
- package/dist/init.d.ts +87 -0
- package/dist/init.js +337 -0
- package/dist/manifest.d.ts +41 -3
- package/dist/manifest.js +79 -40
- package/dist/overlay.d.ts +12 -0
- package/dist/overlay.js +300 -0
- package/dist/page.d.ts +25 -0
- package/dist/page.js +21 -0
- package/dist/performance.d.ts +34 -0
- package/dist/performance.js +121 -0
- package/dist/report.d.ts +86 -0
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.js +39 -0
- package/dist/schedule.d.ts +57 -0
- package/dist/schedule.js +84 -0
- package/dist/usage.d.ts +34 -4
- package/dist/usage.js +38 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,38 +8,147 @@ 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
|
+
|
|
11
15
|
```js
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import headless from '@fusion.dev/helix-headless/custom-elements.json' with { type: 'json' }
|
|
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
|
-
)
|
|
16
|
+
import { initDiagnostics } from '@fusion.dev/helix-diagnostics'
|
|
17
|
+
|
|
18
|
+
initDiagnostics({ environment: import.meta.env.MODE })
|
|
31
19
|
```
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
- **Hears what the library reports as it runs**, such as a selectable table
|
|
53
|
+
with no `row-key`, and lists each event in the run, the badge, and the relay.
|
|
54
|
+
Core still prints each one as it happens, with the stack it happened in. Set
|
|
55
|
+
`core: { warnings: false }` to have diagnostics print them in its group
|
|
56
|
+
instead.
|
|
57
|
+
- **Measures what the components cost, where you ask**: each tag's first
|
|
58
|
+
updates, layout shift, and long tasks. See [Performance](#performance).
|
|
59
|
+
- **Hands each run to your app**: the `onReport` option, `subscribe()` on the
|
|
60
|
+
handle it returns, and a `helix-diagnostics` event on `document`.
|
|
61
|
+
|
|
62
|
+
**It never runs in production by accident.** With no `environment`, it runs only
|
|
63
|
+
on a local host: `localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, and `*.test`.
|
|
64
|
+
Production runs only with `environment: 'production'` and
|
|
65
|
+
`environments: { production: { sample } }`, and there each run goes to your app
|
|
66
|
+
alone. A visitor sees nothing in the console or on the page.
|
|
67
|
+
|
|
68
|
+
**It never breaks your app.** It never throws. A setting that names nothing is
|
|
69
|
+
printed with the closest real one, and the handle does nothing. A run that
|
|
70
|
+
throws is printed, and three failures in a row stop it. A subscriber that
|
|
71
|
+
throws stops neither the others nor the console.
|
|
72
|
+
|
|
73
|
+
| Option | What it is |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `environment` | `development`, `ci`, or `production`, as your build says it |
|
|
76
|
+
| `tiers` | Lint data or manifests to check against, where you pass them yourself |
|
|
77
|
+
| `cdn` | Where lint data loads from; `false` loads nothing |
|
|
78
|
+
| `page` | A function returning the page's path as your router names it, such as `/products/[id]` |
|
|
79
|
+
| `onReport` | Called with each run |
|
|
80
|
+
| `duplicates` | `duplicateRegistrations` from `@fusion.dev/helix-core` |
|
|
81
|
+
| `file` | Your project's `helix-diagnostics.json`, imported |
|
|
82
|
+
| Any setting below | `rules`, `console`, `core`, `sinks`, `send`, `detail`, `environments`, `project` |
|
|
83
|
+
|
|
84
|
+
The handle has `active`, `latest`, `run()`, `subscribe()`, and `stop()`. An
|
|
85
|
+
end-to-end test reaches it at `globalThis[Symbol.for('fusion.dev/helix/diagnostics')]`
|
|
86
|
+
to assert that a page has no findings.
|
|
87
|
+
|
|
88
|
+
The badge is one element, `helix-diagnostics-overlay`, with its own shadow root
|
|
89
|
+
and its own styles. Diagnostics leaves it out of everything it counts and reads.
|
|
90
|
+
It is never on a page in CI, so your screenshots never hold it. Turn it off in
|
|
91
|
+
development with `sinks: { overlay: false }`.
|
|
92
|
+
|
|
93
|
+
## Print the findings in your terminal
|
|
94
|
+
|
|
95
|
+
A page cannot write to your terminal, so the Helix CLI has a command that
|
|
96
|
+
receives each run beside your dev server. Install the CLI, then start the
|
|
97
|
+
command in the project's directory:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
npm install --save-dev @fusion.dev/helix-cli
|
|
101
|
+
npx helix diagnostics
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then turn the relay on where you call `initDiagnostics()`:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
initDiagnostics({ environment: import.meta.env.MODE, sinks: { relay: true } })
|
|
108
|
+
```
|
|
39
109
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
110
|
+
Each run that differs from the last one for its page is posted to
|
|
111
|
+
`http://127.0.0.1:7417`, and the command prints each finding the first time a
|
|
112
|
+
page has it. For a relay on another port, such as one started with
|
|
113
|
+
`--port 7500`, pass its address: `sinks: { relay: 'http://127.0.0.1:7500' }`.
|
|
114
|
+
The address is always on a local host.
|
|
115
|
+
|
|
116
|
+
- **It is off by default.** A browser prints a refused connection in DevTools
|
|
117
|
+
on every page load where nothing listens. With the relay on and the command
|
|
118
|
+
not running, one warning is printed in the console, and nothing is sent.
|
|
119
|
+
- **It serves your settings.** Where you pass no `file`, the page loads
|
|
120
|
+
`helix-diagnostics.json` from the directory the command runs in, so you do
|
|
121
|
+
not need to import it. Pass `--config <path>` for a file elsewhere. A file
|
|
122
|
+
that does not parse is not served.
|
|
123
|
+
- **It writes the CI file.** With `--out reports/helix-diagnostics.json`, it
|
|
124
|
+
keeps the latest run of every page there, in the shape `ciSink` writes.
|
|
125
|
+
- **It takes runs from pages on a local host only**: `localhost`, `127.0.0.1`,
|
|
126
|
+
`[::1]`, `*.localhost`, and `*.test`. It listens on `127.0.0.1`, so nothing
|
|
127
|
+
else on your network reaches it, and production pages never send to it.
|
|
128
|
+
|
|
129
|
+
## Run it once, directly
|
|
130
|
+
|
|
131
|
+
`diagnose()` is one run, which `initDiagnostics()` makes for you. Call it
|
|
132
|
+
yourself in a test, a script, or anywhere you want a single report:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
import { diagnose, report } from '@fusion.dev/helix-diagnostics'
|
|
136
|
+
import styled from '@fusion.dev/helix/lint.json' with { type: 'json' }
|
|
137
|
+
import headless from '@fusion.dev/helix-headless/lint.json' with { type: 'json' }
|
|
138
|
+
|
|
139
|
+
report(diagnose({ tiers: [{ lint: styled }, { lint: headless }] }))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`diagnose` needs at least one tier and says so instead of defaulting: with none,
|
|
143
|
+
every part name is unknown and every finding would be noise. It takes lint data
|
|
144
|
+
or a full `custom-elements.json`. Pass both tiers when both are installed, since
|
|
145
|
+
a part the headless tier publishes and the styled tier never forwarded is only
|
|
146
|
+
visible with both. `report` is the console sink; the `Report` it returns is the
|
|
147
|
+
same object, so you can send it somewhere else as well.
|
|
148
|
+
|
|
149
|
+
None of these faults produces an error anywhere. They are detectable only against
|
|
150
|
+
the manifest, which every Helix package publishes, so the diagnostic cannot
|
|
151
|
+
drift from what is true.
|
|
43
152
|
|
|
44
153
|
## What a report holds
|
|
45
154
|
|
|
@@ -61,6 +170,13 @@ A report is one run, in one shape for every sink, versioned by `schema`:
|
|
|
61
170
|
`integrity`, or `guideline`), an `owner` (`consumer`, or `helix` for a fault
|
|
62
171
|
in the library), a `fingerprint` that stays the same from one run to the next
|
|
63
172
|
for the same fault, and the rule's own text as evidence.
|
|
173
|
+
- **`performance`**: what the components cost, by tag, where you asked for it.
|
|
174
|
+
See [Performance](#performance).
|
|
175
|
+
- **`runtime`**: what the library reported going wrong as it ran on the page,
|
|
176
|
+
from its first load. One entry per distinct event, with its `kind`, its
|
|
177
|
+
`tag`, the sentence that says what to do, how many times it was reported,
|
|
178
|
+
and whether core printed it. A `fingerprint` stays the same from one run to
|
|
179
|
+
the next, with the numbers in the sentence left out.
|
|
64
180
|
- **`usage`**: the Helix elements on the page by tag, the axes set on them
|
|
65
181
|
(`emphasis`, `material`, `separation`, and `tone`), the named slots filled,
|
|
66
182
|
the elements Helix renders inside other Helix elements, and the parts and
|
|
@@ -86,9 +202,11 @@ works on it:
|
|
|
86
202
|
| A finding at `error` | `console.error` |
|
|
87
203
|
| A finding at `warn` | `console.warn` |
|
|
88
204
|
| A finding at `info` | `console.info` |
|
|
205
|
+
| A runtime event core left to diagnostics | `console.warn`, or `console.error` for a failure |
|
|
89
206
|
| Stylesheets that could not be read | `console.warn` |
|
|
90
207
|
| Coverage, and findings turned off | `console.info` |
|
|
91
208
|
| Usage | `console.debug`, hidden at the default level |
|
|
209
|
+
| Performance, where you asked for it | `console.info`, when the measures change |
|
|
92
210
|
|
|
93
211
|
## Finding kinds
|
|
94
212
|
|
|
@@ -114,6 +232,58 @@ for another route.
|
|
|
114
232
|
attributes, `aria-*`, `data-*`, and the prefixes frameworks write are never
|
|
115
233
|
findings. The markup Helix renders inside its own components is Helix's.
|
|
116
234
|
|
|
235
|
+
## Runtime events
|
|
236
|
+
|
|
237
|
+
Helix reports these itself as it runs, through one channel in
|
|
238
|
+
`@fusion.dev/helix-core`. Each is printed in the console as it happens, and
|
|
239
|
+
diagnostics counts it by kind and tag. None fails a CI job.
|
|
240
|
+
|
|
241
|
+
| Kind | Means |
|
|
242
|
+
| --- | --- |
|
|
243
|
+
| `duplicate-registration` | Two copies of Helix are on the page. Also the finding of the same name |
|
|
244
|
+
| `fullscreen-refused` | The browser refused fullscreen, usually in a frame without `allow="fullscreen"` |
|
|
245
|
+
| `max-past-fold` | An avatar group's `max` is past the number its fold is written for |
|
|
246
|
+
| `min-out-of-range` | A rating's `min` is not a score between 0 and its `max` |
|
|
247
|
+
| `misplaced-handler` | A React `on` prop names an event that another element dispatches |
|
|
248
|
+
| `missing-layout` | The three.js graph driver could not load `d3-force-3d` |
|
|
249
|
+
| `missing-row-key` | A selectable data table has no `row-key`, so it knows each row by its place |
|
|
250
|
+
| `missing-stencil-parts` | A graph stencil names parts its shape does not have |
|
|
251
|
+
| `missing-trigger` | A tooltip's `for` names no element in its own tree |
|
|
252
|
+
| `negative-value` | An area chart was given a negative number, which it draws as none |
|
|
253
|
+
| `not-a-graph` | `connectGraph` was given an element that is not a graph frame |
|
|
254
|
+
| `picture-in-picture-refused` | The browser refused picture in picture |
|
|
255
|
+
| `removed-callback` | A React prop is a callback that was removed and never ran from JSX |
|
|
256
|
+
| `solid-handler-syntax` | A React prop uses Solid's `on:` syntax |
|
|
257
|
+
| `stencil-regions-unused` | A graph stencil's regions could not be used |
|
|
258
|
+
| `stencil-without-inside` | A graph stencil has no inside to place the notes in |
|
|
259
|
+
| `too-many-rows` | A data table is drawing more rows at once than it stays responsive to |
|
|
260
|
+
| `two-default-operators` | A filter field marks more than one operator as its default |
|
|
261
|
+
| `unknown-event` | A React `on` prop names an event that no element dispatches |
|
|
262
|
+
| `unlinked-trigger` | A tooltip's trigger does not name it in `aria-describedby` |
|
|
263
|
+
| `unsettled-window` | A virtualized list's box has no height of its own |
|
|
264
|
+
| `untold-trigger` | A slotted trigger is a plain element, which cannot carry `aria-expanded` |
|
|
265
|
+
| `wrapped-trigger` | A tooltip wraps its trigger, so its text reaches no screen reader |
|
|
266
|
+
|
|
267
|
+
## Performance
|
|
268
|
+
|
|
269
|
+
Diagnostics measures what the components cost only where you ask for it. Set
|
|
270
|
+
`console: { performance: 'info' }` to print the measures in DevTools, or
|
|
271
|
+
`send: { performance: true }` to keep them in each run, for your app, the
|
|
272
|
+
relay, and the CI file. For each tag rendered on the page, it measures:
|
|
273
|
+
|
|
274
|
+
- **First updates**: each element's own first update, from `willUpdate` to
|
|
275
|
+
`updated`, as a count, a median, a 95th percentile, and the slowest. Time an
|
|
276
|
+
element spends waiting behind other elements is not counted.
|
|
277
|
+
- **Layout shift**: the shift its elements moved, summed as the browser scores
|
|
278
|
+
it. A shift that follows a press or a keystroke is left out.
|
|
279
|
+
- **Long tasks**: how many tasks of 50 ms or more one of its first updates ran
|
|
280
|
+
in.
|
|
281
|
+
|
|
282
|
+
Core times each first update only while diagnostics is measuring, so a page
|
|
283
|
+
that has not asked pays one property read per element. Layout shift and long
|
|
284
|
+
tasks are measured in Chromium only. Elsewhere `measured` says so, and their
|
|
285
|
+
numbers are left out.
|
|
286
|
+
|
|
117
287
|
## Configure it
|
|
118
288
|
|
|
119
289
|
Every setting can be given in three places, and a later place wins, field by
|
|
@@ -148,6 +318,11 @@ diagnose({
|
|
|
148
318
|
| `console.findings` | `error`, `warn`, `info`, or `off`: the lowest severity printed | `info` |
|
|
149
319
|
| `console.coverage` | `info` or `off` | `info` |
|
|
150
320
|
| `console.usage` | `debug` or `off` | `debug` |
|
|
321
|
+
| `console.runtime` | `warn` or `off`: the runtime events core left to diagnostics | `warn` |
|
|
322
|
+
| `console.performance` | `info` or `off`: what the components cost, which is measured where this is on | `off` |
|
|
323
|
+
| `core.warnings` | `true` or `false`: whether core prints each runtime event as it happens | `true` |
|
|
324
|
+
| `sinks.overlay` | `true` or `false`, for the badge in development | `true` |
|
|
325
|
+
| `sinks.relay` | `true`, `false`, or an address on a local host, such as `http://127.0.0.1:7417` | `false` |
|
|
151
326
|
| `send.<category>` | `true` or `false`, for `findings`, `usage`, `coverage`, `configuration`, `runtime`, `performance`, and `assist` | all on but `performance` |
|
|
152
327
|
| `detail.evidence` | `full`, `selector`, or `none` | `full` |
|
|
153
328
|
| `detail.hookValues` | `true` or `false` | `true` |
|
|
@@ -155,13 +330,14 @@ diagnose({
|
|
|
155
330
|
| `environments.<name>` | `true` or `false`; `production` takes `{ sample }` from 0 to 1 | development and CI on |
|
|
156
331
|
| `project` | `key`, `name`, and `tags` | none |
|
|
157
332
|
|
|
158
|
-
`send`, `detail`, and `environments` decide what
|
|
159
|
-
|
|
160
|
-
knows what a project withheld.
|
|
161
|
-
|
|
333
|
+
`send`, `detail`, and `environments` decide what the diagnostics service is
|
|
334
|
+
sent, and the service is not built yet. They are recorded in every report, so a
|
|
335
|
+
dashboard knows what a project withheld. The relay runs on your own machine and
|
|
336
|
+
receives each run whole. A setting or a value that does not exist throws, and
|
|
337
|
+
names the closest that does.
|
|
162
338
|
|
|
163
|
-
|
|
164
|
-
|
|
339
|
+
**What the page requests**: the lint data, from `cdn`, once per page load, and
|
|
340
|
+
the relay, where `sinks.relay` is on. Nothing else leaves the page.
|
|
165
341
|
|
|
166
342
|
## In CI
|
|
167
343
|
|
package/dist/analyze.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
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
|
*
|
|
@@ -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
|
|
@@ -84,4 +95,4 @@ export declare const versionSkew: (manifests: Record<string, string>, seen: stri
|
|
|
84
95
|
* Helix's own markup. One finding per tag, attribute, and value, however many
|
|
85
96
|
* elements carry it.
|
|
86
97
|
*/
|
|
87
|
-
export declare const markup: (tags: Map<string, Published>, from: Document) => Raw[];
|
|
98
|
+
export declare const markup: (tags: Map<string, Published>, from: Document, page?: Page) => Raw[];
|
package/dist/analyze.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { nearest } from './manifest.js';
|
|
2
|
+
import { pageOf } from './page.js';
|
|
2
3
|
export const targets = (selector) => {
|
|
3
4
|
const found = new Set();
|
|
4
5
|
for (const match of selector.matchAll(/(?<![\w.#-])helix-[a-z0-9]+(?:-[a-z0-9]+)*/g)) {
|
|
@@ -16,7 +17,7 @@ export const parts = (selector) => {
|
|
|
16
17
|
}
|
|
17
18
|
return found;
|
|
18
19
|
};
|
|
19
|
-
const matchable = (selector) => {
|
|
20
|
+
export const matchable = (selector) => {
|
|
20
21
|
if (selector.includes(':host')) {
|
|
21
22
|
return null;
|
|
22
23
|
}
|
|
@@ -61,7 +62,7 @@ const reachesAnswer = (host, property, answering, readers, depth = 0) => {
|
|
|
61
62
|
}
|
|
62
63
|
return false;
|
|
63
64
|
};
|
|
64
|
-
const loose = (located, answerers, from, used, onPage, listedBy, readers) => {
|
|
65
|
+
const loose = (located, answerers, from, used, onPage, listedBy, readers, suggested) => {
|
|
65
66
|
const findings = [];
|
|
66
67
|
if (located.scope instanceof ShadowRoot && located.scope.host.localName.startsWith('helix-')) {
|
|
67
68
|
return findings;
|
|
@@ -78,7 +79,11 @@ const loose = (located, answerers, from, used, onPage, listedBy, readers) => {
|
|
|
78
79
|
if (used.has(property)) {
|
|
79
80
|
continue;
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
+
let suggestion = suggested.get(property);
|
|
83
|
+
if (!suggested.has(property)) {
|
|
84
|
+
suggestion = nearest(property, answerers.keys(), HOOK_DISTANCE);
|
|
85
|
+
suggested.set(property, suggestion);
|
|
86
|
+
}
|
|
82
87
|
if (!suggestion) {
|
|
83
88
|
continue;
|
|
84
89
|
}
|
|
@@ -152,6 +157,7 @@ export const analyze = (rules, tags, from, answered = new Map(), used = new Set(
|
|
|
152
157
|
}
|
|
153
158
|
}
|
|
154
159
|
}
|
|
160
|
+
const suggested = new Map();
|
|
155
161
|
const present = new Map();
|
|
156
162
|
const onPage = (property, answering) => {
|
|
157
163
|
let found = present.get(property);
|
|
@@ -213,7 +219,7 @@ export const analyze = (rules, tags, from, answered = new Map(), used = new Set(
|
|
|
213
219
|
const mentioned = targets(selector).map((tag) => [tag, tags.get(tag)]);
|
|
214
220
|
const known = mentioned.filter(([, element]) => element);
|
|
215
221
|
if (known.length === 0) {
|
|
216
|
-
findings.push(...loose(located, answeredBy, from, used, onPage, listedBy, readers));
|
|
222
|
+
findings.push(...loose(located, answeredBy, from, used, onPage, listedBy, readers, suggested));
|
|
217
223
|
continue;
|
|
218
224
|
}
|
|
219
225
|
for (const property of Array.from(located.rule.style ?? [])) {
|
|
@@ -267,7 +273,7 @@ export const analyze = (rules, tags, from, answered = new Map(), used = new Set(
|
|
|
267
273
|
}
|
|
268
274
|
return findings;
|
|
269
275
|
};
|
|
270
|
-
export const registrations = (from = document, known = () => []) => {
|
|
276
|
+
export const registrations = (from = document, known = () => [], page = pageOf(from)) => {
|
|
271
277
|
const findings = [];
|
|
272
278
|
const registry = globalThis.customElements;
|
|
273
279
|
const said = new Set();
|
|
@@ -289,7 +295,7 @@ export const registrations = (from = document, known = () => []) => {
|
|
|
289
295
|
if (!registry) {
|
|
290
296
|
return findings;
|
|
291
297
|
}
|
|
292
|
-
for (const element of
|
|
298
|
+
for (const element of page.elements) {
|
|
293
299
|
const tag = element.localName;
|
|
294
300
|
if (!tag.startsWith('helix-') || said.has(tag)) {
|
|
295
301
|
continue;
|
|
@@ -301,37 +307,30 @@ export const registrations = (from = document, known = () => []) => {
|
|
|
301
307
|
}
|
|
302
308
|
return findings;
|
|
303
309
|
};
|
|
304
|
-
export const skew = (tags, from) => {
|
|
310
|
+
export const skew = (tags, from, page = pageOf(from)) => {
|
|
305
311
|
const findings = [];
|
|
306
312
|
const said = new Set();
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
313
|
+
for (const element of page.elements) {
|
|
314
|
+
const shadow = element.shadowRoot;
|
|
315
|
+
if (!shadow) {
|
|
316
|
+
continue;
|
|
310
317
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
kind: 'manifest-skew',
|
|
325
|
-
message: `\`${tag}\` renders ${missing.map((name) => `\`${name}\``).join(', ')}, which the manifest passed in does not list, so the manifest is from a different version of Helix than the page runs. Every other finding would be judged against the wrong version, so none are reported. Pass the \`custom-elements.json\` from the installed packages.`,
|
|
326
|
-
subject: tag,
|
|
327
|
-
tag,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
318
|
+
const tag = element.localName;
|
|
319
|
+
const published = tags.get(tag);
|
|
320
|
+
if (published && !said.has(tag)) {
|
|
321
|
+
const rendered = new Set([...shadow.querySelectorAll('[part]')].flatMap((node) => (node.getAttribute('part') ?? '').split(/\s+/).filter(Boolean)));
|
|
322
|
+
const missing = [...rendered].filter((name) => !published.parts.has(name)).sort();
|
|
323
|
+
if (missing.length > 0) {
|
|
324
|
+
said.add(tag);
|
|
325
|
+
findings.push({
|
|
326
|
+
kind: 'manifest-skew',
|
|
327
|
+
message: `\`${tag}\` renders ${missing.map((name) => `\`${name}\``).join(', ')}, which the manifest passed in does not list, so the manifest is from a different version of Helix than the page runs. Every other finding would be judged against the wrong version, so none are reported. Pass the \`custom-elements.json\` from the installed packages.`,
|
|
328
|
+
subject: tag,
|
|
329
|
+
tag,
|
|
330
|
+
});
|
|
330
331
|
}
|
|
331
|
-
visit(shadow, depth + 1);
|
|
332
332
|
}
|
|
333
|
-
}
|
|
334
|
-
visit(from, 0);
|
|
333
|
+
}
|
|
335
334
|
return findings;
|
|
336
335
|
};
|
|
337
336
|
export const versionSkew = (manifests, seen) => seen.length === 0
|
|
@@ -380,66 +379,57 @@ const GLOBAL = new Set([
|
|
|
380
379
|
]);
|
|
381
380
|
const PREFIXES = ['aria-', 'data-', 'on', '_ngcontent-', '_nghost-', 'ng-', 'v-', 'x-', ':', '@'];
|
|
382
381
|
const HOOK_DISTANCE = 2;
|
|
383
|
-
export const markup = (tags, from) => {
|
|
382
|
+
export const markup = (tags, from, page = pageOf(from)) => {
|
|
384
383
|
const found = new Map();
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
384
|
+
for (const element of page.elements) {
|
|
385
|
+
const host = element.getRootNode();
|
|
386
|
+
const composed = host instanceof ShadowRoot && host.host.localName.startsWith('helix-');
|
|
387
|
+
const published = composed ? undefined : tags.get(element.localName);
|
|
388
|
+
if (published) {
|
|
389
|
+
for (const { name, value } of Array.from(element.attributes)) {
|
|
390
|
+
if (GLOBAL.has(name) ||
|
|
391
|
+
PREFIXES.some((prefix) => name.startsWith(prefix)) ||
|
|
392
|
+
published.written.has(name)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const tag = element.localName;
|
|
396
|
+
const allows = published.attributes.get(name);
|
|
397
|
+
let raw;
|
|
398
|
+
if (allows === undefined) {
|
|
399
|
+
const suggestion = nearest(name, published.attributes.keys());
|
|
400
|
+
raw = {
|
|
401
|
+
expected: suggestion,
|
|
402
|
+
kind: 'unknown-attribute',
|
|
403
|
+
message: suggestion
|
|
404
|
+
? `\`${tag}\` takes no attribute \`${name}\`, so it does nothing. The closest it takes is \`${suggestion}\`.`
|
|
405
|
+
: `\`${tag}\` takes no attribute \`${name}\`, so it does nothing.`,
|
|
406
|
+
subject: `<${tag} ${name}>`,
|
|
407
|
+
tag,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
else if (allows && value !== '' && !allows.has(value)) {
|
|
411
|
+
const suggestion = nearest(value, allows);
|
|
412
|
+
raw = {
|
|
413
|
+
expected: suggestion,
|
|
414
|
+
kind: 'invalid-value',
|
|
415
|
+
message: `\`${tag}\`'s \`${name}\` takes ${[...allows].map((one) => `\`${one}\``).join(', ')}, so \`${value}\` falls back to the default.${suggestion ? ` The closest is \`${suggestion}\`.` : ''}`,
|
|
416
|
+
subject: `<${tag} ${name}="${value}">`,
|
|
417
|
+
tag,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (raw) {
|
|
421
|
+
const key = `${raw.kind} ${raw.subject}`;
|
|
422
|
+
const seen = found.get(key);
|
|
423
|
+
if (seen) {
|
|
424
|
+
seen.count += 1;
|
|
424
425
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const seen = found.get(key);
|
|
428
|
-
if (seen) {
|
|
429
|
-
seen.count += 1;
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
found.set(key, { ...raw, count: 1 });
|
|
433
|
-
}
|
|
426
|
+
else {
|
|
427
|
+
found.set(key, { ...raw, count: 1 });
|
|
434
428
|
}
|
|
435
429
|
}
|
|
436
430
|
}
|
|
437
|
-
if (element.shadowRoot) {
|
|
438
|
-
visit(element.shadowRoot, depth + 1);
|
|
439
|
-
}
|
|
440
431
|
}
|
|
441
|
-
}
|
|
442
|
-
visit(from, 0);
|
|
432
|
+
}
|
|
443
433
|
return [...found.values()].map(({ count, ...raw }) => ({
|
|
444
434
|
...raw,
|
|
445
435
|
message: count > 1 ? `${raw.message} On ${count} elements.` : raw.message,
|
package/dist/ci.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
+
import { shownRuntime } from './runtime.js';
|
|
3
4
|
const RANK = { error: 3, info: 1, warn: 2 };
|
|
4
5
|
const counted = (count, noun) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
5
6
|
export const ciSink = (runs, { file, failOn = 'error' }) => {
|
|
@@ -17,8 +18,17 @@ export const ciSink = (runs, { file, failOn = 'error' }) => {
|
|
|
17
18
|
const of = (severity) => all.filter(({ finding }) => finding.severity === severity).length;
|
|
18
19
|
const failing = failOn === 'never' ? [] : all.filter(({ finding }) => RANK[finding.severity] >= RANK[failOn]);
|
|
19
20
|
const unread = runs.reduce((total, run) => total + run.coverage.unreadable.length, 0);
|
|
21
|
+
const events = new Map();
|
|
22
|
+
for (const run of runs) {
|
|
23
|
+
for (const entry of shownRuntime(run.runtime ?? [])) {
|
|
24
|
+
const event = events.get(entry.fingerprint) ?? { entry, pages: new Set() };
|
|
25
|
+
event.pages.add(run.run.page ?? '(no page)');
|
|
26
|
+
events.set(entry.fingerprint, event);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
20
29
|
const lines = [
|
|
21
30
|
`helix diagnostics: ${counted(runs.length, 'run')}, ${counted(of('error'), 'error')}, ${counted(of('warn'), 'warning')}, ${of('info')} info` +
|
|
31
|
+
(events.size > 0 ? `, ${events.size} runtime` : '') +
|
|
22
32
|
(unread > 0 ? `, ${counted(unread, 'unreadable stylesheet')}` : '') +
|
|
23
33
|
`. Written to ${file}.`,
|
|
24
34
|
];
|
|
@@ -26,5 +36,9 @@ export const ciSink = (runs, { file, failOn = 'error' }) => {
|
|
|
26
36
|
const where = [...pages].sort();
|
|
27
37
|
lines.push(` ${finding.severity} ${finding.kind}: ${finding.subject}`, ` ${finding.message}`, ` on ${where.slice(0, 5).join(', ')}${where.length > 5 ? `, and ${where.length - 5} more` : ''}`);
|
|
28
38
|
}
|
|
39
|
+
for (const { entry, pages } of events.values()) {
|
|
40
|
+
const where = [...pages].sort();
|
|
41
|
+
lines.push(` runtime ${entry.kind}${entry.tag ? ` <${entry.tag}>` : ''}`, ` ${entry.message}`, ` on ${where.slice(0, 5).join(', ')}${where.length > 5 ? `, and ${where.length - 5} more` : ''}`);
|
|
42
|
+
}
|
|
29
43
|
return { code: failing.length > 0 ? 1 : 0, summary: lines.join('\n') };
|
|
30
44
|
};
|
package/dist/collect.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* the only way to know. Each one becomes a `Coverage.unreadable` entry, so the
|
|
16
16
|
* hole is in the output rather than in the reader's assumptions.
|
|
17
17
|
*/
|
|
18
|
+
import { type Page } from './page.js';
|
|
18
19
|
import type { Coverage } from './report.js';
|
|
19
20
|
/** One rule, with where it came from. */
|
|
20
21
|
export interface Located {
|
|
@@ -67,7 +68,7 @@ export type Readers = Map<string, Array<{
|
|
|
67
68
|
* hundred passes over rules already read. A `Set` keyed on the sheet object
|
|
68
69
|
* fixes the number and the cost together.
|
|
69
70
|
*/
|
|
70
|
-
export declare const collect: (from?: Document) => {
|
|
71
|
+
export declare const collect: (from?: Document, page?: Page) => {
|
|
71
72
|
answered: Answered;
|
|
72
73
|
coverage: Coverage;
|
|
73
74
|
readers: Readers;
|