@brighterly/lib-fe-codestyle

Shared invariant-validator engine, rule pack and ESLint config for Brighterly Nuxt frontend apps
1,330
Install
npm install @brighterly/lib-fe-codestyle
Latest Version:1.0.3
License:Proprietary
Last Updated:Oct 22, 2026
Links: npm
Maintainer: vladislavow

@brighterly/lib-fe-codestyle

Shared, deterministic code review for the Brighterly Nuxt apps: a rule engine with a binary (codestyle), eleven package rules, and a shared ESLint flat config. A consumer declares one file, codestyle.config.ts; its own domain rules live next to it.

New here? docs/how-it-works.md explains what one run does, the proof forms, overrides and tags. Moving a repo from 0.5.x: docs/migration-1.0.md.

Quick start

pnpm add -D @brighterly/lib-fe-codestyle

typescript and @vue/compiler-sfc are peers: the engine parses your files with the same compiler instances your build uses. Every Brighterly app already has both.

// codestyle.config.ts — the only required file
import { defineConfig } from '@brighterly/lib-fe-codestyle'
import repo from './codestyle/index.ts'          // { custom: [...] } — this repo's own rules

export default defineConfig({
  settings: {                                    // app landmarks the package rules read
    scssTokensFile: 'app/assets/scss/_colors.scss',
    typographySource: 'the font() mixin',
    scriptAttrOrder: 'setup-first',
  },
  rules: {                                       // one level map for package AND custom rules
    'script-attr-order': 'error',
    'vueuse-first': 'off',
  },
  ...repo,                                       // contributes `custom` only — levels stay in `rules`
  overrides: [                                   // never "the rule is off" — always "this place, this reason"
    { files: ['app/components/UI/Buttons/BtnAuth*.vue'], rules: { 'inline-scss': 'off' },
      reason: 'UI-kit exception, decided 2026-08-18' },
    { files: ['app/components/Views/ViewQuizExp132.vue'], rules: { 'journey-coverage': 'warn' },
      reason: 'funnel still being built; until 2026-10-31' },
  ],
})

A rule not named in rules runs at its own level. Keep the spread object to { custom }: object spread replaces a same-named key, so a rules map inside it and the one above would silently overwrite each other.

Two loading facts, because Node runs this file with its own type stripping:

  • Relative imports spell .ts (./codestyle/index.ts, and inside codestyle/ too) — Node does not rewrite .js to .ts. The tsconfig that covers these files needs allowImportingTsExtensions.
  • Erasable syntax only — no enum, namespace or parameter properties. codestyle names the file and the reason if one slips in. Node 22.18+ is required for a .ts config; codestyle.config.mjs works on older Node — unless it imports .ts rule files, which still need 22.18+.

Config keys

Key Default What it is
settings {} app landmarks for the package rules (SharedRuleSettings — see below)
rules {} name → 'off' | 'warn' | 'error' for package and custom rules. An unknown name, an engine rule (IG0–IG2) or an ESLint/test-owned declaration here is a config error
custom [] this repo's rules (each carries a proof) and check-less declarations owned by ESLint or a test. Shape-checked: a rule has no owner and needs level (off/warn/error), a non-empty scope, a check function and title/why/fix; a declaration has owner: 'eslint' | 'test' and ref/title/why. Errors name the field (custom[2].level)
overrides [] { files, rules, reason } — a level for some rules on some files, reason mandatory
files ['app', 'config', 'tests', 'codestyle'] scan roots: directories, files or globs
ignores ['node_modules', '.nuxt', '.output', 'dist', 'coverage'] pruned from the walk: names, paths or globs
ruleSources ['codestyle/'] where this repo's rules live: a staged change there triggers the self-test; their files are exempt from ignore-comment parsing (fixture strings carry ignore comments on purpose)
docs [] { template, output, tags? } rendered by codestyle docs
validate [] { name, run } steps for codestyle validate
precommit [] { name, run } steps codestyle precommit runs after the staged check

An unknown top-level key is a config error, so a typo never silently does nothing.

Package rules

