@brighterly/lib-fe-codestyle
| Install | |
|---|---|
npm install @brighterly/lib-fe-codestyle |
|
| Latest Version: | 1.0.3 |
| License: | Proprietary |
| Last Updated: | Oct 22, 2026 |
| Links: | npm |
@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 insidecodestyle/too) — Node does not rewrite.jsto.ts. The tsconfig that covers these files needsallowImportingTsExtensions. - Erasable syntax only — no
enum,namespaceor parameter properties.codestylenames the file and the reason if one slips in. Node 22.18+ is required for a.tsconfig;codestyle.config.mjsworks on older Node — unless it imports.tsrule 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
untilthat 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 frompostinstallwithgit config core.hooksPath <dir> || true(the|| truekeeps installs without.git, such as Docker builds, green). - CI: one step,
pnpm exec codestyle validate, with the full chain declared invalidate(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 outsidefiles, underignores, or scoped by no selected rule comes back asskipped. - Agent rules file: declare
docs: [{ template, output, tags }]and runcodestyle docs; the generated file says so in its first line and is never edited by hand. Putcodestyle docs --checkinvalidateso 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 | |
| 0.5.1 | Proprietary | |
| 0.4.1 | Proprietary | |
| 0.3.0 | Proprietary | |
| 0.2.0 | Proprietary | |
| 0.1.2 | Proprietary | |
| 0.1.0 | Proprietary |