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

costguard

v0.6.0

Published

Detects expensive code patterns — runaway Firebase reads, missing real-time listener cleanup, polling abuse, and React render loops — before they hit production.

Readme

Stop Firebase bill shock before it hits production — CostGuard catches expensive patterns as you type, blocks risky commits, and gates your deploys.

CostGuard demo

CostGuard

Catch expensive Firebase and React patterns before they hit production — and before they hit your bill.

CostGuard is a VS Code extension that detects runaway Firestore reads, missing listener cleanup, render loops, and other cost-heavy patterns as you write code. It adds inline squiggles, a per-file risk score, and optional gates that block bad code from being committed, merged, or deployed.

Install from the VS Code Marketplace →


What it catches

Example: one unbounded read can quietly cost ~$90/month

// Bad — reads every document in the collection, on every load
const snap = await getDocs(collection(db, 'invoices')); // ← FCG002

If invoices has 10,000 documents and this query runs on a dashboard visited by 500 users a day:

10,000 docs × 500 loads/day = 5,000,000 reads/day
5,000,000 × 30 days         = 150,000,000 reads/month
150,000,000 / 100,000 × $0.06 (Firestore read price) ≈ $90/month

— for one query, before the feature that uses it has shipped. CostGuard flags this the instant you type it, with the fix inline:

// Fix — cap it, then paginate or query for more as needed
const snap = await getDocs(query(collection(db, 'invoices'), limit(50)));

Features

Live diagnostics

Squiggles appear as you type (500ms debounce). No save required.

Copy fix prompt for AI

Click the lightbulb (or Cmd/Ctrl+.) on any squiggle and choose Copy fix prompt for AI. CostGuard copies the violation message plus the surrounding code to your clipboard as a ready-to-paste prompt — drop it straight into ChatGPT, Claude, or Copilot Chat to get a fix.

Risk scoring

Every flagged file gets a score and a breakdown by risk category, visible inline and in the status bar.

$(shield) Risk Score: 82/100   Cost: HIGH   Scalability: MEDIUM   Memory Leak: HIGH

Deployment gates

Three layers that stop risky code before it ships:

| Gate | When it runs | Blocks on | |---|---|---| | Pre-commit hook | git commit | HIGH risk | | GitHub Actions PR gate | Every pull request | HIGH risk | | Deploy gate | firebase deploy / npm run deploy | MEDIUM+ risk |


Installation

From the Marketplace (recommended): open VS Code → Extensions → search for CostGuard, or run:

code --install-extension soarone.costguard

From a VSIX (offline installs): download the .vsix from the GitHub releases page, then Extensions··· menu → Install from VSIX.

After install, the setup prompt appears automatically — choose which protection layers to enable.


Setup Wizard

On first install (and after major version upgrades), a notification appears a few seconds after VS Code loads. Click Set Up Now to open the protection-layer picker:

CostGuard — Choose your protection layers

  ●  Pre-commit Hook         Block commits with HIGH risk violations
  ●  GitHub Actions PR Gate  Post risk card on PRs, block HIGH risk merges
  ○  Deploy Gate             Block firebase deploy / npm run deploy on MEDIUM+ risk

Select what you want, press Enter. CostGuard writes all the necessary files automatically — no manual path setup.

To re-run the wizard at any time: Command PaletteCostGuard: Setup


What it detects

FCG001 — Unstable useEffect dependency error

Objects, arrays, functions, or call results used as useEffect dependencies without useMemo/useCallback. Every render creates a new reference, causing the effect to re-run infinitely — the root cause of most Firestore read runaway bills.

// Bad — query is a new object on every render
const query = { collection: 'invoices', status: 'open' };
useEffect(() => { fetchData(query); }, [query]); // ← FCG001

// Fix
const query = useMemo(() => ({ collection: 'invoices', status: 'open' }), []);

FCG002 — Unbounded Firestore read error

getDocs or collection calls without .limit(). On a large collection this can read thousands of documents in one call.

