@fusion.dev/helix-diagnostics 0.136.0 → 0.138.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.
@@ -16,8 +16,31 @@
16
16
  */
17
17
  /** The subset of the manifest schema this reads. Nothing else is touched. */
18
18
  export interface ManifestSource {
19
+ /**
20
+ * The package and version the manifest describes, which `helix manifest
21
+ * build` writes from the package's `package.json`. Absent from a manifest
22
+ * built before 0.137.
23
+ */
24
+ helix?: {
25
+ package?: string;
26
+ version?: string;
27
+ };
19
28
  modules?: Array<{
29
+ /** Where the module is, which names the family: `src/button/root/button-root.ts`. */
30
+ path?: string;
20
31
  declarations?: Array<{
32
+ attributes?: Array<{
33
+ name: string;
34
+ type?: {
35
+ text?: string;
36
+ };
37
+ }>;
38
+ members?: Array<{
39
+ kind?: string;
40
+ name: string;
41
+ readonly?: boolean;
42
+ static?: boolean;
43
+ }>;
21
44
  cssParts?: Array<{
22
45
  name: string;
23
46
  }>;
@@ -41,7 +64,20 @@ export interface Tier {
41
64
  * scope, and `analyze.ts` needs both in the same file.
42
65
  */
43
66
  export interface Published {
67
+ /**
68
+ * Each attribute the tag takes, with the values it allows where its type
69
+ * is a union of string literals, and `null` where it takes any value.
70
+ */
71
+ attributes: Map<string, Set<string> | null>;
72
+ /** The family, from the module's path: `button` for `src/button/root/button-root.ts`. */
73
+ family?: string | undefined;
44
74
  hooks: Map<string, Set<string>>;
75
+ /**
76
+ * The attributes the element writes on itself: one named for a read-only
77
+ * member, as `helix-notifications` writes `unread`. Output, so never a
78
+ * finding when the page shows it.
79
+ */
80
+ written: Set<string>;
45
81
  parts: Map<string, Set<string>>;
46
82
  }
47
83
  /**
@@ -52,6 +88,8 @@ export interface Published {
52
88
  * styled one did not forward it", and that sentence needs the names.
53
89
  */
54
90
  export declare const index: (tiers: Tier[]) => Map<string, Published>;
91
+ /** The version each manifest describes, by package, where it says. */
92
+ export declare const versionsOf: (tiers: Tier[]) => Record<string, string>;
55
93
  /**
56
94
  * The published name closest to a wrong one, or nothing when none is close.
57
95
  *
@@ -65,4 +103,4 @@ export declare const index: (tiers: Tier[]) => Map<string, Published>;
65
103
  * `--button-background-color` however close they sort, and a confident wrong
66
104
  * suggestion costs more than none.
67
105
  */
68
- export declare const nearest: (wrong: string, candidates: Iterable<string>) => string | undefined;
106
+ export declare const nearest: (wrong: string, candidates: Iterable<string>, ceiling?: number) => string | undefined;
package/dist/manifest.js CHANGED
@@ -2,6 +2,7 @@ export const index = (tiers) => {
2
2
  const tags = new Map();
3
3
  for (const tier of tiers) {
4
4
  for (const module of tier.manifest.modules ?? []) {
5
+ const family = /^src\/([^/]+)\//.exec(module.path ?? '')?.[1];
5
6
  for (const declaration of module.declarations ?? []) {
6
7
  const tag = declaration.tagName;
7
8
  if (!tag) {
@@ -9,9 +10,24 @@ export const index = (tiers) => {
9
10
  }
10
11
  let element = tags.get(tag);
11
12
  if (!element) {
12
- element = { hooks: new Map(), parts: new Map() };
13
+ element = {
14
+ attributes: new Map(),
15
+ family,
16
+ hooks: new Map(),
17
+ parts: new Map(),
18
+ written: new Set(),
19
+ };
13
20
  tags.set(tag, element);
14
21
  }
22
+ for (const member of declaration.members ?? []) {
23
+ if (member.readonly && !member.static && member.kind === 'field') {
24
+ element.written.add(member.name);
25
+ element.written.add(member.name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`));
26
+ }
27
+ }
28
+ for (const attribute of declaration.attributes ?? []) {
29
+ element.attributes.set(attribute.name, allowed(attribute.type?.text));
30
+ }
15
31
  for (const [field, into] of [
16
32
  [declaration.cssParts, element.parts],
17
33
  [declaration.cssProperties, element.hooks],
@@ -27,9 +43,30 @@ export const index = (tiers) => {
27
43
  }
28
44
  return tags;
29
45
  };
30
- export const nearest = (wrong, candidates) => {
46
+ const allowed = (type) => {
47
+ if (!type) {
48
+ return null;
49
+ }
50
+ const literals = [...type.matchAll(/'([^']*)'/g)].map((match) => match[1]);
51
+ const rest = type
52
+ .replace(/'[^']*'/g, '')
53
+ .replace(/\b(undefined|null)\b/g, '')
54
+ .replace(/[|\s]/g, '');
55
+ return literals.length > 0 && rest === '' ? new Set(literals) : null;
56
+ };
57
+ export const versionsOf = (tiers) => {
58
+ const versions = {};
59
+ for (const tier of tiers) {
60
+ const version = tier.manifest.helix?.version;
61
+ if (version) {
62
+ versions[tier.manifest.helix?.package ?? tier.name] = version;
63
+ }
64
+ }
65
+ return versions;
66
+ };
67
+ export const nearest = (wrong, candidates, ceiling = Math.floor(wrong.length / 3)) => {
31
68
  let best;
32
- let distance = Math.floor(wrong.length / 3);
69
+ let distance = Math.min(ceiling, Math.floor(wrong.length / 3));
33
70
  for (const candidate of candidates) {
34
71
  const found = within(wrong, candidate, distance);
35
72
  if (found !== undefined && found < distance + 1) {
package/dist/report.d.ts CHANGED
@@ -38,6 +38,12 @@ export interface Origin {
38
38
  * rather than a finding.
39
39
  */
40
40
  export interface Finding {
41
+ /**
42
+ * What the fault is about: `styling` for parts and hooks, `markup` for
43
+ * attributes and slots, `integrity` for the page and the library
44
+ * disagreeing, `guideline` for advice. A dashboard groups by it.
45
+ */
46
+ category: FindingCategory;
41
47
  /**
42
48
  * What the collector believes the author meant, when it can tell.
43
49
  *
@@ -48,12 +54,25 @@ export interface Finding {
48
54
  * catch.
49
55
  */
50
56
  expected?: string | undefined;
57
+ /** The family the tag belongs to, from the manifest's module path. */
58
+ family?: string | undefined;
59
+ /**
60
+ * The finding's identity across runs: the same fault in the next run has
61
+ * the same fingerprint, which is what makes fixed and regressed computable.
62
+ * See `fingerprintOf`.
63
+ */
64
+ fingerprint: string;
51
65
  /** Machine-readable fault class, stable across versions. */
52
66
  kind: Kind;
53
67
  /** One sentence, addressed to whoever wrote the CSS. */
54
68
  message: string;
55
69
  /** Where it was read. Absent only for faults that are not in a stylesheet. */
56
70
  origin?: Origin;
71
+ /**
72
+ * Who fixes it: `consumer`, the page's authors, or `helix`, the library,
73
+ * as for a part the styled tier never forwarded.
74
+ */
75
+ owner: Owner;
57
76
  /** How serious it is, after the shipped rules and any overrides. */
58
77
  severity: Severity;
59
78
  /**
@@ -68,15 +87,30 @@ export interface Finding {
68
87
  tag: string;
69
88
  }
70
89
  /** Every fault class, stable across versions. */
71
- export declare const KINDS: readonly ['dead-hook', 'dead-part', 'duplicate-registration', 'manifest-skew', 'unforwarded-part', 'unknown-tag'];
90
+ export declare const KINDS: readonly ['dead-hook', 'dead-part', 'duplicate-registration', 'invalid-value', 'manifest-skew', 'unforwarded-part', 'unknown-attribute', 'unknown-tag'];
72
91
  /** A fault class. */
73
92
  export type Kind = (typeof KINDS)[number];
74
93
  /** How serious a finding is. */
75
94
  export type Severity = 'error' | 'info' | 'warn';
76
95
  /** Where a finding's severity was decided. */
77
96
  export type Layer = 'argument' | 'file' | 'shipped';
78
- /** A finding before the rules have given it a severity. */
79
- export type Raw = Omit<Finding, 'decidedBy' | 'severity'>;
97
+ /** What a fault is about. */
98
+ export type FindingCategory = 'guideline' | 'integrity' | 'markup' | 'styling';
99
+ /** Who fixes a fault. */
100
+ export type Owner = 'consumer' | 'helix';
101
+ /** A finding as a check first writes it. */
102
+ export type Raw = Omit<Finding, 'category' | 'decidedBy' | 'family' | 'fingerprint' | 'owner' | 'severity'>;
103
+ /** A finding with its identity, before the rules have given it a severity. */
104
+ export type Identified = Omit<Finding, 'decidedBy' | 'severity'>;
105
+ /**
106
+ * The kinds of telemetry a run can hold, each its own shape. A project turns
107
+ * each on or off for sending; see `Config.send`.
108
+ */
109
+ export declare const CATEGORIES: readonly ['assist', 'configuration', 'coverage', 'findings', 'performance', 'runtime', 'usage'];
110
+ /** A kind of telemetry. */
111
+ export type Category = (typeof CATEGORIES)[number];
112
+ /** Where a run was declared to happen. Undeclared is never sent. */
113
+ export type Environment = 'ci' | 'development' | 'production';
80
114
  /** A sheet the collector could not read, and the reason. */
81
115
  export interface Unreadable {
82
116
  reason: 'cross-origin' | 'detached' | 'threw';
@@ -128,6 +162,13 @@ export interface Usage {
128
162
  counted: 'walked' | 'watched';
129
163
  /** Each hook the page's stylesheets set, by tag, counted per declaration. */
130
164
  hooks: Record<string, Record<string, number>>;
165
+ /**
166
+ * The values each hook is set to, by tag, hook, and value, where the
167
+ * project allows it (`detail.hookValues`). A hook set to one value across
168
+ * projects is a token Helix should have shipped. Values over 200
169
+ * characters are cut there.
170
+ */
171
+ hookValues: Record<string, Record<string, Record<string, number>>>;
131
172
  /** Each part the page's stylesheets style, by tag, counted per rule. */
132
173
  parts: Record<string, Record<string, number>>;
133
174
  /** Each named slot filled, by tag. */
@@ -141,18 +182,99 @@ export interface Usage {
141
182
  */
142
183
  unlisted: Record<string, number>;
143
184
  }
144
- /** The whole result. Coverage and findings, never one without the other. */
185
+ /** The settings a run was made under, and what they turned off. */
186
+ export interface Configuration {
187
+ /** The lowest level each category prints at in the console, or `off`. */
188
+ console: {
189
+ coverage: 'info' | 'off';
190
+ findings: Severity | 'off';
191
+ usage: 'debug' | 'off';
192
+ };
193
+ /** How much detail goes with what is sent. */
194
+ detail: {
195
+ evidence: 'full' | 'none' | 'selector';
196
+ hookValues: boolean;
197
+ pages: 'full' | 'none' | 'patterns';
198
+ };
199
+ /** Where sending happens, and production's sample rate. */
200
+ environments: {
201
+ ci: boolean;
202
+ development: boolean;
203
+ production: false | {
204
+ sample: number;
205
+ };
206
+ };
207
+ /** Each override in effect, from the file or the call. */
208
+ overrides: Array<{
209
+ kind: Kind;
210
+ layer: Exclude<Layer, 'shipped'>;
211
+ severity: Severity | 'off';
212
+ tags?: string[];
213
+ }>;
214
+ /**
215
+ * How many findings each override turned off, by kind and layer. A finding
216
+ * turned off is still a fault, and a report that hid the count would read
217
+ * as cleaner than the page is.
218
+ */
219
+ silenced: Array<{
220
+ count: number;
221
+ kind: Kind;
222
+ layer: Exclude<Layer, 'shipped'>;
223
+ }>;
224
+ }
225
+ /**
226
+ * One run: what was found, what is used, how much could be seen, and the
227
+ * settings it was made under, with everything a dashboard needs to place it.
228
+ *
229
+ * **The same shape from every source and to every sink**, versioned by
230
+ * `schema`, so a run written by CI today is read by the dashboard next year.
231
+ */
145
232
  export interface Report {
233
+ /** The shape's version. Raised when a field changes meaning or goes. */
234
+ schema: 1;
235
+ configuration: Configuration;
146
236
  coverage: Coverage;
147
237
  findings: Finding[];
148
238
  /**
149
- * How many findings each override layer turned off. A finding turned off
150
- * is still a fault, and a report that hid the count would read as cleaner
151
- * than the page is.
239
+ * The project's own name and labels, as its configuration gives them. The
240
+ * key is never written into a report.
152
241
  */
153
- silenced: {
154
- argument: number;
155
- file: number;
242
+ project: {
243
+ name?: string;
244
+ tags: string[];
245
+ };
246
+ run: {
247
+ /** When the run began, as an ISO 8601 time. */
248
+ at: string;
249
+ /** How long the run took, in milliseconds. */
250
+ duration: number;
251
+ /** Where it was declared to run. Absent where nobody declared it. */
252
+ environment?: Environment;
253
+ /** A new identity per run, so a run sent twice is stored once. */
254
+ id: string;
255
+ /** The page, as `detail.pages` allows: a pattern, a path, or absent. */
256
+ page?: string;
257
+ /** Where the run was made: in the page, or from the code. */
258
+ source: 'code' | 'page';
156
259
  };
157
260
  usage: Usage;
261
+ versions: {
262
+ /** This package's version, which collected the run. */
263
+ collector: string;
264
+ /** The Helix versions loaded on the page, one per copy of core: two means two copies. */
265
+ helix: string[];
266
+ /** The version each manifest passed in describes, by package. */
267
+ manifests: Record<string, string>;
268
+ /** The shipped rules' version, which is the collector's. */
269
+ rules: string;
270
+ };
271
+ /**
272
+ * The categories the project turned off for sending, and which place
273
+ * turned each off, so an empty category reads as withheld and never as
274
+ * nothing there.
275
+ */
276
+ withheld: Array<{
277
+ category: Category;
278
+ decidedBy: Layer;
279
+ }>;
158
280
  }
package/dist/report.js CHANGED
@@ -2,7 +2,18 @@ export const KINDS = [
2
2
  'dead-hook',
3
3
  'dead-part',
4
4
  'duplicate-registration',
5
+ 'invalid-value',
5
6
  'manifest-skew',
6
7
  'unforwarded-part',
8
+ 'unknown-attribute',
7
9
  'unknown-tag',
8
10
  ];
11
+ export const CATEGORIES = [
12
+ 'assist',
13
+ 'configuration',
14
+ 'coverage',
15
+ 'findings',
16
+ 'performance',
17
+ 'runtime',
18
+ 'usage',
19
+ ];
package/dist/rules.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Finding, type Kind, type Layer, type Raw, type Severity } from './report.js';
1
+ import { type Configuration, type Finding, type Identified, type Kind, type Layer, type Severity } from './report.js';
2
2
  /** One override: a severity, `off`, or either for some tags only. */
3
3
  export type Override = Severity | 'off' | {
4
4
  /** The severity, or `off`. */
@@ -20,15 +20,15 @@ export declare const SHIPPED_RULES: Readonly<Record<Kind, Severity>>;
20
20
  /** Throws on an override that would silently do nothing. */
21
21
  export declare const checkRules: (rules: Rules | undefined, layer: Exclude<Layer, 'shipped'>) => void;
22
22
  /**
23
- * The findings with their severities, and how many each layer turned off.
23
+ * The findings with their severities, and how many each override turned off,
24
+ * by kind and layer.
24
25
  *
25
26
  * The shipped rule first, then the file, then the argument, each later layer
26
27
  * replacing what it speaks to.
27
28
  */
28
- export declare const applyRules: (findings: Raw[], file: Rules | undefined, argument: Rules | undefined) => {
29
+ export declare const applyRules: (findings: Identified[], file: Rules | undefined, argument: Rules | undefined) => {
29
30
  findings: Finding[];
30
- silenced: {
31
- argument: number;
32
- file: number;
33
- };
31
+ silenced: Configuration['silenced'];
34
32
  };
33
+ /** Every override in effect, flattened, for the report's configuration. */
34
+ export declare const overridesOf: (file: Rules | undefined, argument: Rules | undefined) => Configuration['overrides'];
package/dist/rules.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { nearest } from './manifest.js';
2
- import { KINDS } from './report.js';
2
+ import { KINDS, } from './report.js';
3
3
  export const SHIPPED_RULES = {
4
4
  'dead-hook': 'error',
5
5
  'dead-part': 'error',
6
6
  'duplicate-registration': 'error',
7
+ 'invalid-value': 'error',
7
8
  'manifest-skew': 'error',
8
9
  'unforwarded-part': 'warn',
10
+ 'unknown-attribute': 'error',
9
11
  'unknown-tag': 'error',
10
12
  };
11
13
  const SEVERITIES = new Set(['error', 'info', 'off', 'warn']);
@@ -41,7 +43,7 @@ const said = (rules, finding) => {
41
43
  return typeof plain === 'string' ? plain : undefined;
42
44
  };
43
45
  export const applyRules = (findings, file, argument) => {
44
- const silenced = { argument: 0, file: 0 };
46
+ const off = new Map();
45
47
  const kept = [];
46
48
  for (const finding of findings) {
47
49
  let severity = SHIPPED_RULES[finding.kind];
@@ -57,10 +59,20 @@ export const applyRules = (findings, file, argument) => {
57
59
  }
58
60
  }
59
61
  if (severity === 'off') {
60
- silenced[decidedBy] += 1;
62
+ const layer = decidedBy;
63
+ const key = `${finding.kind} ${layer}`;
64
+ const entry = off.get(key) ?? { count: 0, kind: finding.kind, layer };
65
+ entry.count += 1;
66
+ off.set(key, entry);
61
67
  continue;
62
68
  }
63
69
  kept.push({ ...finding, decidedBy, severity });
64
70
  }
65
- return { findings: kept, silenced };
71
+ return { findings: kept, silenced: [...off.values()] };
66
72
  };
73
+ export const overridesOf = (file, argument) => [
74
+ ['file', file],
75
+ ['argument', argument],
76
+ ].flatMap(([layer, rules]) => Object.entries(rules ?? {}).flatMap(([kind, value]) => (Array.isArray(value) ? value : [value]).map((one) => typeof one === 'string'
77
+ ? { kind: kind, layer, severity: one }
78
+ : { kind: kind, layer, severity: one.severity, tags: one.tags })));
package/dist/usage.d.ts CHANGED
@@ -3,6 +3,12 @@ import type { Published } from './manifest.js';
3
3
  import type { Usage } from './report.js';
4
4
  /** The element counts, before the stylesheets add theirs. */
5
5
  type Counted = Pick<Usage, 'axes' | 'composed' | 'slots' | 'tags'>;
6
+ /**
7
+ * The Helix versions loaded on the page: every copy of core adds its own to
8
+ * a list under a registered symbol as it loads. Empty for a core older than
9
+ * 0.137, which lists nothing.
10
+ */
11
+ export declare const loadedVersions: () => string[];
6
12
  /** The elements in the page now, counted once each. */
7
13
  export declare const walk: (from: Document) => Counted;
8
14
  /**
@@ -15,7 +21,7 @@ export declare const listed: (counted: Counted, tags: Map<string, Published>) =>
15
21
  /** A running count of what renders, from `watch()`. */
16
22
  export interface Watcher {
17
23
  /** The counts so far. A copy: counting goes on after it is read. */
18
- counts(): Pick<Usage, 'axes' | 'composed' | 'slots' | 'tags'>;
24
+ counts(): Counted;
19
25
  /** Stops counting, and gives the slot back to whatever held it before. */
20
26
  stop(): void;
21
27
  }
@@ -40,5 +46,5 @@ export declare const watch: (from?: Document) => Watcher;
40
46
  * the element's own styles say they exist. A dead one is a finding and is not
41
47
  * counted as use.
42
48
  */
43
- export declare const styled: (rules: Located[], tags: Map<string, Published>, answered: Answered) => Pick<Usage, 'hooks' | 'parts'>;
49
+ export declare const styled: (rules: Located[], tags: Map<string, Published>, answered: Answered, values?: boolean) => Pick<Usage, 'hookValues' | 'hooks' | 'parts'>;
44
50
  export {};
package/dist/usage.js CHANGED
@@ -13,6 +13,10 @@ const under = (into, key, make) => {
13
13
  return found;
14
14
  };
15
15
  const empty = () => ({ axes: {}, composed: {}, slots: {}, tags: {} });
16
+ export const loadedVersions = () => {
17
+ const listed = globalThis[Symbol.for('fusion.dev/helix/versions')];
18
+ return Array.isArray(listed) ? listed.filter((one) => typeof one === 'string').sort() : [];
19
+ };
16
20
  const tally = (counted, element) => {
17
21
  const tag = element.localName;
18
22
  if (!tag.startsWith('helix-')) {
@@ -104,26 +108,55 @@ export const watch = (from = document) => {
104
108
  },
105
109
  };
106
110
  };
107
- export const styled = (rules, tags, answered) => {
108
- const counted = { hooks: {}, parts: {} };
111
+ export const styled = (rules, tags, answered, values = true) => {
112
+ const counted = {
113
+ hooks: {},
114
+ hookValues: {},
115
+ parts: {},
116
+ };
117
+ const listing = new Map();
118
+ for (const [tag, element] of tags) {
119
+ for (const property of element.hooks.keys()) {
120
+ listing.set(property, [...(listing.get(property) ?? []), tag]);
121
+ }
122
+ }
123
+ const count = (located, tag, property) => {
124
+ bump(under(counted.hooks, tag, () => ({})), property);
125
+ if (!values) {
126
+ return;
127
+ }
128
+ const value = (located.rule.style?.getPropertyValue?.(property) ?? '').trim().slice(0, 200);
129
+ if (value !== '') {
130
+ bump(under(under(counted.hookValues, tag, () => ({})), property, () => ({})), value);
131
+ }
132
+ };
109
133
  for (const located of rules) {
110
134
  const selector = located.rule.selectorText;
111
- if (!selector?.includes('helix-')) {
135
+ if (!selector) {
112
136
  continue;
113
137
  }
114
- for (const { names, tag } of parts(selector)) {
138
+ for (const { names, tag } of selector.includes('helix-') ? parts(selector) : []) {
115
139
  for (const name of names) {
116
140
  if (tags.get(tag)?.parts.has(name)) {
117
141
  bump(under(counted.parts, tag, () => ({})), name);
118
142
  }
119
143
  }
120
144
  }
121
- for (const tag of targets(selector)) {
122
- for (const property of Array.from(located.rule.style ?? [])) {
123
- if (property.startsWith('--') &&
124
- (tags.get(tag)?.hooks.has(property) || answered.get(tag)?.has(property))) {
125
- bump(under(counted.hooks, tag, () => ({})), property);
145
+ const named = targets(selector).filter((tag) => tags.has(tag));
146
+ for (const property of Array.from(located.rule.style ?? [])) {
147
+ if (!property.startsWith('--')) {
148
+ continue;
149
+ }
150
+ if (named.length > 0) {
151
+ for (const tag of named) {
152
+ if (tags.get(tag)?.hooks.has(property) || answered.get(tag)?.has(property)) {
153
+ count(located, tag, property);
154
+ }
126
155
  }
156
+ continue;
157
+ }
158
+ for (const tag of listing.get(property) ?? []) {
159
+ count(located, tag, property);
127
160
  }
128
161
  }
129
162
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The version of the package this file ships in.
3
+ *
4
+ * **Written by `fusion/scripts/version.mjs`** wherever it writes the package's
5
+ * `package.json`, and stamped back into the repository beside it after a
6
+ * release, so the two never disagree; a spec beside this holds them to it.
7
+ * Nothing else in the running code knows which release it is.
8
+ */
9
+ export declare const VERSION = "0.138.0";
@@ -0,0 +1 @@
1
+ export const VERSION = '0.138.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.136.0",
4
+ "version": "0.138.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -16,6 +16,10 @@
16
16
  ".": {
17
17
  "types": "./dist/index.d.ts",
18
18
  "default": "./dist/index.js"
19
+ },
20
+ "./ci": {
21
+ "types": "./dist/ci.d.ts",
22
+ "default": "./dist/ci.js"
19
23
  }
20
24
  },
21
25
  "files": [
@@ -29,7 +33,7 @@
29
33
  },
30
34
  "bugs": "https://github.com/project-fusion/fusion/issues",
31
35
  "devDependencies": {
32
- "@fusion.dev/helix-testing": "0.136.0",
36
+ "@fusion.dev/helix-testing": "0.138.0",
33
37
  "@types/bun": "^1.3.0"
34
38
  },
35
39
  "homepage": "https://fusion.dev",