@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/collect.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OVERLAY, pageOf } from './page.js';
|
|
1
2
|
const name = (sheet, seen, host) => {
|
|
2
3
|
if (sheet.href) {
|
|
3
4
|
return sheet.href;
|
|
@@ -62,19 +63,7 @@ const read = (sheet, into, unreadable, labels, depth = 0, host, reads) => {
|
|
|
62
63
|
rules(list, label, into, depth, reads);
|
|
63
64
|
return true;
|
|
64
65
|
};
|
|
65
|
-
const
|
|
66
|
-
if (depth > 32) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
for (const element of from.querySelectorAll('*')) {
|
|
70
|
-
const root = element.shadowRoot;
|
|
71
|
-
if (root) {
|
|
72
|
-
found.push(root);
|
|
73
|
-
roots(root, found, depth + 1);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
export const collect = (from = document) => {
|
|
66
|
+
export const collect = (from = document, page = pageOf(from)) => {
|
|
78
67
|
const located = [];
|
|
79
68
|
const unreadable = [];
|
|
80
69
|
const seen = new Set();
|
|
@@ -119,8 +108,7 @@ export const collect = (from = document) => {
|
|
|
119
108
|
for (const sheet of from.adoptedStyleSheets ?? []) {
|
|
120
109
|
take(sheet);
|
|
121
110
|
}
|
|
122
|
-
const shadows =
|
|
123
|
-
roots(from, shadows);
|
|
111
|
+
const shadows = page.roots.filter((root) => root.host.localName !== OVERLAY);
|
|
124
112
|
for (const root of shadows) {
|
|
125
113
|
for (const sheet of Array.from(root.styleSheets ?? [])) {
|
|
126
114
|
take(sheet, root.host);
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { type Rules } from './rules.js';
|
|
|
4
4
|
export interface Config {
|
|
5
5
|
/** The lowest level each category prints at in the console, or `off`. */
|
|
6
6
|
console?: Partial<Configuration['console']>;
|
|
7
|
+
/** What `@fusion.dev/helix-core` does itself: whether it prints its runtime warnings. */
|
|
8
|
+
core?: Partial<Configuration['core']>;
|
|
7
9
|
/** How much detail goes with what is sent. */
|
|
8
10
|
detail?: Partial<Configuration['detail']>;
|
|
9
11
|
/** Where sending happens. Production takes `{ sample }`, from 0 to 1. */
|
|
@@ -21,14 +23,25 @@ export interface Config {
|
|
|
21
23
|
rules?: Rules;
|
|
22
24
|
/** Each category, sent or withheld. Nothing is sent without a key. */
|
|
23
25
|
send?: Partial<Record<Category, boolean>>;
|
|
26
|
+
/**
|
|
27
|
+
* Where `initDiagnostics()` hands each run, beside the console: the
|
|
28
|
+
* `overlay` on the page, and the `relay`, `helix diagnostics`, on or off or
|
|
29
|
+
* at an address of its own.
|
|
30
|
+
*/
|
|
31
|
+
sinks?: Partial<Configuration['sinks']>;
|
|
24
32
|
}
|
|
25
33
|
/** The shipped defaults. */
|
|
26
34
|
export declare const DEFAULTS: {
|
|
27
35
|
readonly console: {
|
|
28
36
|
readonly coverage: 'info';
|
|
29
37
|
readonly findings: 'info';
|
|
38
|
+
readonly performance: 'off';
|
|
39
|
+
readonly runtime: 'warn';
|
|
30
40
|
readonly usage: 'debug';
|
|
31
41
|
};
|
|
42
|
+
readonly core: {
|
|
43
|
+
readonly warnings: true;
|
|
44
|
+
};
|
|
32
45
|
readonly detail: {
|
|
33
46
|
readonly evidence: 'full';
|
|
34
47
|
readonly hookValues: true;
|
|
@@ -48,7 +61,18 @@ export declare const DEFAULTS: {
|
|
|
48
61
|
readonly runtime: true;
|
|
49
62
|
readonly usage: true;
|
|
50
63
|
};
|
|
64
|
+
readonly sinks: {
|
|
65
|
+
readonly overlay: true;
|
|
66
|
+
readonly relay: false;
|
|
67
|
+
};
|
|
51
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* A local host: `localhost`, `127.0.0.1`, `[::1]`, `*.localhost`, and `*.test`.
|
|
71
|
+
* A page with no declared environment runs only on one, and the relay is only
|
|
72
|
+
* ever at one, as `helix diagnostics` takes runs only from one: a relay at
|
|
73
|
+
* another address would be a second way to send, with no key and no `send`.
|
|
74
|
+
*/
|
|
75
|
+
export declare const LOCAL: RegExp;
|
|
52
76
|
/** Throws on a setting that names nothing, or a value it cannot take. */
|
|
53
77
|
export declare const checkConfig: (config: Config | undefined, layer: Exclude<Layer, 'shipped'>) => void;
|
|
54
78
|
/** The settings in effect, from the defaults, the file, and the call. */
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,14 @@ import { nearest } from './manifest.js';
|
|
|
2
2
|
import { CATEGORIES } from './report.js';
|
|
3
3
|
import { checkRules } from './rules.js';
|
|
4
4
|
export const DEFAULTS = {
|
|
5
|
-
console: {
|
|
5
|
+
console: {
|
|
6
|
+
coverage: 'info',
|
|
7
|
+
findings: 'info',
|
|
8
|
+
performance: 'off',
|
|
9
|
+
runtime: 'warn',
|
|
10
|
+
usage: 'debug',
|
|
11
|
+
},
|
|
12
|
+
core: { warnings: true },
|
|
6
13
|
detail: { evidence: 'full', hookValues: true, pages: 'patterns' },
|
|
7
14
|
environments: { ci: true, development: true, production: false },
|
|
8
15
|
send: {
|
|
@@ -14,13 +21,29 @@ export const DEFAULTS = {
|
|
|
14
21
|
runtime: true,
|
|
15
22
|
usage: true,
|
|
16
23
|
},
|
|
24
|
+
sinks: { overlay: true, relay: false },
|
|
25
|
+
};
|
|
26
|
+
export const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost|.+\.test)$/;
|
|
27
|
+
const local = (address) => {
|
|
28
|
+
if (!/^https?:\/\/[^/?#@]+$/.test(address)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return LOCAL.test(new URL(address).hostname);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
17
37
|
};
|
|
18
38
|
const ALLOWED = {
|
|
19
39
|
console: {
|
|
20
40
|
coverage: ['info', 'off'],
|
|
21
41
|
findings: ['error', 'warn', 'info', 'off'],
|
|
42
|
+
performance: ['info', 'off'],
|
|
43
|
+
runtime: ['warn', 'off'],
|
|
22
44
|
usage: ['debug', 'off'],
|
|
23
45
|
},
|
|
46
|
+
core: { warnings: [true, false] },
|
|
24
47
|
detail: {
|
|
25
48
|
evidence: ['full', 'selector', 'none'],
|
|
26
49
|
hookValues: [true, false],
|
|
@@ -29,6 +52,7 @@ const ALLOWED = {
|
|
|
29
52
|
environments: { ci: [true, false], development: [true, false], production: 'sample' },
|
|
30
53
|
project: { key: 'string', name: 'string', tags: 'tags' },
|
|
31
54
|
send: Object.fromEntries(CATEGORIES.map((category) => [category, [true, false]])),
|
|
55
|
+
sinks: { overlay: [true, false], relay: 'address' },
|
|
32
56
|
};
|
|
33
57
|
const shown = (value) => typeof value === 'string' ? `\`${value}\`` : String(value);
|
|
34
58
|
export const checkConfig = (config, layer) => {
|
|
@@ -60,22 +84,26 @@ export const checkConfig = (config, layer) => {
|
|
|
60
84
|
? typeof one === 'string'
|
|
61
85
|
: takes === 'tags'
|
|
62
86
|
? Array.isArray(one) && one.every((tag) => typeof tag === 'string')
|
|
63
|
-
: takes === '
|
|
64
|
-
? one ===
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
typeof one
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
: takes === 'address'
|
|
88
|
+
? typeof one === 'boolean' || (typeof one === 'string' && local(one))
|
|
89
|
+
: takes === 'sample'
|
|
90
|
+
? one === false ||
|
|
91
|
+
(typeof one === 'object' &&
|
|
92
|
+
one !== null &&
|
|
93
|
+
typeof one.sample === 'number' &&
|
|
94
|
+
one.sample >= 0 &&
|
|
95
|
+
one.sample <= 1)
|
|
96
|
+
: takes.includes(one);
|
|
71
97
|
if (!fits) {
|
|
72
98
|
const expected = takes === 'string'
|
|
73
99
|
? 'a string'
|
|
74
100
|
: takes === 'tags'
|
|
75
101
|
? 'a list of strings'
|
|
76
|
-
: takes === '
|
|
77
|
-
? '`false`, or
|
|
78
|
-
: takes
|
|
102
|
+
: takes === 'address'
|
|
103
|
+
? '`true`, `false`, or an address on a local host, such as `http://127.0.0.1:7417`'
|
|
104
|
+
: takes === 'sample'
|
|
105
|
+
? '`false`, or `{ sample }` from 0 to 1'
|
|
106
|
+
: takes.map(shown).join(', ');
|
|
79
107
|
throw new Error(`The ${layer} sets \`${section}.${name}\` to \`${JSON.stringify(one)}\`. It takes ${expected}.`);
|
|
80
108
|
}
|
|
81
109
|
}
|
|
@@ -105,8 +133,10 @@ export const resolveConfig = (file, call) => {
|
|
|
105
133
|
return {
|
|
106
134
|
configuration: {
|
|
107
135
|
console: merged('console', { ...DEFAULTS.console }),
|
|
136
|
+
core: merged('core', { ...DEFAULTS.core }),
|
|
108
137
|
detail: merged('detail', { ...DEFAULTS.detail }),
|
|
109
138
|
environments: merged('environments', { ...DEFAULTS.environments }),
|
|
139
|
+
sinks: merged('sinks', { ...DEFAULTS.sinks }),
|
|
110
140
|
},
|
|
111
141
|
project: {
|
|
112
142
|
...(project.name === undefined ? {} : { name: project.name }),
|
package/dist/console.d.ts
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* | A finding at `error` | `console.error` | The CSS does nothing where it was written |
|
|
10
10
|
* | A finding at `warn` | `console.warn` | Worth fixing, and the page works |
|
|
11
11
|
* | A finding at `info` | `console.info` | Advice |
|
|
12
|
+
* | A runtime event core left to diagnostics | `console.warn`, or `console.error` for a failure | The library was used wrong as it ran |
|
|
12
13
|
* | Unreadable stylesheets | `console.warn` | The run could not see part of the page |
|
|
13
14
|
* | Coverage, overrides, a clean run | `console.info` | What the run looked at |
|
|
14
15
|
* | Usage | `console.debug` | For whoever studies usage, hidden at the default level |
|
|
16
|
+
* | Performance, where asked for | `console.info` | What the components cost, by tag |
|
|
15
17
|
*
|
|
16
18
|
* **All of it in one group**, headed by the counts, so a page with other logs
|
|
17
19
|
* keeps a run together and a clean run collapses to one line.
|
|
@@ -35,4 +37,12 @@ export type ConsoleSink = Pick<Console, 'debug' | 'error' | 'group' | 'groupColl
|
|
|
35
37
|
* every finding whatever is printed under it, and the ones the rules turned
|
|
36
38
|
* off, so a quiet console never reads as a clean page.
|
|
37
39
|
*/
|
|
38
|
-
export declare const report: (result: Report, to?: ConsoleSink
|
|
40
|
+
export declare const report: (result: Report, to?: ConsoleSink, { shown, }?: {
|
|
41
|
+
/**
|
|
42
|
+
* The fingerprints already printed for this page. Given, a finding prints
|
|
43
|
+
* only the first time its fingerprint is seen, and is added; the heading
|
|
44
|
+
* still counts every finding. `initDiagnostics()` keeps one per page, so
|
|
45
|
+
* a run after every change does not repeat itself.
|
|
46
|
+
*/
|
|
47
|
+
shown?: Set<string> | undefined;
|
|
48
|
+
}) => Report;
|
package/dist/console.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shownRuntime } from './runtime.js';
|
|
1
2
|
const entry = (finding) => {
|
|
2
3
|
const by = finding.decidedBy === 'shipped' ? '' : ` (${finding.severity}, set by the ${finding.decidedBy})`;
|
|
3
4
|
const lines = [`${finding.kind}${by}: ${finding.subject}`, finding.message];
|
|
@@ -8,7 +9,7 @@ const entry = (finding) => {
|
|
|
8
9
|
};
|
|
9
10
|
const counted = (count, noun) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
10
11
|
const RANK = { error: 3, info: 1, warn: 2 };
|
|
11
|
-
export const report = (result, to = console) => {
|
|
12
|
+
export const report = (result, to = console, { shown, } = {}) => {
|
|
12
13
|
const { configuration, coverage, findings } = result;
|
|
13
14
|
const levels = configuration.console;
|
|
14
15
|
const bySeverity = (severity) => findings.filter((finding) => finding.severity === severity).length;
|
|
@@ -16,16 +17,18 @@ export const report = (result, to = console) => {
|
|
|
16
17
|
const warnings = bySeverity('warn');
|
|
17
18
|
const off = configuration.silenced.reduce((total, one) => total + one.count, 0);
|
|
18
19
|
const missed = coverage.sheets - coverage.read;
|
|
20
|
+
const runtime = shownRuntime(result.runtime);
|
|
19
21
|
const counts = [
|
|
20
22
|
counted(errors, 'error'),
|
|
21
23
|
counted(warnings, 'warning'),
|
|
22
24
|
`${bySeverity('info')} info`,
|
|
25
|
+
...(runtime.length > 0 ? [`${runtime.length} runtime`] : []),
|
|
23
26
|
...(off > 0 ? [`${off} turned off`] : []),
|
|
24
27
|
];
|
|
25
|
-
const heading = findings.length === 0 && off === 0
|
|
28
|
+
const heading = findings.length === 0 && off === 0 && runtime.length === 0
|
|
26
29
|
? 'helix diagnostics: no findings'
|
|
27
30
|
: `helix diagnostics: ${counts.join(', ')}`;
|
|
28
|
-
if (errors + warnings > 0 || missed > 0) {
|
|
31
|
+
if (errors + warnings + runtime.length > 0 || missed > 0) {
|
|
29
32
|
to.group(heading);
|
|
30
33
|
}
|
|
31
34
|
else {
|
|
@@ -48,10 +51,48 @@ export const report = (result, to = console) => {
|
|
|
48
51
|
}
|
|
49
52
|
if (levels.findings !== 'off') {
|
|
50
53
|
const floor = RANK[levels.findings];
|
|
54
|
+
let before = 0;
|
|
51
55
|
for (const finding of findings) {
|
|
52
|
-
if (RANK[finding.severity]
|
|
53
|
-
|
|
56
|
+
if (RANK[finding.severity] < floor) {
|
|
57
|
+
continue;
|
|
54
58
|
}
|
|
59
|
+
if (shown?.has(finding.fingerprint)) {
|
|
60
|
+
before += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
shown?.add(finding.fingerprint);
|
|
64
|
+
to[finding.severity](entry(finding));
|
|
65
|
+
}
|
|
66
|
+
if (before > 0) {
|
|
67
|
+
to.info(`${counted(before, 'finding')} printed before, on this page, and not again`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (levels.runtime !== 'off') {
|
|
71
|
+
for (const one of runtime) {
|
|
72
|
+
if (one.printed || shown?.has(one.fingerprint)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
shown?.add(one.fingerprint);
|
|
76
|
+
to[one.level](`${one.kind}${one.tag ? ` <${one.tag}>` : ''}: ${one.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (levels.performance !== 'off' && result.performance) {
|
|
80
|
+
const { measured, tags } = result.performance;
|
|
81
|
+
const rows = Object.entries(tags)
|
|
82
|
+
.sort(([, a], [, b]) => b.render.p95 - a.render.p95)
|
|
83
|
+
.slice(0, 10);
|
|
84
|
+
const lines = rows.map(([tag, one]) => `${tag}: ${one.render.p50} ms median, ${one.render.p95} ms p95, ${one.render.max} ms slowest, of ${counted(one.render.count, 'first update')}` +
|
|
85
|
+
(measured.layoutShift && one.shift > 0 ? `; ${one.shift} layout shift` : '') +
|
|
86
|
+
(measured.longTasks && one.longTasks > 0 ? `; ${counted(one.longTasks, 'long task')}` : ''));
|
|
87
|
+
const key = `performance ${result.run.page ?? ''} ${lines.join('|')}`;
|
|
88
|
+
if (rows.length > 0 && !shown?.has(key)) {
|
|
89
|
+
shown?.add(key);
|
|
90
|
+
to.info([
|
|
91
|
+
`performance: ${counted(Object.keys(tags).length, 'tag')} measured, slowest first` +
|
|
92
|
+
(measured.layoutShift ? '' : '. Layout shift is not measured in this browser') +
|
|
93
|
+
(measured.longTasks ? '' : '. Long tasks are not measured in this browser'),
|
|
94
|
+
...lines,
|
|
95
|
+
].join('\n'));
|
|
55
96
|
}
|
|
56
97
|
}
|
|
57
98
|
if (levels.usage !== 'off') {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type Config } from './config.js';
|
|
2
|
+
import type { Tier } from './manifest.js';
|
|
3
|
+
import type { Environment, Report } from './report.js';
|
|
4
|
+
import { type Watcher } from './usage.js';
|
|
5
|
+
/**
|
|
6
|
+
* What to look at, what to compare it against, and the settings for this
|
|
7
|
+
* call, which win over the project's file field by field.
|
|
8
|
+
*/
|
|
9
|
+
export interface Options extends Config {
|
|
10
|
+
/**
|
|
11
|
+
* `duplicateRegistrations` from `@fusion.dev/helix-core`, when it is installed.
|
|
12
|
+
*
|
|
13
|
+
* **Passed in rather than imported**, so this package does not depend on the
|
|
14
|
+
* library it diagnoses. A diagnostics release would otherwise have to follow
|
|
15
|
+
* every core release, and a consumer on an older core could not run a newer
|
|
16
|
+
* collector at all. Omitted, the duplicate check falls back to comparing a
|
|
17
|
+
* live element against its own registered class, which finds the same fault
|
|
18
|
+
* from outside and cannot name the losing class.
|
|
19
|
+
*/
|
|
20
|
+
duplicates?: (() => string[]) | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Where this run happens, as the project declares it. **Never inferred**:
|
|
23
|
+
* a bundler's `development` condition misfires both ways, and a run whose
|
|
24
|
+
* environment nobody declared is never sent.
|
|
25
|
+
*/
|
|
26
|
+
environment?: Environment | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The project's own file, `helix-diagnostics.json`, parsed. The build reads
|
|
29
|
+
* the file, since the page cannot: import the JSON and pass it here.
|
|
30
|
+
*/
|
|
31
|
+
file?: Config | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The document to scan. Defaults to the ambient one.
|
|
34
|
+
*
|
|
35
|
+
* Present so a test can hand over a constructed document, and so a page with
|
|
36
|
+
* an iframe it owns can scan that too.
|
|
37
|
+
*/
|
|
38
|
+
from?: Document | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* The page's path, for a router that knows its own pattern, such as
|
|
41
|
+
* `/products/[id]`. Otherwise read from the location and made a pattern, as
|
|
42
|
+
* `detail.pages` says.
|
|
43
|
+
*/
|
|
44
|
+
page?: string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The manifests to check against, one per installed tier.
|
|
47
|
+
*
|
|
48
|
+
* **Pass both tiers when both are installed.** A part `@fusion.dev/helix-headless`
|
|
49
|
+
* publishes and `@fusion.dev/helix` never forwarded is a real fault and is
|
|
50
|
+
* invisible to either manifest alone.
|
|
51
|
+
*/
|
|
52
|
+
tiers: Tier[];
|
|
53
|
+
/**
|
|
54
|
+
* A watcher from `watch()`, so usage counts every element that rendered
|
|
55
|
+
* since it began. Without one, usage counts the elements in the page now.
|
|
56
|
+
*/
|
|
57
|
+
watcher?: Watcher | undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One run over the page.
|
|
61
|
+
*
|
|
62
|
+
* **It returns coverage and findings together and offers no way to have one
|
|
63
|
+
* without the other.** A collector that cannot read cross-origin sheets reports
|
|
64
|
+
* the same empty array whether it read everything or nothing. An empty
|
|
65
|
+
* `findings` is only meaningful beside the `coverage` that says what was looked
|
|
66
|
+
* at.
|
|
67
|
+
*
|
|
68
|
+
* Cheap enough to call on a route change. There is no observer and no polling
|
|
69
|
+
* loop: sheets that arrive later are found by calling it again.
|
|
70
|
+
*
|
|
71
|
+
* **A manifest from another version is refused**, with one finding that says
|
|
72
|
+
* so in place of every other: by version where both sides carry one, and
|
|
73
|
+
* otherwise by a part the manifest does not list rendering in the page.
|
|
74
|
+
*/
|
|
75
|
+
export declare const diagnose: (options: Options) => Report;
|
package/dist/diagnose.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { analyze, markup, registrations, skew, versionSkew } from './analyze.js';
|
|
2
|
+
import { collect } from './collect.js';
|
|
3
|
+
import { pagePattern, resolveConfig } from './config.js';
|
|
4
|
+
import { identify } from './identity.js';
|
|
5
|
+
import { index, lintFor, versionsOf } from './manifest.js';
|
|
6
|
+
import { pageOf } from './page.js';
|
|
7
|
+
import { applyRules, overridesOf } from './rules.js';
|
|
8
|
+
import { logged, tally } from './runtime.js';
|
|
9
|
+
import { listed, loadedVersions, styled, walk } from './usage.js';
|
|
10
|
+
import { VERSION } from './version.js';
|
|
11
|
+
const runId = () => globalThis.crypto?.randomUUID?.() ??
|
|
12
|
+
`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
|
|
13
|
+
export const diagnose = (options) => {
|
|
14
|
+
const started = performance.now();
|
|
15
|
+
const at = new Date().toISOString();
|
|
16
|
+
const { duplicates, environment, file, from, page, tiers, watcher, ...call } = options;
|
|
17
|
+
if (tiers.length === 0) {
|
|
18
|
+
throw new Error('diagnose() needs at least one manifest. Without one every part name is unknown and every finding would be noise.');
|
|
19
|
+
}
|
|
20
|
+
const resolved = resolveConfig(file, call);
|
|
21
|
+
const { detail } = resolved.configuration;
|
|
22
|
+
const document_ = from ?? globalThis.document;
|
|
23
|
+
const walked = pageOf(document_);
|
|
24
|
+
const { answered, coverage, readers, rules: located, used } = collect(document_, walked);
|
|
25
|
+
const lints = tiers.map((tier) => ({ lint: lintFor(tier) }));
|
|
26
|
+
const tags = index(lints);
|
|
27
|
+
const counted = watcher ? watcher.counts() : walk(document_, walked);
|
|
28
|
+
const runtime = watcher
|
|
29
|
+
? watcher.runtime()
|
|
30
|
+
: (() => {
|
|
31
|
+
const kept = tally();
|
|
32
|
+
for (const event of logged()) {
|
|
33
|
+
kept.add(event, true);
|
|
34
|
+
}
|
|
35
|
+
return kept.entries();
|
|
36
|
+
})();
|
|
37
|
+
const reported = runtime
|
|
38
|
+
.filter((one) => one.kind === 'duplicate-registration' && one.tag !== undefined)
|
|
39
|
+
.map((one) => one.tag);
|
|
40
|
+
const twice = duplicates ?? (reported.length > 0 ? () => reported : undefined);
|
|
41
|
+
const measured = watcher?.performance();
|
|
42
|
+
const seen = loadedVersions();
|
|
43
|
+
const manifests = versionsOf(lints);
|
|
44
|
+
const byVersion = versionSkew(manifests, seen);
|
|
45
|
+
const known = seen.length > 0 && Object.keys(manifests).length > 0;
|
|
46
|
+
const skewed = applyRules((byVersion.length > 0 ? byVersion : known ? [] : skew(tags, document_, walked)).map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
47
|
+
const judged = skewed.findings.length > 0
|
|
48
|
+
? { findings: [], silenced: [] }
|
|
49
|
+
: applyRules([
|
|
50
|
+
...analyze(located, tags, document_, answered, used, readers),
|
|
51
|
+
...markup(tags, document_, walked),
|
|
52
|
+
...registrations(document_, twice, walked),
|
|
53
|
+
].map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
54
|
+
const path = page ?? globalThis.location?.pathname;
|
|
55
|
+
const shownPage = path === undefined || detail.pages === 'none'
|
|
56
|
+
? undefined
|
|
57
|
+
: detail.pages === 'patterns'
|
|
58
|
+
? pagePattern(path)
|
|
59
|
+
: path;
|
|
60
|
+
return {
|
|
61
|
+
configuration: {
|
|
62
|
+
...resolved.configuration,
|
|
63
|
+
overrides: overridesOf(file?.rules, call.rules),
|
|
64
|
+
silenced: [...skewed.silenced, ...judged.silenced],
|
|
65
|
+
},
|
|
66
|
+
coverage,
|
|
67
|
+
findings: [...skewed.findings, ...judged.findings],
|
|
68
|
+
project: resolved.project,
|
|
69
|
+
run: {
|
|
70
|
+
at,
|
|
71
|
+
duration: Math.round(performance.now() - started),
|
|
72
|
+
id: runId(),
|
|
73
|
+
source: 'page',
|
|
74
|
+
...(environment === undefined ? {} : { environment }),
|
|
75
|
+
...(shownPage === undefined ? {} : { page: shownPage }),
|
|
76
|
+
},
|
|
77
|
+
...(measured === undefined ? {} : { performance: measured }),
|
|
78
|
+
runtime,
|
|
79
|
+
schema: 1,
|
|
80
|
+
usage: {
|
|
81
|
+
...listed(counted, tags),
|
|
82
|
+
...styled(located, tags, answered, detail.hookValues),
|
|
83
|
+
counted: watcher ? 'watched' : 'walked',
|
|
84
|
+
},
|
|
85
|
+
versions: { collector: VERSION, helix: seen, manifests, rules: VERSION },
|
|
86
|
+
withheld: Object.entries(resolved.send)
|
|
87
|
+
.filter(([, sent]) => !sent.on)
|
|
88
|
+
.map(([category, sent]) => ({
|
|
89
|
+
category: category,
|
|
90
|
+
decidedBy: sent.decidedBy,
|
|
91
|
+
})),
|
|
92
|
+
};
|
|
93
|
+
};
|
package/dist/identity.d.ts
CHANGED
|
@@ -30,5 +30,11 @@ export declare const normalize: (subject: string) => string;
|
|
|
30
30
|
* seen twice, which is how a reader thinks of it.
|
|
31
31
|
*/
|
|
32
32
|
export declare const fingerprintOf: (finding: Pick<Raw, 'kind' | 'subject' | 'tag'>) => string;
|
|
33
|
+
/**
|
|
34
|
+
* A runtime event's identity across runs: its kind, its tag, and its sentence
|
|
35
|
+
* with each run of digits read as one, so a count in the sentence is not part
|
|
36
|
+
* of what the event is.
|
|
37
|
+
*/
|
|
38
|
+
export declare const runtimeFingerprint: (kind: string, tag: string | undefined, message: string) => string;
|
|
33
39
|
/** A finding with its category, owner, family, and fingerprint. */
|
|
34
40
|
export declare const identify: (finding: Raw, tags: Map<string, Published>) => Identified;
|
package/dist/identity.js
CHANGED
|
@@ -37,6 +37,7 @@ const hash = (text) => {
|
|
|
37
37
|
return (4294967296 * (2097151 & two) + (one >>> 0)).toString(16).padStart(14, '0');
|
|
38
38
|
};
|
|
39
39
|
export const fingerprintOf = (finding) => hash(`${finding.kind}\u0000${finding.tag}\u0000${normalize(finding.subject)}`);
|
|
40
|
+
export const runtimeFingerprint = (kind, tag, message) => hash(`runtime\u0000${kind}\u0000${tag ?? ''}\u0000${message.replace(/\d[\d,.]*/g, '#')}`);
|
|
40
41
|
export const identify = (finding, tags) => ({
|
|
41
42
|
...finding,
|
|
42
43
|
category: CATEGORY[finding.kind],
|
package/dist/index.d.ts
CHANGED
|
@@ -30,86 +30,14 @@
|
|
|
30
30
|
export { collect } from './collect.js';
|
|
31
31
|
export { type Config, DEFAULTS, pagePattern } from './config.js';
|
|
32
32
|
export { report } from './console.js';
|
|
33
|
+
export { diagnose, type Options } from './diagnose.js';
|
|
33
34
|
export { fingerprintOf, normalize } from './identity.js';
|
|
34
|
-
export type
|
|
35
|
-
export {
|
|
35
|
+
export { type Diagnostics, INSTANCE, type InitOptions, initDiagnostics } from './init.js';
|
|
36
|
+
export type { LintData, LintTag, ManifestSource, Published, Tier } from './manifest.js';
|
|
37
|
+
export { index, lintOf, nearest } from './manifest.js';
|
|
36
38
|
export type { Category, Configuration, Coverage, Environment, Finding, FindingCategory, Kind, Layer, Origin, Owner, Report, Severity, Unreadable, Usage, } from './report.js';
|
|
37
39
|
export { CATEGORIES, KINDS } from './report.js';
|
|
38
40
|
export { type Override, type Rules, SHIPPED_RULES } from './rules.js';
|
|
41
|
+
export { TIMING, type Timing } from './schedule.js';
|
|
39
42
|
export { type Watcher, watch } from './usage.js';
|
|
40
43
|
export { VERSION } from './version.js';
|
|
41
|
-
import { type Config } from './config.js';
|
|
42
|
-
import type { Tier } from './manifest.js';
|
|
43
|
-
import type { Environment, Report } from './report.js';
|
|
44
|
-
import { type Watcher } from './usage.js';
|
|
45
|
-
/**
|
|
46
|
-
* What to look at, what to compare it against, and the settings for this
|
|
47
|
-
* call, which win over the project's file field by field.
|
|
48
|
-
*/
|
|
49
|
-
export interface Options extends Config {
|
|
50
|
-
/**
|
|
51
|
-
* `duplicateRegistrations` from `@fusion.dev/helix-core`, when it is installed.
|
|
52
|
-
*
|
|
53
|
-
* **Passed in rather than imported**, so this package does not depend on the
|
|
54
|
-
* library it diagnoses. A diagnostics release would otherwise have to follow
|
|
55
|
-
* every core release, and a consumer on an older core could not run a newer
|
|
56
|
-
* collector at all. Omitted, the duplicate check falls back to comparing a
|
|
57
|
-
* live element against its own registered class, which finds the same fault
|
|
58
|
-
* from outside and cannot name the losing class.
|
|
59
|
-
*/
|
|
60
|
-
duplicates?: () => string[];
|
|
61
|
-
/**
|
|
62
|
-
* Where this run happens, as the project declares it. **Never inferred**:
|
|
63
|
-
* a bundler's `development` condition misfires both ways, and a run whose
|
|
64
|
-
* environment nobody declared is never sent.
|
|
65
|
-
*/
|
|
66
|
-
environment?: Environment | undefined;
|
|
67
|
-
/**
|
|
68
|
-
* The project's own file, `helix-diagnostics.json`, parsed. The build reads
|
|
69
|
-
* the file, since the page cannot: import the JSON and pass it here.
|
|
70
|
-
*/
|
|
71
|
-
file?: Config | undefined;
|
|
72
|
-
/**
|
|
73
|
-
* The document to scan. Defaults to the ambient one.
|
|
74
|
-
*
|
|
75
|
-
* Present so a test can hand over a constructed document, and so a page with
|
|
76
|
-
* an iframe it owns can scan that too.
|
|
77
|
-
*/
|
|
78
|
-
from?: Document;
|
|
79
|
-
/**
|
|
80
|
-
* The page's path, for a router that knows its own pattern, such as
|
|
81
|
-
* `/products/[id]`. Otherwise read from the location and made a pattern, as
|
|
82
|
-
* `detail.pages` says.
|
|
83
|
-
*/
|
|
84
|
-
page?: string | undefined;
|
|
85
|
-
/**
|
|
86
|
-
* The manifests to check against, one per installed tier.
|
|
87
|
-
*
|
|
88
|
-
* **Pass both tiers when both are installed.** A part `@fusion.dev/helix-headless`
|
|
89
|
-
* publishes and `@fusion.dev/helix` never forwarded is a real fault and is
|
|
90
|
-
* invisible to either manifest alone.
|
|
91
|
-
*/
|
|
92
|
-
tiers: Tier[];
|
|
93
|
-
/**
|
|
94
|
-
* A watcher from `watch()`, so usage counts every element that rendered
|
|
95
|
-
* since it began. Without one, usage counts the elements in the page now.
|
|
96
|
-
*/
|
|
97
|
-
watcher?: Watcher | undefined;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* One run over the page.
|
|
101
|
-
*
|
|
102
|
-
* **It returns coverage and findings together and offers no way to have one
|
|
103
|
-
* without the other.** A collector that cannot read cross-origin sheets reports
|
|
104
|
-
* the same empty array whether it read everything or nothing. An empty
|
|
105
|
-
* `findings` is only meaningful beside the `coverage` that says what was looked
|
|
106
|
-
* at.
|
|
107
|
-
*
|
|
108
|
-
* Cheap enough to call on a route change. There is no observer and no polling
|
|
109
|
-
* loop: sheets that arrive later are found by calling it again.
|
|
110
|
-
*
|
|
111
|
-
* **A manifest from another version is refused**, with one finding that says
|
|
112
|
-
* so in place of every other: by version where both sides carry one, and
|
|
113
|
-
* otherwise by a part the manifest does not list rendering in the page.
|
|
114
|
-
*/
|
|
115
|
-
export declare const diagnose: (options: Options) => Report;
|