// Bad
const snap = await getDocs(collection(db, 'invoices')); // ← FCG002

// Fix
const snap = await getDocs(query(collection(db, 'invoices'), limit(50)));

FCG003 — Real-time listener on UI state warning

onSnapshot listeners that re-register whenever UI state like activeTab, selectedId, or showModal changes. Each re-registration opens a new listener and the old one may not be cleaned up.

// Bad — re-registers every time activeTab changes
useEffect(() => {
  return onSnapshot(collection(db, activeTab), ...);
}, [activeTab]); // ← FCG003

FCG004 — onSnapshot without cleanup error

Real-time listeners must return their unsubscribe function. Without it, every component mount adds a new listener that never closes.

// Bad — listener leaks on every remount
useEffect(() => {
  onSnapshot(collection(db, 'invoices'), handler); // ← FCG004
}, []);

// Fix
useEffect(() => {
  return onSnapshot(collection(db, 'invoices'), handler); // return the unsub
}, []);

FCG005 — Firestore read inside a loop error

getDoc or getDocs calls inside for, while, forEach, map, or similar constructs. Each iteration makes a separate network round-trip (N+1 reads).

// Bad — one read per invoice
for (const id of invoiceIds) {
  const doc = await getDoc(doc(db, 'invoices', id)); // ← FCG005
}

// Fix — use getAll / batched reads / a single query

FCG006 — setInterval without cleanup error

setInterval inside useEffect without a corresponding clearInterval return. Intervals stack up on every remount, polling Firebase endlessly.

// Bad
useEffect(() => {
  setInterval(() => fetchStats(), 5000); // ← FCG006
}, []);

// Fix
useEffect(() => {
  const id = setInterval(() => fetchStats(), 5000);
  return () => clearInterval(id);
}, []);

FCG007 — addEventListener without cleanup error

addEventListener inside a useEffect with no removeEventListener returned. Each remount adds a duplicate listener that's never removed, so handlers fire multiple times and the component leaks memory.

// Bad — listener leaks on every remount
useEffect(() => {
  window.addEventListener('resize', handler); // ← FCG007
}, []);

