@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/config.js
CHANGED
|
@@ -14,6 +14,19 @@ export const DEFAULTS = {
|
|
|
14
14
|
runtime: true,
|
|
15
15
|
usage: true,
|
|
16
16
|
},
|
|
17
|
+
sinks: { overlay: true, relay: false },
|
|
18
|
+
};
|
|
19
|
+
export const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost|.+\.test)$/;
|
|
20
|
+
const local = (address) => {
|
|
21
|
+
if (!/^https?:\/\/[^/?#@]+$/.test(address)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return LOCAL.test(new URL(address).hostname);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
17
30
|
};
|
|
18
31
|
const ALLOWED = {
|
|
19
32
|
console: {
|
|
@@ -29,6 +42,7 @@ const ALLOWED = {
|
|
|
29
42
|
environments: { ci: [true, false], development: [true, false], production: 'sample' },
|
|
30
43
|
project: { key: 'string', name: 'string', tags: 'tags' },
|
|
31
44
|
send: Object.fromEntries(CATEGORIES.map((category) => [category, [true, false]])),
|
|
45
|
+
sinks: { overlay: [true, false], relay: 'address' },
|
|
32
46
|
};
|
|
33
47
|
const shown = (value) => typeof value === 'string' ? `\`${value}\`` : String(value);
|
|
34
48
|
export const checkConfig = (config, layer) => {
|
|
@@ -60,22 +74,26 @@ export const checkConfig = (config, layer) => {
|
|
|
60
74
|
? typeof one === 'string'
|
|
61
75
|
: takes === 'tags'
|
|
62
76
|
? Array.isArray(one) && one.every((tag) => typeof tag === 'string')
|
|
63
|
-
: takes === '
|
|
64
|
-
? one ===
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
typeof one
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
: takes === 'address'
|
|
78
|
+
? typeof one === 'boolean' || (typeof one === 'string' && local(one))
|
|
79
|
+
: takes === 'sample'
|
|
80
|
+
? one === false ||
|
|
81
|
+
(typeof one === 'object' &&
|
|
82
|
+
one !== null &&
|
|
83
|
+
typeof one.sample === 'number' &&
|
|
84
|
+
one.sample >= 0 &&
|
|
85
|
+
one.sample <= 1)
|
|
86
|
+
: takes.includes(one);
|
|
71
87
|
if (!fits) {
|
|
72
88
|
const expected = takes === 'string'
|
|
73
89
|
? 'a string'
|
|
74
90
|
: takes === 'tags'
|
|
75
91
|
? 'a list of strings'
|
|
76
|
-
: takes === '
|
|
77
|
-
? '`false`, or
|
|
78
|
-
: takes
|
|
92
|
+
: takes === 'address'
|
|
93
|
+
? '`true`, `false`, or an address on a local host, such as `http://127.0.0.1:7417`'
|
|
94
|
+
: takes === 'sample'
|
|
95
|
+
? '`false`, or `{ sample }` from 0 to 1'
|
|
96
|
+
: takes.map(shown).join(', ');
|
|
79
97
|
throw new Error(`The ${layer} sets \`${section}.${name}\` to \`${JSON.stringify(one)}\`. It takes ${expected}.`);
|
|
80
98
|
}
|
|
81
99
|
}
|
|
@@ -107,6 +125,7 @@ export const resolveConfig = (file, call) => {
|
|
|
107
125
|
console: merged('console', { ...DEFAULTS.console }),
|
|
108
126
|
detail: merged('detail', { ...DEFAULTS.detail }),
|
|
109
127
|
environments: merged('environments', { ...DEFAULTS.environments }),
|
|
128
|
+
sinks: merged('sinks', { ...DEFAULTS.sinks }),
|
|
110
129
|
},
|
|
111
130
|
project: {
|
|
112
131
|
...(project.name === undefined ? {} : { name: project.name }),
|
package/dist/console.d.ts
CHANGED
|
@@ -35,4 +35,12 @@ export type ConsoleSink = Pick<Console, 'debug' | 'error' | 'group' | 'groupColl
|
|
|
35
35
|
* every finding whatever is printed under it, and the ones the rules turned
|
|
36
36
|
* off, so a quiet console never reads as a clean page.
|
|
37
37
|
*/
|
|
38
|
-
export declare const report: (result: Report, to?: ConsoleSink
|
|
38
|
+
export declare const report: (result: Report, to?: ConsoleSink, { shown, }?: {
|
|
39
|
+
/**
|
|
40
|
+
* The fingerprints already printed for this page. Given, a finding prints
|
|
41
|
+
* only the first time its fingerprint is seen, and is added; the heading
|
|
42
|
+
* still counts every finding. `initDiagnostics()` keeps one per page, so
|
|
43
|
+
* a run after every change does not repeat itself.
|
|
44
|
+
*/
|
|
45
|
+
shown?: Set<string> | undefined;
|
|
46
|
+
}) => Report;
|
package/dist/console.js
CHANGED
|
@@ -8,7 +8,7 @@ const entry = (finding) => {
|
|
|
8
8
|
};
|
|
9
9
|
const counted = (count, noun) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
10
10
|
const RANK = { error: 3, info: 1, warn: 2 };
|
|
11
|
-
export const report = (result, to = console) => {
|
|
11
|
+
export const report = (result, to = console, { shown, } = {}) => {
|
|
12
12
|
const { configuration, coverage, findings } = result;
|
|
13
13
|
const levels = configuration.console;
|
|
14
14
|
const bySeverity = (severity) => findings.filter((finding) => finding.severity === severity).length;
|
|
@@ -48,10 +48,20 @@ export const report = (result, to = console) => {
|
|
|
48
48
|
}
|
|
49
49
|
if (levels.findings !== 'off') {
|
|
50
50
|
const floor = RANK[levels.findings];
|
|
51
|
+
let before = 0;
|
|
51
52
|
for (const finding of findings) {
|
|
52
|
-
if (RANK[finding.severity]
|
|
53
|
-
|
|
53
|
+
if (RANK[finding.severity] < floor) {
|
|
54
|
+
continue;
|
|
54
55
|
}
|
|
56
|
+
if (shown?.has(finding.fingerprint)) {
|
|
57
|
+
before += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
shown?.add(finding.fingerprint);
|
|
61
|
+
to[finding.severity](entry(finding));
|
|
62
|
+
}
|
|
63
|
+
if (before > 0) {
|
|
64
|
+
to.info(`${counted(before, 'finding')} printed before, on this page, and not again`);
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
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,76 @@
|
|
|
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 { listed, loadedVersions, styled, walk } from './usage.js';
|
|
9
|
+
import { VERSION } from './version.js';
|
|
10
|
+
const runId = () => globalThis.crypto?.randomUUID?.() ??
|
|
11
|
+
`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
|
|
12
|
+
export const diagnose = (options) => {
|
|
13
|
+
const started = performance.now();
|
|
14
|
+
const at = new Date().toISOString();
|
|
15
|
+
const { duplicates, environment, file, from, page, tiers, watcher, ...call } = options;
|
|
16
|
+
if (tiers.length === 0) {
|
|
17
|
+
throw new Error('diagnose() needs at least one manifest. Without one every part name is unknown and every finding would be noise.');
|
|
18
|
+
}
|
|
19
|
+
const resolved = resolveConfig(file, call);
|
|
20
|
+
const { detail } = resolved.configuration;
|
|
21
|
+
const document_ = from ?? globalThis.document;
|
|
22
|
+
const walked = pageOf(document_);
|
|
23
|
+
const { answered, coverage, readers, rules: located, used } = collect(document_, walked);
|
|
24
|
+
const lints = tiers.map((tier) => ({ lint: lintFor(tier) }));
|
|
25
|
+
const tags = index(lints);
|
|
26
|
+
const counted = watcher ? watcher.counts() : walk(document_, walked);
|
|
27
|
+
const seen = loadedVersions();
|
|
28
|
+
const manifests = versionsOf(lints);
|
|
29
|
+
const byVersion = versionSkew(manifests, seen);
|
|
30
|
+
const known = seen.length > 0 && Object.keys(manifests).length > 0;
|
|
31
|
+
const skewed = applyRules((byVersion.length > 0 ? byVersion : known ? [] : skew(tags, document_, walked)).map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
32
|
+
const judged = skewed.findings.length > 0
|
|
33
|
+
? { findings: [], silenced: [] }
|
|
34
|
+
: applyRules([
|
|
35
|
+
...analyze(located, tags, document_, answered, used, readers),
|
|
36
|
+
...markup(tags, document_, walked),
|
|
37
|
+
...registrations(document_, duplicates, walked),
|
|
38
|
+
].map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
39
|
+
const path = page ?? globalThis.location?.pathname;
|
|
40
|
+
const shownPage = path === undefined || detail.pages === 'none'
|
|
41
|
+
? undefined
|
|
42
|
+
: detail.pages === 'patterns'
|
|
43
|
+
? pagePattern(path)
|
|
44
|
+
: path;
|
|
45
|
+
return {
|
|
46
|
+
configuration: {
|
|
47
|
+
...resolved.configuration,
|
|
48
|
+
overrides: overridesOf(file?.rules, call.rules),
|
|
49
|
+
silenced: [...skewed.silenced, ...judged.silenced],
|
|
50
|
+
},
|
|
51
|
+
coverage,
|
|
52
|
+
findings: [...skewed.findings, ...judged.findings],
|
|
53
|
+
project: resolved.project,
|
|
54
|
+
run: {
|
|
55
|
+
at,
|
|
56
|
+
duration: Math.round(performance.now() - started),
|
|
57
|
+
id: runId(),
|
|
58
|
+
source: 'page',
|
|
59
|
+
...(environment === undefined ? {} : { environment }),
|
|
60
|
+
...(shownPage === undefined ? {} : { page: shownPage }),
|
|
61
|
+
},
|
|
62
|
+
schema: 1,
|
|
63
|
+
usage: {
|
|
64
|
+
...listed(counted, tags),
|
|
65
|
+
...styled(located, tags, answered, detail.hookValues),
|
|
66
|
+
counted: watcher ? 'watched' : 'walked',
|
|
67
|
+
},
|
|
68
|
+
versions: { collector: VERSION, helix: seen, manifests, rules: VERSION },
|
|
69
|
+
withheld: Object.entries(resolved.send)
|
|
70
|
+
.filter(([, sent]) => !sent.on)
|
|
71
|
+
.map(([category, sent]) => ({
|
|
72
|
+
category: category,
|
|
73
|
+
decidedBy: sent.decidedBy,
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
};
|
package/dist/identity.js
CHANGED
|
@@ -2,16 +2,20 @@ const CATEGORY = {
|
|
|
2
2
|
'dead-hook': 'styling',
|
|
3
3
|
'dead-part': 'styling',
|
|
4
4
|
'duplicate-registration': 'integrity',
|
|
5
|
+
'invalid-value': 'markup',
|
|
5
6
|
'manifest-skew': 'integrity',
|
|
6
7
|
'unforwarded-part': 'styling',
|
|
8
|
+
'unknown-attribute': 'markup',
|
|
7
9
|
'unknown-tag': 'integrity',
|
|
8
10
|
};
|
|
9
11
|
const OWNER = {
|
|
10
12
|
'dead-hook': 'consumer',
|
|
11
13
|
'dead-part': 'consumer',
|
|
12
14
|
'duplicate-registration': 'consumer',
|
|
15
|
+
'invalid-value': 'consumer',
|
|
13
16
|
'manifest-skew': 'consumer',
|
|
14
17
|
'unforwarded-part': 'helix',
|
|
18
|
+
'unknown-attribute': 'consumer',
|
|
15
19
|
'unknown-tag': 'consumer',
|
|
16
20
|
};
|
|
17
21
|
export const normalize = (subject) => subject
|
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;
|
package/dist/index.js
CHANGED
|
@@ -1,81 +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, 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, rules: located } = 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),
|
|
42
|
-
...registrations(document_, duplicates),
|
|
43
|
-
].map((one) => identify(one, tags)), file?.rules, call.rules);
|
|
44
|
-
const path = page ?? globalThis.location?.pathname;
|
|
45
|
-
const shownPage = path === undefined || detail.pages === 'none'
|
|
46
|
-
? undefined
|
|
47
|
-
: detail.pages === 'patterns'
|
|
48
|
-
? pagePattern(path)
|
|
49
|
-
: path;
|
|
50
|
-
return {
|
|
51
|
-
configuration: {
|
|
52
|
-
...resolved.configuration,
|
|
53
|
-
overrides: overridesOf(file?.rules, call.rules),
|
|
54
|
-
silenced: [...skewed.silenced, ...judged.silenced],
|
|
55
|
-
},
|
|
56
|
-
coverage,
|
|
57
|
-
findings: [...skewed.findings, ...judged.findings],
|
|
58
|
-
project: resolved.project,
|
|
59
|
-
run: {
|
|
60
|
-
at,
|
|
61
|
-
duration: Math.round(performance.now() - started),
|
|
62
|
-
id: runId(),
|
|
63
|
-
source: 'page',
|
|
64
|
-
...(environment === undefined ? {} : { environment }),
|
|
65
|
-
...(shownPage === undefined ? {} : { page: shownPage }),
|
|
66
|
-
},
|
|
67
|
-
schema: 1,
|
|
68
|
-
usage: {
|
|
69
|
-
...listed(counted, tags),
|
|
70
|
-
...styled(located, tags, answered, detail.hookValues),
|
|
71
|
-
counted: watcher ? 'watched' : 'walked',
|
|
72
|
-
},
|
|
73
|
-
versions: { collector: VERSION, helix: seen, manifests, rules: VERSION },
|
|
74
|
-
withheld: Object.entries(resolved.send)
|
|
75
|
-
.filter(([, sent]) => !sent.on)
|
|
76
|
-
.map(([category, sent]) => ({
|
|
77
|
-
category: category,
|
|
78
|
-
decidedBy: sent.decidedBy,
|
|
79
|
-
})),
|
|
80
|
-
};
|
|
81
|
-
};
|
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;
|