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

@f0rbit/eslint-plugin

v0.3.0

Published

Custom typed org rules for the f0rbit ecosystem — f0rbit/must-use-result over the structural Result<T, E> shape

Readme

@f0rbit/eslint-plugin

Custom typed org rules for the f0rbit ecosystem. Consumed through @f0rbit/eslint-config's define_lint_config factory under the f0rbit namespace — consumers never install this package directly.

Rules

Heading text below is the bare rule name — it's the exact anchor fragment ESLintUtils.RuleCreator's doc-URL builder points at (.../packages/eslint-plugin#<name>), so don't add prefixes or suffixes to these headings.

must-use-result

Rule ID: f0rbit/must-use-result · Tier: error · Type-aware: yes · Since: 0.1.0

Flags a statement that discards a call returning a Result — a discarded Result is a silently swallowed error.

Detection is structural, not name-based: any union where every non-nullish member carries an ok boolean-literal discriminant, with a { ok: true; value } arm and a { ok: false; error } arm, counts as a Result. This matches corpus's local Result, @f0rbit/corpus's export, and any structurally-identical homegrown copy. Promise<Result> is unwrapped via the checker's awaited type, so both await f(); and a floating f(); are caught.

get_result();              // error — discarded
await get_result_async();  // error — awaited then discarded
void get_result();         // error — void still hides the error arm
get_result_async();        // error — floating Promise<Result>

const result = get_result();       // ok — assigned
return get_result();               // ok — returned
take(get_result());                // ok — passed along
if (get_result().ok) { ... }       // ok — inspected

Intentional discards are config-scoped, not comment-scoped: use a files-scoped override in the consumer's eslint.config.ts, or a described eslint-disable-next-line f0rbit/must-use-result -- reason.

Fire-and-forget convention: the void operator is the sanctioned marker for intentionally discarding a Promise that cannot be awaited. Bare floating promises stay flagged by @typescript-eslint/no-floating-promises; once wrapped in void, the promise rejection is the callee's responsibility to handle internally. This is a Promise-only convention — it does not extend to Result: void get_result(); above is still an error, because must-use-result deliberately peels the void operator before checking. A Result's error arm can never be fire-and-forgotten; only a Promise's resolution can.

void emit_telemetry(); // ok — intentional fire-and-forget, marked with void
emit_telemetry(); // error — floating Promise flagged by no-floating-promises
await emit_telemetry(); // ok — awaited
const _ = emit_telemetry(); // ok — assigned (though void is clearer for pure side-effects)

prefer-pipe

Rule ID: f0rbit/prefer-pipe · Tier: warn (graduating) · Type-aware: yes · Since: 0.2.0

Flags two or more consecutive manual Result-unwrap guards in the same statement list — an unwrap guard is an if statement testing !x.ok or x.ok === false whose body is exactly return x;, where x's type is a Result. Two guards separated by at most one intervening statement (typically the next step's declaration) form a run; a run of length ≥ 2 reports once, on its first guard. A single unwrap never reports — this rule is about the repeated boilerplate, not the guard shape itself.

// warn (1 report, on the first guard) — 2 consecutive manual unwraps
const a = get_a();
if (!a.ok) return a;
const b = get_b(a.value);
if (!b.ok) return b;
return ok(b.value);

// ok — compose with pipe() instead
return pipe(get_a(), (a) => get_b(a.value));

// ok — a single unwrap never reports
const a = get_a();
if (!a.ok) return a;
return ok(a.value);

No autofix or suggestions — the message names pipe() and points at the corpus-patterns recipe; exemptions are config-scoped files-scoped overrides in the consumer's eslint.config.ts (no factory option).

no-ambient-effects

Rule ID: f0rbit/no-ambient-effects · Tier: warn (graduating) · Type-aware: no (syntactic) · Since: 0.2.0

Flags ambient reads of time and randomness that make code untestable against in-memory providers: Date.now(), new Date() called with zero arguments, and Math.random(). Detection is scope-resolved, not name-matched — Date/Math only flag when they resolve to the global (via ESLint scope analysis); a local shadow (an injected clock/rng provider) never flags.

// warn — ambient clock/rng read
const created_at = Date.now();
const roll = Math.random();
const now = new Date();

