@fusion.dev/helix-diagnostics 0.137.0 → 0.139.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 +172 -36
- package/dist/analyze.d.ts +25 -4
- package/dist/analyze.js +268 -50
- package/dist/ci.d.ts +18 -0
- package/dist/ci.js +30 -0
- package/dist/collect.d.ts +20 -1
- package/dist/collect.js +32 -17
- package/dist/config.d.ts +17 -0
- package/dist/config.js +30 -11
- package/dist/console.d.ts +9 -1
- package/dist/console.js +13 -3
- package/dist/diagnose.d.ts +75 -0
- package/dist/diagnose.js +76 -0
- package/dist/identity.js +4 -0
- package/dist/index.d.ts +5 -77
- package/dist/index.js +4 -73
- package/dist/init.d.ts +87 -0
- package/dist/init.js +326 -0
- package/dist/manifest.d.ts +65 -4
- package/dist/manifest.js +92 -27
- package/dist/overlay.d.ts +12 -0
- package/dist/overlay.js +278 -0
- package/dist/page.d.ts +25 -0
- package/dist/page.js +21 -0
- package/dist/report.d.ts +8 -1
- package/dist/report.js +2 -0
- package/dist/rules.js +2 -0
- package/dist/schedule.d.ts +57 -0
- package/dist/schedule.js +84 -0
- package/dist/usage.d.ts +10 -2
- package/dist/usage.js +42 -30
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -2
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { matchable } from './analyze.js';
|
|
2
|
+
import { OVERLAY } from './page.js';
|
|
3
|
+
const STYLE = `
|
|
4
|
+
:host {
|
|
5
|
+
all: initial;
|
|
6
|
+
--ink: #111113;
|
|
7
|
+
--muted: #55555c;
|
|
8
|
+
--ground: #ffffff;
|
|
9
|
+
--raised: #f4f4f5;
|
|
10
|
+
--edge: #d4d4d8;
|
|
11
|
+
--error: #b42318;
|
|
12
|
+
--warn: #8a5a00;
|
|
13
|
+
--info: #1d4ed8;
|
|
14
|
+
color-scheme: light dark;
|
|
15
|
+
font: 13px/1.45 system-ui, sans-serif;
|
|
16
|
+
inset-block-end: 16px;
|
|
17
|
+
inset-inline-end: 16px;
|
|
18
|
+
position: fixed;
|
|
19
|
+
z-index: 2147483647;
|
|
20
|
+
}
|
|
21
|
+
@media (prefers-color-scheme: dark) {
|
|
22
|
+
:host {
|
|
23
|
+
--ink: #f4f4f5;
|
|
24
|
+
--muted: #a1a1aa;
|
|
25
|
+
--ground: #18181b;
|
|
26
|
+
--raised: #27272a;
|
|
27
|
+
--edge: #3f3f46;
|
|
28
|
+
--error: #f97066;
|
|
29
|
+
--warn: #fdb022;
|
|
30
|
+
--info: #84adff;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
[hidden] { display: none !important; }
|
|
34
|
+
.stack {
|
|
35
|
+
align-items: flex-end;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
}
|
|
40
|
+
button {
|
|
41
|
+
background: var(--ground);
|
|
42
|
+
border: 1px solid var(--edge);
|
|
43
|
+
border-radius: 999px;
|
|
44
|
+
color: var(--ink);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
font: inherit;
|
|
47
|
+
}
|
|
48
|
+
button:focus-visible, li:focus-visible {
|
|
49
|
+
outline: 2px solid var(--info);
|
|
50
|
+
outline-offset: 2px;
|
|
51
|
+
}
|
|
52
|
+
.badge {
|
|
53
|
+
align-items: center;
|
|
54
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
padding: 6px 12px;
|
|
58
|
+
}
|
|
59
|
+
.dot {
|
|
60
|
+
block-size: 8px;
|
|
61
|
+
border-radius: 50%;
|
|
62
|
+
inline-size: 8px;
|
|
63
|
+
}
|
|
64
|
+
.panel {
|
|
65
|
+
background: var(--ground);
|
|
66
|
+
border: 1px solid var(--edge);
|
|
67
|
+
border-radius: 10px;
|
|
68
|
+
box-shadow: 0 8px 30px rgb(0 0 0 / 0.24);
|
|
69
|
+
color: var(--ink);
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
inline-size: min(440px, calc(100vw - 32px));
|
|
73
|
+
max-block-size: min(70vh, 560px);
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
}
|
|
76
|
+
header {
|
|
77
|
+
align-items: center;
|
|
78
|
+
border-block-end: 1px solid var(--edge);
|
|
79
|
+
display: flex;
|
|
80
|
+
gap: 8px;
|
|
81
|
+
padding: 10px 12px;
|
|
82
|
+
}
|
|
83
|
+
h2 { font-size: 14px; font-weight: 600; margin: 0; }
|
|
84
|
+
.counts { color: var(--muted); flex: 1; margin: 0; }
|
|
85
|
+
.close { border-radius: 6px; padding: 2px 8px; }
|
|
86
|
+
ol { list-style: none; margin: 0; overflow: auto; padding: 0; }
|
|
87
|
+
li {
|
|
88
|
+
border-block-end: 1px solid var(--edge);
|
|
89
|
+
display: grid;
|
|
90
|
+
gap: 4px;
|
|
91
|
+
padding: 10px 12px;
|
|
92
|
+
}
|
|
93
|
+
li:last-child { border-block-end: none; }
|
|
94
|
+
.kind { font-weight: 600; }
|
|
95
|
+
.error { color: var(--error); }
|
|
96
|
+
.warn { color: var(--warn); }
|
|
97
|
+
.info { color: var(--info); }
|
|
98
|
+
.message code {
|
|
99
|
+
background: var(--raised);
|
|
100
|
+
border-radius: 3px;
|
|
101
|
+
font: 12px/1.4 ui-monospace, monospace;
|
|
102
|
+
padding: 1px 4px;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
}
|
|
105
|
+
.subject, pre {
|
|
106
|
+
font: 12px/1.4 ui-monospace, monospace;
|
|
107
|
+
margin: 0;
|
|
108
|
+
overflow-wrap: anywhere;
|
|
109
|
+
white-space: pre-wrap;
|
|
110
|
+
}
|
|
111
|
+
pre {
|
|
112
|
+
background: var(--raised);
|
|
113
|
+
border-radius: 6px;
|
|
114
|
+
padding: 6px 8px;
|
|
115
|
+
}
|
|
116
|
+
.where { color: var(--muted); font-size: 12px; }
|
|
117
|
+
.outline {
|
|
118
|
+
border: 2px dashed var(--error);
|
|
119
|
+
border-radius: 4px;
|
|
120
|
+
pointer-events: none;
|
|
121
|
+
position: fixed;
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
const WORDS = {
|
|
125
|
+
error: ['error', 'errors'],
|
|
126
|
+
info: ['info', 'info'],
|
|
127
|
+
warn: ['warning', 'warnings'],
|
|
128
|
+
};
|
|
129
|
+
const counted = (count, severity) => `${count} ${WORDS[severity][count === 1 ? 0 : 1]}`;
|
|
130
|
+
const make = (tag, name, text) => {
|
|
131
|
+
const element = document.createElement(tag);
|
|
132
|
+
if (name) {
|
|
133
|
+
element.className = name;
|
|
134
|
+
}
|
|
135
|
+
if (text !== undefined) {
|
|
136
|
+
element.textContent = text;
|
|
137
|
+
}
|
|
138
|
+
return element;
|
|
139
|
+
};
|
|
140
|
+
const worded = (message) => {
|
|
141
|
+
const out = make('span', 'message');
|
|
142
|
+
message.split('`').forEach((piece, at) => {
|
|
143
|
+
if (piece === '') {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
out.append(at % 2 === 1 ? make('code', undefined, piece) : document.createTextNode(piece));
|
|
147
|
+
});
|
|
148
|
+
return out;
|
|
149
|
+
};
|
|
150
|
+
const locate = (finding, page) => {
|
|
151
|
+
const markup = /^<([a-z][a-z0-9-]*) ([^\s=>]+)(?:="(.*)")?>$/.exec(finding.subject);
|
|
152
|
+
let selector;
|
|
153
|
+
if (markup) {
|
|
154
|
+
const [, tag, name, value] = markup;
|
|
155
|
+
selector =
|
|
156
|
+
value === undefined
|
|
157
|
+
? `${tag}[${CSS.escape(name ?? '')}]`
|
|
158
|
+
: `${tag}[${CSS.escape(name ?? '')}="${CSS.escape(value)}"]`;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
selector = matchable(finding.subject.split(' { ')[0] ?? '');
|
|
162
|
+
}
|
|
163
|
+
if (!selector) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return page.querySelector(selector);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const define = () => {
|
|
174
|
+
if (customElements.get(OVERLAY)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
customElements.define(OVERLAY, class extends HTMLElement {
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
export const overlayOn = (page) => {
|
|
181
|
+
define();
|
|
182
|
+
const element = page.createElement(OVERLAY);
|
|
183
|
+
const root = element.attachShadow({ mode: 'open' });
|
|
184
|
+
const style = make('style');
|
|
185
|
+
style.textContent = STYLE;
|
|
186
|
+
const badge = make('button', 'badge');
|
|
187
|
+
badge.type = 'button';
|
|
188
|
+
badge.hidden = true;
|
|
189
|
+
badge.setAttribute('aria-expanded', 'false');
|
|
190
|
+
badge.setAttribute('aria-controls', 'panel');
|
|
191
|
+
const dot = make('span', 'dot');
|
|
192
|
+
const label = make('span');
|
|
193
|
+
badge.append(dot, label);
|
|
194
|
+
const panel = make('section', 'panel');
|
|
195
|
+
panel.id = 'panel';
|
|
196
|
+
panel.hidden = true;
|
|
197
|
+
panel.setAttribute('aria-label', 'Helix diagnostics');
|
|
198
|
+
const header = make('header');
|
|
199
|
+
const title = make('h2', undefined, 'Helix diagnostics');
|
|
200
|
+
const summary = make('p', 'counts');
|
|
201
|
+
const close = make('button', 'close', '×');
|
|
202
|
+
close.type = 'button';
|
|
203
|
+
close.setAttribute('aria-label', 'Close Helix diagnostics');
|
|
204
|
+
header.append(title, summary, close);
|
|
205
|
+
const list = make('ol');
|
|
206
|
+
panel.append(header, list);
|
|
207
|
+
const outline = make('div', 'outline');
|
|
208
|
+
outline.hidden = true;
|
|
209
|
+
const stack = make('div', 'stack');
|
|
210
|
+
stack.append(panel, badge);
|
|
211
|
+
root.append(style, stack, outline);
|
|
212
|
+
(page.body ?? page.documentElement).append(element);
|
|
213
|
+
const open = (value) => {
|
|
214
|
+
panel.hidden = !value;
|
|
215
|
+
badge.setAttribute('aria-expanded', String(value));
|
|
216
|
+
if (!value) {
|
|
217
|
+
outline.hidden = true;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
badge.addEventListener('click', () => open(panel.hasAttribute('hidden')));
|
|
221
|
+
close.addEventListener('click', () => {
|
|
222
|
+
open(false);
|
|
223
|
+
badge.focus();
|
|
224
|
+
});
|
|
225
|
+
element.addEventListener('keydown', (event) => {
|
|
226
|
+
if (event.key === 'Escape' && !panel.hidden) {
|
|
227
|
+
open(false);
|
|
228
|
+
badge.focus();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
const point = (finding) => {
|
|
232
|
+
const target = finding ? locate(finding, page) : null;
|
|
233
|
+
if (!target) {
|
|
234
|
+
outline.hidden = true;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const box = target.getBoundingClientRect();
|
|
238
|
+
outline.style.borderColor = `var(--${finding?.severity})`;
|
|
239
|
+
outline.style.insetBlockStart = `${box.top - 3}px`;
|
|
240
|
+
outline.style.insetInlineStart = `${box.left - 3}px`;
|
|
241
|
+
outline.style.blockSize = `${box.height + 2}px`;
|
|
242
|
+
outline.style.inlineSize = `${box.width + 2}px`;
|
|
243
|
+
outline.hidden = false;
|
|
244
|
+
};
|
|
245
|
+
const show = (run) => {
|
|
246
|
+
const findings = run.findings;
|
|
247
|
+
const of = (severity) => findings.filter((one) => one.severity === severity).length;
|
|
248
|
+
const worst = of('error') > 0 ? 'error' : of('warn') > 0 ? 'warn' : 'info';
|
|
249
|
+
const counts = ['error', 'warn', 'info']
|
|
250
|
+
.filter((severity) => of(severity) > 0)
|
|
251
|
+
.map((severity) => counted(of(severity), severity))
|
|
252
|
+
.join(', ');
|
|
253
|
+
badge.hidden = findings.length === 0;
|
|
254
|
+
if (findings.length === 0) {
|
|
255
|
+
open(false);
|
|
256
|
+
}
|
|
257
|
+
dot.style.background = `var(--${worst})`;
|
|
258
|
+
label.textContent = counts;
|
|
259
|
+
badge.setAttribute('aria-label', `Helix diagnostics: ${counts}`);
|
|
260
|
+
summary.textContent = run.run.page ? `${counts} on ${run.run.page}` : counts;
|
|
261
|
+
list.replaceChildren(...findings.map((finding) => {
|
|
262
|
+
const item = make('li');
|
|
263
|
+
item.tabIndex = 0;
|
|
264
|
+
const head = make('span', 'kind');
|
|
265
|
+
head.append(make('span', finding.severity, `${finding.severity} `), document.createTextNode(finding.kind));
|
|
266
|
+
item.append(head, make('code', 'subject', finding.subject), worded(finding.message));
|
|
267
|
+
if (finding.origin) {
|
|
268
|
+
item.append(make('span', 'where', `in ${finding.origin.sheet}`), make('pre', undefined, finding.origin.cssText));
|
|
269
|
+
}
|
|
270
|
+
item.addEventListener('mouseenter', () => point(finding));
|
|
271
|
+
item.addEventListener('focus', () => point(finding));
|
|
272
|
+
item.addEventListener('mouseleave', () => point(undefined));
|
|
273
|
+
item.addEventListener('blur', () => point(undefined));
|
|
274
|
+
return item;
|
|
275
|
+
}));
|
|
276
|
+
};
|
|
277
|
+
return { element, remove: () => element.remove(), show };
|
|
278
|
+
};
|
package/dist/page.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything on a page, gathered in one walk.
|
|
3
|
+
*
|
|
4
|
+
* **One walk for every check in a run.** The stylesheets, the usage, the
|
|
5
|
+
* attributes, the version evidence, and the duplicate check each walked the
|
|
6
|
+
* whole page, shadow roots included, before this: five walks a run, which a
|
|
7
|
+
* run by hand never feels and a run after every change would.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The tag of the overlay `initDiagnostics()` puts on a page: the one element
|
|
11
|
+
* diagnostics adds, which it leaves out of everything it counts and reads.
|
|
12
|
+
*/
|
|
13
|
+
export declare const OVERLAY = "helix-diagnostics-overlay";
|
|
14
|
+
/** Every element on a page and every shadow root, from one walk. */
|
|
15
|
+
export interface Page {
|
|
16
|
+
/**
|
|
17
|
+
* Every element, the document's and every shadow root's, each shadow
|
|
18
|
+
* root's elements following the host it belongs to.
|
|
19
|
+
*/
|
|
20
|
+
elements: Element[];
|
|
21
|
+
/** Every shadow root, nested ones included. */
|
|
22
|
+
roots: ShadowRoot[];
|
|
23
|
+
}
|
|
24
|
+
/** Walks `from` once, shadow roots included. */
|
|
25
|
+
export declare const pageOf: (from: ParentNode) => Page;
|
package/dist/page.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const OVERLAY = 'helix-diagnostics-overlay';
|
|
2
|
+
const DEPTH = 32;
|
|
3
|
+
export const pageOf = (from) => {
|
|
4
|
+
const elements = [];
|
|
5
|
+
const roots = [];
|
|
6
|
+
const visit = (root, depth) => {
|
|
7
|
+
if (depth > DEPTH) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
for (const element of root.querySelectorAll('*')) {
|
|
11
|
+
elements.push(element);
|
|
12
|
+
const shadow = element.shadowRoot;
|
|
13
|
+
if (shadow) {
|
|
14
|
+
roots.push(shadow);
|
|
15
|
+
visit(shadow, depth + 1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
visit(from, 0);
|
|
20
|
+
return { elements, roots };
|
|
21
|
+
};
|
package/dist/report.d.ts
CHANGED
|
@@ -87,7 +87,7 @@ export interface Finding {
|
|
|
87
87
|
tag: string;
|
|
88
88
|
}
|
|
89
89
|
/** Every fault class, stable across versions. */
|
|
90
|
-
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'];
|
|
91
91
|
/** A fault class. */
|
|
92
92
|
export type Kind = (typeof KINDS)[number];
|
|
93
93
|
/** How serious a finding is. */
|
|
@@ -204,6 +204,13 @@ export interface Configuration {
|
|
|
204
204
|
sample: number;
|
|
205
205
|
};
|
|
206
206
|
};
|
|
207
|
+
/** Where `initDiagnostics()` hands each run, beside the console. */
|
|
208
|
+
sinks: {
|
|
209
|
+
/** The overlay on the page. */
|
|
210
|
+
overlay: boolean;
|
|
211
|
+
/** The relay, `helix diagnostics`: off, on at its own address, or at this one. */
|
|
212
|
+
relay: boolean | string;
|
|
213
|
+
};
|
|
207
214
|
/** Each override in effect, from the file or the call. */
|
|
208
215
|
overrides: Array<{
|
|
209
216
|
kind: Kind;
|
package/dist/report.js
CHANGED
package/dist/rules.js
CHANGED
|
@@ -4,8 +4,10 @@ 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']);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When `initDiagnostics()` runs: debounced, idle, and bounded by its own cost.
|
|
3
|
+
*
|
|
4
|
+
* - **After a change**, once changes stop for `quiet` milliseconds, or after
|
|
5
|
+
* `longest` milliseconds whatever keeps changing: a page that never stops,
|
|
6
|
+
* such as a carousel adding slides, would otherwise never be judged.
|
|
7
|
+
* - **In an idle moment**, at most `idle` milliseconds later.
|
|
8
|
+
* - **Never sooner than `gap` after the last run, nor sooner than `factor`
|
|
9
|
+
* times its duration**, so a slow page is judged less often.
|
|
10
|
+
* - **Never in a hidden tab**: a change there waits for the tab to be shown.
|
|
11
|
+
* - **Never when nothing changed** since the last run.
|
|
12
|
+
*
|
|
13
|
+
* The browser's hooks are passed in, so a spec drives it with small numbers
|
|
14
|
+
* and a pretend tab.
|
|
15
|
+
*/
|
|
16
|
+
/** How long each wait is, in milliseconds. */
|
|
17
|
+
export interface Timing {
|
|
18
|
+
/** How long changes must stop for before a run. */
|
|
19
|
+
quiet: number;
|
|
20
|
+
/** The longest an idle callback may wait. */
|
|
21
|
+
idle: number;
|
|
22
|
+
/** The least time between the end of one run and the start of the next. */
|
|
23
|
+
gap: number;
|
|
24
|
+
/** The least time between runs, as a multiple of the last run's duration. */
|
|
25
|
+
factor: number;
|
|
26
|
+
/** The longest a change waits for the page to go quiet. */
|
|
27
|
+
longest: number;
|
|
28
|
+
}
|
|
29
|
+
/** The shipped timing. */
|
|
30
|
+
export declare const TIMING: Timing;
|
|
31
|
+
/** What the schedule asks of the browser. */
|
|
32
|
+
export interface Browser {
|
|
33
|
+
/** Whether the tab is hidden now. */
|
|
34
|
+
hidden(): boolean;
|
|
35
|
+
/** Calls `then` once in an idle moment, at most `timeout` later; returns a cancel. */
|
|
36
|
+
idle(then: () => void, timeout: number): () => void;
|
|
37
|
+
/** The time now, in milliseconds. */
|
|
38
|
+
now(): number;
|
|
39
|
+
/** Calls `then` when the tab is next shown; returns a removal. */
|
|
40
|
+
shown(then: () => void): () => void;
|
|
41
|
+
}
|
|
42
|
+
/** The browser's own hooks, with a timer where a browser has no idle callback. */
|
|
43
|
+
export declare const browser: () => Browser;
|
|
44
|
+
/** A schedule: told of changes, it decides when `run` runs. */
|
|
45
|
+
export interface Schedule {
|
|
46
|
+
/** Something changed: a run follows, as the schedule allows. */
|
|
47
|
+
poke(): void;
|
|
48
|
+
/** Ends it: nothing more runs. */
|
|
49
|
+
stop(): void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A schedule for `run`, which returns how long it took in milliseconds.
|
|
53
|
+
*
|
|
54
|
+
* One run at a time, and a change during a run is a change after it: the run
|
|
55
|
+
* that follows sees it.
|
|
56
|
+
*/
|
|
57
|
+
export declare const schedule: (run: () => number, timing?: Timing, on?: Browser) => Schedule;
|
package/dist/schedule.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const TIMING = { factor: 20, gap: 2000, idle: 3000, longest: 10000, quiet: 1000 };
|
|
2
|
+
export const browser = () => ({
|
|
3
|
+
hidden: () => globalThis.document?.visibilityState === 'hidden',
|
|
4
|
+
idle: (then, timeout) => {
|
|
5
|
+
if (typeof globalThis.requestIdleCallback === 'function') {
|
|
6
|
+
const id = globalThis.requestIdleCallback(then, { timeout });
|
|
7
|
+
return () => globalThis.cancelIdleCallback(id);
|
|
8
|
+
}
|
|
9
|
+
const id = setTimeout(then, 1);
|
|
10
|
+
return () => clearTimeout(id);
|
|
11
|
+
},
|
|
12
|
+
now: () => globalThis.performance?.now() ?? Date.now(),
|
|
13
|
+
shown: (then) => {
|
|
14
|
+
const listener = () => {
|
|
15
|
+
if (globalThis.document?.visibilityState !== 'hidden') {
|
|
16
|
+
globalThis.document?.removeEventListener('visibilitychange', listener);
|
|
17
|
+
then();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
globalThis.document?.addEventListener('visibilitychange', listener);
|
|
21
|
+
return () => globalThis.document?.removeEventListener('visibilitychange', listener);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
export const schedule = (run, timing = TIMING, on = browser()) => {
|
|
25
|
+
let dirty = false;
|
|
26
|
+
let since = Number.POSITIVE_INFINITY;
|
|
27
|
+
let live = true;
|
|
28
|
+
let ended = Number.NEGATIVE_INFINITY;
|
|
29
|
+
let last = 0;
|
|
30
|
+
let cancel;
|
|
31
|
+
let quiet;
|
|
32
|
+
let wait;
|
|
33
|
+
const go = () => {
|
|
34
|
+
cancel = undefined;
|
|
35
|
+
if (!live || !dirty) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (on.hidden()) {
|
|
39
|
+
cancel = on.shown(settle);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const rest = Math.max(timing.gap, timing.factor * last) - (on.now() - ended);
|
|
43
|
+
if (rest > 0) {
|
|
44
|
+
wait = setTimeout(go, rest);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
cancel = on.idle(() => {
|
|
48
|
+
cancel = undefined;
|
|
49
|
+
if (!live || !dirty) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
dirty = false;
|
|
53
|
+
since = Number.POSITIVE_INFINITY;
|
|
54
|
+
last = run();
|
|
55
|
+
ended = on.now();
|
|
56
|
+
if (dirty) {
|
|
57
|
+
settle();
|
|
58
|
+
}
|
|
59
|
+
}, timing.idle);
|
|
60
|
+
};
|
|
61
|
+
const settle = () => {
|
|
62
|
+
if (!live) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
clearTimeout(quiet);
|
|
66
|
+
clearTimeout(wait);
|
|
67
|
+
cancel?.();
|
|
68
|
+
cancel = undefined;
|
|
69
|
+
quiet = setTimeout(go, Math.min(timing.quiet, Math.max(0, since + timing.longest - on.now())));
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
poke: () => {
|
|
73
|
+
dirty = true;
|
|
74
|
+
since = Math.min(since, on.now());
|
|
75
|
+
settle();
|
|
76
|
+
},
|
|
77
|
+
stop: () => {
|
|
78
|
+
live = false;
|
|
79
|
+
clearTimeout(quiet);
|
|
80
|
+
clearTimeout(wait);
|
|
81
|
+
cancel?.();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
package/dist/usage.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Answered, Located } from './collect.js';
|
|
2
2
|
import type { Published } from './manifest.js';
|
|
3
|
+
import { type Page } from './page.js';
|
|
3
4
|
import type { Usage } from './report.js';
|
|
4
5
|
/** The element counts, before the stylesheets add theirs. */
|
|
5
6
|
type Counted = Pick<Usage, 'axes' | 'composed' | 'slots' | 'tags'>;
|
|
@@ -10,7 +11,7 @@ type Counted = Pick<Usage, 'axes' | 'composed' | 'slots' | 'tags'>;
|
|
|
10
11
|
*/
|
|
11
12
|
export declare const loadedVersions: () => string[];
|
|
12
13
|
/** The elements in the page now, counted once each. */
|
|
13
|
-
export declare const walk: (from: Document) => Counted;
|
|
14
|
+
export declare const walk: (from: Document, page?: Page) => Counted;
|
|
14
15
|
/**
|
|
15
16
|
* The counts for the tags a manifest lists, and the rest apart as `unlisted`.
|
|
16
17
|
*
|
|
@@ -40,7 +41,14 @@ export interface Watcher {
|
|
|
40
41
|
* Two watchers on one page both count: a second one keeps the first's
|
|
41
42
|
* listener and calls it too.
|
|
42
43
|
*/
|
|
43
|
-
export declare const watch: (from?: Document
|
|
44
|
+
export declare const watch: (from?: Document, { onRender, }?: {
|
|
45
|
+
/**
|
|
46
|
+
* Called as each Helix element renders for the first time, after it is
|
|
47
|
+
* counted: how `initDiagnostics()` hears that the page changed when an
|
|
48
|
+
* element upgrades without adding anything to the DOM.
|
|
49
|
+
*/
|
|
50
|
+
onRender?: ((element: Element) => void) | undefined;
|
|
51
|
+
}) => Watcher;
|
|
44
52
|
/**
|
|
45
53
|
* The parts and hooks a page's stylesheets reach for, where the manifest or
|
|
46
54
|
* the element's own styles say they exist. A dead one is a finding and is not
|
package/dist/usage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parts, targets } from './analyze.js';
|
|
2
|
+
import { OVERLAY, pageOf } from './page.js';
|
|
2
3
|
const SLOT = Symbol.for('fusion.dev/helix/usage');
|
|
3
4
|
const AXES = ['emphasis', 'material', 'separation', 'tone'];
|
|
4
5
|
const bump = (into, key, by = 1) => {
|
|
@@ -19,7 +20,7 @@ export const loadedVersions = () => {
|
|
|
19
20
|
};
|
|
20
21
|
const tally = (counted, element) => {
|
|
21
22
|
const tag = element.localName;
|
|
22
|
-
if (!tag.startsWith('helix-')) {
|
|
23
|
+
if (!tag.startsWith('helix-') || tag === OVERLAY) {
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
const root = element.getRootNode();
|
|
@@ -42,20 +43,11 @@ const tally = (counted, element) => {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
|
-
const
|
|
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) => {
|
|
46
|
+
export const walk = (from, page = pageOf(from)) => {
|
|
57
47
|
const counted = empty();
|
|
58
|
-
|
|
48
|
+
for (const element of page.elements) {
|
|
49
|
+
tally(counted, element);
|
|
50
|
+
}
|
|
59
51
|
return counted;
|
|
60
52
|
};
|
|
61
53
|
export const listed = (counted, tags) => {
|
|
@@ -76,7 +68,7 @@ export const listed = (counted, tags) => {
|
|
|
76
68
|
unlisted,
|
|
77
69
|
};
|
|
78
70
|
};
|
|
79
|
-
export const watch = (from = document) => {
|
|
71
|
+
export const watch = (from = document, { onRender, } = {}) => {
|
|
80
72
|
const counted = empty();
|
|
81
73
|
const seen = new WeakSet();
|
|
82
74
|
const see = (element) => {
|
|
@@ -91,13 +83,16 @@ export const watch = (from = document) => {
|
|
|
91
83
|
const listener = (element) => {
|
|
92
84
|
if (live) {
|
|
93
85
|
see(element);
|
|
86
|
+
onRender?.(element);
|
|
94
87
|
}
|
|
95
88
|
if (typeof before === 'function') {
|
|
96
89
|
before(element);
|
|
97
90
|
}
|
|
98
91
|
};
|
|
99
92
|
store[SLOT] = listener;
|
|
100
|
-
|
|
93
|
+
for (const element of pageOf(from).elements) {
|
|
94
|
+
see(element);
|
|
95
|
+
}
|
|
101
96
|
return {
|
|
102
97
|
counts: () => structuredClone(counted),
|
|
103
98
|
stop: () => {
|
|
@@ -114,32 +109,49 @@ export const styled = (rules, tags, answered, values = true) => {
|
|
|
114
109
|
hookValues: {},
|
|
115
110
|
parts: {},
|
|
116
111
|
};
|
|
112
|
+
const listing = new Map();
|
|
113
|
+
for (const [tag, element] of tags) {
|
|
114
|
+
for (const property of element.hooks.keys()) {
|
|
115
|
+
listing.set(property, [...(listing.get(property) ?? []), tag]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const count = (located, tag, property) => {
|
|
119
|
+
bump(under(counted.hooks, tag, () => ({})), property);
|
|
120
|
+
if (!values) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const value = (located.rule.style?.getPropertyValue?.(property) ?? '').trim().slice(0, 200);
|
|
124
|
+
if (value !== '') {
|
|
125
|
+
bump(under(under(counted.hookValues, tag, () => ({})), property, () => ({})), value);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
117
128
|
for (const located of rules) {
|
|
118
129
|
const selector = located.rule.selectorText;
|
|
119
|
-
if (!selector
|
|
130
|
+
if (!selector) {
|
|
120
131
|
continue;
|
|
121
132
|
}
|
|
122
|
-
for (const { names, tag } of parts(selector)) {
|
|
133
|
+
for (const { names, tag } of selector.includes('helix-') ? parts(selector) : []) {
|
|
123
134
|
for (const name of names) {
|
|
124
135
|
if (tags.get(tag)?.parts.has(name)) {
|
|
125
136
|
bump(under(counted.parts, tag, () => ({})), name);
|
|
126
137
|
}
|
|
127
138
|
}
|
|
128
139
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (value !== '') {
|
|
139
|
-
bump(under(under(counted.hookValues, tag, () => ({})), property, () => ({})), value);
|
|
140
|
-
}
|
|
140
|
+
const named = targets(selector).filter((tag) => tags.has(tag));
|
|
141
|
+
for (const property of Array.from(located.rule.style ?? [])) {
|
|
142
|
+
if (!property.startsWith('--')) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (named.length > 0) {
|
|
146
|
+
for (const tag of named) {
|
|
147
|
+
if (tags.get(tag)?.hooks.has(property) || answered.get(tag)?.has(property)) {
|
|
148
|
+
count(located, tag, property);
|
|
141
149
|
}
|
|
142
150
|
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (const tag of listing.get(property) ?? []) {
|
|
154
|
+
count(located, tag, property);
|
|
143
155
|
}
|
|
144
156
|
}
|
|
145
157
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.139.0';
|