Functions

The package is ESM. Everything below is a named export of safi-studio-scanner.

import {
  audit,
  auditToHtml,
  auditToMarkdown,
  auditScore,
  render,
  allRules,
  selectRules,
  DEFAULTS,
} from "safi-studio-scanner";

audit

function audit(
  startUrl: string,
  options?: Partial<AuditOptions>,
): Promise<AuditReport>;

Crawls from startUrl, runs the selected rules against every page, scores the findings, and resolves to the report object. This is the only function that performs a run; everything else either wraps it or formats its output.

const report = await audit("https://example.com", {
  maxPages: 20,
  concurrency: 5,
  skip: ["links"],
});

Order of work: fetch robots.txt and sitemap.xml, crawl the pages, render them if a browser or a PageSpeed key was supplied, run the rules, aggregate.

Throws if startUrl is not a valid URL. A page that fails to fetch is still reported, carrying its status and error rather than taking the run down.

auditToHtml

function auditToHtml(
  url: string,
  options?: Partial<AuditOptions>,
): Promise<string>;

Runs an audit and returns the report as one self-contained HTML document: inline styles, no external requests. Write it to a file, serve it, or attach it to an email.

await writeFile("report.html", await auditToHtml("https://example.com"));

auditToMarkdown

function auditToMarkdown(
  url: string,
  options?: Partial<AuditOptions>,
): Promise<string>;

Runs an audit and returns Markdown: a score summary, then per-category headings with a findings table.

auditScore

function auditScore(
  url: string,
  options?: Partial<AuditOptions>,
): Promise<number>;

Runs an audit and returns only the overall score, 0 to 100. The whole audit still runs, so this is a convenience, not a fast path.

render

function render(report: AuditReport, format: "json" | "md" | "html"): string;

Formats a report you already have. Synchronous. Audit once, emit every format:

const report = await audit(url);
await writeFile("report.html", render(report, "html"));
await writeFile("report.json", render(report, "json"));
await writeFile("report.md", render(report, "md"));

allRules

const allRules: Rule[];

Every built-in rule, in registration order. 94 of them.

console.log(allRules.length);
console.log(new Set(allRules.map((r) => r.category)).size); // 15

auditStream

function auditStream(url: string, options?: Partial<AuditOptions>): AuditStream;

class AuditStream implements AsyncIterable<AuditEvent> {
  on<T extends AuditEventType>(type: T, handler: (event: AuditEventOf<T>) => void): this;
  on(type: "event", handler: (event: AuditEvent) => void): this;
  off(type: string, handler: (event: never) => void): this;
  finalReport(): Promise<AuditReport>;
  [Symbol.asyncIterator](): AsyncIterator<AuditEvent>;
}

Runs the same audit as audit() and reports progress while it does. The audit starts on the call, not on the first read.

const stream = auditStream("https://example.com", { maxPages: 20 });
for await (const event of stream) {
  if (event.type === "page_complete") console.log(event.index, event.url);
}
const report = await stream.finalReport();

Iterating and .on() can be used together, and both see every event. finalReport() resolves with the same object the audit_complete event carries; awaiting it without iterating runs the audit exactly as audit() would. A failed audit throws from the iterator and rejects finalReport().

See streaming progress for the event list and worked examples.

selectRules

function selectRules(rules: Rule[], only?: string[], skip?: string[]): Rule[];

Filters a rule array by category. only is applied first, then skip removes from what is left. This is the same function audit() uses internally, exported so you can preview exactly what a set of options will run.

const chosen = selectRules(allRules, undefined, ["links", "images"]);
console.log(`${chosen.length} rules will run`);

It is generic over anything with a category, so it filters collectors too.

allCollectors

const allCollectors: Collector[];

Every built-in measurement collector. A collector returns numbers rather than verdicts; see the measurement catalog for what each one reports.

import { allCollectors, selectRules } from "safi-studio-scanner";

const linkMetrics = selectRules(allCollectors, ["links"]);

value and table

function value(
  id: string,
  label: string,
  v: string | number | null,
  opts?: { unit?: string; status?: ValueStatus; note?: string },
): MetricInput;

function table(
  id: string,
  label: string,
  columns: string[],
  rows: MetricCell[][],
  opts?: { cap?: number; status?: ValueStatus; note?: string },
): MetricInput;

The helpers a collector builds its output with. table applies the row cap (25 by default) and records the true row count, so a renderer can print "showing 25 of 340". See writing a collector.

DEFAULTS

const DEFAULTS: AuditOptions;

The default option object. Read it rather than hard-coding values, so your code follows the package if a default changes.

const report = await audit(url, { ...DEFAULTS, maxPages: 100 });

Types

Every type is exported as well: AuditOptions, AuditReport, PageReport, CategoryScore, Finding, Rule, RuleResult, RuleContext, PageContext, SiteContext, Format, Status, Severity. See types.