@brighterly/lib-fe-codestyle 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,6 +5,10 @@ cs-app, app-tutor, app-admin.
5
5
 
6
6
  It holds the **mechanisms**. Domain rules stay in each app.
7
7
 
8
+ **New here?** [docs/how-it-works.md](docs/how-it-works.md) — how a check reads real
9
+ code, what the gate can and cannot cover, what is already enforced, how to extend it,
10
+ and how to run it.
11
+
8
12
  ## Subpaths
9
13
 
10
14
  | Import | What it is |
@@ -53,9 +57,9 @@ exported as data.
53
57
 
54
58
  | Rule | Level | Checks |
55
59
  |---|---|---|
56
- | `core/R1` | error | bare `localStorage` / `sessionStorage` — the accessor itself throws on Safari ITP and in-app webviews |
60
+ | `core/R1` | warn* | bare `localStorage` / `sessionStorage` — the accessor itself throws on Safari ITP and in-app webviews |
57
61
  | `core/R2` | error | SCSS rules inline in a Vue `<style>` block instead of a referenced `.scss` file |
58
- | `core/R3` | error | `JSON.parse` with no enclosing `try`/`catch` — a corrupt entry breaks that path permanently |
62
+ | `core/R3` | warn* | `JSON.parse` with no enclosing `try`/`catch` — a corrupt entry breaks that path permanently |
59
63
  | `core/R4` | error | `@font-face` declaring a font *stack* — an invalid descriptor, so the browser drops the face silently |
60
64
  | `core/C1` | warn | hand-rolled browser API with a VueUse equivalent |
61
65
  | `core/C2` | warn | a component duplicating an existing UI-kit component name |
@@ -65,10 +69,12 @@ exported as data.
65
69
  | `core/C6` | warn | a raw `font-family` / `font-size` / `font-weight` declaration instead of the app's typography source |
66
70
  | `core/C7` | warn | `<script>` attribute order differing from the app's canonical (`setup lang` vs `lang setup`) |
67
71
 
68
- Level discipline: `error` is reserved for crash-class rules (`core/R*` — a visitor-facing
69
- throw or silent production breakage); `warn` is for conventions and style choices
70
- (`core/C*`). A consumer's `levels` ceiling on an `core/R*` rule is NOT the tool for an existing
71
- backlog — fix the call sites or allowlist the reviewed exceptions with reasons.
72
+ Level discipline: `error` is for crash-class rules (`core/R*`); `warn` is for
73
+ conventions (`core/C*`). `core/R1`/`core/R3` (marked *) ship at `warn` **as an interim
74
+ state** (2026-09-25): their fleet backlog is real code nobody has bandwidth to migrate
75
+ and re-verify right now. Their target level is `error` — promote once the backlog
76
+ clears (a breaking-style bump). `core/R2`/`core/R4` stay `error`. A consumer's `levels`
77
+ ceiling is still never the tool for a backlog on an `error` rule — fix or allowlist.
72
78
  Underscore-prefixed SCSS partials (`_extends.scss`, `_colors-*.scss`, …) are declaration
73
79
  files by convention in every Brighterly app and are exempt from `core/C3`/`core/C6` wholesale.
74
80
 
@@ -108,6 +114,7 @@ export const settings = {
108
114
  typesRoot: 'app/types', // core/C5 (default shown)
109
115
  typographySource: 'the font() mixin from @scss/_mixins.scss', // core/C6 — cs-app points this at its AppText component instead
110
116
  scriptAttrOrder: 'setup-first', // core/C7 — or 'lang-first' (default 'setup-first')
117
+ selfTestFile: 'invariants/self-test.mts', // its ignore-strings are fixture DATA — skipped by IG0/IG1
111
118
  }