Rule Default Catches
no-bare-storage warn* bare localStorage / sessionStorage — the accessor itself throws on Safari ITP and in in-app webviews
inline-scss error SCSS rules inside a Vue <style> block instead of a referenced .scss file
guarded-json-parse warn* JSON.parse with no enclosing try/catch — a corrupt stored value breaks that path permanently
font-face-single-family error @font-face declaring a font stack — an invalid descriptor, so the browser drops the face silently
vueuse-first warn a hand-rolled browser API with a VueUse equivalent
ui-kit-unique-names warn a component whose name duplicates a UI-kit component
scss-tokens warn a literal colour in SCSS instead of a design token
date-format-enum warn a hand-rolled date format string, or the date part cut out of toISOString()
types-layout warn a type file outside types/<entity>/{enums,interfaces,types}.ts
typography-source warn a raw font-family / font-size / font-weight instead of the app's typography source
script-attr-order warn <script> attribute order differing from the app's canonical

error is reserved for crash-class rules; conventions ship at warn so each app measures its backlog before anything blocks. no-bare-storage and guarded-json-parse (marked *) ship at warn as an interim state while the fleet backlog exists; their target is error. Underscore-prefixed SCSS partials are declaration files and exempt from scss-tokens and typography-source. Hover a packageRuleNames member for the rule's contract.

settings

settings: {
  safeStorageModule: 'safeLocalStorage from @helpers/safe-local-storage', // no-bare-storage / guarded-json-parse messages
  safeStorageFile:   'app/utils/helpers/safe-local-storage.ts',           // no-bare-storage exempts the wrapper itself
  safeJsonParse:     'parseStoredJson() from @helpers/safe-local-storage',
  scssTokensFile:    'app/assets/scss/_colors.scss',
  uiKitRoot:         'app/components/UI',                                 // ui-kit-unique-names (default shown)
  dateFormatModule:  'DateFormat from @ts-types/date/enums',
  typesRoot:         'app/types',                                         // types-layout (default shown)
  typographySource:  'the font() mixin from @scss/_mixins.scss',
  scriptAttrOrder:   'setup-first',                                       // or 'lang-first' (default 'setup-first')
}

A missing key degrades the rule's message or falls back to the documented default — never silences the rule.

Commands

Command What it does
codestyle check runs the rules. Flags: --staged (this commit), --stdin --file=<path> (check piped content as that file), --json, --only=<a,b> (by name; --rule=<a,b> is an alias, merged with --only), --tags=<a,b> (by tag — a tag no rule carries is exit 2), --no-self-test, --no-color
codestyle list every rule: name · level · owner · tags
codestyle self-test [--package] proves this repo's custom rules (self-test: 0 rule(s) proven, exit 0, when there are none); --package proves the package's own rules
codestyle docs [--check] renders every docs entry; --check writes nothing and fails on a stale or missing output. No docs entries is exit 2
codestyle validate runs the validate steps — every step, one exit code. No steps is exit 2
codestyle precommit check --staged first; if it fails, the steps are skipped. Then the precommit steps run as one group — every step, one exit code. Any other argument is passed to check
codestyle migrate [--write] moves a 0.5.x repo: suppression spelling, renamed ids, a config skeleton

Every command takes --root=<path> (default: the working directory). Exit codes: 0 clean or warnings only · 1 an error-level finding, a failed self-test, a stale doc or a failed step · 2 the tool itself failed (bad flags, no or bad config — a malformed until such as until 2026-1-5 included — or a crash).

Suppressions and overrides

One line, reviewed: // codestyle-ignore: <name> — <reason> suppresses that line and the next. The reason is mandatory. Three engine rules police the escape hatch:

  • IG0 — an ignore without a reason;
  • IG1 — a name that is not a live rule (a renamed package rule says what it became), or an override that no longer applies (a glob that matches no file, an until that has passed);
  • IG2 — an ignore naming a rule declared noIgnore; the comment suppresses nothing.

// invariant-ignore: is still accepted through 1.x with a one-line deprecation warning per file; codestyle migrate --write rewrites it.

A place-wide exemption is an override: { files, rules, reason }. An override can only lower a level, never raise it. A rules entry is the base level of its rule and a ceiling over a finding's own level; without one, a finding's own level wins. A reason may carry until YYYY-MM-DD: from until − 7 days through until itself (inclusive) the exemption reports at warn; after the date it stops applying and is reported. A malformed date is a config error. A rule may require that date on every override of it (exemptionsRequireUntil).

--json