// Fix
useEffect(() => {
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

FCG008 — fetch/axios inside a loop error

fetch or axios calls inside for, while, forEach, map, or similar constructs. The same N+1 problem as FCG005, but for any billed HTTP API (Stripe, OpenAI, maps, etc.) instead of Firestore.

// Bad — one HTTP request per item
for (const id of ids) {
  await fetch(`/api/items/${id}`); // ← FCG008
}

// Fix — fan out in parallel or use a bulk endpoint
await Promise.all(ids.map(id => fetch(`/api/items/${id}`)));

FCG009 — Firestore read in component body error

getDoc/getDocs (or a Cloud Function call) made directly in a component or hook body instead of inside useEffect. It re-fires on every render with no caching — a component re-rendering 10×/second runs 10 reads/second.

// Bad — refetches every render
function Invoice({ id }: { id: string }) {
  const snap = getDoc(doc(db, 'invoices', id)); // ← FCG009
}

// Fix
useEffect(() => { getDoc(doc(db, 'invoices', id)).then(setInvoice); }, [id]);

FCG010 — Compound render loop error

A useEffect with both an unstable dependency (new object/array/function reference every render) and an expensive operation (getDoc, onSnapshot, fetch) in its body. The two problems amplify each other: the effect re-runs every render, fires the expensive call, which may update state and re-render — an infinite billing loop.

// Bad — config is a new reference every render, and onSnapshot reads on every re-run
const config = getFirebaseConfig();
useEffect(() => {
  return onSnapshot(collection(db, 'dunning_log'), handler);
}, [config]); // ← FCG010

// Fix — stabilize the dependency first
const config = useMemo(() => getFirebaseConfig(), []);

FCG011 — Expensive operation in a high-frequency handler error

getDoc, fetch, or similar calls inside scroll, mousemove, resize, keydown, or input handlers with no debounce/throttle. These events fire up to hundreds of times per second.

// Bad — fires a read on every scroll event
window.addEventListener('scroll', () => getDoc(ref)); // ← FCG011

// Fix
window.addEventListener('scroll', debounce(() => getDoc(ref), 300));

FCG012 — Unbatched Firestore writes in a loop error

addDoc/setDoc/updateDoc/deleteDoc inside a loop fires one separate billed write and network round-trip per iteration.

// Bad — one write per item
for (const item of items) {
  await setDoc(doc(db, 'items', item.id), item); // ← FCG012
}

// Fix — up to 500 ops per batch
const batch = writeBatch(db);
items.forEach(item => batch.set(doc(db, 'items', item.id), item));
await batch.commit();

FCG013 — Polling Firestore with setInterval warning

setInterval/recursive setTimeout wrapping getDoc/getDocs polls on every tick regardless of whether the data changed — every tick is a billed read.

// Bad — billed read every 5 seconds, changed or not
setInterval(() => getDocs(collection(db, 'orders')), 5000); // ← FCG013

// Fix — only fires when data actually changes
onSnapshot(collection(db, 'orders'), handler);

FCG014 — Client-side filtering of an unfiltered read warning

.filter()/.find() applied to a getDocs() result with no .where() clause. The entire collection is fetched and billed before most of it is discarded.

// Bad — fetches everything, keeps a fraction
const snap = await getDocs(collection(db, 'orders'));
const pending = snap.docs.filter(d => d.data().status === 'pending'); // ← FCG014

// Fix — filter server-side
const q = query(collection(db, 'orders'), where('status', '==', 'pending'));
const snap = await getDocs(q);

FCG015 — Read-modify-write instead of FieldValue atomics warning

Reading a document, mutating an array/counter field in JS, then writing the whole document back. Costs an extra read and loses updates under concurrent writes.

// Bad — extra read, race condition under concurrent writes
const data = (await getDoc(ref)).data();
await updateDoc(ref, { count: data.count + 1 }); // ← FCG015

// Fix — atomic, no read needed
await updateDoc(ref, { count: increment(1) });

FCG016 — Cloud Function defined but not exported warning

A Cloud Function assigned to a variable without export. Firebase will never deploy it — it's dead weight in the functions bundle that slows cold starts for every other function in the file.

// Bad — never deployed
const onUserCreate = onDocumentCreated('users/{id}', handler); // ← FCG016

// Fix
export const onUserCreate = onDocumentCreated('users/{id}', handler);

FCG017 — Cloud Function invoked in a loop error

A function created with httpsCallable() invoked inside a loop. Each call bills a separate Cloud Function execution — N items costs N executions instead of one.

// Bad — one billed execution per item
for (const item of items) {
  await sendNotification(item); // ← FCG017
}

// Fix — redesign for a batch payload, or fan out in parallel
await Promise.all(items.map(item => sendNotification(item)));

Suppressing a violation

The rules are heuristics — when one flags code you've decided is fine, suppress it inline (ESLint-style). The suppression is honored everywhere: editor squiggles, the CLI, all three gates, and reports.

// costguard-disable-next-line FCG002
const snap = await getDocs(collection(db, 'smallLookupTable'));

const snap = await getDocs(collection(db, 'tiny')); // costguard-disable-line

// costguard-disable-file FCG014   ← at top of file: suppress FCG014 in the whole file

Omit the rule codes to suppress every rule on that line (// costguard-disable-next-line). The quickest way to add one: click the lightbulb on any squiggle → Suppress FCGxxx on this line.


Risk scoring

Each violation carries a point weight based on its real-world cost impact. Scores are capped at 100.

| Rule | Risk categories | Points | |---|---|---| | FCG001 Unstable deps | Cost + Memory Leak | 10 | | FCG002 Unbounded read | Cost + Scalability | 18 | | FCG003 Listener UI dep | Cost | 12 | | FCG004 No snapshot cleanup | Memory Leak | 22 | | FCG005 Read in loop | Cost + Scalability | 20 | | FCG006 No interval cleanup | Memory Leak | 18 | | FCG007 No event listener cleanup | Memory Leak | 15 | | FCG008 Fetch/axios in loop | Cost + Scalability | 20 | | FCG009 Read in render | Cost + Scalability | 16 | | FCG010 Compound render loop | Cost + Scalability + Memory Leak | 35 | | FCG011 Expensive op in high-freq handler | Cost + Scalability | 25 | | FCG012 Unbatched writes in loop | Cost + Scalability | 20 | | FCG013 Polling instead of onSnapshot | Cost + Scalability | 18 | | FCG014 Client-side filter | Cost + Scalability | 16 | | FCG015 Read-modify-write | Cost | 12 | | FCG016 Unexported Cloud Function | Cost + Scalability | 10 | | FCG017 Cloud Function in loop | Cost + Scalability | 25 |

Risk levels per category

| Points | Level | |---|---| | 0 | LOW | | 1 – 24 | MEDIUM | | 25+ | HIGH |


Deployment gates

Pre-commit hook

Blocks git commit if staged files contain HIGH risk violations.

  CostGuard
  ────────────────────────────────────────────────────────────

  src/invoices.tsx
  Risk 58/100  |  Cost: HIGH  |  Scalability: LOW  |  Memory Leak: HIGH
    ✗  Line 42  [FCG002]  Unbounded Firestore read — add .limit()
    ✗  Line 71  [FCG004]  onSnapshot missing cleanup return

  ────────────────────────────────────────────────────────────
  2 violations in 1 file

  ✗  Blocked — fix HIGH risk violations before proceeding.

Installed automatically by the setup wizard into your git hooks directory — .git/hooks/pre-commit by default, or wherever core.hooksPath points (so husky setups work too).

GitHub Actions PR gate

Runs on every pull request and posts a risk card comment. Fails the required check if HIGH risk violations are found, blocking the merge.

CostGuard PR gate risk card

The workflow is written to .github/workflows/costguard.yml by the setup wizard. Requires costguard in devDependencies (added automatically).

Deploy gate

Blocks deploys when MEDIUM+ risk violations are found — wired in two places by the setup wizard:

  • firebase.json predeploy hooks — added to every deploy target present (hosting, functions, firestore, storage, database), so firebase deploy (and firebase deploy --only <target>) is gated directly.
  • package.json predeploy script — runs automatically before npm run deploy.
firebase deploy      # gated via firebase.json predeploy hooks
npm run deploy       # gated via the npm predeploy script

If your project has no firebase.json, only the npm script gate is installed.


CLI

The analyzer is also available as a command-line tool for use in scripts and CI pipelines.

# Install (or use npx without installing)
npm install -D costguard

# Scan a directory
npx costguard src/

# Scan only staged files (for pre-commit hooks)
npx costguard --staged

# Output JSON for downstream tooling
npx costguard src/ --json

# GitHub Actions annotation format
npx costguard src/ --format=github

# Set the blocking threshold (default: HIGH)
npx costguard src/ --max-risk=MEDIUM

Exit codes: 0 = no violations at or above the --max-risk threshold · 1 = violations at or above the threshold (or invalid arguments)


Configuration

| Setting | Default | Description | |---|---|---| | costGuard.enable | true | Enable / disable all diagnostics |

Toggle from Settings → search costGuard, or add to your workspace settings.json:

{
  "costGuard.enable": false
}

Commands

| Command | Description | |---|---| | CostGuard: Setup | Re-run the feature setup wizard | | CostGuard: Show Risk Score Details | Show full risk breakdown for the active file | | CostGuard: Generate Report | Scan the workspace and write a markdown report (with a copy-paste AI fix prompt) to costguard/ | | CostGuard: Copy Fix Prompt for AI | Copy the selected violation + surrounding code as an AI-ready prompt (also on the lightbulb menu) |