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

@gamesheet-inc/oxlint-plugin

v0.2.0

Published

GameSheet's first-party Oxlint rules.

Readme

@gamesheet-inc/oxlint-plugin

GameSheet's first-party Oxlint rules.

Install

pnpm add -D @gamesheet-inc/oxlint-plugin @oxlint/plugins oxlint

@oxlint/plugins is a peer dependency and must match your oxlint version.

Configure

In .oxlintrc.json:

{
	"jsPlugins": [{ "name": "gamesheet", "specifier": "@gamesheet-inc/oxlint-plugin" }],
	"rules": {
		"gamesheet/no-comments": "error",
		"gamesheet/no-tailwind-v3-classes": "error",
		"gamesheet/no-tailwind-important-prefix": "error"
	}
}

The two Tailwind rules are for repos on Tailwind v4. A repo still on v3 should leave them off: everything they flag is correct v3.

Rules

gamesheet/no-comments

Bans // line comments and /* */ block comments. Autofixable.

Exempt unconditionally, regardless of allow: JSDoc (/** */) of any length, a /*! preserve/license banner, shebangs, and — only in block-comment form (/* global foo */, never // global foo) — a bare ESLint config comment (eslint <rule>: ...), eslint-env, global, globals, exported. ESLint itself never recognizes these five as a // line comment, and gating them on the comment's actual type is what keeps this rule from also exempting ordinary prose that happens to contain the same words — "exported for testing", "global state is a problem here" — which a text-only pattern can't tell apart from the real directive.

Exempt by default (overridable via the allow option below) — machine-read directives:

  • eslint-disable* / eslint-enable*, oxlint-disable* / oxlint-enable*
  • @ts-*
  • @jsx* (@jsxImportSource, @jsxFrag, @jsxRuntime, and friends)
  • /// <reference>
  • prettier-ignore, oxfmt-ignore
  • v8/c8/istanbul ignore
  • #__PURE__ / @__PURE__, #__NO_SIDE_EFFECTS__
  • the webpack magic-comment family: webpackChunkName, webpackIgnore, webpackPreload, webpackPrefetch, webpackMode, webpackExports, webpackInclude
  • @vite-ignore
  • //# sourceMappingURL=, //# sourceURL=
  • falls through / fallthrough / Fall through. — but only when that phrase is the entire comment, optionally with a trailing period. Unlike ESLint's own no-fallthrough, which only ever looks at the last comment before a case label, this rule scans every comment in the file, so an unanchored version of this pattern would also exempt any sentence that merely mentions falling through something ("the request falls through the cache and hits origin").
  • @vitest-environment, @jest-environment
  • any comment containing SAFETY:

SAFETY: is exempt because anti-slop/require-safety-comment-for-type-assertion requires such a comment for every non-const type assertion. Banning them makes that rule unsatisfiable.

Option allow replaces the default list entirely, so include the directive patterns you still need. Every pattern must tolerate leading whitespace — Oxlint's Comment.value retains the space after //, so a pattern anchored with a bare ^ will match nothing. Patterns compile with the u flag, so an unnecessary escape — prettier\-ignore, say — throws Invalid regular expression: /prettier\-ignore/u: Invalid escape and fails every file, not just the pattern that has the typo.

{
	"rules": {
		"gamesheet/no-comments": ["error", { "allow": ["^\\s*@ts-", "\\bSAFETY\\s*:"] }]
	}
}

Durable knowledge does not belong in a deleted comment. Move it to a JSDoc block or a docs/constraints.md in the consuming repo before running --fix.

gamesheet/no-tailwind-v3-classes

Flags Tailwind v3 class names that Tailwind v4 removed, renamed, or compiles to invalid CSS. What v4 actually does with each class was checked against the real v4.3 compiler, not the upgrade guide, and __tests__/tailwind-oracle.test.ts keeps checking it.

| What v4 does with it | Classes | Write instead | Fix | | --- | --- | --- | --- | | Produces no CSS at all | bg-opacity-*, text-opacity-*, border-opacity-*, divide-opacity-*, ring-opacity-*, placeholder-opacity-*, with any suffix: a number, a theme key, an arbitrary value | the opacity modifier on the colour utility: bg-black/50 | report only; the rewrite needs the colour class it pairs with | | Compiles to invalid CSS (background-color: --brand) | bg-[--brand], any <utility>-[--var], with or without a /50 modifier | bg-(--brand) | autofix | | Still compiles through a legacy table, but is not the v4 name | flex-shrink, flex-shrink-*, flex-grow, flex-grow-*, overflow-ellipsis, decoration-slice, decoration-clone, bg-gradient-to-*, bg-left-top and the other three bg- corners, object-left-top and the other three object- corners | shrink, shrink-*, grow, grow-*, text-ellipsis, box-decoration-slice, box-decoration-clone, bg-linear-to-*, bg-top-left, object-top-left | autofix |

