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

@feugene/granularity-test-kit

v0.7.4

Published

Test gates for @feugene/granularity design-system packages — token, registry and defaults contracts as reusable factories.

Downloads

1,538

Readme

@feugene/granularity-test-kit

Contract gates for packages of the @feugene/granularity design system — token registries, component registries and defaults augmentation, as reusable factories.

Every package of the design system owes the same set of promises: component tokens are declared, style values come from the scale, all registration points agree with the file system, and defaults.ts augments the registry where it is declared. Those promises used to be kept by copying four test files into every new package — about 450 lines each time, drifting silently apart.

This package turns each of them into one call.

// src/__tests__/styleTokens.test.ts
import { defineStyleTokensGate } from '@feugene/granularity-test-kit/gates'

defineStyleTokensGate()

No dependency on @feugene/granularity. Everything package-specific — token registries, the provider registry, the source root — is passed in as an argument. That keeps the direction of dependencies one-way (a companion knows about the core, never the reverse) and lets the core itself use the same factories: its gates read sources, not a built dist.

Install

yarn add -D @feugene/granularity-test-kit

vitest is an optional peer: bring the one your package already runs.

Gates

defineStyleTokensGate(options?)

Fails on values that a theme cannot reach: pixel literals (text-[14px]), uno-scale utilities (text-sm, rounded-md), raw durations (duration-150, 200ms) and easing keywords. The uno-scale utilities matter most — they look correct and stay 14px no matter what --gr-text-sm says.

| Option | Default | What it is for | | --- | --- | --- | | srcDir | <cwd>/src | source root | | excludeTopDirs | [] | generated top-level dirs (['styles', 'tokens'] in the core) | | requireTokenUsage | true | motion and typography tokens are actually used | | requirePairedLeading | false | every text-[length:var(--gr-*text-*)] carries a leading-* in the same class literal. Off by default: a package moves onto pairing in one edit and would otherwise go red for a debt it has not paid | | pairedLeadingExceptions | [] | places where a line height is deliberately set next to the size (leading-none on a key cap) |

defineComponentTokensGate(options)

Checks the per-component theme contract: every --gr-* in the sources is declared — globally or in the component's own tokens.json — registries are not stale, no one redeclares someone else's token, and entries carry name, kind, default and description.

| Option | Default | What it is for | | --- | --- | --- | | globalTokens | — | tokens declared outside per-component registries. A companion passes everything the core declares | | extraRegistries | [] | registries outside components/ (['composables'] in the core) | | requireOwnerPrefix | true | a component's own token starts with its own prefix | | ownerPrefixOverrides | {} | families named shorter than their component (GrProgressBar--gr-progress-) | | requireThemeKind | true | variables declared in themes/*.css are registered as kind: "theme" | | requireRegistries | true | the package has at least one component with its own tokens |

defineRegistryGate(options)

Runs the package's registry generator with --check and verifies what the built modules actually contain: the provider registry, the name list, the root barrel, package.json#exports and the vite entries all match src/components/.

| Option | Default | What it is for | | --- | --- | --- | | componentConfigs | — | the provider registry | | componentNames | — | the separate name list, if the package keeps one | | generatorScript | scripts/generate-registry.mjs | null when the package has no generator | | requireExactExports | true | no exports entries for components that no longer exist |

defineComponentDefaultsGate(options)

Every defaults.ts augments the registry at the module where it is declared, and nowhere else. An augmentation routed through a re-export works until a second one — declared directly — enters the same program; then the first silently stops applying, with no error at all.

The gate also checks that a declared default is read. Declaring one and never reading it is a promise nobody keeps: GrConfigProvider configures, the component never looks, and nothing says so. Two such props lived that way and were both found by hand. Reads are collected across the whole src — not the component's own directory — because the resolution is sometimes hoisted into a shared module: chrono's four pickers read their props from src/internal/usePickerShell.ts. Four channels count as evidence: a literal call (newlines included), useGrComponentSize with { component } for the size key, a manual chain through useGrComponentDefaults, and a call whose component name is a variable — that last one counts the key for every component in the package. Coarse on purpose: it can miss a dead default, but it never invents one, and a gate that cries wolf gets switched off.

| Option | Default | What it is for | | --- | --- | --- | | registryModule | — | the only accepted augmentation target | | minComponents | 1 | guards against a gate that is green because the list went empty | | registryDeclaration | — | for the package that declares the registry: where it lives and what proves it |

defineComponentDocsGate(options?)