{ schemaVersion: 1, ok, scanned, errors, warnings,
  rules: [{ name, level, title, tags, owner: 'package' | 'custom' | 'eslint' | 'test' }],
  findings: [{ rule, level, file, line, message }],
  skipped?: { file, reason },                      // --stdin: outside `files`, under `ignores`, or no rule scopes it
  selfTest?: { ok, failures } }                    // present whenever the self-test ran

// a tool failure (exit 2) under --json:
{ schemaVersion: 1, ok: false, error }

owner is package (this package's rules), custom (the repo's own), eslint or test (declarations: never run, here so their names stay live). schemaVersion changes only with a breaking shape change. ESLint- and test-owned declarations appear in rules with no findings. Output always drains before the process exits — a large report through a pipe arrives whole.

What checks what

Tool Answers Runs
ESLint (+ this package's flat config and local rules) "is this file well-formed by our style?" — one file's AST, IDE feedback, autofix editor, pre-commit, CI
This package "does the repo still satisfy its contracts?" — anything needing a second file, a registry, the test tree, SCSS/assets/config surfaces, project semantics, or an audited exemption pre-commit (--staged), CI (whole repo), the AI write-time hook (--stdin)
Auditor "what in the repo's state needs a human decision?" — dead code, dependency health, churn, LLM change review, and the trend of this package's warnings explicit, human-triggered

If one file's AST decides it and a plugin or a small shared rule can say it, it is ESLint — declared in custom with owner: 'eslint' so there is one catalogue. If the author can fix it before the commit lands, gate it here; if it needs history, external signals or judgment, it belongs to the auditor, which reads codestyle check --json rather than grepping.

Wiring a consumer

// package.json
"scripts": {
  "codestyle": "codestyle check",
  "codestyle:e2e": "codestyle check --tags=e2e",
  "validate": "codestyle validate"
}
  • Pre-commit: the hook file is one line, pnpm exec codestyle precommit. Keep it in a tracked directory and arm it from postinstall with git config core.hooksPath <dir> || true (the || true keeps installs without .git, such as Docker builds, green).
  • CI: one step, pnpm exec codestyle validate, with the full chain declared in validate (the codestyle check, lint, tests, typecheck).
  • AI write-time hook: codestyle check --stdin --file=<repo-relative path> [--tags=<x>] checks content before it reaches the disk; a path outside files, under ignores, or scoped by no selected rule comes back as skipped.
  • Agent rules file: declare docs: [{ template, output, tags }] and run codestyle docs; the generated file says so in its first line and is never edited by hand. Put codestyle docs --check in validate so drift fails the gate.
  • CLAUDE.md: paste docs/consumer-claude-md.md.

The ESLint config — export data, never factories

// 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 an override that does not spread the shared map silently discards every shared entry — that is why the maps are exported as data. Seven local rules: padding-after-macros, async-components-after-macros, no-import-group-labels, ref-prefix-prop-mirrors, require-component-name, on-prefixed-callbacks, optional-callback-chaining.

Imports

Import What it is
@brighterly/lib-fe-codestyle defineConfig; the rule contract types (Rule, RuleContext, the proofs, …); the helpers custom rules share (eachLine, setting, stripCodeComments, matchesAny, the AST helpers, …); packageRuleNames
@brighterly/lib-fe-codestyle/eslint the shared flat config, its local rules and the maps behind them

Everything else is reached through the codestyle binary. src/ ships compiled as dist/ (Node does not strip types inside node_modules); eslint/ ships as plain .mjs.

In the IDE: defineConfig types the whole config, a packageRuleNames member's hover is that rule's contract, and ESLint squiggles carry each local rule's description. The rule engine is not an IDE inspection: its findings appear in the terminal and the pre-commit hook. After upgrading this package, run ESLint: Restart Service.

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 name? Yes → major. Adds signal without blocking → minor. Everything else → patch.

Change Bump Why
New package 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 or removing a rule name major breaks every codestyle-ignore: comment, rules/overrides key and doc that names it — 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 behaviour change (exit codes, the --json shape, the config or rule contract) major consumers' configs, hooks and CI are written against it
New package 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, proof 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.

Publishing

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.

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.

Design

docs/superpowers/specs/2026-10-16-consumer-architecture-v1-design.md

Version History

Version Released License
1.0.3 Proprietary
1.0.2 Proprietary
1.0.1 Proprietary
1.0.0 Proprietary
0.5.2 Proprietary