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

@acsaven/astro-ops

v0.2.4

Published

Production gates for Astro sites — content-hashed build ids, freshness watchdogs, performance budgets, discovery wiring, and a no-third-party-asset tripwire.

Readme

@acsaven/astro-ops

Production gates for Astro sites: the operational work that keeps a site correct six months later, when nobody is looking at it.

Extracted from four sites that run these gates in production — not written as a demo. It exists because the same ~20 build scripts had been copy-pasted into every repo and were drifting. Every check here answers something that went wrong once, and the comment above it says what.

Status: stable. All seven gates are built, tested, and running in production on three live sites. template/ is a working Astro site with every gate wired into its build and CI — green out of the box, and each documented failure verified to fail.


New to this codebase? Read CODE_GUIDE.md — it explains how the internals work and why, assuming no prior knowledge.

Why this exists

The four sites it came from had accumulated the same ~20 build scripts, copy-pasted into each repo. Measured before extraction:

| | | |---|---| | Core scripts still byte-identical across 3 repos | 7 of 12 | | The other 5 | differed by 1–4 lines — a site name, a path, a threshold |

The logic never diverged. Only the config did. But the copies were drifting anyway, because a fix landed in whichever repo hit the bug first and the other two kept the defect. One of them had been carrying a known-broken build id for months after another repo had already fixed it.

That is the whole thesis: one maintained implementation, one config file per site.

What is in the box

Each gate answers a failure that a normal test suite cannot see, because in every case the origin is fine and only the edge, the crawler, or the calendar is wrong.

| Gate | Catches | Command | |---|---|---| | Source integrity | A machine out of disk silently emptying your source files mid-build, and a build about to do it | check:integrity | | Content-hashed build id | A deploy silently serving last week's HTML from a cache that was never invalidated | check:build-id | | Freshness watchdog | A fact you verified once that the authority changed without telling you | check:freshness | | External-asset tripwire | A third-party script, font or embed that appeared without anyone deciding to add it | check:external | | Discovery wiring | A sitemap and a set of pages that no longer agree — noindex conflicts, canonicals pointing at 404s | check:discovery | | Link integrity | A link to a page you no longer ship, and trailing-slash drift that costs a redirect hop | check:links | | Performance budgets | A slow regression nobody would have blocked, because the score was only ever a dashboard | check:budgets |

astro-ops check runs all seven and reports every failure, not just the first — a CI run that surfaces one problem per push turns a five-minute fix into five pushes.

Integrity runs first, because every other gate reads files: if the disk emptied some of them, a link checker finds no links and a claims scanner finds no claims, and the run goes green on a repository that was just damaged. A write that runs out of space does not throw — it leaves the file empty, and nothing downstream notices, because an emptied module simply exports nothing and a deleted test reports zero failures. Set integrity.minFreeBytes: 0 to skip the disk floor on a platform that cannot report free space; the empty-file scan still runs.

Install

From npm:

npm i -D @acsaven/astro-ops

Or as a git dependency, which is how the Try family installs it:

// package.json
{
  "devDependencies": {
    "@acsaven/astro-ops": "github:acsavenhq/astro-ops#v0.2.2"
  }
}

npm ci resolves git dependencies natively, so CI needs no extra setup.

Zero runtime dependencies. It installs into your build pipeline, so every dependency it carried would become one you inherit — it carries none.

Updating

Nothing here updates itself. npm never pushes a new version at an installed project: a range only re-resolves on a fresh install or an explicit npm update, and a committed package-lock.json pins the resolved version until something changes it.

What a given consumer gets:

| declared | fresh install | npm update | left alone | | ------------------- | ------------- | ------------ | ---------- | | ^0.2.2 / ~0.2.2 | newest 0.2.x | newest 0.2.x | unchanged | | 0.2.2 exact | 0.2.2 | 0.2.2 | unchanged | | github:…#v0.2.2 | v0.2.2 | v0.2.2 | unchanged |

The Try sites pin the git tag deliberately. They used to say github:acsavenhq/astro-ops with no ref, so every install pulled whatever main happened to be — meaning the gates guarding a deploy could change without anything recording that they had. A pinned tag costs a manual bump and buys the ability to say which version of the checks a given release actually passed.

To move a site to a new release:

npm i -D github:acsavenhq/astro-ops#v0.2.3
npx astro-ops check          # confirm the gates still pass before deploying

Cutting a release, for whoever does it next:

npm test                     # the gates gate themselves
npm version patch            # or minor / major
git push && git push --tags
npm publish --access public  # 2FA prompts in a browser

Publishing to npm and tagging git are separate acts, and both are needed: the registry serves anyone who installs by name, the tag serves the sites that install by ref. Shipping one without the other leaves the two disagreeing about what 0.2.x means.

Quick start

Add the gate to your build and your CI:

// package.json
{
  "scripts": {
    "postbuild": "astro-ops build-id",
    "check": "astro-ops check"
  }
}
# .github/workflows/ci.yml
- run: npm run build
- run: npm run check          # fails if the committed build id is stale