// ok — new Date(x) with an argument is not an ambient "now" read
const parsed = new Date(timestamp);

// ok — Date resolves to the injected local, not the global
function handler(Date: ClockProvider) {
	const created_at = Date.now();
}

Sanctioned alternative: inject a clock/rng provider (a Result-returning ClockProvider/RngProvider interface with an in-memory fake for tests) instead of reading the ambient global directly. The exemption is a factory option, not a files-scoped override — designate the files where an ambient read is the point (the one adapter that reads the real clock/rng before handing it to injected consumers):

define_lint_config({
	...,
	ambient_effect_files: ["src/providers/**"], // f0rbit/no-ambient-effects: off for these files
});

no-test-mocks

Rule ID: f0rbit/no-test-mocks · Tier: error · Type-aware: no (syntactic) · Since: 0.2.0

Flags mock/spyOn/jest imported from bun:test (on the import specifier) and their resolved usages (on the call), plus the bare global jest.fn/mock/spyOn/mocked() convenience surface when jest has no import or local declaration at all. The testing-strategy convention is in-memory fakes (the Provider pattern) over mocking framework internals — a mock hides the contract behind a stub; a fake is a real, inspectable implementation.

Detection is binding-resolved via ESLint scope analysis, not name-matched: a locally-defined mock function, or mock/spyOn imported from anywhere other than bun:test, never flags — including through an alias (import { mock as m } from "bun:test" flags both the import and every resolved usage of m).

// error — mock/spyOn imported from bun:test, plus their usage
import { mock, spyOn } from "bun:test";
const send = mock(() => Promise.resolve());
const spy = spyOn(logger, "warn");

// error — bare jest.* convenience global (flags even with no import)
jest.spyOn(logger, "warn");

// ok — in-memory fake instead
class FakeLogger implements Logger {
	warnings: string[] = [];
	warn(message: string) {
		this.warnings.push(message);
	}
}

// ok — a locally-defined mock, or one imported from elsewhere, is not bun:test's
function mock() {
	return "not a test double";
}

vitest's vi equivalents are out of scope this wave (bun test only — no vitest in the ecosystem). No factory option: exemptions are the standard config-scoped files-scoped override in the consumer's eslint.config.ts.

require-schema-at-boundary

Rule ID: f0rbit/require-schema-at-boundary · Tier: error · Type-aware: yes · Since: 0.3.0 — not part of the 0.2.0 release.

Flags raw external data flowing through the codebase without a Zod parse. Sources (v1): JSON.parse(...) where JSON resolves to the global (a local shadow doesn't count), and .json() calls on a receiver that's structurally fetch-Body-like (has json, text, and headers members — covers lib.dom Response/Request, undici, @cloudflare/workers-types, and framework wrappers like Hono's HonoRequest). D1/Drizzle rows, R2 body reads, and KV.get(..., "json") are not sources in v1 (phase-in candidates once the JSON/fetch sources have proven false-positive-free).

A boundary expression is sanctioned once it flows into a Zod .parse()/.safeParse()/.parseAsync()/.safeParseAsync() call (structurally ZodType-like — any custom validator exposing both parse and safeParse counts), or into a slot explicitly typed unknown. Everything else — member access, destructuring, an as T cast (including as unknown as T, peeling the unknown layer before judging the outer cast) — is a violation. Variable tracking is single-hop: assigning a raw boundary variable onward is itself a violation, not a second hop to re-check.

// error (unvalidated_boundary) — JSON.parse output used without validation
const data = JSON.parse(raw);
console.log(data.user.name);

// error (cast_boundary) — casting a fetch response straight through
const body = (await response.json()) as User;

// ok — validated at the boundary
const user = UserSchema.parse(JSON.parse(raw));

// ok — explicitly typed unknown, validation deferred downstream
const raw_body: unknown = await response.json();

No factory option: exemptions are config-scoped files-scoped overrides only.


Adding a rule

One file per rule under src/rules/, created with ESLintUtils.RuleCreator, exported through src/index.ts, wired into the factory in @f0rbit/eslint-config, and covered by a @typescript-eslint/rule-tester suite under tests/.