@brighterly/lib-fe-codestyle 0.2.0 → 0.3.0
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 +30 -30
- package/dist/invariants/rules/convention.d.ts +2 -2
- package/dist/invariants/rules/convention.js +10 -10
- package/dist/invariants/rules/fixtures.d.ts +1 -1
- package/dist/invariants/rules/fixtures.js +11 -11
- package/dist/invariants/rules/index.d.ts +12 -12
- package/dist/invariants/rules/index.js +12 -12
- package/dist/invariants/rules/regression.js +4 -4
- package/dist/invariants/types.d.ts +11 -11
- package/dist/invariants/util.js +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,28 +53,28 @@ exported as data.
|
|
|
53
53
|
|
|
54
54
|
| Rule | Level | Checks |
|
|
55
55
|
|---|---|---|
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
|
|
68
|
-
Level discipline: `error` is reserved for crash-class rules (`
|
|
56
|
+
| `core/R1` | error | bare `localStorage` / `sessionStorage` — the accessor itself throws on Safari ITP and in-app webviews |
|
|
57
|
+
| `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 |
|
|
59
|
+
| `core/R4` | error | `@font-face` declaring a font *stack* — an invalid descriptor, so the browser drops the face silently |
|
|
60
|
+
| `core/C1` | warn | hand-rolled browser API with a VueUse equivalent |
|
|
61
|
+
| `core/C2` | warn | a component duplicating an existing UI-kit component name |
|
|
62
|
+
| `core/C3` | warn | a literal colour in SCSS instead of a design token |
|
|
63
|
+
| `core/C4` | warn | a hand-rolled date format string, or `toISOString()` |
|
|
64
|
+
| `core/C5` | warn | a type file outside the `types/<entity>/{enums,interfaces,types}.ts` structure |
|
|
65
|
+
| `core/C6` | warn | a raw `font-family` / `font-size` / `font-weight` declaration instead of the app's typography source |
|
|
66
|
+
| `core/C7` | warn | `<script>` attribute order differing from the app's canonical (`setup lang` vs `lang setup`) |
|
|
67
|
+
|
|
68
|
+
Level discipline: `error` is reserved for crash-class rules (`core/R*` — a visitor-facing
|
|
69
69
|
throw or silent production breakage); `warn` is for conventions and style choices
|
|
70
|
-
(`
|
|
70
|
+
(`core/C*`). A consumer's `levels` ceiling on an `core/R*` rule is NOT the tool for an existing
|
|
71
71
|
backlog — fix the call sites or allowlist the reviewed exceptions with reasons.
|
|
72
72
|
Underscore-prefixed SCSS partials (`_extends.scss`, `_colors-*.scss`, …) are declaration
|
|
73
|
-
files by convention in every Brighterly app and are exempt from `
|
|
73
|
+
files by convention in every Brighterly app and are exempt from `core/C3`/`core/C6` wholesale.
|
|
74
74
|
|
|
75
|
-
`
|
|
75
|
+
`core/R*` were ported from land-app, where each was a production scar (as `R1`, `R7`, `R18`,
|
|
76
76
|
`R28` — renumbered sequentially when the prefix landed, pre-publish, while no released
|
|
77
|
-
version pinned the old tokens). `
|
|
77
|
+
version pinned the old tokens). `core/C*` are conventions with
|
|
78
78
|
no incident behind them *yet* — they ship at `warn` so a consuming app can measure what they
|
|
79
79
|
find before anyone decides to block on them. Promoting one to `error` is a **major** bump.
|
|
80
80
|
|
|
@@ -100,21 +100,21 @@ A shared rule must not hardcode a consuming app's paths. Each app declares them
|
|
|
100
100
|
// invariants/config.mts
|
|
101
101
|
export const settings = {
|
|
102
102
|
safeStorageModule: 'safeLocalStorage from @helpers/safe-local-storage',
|
|
103
|
-
safeStorageFile: 'app/utils/helpers/safe-local-storage.ts', //
|
|
103
|
+
safeStorageFile: 'app/utils/helpers/safe-local-storage.ts', // core/R1 auto-exempts the wrapper itself
|
|
104
104
|
safeJsonParse: 'parseStoredJson() from @helpers/safe-local-storage',
|
|
105
105
|
scssTokensFile: 'app/assets/scss/_colors.scss',
|
|
106
106
|
uiKitRoot: 'app/components/UI',
|
|
107
107
|
dateFormatModule: 'DateFormat from @ts-types/date/enums',
|
|
108
|
-
typesRoot: 'app/types', //
|
|
109
|
-
typographySource: 'the font() mixin from @scss/_mixins.scss', //
|
|
110
|
-
scriptAttrOrder: 'setup-first', //
|
|
108
|
+
typesRoot: 'app/types', // core/C5 (default shown)
|
|
109
|
+
typographySource: 'the font() mixin from @scss/_mixins.scss', // core/C6 — cs-app points this at its AppText component instead
|
|
110
|
+
scriptAttrOrder: 'setup-first', // core/C7 — or 'lang-first' (default 'setup-first')
|
|
111
111
|
}
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
A missing key degrades the rule's *message*, never its firing. A rule that went quiet
|
|
115
115
|
because a path was unset would be the exact silent failure this package exists to prevent.
|
|
116
|
-
Two keys shape behaviour rather than the message — `typesRoot` (where `
|
|
117
|
-
`scriptAttrOrder` (which direction `
|
|
116
|
+
Two keys shape behaviour rather than the message — `typesRoot` (where `core/C5` looks) and
|
|
117
|
+
`scriptAttrOrder` (which direction `core/C7` flags) — and both fall back to the documented
|
|
118
118
|
default above, so leaving them unset still never silences the rule.
|
|
119
119
|
|
|
120
120
|
## What consumers see in the IDE
|
|
@@ -124,7 +124,7 @@ requires opening this repo:
|
|
|
124
124
|
|
|
125
125
|
- **`sharedRuleIds`** (from `…/invariants/rules`) — the catalog: one constant per rule,
|
|
126
126
|
each JSDoc'd with level, what it checks, and the fix. Use them as `allowlists` /
|
|
127
|
-
`levels` keys (`[sharedRuleIds.
|
|
127
|
+
`levels` keys (`[sharedRuleIds.core/C6]: 'off'`) and hover for the contract. A test pins
|
|
128
128
|
the catalog to the shipped rules, so it cannot drift.
|
|
129
129
|
- **`SharedRuleSettings`** (from `…/invariants`) — annotate the app's `settings` const
|
|
130
130
|
with it to get completion plus the per-key contract, and a type error on an invalid
|
|
@@ -171,18 +171,18 @@ The ESLint config and its seven rules are already plain `.mjs` and ship as-is.
|
|
|
171
171
|
|
|
172
172
|
## Rule IDs
|
|
173
173
|
|
|
174
|
-
Shared rules carry
|
|
175
|
-
`R1`, `B2`. The prefix marks provenance —
|
|
174
|
+
Shared rules carry the **`core/` prefix**: `core/R1`, `core/C3`. App-local rules stay bare:
|
|
175
|
+
`R1`, `B2`. The prefix marks provenance — a `core/`-id is shipped by this package, never
|
|
176
176
|
defined, renumbered or recycled in a consumer — and it keeps the two number lines
|
|
177
177
|
independent: every app allocates its own `R1, R2, …` naturally, with no risk of
|
|
178
178
|
colliding with a rule the package ships later. `run()` still exits 2 on a duplicate,
|
|
179
179
|
so a collision is loud, not silent.
|
|
180
180
|
|
|
181
|
-
Within the package each family is numbered sequentially with no gaps (`
|
|
182
|
-
`
|
|
181
|
+
Within the package each family is numbered sequentially with no gaps (`core/R1`–`core/R4`,
|
|
182
|
+
`core/C1`–`core/C7`) — enforced by `test/invariants/rule-ids.test.ts`. The family letter keeps
|
|
183
183
|
meaning provenance — `R` regression scar, `B` business, `C` convention, `L`
|
|
184
|
-
linter-owned — not topic.
|
|
185
|
-
the
|
|
184
|
+
linter-owned — not topic. `core/` in a report means exactly one thing: this finding
|
|
185
|
+
comes from the library, and its numbering is the library's to own.
|
|
186
186
|
|
|
187
187
|
## Versioning is about blocking power
|
|
188
188
|
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Convention rules — preferences we chose, with no production incident behind them.
|
|
3
3
|
* Advisory by default (`warn`): nudged, not policed, and droppable by team consensus.
|
|
4
4
|
*
|
|
5
|
-
* `
|
|
5
|
+
* `core/C1` was land-app's `C1`, and reads the same everywhere. `core/C2`-`core/C4` are new, and came out of a
|
|
6
6
|
* real code review rather than an outage: a developer shipped a component that duplicated
|
|
7
7
|
* an existing UI-kit one, with colours that were not design tokens. Review caught it;
|
|
8
8
|
* nothing automated did, because the conventions existed only in reviewers\' heads.
|
|
9
9
|
*
|
|
10
|
-
* `
|
|
10
|
+
* `core/C5`-`core/C7` (2026-09-23) come from the org's Notion frontend-conventions doc and from
|
|
11
11
|
* mining 400 PR review comments across the four apps: entity-folder type organization,
|
|
12
12
|
* typography through the app's declared source, and one canonical <script> attribute order.
|
|
13
13
|
*
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Convention rules — preferences we chose, with no production incident behind them.
|
|
3
3
|
* Advisory by default (`warn`): nudged, not policed, and droppable by team consensus.
|
|
4
4
|
*
|
|
5
|
-
* `
|
|
5
|
+
* `core/C1` was land-app's `C1`, and reads the same everywhere. `core/C2`-`core/C4` are new, and came out of a
|
|
6
6
|
* real code review rather than an outage: a developer shipped a component that duplicated
|
|
7
7
|
* an existing UI-kit one, with colours that were not design tokens. Review caught it;
|
|
8
8
|
* nothing automated did, because the conventions existed only in reviewers\' heads.
|
|
9
9
|
*
|
|
10
|
-
* `
|
|
10
|
+
* `core/C5`-`core/C7` (2026-09-23) come from the org's Notion frontend-conventions doc and from
|
|
11
11
|
* mining 400 PR review comments across the four apps: entity-folder type organization,
|
|
12
12
|
* typography through the app's declared source, and one canonical <script> attribute order.
|
|
13
13
|
*
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { eachLine, setting } from './shared.js';
|
|
18
18
|
const vueUseFirst = {
|
|
19
|
-
id: '
|
|
19
|
+
id: 'core/C1',
|
|
20
20
|
title: 'hand-rolled browser API with a VueUse equivalent',
|
|
21
21
|
level: 'warn',
|
|
22
22
|
why: 'VueUse handles the lifecycle, SSR and error paths we otherwise re-implement (and forget to tear down) per call site',
|
|
@@ -47,7 +47,7 @@ const vueUseFirst = {
|
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
49
|
const uiKitReuseFirst = {
|
|
50
|
-
id: '
|
|
50
|
+
id: 'core/C2',
|
|
51
51
|
title: 'component duplicates an existing UI-kit component name',
|
|
52
52
|
level: 'warn',
|
|
53
53
|
once: true,
|
|
@@ -81,7 +81,7 @@ const uiKitReuseFirst = {
|
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
83
|
const scssColourTokens = {
|
|
84
|
-
id: '
|
|
84
|
+
id: 'core/C3',
|
|
85
85
|
title: 'literal colour in SCSS instead of a design token',
|
|
86
86
|
level: 'warn',
|
|
87
87
|
why: 'a hardcoded colour is invisible to the token system — it survives theme changes and palette corrections silently, and only a side-by-side screenshot shows the drift',
|
|
@@ -108,7 +108,7 @@ const scssColourTokens = {
|
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
110
|
const dateFormatEnum = {
|
|
111
|
-
id: '
|
|
111
|
+
id: 'core/C4',
|
|
112
112
|
title: 'hand-rolled date format string instead of the shared enum',
|
|
113
113
|
level: 'warn',
|
|
114
114
|
why: 'a format string written at the call site makes the date\'s shape an accident rather than a contract; `date`-validated endpoints accept several shapes, so a wrong one ships silently and surfaces later as bad data',
|
|
@@ -134,7 +134,7 @@ const dateFormatEnum = {
|
|
|
134
134
|
},
|
|
135
135
|
};
|
|
136
136
|
const typesOrganization = {
|
|
137
|
-
id: '
|
|
137
|
+
id: 'core/C5',
|
|
138
138
|
title: 'type file outside the entity-folder structure',
|
|
139
139
|
level: 'warn',
|
|
140
140
|
once: true,
|
|
@@ -166,7 +166,7 @@ const typesOrganization = {
|
|
|
166
166
|
},
|
|
167
167
|
};
|
|
168
168
|
const typographySource = {
|
|
169
|
-
id: '
|
|
169
|
+
id: 'core/C6',
|
|
170
170
|
title: 'raw font declaration instead of the app\'s typography source',
|
|
171
171
|
level: 'warn',
|
|
172
172
|
why: 'a hand-written font-family/size/weight is invisible to the UI-kit type scale — it survives font and scale changes silently, and the page drifts off-brand one declaration at a time (Notion: UI та стилі)',
|
|
@@ -179,7 +179,7 @@ const typographySource = {
|
|
|
179
179
|
// Brighterly apps, decided 2026-09-24), plus non-underscore fonts/typography files.
|
|
180
180
|
if (/\/(?:_[^/]*|fonts|typography)\.scss$/.test(file))
|
|
181
181
|
return [];
|
|
182
|
-
// @font-face declares a face, not a usage —
|
|
182
|
+
// @font-face declares a face, not a usage — core/R4 polices those blocks.
|
|
183
183
|
const stripped = code.replace(/@font-face\s*\{[^}]*\}/g, m => m.replace(/[^\n]/g, ' '));
|
|
184
184
|
return eachLine(stripped, (line, n) => {
|
|
185
185
|
const m = /\bfont(?:-family|-size|-weight)?\s*:/.exec(line);
|
|
@@ -190,7 +190,7 @@ const typographySource = {
|
|
|
190
190
|
},
|
|
191
191
|
};
|
|
192
192
|
const scriptAttrOrder = {
|
|
193
|
-
id: '
|
|
193
|
+
id: 'core/C7',
|
|
194
194
|
title: 'script tag attribute order differs from the app canonical',
|
|
195
195
|
level: 'warn',
|
|
196
196
|
why: '<script setup lang="ts"> and <script lang="ts" setup> are compiler-equivalent, so nothing ever fails — the split just grows (723 vs 384 files in one app, Sep 2026) and every diff, codemod and grep has to handle both spellings',
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { Fixture } from '../self-test.js';
|
|
11
11
|
export declare const commonFixtures: Record<string, Fixture>;
|
|
12
12
|
/**
|
|
13
|
-
* `
|
|
13
|
+
* `core/C2` and `core/C5` are `once` rules over the whole file list — a single-file fixture
|
|
14
14
|
* cannot express them, so they are exempt from the fixture requirement and covered by
|
|
15
15
|
* their own tests instead.
|
|
16
16
|
*/
|
|
@@ -8,47 +8,47 @@
|
|
|
8
8
|
* `bad` must produce at least one finding, `good` none.
|
|
9
9
|
*/
|
|
10
10
|
export const commonFixtures = {
|
|
11
|
-
'
|
|
11
|
+
'core/R1': {
|
|
12
12
|
file: 'app/a.ts',
|
|
13
13
|
bad: 'const t = localStorage.getItem("k")',
|
|
14
14
|
good: 'const t = safeLocalStorage.getItem("k")',
|
|
15
15
|
},
|
|
16
|
-
'
|
|
16
|
+
'core/R2': {
|
|
17
17
|
file: 'app/components/A.vue',
|
|
18
18
|
bad: '<template><div /></template>\n<style scoped lang="scss">.a { color: red; }</style>',
|
|
19
19
|
good: '<template><div /></template>\n<style scoped lang="scss" src="@scss/components/A.scss" />',
|
|
20
20
|
},
|
|
21
|
-
'
|
|
21
|
+
'core/R3': {
|
|
22
22
|
file: 'app/a.ts',
|
|
23
23
|
bad: 'const v = JSON.parse(raw)',
|
|
24
24
|
good: 'let v = null\ntry { v = JSON.parse(raw) } catch { v = null }',
|
|
25
25
|
},
|
|
26
|
-
'
|
|
26
|
+
'core/R4': {
|
|
27
27
|
file: 'app/assets/scss/fonts.scss',
|
|
28
28
|
bad: '@font-face { font-family: Rubik, sans-serif; src: url(a.woff2); }',
|
|
29
29
|
good: '@font-face { font-family: Rubik; src: url(a.woff2); }',
|
|
30
30
|
},
|
|
31
|
-
'
|
|
31
|
+
'core/C1': {
|
|
32
32
|
file: 'app/a.ts',
|
|
33
33
|
bad: 'window.addEventListener("resize", onResize)',
|
|
34
34
|
good: 'useEventListener(window, "resize", onResize)',
|
|
35
35
|
},
|
|
36
|
-
'
|
|
36
|
+
'core/C3': {
|
|
37
37
|
file: 'app/assets/scss/components/Card.scss',
|
|
38
38
|
bad: '.card { color: #1f2937; }',
|
|
39
39
|
good: '.card { color: $slate-800; }',
|
|
40
40
|
},
|
|
41
|
-
'
|
|
41
|
+
'core/C4': {
|
|
42
42
|
file: 'app/a.ts',
|
|
43
43
|
bad: "const d = dayjs().format('YYYY-MM-DD')",
|
|
44
44
|
good: 'const d = dayjs().format(DateFormat.DefaultDate)',
|
|
45
45
|
},
|
|
46
|
-
'
|
|
46
|
+
'core/C6': {
|
|
47
47
|
file: 'app/assets/scss/components/Card.scss',
|
|
48
48
|
bad: '.card { font-size: 14px; }',
|
|
49
49
|
good: '.card { @include font(14px, 20px); color: $slate-800; }',
|
|
50
50
|
},
|
|
51
|
-
'
|
|
51
|
+
'core/C7': {
|
|
52
52
|
// Two script tags in OPPOSITE orders, so the bad fixture violates whichever
|
|
53
53
|
// direction the consuming app declares in settings.scriptAttrOrder. The good
|
|
54
54
|
// fixture carries no lang attribute, so it is clean under either canonical.
|
|
@@ -58,8 +58,8 @@ export const commonFixtures = {
|
|
|
58
58
|
},
|
|
59
59
|
};
|
|
60
60
|
/**
|
|
61
|
-
* `
|
|
61
|
+
* `core/C2` and `core/C5` are `once` rules over the whole file list — a single-file fixture
|
|
62
62
|
* cannot express them, so they are exempt from the fixture requirement and covered by
|
|
63
63
|
* their own tests instead.
|
|
64
64
|
*/
|
|
65
|
-
export const commonSelfTestSkip = ['
|
|
65
|
+
export const commonSelfTestSkip = ['core/C2', 'core/C5'];
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* C — convention: a preference we chose → team consensus
|
|
10
10
|
* L — linter: a built-in ESLint rule → eslint.config.mjs
|
|
11
11
|
*
|
|
12
|
-
* Shared rule ids carry the
|
|
12
|
+
* Shared rule ids carry the `core/` prefix (`core/R1`); app-local ids stay bare (`R1`).
|
|
13
13
|
* The prefix marks provenance and keeps the two number lines independent — and `run()`
|
|
14
14
|
* still fails fast on a duplicate rather than letting an allowlist silently suppress
|
|
15
15
|
* the wrong rule.
|
|
@@ -29,27 +29,27 @@ export declare const commonRules: Rule[];
|
|
|
29
29
|
*/
|
|
30
30
|
export declare const sharedRuleIds: {
|
|
31
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`). */
|
|
32
|
-
readonly
|
|
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
|
-
readonly
|
|
34
|
+
readonly 'core/R2': "core/R2";
|
|
35
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. */
|
|
36
|
-
readonly
|
|
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
|
-
readonly
|
|
38
|
+
readonly 'core/R4': "core/R4";
|
|
39
39
|
/** `warn` — hand-rolled browser API with a VueUse equivalent (`new IntersectionObserver`, `addEventListener`, `setInterval`, …). VueUse owns the lifecycle/SSR/error paths. Fix: the composable named in the message, imported from `@vueuse/core`. */
|
|
40
|
-
readonly
|
|
40
|
+
readonly 'core/C1': "core/C1";
|
|
41
41
|
/** `warn` — a component whose filename duplicates a UI-kit component (`settings.uiKitRoot`). A same-name second component splits the design system in two. Fix: import or extend the UI-kit one. */
|
|
42
|
-
readonly
|
|
42
|
+
readonly 'core/C2': "core/C2";
|
|
43
43
|
/** `warn` — a literal colour (`#hex`, numeric `rgba()`) in SCSS/Vue instead of a palette token. Hardcoded colours survive theme and palette changes silently. Fix: a token from `settings.scssTokensFile`. */
|
|
44
|
-
readonly
|
|
44
|
+
readonly 'core/C3': "core/C3";
|
|
45
45
|
/** `warn` — a hand-rolled date format string (`'YYYY-MM-DD'`) or `toISOString()`. The date's shape becomes an accident instead of a contract. Fix: the shared enum (`settings.dateFormatModule`). */
|
|
46
|
-
readonly
|
|
46
|
+
readonly 'core/C4': "core/C4";
|
|
47
47
|
/** `warn` — a type file outside `types/<entity>/{enums,interfaces,types}.ts` (root under `settings.typesRoot`). Root-level or mixed-kind type files accrete unrelated entities. `.d.ts` ambient files are exempt. */
|
|
48
|
-
readonly
|
|
48
|
+
readonly 'core/C5': "core/C5";
|
|
49
49
|
/** `warn` — a raw `font-family`/`font-size`/`font-weight` declaration. Hand-written fonts drift off the UI-kit type scale silently. Fix: the app's typography source (`settings.typographySource`). */
|
|
50
|
-
readonly
|
|
50
|
+
readonly 'core/C6': "core/C6";
|
|
51
51
|
/** `warn` — `<script>` attribute order differing from the app canonical (`settings.scriptAttrOrder`, default `setup-first`). The two spellings are compiler-equivalent, so the split only ever grows. */
|
|
52
|
-
readonly
|
|
52
|
+
readonly 'core/C7': "core/C7";
|
|
53
53
|
};
|
|
54
54
|
/** Union of every id the shared pack ships — for typing app config keys. */
|
|
55
55
|
export type SharedRuleId = keyof typeof sharedRuleIds;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* C — convention: a preference we chose → team consensus
|
|
10
10
|
* L — linter: a built-in ESLint rule → eslint.config.mjs
|
|
11
11
|
*
|
|
12
|
-
* Shared rule ids carry the
|
|
12
|
+
* Shared rule ids carry the `core/` prefix (`core/R1`); app-local ids stay bare (`R1`).
|
|
13
13
|
* The prefix marks provenance and keeps the two number lines independent — and `run()`
|
|
14
14
|
* still fails fast on a duplicate rather than letting an allowlist silently suppress
|
|
15
15
|
* the wrong rule.
|
|
@@ -29,25 +29,25 @@ export const commonRules = [...rRules, ...cRules];
|
|
|
29
29
|
*/
|
|
30
30
|
export const sharedRuleIds = {
|
|
31
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`). */
|
|
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
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. */
|
|
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',
|
|
39
39
|
/** `warn` — hand-rolled browser API with a VueUse equivalent (`new IntersectionObserver`, `addEventListener`, `setInterval`, …). VueUse owns the lifecycle/SSR/error paths. Fix: the composable named in the message, imported from `@vueuse/core`. */
|
|
40
|
-
|
|
40
|
+
'core/C1': 'core/C1',
|
|
41
41
|
/** `warn` — a component whose filename duplicates a UI-kit component (`settings.uiKitRoot`). A same-name second component splits the design system in two. Fix: import or extend the UI-kit one. */
|
|
42
|
-
|
|
42
|
+
'core/C2': 'core/C2',
|
|
43
43
|
/** `warn` — a literal colour (`#hex`, numeric `rgba()`) in SCSS/Vue instead of a palette token. Hardcoded colours survive theme and palette changes silently. Fix: a token from `settings.scssTokensFile`. */
|
|
44
|
-
|
|
44
|
+
'core/C3': 'core/C3',
|
|
45
45
|
/** `warn` — a hand-rolled date format string (`'YYYY-MM-DD'`) or `toISOString()`. The date's shape becomes an accident instead of a contract. Fix: the shared enum (`settings.dateFormatModule`). */
|
|
46
|
-
|
|
46
|
+
'core/C4': 'core/C4',
|
|
47
47
|
/** `warn` — a type file outside `types/<entity>/{enums,interfaces,types}.ts` (root under `settings.typesRoot`). Root-level or mixed-kind type files accrete unrelated entities. `.d.ts` ambient files are exempt. */
|
|
48
|
-
|
|
48
|
+
'core/C5': 'core/C5',
|
|
49
49
|
/** `warn` — a raw `font-family`/`font-size`/`font-weight` declaration. Hand-written fonts drift off the UI-kit type scale silently. Fix: the app's typography source (`settings.typographySource`). */
|
|
50
|
-
|
|
50
|
+
'core/C6': 'core/C6',
|
|
51
51
|
/** `warn` — `<script>` attribute order differing from the app canonical (`settings.scriptAttrOrder`, default `setup-first`). The two spellings are compiler-equivalent, so the split only ever grows. */
|
|
52
|
-
|
|
52
|
+
'core/C7': 'core/C7',
|
|
53
53
|
};
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { findJsonParseCalls } from '../ast.js';
|
|
13
13
|
import { eachLine, setting } from './shared.js';
|
|
14
14
|
const bareStorage = {
|
|
15
|
-
id: '
|
|
15
|
+
id: 'core/R1',
|
|
16
16
|
title: 'bare localStorage / sessionStorage access',
|
|
17
17
|
level: 'error',
|
|
18
18
|
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',
|
|
@@ -37,7 +37,7 @@ const bareStorage = {
|
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
const scssInStyleBlock = {
|
|
40
|
-
id: '
|
|
40
|
+
id: 'core/R2',
|
|
41
41
|
title: 'SCSS rules inside a Vue <style> block',
|
|
42
42
|
level: 'error',
|
|
43
43
|
why: 'inline style blocks drift from the design tokens and are invisible to the SCSS build — the component stops sharing the system without anything failing',
|
|
@@ -66,7 +66,7 @@ const scssInStyleBlock = {
|
|
|
66
66
|
},
|
|
67
67
|
};
|
|
68
68
|
const unguardedJsonParse = {
|
|
69
|
-
id: '
|
|
69
|
+
id: 'core/R3',
|
|
70
70
|
title: 'unguarded JSON.parse',
|
|
71
71
|
level: 'error',
|
|
72
72
|
async: true,
|
|
@@ -83,7 +83,7 @@ const unguardedJsonParse = {
|
|
|
83
83
|
},
|
|
84
84
|
};
|
|
85
85
|
const fontFaceStack = {
|
|
86
|
-
id: '
|
|
86
|
+
id: 'core/R4',
|
|
87
87
|
title: '@font-face with a font stack instead of one family name',
|
|
88
88
|
level: 'error',
|
|
89
89
|
why: 'inside @font-face, font-family names the face being defined; a comma-separated stack is an invalid descriptor and the browser drops the whole face silently. Every page then renders in the fallback font, nothing errors, and only a side-by-side screenshot shows it',
|
|
@@ -17,8 +17,8 @@ export type Level = 'error' | 'warn' | 'off';
|
|
|
17
17
|
* IDs are load-bearing (`--rule=`, `invariant-ignore:`, allowlists, levels) —
|
|
18
18
|
* retire one, never recycle it.
|
|
19
19
|
*/
|
|
20
|
-
/** App-local ids are bare; shared-pack ids carry the
|
|
21
|
-
export type RuleId = `${'' | '
|
|
20
|
+
/** App-local ids are bare; shared-pack ids carry the `core/` prefix (`core/R1`). */
|
|
21
|
+
export type RuleId = `${'' | 'core/'}${'R' | 'B' | 'C' | 'L'}${number}`;
|
|
22
22
|
/**
|
|
23
23
|
* Meta rules. They police the escape hatch rather than the app, so they have no
|
|
24
24
|
* family letter — but they are addressable exactly like any other rule
|
|
@@ -56,23 +56,23 @@ export interface ReportedFinding extends Finding {
|
|
|
56
56
|
* the documented default), never its firing.
|
|
57
57
|
*/
|
|
58
58
|
export interface SharedRuleSettings {
|
|
59
|
-
/** `
|
|
59
|
+
/** `core/R1`/`core/R3` fix messages: how this app names its safe storage wrapper, e.g. 'safeLocalStorage from @helpers/safe-local-storage'. */
|
|
60
60
|
safeStorageModule?: string;
|
|
61
|
-
/** `
|
|
61
|
+
/** `core/R1`: repo-relative path of the wrapper FILE itself — the one place bare storage access belongs, auto-exempted so no app has to allowlist its own wrapper. E.g. 'app/utils/helpers/safe-local-storage.ts'. */
|
|
62
62
|
safeStorageFile?: string;
|
|
63
|
-
/** `
|
|
63
|
+
/** `core/R3` fix message: the guarded parse helper, e.g. 'parseStoredJson() from @helpers/safe-local-storage'. */
|
|
64
64
|
safeJsonParse?: string;
|
|
65
|
-
/** `
|
|
65
|
+
/** `core/C3`: the palette file that legitimately DEFINES colour literals, e.g. 'app/assets/scss/_colors.scss'. */
|
|
66
66
|
scssTokensFile?: string;
|
|
67
|
-
/** `
|
|
67
|
+
/** `core/C2`: root of the UI-kit tree whose component names must not be duplicated. Default 'app/components/UI'. */
|
|
68
68
|
uiKitRoot?: string;
|
|
69
|
-
/** `
|
|
69
|
+
/** `core/C4` fix message: the shared date-format enum, e.g. 'DateFormat from @ts-types/date/enums'. */
|
|
70
70
|
dateFormatModule?: string;
|
|
71
|
-
/** `
|
|
71
|
+
/** `core/C5`: where entity type folders live. Default 'app/types'. */
|
|
72
72
|
typesRoot?: string;
|
|
73
|
-
/** `
|
|
73
|
+
/** `core/C6` fix message: this app's typography source — a mixin in some apps ('the font() mixin from @scss/_mixins.scss'), a component in others ('the AppText component'). */
|
|
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
77
|
}
|
|
78
78
|
/** What a rule's `check` receives. */
|
package/dist/invariants/util.js
CHANGED
|
@@ -95,8 +95,8 @@ export const createReader = (root) => {
|
|
|
95
95
|
};
|
|
96
96
|
};
|
|
97
97
|
export const toRelative = (root, p) => relative(root, resolve(root, p)).split(sep).join('/');
|
|
98
|
-
// `
|
|
99
|
-
const IGNORE_RE = /invariant-ignore:\s*(
|
|
98
|
+
// `(?:core\/)?` — shared-pack rules carry the core/ prefix (`core/R1`); app-local rules are bare (`R1`).
|
|
99
|
+
const IGNORE_RE = /invariant-ignore:\s*((?:core\/)?[A-Z]{1,3}\d{1,3}(?:\s*,\s*(?:core\/)?[A-Z]{1,3}\d{1,3})*)\s*(?:—|–|--|-|:)?\s*(.*)$/;
|
|
100
100
|
/**
|
|
101
101
|
* Parse every `invariant-ignore:` comment in a file.
|
|
102
102
|
* Returns { suppressions: Map<lineNumber, Set<ruleId>>, unreasoned: [{ line, ids }] }
|
|
@@ -198,7 +198,9 @@ export const findDanglingRuleRefs = (input) => {
|
|
|
198
198
|
const start = src.indexOf(block);
|
|
199
199
|
if (start === -1)
|
|
200
200
|
return 1;
|
|
201
|
-
|
|
201
|
+
// Ids may contain `/` (core/R1) and appear as quoted keys — escape and allow quotes.
|
|
202
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
203
|
+
const rel = src.slice(start).search(new RegExp(`^\\s*(?:'|")?${escaped}(?:'|")?\\s*:`, 'm'));
|
|
202
204
|
return rel === -1 ? 1 : lineOf(src, start + rel);
|
|
203
205
|
};
|
|
204
206
|
const explain = (id) => retired[id] ? `${id} was retired — ${retired[id]}` : `${id} does not name any rule`;
|
package/package.json
CHANGED