@fusion.dev/helix-diagnostics 0.186.1 → 0.188.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 +24 -1
- package/dist/analyze.d.ts +2 -2
- package/dist/analyze.js +94 -1
- package/dist/collect.d.ts +2 -2
- package/dist/identity.js +12 -0
- package/dist/manifest.d.ts +13 -0
- package/dist/manifest.js +11 -0
- package/dist/report.d.ts +2 -2
- package/dist/report.js +6 -0
- package/dist/rules.js +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -174,6 +174,23 @@ runs in, or the file `--config <path>` names, and refuses a setting that names
|
|
|
174
174
|
nothing, as the page does. It exits 1 on a finding at `error`, and 3 where the
|
|
175
175
|
project installs no Helix lint data. `--json` prints the run.
|
|
176
176
|
|
|
177
|
+
## Run it on a page, from the command line
|
|
178
|
+
|
|
179
|
+
`helix diagnostics --once <url>` opens the page headless, runs this package's
|
|
180
|
+
diagnostics in it once the page holds still, prints each finding and what the
|
|
181
|
+
library logged as it ran, and exits. Nothing is added to your app: the
|
|
182
|
+
command serves the installed package on the page's own origin. Repeat
|
|
183
|
+
`--once` for each page.
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
npx helix diagnostics --once http://localhost:5173/ --once http://localhost:5173/settings
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
It needs `@fusion.dev/helix-cli` and Playwright installed beside this package.
|
|
190
|
+
It exits 1 on a finding at `error`, and 3 where a page could not be opened.
|
|
191
|
+
`--out` writes the runs to the CI file, and `--json` prints one document a
|
|
192
|
+
page. An agent gets the same runs from the MCP tool `helix_check_page`.
|
|
193
|
+
|
|
177
194
|
## Run it once, directly
|
|
178
195
|
|
|
179
196
|
`diagnose()` is one run, which `initDiagnostics()` makes for you. Call it
|
|
@@ -269,7 +286,13 @@ works on it:
|
|
|
269
286
|
| `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 |
|
|
270
287
|
| `invalid-value` | error | An attribute with a closed set of values carries one outside it, so the element falls back to its default |
|
|
271
288
|
| `unforwarded-part` | warn | The headless tier publishes the part and the styled tier does not forward it, which is a gap in Helix |
|
|
272
|
-
| `unknown-tag` | error | A rule names a tag no manifest lists |
|
|
289
|
+
| `unknown-tag` | error | A rule or an element names a tag no manifest lists |
|
|
290
|
+
| `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
|
+
| `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 |
|
|
292
|
+
| `disabled-attribute` | error | A rule styles a form control's `[disabled]`, which a control disabled by a `<fieldset disabled>` does not carry. Write `:disabled`. Found by `helix scan` |
|
|
293
|
+
| `primitive-token` | error | A rule reads a `--_helix-` primitive, which is internal to `@fusion.dev/helix-styles`. Found by `helix scan` |
|
|
294
|
+
| `unknown-token` | error | A rule reads or sets a `--helix-` name the token set does not have, so it falls back silently. Found by `helix scan` |
|
|
295
|
+
| `unreflected-attribute` | warn | A rule selects a Helix element by an attribute it takes and never writes back, so it matches only markup a server wrote and nothing after a client navigation |
|
|
273
296
|
| `duplicate-registration` | error | Two copies of Helix are on the page |
|
|
274
297
|
| `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 |
|
|
275
298
|
|
package/dist/analyze.d.ts
CHANGED
|
@@ -54,8 +54,8 @@ export declare const analyze: (rules: Located[], tags: Map<string, Published>, f
|
|
|
54
54
|
* it happens, with both class objects in hand, which is a position nothing
|
|
55
55
|
* later can improve on: by the time a page can be scanned, the losing class is
|
|
56
56
|
* unreachable and all that is left is an identity mismatch to infer from. So
|
|
57
|
-
* this reads `duplicateRegistrations()`
|
|
58
|
-
*
|
|
57
|
+
* this reads `duplicateRegistrations()` and does not re-derive it, so the fact
|
|
58
|
+
* comes from where the fault starts.
|
|
59
59
|
*
|
|
60
60
|
* The identity check stays as a fallback, for the copy that never went through
|
|
61
61
|
* Helix's own decorator: a fork, a hand-written `customElements.define`, or a
|
package/dist/analyze.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { nearest } from './manifest.js';
|
|
2
|
-
import { pageOf } from './page.js';
|
|
2
|
+
import { OVERLAY, pageOf } from './page.js';
|
|
3
3
|
export const targets = (selector) => {
|
|
4
4
|
const found = new Set();
|
|
5
5
|
for (const match of selector.matchAll(/(?<![\w.#-])helix-[a-z0-9]+(?:-[a-z0-9]+)*/g)) {
|
|
@@ -153,6 +153,67 @@ const loose = (located, answerers, from, used, onPage, listedBy, readers, sugges
|
|
|
153
153
|
}
|
|
154
154
|
return findings;
|
|
155
155
|
};
|
|
156
|
+
const ALWAYS_WRITTEN = new Set([
|
|
157
|
+
'class',
|
|
158
|
+
'dir',
|
|
159
|
+
'hidden',
|
|
160
|
+
'id',
|
|
161
|
+
'lang',
|
|
162
|
+
'part',
|
|
163
|
+
'role',
|
|
164
|
+
'slot',
|
|
165
|
+
'style',
|
|
166
|
+
'tabindex',
|
|
167
|
+
'title',
|
|
168
|
+
]);
|
|
169
|
+
const SELECTS = /([^\s>+~,()]*)\[\s*([a-zA-Z][\w-]*)\s*[~^|$*]?=?[^\]]*\]/g;
|
|
170
|
+
const unreflected = (selector, located, tags, from) => {
|
|
171
|
+
if (!from || !selector.includes('[')) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const found = [];
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
for (const [, compound = '', attribute = ''] of selector.matchAll(SELECTS)) {
|
|
177
|
+
if (ALWAYS_WRITTEN.has(attribute) ||
|
|
178
|
+
attribute.startsWith('aria-') ||
|
|
179
|
+
attribute.startsWith('data-') ||
|
|
180
|
+
seen.has(attribute)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const base = compound.replace(/\[[^\]]*\]/g, '').replace(/::?[\w-]+(\([^)]*\))?/g, '');
|
|
184
|
+
if (!base) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
let reached;
|
|
188
|
+
try {
|
|
189
|
+
reached = [...from.querySelectorAll(base)];
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const tag = reached
|
|
195
|
+
.map((element) => element.localName)
|
|
196
|
+
.find((name) => {
|
|
197
|
+
const published = tags.get(name);
|
|
198
|
+
return (published?.reflected !== undefined &&
|
|
199
|
+
published.attributes.has(attribute) &&
|
|
200
|
+
!published.reflected.has(attribute) &&
|
|
201
|
+
!published.written.has(attribute));
|
|
202
|
+
});
|
|
203
|
+
if (!tag) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
seen.add(attribute);
|
|
207
|
+
found.push({
|
|
208
|
+
kind: 'unreflected-attribute',
|
|
209
|
+
message: `\`${selector}\` selects \`${tag}\` by \`${attribute}\`, which the element takes and never writes back to itself, so the rule matches only markup a server wrote: after a client navigation the property is set and the attribute is absent. Select a class or an attribute the element reflects, or set \`${attribute}\` as an attribute.`,
|
|
210
|
+
origin: origin(located),
|
|
211
|
+
subject: `${selector} [${attribute}]`,
|
|
212
|
+
tag,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return found;
|
|
216
|
+
};
|
|
156
217
|
const origin = (located) => ({
|
|
157
218
|
cssText: located.rule.cssText,
|
|
158
219
|
index: located.index,
|
|
@@ -197,6 +258,7 @@ export const analyze = (rules, tags, from, answered = new Map(), used = new Set(
|
|
|
197
258
|
if (!selector) {
|
|
198
259
|
continue;
|
|
199
260
|
}
|
|
261
|
+
findings.push(...unreflected(selector, located, tags, from));
|
|
200
262
|
for (const { names, tag } of selector.includes('helix-') ? parts(selector) : []) {
|
|
201
263
|
const element = tags.get(tag);
|
|
202
264
|
if (!element) {
|
|
@@ -402,12 +464,43 @@ const GLOBAL = new Set([
|
|
|
402
464
|
]);
|
|
403
465
|
const PREFIXES = ['aria-', 'data-', 'on', '_ngcontent-', '_nghost-', 'ng-', 'v-', 'x-', ':', '@'];
|
|
404
466
|
const HOOK_DISTANCE = 2;
|
|
467
|
+
const unknownTag = (tags, tag) => {
|
|
468
|
+
if (!tag.startsWith('helix-') || tag === OVERLAY) {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
const bare = nearest(tag.slice('helix-'.length), [...tags.keys()]
|
|
472
|
+
.filter((name) => name.startsWith('helix-'))
|
|
473
|
+
.map((name) => name.slice('helix-'.length)));
|
|
474
|
+
const suggestion = bare === undefined ? undefined : `helix-${bare}`;
|
|
475
|
+
return {
|
|
476
|
+
expected: suggestion,
|
|
477
|
+
kind: 'unknown-tag',
|
|
478
|
+
message: suggestion
|
|
479
|
+
? `\`${tag}\` is in no manifest passed to the collector, so the browser draws it as an unknown element. The closest tag is \`${suggestion}\`.`
|
|
480
|
+
: `\`${tag}\` is in no manifest passed to the collector, so the browser draws it as an unknown element. Check the spelling, or load the package that publishes the tag.`,
|
|
481
|
+
subject: `<${tag}>`,
|
|
482
|
+
tag,
|
|
483
|
+
};
|
|
484
|
+
};
|
|
405
485
|
export const markup = (tags, from, page = pageOf(from)) => {
|
|
406
486
|
const found = new Map();
|
|
407
487
|
for (const element of page.elements) {
|
|
408
488
|
const host = element.getRootNode();
|
|
409
489
|
const composed = host instanceof ShadowRoot && host.host.localName.startsWith('helix-');
|
|
410
490
|
const published = composed ? undefined : tags.get(element.localName);
|
|
491
|
+
if (!(composed || published) && tags.size > 0) {
|
|
492
|
+
const raw = unknownTag(tags, element.localName);
|
|
493
|
+
if (raw) {
|
|
494
|
+
const key = `${raw.kind} ${raw.subject}`;
|
|
495
|
+
const seen = found.get(key);
|
|
496
|
+
if (seen) {
|
|
497
|
+
seen.count += 1;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
found.set(key, { ...raw, count: 1 });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
411
504
|
if (published) {
|
|
412
505
|
for (const { name, value } of Array.from(element.attributes)) {
|
|
413
506
|
if (GLOBAL.has(name) ||
|
package/dist/collect.d.ts
CHANGED
|
@@ -36,8 +36,8 @@ export interface Located {
|
|
|
36
36
|
* because its class sets `styles = [HelixIconButton.styles, styles].flat()`:
|
|
37
37
|
* it borrows another component's stylesheet outright. That is not inheritance,
|
|
38
38
|
* not a mixin and not a part, so no manifest field describes it and `cem` records
|
|
39
|
-
* nothing. Two correct declarations in
|
|
40
|
-
*
|
|
39
|
+
* nothing. Two correct declarations in a site's stylesheet were called dead on
|
|
40
|
+
* the strength of that silence.
|
|
41
41
|
*
|
|
42
42
|
* A `var(--x)` inside the element's own shadow styles is direct evidence that
|
|
43
43
|
* the element reads the property, which is a stronger claim than the manifest
|
package/dist/identity.js
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
const CATEGORY = {
|
|
2
2
|
'dead-hook': 'styling',
|
|
3
3
|
'dead-part': 'styling',
|
|
4
|
+
'disabled-attribute': 'styling',
|
|
4
5
|
'duplicate-registration': 'integrity',
|
|
5
6
|
'invalid-value': 'markup',
|
|
6
7
|
'manifest-skew': 'integrity',
|
|
8
|
+
'primitive-token': 'styling',
|
|
7
9
|
'unforwarded-part': 'styling',
|
|
10
|
+
'unimported-tag': 'integrity',
|
|
8
11
|
'unknown-attribute': 'markup',
|
|
12
|
+
'unknown-event': 'markup',
|
|
9
13
|
'unknown-slot': 'markup',
|
|
10
14
|
'unknown-tag': 'integrity',
|
|
15
|
+
'unknown-token': 'styling',
|
|
16
|
+
'unreflected-attribute': 'styling',
|
|
11
17
|
};
|
|
12
18
|
const OWNER = {
|
|
13
19
|
'dead-hook': 'consumer',
|
|
14
20
|
'dead-part': 'consumer',
|
|
21
|
+
'disabled-attribute': 'consumer',
|
|
15
22
|
'duplicate-registration': 'consumer',
|
|
16
23
|
'invalid-value': 'consumer',
|
|
17
24
|
'manifest-skew': 'consumer',
|
|
25
|
+
'primitive-token': 'consumer',
|
|
18
26
|
'unforwarded-part': 'helix',
|
|
27
|
+
'unimported-tag': 'consumer',
|
|
19
28
|
'unknown-attribute': 'consumer',
|
|
29
|
+
'unknown-event': 'consumer',
|
|
20
30
|
'unknown-slot': 'consumer',
|
|
21
31
|
'unknown-tag': 'consumer',
|
|
32
|
+
'unknown-token': 'consumer',
|
|
33
|
+
'unreflected-attribute': 'consumer',
|
|
22
34
|
};
|
|
23
35
|
export const normalize = (subject) => subject
|
|
24
36
|
.replace(/___[A-Za-z0-9_-]{5}\b/g, '')
|
package/dist/manifest.d.ts
CHANGED
|
@@ -36,9 +36,11 @@ export interface ManifestSource {
|
|
|
36
36
|
};
|
|
37
37
|
}>;
|
|
38
38
|
members?: Array<{
|
|
39
|
+
attribute?: string;
|
|
39
40
|
kind?: string;
|
|
40
41
|
name: string;
|
|
41
42
|
readonly?: boolean;
|
|
43
|
+
reflects?: boolean;
|
|
42
44
|
static?: boolean;
|
|
43
45
|
}>;
|
|
44
46
|
cssParts?: Array<{
|
|
@@ -85,6 +87,12 @@ export interface LintTag {
|
|
|
85
87
|
* lint data written before 0.165, which a reader takes as unknown.
|
|
86
88
|
*/
|
|
87
89
|
slots?: string[];
|
|
90
|
+
/**
|
|
91
|
+
* The attributes a property writes back to the element when it is set, so a
|
|
92
|
+
* selector on one matches however the property was set. Absent from lint data
|
|
93
|
+
* written before this field, which a reader takes as unknown.
|
|
94
|
+
*/
|
|
95
|
+
reflected?: string[];
|
|
88
96
|
/** The attributes the element writes on itself: one named for a read-only member. */
|
|
89
97
|
written: string[];
|
|
90
98
|
}
|
|
@@ -118,6 +126,11 @@ export interface Published {
|
|
|
118
126
|
/** The family, from the module's path: `button` for `src/button/root/button-root.ts`. */
|
|
119
127
|
family?: string | undefined;
|
|
120
128
|
hooks: Map<string, Set<string>>;
|
|
129
|
+
/**
|
|
130
|
+
* The attributes a property writes back when it is set. Absent where a tier's
|
|
131
|
+
* lint data predates the field, which says nothing either way.
|
|
132
|
+
*/
|
|
133
|
+
reflected?: Set<string> | undefined;
|
|
121
134
|
/**
|
|
122
135
|
* The attributes the element writes on itself: one named for a read-only
|
|
123
136
|
* member, as `helix-notifications` writes `unread`. Output, so never a
|
package/dist/manifest.js
CHANGED
|
@@ -12,11 +12,15 @@ export const lintOf = (manifest, name) => {
|
|
|
12
12
|
family,
|
|
13
13
|
hooks: [],
|
|
14
14
|
parts: [],
|
|
15
|
+
reflected: [],
|
|
15
16
|
slots: [],
|
|
16
17
|
written: [],
|
|
17
18
|
};
|
|
18
19
|
tags[tag] = entry;
|
|
19
20
|
for (const member of declaration.members ?? []) {
|
|
21
|
+
if (member.reflects && member.attribute) {
|
|
22
|
+
entry.reflected?.push(member.attribute);
|
|
23
|
+
}
|
|
20
24
|
if (member.readonly && !member.static && member.kind === 'field') {
|
|
21
25
|
entry.written.push(member.name, member.name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`));
|
|
22
26
|
}
|
|
@@ -35,6 +39,7 @@ export const lintOf = (manifest, name) => {
|
|
|
35
39
|
entry.parts = [...new Set(entry.parts)];
|
|
36
40
|
entry.hooks = [...new Set(entry.hooks)];
|
|
37
41
|
entry.slots = [...new Set(entry.slots)];
|
|
42
|
+
entry.reflected = [...new Set(entry.reflected)];
|
|
38
43
|
}
|
|
39
44
|
return {
|
|
40
45
|
package: manifest.helix?.package ?? name,
|
|
@@ -62,6 +67,12 @@ export const index = (tiers) => {
|
|
|
62
67
|
for (const name of entry.written) {
|
|
63
68
|
element.written.add(name);
|
|
64
69
|
}
|
|
70
|
+
if (entry.reflected) {
|
|
71
|
+
element.reflected ??= new Set();
|
|
72
|
+
for (const name of entry.reflected) {
|
|
73
|
+
element.reflected.add(name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
65
76
|
for (const [name, values] of Object.entries(entry.attributes)) {
|
|
66
77
|
element.attributes.set(name, values ? new Set(values) : null);
|
|
67
78
|
}
|
package/dist/report.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* **A bare pass is a lie this collector is structurally capable of telling.**
|
|
6
6
|
* Cross-origin stylesheets throw on `.cssRules`, and a run that could not read
|
|
7
7
|
* half its input reports the same empty array as a run that read all of it.
|
|
8
|
-
* `fusion prose`'s `dialect` shipped in exactly that condition for weeks: green, and
|
|
8
|
+
* `fusion x prose`'s `dialect` shipped in exactly that condition for weeks: green, and
|
|
9
9
|
* incapable of being anything else. So `Report` has no `findings` that can be
|
|
10
10
|
* destructured away from the `coverage` that qualifies them, and the console
|
|
11
11
|
* sink prints coverage first whether or not anything was found.
|
|
@@ -97,7 +97,7 @@ export interface Finding {
|
|
|
97
97
|
tag: string;
|
|
98
98
|
}
|
|
99
99
|
/** Every fault class, stable across versions. */
|
|
100
|
-
export declare const KINDS: readonly ['dead-hook', 'dead-part', 'duplicate-registration', 'invalid-value', 'manifest-skew', 'unforwarded-part', 'unknown-attribute', 'unknown-slot', 'unknown-tag'];
|
|
100
|
+
export declare const KINDS: readonly ['dead-hook', 'dead-part', 'disabled-attribute', 'duplicate-registration', 'invalid-value', 'manifest-skew', 'primitive-token', 'unforwarded-part', 'unimported-tag', 'unknown-attribute', 'unknown-event', 'unknown-slot', 'unknown-tag', 'unknown-token', 'unreflected-attribute'];
|
|
101
101
|
/** A fault class. */
|
|
102
102
|
export type Kind = (typeof KINDS)[number];
|
|
103
103
|
/** How serious a finding is. */
|
package/dist/report.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
export const KINDS = [
|
|
2
2
|
'dead-hook',
|
|
3
3
|
'dead-part',
|
|
4
|
+
'disabled-attribute',
|
|
4
5
|
'duplicate-registration',
|
|
5
6
|
'invalid-value',
|
|
6
7
|
'manifest-skew',
|
|
8
|
+
'primitive-token',
|
|
7
9
|
'unforwarded-part',
|
|
10
|
+
'unimported-tag',
|
|
8
11
|
'unknown-attribute',
|
|
12
|
+
'unknown-event',
|
|
9
13
|
'unknown-slot',
|
|
10
14
|
'unknown-tag',
|
|
15
|
+
'unknown-token',
|
|
16
|
+
'unreflected-attribute',
|
|
11
17
|
];
|
|
12
18
|
export const CATEGORIES = [
|
|
13
19
|
'assist',
|
package/dist/rules.js
CHANGED
|
@@ -3,13 +3,19 @@ import { KINDS, } from './report.js';
|
|
|
3
3
|
export const SHIPPED_RULES = {
|
|
4
4
|
'dead-hook': 'error',
|
|
5
5
|
'dead-part': 'error',
|
|
6
|
+
'disabled-attribute': 'error',
|
|
6
7
|
'duplicate-registration': 'error',
|
|
7
8
|
'invalid-value': 'error',
|
|
8
9
|
'manifest-skew': 'error',
|
|
10
|
+
'primitive-token': 'error',
|
|
9
11
|
'unforwarded-part': 'warn',
|
|
12
|
+
'unimported-tag': 'warn',
|
|
10
13
|
'unknown-attribute': 'error',
|
|
14
|
+
'unknown-event': 'error',
|
|
11
15
|
'unknown-slot': 'error',
|
|
12
16
|
'unknown-tag': 'error',
|
|
17
|
+
'unknown-token': 'error',
|
|
18
|
+
'unreflected-attribute': 'warn',
|
|
13
19
|
};
|
|
14
20
|
const SEVERITIES = new Set(['error', 'info', 'off', 'warn']);
|
|
15
21
|
export const checkRules = (rules, layer) => {
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.188.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.188.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@fusion.dev/helix-testing": "0.
|
|
36
|
+
"@fusion.dev/helix-testing": "0.188.0",
|
|
37
37
|
"@types/bun": "^1.4.2"
|
|
38
38
|
},
|
|
39
39
|
"homepage": "https://fusion.dev",
|