112
119
  ```
113
120
 
@@ -28,11 +28,11 @@ export declare const commonRules: Rule[];
28
28
  * instead. A test pins this map to `commonRules`, so it cannot drift.
29
29
  */
30
30
  export declare const sharedRuleIds: {
31
- /** `error` — bare `localStorage`/`sessionStorage` access. The accessor itself throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews; `if (localStorage)` guards throw too. Fix: the app's safe storage wrapper (`settings.safeStorageModule`). */
31
+ /** `warn` (target `error`, interim 2026-09-25) — bare `localStorage`/`sessionStorage` access. The accessor itself throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews; `if (localStorage)` guards throw too. Fix: the app's safe storage wrapper (`settings.safeStorageModule`). */
32
32
  readonly 'core/R1': "core/R1";
33
33
  /** `error` — SCSS rules written inline in a Vue `<style>` block. Inline styles drift from the design tokens invisibly. Fix: a dedicated `.scss` file referenced via `<style scoped lang="scss" src="…" />`. */
34
34
  readonly 'core/R2': "core/R2";
35
- /** `error` — `JSON.parse` with no enclosing `try`/`catch`. A corrupt stored value crashes that code path permanently for the visitor. Fix: the app's guarded parse helper (`settings.safeJsonParse`) or try/catch. */
35
+ /** `warn` (target `error`, interim 2026-09-25) — `JSON.parse` with no enclosing `try`/`catch`. A corrupt stored value crashes that code path permanently for the visitor. Fix: the app's guarded parse helper (`settings.safeJsonParse`) or try/catch. */
36
36
  readonly 'core/R3': "core/R3";
37
37
  /** `error` — `@font-face` declaring a font *stack*. Inside `@font-face`, `font-family` names the face being defined; a comma-separated stack is invalid and the browser drops the face silently. Fix: single family name inside, the stack on the selectors that use it. */
38
38
  readonly 'core/R4': "core/R4";
@@ -28,11 +28,11 @@ export const commonRules = [...rRules, ...cRules];
28
28
  * instead. A test pins this map to `commonRules`, so it cannot drift.
29
29
  */
30
30
  export const sharedRuleIds = {
31
- /** `error` — bare `localStorage`/`sessionStorage` access. The accessor itself throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews; `if (localStorage)` guards throw too. Fix: the app's safe storage wrapper (`settings.safeStorageModule`). */
31
+ /** `warn` (target `error`, interim 2026-09-25) — bare `localStorage`/`sessionStorage` access. The accessor itself throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews; `if (localStorage)` guards throw too. Fix: the app's safe storage wrapper (`settings.safeStorageModule`). */
32
32
  'core/R1': 'core/R1',
33
33
  /** `error` — SCSS rules written inline in a Vue `<style>` block. Inline styles drift from the design tokens invisibly. Fix: a dedicated `.scss` file referenced via `<style scoped lang="scss" src="…" />`. */
34
34
  'core/R2': 'core/R2',
35
- /** `error` — `JSON.parse` with no enclosing `try`/`catch`. A corrupt stored value crashes that code path permanently for the visitor. Fix: the app's guarded parse helper (`settings.safeJsonParse`) or try/catch. */
35
+ /** `warn` (target `error`, interim 2026-09-25) — `JSON.parse` with no enclosing `try`/`catch`. A corrupt stored value crashes that code path permanently for the visitor. Fix: the app's guarded parse helper (`settings.safeJsonParse`) or try/catch. */
36
36
  'core/R3': 'core/R3',
37
37
  /** `error` — `@font-face` declaring a font *stack*. Inside `@font-face`, `font-family` names the face being defined; a comma-separated stack is invalid and the browser drops the face silently. Fix: single family name inside, the stack on the selectors that use it. */
38
38
  'core/R4': 'core/R4',
@@ -14,7 +14,11 @@ import { eachLine, setting } from './shared.js';
14
14
  const bareStorage = {
15
15
  id: 'core/R1',
16
16
  title: 'bare localStorage / sessionStorage access',
17
- level: 'error',
17
+ // Target level is `error` (crash-class). Shipped at `warn` since 2026-09-25: the fleet
18
+ // backlog (cs-app ~42 sites, main-app ~28) is real code the team has no bandwidth to
19
+ // migrate and re-verify right now, and a rushed storage migration is its own risk.
20
+ // Promote back to `error` once the backlog is cleared — that is a major-style bump.
21
+ level: 'warn',
18
22
  why: 'the bare API throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews (Instagram/Facebook/TikTok) — the `if (localStorage)` guard does not help, the property accessor itself throws',
19
23
  fix: 'use the app\'s safe storage wrapper instead of the bare API',
20
24
  scope: ['app/**/*.{ts,vue}'],
@@ -68,7 +72,8 @@ const scssInStyleBlock = {
68
72
  const unguardedJsonParse = {
69
73
  id: 'core/R3',
70
74
  title: 'unguarded JSON.parse',
71
- level: 'error',
75
+ // Target level is `error` — see core/R1 above; same interim decision (2026-09-25).
76
+ level: 'warn',
72
77
  async: true,
73
78
  why: 'a truncated write or a hand-edited storage entry makes JSON.parse throw on every re-entry to that code path, until the visitor clears storage — the failure is permanent for them, not transient',
74
79
  fix: 'parse through the app\'s guarded helper, or wrap the call in try/catch',
@@ -228,7 +228,9 @@ export const run = async (options) => {
228
228
  const ignoreCache = new Map();
229
229
  const ignoresFor = (file) => {
230
230
  if (!ignoreCache.has(file)) {
231
- const src = read(file);
231
+ // The consumer's self-test file holds literal `invariant-ignore:` strings as
232
+ // fixture DATA — parsing them would report the fixtures themselves (IG0/IG1).
233
+ const src = file === settings.selfTestFile ? null : read(file);
232
234
  ignoreCache.set(file, src === null ? { suppressions: new Map(), unreasoned: [], comments: [] } : parseIgnores(src));
233
235
  }
234
236
  return ignoreCache.get(file);
@@ -348,6 +350,7 @@ export const run = async (options) => {
348
350
  live, retired, allowlists, levels, ignores,
349
351
  configFile,
350
352
  configSource: read(configFile) ?? undefined,
353
+ pathExists: p => read(p) !== null,
351
354
  })) {
352
355
  findings.push({ ruleId: 'IG1', level: 'error', ...v });
353
356
  }
@@ -74,6 +74,8 @@ export interface SharedRuleSettings {
74
74
  typographySource?: string;
75
75
  /** `core/C7`: which `<script>` attribute order is canonical in this app. Default 'setup-first' (`<script setup lang="ts">`). */
76
76
  scriptAttrOrder?: 'setup-first' | 'lang-first';
77
+ /** The consumer file whose `invariant-ignore:` strings are self-test FIXTURE DATA, not real suppressions — the engine skips ignore-parsing it entirely, replacing per-file IG0/IG1 allowlist boilerplate. E.g. 'invariants/self-test.mts'. */
78
+ selfTestFile?: string;
77
79
  }
78
80
  /** What a rule's `check` receives. */
79
81
  export interface RuleContext {
@@ -164,6 +166,8 @@ export interface DanglingRefInput {
164
166
  configFile?: string;
165
167
  /** Contents of `configFile`, so a stale key can be reported on its own line. */
166
168
  configSource?: string;
169
+ /** Answers "does this repo-relative file exist?" — enables the allowlist-path staleness check. */
170
+ pathExists?: (path: string) => boolean;
167
171
  }
168
172
  /** An invariant a built-in ESLint rule already covers. */
169
173
  export interface LinterOwnedRule {
@@ -214,6 +214,21 @@ export const findDanglingRuleRefs = (input) => {
214
214
  continue;
215
215
  out.push({ file: configFile, line: lineOfKey('levels', id), message: `levels names "${id}", but ${explain(id)} — the severity override does nothing; drop it` });
216
216
  }
217
+ // Allowlist PATHS had no staleness guard: an entry whose file is gone (or renamed)
218
+ // is inert and silent. Found live in app-funnels — a dead BtnAuthGoogle.vue entry
219
+ // survived a fork divergence (2026-09-25). Checked only when the caller can answer
220
+ // existence, so the self-test can still exercise this function without a filesystem.
221
+ if (input.pathExists) {
222
+ for (const [id, entries] of Object.entries(allowlists)) {
223
+ if (!live.has(id))
224
+ continue; // the dead-id finding above already covers the whole entry
225
+ for (const path of Object.keys(entries ?? {})) {
226
+ if (input.pathExists(path))
227
+ continue;
228
+ out.push({ file: configFile, line: lineOfKey('allowlists', path), message: `allowlists["${id}"] names "${path}", but no such file exists — the exemption is inert; drop it or fix the path` });
229
+ }
230
+ }
231
+ }
217
232
  for (const { file, line, ids } of ignores) {
218
233
  for (const id of ids) {
219
234
  if (live.has(id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brighterly/lib-fe-codestyle",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Shared invariant-validator engine, rule pack and ESLint config for Brighterly Nuxt frontend apps",