@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/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 === 'sample'
64
- ? one === false ||
65
- (typeof one === 'object' &&
66
- one !== null &&
67
- typeof one.sample === 'number' &&
68
- one.sample >= 0 &&
69
- one.sample <= 1)
70
- : takes.includes(one);
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 === 'sample'
77
- ? '`false`, or `{ sample }` from 0 to 1'
78
- : takes.map(shown).join(', ');
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) => Report;
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] >= floor) {
53
- to[finding.severity](entry(finding));
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;
@@ -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 { ManifestSource, Published, Tier } from './manifest.js';
35
- export { index, nearest } from './manifest.js';
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 { index, nearest } from './manifest.js';
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;