npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@webgrip/astro-site-toolkit

v0.6.0

Published

Build-time guards and rigs shared by Webgrip Astro static sites: the CSP validator, the production-faithful dist server, the axe scan engine, the mail-auth drift check, the copy-claims guard, and the newsletter mail pipeline.

Readme

@webgrip/astro-site-toolkit

Build-time guards and rigs shared by Webgrip Astro static sites. Extracted after the two sites' copies of the CSP validator diverged 125 vs 171 lines within a month — copy-reuse demonstrably fails for these; this package is reference-reuse.

Bins

  • webgrip-validate-csp [dist] — asserts every governed inline script AND stylesheet is authorised by its page's own meta CSP, per directive (a style-src hash cannot authorise a script). Wire into the build: "build": "astro build && webgrip-validate-csp".
  • webgrip-serve-dist [dist] — serves dist/ with Cloudflare's auto-trailing-slash semantics and gzip, for Lighthouse rigs that must measure the site, not the server. PORT=0 binds ephemerally; the bound port is printed.

Axe engine

// scripts/axe-scan.ts — the thin per-site file that stays in the consumer:
import { runAxeScan } from '@webgrip/astro-site-toolkit/axe-engine';

await runAxeScan({
  // Pages that matter for a11y but carry no Lighthouse budget — with each
  // site's own argued rationale. This is exactly the part that must NOT be
  // shared.
  a11yOnlyPages: ['/404.html'],
});

Ships TypeScript declarations (lib/axe-engine.d.mts), so a strict consumer needs no shims. Peer deps (consumer installs): @axe-core/playwright, playwright-core. The page list is read from lighthouserc.json so the two gates cannot drift apart.

Rules axe ships disabled

axe-core 4.13 ships 16 of its 105 rules with enabled: false, so a default analyze() does not run them. The scan turns on the eight that map to WCAG 2.2 A/AA, the standard the estate's sites commit to: aria-roledescription, audio-caption, css-orientation-lock, label-content-name-mismatch, p-as-heading, table-fake-caption, target-size and td-has-header. target-size and label-content-name-mismatch are the two that catch defects nothing else here does — a control too small to hit, and a control whose visible text is missing from the name voice control matches against.

The other eight stay off on purpose. color-contrast-enhanced, identical-links-same-purpose and meta-refresh-no-exceptions are AAA, which W3C advises against adopting as a blanket policy. duplicate-id and duplicate-id-active test a criterion WCAG 2.2 removed. focus-order-semantics, hidden-content and landmark-complementary-is-top-level carry no WCAG tag and report style preferences, and hidden-content is noisy enough that Deque flags it as such.

Pass enableRules to override the set; [] restores axe's own defaults.

Mail-auth drift

  • webgrip-validate-mail-auth [intent] (default ops/mail-auth.intent.yml) — reads a per-site intent file that declares, per domain, what should be published, and fails when the world disagrees: MX, SPF, DKIM key length, DMARC policy and report addresses, CAA, DNSSEC, and the MTA-STS policy (DNS records, served as text/plain, byte-identical to the repo copy). Exit 1 on drift, 2 without an intent file. Run it nightly from a scheduled workflow; wire it as "validate:mail-auth": "webgrip-validate-mail-auth".

Two things a generic checker gets wrong are decisions in the intent, not in the engine: a domain whose spf is deliberately-absent fails when an SPF record appears, and a record that is published while the intent still says published: false fails, so the declared state cannot quietly fall behind the world. Steps the intent declares as not yet done are printed, not failed.

# ops/mail-auth.intent.yml
resolver: 8.8.8.8
mtaSts:
  policyInRepo: public/.well-known/mta-sts.txt
  policyUrl: https://mta-sts.example.nl/.well-known/mta-sts.txt
  dnsRecordsPublished: true
domains:
  example.nl:
    mx: [smtp.google.com]
    spf: v=spf1 include:_spf.google.com ~all
    dkim:
      - selector: google
        keyCharacters: 408
    dmarcPolicy: p=none
    dmarcReportsTo: [[email protected]]
    caa:
      published: true
      issuers: [letsencrypt.org]
    dnssec:
      dsPublished: true

mtaSts is optional; a site without a policy omits it. The engine is importable as @webgrip/astro-site-toolkit/mail-auth (auditMailAuth(intent, lookups)), with lookups injectable so a consumer can test its intent file against a fixture world without touching DNS.