Every component in the registry has a page, every page has a component, and both required sections (## Когда брать, ## Когда взять другое) are there and not a placeholder. It also refuses hand-written prop tables: those are generated from the sources, and a hand-kept copy drifts silently — precisely when someone is reading it.

| Option | Default | What it does | | --- | --- | --- | | docsDir | docs/components | where the pages live | | indexPath | docs/components.md | the index that must link every name | | requiredSections | both of the above | headings a page cannot ship without | | redirectExempt | {} | components with no neighbour to point at, and why |

defineEmitNamingGate(options?)

One canon for emit names: camelCase, no kebab-case except update:*, and the declaration parameter is e. Two packages had byte-identical copies of this check, a third had none at all.

defineLocaleCompletenessGate(options)

Every locale covers the base one, no orphan keys, placeholders match, and — with keyParity — the keys the code asks for are exactly the keys the dictionaries declare. A missing translation is silent by design: t() falls back to English, so only a gate ever notices.

| Option | Default | What it does | | --- | --- | --- | | block | — | the i18n block of the package (gr, grChrono, …) | | localesDir | src/i18n/locales | where the flat JSON dictionaries live | | locales | ['en','ru','es'] | the first one is the base | | pluralForms | false | require CLDR plural categories | | keyParity | { sourceDirs: ['src'] } | compare asked-for keys against declared ones |

defineEnvGuardGate(options?)

One symbol guards every dev-time warning, and this gate keeps it that way. It fails on two things: an environment check written by hand (process.env.NODE_ENV is a ReferenceError in a worker; import.meta.env?.DEV is silently undefined outside Vite), and a console.* call with no guard above it — that one shipped four warnings into consumers' production builds, because nothing was checking for the absence of a condition.

The guard counts on the same line (inline && __GR_DEV__) and on any line above: one block guard routinely covers four warnings in a row. A warning whose guard sits lower — in a module function called from a guarded site — goes into allowUnguarded with a reason.

| Option | Default | What it does | | --- | --- | --- | | srcDir | <cwd>/src | where to scan | | guard | '__GR_DEV__' | the symbol | | guardValue | — | pass __GR_DEV__ from the package: define is textual and never reaches this factory from node_modules | | minFiles | 0 | anti-silence floor for the scan | | minGuards | — | anti-silence floor for guards; only for packages that certainly have them | | allowUnguarded | {} | { path: reason }; the reason is required |

The paired check runs on the built package: gr-check-dist-dev-guard, a bin this package ships. Add it to buildvite build && gr-check-dist-dev-guard && vue-tsc …. Unit tests run with the guard defined as true, so a define that stopped working stays invisible to them while consumers get __GR_DEV__ is not defined on import.

defineGateCoverage(options?)

Fails when a package forgot to wire one of the gates above. While gates were copied as files, forgetting one cost nothing — a package without styleTokens lived with pixel literals and stayed green.

Test helpers

Three more subpaths, split by peer rather than by topic — so that the heavy one reaches only the package that asked for it. Every peer below is optional.

@feugene/granularity-test-kit/vue — component tests

import { nextFrame, queryOne, stubElementRects } from '@feugene/granularity-test-kit/vue'

| Export | What it does | | --- | --- | | queryOne(selector, root?) | the element, or a throw naming the selector. Root defaults to document: overlay panels live in a portal and are not inside the wrapper | | queryWrapper(selector, root?) | the same as a DOMWrapper, when trigger and text are next | | stubElementRects(rect) | a rectangle for every element, prototype-wide, with a restore. A component that measures its root in onMounted reads the size before the test can reach the node | | nextFrame() | one requestAnimationFrame. nextTick is the Vue queue, not the browser frame | | fintI18nGlobal(i18n) | { provide } with the instance under Symbol.for('FintI18n'), shaped to compose with other provide blocks |

@feugene/granularity-test-kit/a11y — axe in jsdom

import { axeViolations } from '@feugene/granularity-test-kit/a11y'

expect(await axeViolations(wrapper.element)).toEqual([])

Serious and critical violations as id: help (n) strings — an empty array reads as "clean", and the failure message is readable without unpacking axe objects. color-contrast is off by default: jsdom paints nothing, so the rule has nothing to measure. A separate subpath because axe-core is a peer the core does not have.

@feugene/granularity-test-kit/e2e — Playwright

import { createA11yBaseline, expectNoA11yRegressions, expectTabCycle } from '@feugene/granularity-test-kit/e2e'

| Export | What it does | | --- | --- | | expectNoA11yRegressions(page, options) | axe over an area, minus the accepted debt, with the list itself in the failure message | | a11yRegressions / selectRegressions | the same without the assertion; the latter is pure and takes axe results directly | | createA11yBaseline(known, options?) | the debt mechanics. Data stays with the application; A11Y_AUDIT=1 zeroes the debt so a run reports all of it | | expectTabCycle(page, layer) | Tab really walks the layer. "Focus did not leave" is not enough: a trap pinning focus to one button passes that | | tabUntil, focusedDescription | reach an element by Tab; describe what is focused, for the failure message | | waitForOpaque(page, selector) | wait out the enter animation. On a half-transparent panel axe computes a blended colour and reports a contrast defect that does not exist |

What is not here

Test helpers that know about the design system — granularityGlobal(), announced(), resetGranularityDom() — live in the core as @feugene/granularity/testing. They reach into the core's own internals, so moving them here would make this package depend on the core, and the core itself calls these factories: the dependency would close into a cycle.

License

SEE LICENSE IN LICENSE