@fusion.dev/helix-diagnostics 0.206.0 → 0.207.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 +103 -19
- package/dist/analyze.d.ts +5 -3
- package/dist/analyze.js +103 -20
- package/dist/config.d.ts +6 -0
- package/dist/config.js +9 -1
- package/dist/diagnose.js +2 -1
- package/dist/identity.js +4 -0
- package/dist/init.js +2 -1
- package/dist/manifest.d.ts +45 -1
- package/dist/manifest.js +24 -2
- package/dist/report.d.ts +7 -4
- package/dist/report.js +3 -0
- package/dist/rules.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Finds the Helix mistakes that are valid CSS and therefore silent: a `::part()`
|
|
4
4
|
naming a part nothing publishes, a custom property named for the wrong family, a
|
|
5
5
|
part a composed component never forwarded, two copies of the library on one page.
|
|
6
|
+
[Diagnostics](https://fusion.dev/helix/docs/diagnostics) in the Helix
|
|
7
|
+
documentation walks through the setup.
|
|
6
8
|
|
|
7
9
|
```sh
|
|
8
10
|
npm install --save-dev @fusion.dev/helix-diagnostics
|
|
@@ -10,14 +12,44 @@ npm install --save-dev @fusion.dev/helix-diagnostics
|
|
|
10
12
|
|
|
11
13
|
## Start it in your app
|
|
12
14
|
|
|
13
|
-
Call `initDiagnostics()` once, at your app's entry, before the app mounts
|
|
15
|
+
Call `initDiagnostics()` once, at your app's entry, before the app mounts. To
|
|
16
|
+
keep it out of what you ship, put the call in a module of its own and load that
|
|
17
|
+
module in development only:
|
|
14
18
|
|
|
15
19
|
```js
|
|
20
|
+
// diagnostics.js
|
|
16
21
|
import { initDiagnostics } from '@fusion.dev/helix-diagnostics'
|
|
17
22
|
|
|
18
|
-
initDiagnostics({ environment:
|
|
23
|
+
initDiagnostics({ environment: 'development' })
|
|
19
24
|
```
|
|
20
25
|
|
|
26
|
+
```js
|
|
27
|
+
// main.js
|
|
28
|
+
if (import.meta.env.DEV) {
|
|
29
|
+
void import('./diagnostics.js')
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`vite build` replaces `import.meta.env.DEV` with `false` and drops the import
|
|
34
|
+
with it, so a production page never loads diagnostics. Next.js does the same
|
|
35
|
+
with `process.env.NODE_ENV === 'development'`, and Angular with `isDevMode()`.
|
|
36
|
+
Every Helix template starts diagnostics this way.
|
|
37
|
+
|
|
38
|
+
To sample production pages as well, call it from the entry itself and declare
|
|
39
|
+
where each build runs:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import { initDiagnostics } from '@fusion.dev/helix-diagnostics'
|
|
43
|
+
|
|
44
|
+
initDiagnostics({
|
|
45
|
+
environment: import.meta.env.MODE,
|
|
46
|
+
environments: { production: { sample: 0.1 } },
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`environment` takes `development`, `ci`, or `production`. Any other value, such
|
|
51
|
+
as a `staging` mode, is printed in the console, and nothing starts.
|
|
52
|
+
|
|
21
53
|
It needs nothing from a framework, so the call is the same wherever Helix runs:
|
|
22
54
|
|
|
23
55
|
- `main.ts` in Vite, Vue, Svelte, or Solid;
|
|
@@ -35,9 +67,13 @@ so React's strict mode and hot module replacement start nothing twice.
|
|
|
35
67
|
- **Counts from the start**, every Helix element that renders, including ones
|
|
36
68
|
that go, such as a closed dialog.
|
|
37
69
|
- **Loads the lint data for the exact Helix version on the page**, from a public
|
|
38
|
-
npm CDN,
|
|
39
|
-
|
|
70
|
+
npm CDN, one file for each tier. Offline, or on a locked-down network, pass it
|
|
71
|
+
yourself:
|
|
40
72
|
`tiers: [{ lint }]` with `import lint from '@fusion.dev/helix/lint.json'`.
|
|
73
|
+
An add-on package's lint data is passed the same way, and only that way:
|
|
74
|
+
`{ lint: sandbox }` with
|
|
75
|
+
`import sandbox from '@fusion.dev/helix-sandbox/lint.json'`. Without it,
|
|
76
|
+
each of the add-on's elements is an unknown tag.
|
|
41
77
|
- **Runs itself**: after the page loads, after a navigation, and after the page
|
|
42
78
|
changes, once changes stop for a second, in an idle moment, and never sooner
|
|
43
79
|
than two seconds after the last run, nor sooner than twenty times its
|
|
@@ -72,7 +108,7 @@ throws stops neither the others nor the console.
|
|
|
72
108
|
|
|
73
109
|
| Option | What it is |
|
|
74
110
|
| --- | --- |
|
|
75
|
-
| `environment` | `development`, `ci`, or `production`, as your build says it |
|
|
111
|
+
| `environment` | `development`, `ci`, or `production`, as your build says it. Any other value is printed, and nothing starts |
|
|
76
112
|
| `tiers` | Lint data or manifests to check against, where you pass them yourself |
|
|
77
113
|
| `cdn` | Where lint data loads from; `false` loads nothing |
|
|
78
114
|
| `page` | A function returning the page's path as your router names it, such as `/products/[id]` |
|
|
@@ -119,18 +155,22 @@ The address is always on a local host.
|
|
|
119
155
|
- **It serves your settings.** Where you pass no `file`, the page loads
|
|
120
156
|
`helix-diagnostics.json` from the directory the command runs in, so you do
|
|
121
157
|
not need to import it. Pass `--config <path>` for a file elsewhere. A file
|
|
122
|
-
that does not parse is not served.
|
|
158
|
+
that does not parse is not served, and the command prints why.
|
|
123
159
|
- **It writes the CI file.** With `--out reports/helix-diagnostics.json`, it
|
|
124
160
|
keeps the latest run of every page there, in the shape `ciSink` writes.
|
|
125
161
|
- **It takes runs from pages on a local host only**: `localhost`, `127.0.0.1`,
|
|
126
162
|
`[::1]`, `*.localhost`, and `*.test`. It listens on `127.0.0.1`, so nothing
|
|
127
163
|
else on your network reaches it, and production pages never send to it.
|
|
164
|
+
- **It exits 3 where it cannot listen**, such as on a port another process
|
|
165
|
+
holds.
|
|
128
166
|
|
|
129
167
|
## Scan your code
|
|
130
168
|
|
|
131
169
|
`helix scan` reads every stylesheet, template, and module in your repository,
|
|
132
170
|
judges them against the lint data of the Helix packages you installed, and
|
|
133
|
-
writes one run in the shape a page writes, with `source: "code"
|
|
171
|
+
writes one run in the shape a page writes, with `source: "code"`. It reads the
|
|
172
|
+
lint data of each installed `@fusion.dev` package that publishes `./lint.json`
|
|
173
|
+
and is not private, an add-on such as `@fusion.dev/helix-sandbox` among them:
|
|
134
174
|
|
|
135
175
|
```sh
|
|
136
176
|
npx helix scan
|
|
@@ -164,15 +204,22 @@ npx helix scan --build dist --out reports/helix-diagnostics.json
|
|
|
164
204
|
- **With `--build`, it weighs your bundle**: what each Helix family and
|
|
165
205
|
package adds to it, from its source maps, as `performance.weight`. Turn
|
|
166
206
|
source maps on in your bundler, and pass the folder it writes the browser's
|
|
167
|
-
files to, such as `dist` or `.next/static`.
|
|
207
|
+
files to, such as `dist` or `.next/static`. A bundle's map is the one the
|
|
208
|
+
comment on its last line names, or the `.map` file beside it, so a hidden
|
|
209
|
+
map is read too.
|
|
168
210
|
- **With `--out`, it writes its run to the CI file**, and keeps the runs
|
|
169
211
|
`helix diagnostics` wrote there. The relay keeps the scan's run the same
|
|
170
212
|
way, so the two share one file, in either order.
|
|
171
213
|
|
|
172
214
|
It reads `helix-diagnostics.json` from the project, then from the directory it
|
|
173
215
|
runs in, or the file `--config <path>` names, and refuses a setting that names
|
|
174
|
-
nothing, as the page does. It exits 1 on a finding at
|
|
175
|
-
|
|
216
|
+
nothing, as the page does. `--json` prints the run. It exits 1 on a finding at
|
|
217
|
+
`error`, and 3 where it cannot run, such as where:
|
|
218
|
+
|
|
219
|
+
- no installed Helix package has lint data;
|
|
220
|
+
- the settings file does not parse, or holds a setting or a value the page
|
|
221
|
+
refuses;
|
|
222
|
+
- the project's folder, or the `--build` folder, does not exist.
|
|
176
223
|
|
|
177
224
|
## Run it on a page, from the command line
|
|
178
225
|
|
|
@@ -187,7 +234,9 @@ npx helix diagnostics --once http://localhost:5173/ --once http://localhost:5173
|
|
|
187
234
|
```
|
|
188
235
|
|
|
189
236
|
It needs `@fusion.dev/helix-cli` and Playwright installed beside this package.
|
|
190
|
-
It
|
|
237
|
+
It judges each page against the lint data `helix scan` reads, add-on packages'
|
|
238
|
+
included. It exits 1 on a finding at `error`, and 3 where a page could not be
|
|
239
|
+
opened, or where this package or Helix is not installed.
|
|
191
240
|
`--out` writes the runs to the CI file, and `--json` prints one document a
|
|
192
241
|
page. An agent gets the same runs from the MCP tool `helix_check_page`.
|
|
193
242
|
|
|
@@ -208,7 +257,8 @@ report(diagnose({ tiers: [{ lint: styled }, { lint: headless }] }))
|
|
|
208
257
|
every part name is unknown and every finding would be noise. It takes lint data
|
|
209
258
|
or a full `custom-elements.json`. Pass both tiers when both are installed, since
|
|
210
259
|
a part the headless tier publishes and the styled tier never forwarded is only
|
|
211
|
-
visible with both.
|
|
260
|
+
visible with both. Pass an add-on package's lint data beside them, such as
|
|
261
|
+
`@fusion.dev/helix-sandbox/lint.json`, for its elements. `report` is the console sink; the `Report` it returns is the
|
|
212
262
|
same object, so you can send it somewhere else as well.
|
|
213
263
|
|
|
214
264
|
None of these faults produces an error anywhere. They are detectable only against
|
|
@@ -252,7 +302,9 @@ A report is one run, in one shape for every sink, versioned by `schema`:
|
|
|
252
302
|
watcher it counts every element that rendered since `watch()` began,
|
|
253
303
|
including ones since removed, such as a closed dialog. Without one it counts
|
|
254
304
|
the elements on the page at the moment of the run. Elements named `helix-*`
|
|
255
|
-
that no manifest lists
|
|
305
|
+
that no manifest lists are counted apart as `unlisted`, and each is also an
|
|
306
|
+
`unknown-tag` finding. For a tag of your own, turn that kind off with
|
|
307
|
+
`rules: { 'unknown-tag': { severity: 'off', tags: ['helix-mine'] } }`.
|
|
256
308
|
- **`configuration`**: the settings the run was made under, each override in
|
|
257
309
|
effect, and how many findings each override turned off, by kind.
|
|
258
310
|
- **`withheld`**: the categories your configuration turned off for sending,
|
|
@@ -280,12 +332,14 @@ works on it:
|
|
|
280
332
|
|
|
281
333
|
| Kind | Severity | Means |
|
|
282
334
|
| --- | --- | --- |
|
|
283
|
-
| `dead-part` | error | A `::part()` names a part the element does not publish |
|
|
335
|
+
| `dead-part` | error | A `::part()` names a part the element does not publish, or follows another `::part()`, which is not valid CSS. The browser drops a rule of the second kind before the page reads it, so `helix scan` finds it |
|
|
284
336
|
| `dead-hook` | error | A hook is set where nothing answers it, or a name close to a hook is set that nothing reads |
|
|
285
337
|
| `unknown-attribute` | error | A Helix element carries an attribute it does not take |
|
|
286
338
|
| `unknown-slot` | error | A child names a slot its Helix element does not have, so it is not drawn. Found by `helix scan`, from the templates |
|
|
287
339
|
| `invalid-value` | error | An attribute with a closed set of values carries one outside it, so the element falls back to its default |
|
|
288
|
-
| `
|
|
340
|
+
| `false-boolean` | error | A boolean attribute is written `="false"`, which turns it on: the element reads it as on wherever it is present, as `disabled` is. Remove the attribute to turn it off. An attribute that `"false"` turns off, such as Drawer's `modal`, is not one. `helix scan` leaves a framework's binding, such as Lit's `?disabled=${false}` or Vue's `:disabled="false"`, to the page, which judges what it writes |
|
|
341
|
+
| `nested-interactive` | error | An interactive element is inside a button or a link, which HTML forbids: a Helix element inside a native `<button>` or an `<a>` with an `href`, or anything a person presses or types in inside a Helix element drawn inside one, such as `helix-icon-button` inside `helix-button` |
|
|
342
|
+
| `unforwarded-part` | warn | The element's headless tag, such as `helix-headless-button` for `helix-button`, publishes the part and the styled tag does not forward it, which is a gap in Helix |
|
|
289
343
|
| `unknown-tag` | error | A rule or an element names a tag no manifest lists |
|
|
290
344
|
| `unknown-event` | error | A React `on` prop on a Helix element names an event the element does not dispatch, so the handler never runs. Found by `helix scan`, from the templates |
|
|
291
345
|
| `unimported-tag` | warn | An element is written and nothing in the project imports the entry that registers it, so the browser draws an unknown element. Found by `helix scan`, from the imports |
|
|
@@ -308,6 +362,32 @@ for another route.
|
|
|
308
362
|
attributes, `aria-*`, `data-*`, and the prefixes frameworks write are never
|
|
309
363
|
findings. The markup Helix renders inside its own components is Helix's.
|
|
310
364
|
|
|
365
|
+
**Nesting is judged through elements whose place the markup settles.** The
|
|
366
|
+
Helix elements drawn inside a native button are Button, Icon Button, Copy
|
|
367
|
+
Button, Toggle, Motion Toggle, Radio, and Tab, and Skip Link is drawn inside a
|
|
368
|
+
link. A slot for actions, such as a card's `footer-actions`, holds a button
|
|
369
|
+
outside anything pressable, and is not a finding. A custom element of your own,
|
|
370
|
+
or a framework's component such as a router's `<Link>`, places its children
|
|
371
|
+
itself, so the check stops there: `helix scan` stops at the component, and the
|
|
372
|
+
page sees the `<a>` it renders.
|
|
373
|
+
|
|
374
|
+
## Scope
|
|
375
|
+
|
|
376
|
+
The scan and the page check what the lint data can settle. They do not check:
|
|
377
|
+
|
|
378
|
+
- the value a token or a hook is set to, such as `--helix-radii-md: red`.
|
|
379
|
+
`helix scan` checks each `--helix-` name you write;
|
|
380
|
+
- a selector that descends into a component, such as `helix-card .title` or
|
|
381
|
+
`helix-button span`. It matches only the elements you put inside the
|
|
382
|
+
component;
|
|
383
|
+
- a `::part()` on a selector with no Helix tag in it, such as
|
|
384
|
+
`.cta::part(label)`, since the class names no element whose parts can be
|
|
385
|
+
read;
|
|
386
|
+
- an attribute value an expression sets, such as `tone={tone}` in JSX or
|
|
387
|
+
`:tone` in Vue, in `helix scan`. The page judges the attribute it writes;
|
|
388
|
+
- the contrast of a color you write yourself. `helix theme audit` checks the
|
|
389
|
+
theme's tokens.
|
|
390
|
+
|
|
311
391
|
## Runtime events
|
|
312
392
|
|
|
313
393
|
Helix reports these itself as it runs, through one channel in
|
|
@@ -336,6 +416,7 @@ diagnostics counts it by kind and tag. None fails a CI job.
|
|
|
336
416
|
| `missing-trigger` | A tooltip's `for` names no element in its own tree |
|
|
337
417
|
| `negative-value` | An area chart was given a negative number, which it draws as none |
|
|
338
418
|
| `not-a-graph` | `connectGraph` was given an element that is not a graph frame |
|
|
419
|
+
| `not-a-number` | A numeric attribute holds something that is not a number, such as `page-size="1O"`, or an attribute in milliseconds something that is not a time, such as `delay="soon"`, so the property keeps its default. Development builds only (the `development` export condition) |
|
|
339
420
|
| `not-a-plot` | `connectSpatialScatterChart` or `connectSpatialSurfaceChart` was given an element that is not the chart it connects |
|
|
340
421
|
| `picture-in-picture-refused` | The browser refused picture in picture |
|
|
341
422
|
| `refused-length` | A grid's `min` is not a length it can write, so it lays out by `columns` |
|
|
@@ -349,6 +430,7 @@ diagnostics counts it by kind and tag. None fails a CI job.
|
|
|
349
430
|
| `typed-marks` | A quote's text starts or ends with a typed quotation mark, which the browser's marks double |
|
|
350
431
|
| `unknown-event` | A React `on` prop names an event that no element dispatches. Development builds only (the `development` export condition) |
|
|
351
432
|
| `unknown-icon` | An icon's `name` is not in the Helix set, so it draws nothing |
|
|
433
|
+
| `unknown-language` | A sandbox editor's `language` is not a language it reads, so its source is drawn plain |
|
|
352
434
|
| `unlinked-trigger` | A tooltip's trigger does not name it in `aria-describedby` |
|
|
353
435
|
| `unreachable-label` | A text is rendered as a label, a deprecated value that labels no control |
|
|
354
436
|
| `unregistered-part` | A part with an entry of its own, such as a filter menu, is in the page and its entry was never imported |
|
|
@@ -425,14 +507,16 @@ diagnose({
|
|
|
425
507
|
| `detail.evidence` | `full`, `selector`, or `none` | `full` |
|
|
426
508
|
| `detail.hookValues` | `true` or `false` | `true` |
|
|
427
509
|
| `detail.pages` | `patterns`, `full` (the path, never the query), or `none` | `patterns` |
|
|
428
|
-
| `environments.<name>` | `true` or `false
|
|
510
|
+
| `environments.<name>` | `true` or `false`, for sending runs from that environment. `production` takes `false` or `{ sample }` from 0 to 1, which is also the share of production page loads diagnostics runs on | development and CI on |
|
|
429
511
|
| `project` | `key`, `name`, and `tags` | none |
|
|
430
512
|
|
|
431
513
|
`send`, `detail`, and `environments` decide what the diagnostics service is
|
|
432
514
|
sent, and the service is not built yet. They are recorded in every report, so a
|
|
433
|
-
dashboard knows what a project withheld.
|
|
434
|
-
|
|
435
|
-
|
|
515
|
+
dashboard knows what a project withheld. `environments.production` also
|
|
516
|
+
decides whether diagnostics runs in production at all, and
|
|
517
|
+
`environments.development` and `environments.ci` change nothing on the page.
|
|
518
|
+
The relay runs on your own machine and receives each run whole. A setting or a
|
|
519
|
+
value that does not exist throws, and names the closest that does.
|
|
436
520
|
|
|
437
521
|
**What the page requests**: the lint data, from `cdn`, once per page load, and
|
|
438
522
|
the relay, where `sinks.relay` is on. Nothing else leaves the page.
|
package/dist/analyze.d.ts
CHANGED
|
@@ -87,9 +87,11 @@ export declare const skew: (tags: Map<string, Published>, from: Document, page?:
|
|
|
87
87
|
*/
|
|
88
88
|
export declare const versionSkew: (manifests: Record<string, string>, seen: string[]) => Raw[];
|
|
89
89
|
/**
|
|
90
|
-
* The attributes on the page's Helix elements that do nothing
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* The attributes on the page's Helix elements that do nothing, or the
|
|
91
|
+
* opposite of what they say: a name the element does not take, a value
|
|
92
|
+
* outside the ones a closed set allows, and `"false"` on an attribute that is
|
|
93
|
+
* on wherever it is present. And an interactive element inside a button or a
|
|
94
|
+
* link. Each is valid to the parser and silent.
|
|
93
95
|
*
|
|
94
96
|
* **Only what the page wrote.** An element Helix renders inside another is
|
|
95
97
|
* Helix's own markup. One finding per tag, attribute, and value, however many
|
package/dist/analyze.js
CHANGED
|
@@ -214,6 +214,18 @@ const unreflected = (selector, located, tags, from) => {
|
|
|
214
214
|
}
|
|
215
215
|
return found;
|
|
216
216
|
};
|
|
217
|
+
const headlessOf = (tag) => tag.startsWith('helix-') && !tag.startsWith('helix-headless-')
|
|
218
|
+
? `helix-headless-${tag.slice('helix-'.length)}`
|
|
219
|
+
: undefined;
|
|
220
|
+
const listed = (names) => {
|
|
221
|
+
const quoted = names.slice(0, 3).map((name) => `\`${name}\``);
|
|
222
|
+
if (names.length > 3) {
|
|
223
|
+
return `${quoted.join(', ')}, and ${names.length - 3} more`;
|
|
224
|
+
}
|
|
225
|
+
return quoted.length < 3
|
|
226
|
+
? quoted.join(' and ')
|
|
227
|
+
: `${quoted.slice(0, 2).join(', ')}, and ${quoted[2]}`;
|
|
228
|
+
};
|
|
217
229
|
const origin = (located) => ({
|
|
218
230
|
cssText: located.rule.cssText,
|
|
219
231
|
index: located.index,
|
|
@@ -276,12 +288,12 @@ export const analyze = (rules, tags, from, answered = new Map(), used = new Set(
|
|
|
276
288
|
if (publishers) {
|
|
277
289
|
continue;
|
|
278
290
|
}
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
291
|
+
const headless = headlessOf(tag);
|
|
292
|
+
if (headless !== undefined && tags.get(headless)?.parts.has(part)) {
|
|
281
293
|
findings.push({
|
|
282
294
|
expected: part,
|
|
283
295
|
kind: 'unforwarded-part',
|
|
284
|
-
message: `\`${tag}\` does not publish \`${part}\`, though \`${
|
|
296
|
+
message: `\`${tag}\` does not publish \`${part}\`, though \`${headless}\` does. The rule matches nothing until the part is forwarded, which is a gap in Helix rather than in this CSS.`,
|
|
285
297
|
origin: origin(located),
|
|
286
298
|
subject: `${tag}::part(${part})`,
|
|
287
299
|
tag,
|
|
@@ -349,7 +361,7 @@ export const analyze = (rules, tags, from, answered = new Map(), used = new Set(
|
|
|
349
361
|
kind: 'dead-hook',
|
|
350
362
|
message: suggestion
|
|
351
363
|
? `\`${tag}\` does not answer \`${property}\`. It answers \`${suggestion}\`, two edits or fewer away, which is almost certainly what was meant.`
|
|
352
|
-
: `\`${tag}\` does not answer \`${property}\`, and nothing under it that does was found in the page. ${[...answerers].sort().
|
|
364
|
+
: `\`${tag}\` does not answer \`${property}\`, and nothing under it that does was found in the page. ${listed([...answerers].sort())} ${answerers.size === 1 ? 'answers' : 'answer'} it, so this either targets the wrong element or expects a descendant that is not there.`,
|
|
353
365
|
origin: origin(located),
|
|
354
366
|
subject: `${selector} { ${property} }`,
|
|
355
367
|
tag,
|
|
@@ -482,23 +494,93 @@ const unknownTag = (tags, tag) => {
|
|
|
482
494
|
tag,
|
|
483
495
|
};
|
|
484
496
|
};
|
|
497
|
+
const NATIVE_INTERACTIVE = {
|
|
498
|
+
a: (element) => element.hasAttribute('href'),
|
|
499
|
+
audio: (element) => element.hasAttribute('controls'),
|
|
500
|
+
button: () => true,
|
|
501
|
+
details: () => true,
|
|
502
|
+
embed: () => true,
|
|
503
|
+
iframe: () => true,
|
|
504
|
+
img: (element) => element.hasAttribute('usemap'),
|
|
505
|
+
input: (element) => (element.getAttribute('type') ?? '').toLowerCase() !== 'hidden',
|
|
506
|
+
label: () => true,
|
|
507
|
+
select: () => true,
|
|
508
|
+
textarea: () => true,
|
|
509
|
+
video: (element) => element.hasAttribute('controls'),
|
|
510
|
+
};
|
|
511
|
+
const enclosing = (inner, tags) => {
|
|
512
|
+
for (let at = inner.parentElement; at; at = at.parentElement) {
|
|
513
|
+
const name = at.localName;
|
|
514
|
+
const published = tags.get(name);
|
|
515
|
+
if (published?.interactive === 'button' || published?.interactive === 'link') {
|
|
516
|
+
return { element: at, noun: published.interactive };
|
|
517
|
+
}
|
|
518
|
+
if (name === 'button') {
|
|
519
|
+
return { element: at, noun: 'button' };
|
|
520
|
+
}
|
|
521
|
+
if (name === 'a' && at.hasAttribute('href')) {
|
|
522
|
+
return { element: at, noun: 'link' };
|
|
523
|
+
}
|
|
524
|
+
if (name.includes('-') && !published) {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return undefined;
|
|
529
|
+
};
|
|
530
|
+
const nested = (element, tags) => {
|
|
531
|
+
const inner = element.localName;
|
|
532
|
+
const kind = tags.get(inner)?.interactive;
|
|
533
|
+
if (kind === undefined && !NATIVE_INTERACTIVE[inner]?.(element)) {
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
const found = enclosing(element, tags);
|
|
537
|
+
if (!found) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
const outer = found.element.localName;
|
|
541
|
+
const helix = tags.has(outer);
|
|
542
|
+
if (!(helix || kind)) {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
const named = (tag, article) => tags.has(tag) ? `\`${tag}\`` : `${article} \`<${tag}>\``;
|
|
546
|
+
const some = (tag) => named(tag, /^[aeiou]/.test(tag) ? 'an' : 'a');
|
|
547
|
+
const opening = some(inner);
|
|
548
|
+
const hint = outer === 'a' && kind === 'button'
|
|
549
|
+
? ' To draw the link as a button, write `data-button` on the `<a>`.'
|
|
550
|
+
: '';
|
|
551
|
+
return {
|
|
552
|
+
kind: 'nested-interactive',
|
|
553
|
+
message: `${opening.charAt(0).toUpperCase()}${opening.slice(1)} is inside ${some(outer)}${helix ? `, which draws its content inside a native ${found.noun}` : ''}. HTML allows no interactive element inside a button or a link, and browsers and screen readers differ on what a press or a focus there does. Move ${named(inner, 'the')} out of ${named(outer, 'the')}.${hint}`,
|
|
554
|
+
subject: `<${outer}> <${inner}>`,
|
|
555
|
+
tag: helix ? outer : inner,
|
|
556
|
+
};
|
|
557
|
+
};
|
|
485
558
|
export const markup = (tags, from, page = pageOf(from)) => {
|
|
486
559
|
const found = new Map();
|
|
560
|
+
const keep = (raw) => {
|
|
561
|
+
const key = `${raw.kind} ${raw.subject}`;
|
|
562
|
+
const seen = found.get(key);
|
|
563
|
+
if (seen) {
|
|
564
|
+
seen.count += 1;
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
found.set(key, { ...raw, count: 1 });
|
|
568
|
+
}
|
|
569
|
+
};
|
|
487
570
|
for (const element of page.elements) {
|
|
488
571
|
const host = element.getRootNode();
|
|
489
572
|
const composed = host instanceof ShadowRoot && host.host.localName.startsWith('helix-');
|
|
490
573
|
const published = composed ? undefined : tags.get(element.localName);
|
|
574
|
+
if (!composed) {
|
|
575
|
+
const raw = nested(element, tags);
|
|
576
|
+
if (raw) {
|
|
577
|
+
keep(raw);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
491
580
|
if (!(composed || published) && tags.size > 0) {
|
|
492
581
|
const raw = unknownTag(tags, element.localName);
|
|
493
582
|
if (raw) {
|
|
494
|
-
|
|
495
|
-
const seen = found.get(key);
|
|
496
|
-
if (seen) {
|
|
497
|
-
seen.count += 1;
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
found.set(key, { ...raw, count: 1 });
|
|
501
|
-
}
|
|
583
|
+
keep(raw);
|
|
502
584
|
}
|
|
503
585
|
}
|
|
504
586
|
if (published) {
|
|
@@ -523,6 +605,14 @@ export const markup = (tags, from, page = pageOf(from)) => {
|
|
|
523
605
|
tag,
|
|
524
606
|
};
|
|
525
607
|
}
|
|
608
|
+
else if (published.booleans?.has(name) && value.toLowerCase() === 'false') {
|
|
609
|
+
raw = {
|
|
610
|
+
kind: 'false-boolean',
|
|
611
|
+
message: `\`${name}="${value}"\` turns \`${name}\` on. \`${tag}\` reads the attribute as on wherever it is present, whatever its value. Remove the attribute to turn it off. In a framework, bind it, such as \`?${name}=\${false}\` in Lit or \`:${name}="false"\` in Vue.`,
|
|
612
|
+
subject: `<${tag} ${name}="${value}">`,
|
|
613
|
+
tag,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
526
616
|
else if (allows && value !== '' && !allows.has(value)) {
|
|
527
617
|
const suggestion = nearest(value, allows);
|
|
528
618
|
raw = {
|
|
@@ -534,14 +624,7 @@ export const markup = (tags, from, page = pageOf(from)) => {
|
|
|
534
624
|
};
|
|
535
625
|
}
|
|
536
626
|
if (raw) {
|
|
537
|
-
|
|
538
|
-
const seen = found.get(key);
|
|
539
|
-
if (seen) {
|
|
540
|
-
seen.count += 1;
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
found.set(key, { ...raw, count: 1 });
|
|
544
|
-
}
|
|
627
|
+
keep(raw);
|
|
545
628
|
}
|
|
546
629
|
}
|
|
547
630
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -75,6 +75,12 @@ export declare const DEFAULTS: {
|
|
|
75
75
|
export declare const LOCAL: RegExp;
|
|
76
76
|
/** Throws on a setting that names nothing, or a value it cannot take. */
|
|
77
77
|
export declare const checkConfig: (config: Config | undefined, layer: Exclude<Layer, 'shipped'>) => void;
|
|
78
|
+
/**
|
|
79
|
+
* Throws on an `environment` that names none, and names the closest. Taken as
|
|
80
|
+
* declared, `staging`, which a bundler's mode hands over as readily as
|
|
81
|
+
* `development`, would run on a deployed host with the console and the relay.
|
|
82
|
+
*/
|
|
83
|
+
export declare const checkEnvironment: (environment: unknown) => void;
|
|
78
84
|
/** The settings in effect, from the defaults, the file, and the call. */
|
|
79
85
|
export interface Resolved {
|
|
80
86
|
configuration: Omit<Configuration, 'overrides' | 'silenced'>;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { nearest } from './manifest.js';
|
|
2
|
-
import { CATEGORIES } from './report.js';
|
|
2
|
+
import { CATEGORIES, ENVIRONMENTS, } from './report.js';
|
|
3
3
|
import { checkRules } from './rules.js';
|
|
4
4
|
export const DEFAULTS = {
|
|
5
5
|
console: {
|
|
@@ -109,6 +109,14 @@ export const checkConfig = (config, layer) => {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
};
|
|
112
|
+
export const checkEnvironment = (environment) => {
|
|
113
|
+
if (environment === undefined || ENVIRONMENTS.includes(environment)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const close = typeof environment === 'string' ? nearest(environment, ENVIRONMENTS) : undefined;
|
|
117
|
+
throw new Error(`The argument sets \`environment\` to \`${JSON.stringify(environment)}\`. It takes ${ENVIRONMENTS.map(shown).join(', ')}.` +
|
|
118
|
+
(close ? ` The closest is \`${close}\`.` : ''));
|
|
119
|
+
};
|
|
112
120
|
export const resolveConfig = (file, call) => {
|
|
113
121
|
checkConfig(file, 'file');
|
|
114
122
|
checkConfig(call, 'argument');
|
package/dist/diagnose.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { analyze, markup, registrations, skew, versionSkew } from './analyze.js';
|
|
2
2
|
import { collect } from './collect.js';
|
|
3
|
-
import { pagePattern, resolveConfig } from './config.js';
|
|
3
|
+
import { checkEnvironment, pagePattern, resolveConfig } from './config.js';
|
|
4
4
|
import { identify } from './identity.js';
|
|
5
5
|
import { index, lintFor, versionsOf } from './manifest.js';
|
|
6
6
|
import { pageOf } from './page.js';
|
|
@@ -17,6 +17,7 @@ export const diagnose = (options) => {
|
|
|
17
17
|
if (tiers.length === 0) {
|
|
18
18
|
throw new Error('diagnose() needs at least one manifest. Without one every part name is unknown and every finding would be noise.');
|
|
19
19
|
}
|
|
20
|
+
checkEnvironment(environment);
|
|
20
21
|
const resolved = resolveConfig(file, call);
|
|
21
22
|
const { detail } = resolved.configuration;
|
|
22
23
|
const document_ = from ?? globalThis.document;
|
package/dist/identity.js
CHANGED
|
@@ -3,9 +3,11 @@ const CATEGORY = {
|
|
|
3
3
|
'dead-part': 'styling',
|
|
4
4
|
'disabled-attribute': 'styling',
|
|
5
5
|
'duplicate-registration': 'integrity',
|
|
6
|
+
'false-boolean': 'markup',
|
|
6
7
|
'invalid-value': 'markup',
|
|
7
8
|
'manifest-skew': 'integrity',
|
|
8
9
|
'native-element': 'guideline',
|
|
10
|
+
'nested-interactive': 'markup',
|
|
9
11
|
'primitive-token': 'styling',
|
|
10
12
|
'unforwarded-part': 'styling',
|
|
11
13
|
'unimported-tag': 'integrity',
|
|
@@ -21,9 +23,11 @@ const OWNER = {
|
|
|
21
23
|
'dead-part': 'consumer',
|
|
22
24
|
'disabled-attribute': 'consumer',
|
|
23
25
|
'duplicate-registration': 'consumer',
|
|
26
|
+
'false-boolean': 'consumer',
|
|
24
27
|
'invalid-value': 'consumer',
|
|
25
28
|
'manifest-skew': 'consumer',
|
|
26
29
|
'native-element': 'consumer',
|
|
30
|
+
'nested-interactive': 'consumer',
|
|
27
31
|
'primitive-token': 'consumer',
|
|
28
32
|
'unforwarded-part': 'helix',
|
|
29
33
|
'unimported-tag': 'consumer',
|
package/dist/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkConfig, LOCAL, resolveConfig } from './config.js';
|
|
1
|
+
import { checkConfig, checkEnvironment, LOCAL, resolveConfig } from './config.js';
|
|
2
2
|
import { report as print } from './console.js';
|
|
3
3
|
import { diagnose } from './diagnose.js';
|
|
4
4
|
import { overlayOn } from './overlay.js';
|
|
@@ -56,6 +56,7 @@ const start = (options, page) => {
|
|
|
56
56
|
const { cdn = CDN, duplicates, environment, file, onReport, page: route, tiers, ...call } = options;
|
|
57
57
|
let resolved;
|
|
58
58
|
try {
|
|
59
|
+
checkEnvironment(environment);
|
|
59
60
|
resolved = resolveConfig(file, call);
|
|
60
61
|
}
|
|
61
62
|
catch (error) {
|
package/dist/manifest.d.ts
CHANGED
|
@@ -35,8 +35,21 @@ export interface ManifestSource {
|
|
|
35
35
|
text?: string;
|
|
36
36
|
};
|
|
37
37
|
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Helix's own fields: `interactive`, the native control the element
|
|
40
|
+
* draws its content inside, from `@interactive`, and `family`, the family
|
|
41
|
+
* an element joins from another package, from `@family`.
|
|
42
|
+
*/
|
|
43
|
+
helix?: {
|
|
44
|
+
family?: string;
|
|
45
|
+
interactive?: string;
|
|
46
|
+
};
|
|
38
47
|
members?: Array<{
|
|
39
48
|
attribute?: string;
|
|
49
|
+
/** How the attribute turns a boolean property on, where it is one. */
|
|
50
|
+
helix?: {
|
|
51
|
+
boolean?: string;
|
|
52
|
+
};
|
|
40
53
|
kind?: string;
|
|
41
54
|
name: string;
|
|
42
55
|
readonly?: boolean;
|
|
@@ -78,9 +91,26 @@ export interface LintData {
|
|
|
78
91
|
export interface LintTag {
|
|
79
92
|
/** Each attribute, with the values it allows, or `null` where it takes any. */
|
|
80
93
|
attributes: Record<string, string[] | null>;
|
|
81
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* The attributes that are on wherever they are present, `"false"`
|
|
96
|
+
* included: each turns on a boolean property Lit reads with
|
|
97
|
+
* `type: Boolean`. Absent from lint data written before this field, which a
|
|
98
|
+
* reader takes as unknown.
|
|
99
|
+
*/
|
|
100
|
+
booleans?: string[];
|
|
101
|
+
/** The family, from `@family` or else from the module's path. */
|
|
82
102
|
family?: string | undefined;
|
|
83
103
|
hooks: string[];
|
|
104
|
+
/**
|
|
105
|
+
* What a person presses or types in, where the element is one: `button` or
|
|
106
|
+
* `link` for an element that draws its content inside its own native button
|
|
107
|
+
* or link, and `control` for any other form control. Absent where the
|
|
108
|
+
* element is neither. **Typed as any `string`**: a page passes the
|
|
109
|
+
* `lint.json` it imports, and TypeScript reads every string in an imported
|
|
110
|
+
* JSON file as `string`, so the {@link Interactive} union here refused every
|
|
111
|
+
* page's `{ lint }`. `index` keeps only the kinds it knows.
|
|
112
|
+
*/
|
|
113
|
+
interactive?: string;
|
|
84
114
|
parts: string[];
|
|
85
115
|
/**
|
|
86
116
|
* The slots the element has, by name, the default one as `""`. Absent from
|
|
@@ -96,6 +126,12 @@ export interface LintTag {
|
|
|
96
126
|
/** The attributes the element writes on itself: one named for a read-only member. */
|
|
97
127
|
written: string[];
|
|
98
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* What a person presses or types in: an element drawn inside a native
|
|
131
|
+
* `button` or `link`, which HTML allows no interactive element inside, or
|
|
132
|
+
* another form `control`.
|
|
133
|
+
*/
|
|
134
|
+
export type Interactive = 'button' | 'control' | 'link';
|
|
99
135
|
/**
|
|
100
136
|
* One installed tier: its manifest, with the package name, or its lint data,
|
|
101
137
|
* which names the package itself.
|
|
@@ -123,9 +159,17 @@ export interface Published {
|
|
|
123
159
|
* is a union of string literals, and `null` where it takes any value.
|
|
124
160
|
*/
|
|
125
161
|
attributes: Map<string, Set<string> | null>;
|
|
162
|
+
/**
|
|
163
|
+
* The attributes that are on wherever they are present, `"false"`
|
|
164
|
+
* included. Absent where a tier's lint data predates the field, which says
|
|
165
|
+
* nothing either way.
|
|
166
|
+
*/
|
|
167
|
+
booleans?: Set<string> | undefined;
|
|
126
168
|
/** The family, from the module's path: `button` for `src/button/root/button-root.ts`. */
|
|
127
169
|
family?: string | undefined;
|
|
128
170
|
hooks: Map<string, Set<string>>;
|
|
171
|
+
/** What a person presses or types in, where the element is one. */
|
|
172
|
+
interactive?: Interactive | undefined;
|
|
129
173
|
/**
|
|
130
174
|
* The attributes a property writes back when it is set. Absent where a tier's
|
|
131
175
|
* lint data predates the field, which says nothing either way.
|
package/dist/manifest.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
const isInteractive = (kind) => kind === 'button' || kind === 'control' || kind === 'link';
|
|
1
2
|
export const lintOf = (manifest, name) => {
|
|
2
3
|
const tags = {};
|
|
3
4
|
for (const module of manifest.modules ?? []) {
|
|
4
|
-
const
|
|
5
|
+
const folder = /^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,8 @@ export const lintOf = (manifest, name) => {
|
|
|
9
10
|
}
|
|
10
11
|
const entry = tags[tag] ?? {
|
|
11
12
|
attributes: {},
|
|
12
|
-
|
|
13
|
+
booleans: [],
|
|
14
|
+
family: declaration.helix?.family ?? folder,
|
|
13
15
|
hooks: [],
|
|
14
16
|
parts: [],
|
|
15
17
|
reflected: [],
|
|
@@ -17,10 +19,20 @@ export const lintOf = (manifest, name) => {
|
|
|
17
19
|
written: [],
|
|
18
20
|
};
|
|
19
21
|
tags[tag] = entry;
|
|
22
|
+
const drawn = declaration.helix?.interactive;
|
|
23
|
+
if (drawn === 'button' || drawn === 'link') {
|
|
24
|
+
entry.interactive = drawn;
|
|
25
|
+
}
|
|
26
|
+
else if ((declaration.members ?? []).some((member) => member.name === 'form' && member.kind === 'field' && !member.static)) {
|
|
27
|
+
entry.interactive = 'control';
|
|
28
|
+
}
|
|
20
29
|
for (const member of declaration.members ?? []) {
|
|
21
30
|
if (member.reflects && member.attribute) {
|
|
22
31
|
entry.reflected?.push(member.attribute);
|
|
23
32
|
}
|
|
33
|
+
if (member.attribute && member.helix?.boolean === 'present') {
|
|
34
|
+
entry.booleans?.push(member.attribute);
|
|
35
|
+
}
|
|
24
36
|
if (member.readonly && !member.static && member.kind === 'field') {
|
|
25
37
|
entry.written.push(member.name, member.name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`));
|
|
26
38
|
}
|
|
@@ -40,6 +52,7 @@ export const lintOf = (manifest, name) => {
|
|
|
40
52
|
entry.hooks = [...new Set(entry.hooks)];
|
|
41
53
|
entry.slots = [...new Set(entry.slots)];
|
|
42
54
|
entry.reflected = [...new Set(entry.reflected)];
|
|
55
|
+
entry.booleans = [...new Set(entry.booleans)];
|
|
43
56
|
}
|
|
44
57
|
return {
|
|
45
58
|
package: manifest.helix?.package ?? name,
|
|
@@ -67,6 +80,15 @@ export const index = (tiers) => {
|
|
|
67
80
|
for (const name of entry.written) {
|
|
68
81
|
element.written.add(name);
|
|
69
82
|
}
|
|
83
|
+
if (isInteractive(entry.interactive)) {
|
|
84
|
+
element.interactive ??= entry.interactive;
|
|
85
|
+
}
|
|
86
|
+
if (entry.booleans) {
|
|
87
|
+
element.booleans ??= new Set();
|
|
88
|
+
for (const name of entry.booleans) {
|
|
89
|
+
element.booleans.add(name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
70
92
|
if (entry.reflected) {
|
|
71
93
|
element.reflected ??= new Set();
|
|
72
94
|
for (const name of entry.reflected) {
|
package/dist/report.d.ts
CHANGED
|
@@ -50,8 +50,9 @@ export interface Origin {
|
|
|
50
50
|
export interface Finding {
|
|
51
51
|
/**
|
|
52
52
|
* What the fault is about: `styling` for parts and hooks, `markup` for
|
|
53
|
-
* attributes and
|
|
54
|
-
* disagreeing, `guideline` for advice. A dashboard groups
|
|
53
|
+
* attributes, slots, and what an element holds, `integrity` for the page
|
|
54
|
+
* and the library disagreeing, `guideline` for advice. A dashboard groups
|
|
55
|
+
* by it.
|
|
55
56
|
*/
|
|
56
57
|
category: FindingCategory;
|
|
57
58
|
/**
|
|
@@ -97,7 +98,7 @@ export interface Finding {
|
|
|
97
98
|
tag: string;
|
|
98
99
|
}
|
|
99
100
|
/** Every fault class, stable across versions. */
|
|
100
|
-
export declare const KINDS: readonly ['dead-hook', 'dead-part', 'disabled-attribute', 'duplicate-registration', 'invalid-value', 'manifest-skew', 'native-element', 'primitive-token', 'unforwarded-part', 'unimported-tag', 'unknown-attribute', 'unknown-event', 'unknown-slot', 'unknown-tag', 'unknown-token', 'unreflected-attribute'];
|
|
101
|
+
export declare const KINDS: readonly ['dead-hook', 'dead-part', 'disabled-attribute', 'duplicate-registration', 'false-boolean', 'invalid-value', 'manifest-skew', 'native-element', 'nested-interactive', 'primitive-token', 'unforwarded-part', 'unimported-tag', 'unknown-attribute', 'unknown-event', 'unknown-slot', 'unknown-tag', 'unknown-token', 'unreflected-attribute'];
|
|
101
102
|
/** A fault class. */
|
|
102
103
|
export type Kind = (typeof KINDS)[number];
|
|
103
104
|
/** How serious a finding is. */
|
|
@@ -199,8 +200,10 @@ export interface RuntimeEntry {
|
|
|
199
200
|
/** The tag of the element it happened on, where there is one. */
|
|
200
201
|
tag?: string;
|
|
201
202
|
}
|
|
203
|
+
/** Where a run can be declared to happen. */
|
|
204
|
+
export declare const ENVIRONMENTS: readonly ['ci', 'development', 'production'];
|
|
202
205
|
/** Where a run was declared to happen. Undeclared is never sent. */
|
|
203
|
-
export type Environment =
|
|
206
|
+
export type Environment = (typeof ENVIRONMENTS)[number];
|
|
204
207
|
/**
|
|
205
208
|
* A sheet the collector could not read, and the reason. For a scan, a file
|
|
206
209
|
* that did not parse is `threw`, and a stylesheet in a language the scan does
|
package/dist/report.js
CHANGED
|
@@ -3,9 +3,11 @@ export const KINDS = [
|
|
|
3
3
|
'dead-part',
|
|
4
4
|
'disabled-attribute',
|
|
5
5
|
'duplicate-registration',
|
|
6
|
+
'false-boolean',
|
|
6
7
|
'invalid-value',
|
|
7
8
|
'manifest-skew',
|
|
8
9
|
'native-element',
|
|
10
|
+
'nested-interactive',
|
|
9
11
|
'primitive-token',
|
|
10
12
|
'unforwarded-part',
|
|
11
13
|
'unimported-tag',
|
|
@@ -25,3 +27,4 @@ export const CATEGORIES = [
|
|
|
25
27
|
'runtime',
|
|
26
28
|
'usage',
|
|
27
29
|
];
|
|
30
|
+
export const ENVIRONMENTS = ['ci', 'development', 'production'];
|
package/dist/rules.js
CHANGED
|
@@ -5,9 +5,11 @@ export const SHIPPED_RULES = {
|
|
|
5
5
|
'dead-part': 'error',
|
|
6
6
|
'disabled-attribute': 'error',
|
|
7
7
|
'duplicate-registration': 'error',
|
|
8
|
+
'false-boolean': 'error',
|
|
8
9
|
'invalid-value': 'error',
|
|
9
10
|
'manifest-skew': 'error',
|
|
10
11
|
'native-element': 'warn',
|
|
12
|
+
'nested-interactive': 'error',
|
|
11
13
|
'primitive-token': 'error',
|
|
12
14
|
'unforwarded-part': 'warn',
|
|
13
15
|
'unimported-tag': 'warn',
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.207.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.207.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"./ci": {
|
|
21
21
|
"types": "./dist/ci.d.ts",
|
|
22
22
|
"default": "./dist/ci.js"
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"dist"
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@fusion.dev/helix-testing": "0.
|
|
37
|
+
"@fusion.dev/helix-testing": "0.207.0",
|
|
37
38
|
"@types/bun": "^1.4.2"
|
|
38
39
|
},
|
|
39
40
|
"homepage": "https://fusion.dev",
|