@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/init.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { checkConfig, LOCAL, resolveConfig } from './config.js';
|
|
2
|
+
import { report as print } from './console.js';
|
|
3
|
+
import { diagnose } from './diagnose.js';
|
|
4
|
+
import { overlayOn } from './overlay.js';
|
|
5
|
+
import { schedule } from './schedule.js';
|
|
6
|
+
import { loadedVersions, watch } from './usage.js';
|
|
7
|
+
export const INSTANCE = Symbol.for('fusion.dev/helix/diagnostics');
|
|
8
|
+
const CDN = 'https://cdn.jsdelivr.net/npm';
|
|
9
|
+
const RELAY = 'http://127.0.0.1:7417';
|
|
10
|
+
const KEEPALIVE = 60_000;
|
|
11
|
+
const FAILURES = 3;
|
|
12
|
+
const inert = () => ({
|
|
13
|
+
active: false,
|
|
14
|
+
latest: undefined,
|
|
15
|
+
run: () => undefined,
|
|
16
|
+
stop: () => { },
|
|
17
|
+
subscribe: () => () => { },
|
|
18
|
+
});
|
|
19
|
+
const said = new Set();
|
|
20
|
+
const once = (key, print) => {
|
|
21
|
+
if (said.has(key)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
said.add(key);
|
|
25
|
+
try {
|
|
26
|
+
print();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
32
|
+
export const initDiagnostics = (options = {}) => {
|
|
33
|
+
const page = globalThis.document;
|
|
34
|
+
if (page === undefined) {
|
|
35
|
+
return inert();
|
|
36
|
+
}
|
|
37
|
+
const store = globalThis;
|
|
38
|
+
const existing = store[INSTANCE];
|
|
39
|
+
if (existing?.active) {
|
|
40
|
+
return existing;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const started = start(options, page);
|
|
44
|
+
if (started.active) {
|
|
45
|
+
store[INSTANCE] = started;
|
|
46
|
+
}
|
|
47
|
+
return started;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
once(`start ${messageOf(error)}`, () => console.error('helix diagnostics: not started, and the page goes on as usual.', error));
|
|
51
|
+
return inert();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const start = (options, page) => {
|
|
55
|
+
const { cdn = CDN, duplicates, environment, file, onReport, page: route, tiers, ...call } = options;
|
|
56
|
+
let configuration;
|
|
57
|
+
try {
|
|
58
|
+
configuration = resolveConfig(file, call).configuration;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
once(`settings ${messageOf(error)}`, () => console.error(`helix diagnostics: not started. ${messageOf(error)}`));
|
|
62
|
+
return inert();
|
|
63
|
+
}
|
|
64
|
+
const host = globalThis.location?.hostname ?? '';
|
|
65
|
+
if (environment === undefined && !LOCAL.test(host)) {
|
|
66
|
+
once(`host ${host}`, () => console.info(`helix diagnostics: not running on ${host || 'this page'}, which is no local host, with no environment declared. Pass \`environment\` to run here.`));
|
|
67
|
+
return inert();
|
|
68
|
+
}
|
|
69
|
+
if (environment === 'production') {
|
|
70
|
+
const production = configuration.environments.production;
|
|
71
|
+
if (production === false || Math.random() >= production.sample) {
|
|
72
|
+
return inert();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return running({
|
|
76
|
+
call,
|
|
77
|
+
cdn,
|
|
78
|
+
duplicates,
|
|
79
|
+
environment,
|
|
80
|
+
file,
|
|
81
|
+
onReport,
|
|
82
|
+
page,
|
|
83
|
+
route,
|
|
84
|
+
sinks: configuration.sinks,
|
|
85
|
+
tiers,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const running = (given) => {
|
|
89
|
+
const { call, cdn, duplicates, environment, page, route, sinks } = given;
|
|
90
|
+
const production = environment === 'production';
|
|
91
|
+
const development = environment === undefined || environment === 'development';
|
|
92
|
+
let file = given.file;
|
|
93
|
+
let overlay;
|
|
94
|
+
const relay = production ? undefined : sinks.relay === true ? RELAY : sinks.relay || undefined;
|
|
95
|
+
let relayed = false;
|
|
96
|
+
const sent = new Map();
|
|
97
|
+
const listeners = new Set();
|
|
98
|
+
if (given.onReport) {
|
|
99
|
+
listeners.add(given.onReport);
|
|
100
|
+
}
|
|
101
|
+
const shown = new Map();
|
|
102
|
+
let lint = given.tiers;
|
|
103
|
+
let loading = false;
|
|
104
|
+
let latest;
|
|
105
|
+
let failures = 0;
|
|
106
|
+
let active = true;
|
|
107
|
+
const deliver = (where, send) => {
|
|
108
|
+
try {
|
|
109
|
+
send();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
once(`sink ${where}`, () => console.error(`helix diagnostics: ${where} threw on a run, and the others had it.`, error));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const runNow = () => {
|
|
116
|
+
const begun = globalThis.performance?.now() ?? Date.now();
|
|
117
|
+
if (!active) {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
if (!lint) {
|
|
121
|
+
load();
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
let path;
|
|
125
|
+
try {
|
|
126
|
+
path = route?.();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
path = undefined;
|
|
130
|
+
}
|
|
131
|
+
let run;
|
|
132
|
+
try {
|
|
133
|
+
run = diagnose({
|
|
134
|
+
...call,
|
|
135
|
+
duplicates,
|
|
136
|
+
environment,
|
|
137
|
+
file,
|
|
138
|
+
from: page,
|
|
139
|
+
page: path,
|
|
140
|
+
tiers: lint,
|
|
141
|
+
watcher,
|
|
142
|
+
});
|
|
143
|
+
failures = 0;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
failures += 1;
|
|
147
|
+
once(`run ${messageOf(error)}`, () => console.error('helix diagnostics: a run failed.', error));
|
|
148
|
+
if (failures >= FAILURES) {
|
|
149
|
+
once('stopped', () => console.error(`helix diagnostics: stopped after ${FAILURES} failed runs in a row, and the page goes on as usual.`));
|
|
150
|
+
stop();
|
|
151
|
+
}
|
|
152
|
+
return (globalThis.performance?.now() ?? Date.now()) - begun;
|
|
153
|
+
}
|
|
154
|
+
latest = run;
|
|
155
|
+
const key = run.run.page ?? '';
|
|
156
|
+
const seen = shown.get(key) ?? new Set();
|
|
157
|
+
shown.set(key, seen);
|
|
158
|
+
if (!production) {
|
|
159
|
+
deliver('the console', () => print(run, console, { shown: seen }));
|
|
160
|
+
}
|
|
161
|
+
for (const listener of listeners) {
|
|
162
|
+
deliver('a subscriber', () => listener(run));
|
|
163
|
+
}
|
|
164
|
+
deliver('the helix-diagnostics event', () => page.dispatchEvent(new CustomEvent('helix-diagnostics', { detail: run })));
|
|
165
|
+
if (sinks.overlay && development) {
|
|
166
|
+
deliver('the overlay', () => {
|
|
167
|
+
if (overlay || run.findings.length > 0) {
|
|
168
|
+
overlay ??= overlayOn(page);
|
|
169
|
+
overlay.show(run);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (relay && relayed) {
|
|
174
|
+
deliver('the relay', () => {
|
|
175
|
+
const same = JSON.stringify([
|
|
176
|
+
run.findings.map((one) => [one.fingerprint, one.severity]),
|
|
177
|
+
run.coverage.read,
|
|
178
|
+
run.coverage.sheets,
|
|
179
|
+
run.usage.tags,
|
|
180
|
+
]);
|
|
181
|
+
if (sent.get(key) === same) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
sent.set(key, same);
|
|
185
|
+
const body = JSON.stringify(run);
|
|
186
|
+
fetch(`${relay}/runs`, {
|
|
187
|
+
body,
|
|
188
|
+
credentials: 'omit',
|
|
189
|
+
headers: { 'content-type': 'text/plain' },
|
|
190
|
+
keepalive: body.length < KEEPALIVE,
|
|
191
|
+
method: 'POST',
|
|
192
|
+
}).catch(() => {
|
|
193
|
+
relayed = false;
|
|
194
|
+
once('relay gone', () => console.warn('helix diagnostics: the relay stopped answering, so runs are no longer sent to it.'));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return run.run.duration;
|
|
199
|
+
};
|
|
200
|
+
const load = () => {
|
|
201
|
+
if (loading || cdn === false) {
|
|
202
|
+
if (cdn === false) {
|
|
203
|
+
once('no lint', () => console.warn('helix diagnostics: no lint data, since `cdn` is false and no `tiers` were given, so this page is not checked.'));
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const [version] = loadedVersions();
|
|
208
|
+
if (version === undefined) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const counts = watcher.counts();
|
|
212
|
+
const tags = [...Object.keys(counts.tags), ...Object.keys(counts.composed)];
|
|
213
|
+
const styled = tags.some((tag) => tag.startsWith('helix-') && !tag.startsWith('helix-headless-'));
|
|
214
|
+
const packages = styled
|
|
215
|
+
? ['@fusion.dev/helix', '@fusion.dev/helix-headless']
|
|
216
|
+
: ['@fusion.dev/helix-headless'];
|
|
217
|
+
loading = true;
|
|
218
|
+
Promise.all(packages.map(async (name) => {
|
|
219
|
+
const url = `${cdn}/${name}@${version}/lint.json`;
|
|
220
|
+
const response = await fetch(url, { credentials: 'omit' });
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw new Error(`${url} answered ${response.status}`);
|
|
223
|
+
}
|
|
224
|
+
const data = (await response.json());
|
|
225
|
+
if (typeof data?.tags !== 'object' || data.tags === null) {
|
|
226
|
+
throw new Error(`${url} is not lint data`);
|
|
227
|
+
}
|
|
228
|
+
return { lint: data };
|
|
229
|
+
})).then((loaded) => {
|
|
230
|
+
lint = loaded;
|
|
231
|
+
plan.poke();
|
|
232
|
+
}, (error) => {
|
|
233
|
+
once('no lint', () => console.warn(`helix diagnostics: the lint data for Helix ${version} could not be loaded (${messageOf(error)}), so this page is not checked. Pass \`tiers\` to initDiagnostics() to check it without the network.`));
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
if (relay) {
|
|
237
|
+
const ask = (path) => fetch(`${relay}${path}`, {
|
|
238
|
+
credentials: 'omit',
|
|
239
|
+
...(typeof AbortSignal.timeout === 'function' ? { signal: AbortSignal.timeout(1000) } : {}),
|
|
240
|
+
});
|
|
241
|
+
ask('/health')
|
|
242
|
+
.then(async (answer) => {
|
|
243
|
+
if (!answer.ok) {
|
|
244
|
+
throw new Error(`it answered ${answer.status}`);
|
|
245
|
+
}
|
|
246
|
+
relayed = true;
|
|
247
|
+
if (!given.file) {
|
|
248
|
+
const settings = await ask('/config');
|
|
249
|
+
if (settings.ok) {
|
|
250
|
+
const served = (await settings.json());
|
|
251
|
+
try {
|
|
252
|
+
checkConfig(served, 'file');
|
|
253
|
+
file = served;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
once(`relay settings ${messageOf(error)}`, () => console.error(`helix diagnostics: the settings the relay served were not used. ${messageOf(error)}`));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
plan.poke();
|
|
261
|
+
})
|
|
262
|
+
.catch((error) => once('no relay', () => console.warn(`helix diagnostics: no relay at ${relay} (${messageOf(error)}). Start it with \`helix diagnostics\`, or turn \`sinks.relay\` off.`)));
|
|
263
|
+
}
|
|
264
|
+
const plan = schedule(runNow);
|
|
265
|
+
const watcher = watch(page, { onRender: () => plan.poke() });
|
|
266
|
+
const poke = () => plan.poke();
|
|
267
|
+
const observer = typeof MutationObserver === 'function'
|
|
268
|
+
? new MutationObserver((records) => {
|
|
269
|
+
if (records.some((record) => [...record.addedNodes, ...record.removedNodes].some((node) => node !== overlay?.element))) {
|
|
270
|
+
poke();
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
: undefined;
|
|
274
|
+
observer?.observe(page.documentElement, { childList: true, subtree: true });
|
|
275
|
+
const navigation = globalThis.navigation;
|
|
276
|
+
if (navigation) {
|
|
277
|
+
navigation.addEventListener('navigatesuccess', poke);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
globalThis.addEventListener?.('popstate', poke);
|
|
281
|
+
globalThis.addEventListener?.('hashchange', poke);
|
|
282
|
+
}
|
|
283
|
+
if (page.readyState === 'complete') {
|
|
284
|
+
poke();
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
globalThis.addEventListener?.('load', poke, { once: true });
|
|
288
|
+
}
|
|
289
|
+
const stop = () => {
|
|
290
|
+
if (!active) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
active = false;
|
|
294
|
+
plan.stop();
|
|
295
|
+
observer?.disconnect();
|
|
296
|
+
watcher.stop();
|
|
297
|
+
navigation?.removeEventListener('navigatesuccess', poke);
|
|
298
|
+
globalThis.removeEventListener?.('popstate', poke);
|
|
299
|
+
globalThis.removeEventListener?.('hashchange', poke);
|
|
300
|
+
globalThis.removeEventListener?.('load', poke);
|
|
301
|
+
listeners.clear();
|
|
302
|
+
overlay?.remove();
|
|
303
|
+
const store = globalThis;
|
|
304
|
+
if (store[INSTANCE] === handle) {
|
|
305
|
+
delete store[INSTANCE];
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const handle = {
|
|
309
|
+
get active() {
|
|
310
|
+
return active;
|
|
311
|
+
},
|
|
312
|
+
get latest() {
|
|
313
|
+
return latest;
|
|
314
|
+
},
|
|
315
|
+
run: () => {
|
|
316
|
+
runNow();
|
|
317
|
+
return latest;
|
|
318
|
+
},
|
|
319
|
+
stop,
|
|
320
|
+
subscribe: (listener) => {
|
|
321
|
+
listeners.add(listener);
|
|
322
|
+
return () => listeners.delete(listener);
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
return handle;
|
|
326
|
+
};
|
package/dist/manifest.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ export interface ManifestSource {
|
|
|
29
29
|
/** Where the module is, which names the family: `src/button/root/button-root.ts`. */
|
|
30
30
|
path?: string;
|
|
31
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
|
+
}>;
|
|
32
44
|
cssParts?: Array<{
|
|
33
45
|
name: string;
|
|
34
46
|
}>;
|
|
@@ -39,12 +51,50 @@ export interface ManifestSource {
|
|
|
39
51
|
}>;
|
|
40
52
|
}>;
|
|
41
53
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
54
|
+
/**
|
|
55
|
+
* What diagnostics reads of one package's manifest, and nothing else: per
|
|
56
|
+
* tag, the parts, the hooks, the attributes with the values each allows, the
|
|
57
|
+
* attributes the element writes on itself, and the family.
|
|
58
|
+
*
|
|
59
|
+
* **About a seventieth of the manifest.** Measured 4 Oct 2026: 124 KB for the
|
|
60
|
+
* styled tier and 103 KB for the headless one, 18 KB and 14 KB compressed,
|
|
61
|
+
* against manifests of 8.6 MB and 7.0 MB. `analyze` writes it beside each
|
|
62
|
+
* `custom-elements.json` as `lint.json`, and every Helix package publishes
|
|
63
|
+
* it, so a page can load the data for exactly the version it runs.
|
|
64
|
+
*/
|
|
65
|
+
export interface LintData {
|
|
66
|
+
/** The package the data describes. */
|
|
67
|
+
package: string;
|
|
68
|
+
tags: Record<string, LintTag>;
|
|
69
|
+
/** The package's version, where the manifest says. */
|
|
70
|
+
version?: string | undefined;
|
|
71
|
+
}
|
|
72
|
+
/** What one tag publishes, as lint data says it. */
|
|
73
|
+
export interface LintTag {
|
|
74
|
+
/** Each attribute, with the values it allows, or `null` where it takes any. */
|
|
75
|
+
attributes: Record<string, string[] | null>;
|
|
76
|
+
/** The family, from the module's path. */
|
|
77
|
+
family?: string | undefined;
|
|
78
|
+
hooks: string[];
|
|
79
|
+
parts: string[];
|
|
80
|
+
/** The attributes the element writes on itself: one named for a read-only member. */
|
|
81
|
+
written: string[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* One installed tier: its manifest, with the package name, or its lint data,
|
|
85
|
+
* which names the package itself.
|
|
86
|
+
*/
|
|
87
|
+
export type Tier = {
|
|
44
88
|
manifest: ManifestSource;
|
|
45
89
|
/** Package name, used verbatim in a finding's message. */
|
|
46
90
|
name: string;
|
|
47
|
-
}
|
|
91
|
+
} | {
|
|
92
|
+
lint: LintData;
|
|
93
|
+
};
|
|
94
|
+
/** The lint data for a manifest: what `analyze` writes, and what `index` reads. */
|
|
95
|
+
export declare const lintOf: (manifest: ManifestSource, name: string) => LintData;
|
|
96
|
+
/** A tier's lint data, read from its lint data or derived from its manifest. */
|
|
97
|
+
export declare const lintFor: (tier: Tier) => LintData;
|
|
48
98
|
/**
|
|
49
99
|
* What one tag publishes, per tier.
|
|
50
100
|
*
|
|
@@ -52,9 +102,20 @@ export interface Tier {
|
|
|
52
102
|
* scope, and `analyze.ts` needs both in the same file.
|
|
53
103
|
*/
|
|
54
104
|
export interface Published {
|
|
105
|
+
/**
|
|
106
|
+
* Each attribute the tag takes, with the values it allows where its type
|
|
107
|
+
* is a union of string literals, and `null` where it takes any value.
|
|
108
|
+
*/
|
|
109
|
+
attributes: Map<string, Set<string> | null>;
|
|
55
110
|
/** The family, from the module's path: `button` for `src/button/root/button-root.ts`. */
|
|
56
111
|
family?: string | undefined;
|
|
57
112
|
hooks: Map<string, Set<string>>;
|
|
113
|
+
/**
|
|
114
|
+
* The attributes the element writes on itself: one named for a read-only
|
|
115
|
+
* member, as `helix-notifications` writes `unread`. Output, so never a
|
|
116
|
+
* finding when the page shows it.
|
|
117
|
+
*/
|
|
118
|
+
written: Set<string>;
|
|
58
119
|
parts: Map<string, Set<string>>;
|
|
59
120
|
}
|
|
60
121
|
/**
|
|
@@ -80,4 +141,4 @@ export declare const versionsOf: (tiers: Tier[]) => Record<string, string>;
|
|
|
80
141
|
* `--button-background-color` however close they sort, and a confident wrong
|
|
81
142
|
* suggestion costs more than none.
|
|
82
143
|
*/
|
|
83
|
-
export declare const nearest: (wrong: string, candidates: Iterable<string
|
|
144
|
+
export declare const nearest: (wrong: string, candidates: Iterable<string>, ceiling?: number) => string | undefined;
|
package/dist/manifest.js
CHANGED
|
@@ -1,46 +1,105 @@
|
|
|
1
|
+
export const lintOf = (manifest, name) => {
|
|
2
|
+
const tags = {};
|
|
3
|
+
for (const module of manifest.modules ?? []) {
|
|
4
|
+
const family = /^src\/([^/]+)\//.exec(module.path ?? '')?.[1];
|
|
5
|
+
for (const declaration of module.declarations ?? []) {
|
|
6
|
+
const tag = declaration.tagName;
|
|
7
|
+
if (!tag) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const entry = tags[tag] ?? {
|
|
11
|
+
attributes: {},
|
|
12
|
+
family,
|
|
13
|
+
hooks: [],
|
|
14
|
+
parts: [],
|
|
15
|
+
written: [],
|
|
16
|
+
};
|
|
17
|
+
tags[tag] = entry;
|
|
18
|
+
for (const member of declaration.members ?? []) {
|
|
19
|
+
if (member.readonly && !member.static && member.kind === 'field') {
|
|
20
|
+
entry.written.push(member.name, member.name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const attribute of declaration.attributes ?? []) {
|
|
24
|
+
const values = allowed(attribute.type?.text);
|
|
25
|
+
entry.attributes[attribute.name] = values ? [...values] : null;
|
|
26
|
+
}
|
|
27
|
+
entry.parts.push(...(declaration.cssParts ?? []).map((part) => part.name));
|
|
28
|
+
entry.hooks.push(...(declaration.cssProperties ?? []).map((hook) => hook.name));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const entry of Object.values(tags)) {
|
|
32
|
+
entry.written = [...new Set(entry.written)];
|
|
33
|
+
entry.parts = [...new Set(entry.parts)];
|
|
34
|
+
entry.hooks = [...new Set(entry.hooks)];
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
package: manifest.helix?.package ?? name,
|
|
38
|
+
tags,
|
|
39
|
+
...(manifest.helix?.version ? { version: manifest.helix.version } : {}),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export const lintFor = (tier) => 'lint' in tier ? tier.lint : lintOf(tier.manifest, tier.name);
|
|
1
43
|
export const index = (tiers) => {
|
|
2
44
|
const tags = new Map();
|
|
3
45
|
for (const tier of tiers) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
const lint = lintFor(tier);
|
|
47
|
+
for (const [tag, entry] of Object.entries(lint.tags)) {
|
|
48
|
+
let element = tags.get(tag);
|
|
49
|
+
if (!element) {
|
|
50
|
+
element = {
|
|
51
|
+
attributes: new Map(),
|
|
52
|
+
family: entry.family,
|
|
53
|
+
hooks: new Map(),
|
|
54
|
+
parts: new Map(),
|
|
55
|
+
written: new Set(),
|
|
56
|
+
};
|
|
57
|
+
tags.set(tag, element);
|
|
58
|
+
}
|
|
59
|
+
for (const name of entry.written) {
|
|
60
|
+
element.written.add(name);
|
|
61
|
+
}
|
|
62
|
+
for (const [name, values] of Object.entries(entry.attributes)) {
|
|
63
|
+
element.attributes.set(name, values ? new Set(values) : null);
|
|
64
|
+
}
|
|
65
|
+
for (const [names, into] of [
|
|
66
|
+
[entry.parts, element.parts],
|
|
67
|
+
[entry.hooks, element.hooks],
|
|
68
|
+
]) {
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const publishers = into.get(name) ?? new Set();
|
|
71
|
+
publishers.add(lint.package);
|
|
72
|
+
into.set(name, publishers);
|
|
25
73
|
}
|
|
26
74
|
}
|
|
27
75
|
}
|
|
28
76
|
}
|
|
29
77
|
return tags;
|
|
30
78
|
};
|
|
79
|
+
const allowed = (type) => {
|
|
80
|
+
if (!type) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const literals = [...type.matchAll(/'([^']*)'/g)].map((match) => match[1]);
|
|
84
|
+
const rest = type
|
|
85
|
+
.replace(/'[^']*'/g, '')
|
|
86
|
+
.replace(/\b(undefined|null)\b/g, '')
|
|
87
|
+
.replace(/[|\s]/g, '');
|
|
88
|
+
return literals.length > 0 && rest === '' ? new Set(literals) : null;
|
|
89
|
+
};
|
|
31
90
|
export const versionsOf = (tiers) => {
|
|
32
91
|
const versions = {};
|
|
33
92
|
for (const tier of tiers) {
|
|
34
|
-
const
|
|
35
|
-
if (version) {
|
|
36
|
-
versions[
|
|
93
|
+
const lint = lintFor(tier);
|
|
94
|
+
if (lint.version) {
|
|
95
|
+
versions[lint.package] = lint.version;
|
|
37
96
|
}
|
|
38
97
|
}
|
|
39
98
|
return versions;
|
|
40
99
|
};
|
|
41
|
-
export const nearest = (wrong, candidates) => {
|
|
100
|
+
export const nearest = (wrong, candidates, ceiling = Math.floor(wrong.length / 3)) => {
|
|
42
101
|
let best;
|
|
43
|
-
let distance = Math.floor(wrong.length / 3);
|
|
102
|
+
let distance = Math.min(ceiling, Math.floor(wrong.length / 3));
|
|
44
103
|
for (const candidate of candidates) {
|
|
45
104
|
const found = within(wrong, candidate, distance);
|
|
46
105
|
if (found !== undefined && found < distance + 1) {
|
|
@@ -57,8 +116,14 @@ const within = (from, to, ceiling) => {
|
|
|
57
116
|
let previous = Array.from({ length: to.length + 1 }, (_, column) => column);
|
|
58
117
|
for (let row = 1; row <= from.length; row += 1) {
|
|
59
118
|
const current = [row];
|
|
119
|
+
let least = row;
|
|
60
120
|
for (let column = 1; column <= to.length; column += 1) {
|
|
61
|
-
|
|
121
|
+
const cell = Math.min((previous[column] ?? 0) + 1, (current[column - 1] ?? 0) + 1, (previous[column - 1] ?? 0) + (from[row - 1] === to[column - 1] ? 0 : 1));
|
|
122
|
+
current[column] = cell;
|
|
123
|
+
least = Math.min(least, cell);
|
|
124
|
+
}
|
|
125
|
+
if (least > ceiling) {
|
|
126
|
+
return undefined;
|
|
62
127
|
}
|
|
63
128
|
previous = current;
|
|
64
129
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Report } from './report.js';
|
|
2
|
+
/** What a running instance does with the overlay. */
|
|
3
|
+
export interface Overlay {
|
|
4
|
+
/** The element, so a change it makes to the page is known for its own. */
|
|
5
|
+
readonly element: HTMLElement;
|
|
6
|
+
/** Removes it from the page. */
|
|
7
|
+
remove(): void;
|
|
8
|
+
/** Shows a run: the counts on the badge and the findings in the panel. */
|
|
9
|
+
show(run: Report): void;
|
|
10
|
+
}
|
|
11
|
+
/** Puts the overlay on `page`. */
|
|
12
|
+
export declare const overlayOn: (page: Document) => Overlay;
|