@fusion.dev/helix-diagnostics 0.135.0 → 0.137.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/dist/rules.js ADDED
@@ -0,0 +1,76 @@
1
+ import { nearest } from './manifest.js';
2
+ import { KINDS, } from './report.js';
3
+ export const SHIPPED_RULES = {
4
+ 'dead-hook': 'error',
5
+ 'dead-part': 'error',
6
+ 'duplicate-registration': 'error',
7
+ 'manifest-skew': 'error',
8
+ 'unforwarded-part': 'warn',
9
+ 'unknown-tag': 'error',
10
+ };
11
+ const SEVERITIES = new Set(['error', 'info', 'off', 'warn']);
12
+ export const checkRules = (rules, layer) => {
13
+ for (const [kind, value] of Object.entries(rules ?? {})) {
14
+ if (!KINDS.includes(kind)) {
15
+ const close = nearest(kind, KINDS);
16
+ throw new Error(`The ${layer} overrides \`${kind}\`, which is no kind of finding.` +
17
+ (close ? ` The closest is \`${close}\`.` : ` The kinds are ${KINDS.join(', ')}.`));
18
+ }
19
+ for (const one of Array.isArray(value) ? value : [value]) {
20
+ const severity = typeof one === 'string' ? one : one?.severity;
21
+ if (!SEVERITIES.has(severity)) {
22
+ throw new Error(`The ${layer} sets \`${kind}\` to \`${String(severity)}\`. A severity is error, warn, info, or off.`);
23
+ }
24
+ if (typeof one === 'object' && !(Array.isArray(one.tags) && one.tags.length > 0)) {
25
+ throw new Error(`The ${layer} scopes \`${kind}\` with no tags. Give the tags it applies to, or set the severity alone.`);
26
+ }
27
+ }
28
+ }
29
+ };
30
+ const said = (rules, finding) => {
31
+ const value = rules?.[finding.kind];
32
+ if (value === undefined) {
33
+ return undefined;
34
+ }
35
+ const list = Array.isArray(value) ? value : [value];
36
+ const scoped = list.find((one) => typeof one === 'object' && one.tags.includes(finding.tag));
37
+ if (scoped && typeof scoped === 'object') {
38
+ return scoped.severity;
39
+ }
40
+ const plain = list.find((one) => typeof one === 'string');
41
+ return typeof plain === 'string' ? plain : undefined;
42
+ };
43
+ export const applyRules = (findings, file, argument) => {
44
+ const off = new Map();
45
+ const kept = [];
46
+ for (const finding of findings) {
47
+ let severity = SHIPPED_RULES[finding.kind];
48
+ let decidedBy = 'shipped';
49
+ for (const [layer, rules] of [
50
+ ['file', file],
51
+ ['argument', argument],
52
+ ]) {
53
+ const value = said(rules, finding);
54
+ if (value !== undefined) {
55
+ severity = value;
56
+ decidedBy = layer;
57
+ }
58
+ }
59
+ if (severity === 'off') {
60
+ const layer = decidedBy;
61
+ const key = `${finding.kind} ${layer}`;
62
+ const entry = off.get(key) ?? { count: 0, kind: finding.kind, layer };
63
+ entry.count += 1;
64
+ off.set(key, entry);
65
+ continue;
66
+ }
67
+ kept.push({ ...finding, decidedBy, severity });
68
+ }
69
+ return { findings: kept, silenced: [...off.values()] };
70
+ };
71
+ export const overridesOf = (file, argument) => [
72
+ ['file', file],
73
+ ['argument', argument],
74
+ ].flatMap(([layer, rules]) => Object.entries(rules ?? {}).flatMap(([kind, value]) => (Array.isArray(value) ? value : [value]).map((one) => typeof one === 'string'
75
+ ? { kind: kind, layer, severity: one }
76
+ : { kind: kind, layer, severity: one.severity, tags: one.tags })));
@@ -0,0 +1,50 @@
1
+ import type { Answered, Located } from './collect.js';
2
+ import type { Published } from './manifest.js';
3
+ import type { Usage } from './report.js';
4
+ /** The element counts, before the stylesheets add theirs. */
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[];
12
+ /** The elements in the page now, counted once each. */
13
+ export declare const walk: (from: Document) => Counted;
14
+ /**
15
+ * The counts for the tags a manifest lists, and the rest apart as `unlisted`.
16
+ *
17
+ * Counted by prefix, since `watch()` runs before any manifest is at hand, and
18
+ * sorted here, where one is.
19
+ */
20
+ export declare const listed: (counted: Counted, tags: Map<string, Published>) => Counted & Pick<Usage, 'unlisted'>;
21
+ /** A running count of what renders, from `watch()`. */
22
+ export interface Watcher {
23
+ /** The counts so far. A copy: counting goes on after it is read. */
24
+ counts(): Counted;
25
+ /** Stops counting, and gives the slot back to whatever held it before. */
26
+ stop(): void;
27
+ }
28
+ /**
29
+ * Starts counting the Helix elements that render, from now on, and the ones
30
+ * already in the page.
31
+ *
32
+ * Hand the watcher to `diagnose`, and its report counts every element seen
33
+ * since, including the ones that have gone. Without one, `diagnose` counts the
34
+ * elements in the page at the moment it runs, and says so.
35
+ *
36
+ * **Start it early**, before the components load where you can, so the
37
+ * elements an early route renders and removes are counted. The ones in the
38
+ * page when it starts are counted by walking the page once.
39
+ *
40
+ * Two watchers on one page both count: a second one keeps the first's
41
+ * listener and calls it too.
42
+ */
43
+ export declare const watch: (from?: Document) => Watcher;
44
+ /**
45
+ * The parts and hooks a page's stylesheets reach for, where the manifest or
46
+ * the element's own styles say they exist. A dead one is a finding and is not
47
+ * counted as use.
48
+ */
49
+ export declare const styled: (rules: Located[], tags: Map<string, Published>, answered: Answered, values?: boolean) => Pick<Usage, 'hookValues' | 'hooks' | 'parts'>;
50
+ export {};
package/dist/usage.js ADDED
@@ -0,0 +1,147 @@
1
+ import { parts, targets } from './analyze.js';
2
+ const SLOT = Symbol.for('fusion.dev/helix/usage');
3
+ const AXES = ['emphasis', 'material', 'separation', 'tone'];
4
+ const bump = (into, key, by = 1) => {
5
+ into[key] = (into[key] ?? 0) + by;
6
+ };
7
+ const under = (into, key, make) => {
8
+ let found = into[key];
9
+ if (found === undefined) {
10
+ found = make();
11
+ into[key] = found;
12
+ }
13
+ return found;
14
+ };
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
+ };
20
+ const tally = (counted, element) => {
21
+ const tag = element.localName;
22
+ if (!tag.startsWith('helix-')) {
23
+ return;
24
+ }
25
+ const root = element.getRootNode();
26
+ const host = root instanceof ShadowRoot ? root.host.localName : '';
27
+ if (host.startsWith('helix-')) {
28
+ bump(counted.composed, tag);
29
+ return;
30
+ }
31
+ bump(counted.tags, tag);
32
+ for (const axis of AXES) {
33
+ const value = element.getAttribute(axis);
34
+ if (value !== null && value !== '') {
35
+ bump(under(under(counted.axes, tag, () => ({})), axis, () => ({})), value);
36
+ }
37
+ }
38
+ for (const child of element.children) {
39
+ const name = child.getAttribute('slot');
40
+ if (name) {
41
+ bump(under(counted.slots, tag, () => ({})), name);
42
+ }
43
+ }
44
+ };
45
+ const everyElement = (from, visit, depth = 0) => {
46
+ if (depth > 32) {
47
+ return;
48
+ }
49
+ for (const element of from.querySelectorAll('*')) {
50
+ visit(element);
51
+ if (element.shadowRoot) {
52
+ everyElement(element.shadowRoot, visit, depth + 1);
53
+ }
54
+ }
55
+ };
56
+ export const walk = (from) => {
57
+ const counted = empty();
58
+ everyElement(from, (element) => tally(counted, element));
59
+ return counted;
60
+ };
61
+ export const listed = (counted, tags) => {
62
+ const keep = (counts) => Object.fromEntries(Object.entries(counts).filter(([tag]) => tags.has(tag)));
63
+ const unlisted = {};
64
+ for (const counts of [counted.tags, counted.composed]) {
65
+ for (const [tag, count] of Object.entries(counts)) {
66
+ if (!tags.has(tag)) {
67
+ bump(unlisted, tag, count);
68
+ }
69
+ }
70
+ }
71
+ return {
72
+ axes: keep(counted.axes),
73
+ composed: keep(counted.composed),
74
+ slots: keep(counted.slots),
75
+ tags: keep(counted.tags),
76
+ unlisted,
77
+ };
78
+ };
79
+ export const watch = (from = document) => {
80
+ const counted = empty();
81
+ const seen = new WeakSet();
82
+ const see = (element) => {
83
+ if (!seen.has(element)) {
84
+ seen.add(element);
85
+ tally(counted, element);
86
+ }
87
+ };
88
+ const store = globalThis;
89
+ const before = store[SLOT];
90
+ let live = true;
91
+ const listener = (element) => {
92
+ if (live) {
93
+ see(element);
94
+ }
95
+ if (typeof before === 'function') {
96
+ before(element);
97
+ }
98
+ };
99
+ store[SLOT] = listener;
100
+ everyElement(from, see);
101
+ return {
102
+ counts: () => structuredClone(counted),
103
+ stop: () => {
104
+ live = false;
105
+ if (store[SLOT] === listener) {
106
+ store[SLOT] = before;
107
+ }
108
+ },
109
+ };
110
+ };
111
+ export const styled = (rules, tags, answered, values = true) => {
112
+ const counted = {
113
+ hooks: {},
114
+ hookValues: {},
115
+ parts: {},
116
+ };
117
+ for (const located of rules) {
118
+ const selector = located.rule.selectorText;
119
+ if (!selector?.includes('helix-')) {
120
+ continue;
121
+ }
122
+ for (const { names, tag } of parts(selector)) {
123
+ for (const name of names) {
124
+ if (tags.get(tag)?.parts.has(name)) {
125
+ bump(under(counted.parts, tag, () => ({})), name);
126
+ }
127
+ }
128
+ }
129
+ for (const tag of targets(selector)) {
130
+ for (const property of Array.from(located.rule.style ?? [])) {
131
+ if (property.startsWith('--') &&
132
+ (tags.get(tag)?.hooks.has(property) || answered.get(tag)?.has(property))) {
133
+ bump(under(counted.hooks, tag, () => ({})), property);
134
+ if (values) {
135
+ const value = (located.rule.style?.getPropertyValue?.(property) ?? '')
136
+ .trim()
137
+ .slice(0, 200);
138
+ if (value !== '') {
139
+ bump(under(under(counted.hookValues, tag, () => ({})), property, () => ({})), value);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return counted;
147
+ };
@@ -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.137.0";
@@ -0,0 +1 @@
1
+ export const VERSION = '0.137.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.135.0",
4
+ "version": "0.137.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "bugs": "https://github.com/project-fusion/fusion/issues",
31
31
  "devDependencies": {
32
- "@fusion.dev/helix-testing": "0.135.0",
32
+ "@fusion.dev/helix-testing": "0.137.0",
33
33
  "@types/bun": "^1.3.0"
34
34
  },
35
35
  "homepage": "https://fusion.dev",