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

@brighterly/lib-fe-codestyle

v0.5.2

Published

Shared invariant-validator engine, rule pack and ESLint config for Brighterly Nuxt frontend apps

Readme

@brighterly/lib-fe-codestyle

Shared, deterministic code-review machinery for the Brighterly Nuxt apps — land-app, cs-app, app-tutor, app-admin.

It holds the mechanisms. Domain rules stay in each app.

New here? docs/how-it-works.md — how a check reads real code, what the gate can and cannot cover, what is already enforced, how to extend it, and how to run it.

Subpaths

| Import | What it is | |---|---| | @brighterly/lib-fe-codestyle/invariants | the invariants engine — run(options), the rule contract, AST helpers, util | | @brighterly/lib-fe-codestyle/invariants/rules | the common rule pack, portable across all apps | | @brighterly/lib-fe-codestyle/eslint | the shared flat config, its seven local rules, and the maps behind them | | @brighterly/lib-fe-codestyle/validate | the aggregating runner behind pnpm validate |

exports is the public surface. Anything not listed is private — a linked package resolves deep paths that break the moment it is published.

The one design rule: export data, never factories

Consumers compose. There is no defineBrighterlyEslint({...}), and there never will be — the four apps' configs are ~95% identical and the variance is mostly drift, so a factory would parameterise accidental difference.

// an app that needs nothing extra
import brighterly from '@brighterly/lib-fe-codestyle/eslint'
export default [...brighterly]
// an app extending a map — plain object spread, no API to learn
import brighterly, { checkFileFolders } from '@brighterly/lib-fe-codestyle/eslint'

export default [
  ...brighterly,
  { rules: { 'check-file/folder-naming-convention': ['error', {
      ...checkFileFolders,
      'app/components/pages/**/': 'KEBAB_CASE',
  }] } },
]

ESLint replaces rule options rather than merging them, so appending an override without spreading the shared map silently discards every shared entry. That is why the maps are exported as data.

What the rule pack ships

Invariants — @brighterly/lib-fe-codestyle/invariants/rules

| Rule | Level | Checks | |---|---|---| | core/R1 | warn* | bare localStorage / sessionStorage — the accessor itself throws on Safari ITP and in-app webviews | | core/R2 | error | SCSS rules inline in a Vue <style> block instead of a referenced .scss file | | core/R3 | warn* | JSON.parse with no enclosing try/catch — a corrupt entry breaks that path permanently | | core/R4 | error | @font-face declaring a font stack — an invalid descriptor, so the browser drops the face silently | | core/C1 | warn | hand-rolled browser API with a VueUse equivalent | | core/C2 | warn | a component duplicating an existing UI-kit component name | | core/C3 | warn | a literal colour in SCSS instead of a design token | | core/C4 | warn | a hand-rolled date format string, or toISOString() | | core/C5 | warn | a type file outside the types/<entity>/{enums,interfaces,types}.ts structure | | core/C6 | warn | a raw font-family / font-size / font-weight declaration instead of the app's typography source | | core/C7 | warn | <script> attribute order differing from the app's canonical (setup lang vs lang setup) |

Level discipline: error is for crash-class rules (core/R*); warn is for conventions (core/C*). core/R1/core/R3 (marked *) ship at warn as an interim state (2026-09-25): their fleet backlog is real code nobody has bandwidth to migrate and re-verify right now. Their target level is error — promote once the backlog clears (a breaking-style bump). core/R2/core/R4 stay error. A consumer's levels ceiling is still never the tool for a backlog on an error rule — fix or allowlist. Underscore-prefixed SCSS partials (_extends.scss, _colors-*.scss, …) are declaration files by convention in every Brighterly app and are exempt from core/C3/core/C6 wholesale.