Then use the id in whatever keys your edge cache:

import { BUILD_ID } from './build-id.js';

const key = new URL(request.url);
key.searchParams.set('__build', BUILD_ID);

Commit build-id.js. That is the point — see below.

Configuration

Optional. A project with no config file gets the defaults, which are the values that were actually running in production rather than a neutral guess.

// astro-ops.config.mjs
export default {
  buildId: {
    include: ['dist'],          // hashed — what you DEPLOY, not what you wrote
    out: 'build-id.js',
    constName: 'BUILD_ID',
    length: 16,
  },

  external: {
    // Empty by default, on purpose. Every third party you accept has to be named here,
    // one at a time, so the list is visible and grows where you can see it.
    allowHosts: ['api.producthunt.com'],
  },

  freshness: {
    // Point it at the file holding your claims. It reads `key: 'value'` literals without
    // importing or parsing the file, so TypeScript is fine.
    scan: { file: 'src/data/specs.ts' },
    recheckMonths: 12,
    warnWindowDays: 45,
    // Optional second tripwire: a feed of { claimId: { detectedAt } } that something you
    // run — typically a weekly cron — updates when a source page's hash changes.
    driftApi: 'https://example.com/api/freshness',
    // Fail when a claim names no source. Off by default; on is stricter and better.
    requireSourceUrl: false,
  },

  discovery: {
    ignoreRoutes: [],
    rules: { maxTitle: 60, maxDescription: 160, requireJsonLd: false },
  },

  budgets: {
    // Null skips the gate. Point it at a server you started in CI.
    url: 'http://127.0.0.1:4173/',
    categories: {
      accessibility: { min: 90, blocking: true },
      performance: { min: 80, blocking: false },
    },
  },
};

Why performance is advisory by default

Lighthouse performance moves several points between runs on identical code, and further between a laptop and a loaded CI runner. Made blocking at a tight threshold it fails randomly, people learn to re-run until it passes, and a gate understood as a coin flip protects nothing. Accessibility is close to deterministic, so it blocks. Set performance blocking only at a threshold loose enough that tripping it means something really broke.

Array options replace the defaults rather than merging with them, so you can drop a default you disagree with and your config file always shows the effective value.

If you have to edit a script inside this package to make it fit your project, that is a missing config option — please report it. Patching locally puts you straight back in the copy-drift trap.


The build id, and why it is not a timestamp

This is the module people are most likely to think they can write in five minutes, so it is worth the detail.

A build id keys your edge cache. Change the id, and every cached page becomes unreachable at once — that is how you invalidate a CDN that has no purge API worth trusting.

The trap: most projects grow a second way to deploy — CI plus a manual command, or a host's git integration plus a CLI. That is good; either can ship when the other is down. But it means the same commit can be deployed by two pipelines, and a random or timestamped id cannot survive that:

  • A git-integration build usually has no build step. It uploads the repo as-is, so it ships whatever is committed in your build-id file.
  • A local deploy command rotates that file on disk. Unless you commit the result, the repo still holds the old value.

Push a content change while the rotated id sits uncommitted, and you ship new HTML under the previous id. The cache key never changed, so every colo keeps serving the old page. The origin is correct. The deploy "succeeded". Nothing in CI can see it.

This is not hypothetical. It is the failure this module was written in response to: a page whose origin served the new content while the public URL kept returning the pre-change copy, from every colo, until someone thought to look at the cache key.

Hashing the deployed content fixes it at the root:

  • Both pipelines compute the same id for the same commit, so the committed value is always correct.
  • Any real change produces a new id, so the cache still busts exactly when it should.
  • It is idempotent — running it twice is a no-op, so your working tree stops drifting and the file stops appearing in unrelated diffs.

A random id also throws away your entire edge cache on every deploy, which is why a long TTL never seems to pay off. Content hashing keeps the warm cache across deploys that did not change anything.

astro-ops check:build-id is the half that matters. Emitting the id is easy to remember on the day you set it up. The check is what turns "someone forgot to regenerate" from a silent stale-cache bug into a failed build, a year later, when you have forgotten this file exists.

Exit codes

Because these run unattended and the output is all a human sees when one fails at 2am:

| Code | Meaning | |---|---| | 0 | Pass | | 1 | A gate failed — the site has a problem | | 2 | The tool is misconfigured — a config error must not look like a content failure |

Development

npm test        # node:test, no test-framework dependency

The tests are not incidental. Each one pins a property that the guarantee "both pipelines compute the same id for the same commit" depends on — stable sort order across platforms, path-sensitivity so a rename busts the cache, \0 delimiters so swapping two files' contents cannot collide, and a fileCount of zero surfacing as an error instead of a constant id forever.


The starter

template/ is a complete Astro site with all seven gates wired into its build and CI. Clone it, run npm install && npm run check, and every gate reports.

It scores 100 / 100 / 96 / 100 on Lighthouse, carries zero npm vulnerabilities, and its README lists five ways to deliberately break it — each verified to produce the failure it claims, not asserted.