The rename list is the one @tailwindcss/upgrade applies. Variants, the ! modifier, a negative sign and /opacity modifiers all survive the rewrite: md:!flex-shrink-0 becomes md:!shrink-0, -translate-x-[--shift] becomes -translate-x-(--shift).

Not flagged, on purpose. shadow-sm, shadow, rounded-sm, rounded, blur-sm, blur, drop-shadow*, backdrop-blur*, ring and outline-none all changed meaning between v3 and v4: the size scales shifted one step, ring went from 3px to 1px, and outline-none now really sets outline-style: none (v3's behaviour is outline-hidden). Every one is a valid v4 class, so a linter cannot tell a v3 leftover from a deliberate v4 choice. Across the GameSheet v4 repos there are 126 outline-none, 103 rounded-sm and 55 shadow-sm in legitimate use. Also left alone: v3's variant order (first:*:pt-0 versus *:first:pt-0), placeholder-<colour> (v4 still compiles it), and the later v4.x modernisations such as break-words to wrap-break-word or order-none to order-0, which are v4-to-v4 changes, not v3-to-v4.

A class cut by a ${} boundary, such as flex-shrink-${n}, is skipped: half a class cannot be classified.

Option ignore lists class names never to flag, matched against the whole token. Tailwind v4 compiles flex-grow-1 through its legacy table, and so does Bootstrap. A repo that mixes Bootstrap with Tailwind, as admin-dashboard-v3 does, should list Bootstrap's flex utilities here.

{
	"rules": {
		"gamesheet/no-tailwind-v3-classes": ["error", { "ignore": ["flex-grow-1", "flex-shrink-1"] }]
	}
}

gamesheet/no-tailwind-important-prefix

Tailwind v4 moved the important modifier to the end of the class. !bg-white is the deprecated v3 spelling of bg-white!; both compile to the same CSS today. Autofixable.

  • Variants and negatives stay where they are: hover:!text-red-500 becomes hover:text-red-500!, !-mt-2 becomes -mt-2!, and [&_svg]:!size-4 becomes [&_svg]:size-4!. A colon inside [...] or (...) is part of an arbitrary value, not a variant separator.
  • !important inside a string is CSS, never a class, and is ignored.
  • A class cut by ${}, such as !bg-${tone}-500, is reported without a fix.

Where the Tailwind rules look for classes

Both rules share one finder. Every string literal and template-literal quasi is examined when it sits under

  • a JSX attribute named className or class (option attributes),
  • an argument of cn, clsx, classnames, classNames, cva, tv, twMerge, twJoin or cx (option callees), including the keys of a clsx-style object and the variant maps cva and tv take,
  • a tw tagged template (option tags),

reached through the branches of a conditional (never its test, so state === "flex-grow" ? a : b keeps its comparison), && and ||, + concatenation, arrays, objects, spreads and as casts. Inside a cva or tv config the values of variants and slots and the class/className of a compound entry are classes; variant names, defaultVariants and compound selectors are not, so a variant option that happens to be called flex-grow keeps its name. Anywhere else, a string is scanned only when it reads as a class list: two or more whitespace-separated tokens, every one a plausible utility, a template literal being judged as a whole. A token is a plausible utility when it parses as a class and its utility part has a hyphen, a digit or an arbitrary value, or is one of the 42 hyphen-less names the v4 compiler enumerates (flex, hidden, rounded, ...; the oracle test re-derives that list). That is what finds toastOptions={{ classNames: { success: "!bg-green-50 ..." } }} and const styles = { primary: "..." }, while a one-token string such as setProperty("flex-grow", "1") or import "!raw-loader!./x" is never touched, and prose that merely mentions a class, "supports flex-grow utilities" or a test title ending in "returns !ok", is never scanned, let alone rewritten.

Each option replaces its default list rather than extending it:

{
	"rules": {
		"gamesheet/no-tailwind-important-prefix": [
			"error",
			{ "attributes": ["className", "class", "iconClassName"], "callees": ["cn", "cva"], "tags": ["tw"] }
		]
	}
}

Running --fix with both rules on. Oxlint applies one fix per overlapping range per run and does not loop, so a class both rules want to rewrite, hover:!flex-shrink, becomes hover:shrink! on the second oxlint --fix. Run it until clean, which is already how gamesheet/no-comments is applied in this org's repos. Splitting a fix into disjoint spans would not help: like ESLint, Oxlint merges an array of fixes into one range, so the two rules' edits overlap however they are expressed. __tests__/tailwind-both-rules.test.ts pins the two-pass contract.

Measured on 2026-09-03 against the main checkouts of the GameSheet repos on Tailwind v4, build output excluded, before enabling the rules anywhere:

| repo | no-tailwind-v3-classes | no-tailwind-important-prefix | | --- | --- | --- | | app-stats-widget-next | 23 (flex-shrink-0) | 1 | | mono/apps/teams | 47 (flex-shrink-0) | 0 | | mono/packages/ui | 5 (ring-opacity-5) | 0 | | project-wingman | 15 (9 flex-shrink-0, 6 *-opacity-*) | 4 | | project-forge | 2 | 16 | | tournament-board-builder | 0 | 16 | | standup-picker | 3 (bg-gradient-to-*) | 0 | | admin-dashboard-v3 (not on oxlint) | 5, of which 4 are Bootstrap's flex-grow-1 | 15 |

Every finding was read at its source line; none is a false positive, and the four Bootstrap hits are the case the ignore option exists for. The 32 !-prefix findings in project-forge and tournament-board-builder sit in sonner's classNames object, which only the class-list heuristic reaches.

Adding a rule

Each new rule lands with a measured finding count against a real GameSheet repo and a fixture pair under __fixtures__/. That gate is what keeps the false-positive rate at zero.

Development

pnpm install
pnpm test          # builds dist via prepack, then runs the suite
pnpm build         # tsc -p tsconfig.build.json
pnpm lint
pnpm type-check

Authored in TypeScript, published as JavaScript. dist/ is gitignored and built by prepack (before any pnpm pack) and by prepublishOnly — not by an install-time prepare, because pnpm blocks lifecycle scripts by default and that would force an onlyBuiltDependencies entry in every consuming repo.

The build step is not optional. Node's ESM loader refuses to type-strip any .ts file whose realpath is under a node_modules path, so a package with a .ts entry point cannot be consumed as a dependency — only via a symlink or a workspace link, which is what made the original design look viable.

Rules are tested by shelling the real oxlint binary at fixtures and asserting on its JSON diagnostics, because @oxlint/plugins ships no RuleTester. tailwindcss is a devDependency only so that __tests__/tailwind-oracle.test.ts can compile every class the Tailwind rules name and prove the table is right; consumers never install it through this package. __tests__/packaging.test.ts is the one test that exercises how consumers actually load the plugin — a real pnpm pack, a real install, a real lint.

Releasing

Nothing to click. Every push to main whose test job passes runs the release job in .github/workflows/ci.yml: it derives the next version from the conventional commits since the last v* tag, publishes to npm through Trusted Publishing, and creates the GitHub Release, which creates the tag. scripts/next-version.ts is the computation; __tests__/next-version.test.ts pins it, including the real v0.1.1..6eb80f4 history.

| Commits since the last tag | Result | |---|---| | any feat | minor | | fix, perf, refactor, revert | patch | | ! before the colon, or a BREAKING CHANGE: footer | major — minor while the package is 0.x | | only docs, ci, test, chore, style, build, or non-conventional subjects | no release |

package.json is a floor, not the source of the number: the job publishes max(bump, package.json). Bump it in a PR only to force a release that has no releasable commit, or to jump (to 1.0.0, which commits cannot express from 0.x). Otherwise leave it alone — the commit a tag points at may carry a lower package.json than the tag; the published tarball is what is truthful.

Squash-merged PRs need a conventional title, because the squash commit's subject is what gets parsed. Merge commits themselves are skipped; the commits they bring in are read.

A release is idempotent. Re-running a successful job publishes nothing (the range since the new tag is empty). A run that fails after npm publish skips the publish on re-run and only creates the Release. The run's summary links the npm version and the Release, or says why nothing was released.

Publishing is authorised by an npm trusted publisher keyed to this repository and the workflow file ci.yml. Moving the job to another file needs a new trusted publisher on npmjs.com first, with "allow npm publish" ticked — configurations created after 2026-09-03 default to staged publishing only.