core/R* were ported from land-app, where each was a production scar (as R1, R7, R18, R28 — renumbered sequentially when the prefix landed, pre-publish, while no released version pinned the old tokens). core/C* are conventions with no incident behind them yet — they ship at warn so a consuming app can measure what they find before anyone decides to block on them. Promoting one to error is a major bump.

Fixtures ship with the rules (commonFixtures), so --self-test works in any consumer without it having to write fixtures for rules it did not author.

ESLint — @brighterly/lib-fe-codestyle/eslint

Seven local rules: padding-after-macros, async-components-after-macros, no-import-group-labels, ref-prefix-prop-mirrors, require-component-name, on-prefixed-callbacks, and optional-callback-chaining (new — an optional callback invoked without ?. throws for the first caller that omits it).

Plus the shared flat config: restricted syntax, restricted imports, the types-organization blocks, and the check-file naming maps — all exported as data so a consumer can spread and extend them.

App-specific paths: settings, not parameters

A shared rule must not hardcode a consuming app's paths. Each app declares them once:

// invariants/config.mts
export const settings = {
  safeStorageModule: 'safeLocalStorage from @helpers/safe-local-storage',
  safeStorageFile:   'app/utils/helpers/safe-local-storage.ts', // core/R1 auto-exempts the wrapper itself
  safeJsonParse:     'parseStoredJson() from @helpers/safe-local-storage',
  scssTokensFile:    'app/assets/scss/_colors.scss',
  uiKitRoot:         'app/components/UI',
  dateFormatModule:  'DateFormat from @ts-types/date/enums',
  typesRoot:         'app/types',                              // core/C5 (default shown)
  typographySource:  'the font() mixin from @scss/_mixins.scss', // core/C6 — cs-app points this at its AppText component instead
  scriptAttrOrder:   'setup-first',                            // core/C7 — or 'lang-first' (default 'setup-first')
  selfTestFile:      'invariants/self-test.mts',               // its ignore-strings are fixture DATA — skipped by IG0/IG1
}

A missing key degrades the rule's message, never its firing. A rule that went quiet because a path was unset would be the exact silent failure this package exists to prevent. Two keys shape behaviour rather than the message — typesRoot (where core/C5 looks) and scriptAttrOrder (which direction core/C7 flags) — and both fall back to the documented default above, so leaving them unset still never silences the rule.

What consumers see in the IDE

Three hover surfaces, so "what rules do we have and what do they look for" never requires opening this repo:

  • sharedRuleIds (from …/invariants/rules) — the catalog: one constant per rule, each JSDoc'd with level, what it checks, and the fix. Use them as allowlists / levels keys ([sharedRuleIds.core/C6]: 'off') and hover for the contract. A test pins the catalog to the shipped rules, so it cannot drift.
  • SharedRuleSettings (from …/invariants) — annotate the app's settings const with it to get completion plus the per-key contract, and a type error on an invalid scriptAttrOrder value.
  • ESLint squiggles — every local rule ships meta.docs.description, which is what the editor shows on hover over a violation.

PhpStorm / WebStorm notes: the IDE runs the project's own ESLint binary against the flat eslint.config.mjs, so rules defined inside this package just work — nothing to install or configure beyond the default "Automatic ESLint configuration". Two real behaviours to know: the ESLint daemon caches the resolved config, so after upgrading this package run ESLint: Restart Service (or reopen the project) to pick up new rules; and the invariants engine is not an IDE inspection — its findings appear in the terminal (pnpm validate:invariants) and the pre-commit hook only.

