@fusion.dev/helix-diagnostics 0.138.0 → 0.140.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 +210 -34
- package/dist/analyze.d.ts +14 -3
- package/dist/analyze.js +76 -86
- package/dist/ci.js +14 -0
- package/dist/collect.d.ts +2 -1
- package/dist/collect.js +3 -15
- package/dist/config.d.ts +24 -0
- package/dist/config.js +42 -12
- package/dist/console.d.ts +11 -1
- package/dist/console.js +46 -5
- package/dist/diagnose.d.ts +75 -0
- package/dist/diagnose.js +93 -0
- package/dist/identity.d.ts +6 -0
- package/dist/identity.js +1 -0
- package/dist/index.d.ts +5 -77
- package/dist/index.js +4 -74
- package/dist/init.d.ts +87 -0
- package/dist/init.js +337 -0
- package/dist/manifest.d.ts +41 -3
- package/dist/manifest.js +79 -40
- package/dist/overlay.d.ts +12 -0
- package/dist/overlay.js +300 -0
- package/dist/page.d.ts +25 -0
- package/dist/page.js +21 -0
- package/dist/performance.d.ts +34 -0
- package/dist/performance.js +121 -0
- package/dist/report.d.ts +86 -0
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.js +39 -0
- package/dist/schedule.d.ts +57 -0
- package/dist/schedule.js +84 -0
- package/dist/usage.d.ts +34 -4
- package/dist/usage.js +38 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,82 +1,12 @@
|
|
|
1
1
|
export { collect } from './collect.js';
|
|
2
2
|
export { DEFAULTS, pagePattern } from './config.js';
|
|
3
3
|
export { report } from './console.js';
|
|
4
|
+
export { diagnose } from './diagnose.js';
|
|
4
5
|
export { fingerprintOf, normalize } from './identity.js';
|
|
5
|
-
export {
|
|
6
|
+
export { INSTANCE, initDiagnostics } from './init.js';
|
|
7
|
+
export { index, lintOf, nearest } from './manifest.js';
|
|
6
8
|
export { CATEGORIES, KINDS } from './report.js';
|
|
7
9
|
export { SHIPPED_RULES } from './rules.js';
|
|
10
|
+
export { TIMING } from './schedule.js';
|
|
8
11
|
export { watch } from './usage.js';
|
|
9
12
|
export { VERSION } from './version.js';
|
|
10
|
-
import { analyze, markup, registrations, skew, versionSkew } from './analyze.js';
|
|
11
|
-
import { collect } from './collect.js';
|
|
12
|
-
import { pagePattern, resolveConfig } from './config.js';
|
|
13
|
-
import { identify } from './identity.js';
|
|
14
|
-
import { index, versionsOf } from './manifest.js';
|
|
15
|
-
import { applyRules, overridesOf } from './rules.js';
|
|
16
|
-
import { listed, loadedVersions, styled, walk } from './usage.js';
|
|
17
|
-
import { VERSION } from './version.js';
|
|
18
|
-
const runId = () => globalThis.crypto?.randomUUID?.() ??
|
|
19
|
-
`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
|
|
20
|
-
export const diagnose = (options) => {
|
|
21
|
-
const started = performance.now();
|
|
22
|
-
const at = new Date().toISOString();
|
|
23
|
-
const { duplicates, environment, file, from, page, tiers, watcher, ...call } = options;
|
|
24
|
-
if (tiers.length === 0) {
|
|
25
|
-
throw new Error('diagnose() needs at least one manifest. Without one every part name is unknown and every finding would be noise.');
|
|
26
|
-
}
|
|
27
|
-
const resolved = resolveConfig(file, call);
|
|
28
|
-
const { detail } = resolved.configuration;
|
|
29
|
-
const document_ = from ?? globalThis.document;
|
|
30
|
-
const { answered, coverage, readers, rules: located, used } = collect(document_);
|
|
31
|
-
const tags = index(tiers);
|
|
32
|
-
const counted = watcher ? watcher.counts() : walk(document_);
|
|
33
|
-
const seen = loadedVersions();
|
|
34
|
-
const manifests = versionsOf(tiers);
|
|
35
|
-
const byVersion = versionSkew(manifests, seen);
|
|
36
|
-
const known = seen.length > 0 && Object.keys(manifests).length > 0;
|
|
37
|
-
const skewed = applyRules((byVersion.length > 0 ? byVersion : known ? [] : skew(tags, document_)).map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
38
|
-
const judged = skewed.findings.length > 0
|
|
39
|
-
? { findings: [], silenced: [] }
|
|
40
|
-
: applyRules([
|
|
41
|
-
...analyze(located, tags, document_, answered, used, readers),
|
|
42
|
-
...markup(tags, document_),
|
|
43
|
-
...registrations(document_, duplicates),
|
|
44
|
-
].map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
45
|
-
const path = page ?? globalThis.location?.pathname;
|
|
46
|
-
const shownPage = path === undefined || detail.pages === 'none'
|
|
47
|
-
? undefined
|
|
48
|
-
: detail.pages === 'patterns'
|
|
49
|
-
? pagePattern(path)
|
|
50
|
-
: path;
|
|
51
|
-
return {
|
|
52
|
-
configuration: {
|
|
53
|
-
...resolved.configuration,
|
|
54
|
-
overrides: overridesOf(file?.rules, call.rules),
|
|
55
|
-
silenced: [...skewed.silenced, ...judged.silenced],
|
|
56
|
-
},
|
|
57
|
-
coverage,
|
|
58
|
-
findings: [...skewed.findings, ...judged.findings],
|
|
59
|
-
project: resolved.project,
|
|
60
|
-
run: {
|
|
61
|
-
at,
|
|
62
|
-
duration: Math.round(performance.now() - started),
|
|
63
|
-
id: runId(),
|
|
64
|
-
source: 'page',
|
|
65
|
-
...(environment === undefined ? {} : { environment }),
|
|
66
|
-
...(shownPage === undefined ? {} : { page: shownPage }),
|
|
67
|
-
},
|
|
68
|
-
schema: 1,
|
|
69
|
-
usage: {
|
|
70
|
-
...listed(counted, tags),
|
|
71
|
-
...styled(located, tags, answered, detail.hookValues),
|
|
72
|
-
counted: watcher ? 'watched' : 'walked',
|
|
73
|
-
},
|
|
74
|
-
versions: { collector: VERSION, helix: seen, manifests, rules: VERSION },
|
|
75
|
-
withheld: Object.entries(resolved.send)
|
|
76
|
-
.filter(([, sent]) => !sent.on)
|
|
77
|
-
.map(([category, sent]) => ({
|
|
78
|
-
category: category,
|
|
79
|
-
decidedBy: sent.decidedBy,
|
|
80
|
-
})),
|
|
81
|
-
};
|
|
82
|
-
};
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One call, in any framework: `initDiagnostics()`.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { initDiagnostics } from '@fusion.dev/helix-diagnostics'
|
|
6
|
+
*
|
|
7
|
+
* initDiagnostics({ environment: import.meta.env.MODE })
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* **Once, at the app's entry, before the app mounts**, in whatever builds it:
|
|
11
|
+
* `main.ts` in Vite, Vue, Svelte, or Solid, before `bootstrapApplication` in
|
|
12
|
+
* Angular, `instrumentation-client.ts` in Next.js, or a module script with no
|
|
13
|
+
* bundler. It needs nothing from a framework: it reads the DOM, hears
|
|
14
|
+
* elements render through core's listener, and hears navigation from the
|
|
15
|
+
* browser.
|
|
16
|
+
*
|
|
17
|
+
* **It never breaks the app.** It never throws; a setting that names nothing
|
|
18
|
+
* is printed with the closest setting, and the handle it returns does
|
|
19
|
+
* nothing. A run that throws is printed, and three in a row stop it. Each
|
|
20
|
+
* place a run goes is isolated from the others.
|
|
21
|
+
*
|
|
22
|
+
* **It never runs in production by accident.** With no environment declared
|
|
23
|
+
* it runs only on a local host, since each framework says development its
|
|
24
|
+
* own way. Production runs only with `environment: 'production'` and a sample
|
|
25
|
+
* rate.
|
|
26
|
+
*/
|
|
27
|
+
import { type Config } from './config.js';
|
|
28
|
+
import type { Tier } from './manifest.js';
|
|
29
|
+
import type { Environment, Report } from './report.js';
|
|
30
|
+
/** What `initDiagnostics()` takes: the settings, and where the run comes from. */
|
|
31
|
+
export interface InitOptions extends Config {
|
|
32
|
+
/**
|
|
33
|
+
* Where lint data is loaded from, where `tiers` is not given: a base the
|
|
34
|
+
* package and version follow, as
|
|
35
|
+
* `https://cdn.jsdelivr.net/npm/@fusion.dev/helix@0.138.0/lint.json`.
|
|
36
|
+
* `false` loads nothing. jsDelivr by default.
|
|
37
|
+
*/
|
|
38
|
+
cdn?: string | false | undefined;
|
|
39
|
+
/** `duplicateRegistrations` from `@fusion.dev/helix-core`, as `diagnose` takes it. */
|
|
40
|
+
duplicates?: (() => string[]) | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Where the page runs, as the project declares it. Undeclared, it runs only
|
|
43
|
+
* on a local host: `localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, and
|
|
44
|
+
* `*.test`.
|
|
45
|
+
*/
|
|
46
|
+
environment?: Environment | undefined;
|
|
47
|
+
/** The project's own file, `helix-diagnostics.json`, parsed. */
|
|
48
|
+
file?: Config | undefined;
|
|
49
|
+
/** Called with each run, after the console has it. */
|
|
50
|
+
onReport?: ((run: Report) => void) | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The page's path as the app's router names it, such as `/products/[id]`,
|
|
53
|
+
* asked at each run. The location, made a pattern, otherwise.
|
|
54
|
+
*/
|
|
55
|
+
page?: (() => string | undefined) | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The lint data or manifests to check against. Loaded from `cdn`, for the
|
|
58
|
+
* Helix version core lists on the page, where not given.
|
|
59
|
+
*/
|
|
60
|
+
tiers?: Tier[] | undefined;
|
|
61
|
+
}
|
|
62
|
+
/** What `initDiagnostics()` returns. */
|
|
63
|
+
export interface Diagnostics {
|
|
64
|
+
/** Whether it is running: false where it declined to start, and after `stop()`. */
|
|
65
|
+
readonly active: boolean;
|
|
66
|
+
/** The last run, once there is one. */
|
|
67
|
+
readonly latest: Report | undefined;
|
|
68
|
+
/** Runs now, outside the schedule, and returns the run, where it can run yet. */
|
|
69
|
+
run(): Report | undefined;
|
|
70
|
+
/** Calls `listener` with every run from now on, and returns its removal. */
|
|
71
|
+
subscribe(listener: (run: Report) => void): () => void;
|
|
72
|
+
/** Stops it: nothing more runs. A later `initDiagnostics()` starts afresh. */
|
|
73
|
+
stop(): void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The key the running instance is kept under: a second call returns it, as
|
|
77
|
+
* React's strict mode, hot module replacement, and a component mounting twice
|
|
78
|
+
* each make, and an end-to-end test reads it to assert a page has no findings.
|
|
79
|
+
*/
|
|
80
|
+
export declare const INSTANCE: unique symbol;
|
|
81
|
+
/**
|
|
82
|
+
* Starts diagnostics on this page, or returns the handle it already has.
|
|
83
|
+
*
|
|
84
|
+
* Never throws. On a server, with no `document`, it returns a handle that
|
|
85
|
+
* does nothing, so it is safe in code server-side rendering also runs.
|
|
86
|
+
*/
|
|
87
|
+
export declare const initDiagnostics: (options?: InitOptions) => Diagnostics;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
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 { shownRuntime } from './runtime.js';
|
|
6
|
+
import { schedule } from './schedule.js';
|
|
7
|
+
import { loadedVersions, watch } from './usage.js';
|
|
8
|
+
export const INSTANCE = Symbol.for('fusion.dev/helix/diagnostics');
|
|
9
|
+
const CDN = 'https://cdn.jsdelivr.net/npm';
|
|
10
|
+
const RELAY = 'http://127.0.0.1:7417';
|
|
11
|
+
const KEEPALIVE = 60_000;
|
|
12
|
+
const FAILURES = 3;
|
|
13
|
+
const inert = () => ({
|
|
14
|
+
active: false,
|
|
15
|
+
latest: undefined,
|
|
16
|
+
run: () => undefined,
|
|
17
|
+
stop: () => { },
|
|
18
|
+
subscribe: () => () => { },
|
|
19
|
+
});
|
|
20
|
+
const said = new Set();
|
|
21
|
+
const once = (key, print) => {
|
|
22
|
+
if (said.has(key)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
said.add(key);
|
|
26
|
+
try {
|
|
27
|
+
print();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
33
|
+
export const initDiagnostics = (options = {}) => {
|
|
34
|
+
const page = globalThis.document;
|
|
35
|
+
if (page === undefined) {
|
|
36
|
+
return inert();
|
|
37
|
+
}
|
|
38
|
+
const store = globalThis;
|
|
39
|
+
const existing = store[INSTANCE];
|
|
40
|
+
if (existing?.active) {
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const started = start(options, page);
|
|
45
|
+
if (started.active) {
|
|
46
|
+
store[INSTANCE] = started;
|
|
47
|
+
}
|
|
48
|
+
return started;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
once(`start ${messageOf(error)}`, () => console.error('helix diagnostics: not started, and the page goes on as usual.', error));
|
|
52
|
+
return inert();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const start = (options, page) => {
|
|
56
|
+
const { cdn = CDN, duplicates, environment, file, onReport, page: route, tiers, ...call } = options;
|
|
57
|
+
let resolved;
|
|
58
|
+
try {
|
|
59
|
+
resolved = resolveConfig(file, call);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
once(`settings ${messageOf(error)}`, () => console.error(`helix diagnostics: not started. ${messageOf(error)}`));
|
|
63
|
+
return inert();
|
|
64
|
+
}
|
|
65
|
+
const { configuration } = resolved;
|
|
66
|
+
const host = globalThis.location?.hostname ?? '';
|
|
67
|
+
if (environment === undefined && !LOCAL.test(host)) {
|
|
68
|
+
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.`));
|
|
69
|
+
return inert();
|
|
70
|
+
}
|
|
71
|
+
if (environment === 'production') {
|
|
72
|
+
const production = configuration.environments.production;
|
|
73
|
+
if (production === false || Math.random() >= production.sample) {
|
|
74
|
+
return inert();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return running({
|
|
78
|
+
call,
|
|
79
|
+
cdn,
|
|
80
|
+
duplicates,
|
|
81
|
+
environment,
|
|
82
|
+
file,
|
|
83
|
+
measure: resolved.send.performance.on || configuration.console.performance !== 'off',
|
|
84
|
+
onReport,
|
|
85
|
+
page,
|
|
86
|
+
route,
|
|
87
|
+
sinks: configuration.sinks,
|
|
88
|
+
tiers,
|
|
89
|
+
warnings: configuration.core.warnings,
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
const running = (given) => {
|
|
93
|
+
const { call, cdn, duplicates, environment, page, route, sinks } = given;
|
|
94
|
+
const production = environment === 'production';
|
|
95
|
+
const development = environment === undefined || environment === 'development';
|
|
96
|
+
let file = given.file;
|
|
97
|
+
let warnings = given.warnings;
|
|
98
|
+
let overlay;
|
|
99
|
+
const relay = production ? undefined : sinks.relay === true ? RELAY : sinks.relay || undefined;
|
|
100
|
+
let relayed = false;
|
|
101
|
+
const sent = new Map();
|
|
102
|
+
const listeners = new Set();
|
|
103
|
+
if (given.onReport) {
|
|
104
|
+
listeners.add(given.onReport);
|
|
105
|
+
}
|
|
106
|
+
const shown = new Map();
|
|
107
|
+
let lint = given.tiers;
|
|
108
|
+
let loading = false;
|
|
109
|
+
let latest;
|
|
110
|
+
let failures = 0;
|
|
111
|
+
let active = true;
|
|
112
|
+
const deliver = (where, send) => {
|
|
113
|
+
try {
|
|
114
|
+
send();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
once(`sink ${where}`, () => console.error(`helix diagnostics: ${where} threw on a run, and the others had it.`, error));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const runNow = () => {
|
|
121
|
+
const begun = globalThis.performance?.now() ?? Date.now();
|
|
122
|
+
if (!active) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (!lint) {
|
|
126
|
+
load();
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
let path;
|
|
130
|
+
try {
|
|
131
|
+
path = route?.();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
path = undefined;
|
|
135
|
+
}
|
|
136
|
+
let run;
|
|
137
|
+
try {
|
|
138
|
+
run = diagnose({
|
|
139
|
+
...call,
|
|
140
|
+
duplicates,
|
|
141
|
+
environment,
|
|
142
|
+
file,
|
|
143
|
+
from: page,
|
|
144
|
+
page: path,
|
|
145
|
+
tiers: lint,
|
|
146
|
+
watcher,
|
|
147
|
+
});
|
|
148
|
+
failures = 0;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
failures += 1;
|
|
152
|
+
once(`run ${messageOf(error)}`, () => console.error('helix diagnostics: a run failed.', error));
|
|
153
|
+
if (failures >= FAILURES) {
|
|
154
|
+
once('stopped', () => console.error(`helix diagnostics: stopped after ${FAILURES} failed runs in a row, and the page goes on as usual.`));
|
|
155
|
+
stop();
|
|
156
|
+
}
|
|
157
|
+
return (globalThis.performance?.now() ?? Date.now()) - begun;
|
|
158
|
+
}
|
|
159
|
+
latest = run;
|
|
160
|
+
const key = run.run.page ?? '';
|
|
161
|
+
const seen = shown.get(key) ?? new Set();
|
|
162
|
+
shown.set(key, seen);
|
|
163
|
+
if (!production) {
|
|
164
|
+
deliver('the console', () => print(run, console, { shown: seen }));
|
|
165
|
+
}
|
|
166
|
+
for (const listener of listeners) {
|
|
167
|
+
deliver('a subscriber', () => listener(run));
|
|
168
|
+
}
|
|
169
|
+
deliver('the helix-diagnostics event', () => page.dispatchEvent(new CustomEvent('helix-diagnostics', { detail: run })));
|
|
170
|
+
if (sinks.overlay && development) {
|
|
171
|
+
deliver('the overlay', () => {
|
|
172
|
+
if (overlay || run.findings.length > 0 || shownRuntime(run.runtime).length > 0) {
|
|
173
|
+
overlay ??= overlayOn(page);
|
|
174
|
+
overlay.show(run);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (relay && relayed) {
|
|
179
|
+
deliver('the relay', () => {
|
|
180
|
+
const same = JSON.stringify([
|
|
181
|
+
run.findings.map((one) => [one.fingerprint, one.severity]),
|
|
182
|
+
run.coverage.read,
|
|
183
|
+
run.coverage.sheets,
|
|
184
|
+
run.usage.tags,
|
|
185
|
+
]);
|
|
186
|
+
if (sent.get(key) === same) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
sent.set(key, same);
|
|
190
|
+
const body = JSON.stringify(run);
|
|
191
|
+
fetch(`${relay}/runs`, {
|
|
192
|
+
body,
|
|
193
|
+
credentials: 'omit',
|
|
194
|
+
headers: { 'content-type': 'text/plain' },
|
|
195
|
+
keepalive: body.length < KEEPALIVE,
|
|
196
|
+
method: 'POST',
|
|
197
|
+
}).catch(() => {
|
|
198
|
+
relayed = false;
|
|
199
|
+
once('relay gone', () => console.warn('helix diagnostics: the relay stopped answering, so runs are no longer sent to it.'));
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return run.run.duration;
|
|
204
|
+
};
|
|
205
|
+
const load = () => {
|
|
206
|
+
if (loading || cdn === false) {
|
|
207
|
+
if (cdn === false) {
|
|
208
|
+
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.'));
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const [version] = loadedVersions();
|
|
213
|
+
if (version === undefined) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const counts = watcher.counts();
|
|
217
|
+
const tags = [...Object.keys(counts.tags), ...Object.keys(counts.composed)];
|
|
218
|
+
const styled = tags.some((tag) => tag.startsWith('helix-') && !tag.startsWith('helix-headless-'));
|
|
219
|
+
const packages = styled
|
|
220
|
+
? ['@fusion.dev/helix', '@fusion.dev/helix-headless']
|
|
221
|
+
: ['@fusion.dev/helix-headless'];
|
|
222
|
+
loading = true;
|
|
223
|
+
Promise.all(packages.map(async (name) => {
|
|
224
|
+
const url = `${cdn}/${name}@${version}/lint.json`;
|
|
225
|
+
const response = await fetch(url, { credentials: 'omit' });
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
throw new Error(`${url} answered ${response.status}`);
|
|
228
|
+
}
|
|
229
|
+
const data = (await response.json());
|
|
230
|
+
if (typeof data?.tags !== 'object' || data.tags === null) {
|
|
231
|
+
throw new Error(`${url} is not lint data`);
|
|
232
|
+
}
|
|
233
|
+
return { lint: data };
|
|
234
|
+
})).then((loaded) => {
|
|
235
|
+
lint = loaded;
|
|
236
|
+
plan.poke();
|
|
237
|
+
}, (error) => {
|
|
238
|
+
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.`));
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
if (relay) {
|
|
242
|
+
const ask = (path) => fetch(`${relay}${path}`, {
|
|
243
|
+
credentials: 'omit',
|
|
244
|
+
...(typeof AbortSignal.timeout === 'function' ? { signal: AbortSignal.timeout(1000) } : {}),
|
|
245
|
+
});
|
|
246
|
+
ask('/health')
|
|
247
|
+
.then(async (answer) => {
|
|
248
|
+
if (!answer.ok) {
|
|
249
|
+
throw new Error(`it answered ${answer.status}`);
|
|
250
|
+
}
|
|
251
|
+
relayed = true;
|
|
252
|
+
if (!given.file) {
|
|
253
|
+
const settings = await ask('/config');
|
|
254
|
+
if (settings.ok) {
|
|
255
|
+
const served = (await settings.json());
|
|
256
|
+
try {
|
|
257
|
+
checkConfig(served, 'file');
|
|
258
|
+
file = served;
|
|
259
|
+
warnings = resolveConfig(file, call).configuration.core.warnings;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
once(`relay settings ${messageOf(error)}`, () => console.error(`helix diagnostics: the settings the relay served were not used. ${messageOf(error)}`));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
plan.poke();
|
|
267
|
+
})
|
|
268
|
+
.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.`)));
|
|
269
|
+
}
|
|
270
|
+
const plan = schedule(runNow);
|
|
271
|
+
const watcher = watch(page, {
|
|
272
|
+
onRender: () => plan.poke(),
|
|
273
|
+
onRuntime: () => plan.poke(),
|
|
274
|
+
performance: given.measure,
|
|
275
|
+
quiet: () => !warnings,
|
|
276
|
+
});
|
|
277
|
+
const poke = () => plan.poke();
|
|
278
|
+
const observer = typeof MutationObserver === 'function'
|
|
279
|
+
? new MutationObserver((records) => {
|
|
280
|
+
if (records.some((record) => [...record.addedNodes, ...record.removedNodes].some((node) => node !== overlay?.element))) {
|
|
281
|
+
poke();
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
: undefined;
|
|
285
|
+
observer?.observe(page.documentElement, { childList: true, subtree: true });
|
|
286
|
+
const navigation = globalThis.navigation;
|
|
287
|
+
if (navigation) {
|
|
288
|
+
navigation.addEventListener('navigatesuccess', poke);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
globalThis.addEventListener?.('popstate', poke);
|
|
292
|
+
globalThis.addEventListener?.('hashchange', poke);
|
|
293
|
+
}
|
|
294
|
+
if (page.readyState === 'complete') {
|
|
295
|
+
poke();
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
globalThis.addEventListener?.('load', poke, { once: true });
|
|
299
|
+
}
|
|
300
|
+
const stop = () => {
|
|
301
|
+
if (!active) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
active = false;
|
|
305
|
+
plan.stop();
|
|
306
|
+
observer?.disconnect();
|
|
307
|
+
watcher.stop();
|
|
308
|
+
navigation?.removeEventListener('navigatesuccess', poke);
|
|
309
|
+
globalThis.removeEventListener?.('popstate', poke);
|
|
310
|
+
globalThis.removeEventListener?.('hashchange', poke);
|
|
311
|
+
globalThis.removeEventListener?.('load', poke);
|
|
312
|
+
listeners.clear();
|
|
313
|
+
overlay?.remove();
|
|
314
|
+
const store = globalThis;
|
|
315
|
+
if (store[INSTANCE] === handle) {
|
|
316
|
+
delete store[INSTANCE];
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const handle = {
|
|
320
|
+
get active() {
|
|
321
|
+
return active;
|
|
322
|
+
},
|
|
323
|
+
get latest() {
|
|
324
|
+
return latest;
|
|
325
|
+
},
|
|
326
|
+
run: () => {
|
|
327
|
+
runNow();
|
|
328
|
+
return latest;
|
|
329
|
+
},
|
|
330
|
+
stop,
|
|
331
|
+
subscribe: (listener) => {
|
|
332
|
+
listeners.add(listener);
|
|
333
|
+
return () => listeners.delete(listener);
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
return handle;
|
|
337
|
+
};
|
package/dist/manifest.d.ts
CHANGED
|
@@ -51,12 +51,50 @@ export interface ManifestSource {
|
|
|
51
51
|
}>;
|
|
52
52
|
}>;
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
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 = {
|
|
56
88
|
manifest: ManifestSource;
|
|
57
89
|
/** Package name, used verbatim in a finding's message. */
|
|
58
90
|
name: string;
|
|
59
|
-
}
|
|
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;
|
|
60
98
|
/**
|
|
61
99
|
* What one tag publishes, per tier.
|
|
62
100
|
*
|