@forkpoint/agent-lighthouse-core 0.3.0 → 0.4.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/index.d.mts +111 -5
- package/dist/index.d.ts +111 -5
- package/dist/index.js +174 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +171 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -17396,17 +17396,17 @@ function stubCheck(meta, tag, explanation) {
|
|
|
17396
17396
|
tags: [tag]
|
|
17397
17397
|
};
|
|
17398
17398
|
}
|
|
17399
|
-
|
|
17399
|
+
function planAudits(ctx, config) {
|
|
17400
17400
|
const scannedPageTypes = new Set(ctx.pages.map((p) => p.pageType));
|
|
17401
|
-
const
|
|
17402
|
-
const
|
|
17401
|
+
const runnable = [];
|
|
17402
|
+
const skipped = [];
|
|
17403
17403
|
for (const cat of config.categories) {
|
|
17404
17404
|
const regs = config.audits[cat.id] ?? [];
|
|
17405
17405
|
for (const reg2 of regs) {
|
|
17406
17406
|
const applicable = reg2.meta.applicablePageTypes;
|
|
17407
17407
|
if (applicable && applicable.length > 0) {
|
|
17408
17408
|
if (!applicable.some((pt) => scannedPageTypes.has(pt))) {
|
|
17409
|
-
|
|
17409
|
+
skipped.push(
|
|
17410
17410
|
stubCheck(
|
|
17411
17411
|
reg2.meta,
|
|
17412
17412
|
TAG_SKIPPED_PAGE_TYPE,
|
|
@@ -17416,30 +17416,35 @@ async function runAudits(ctx, config, onProgress) {
|
|
|
17416
17416
|
continue;
|
|
17417
17417
|
}
|
|
17418
17418
|
}
|
|
17419
|
-
|
|
17419
|
+
runnable.push({ reg: reg2, categoryId: cat.id });
|
|
17420
17420
|
}
|
|
17421
17421
|
}
|
|
17422
|
-
|
|
17423
|
-
|
|
17422
|
+
return { runnable, skipped };
|
|
17423
|
+
}
|
|
17424
|
+
async function runAudits(ctx, config, onEvent, plan) {
|
|
17425
|
+
const { runnable, skipped } = plan ?? planAudits(ctx, config);
|
|
17426
|
+
const allChecks = [...skipped];
|
|
17424
17427
|
const batchSize = 20;
|
|
17425
|
-
for (let i = 0; i <
|
|
17426
|
-
const batch =
|
|
17428
|
+
for (let i = 0; i < runnable.length; i += batchSize) {
|
|
17429
|
+
const batch = runnable.slice(i, i + batchSize);
|
|
17427
17430
|
const batchResults = await Promise.all(
|
|
17428
17431
|
batch.map(async ({ reg: reg2 }) => {
|
|
17432
|
+
const label2 = `${reg2.meta.id} ${reg2.meta.title}`;
|
|
17429
17433
|
try {
|
|
17430
17434
|
const instance = reg2.create();
|
|
17431
17435
|
const result = await instance.audit(ctx);
|
|
17432
|
-
|
|
17436
|
+
const check = instance.toCheckResult(result);
|
|
17437
|
+
onEvent?.({ type: "unit:done", label: label2 });
|
|
17438
|
+
return check;
|
|
17433
17439
|
} catch (err) {
|
|
17434
17440
|
logger.error({ err, auditId: reg2.meta.id }, "[scanner] Audit error");
|
|
17435
17441
|
const message = err instanceof Error ? err.message : String(err);
|
|
17442
|
+
onEvent?.({ type: "unit:fail", label: label2, error: message });
|
|
17436
17443
|
return stubCheck(reg2.meta, TAG_SCAN_ERROR, `Audit failed to run: ${message}`);
|
|
17437
17444
|
}
|
|
17438
17445
|
})
|
|
17439
17446
|
);
|
|
17440
17447
|
allChecks.push(...batchResults);
|
|
17441
|
-
completed += batchResults.length;
|
|
17442
|
-
onProgress?.(completed, totalAudits);
|
|
17443
17448
|
}
|
|
17444
17449
|
const categories = config.categories.map((cat) => {
|
|
17445
17450
|
const catChecks = allChecks.filter((c) => c.category === cat.id);
|
|
@@ -17487,6 +17492,110 @@ function buildWeightedCategoryResult(cat, checks2, registrations) {
|
|
|
17487
17492
|
};
|
|
17488
17493
|
}
|
|
17489
17494
|
|
|
17495
|
+
// src/progress.ts
|
|
17496
|
+
var PHASE_WEIGHTS = {
|
|
17497
|
+
"fetch-root": 0.35,
|
|
17498
|
+
"fetch-pages": 0.2,
|
|
17499
|
+
analyze: 0.1,
|
|
17500
|
+
audits: 0.3,
|
|
17501
|
+
report: 0.05
|
|
17502
|
+
};
|
|
17503
|
+
var ProgressTracker = class {
|
|
17504
|
+
onEvent;
|
|
17505
|
+
startMs;
|
|
17506
|
+
doneWeight = 0;
|
|
17507
|
+
phase = null;
|
|
17508
|
+
phaseStartMs = 0;
|
|
17509
|
+
totalUnits = 0;
|
|
17510
|
+
completedUnits = 0;
|
|
17511
|
+
lastFraction = 0;
|
|
17512
|
+
constructor(onEvent) {
|
|
17513
|
+
this.onEvent = onEvent;
|
|
17514
|
+
this.startMs = performance.now();
|
|
17515
|
+
}
|
|
17516
|
+
/** Fraction of the whole scan that is complete, in [0, 1]. Never decreases. */
|
|
17517
|
+
get fraction() {
|
|
17518
|
+
let f = this.doneWeight;
|
|
17519
|
+
if (this.phase !== null) {
|
|
17520
|
+
const ratio = this.totalUnits > 0 ? Math.min(1, this.completedUnits / this.totalUnits) : 0;
|
|
17521
|
+
f += PHASE_WEIGHTS[this.phase] * ratio;
|
|
17522
|
+
}
|
|
17523
|
+
return Math.min(1, Math.max(f, this.lastFraction));
|
|
17524
|
+
}
|
|
17525
|
+
/** Stamp an event with the current fraction/elapsed and advance the floor. */
|
|
17526
|
+
stamp() {
|
|
17527
|
+
const fraction = this.fraction;
|
|
17528
|
+
this.lastFraction = Math.max(this.lastFraction, fraction);
|
|
17529
|
+
return { fraction, elapsedMs: this.elapsedMs() };
|
|
17530
|
+
}
|
|
17531
|
+
elapsedMs() {
|
|
17532
|
+
return Math.max(0, Math.round(performance.now() - this.startMs));
|
|
17533
|
+
}
|
|
17534
|
+
scanStart(url) {
|
|
17535
|
+
this.onEvent({ type: "scan:start", url, ...this.stamp() });
|
|
17536
|
+
}
|
|
17537
|
+
phaseStart(phase, totalUnits) {
|
|
17538
|
+
this.phase = phase;
|
|
17539
|
+
this.phaseStartMs = performance.now();
|
|
17540
|
+
this.totalUnits = Math.max(0, totalUnits);
|
|
17541
|
+
this.completedUnits = 0;
|
|
17542
|
+
this.onEvent({
|
|
17543
|
+
type: "phase:start",
|
|
17544
|
+
phase,
|
|
17545
|
+
totalUnits: this.totalUnits,
|
|
17546
|
+
...this.stamp()
|
|
17547
|
+
});
|
|
17548
|
+
}
|
|
17549
|
+
/** Correct the current phase's unit total (e.g. discovery finds pages mid-phase). */
|
|
17550
|
+
setPhaseTotal(totalUnits) {
|
|
17551
|
+
this.totalUnits = Math.max(this.completedUnits, totalUnits);
|
|
17552
|
+
}
|
|
17553
|
+
unitDone(label2) {
|
|
17554
|
+
const phase = this.phase;
|
|
17555
|
+
if (phase === null) return;
|
|
17556
|
+
this.completedUnits += 1;
|
|
17557
|
+
this.onEvent({
|
|
17558
|
+
type: "unit:done",
|
|
17559
|
+
phase,
|
|
17560
|
+
completed: this.completedUnits,
|
|
17561
|
+
total: this.totalUnits,
|
|
17562
|
+
label: label2,
|
|
17563
|
+
...this.stamp()
|
|
17564
|
+
});
|
|
17565
|
+
}
|
|
17566
|
+
/** A failed unit still counts as settled work so the phase can complete. */
|
|
17567
|
+
unitFail(label2, error) {
|
|
17568
|
+
const phase = this.phase;
|
|
17569
|
+
if (phase === null) return;
|
|
17570
|
+
this.completedUnits += 1;
|
|
17571
|
+
this.onEvent({
|
|
17572
|
+
type: "unit:fail",
|
|
17573
|
+
phase,
|
|
17574
|
+
label: label2,
|
|
17575
|
+
error,
|
|
17576
|
+
...this.stamp()
|
|
17577
|
+
});
|
|
17578
|
+
}
|
|
17579
|
+
phaseDone() {
|
|
17580
|
+
const phase = this.phase;
|
|
17581
|
+
if (phase === null) return;
|
|
17582
|
+
this.phase = null;
|
|
17583
|
+
this.doneWeight += PHASE_WEIGHTS[phase];
|
|
17584
|
+
const durationMs = Math.max(0, Math.round(performance.now() - this.phaseStartMs));
|
|
17585
|
+
this.totalUnits = 0;
|
|
17586
|
+
this.completedUnits = 0;
|
|
17587
|
+
this.onEvent({
|
|
17588
|
+
type: "phase:done",
|
|
17589
|
+
phase,
|
|
17590
|
+
durationMs,
|
|
17591
|
+
...this.stamp()
|
|
17592
|
+
});
|
|
17593
|
+
}
|
|
17594
|
+
scanDone(score) {
|
|
17595
|
+
this.onEvent({ type: "scan:done", durationMs: this.elapsedMs(), score, ...this.stamp() });
|
|
17596
|
+
}
|
|
17597
|
+
};
|
|
17598
|
+
|
|
17490
17599
|
// src/audits/accessibility/runner.ts
|
|
17491
17600
|
import { JSDOM } from "jsdom";
|
|
17492
17601
|
|
|
@@ -22998,9 +23107,11 @@ function discoverPages(homepageUrl, domain, rootFiles, homepage$, exclude, maxAd
|
|
|
22998
23107
|
}
|
|
22999
23108
|
return selected;
|
|
23000
23109
|
}
|
|
23001
|
-
async function runScan(url,
|
|
23002
|
-
const
|
|
23003
|
-
|
|
23110
|
+
async function runScan(url, options) {
|
|
23111
|
+
const onEvent = options?.onEvent;
|
|
23112
|
+
const pageOverrides = options?.pages;
|
|
23113
|
+
const signal = options?.signal;
|
|
23114
|
+
const tracker = new ProgressTracker((event) => onEvent?.(event));
|
|
23004
23115
|
const start = performance.now();
|
|
23005
23116
|
const fetcher = createFetcher();
|
|
23006
23117
|
const baseUrl = new URL(url).origin;
|
|
@@ -23023,7 +23134,7 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23023
23134
|
}
|
|
23024
23135
|
logger.debug({ url, domain }, "[orchestrator] Starting runScan");
|
|
23025
23136
|
signal?.throwIfAborted();
|
|
23026
|
-
|
|
23137
|
+
tracker.scanStart(displayUrl);
|
|
23027
23138
|
const rootFilePaths = [
|
|
23028
23139
|
"/robots.txt",
|
|
23029
23140
|
"/llms.txt",
|
|
@@ -23061,19 +23172,26 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23061
23172
|
"/our-story"
|
|
23062
23173
|
];
|
|
23063
23174
|
logger.debug({ count: rootFilePaths.length }, "[orchestrator] Phase 1: Fetching root files");
|
|
23175
|
+
tracker.phaseStart("fetch-root", rootFilePaths.length);
|
|
23064
23176
|
const rootResults = await Promise.all(
|
|
23065
|
-
rootFilePaths.map(
|
|
23177
|
+
rootFilePaths.map(
|
|
23178
|
+
(path) => fetcher.fetch({ url: `${baseUrl}${path}`, signal }).then((result) => {
|
|
23179
|
+
tracker.unitDone(path);
|
|
23180
|
+
return result;
|
|
23181
|
+
})
|
|
23182
|
+
)
|
|
23066
23183
|
);
|
|
23067
23184
|
const rootFiles = {};
|
|
23068
23185
|
rootFilePaths.forEach((path, i) => {
|
|
23069
23186
|
rootFiles[path] = rootResults[i];
|
|
23070
23187
|
});
|
|
23071
|
-
|
|
23188
|
+
tracker.phaseDone();
|
|
23072
23189
|
logger.debug("[orchestrator] Phase 1 complete: Root files fetched");
|
|
23073
23190
|
signal?.throwIfAborted();
|
|
23074
|
-
await progress(30, "Fetching pages");
|
|
23075
23191
|
logger.debug("[orchestrator] Phase 2: Fetching pages");
|
|
23192
|
+
tracker.phaseStart("fetch-pages", 1);
|
|
23076
23193
|
const homepageResult = await fetcher.fetch({ url, signal });
|
|
23194
|
+
tracker.unitDone(displayUrl);
|
|
23077
23195
|
const homepage$ = homepageResult.status === 200 && homepageResult.body ? parseHtml(homepageResult.body) : null;
|
|
23078
23196
|
const discoverLimit = Math.max(0, MAX_PAGES_PER_SCAN - 1 - overrideUrls.length);
|
|
23079
23197
|
const discoveredUrls = homepage$ ? discoverPages(url, domain, rootFiles, homepage$, new Set(overrideTypeByKey.keys()), discoverLimit) : [];
|
|
@@ -23082,12 +23200,22 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23082
23200
|
"[orchestrator] Page set: overrides + discovered URLs"
|
|
23083
23201
|
);
|
|
23084
23202
|
const extraUrls = [...overrideUrls, ...discoveredUrls];
|
|
23085
|
-
|
|
23203
|
+
tracker.setPhaseTotal(1 + extraUrls.length);
|
|
23086
23204
|
const extraResults = await Promise.all(
|
|
23087
|
-
extraUrls.map(
|
|
23205
|
+
extraUrls.map(
|
|
23206
|
+
(pageUrl) => fetcher.fetch({ url: pageUrl, signal }).then((result) => {
|
|
23207
|
+
tracker.unitDone(pageUrl);
|
|
23208
|
+
return result;
|
|
23209
|
+
})
|
|
23210
|
+
)
|
|
23088
23211
|
);
|
|
23212
|
+
tracker.phaseDone();
|
|
23089
23213
|
const allPageResults = [homepageResult, ...extraResults];
|
|
23090
23214
|
const allPageUrls = [displayUrl, ...extraUrls];
|
|
23215
|
+
tracker.phaseStart(
|
|
23216
|
+
"analyze",
|
|
23217
|
+
allPageResults.filter((r) => r.status === 200 && r.body).length
|
|
23218
|
+
);
|
|
23091
23219
|
const pages = allPageResults.map((r, i) => ({ result: r, url: allPageUrls[i], index: i })).filter((p) => p.result.status === 200 && p.result.body).map((p) => {
|
|
23092
23220
|
const $ = parseHtml(p.result.body);
|
|
23093
23221
|
const jsonLd = extractJsonLd($);
|
|
@@ -23095,6 +23223,7 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23095
23223
|
const meta = extractMetaTags($);
|
|
23096
23224
|
const isFirstPage = p.index === 0;
|
|
23097
23225
|
const forcedType = overrideTypeByKey.get(p.url.replace(/\/$/, ""));
|
|
23226
|
+
tracker.unitDone(p.url);
|
|
23098
23227
|
return {
|
|
23099
23228
|
url: p.url,
|
|
23100
23229
|
pageType: forcedType ?? detectPageType(p.url, $, structuredData, meta, isFirstPage),
|
|
@@ -23112,13 +23241,12 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23112
23241
|
p.a11yResults = await runA11yForHtml(p.fetchResult.body, p.url, A11Y_RULES);
|
|
23113
23242
|
})
|
|
23114
23243
|
);
|
|
23115
|
-
|
|
23244
|
+
tracker.phaseDone();
|
|
23116
23245
|
logger.debug(
|
|
23117
23246
|
{ pagesAnalyzed: pages.length },
|
|
23118
23247
|
"[orchestrator] Phase 2 complete: Page analysis complete"
|
|
23119
23248
|
);
|
|
23120
23249
|
signal?.throwIfAborted();
|
|
23121
|
-
await progress(60, "Running audits");
|
|
23122
23250
|
logger.debug("[orchestrator] Phase 3: Running audits");
|
|
23123
23251
|
const wafProtection = detectWafProtection(url, homepageResult, rootFiles, pages.length);
|
|
23124
23252
|
const ctx = {
|
|
@@ -23126,19 +23254,27 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23126
23254
|
pages,
|
|
23127
23255
|
domain,
|
|
23128
23256
|
baseUrl,
|
|
23129
|
-
fetch: (
|
|
23257
|
+
fetch: (options2) => fetcher.fetch({ ...options2, signal }),
|
|
23130
23258
|
wafProtection: wafProtection ?? void 0
|
|
23131
23259
|
};
|
|
23260
|
+
const auditPlan = planAudits(ctx, defaultConfig);
|
|
23261
|
+
tracker.phaseStart("audits", auditPlan.runnable.length);
|
|
23132
23262
|
const {
|
|
23133
23263
|
checks: allChecks,
|
|
23134
23264
|
categories,
|
|
23135
23265
|
overallScore
|
|
23136
|
-
} = await runAudits(
|
|
23137
|
-
|
|
23138
|
-
|
|
23139
|
-
|
|
23266
|
+
} = await runAudits(
|
|
23267
|
+
ctx,
|
|
23268
|
+
defaultConfig,
|
|
23269
|
+
(event) => {
|
|
23270
|
+
if (event.type === "unit:done") tracker.unitDone(event.label);
|
|
23271
|
+
else tracker.unitFail(event.label, event.error);
|
|
23272
|
+
},
|
|
23273
|
+
auditPlan
|
|
23274
|
+
);
|
|
23275
|
+
tracker.phaseDone();
|
|
23140
23276
|
logger.debug("[orchestrator] Phase 3 complete: Audits finished");
|
|
23141
|
-
|
|
23277
|
+
tracker.phaseStart("report", 1);
|
|
23142
23278
|
logger.debug("[orchestrator] Phase 4: Building final report");
|
|
23143
23279
|
const durationMs = Math.round(performance.now() - start);
|
|
23144
23280
|
const recommendations = allChecks.filter((c) => c.status !== "pass").slice().sort((a, b) => {
|
|
@@ -23155,7 +23291,6 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23155
23291
|
const topPasses = allChecks.filter((c) => c.status === "pass").slice().sort(
|
|
23156
23292
|
(a, b) => (weightMap.get(b.id) ?? 1) - (weightMap.get(a.id) ?? 1)
|
|
23157
23293
|
).slice(0, 10);
|
|
23158
|
-
await progress(100, "Complete");
|
|
23159
23294
|
const readinessVitals = calculateReadinessVitals(allChecks);
|
|
23160
23295
|
const readinessScore = Math.round(
|
|
23161
23296
|
readinessVitals.commerce * READINESS_WEIGHTS.commerce + readinessVitals.content * READINESS_WEIGHTS.content + readinessVitals.botAccessibility * READINESS_WEIGHTS.botAccessibility + readinessVitals.technical * READINESS_WEIGHTS.technical
|
|
@@ -23185,6 +23320,9 @@ async function runScan(url, onProgress, pageOverrides, signal) {
|
|
|
23185
23320
|
productFields: [...overrideTypeByKey.values()].includes("product") ? extractProductFieldVerification(pages) : void 0
|
|
23186
23321
|
};
|
|
23187
23322
|
report.summary = generateScanSummary(report);
|
|
23323
|
+
tracker.unitDone();
|
|
23324
|
+
tracker.phaseDone();
|
|
23325
|
+
tracker.scanDone(overallScore);
|
|
23188
23326
|
logger.debug({ durationMs, score: overallScore }, "[orchestrator] runScan complete");
|
|
23189
23327
|
return report;
|
|
23190
23328
|
}
|
|
@@ -23369,7 +23507,9 @@ export {
|
|
|
23369
23507
|
MAX_PAGES_PER_SCAN,
|
|
23370
23508
|
MAX_RESPONSE_BODY_BYTES,
|
|
23371
23509
|
PAGE_TYPE_LABELS,
|
|
23510
|
+
PHASE_WEIGHTS,
|
|
23372
23511
|
PRESETS,
|
|
23512
|
+
ProgressTracker,
|
|
23373
23513
|
READINESS_WEIGHTS,
|
|
23374
23514
|
REQUEST_TIMEOUT_MS,
|
|
23375
23515
|
SCANNER_USER_AGENT,
|
|
@@ -23413,6 +23553,7 @@ export {
|
|
|
23413
23553
|
logger,
|
|
23414
23554
|
normalizeUrl,
|
|
23415
23555
|
parseHtml,
|
|
23556
|
+
planAudits,
|
|
23416
23557
|
runAudits,
|
|
23417
23558
|
runScan
|
|
23418
23559
|
};
|