Wiring a consumer

  1. pnpm add -D @brighterly/lib-fe-codestyle — plus, if not already direct devDeps, @vue/compiler-sfc and typescript. Both are peers, not dependencies, on purpose: the validator must parse your files with the same compiler instance your build uses, so the package dedupes onto the app's copy instead of carrying its own. Every Brighterly Nuxt app already has both in the store — the devDep line adds a symlink, not a download.
  2. An invariants/ directory with cli.mts (a ~20-line composition root — copy land-app's), config.mts (allowlists, levels, retired, unmaintained, settings — see above), your app-local rules, and specs/ for checks needing real module resolution.
  3. A missing peer is loud, never silent: pnpm warns at install (and auto-installs by default), and the engine's lazy import() throws on the first AST rule otherwise.
  4. Arm the pre-commit hook from install, not by hand: keep the hook script in a tracked directory and add git config core.hooksPath <that-dir> || true to postinstall (the || true keeps .git-less installs — Docker builds — green). Setup stays pnpm i && pnpm dev; a hand-made .git/hooks symlink dies with every fresh clone.

An app with a pre-extraction fork of the engine (app-funnels) deletes its copied ast/util/run/self-test plumbing and keeps only domain rules + config; its existing compiler-sfc devDep already satisfies the peer.

Why src/ is compiled but eslint/ is not

Node refuses to strip types inside node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so the TypeScript half ships as dist/. The ESLint config and its seven rules are already plain .mjs and ship as-is.

Rule IDs

Shared rules carry the core/ prefix: core/R1, core/C3. App-local rules stay bare: R1, B2. The prefix marks provenance — a core/-id is shipped by this package, never defined, renumbered or recycled in a consumer — and it keeps the two number lines independent: every app allocates its own R1, R2, … naturally, with no risk of colliding with a rule the package ships later. run() still exits 2 on a duplicate, so a collision is loud, not silent.

Within the package each family is numbered sequentially with no gaps (core/R1–core/R4, core/C1–core/C7) — enforced by test/invariants/rule-ids.test.ts. The family letter keeps meaning provenance — R regression scar, B business, C convention, L linter-owned — not topic. core/ in a report means exactly one thing: this finding comes from the library, and its numbering is the library's to own.

Versioning is about blocking power

One question decides the bump: can this change turn a green consumer red, or break something a consumer wrote against a rule id? Yes → major. Adds signal without blocking → minor. Everything else → patch.

| Change | Bump | Why | |---|---|---| | New invariant rule at error | major | can turn a green repo red on install | | Promoting a rule warn → error | major | same — new blocking power | | Widening a rule's scope, narrowing its exclude, or making its match stricter | major | existing code that passed now fails | | Renaming, renumbering or removing a rule id | major | breaks every invariant-ignore: comment, allowlists/levels key and doc that names the token — IG1 starts firing in every consumer | | New ESLint rule at error, or adding a selector to restrictedSyntax / restrictedImports / the check-file maps | major | the shared config is spread into every app's lint gate | | Engine behavior change (run() exit codes, finding shape, config contract) | major | consumers' composition roots are written against it | | New invariant rule at warn | minor | new signal, blocks nothing | | New ESLint rule at warn | minor | same | | New export (a map, a helper, a settings key a rule optionally reads) | minor | additive surface | | Demoting a rule error → warn, or narrowing its scope | patch | strictly relaxing | | Bug fix that removes false positives, message/why/fix wording, fixture improvements | patch | nothing new fires |

Two edges worth naming: a bug fix that removes false negatives (the rule now catches what it always claimed to) is still major — correctness of intent does not change what happens in a consumer's CI. And a new settings key a rule requires is major; one it merely reads for a better message is minor (a missing key degrades the message, never the firing — see above).

Publishing

The package is published publicly to npmjs.org under the @brighterly org — the same channel as @brighterly/lib-lumee-ui, so consumers need no registry config or token. publishConfig.access: "public" in package.json makes the scoped publish work without a paid org plan.

npm login                 # must be a member of the @brighterly org
npm version <patch|minor|major>   # per the blocking-power table above
npm publish               # prepublishOnly rebuilds dist and runs the tests first
git push && git push --tags

dist/ is gitignored; prepublishOnly (clean → build → test) guarantees the tarball is built from the current sources, never a stale artifact.

Design

land-app/docs/superpowers/specs/2026-09-22-fe-codestyle-design.md