Copy-claims guard

// src/lib/claims.test.ts — the per-site file; the rules are the site's, the engine is shared:
import { bannedCopyViolations, rulesWithoutProof } from '@webgrip/astro-site-toolkit/claims';
import type { ClaimRule } from '@webgrip/astro-site-toolkit/claims';

const FORBIDDEN: ClaimRule[] = [
  {
    name: 'em dash in copy',
    pattern: /—/,
    rationale: 'house style: rewrite the sentence',
    canonical: 'een zin — met kastlijntje',
  },
];

test('source copy carries no banned variants', () => {
  assert.deepEqual(bannedCopyViolations(FORBIDDEN, { roots: ['src/pages'] }), []);
});
test('every rule still matches its own canonical violation', () => {
  assert.deepEqual(rulesWithoutProof(FORBIDDEN), []);
});
  • bannedCopyViolations(rules, options) walks roots (plus extraFiles), keeps files by extensions (default: astro, md, mdx, yml, yaml, html), skips paths matching ignore, and reports file:line [rule] preview with the rationale. By default it scans copy only: Astro frontmatter, <script>/<style>, HTML comments and {expressions} are blanked (line numbers preserved), as are fenced code in Markdown, comments in TypeScript and YAML. A site that keeps copy in frontmatter passes extract: 'raw'. A line containing claims-allow is exempt. A rule with scope applies only to paths that match it; active: false parks a rule.
  • rulesWithoutProof(rules) is the mutation guard: every rule carries a canonical violation, and a rule whose pattern stops matching it is named, so a regex edit cannot silently disarm a rule.
  • retiredVocabularyViolations({ modelPath, exempt }) reads the retired list from a docs/domain/model.yaml (the domain-language skill's format) and sweeps every tracked file for those words, the model and exempt paths excepted. It returns { retired, violations }.
  • missingOgImages({ files, publicDir }) lists ogImage="…" references that point at no file under public/.
  • sourceFiles(options) and copyOf(path, raw) are exported for custom checks.

What stays in the site is exactly what differs per site: the rules, their rationale, the roots and the runtime facts (twente.dev derives its literal-release-fact pattern from the current release entry). Extracted from twente.dev and webgrip.nl, whose two copies had already grown apart in what they blanked and what they reported.

Newsletter mail pipeline

// scripts/build-mail.ts — the per-site glue; content, copy and brand stay in the site:
import { mailBuildCli } from '@webgrip/astro-site-toolkit/mail';

process.exit(
  await mailBuildCli({
    targets: await collectTargets(),
    locales: LOCALES,
    isLocale,
    outDir: 'build/mail',
    render: (document) => renderMail(document, MAIL_THEME),
    check: (document, html) => checkMail(document, html, { siteUrl, privacyUrl }),
  }),
);
  • renderMail(document, theme) turns a MailDocument (subject, preheader, kicker, headline, lead, facts, one call to action, the reason the reader gets it) into a single-column, table-based HTML mail with inline styles. The theme is the site's: siteUrl, siteName, a hosted logo, a seven-colour palette, two font stacks, and footer(locale) with the reply, unsubscribe and privacy copy. The unsubscribe link is Brevo's {{ unsubscribe }} tag, and a ⟦…⟧ placeholder that survived into the copy throws.
  • checkMail(document, html, options) returns { document, message }[]: exactly one unsubscribe tag, every href absolute https, a privacy link at privacyUrl(locale), subject and preheader within inbox limits and not empty, every image with an alt, no half-empty fact. Pass localePath(locale) on a bilingual site and a link that leaves the mail's locale is a finding too.
  • mailBuildCli(options) is pnpm mail: no arguments lists the targets, <id> [locale] writes the files, --all writes everything, --check renders and checks every target and is the CI gate. Returns the exit code.
  • brevoDraftCli(options) is pnpm mail:draft: renders, checks, and writes the mail into Brevo as a draft campaign named by campaignName(document), updating an existing draft of that name rather than creating a second. --dry-run prints the payload and never calls Brevo; without BREVO_API_KEY in the environment it stops. There is deliberately no send.
  • mailFilename, mailName, escapeHtml and slugify are exported for the site's sources.

What stays in the site: the MailDocument sources (which content becomes a mail, and with which facts), the copy per locale, and the theme. Extracted from twente.dev, where ADR 0017 holds the draft-not-send decision.