@brighterly/lib-fe-codestyle 1.0.0 → 1.0.2
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 +234 -178
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +181 -0
- package/dist/config/define-config.d.ts +11 -0
- package/dist/config/define-config.js +33 -0
- package/dist/config/load-config.d.ts +15 -0
- package/dist/config/load-config.js +52 -0
- package/dist/config/schema.d.ts +58 -0
- package/dist/config/schema.js +139 -0
- package/dist/{invariants → engine}/ast.d.ts +8 -0
- package/dist/{invariants → engine}/ast.js +51 -0
- package/dist/engine/docs.d.ts +14 -0
- package/dist/engine/docs.js +72 -0
- package/dist/engine/meta.d.ts +32 -0
- package/dist/engine/meta.js +47 -0
- package/dist/engine/migrate.d.ts +22 -0
- package/dist/engine/migrate.js +175 -0
- package/dist/engine/overrides.d.ts +36 -0
- package/dist/engine/overrides.js +92 -0
- package/dist/engine/registry.d.ts +35 -0
- package/dist/engine/registry.js +100 -0
- package/dist/engine/report.d.ts +23 -0
- package/dist/engine/report.js +87 -0
- package/dist/engine/run.d.ts +57 -0
- package/dist/engine/run.js +353 -0
- package/dist/engine/self-test.d.ts +33 -0
- package/dist/engine/self-test.js +141 -0
- package/dist/engine/types.d.ts +232 -0
- package/dist/{invariants → engine}/types.js +2 -1
- package/dist/{invariants → engine}/util.d.ts +31 -39
- package/dist/{invariants → engine}/util.js +59 -86
- package/dist/index.d.ts +16 -0
- package/dist/index.js +14 -0
- package/dist/rules/convention.d.ts +20 -0
- package/dist/{invariants/rules → rules}/convention.js +103 -16
- package/dist/rules/index.d.ts +28 -0
- package/dist/{invariants/rules → rules}/index.js +40 -32
- package/dist/{invariants/rules → rules}/regression.d.ts +1 -1
- package/dist/{invariants/rules → rules}/regression.js +26 -6
- package/dist/{invariants/rules → rules}/shared.d.ts +1 -1
- package/dist/validate/index.d.ts +1 -1
- package/dist/validate/index.js +1 -1
- package/package.json +8 -13
- package/dist/invariants/index.d.ts +0 -7
- package/dist/invariants/index.js +0 -5
- package/dist/invariants/rules/convention.d.ts +0 -18
- package/dist/invariants/rules/fixtures.d.ts +0 -17
- package/dist/invariants/rules/fixtures.js +0 -89
- package/dist/invariants/rules/index.d.ts +0 -55
- package/dist/invariants/rules/linter.d.ts +0 -10
- package/dist/invariants/rules/linter.js +0 -9
- package/dist/invariants/run.d.ts +0 -53
- package/dist/invariants/run.js +0 -746
- package/dist/invariants/self-test.d.ts +0 -46
- package/dist/invariants/self-test.js +0 -84
- package/dist/invariants/types.d.ts +0 -222
- /package/dist/{invariants/rules → rules}/shared.js +0 -0
|
@@ -2,28 +2,46 @@
|
|
|
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
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
* an
|
|
8
|
-
*
|
|
5
|
+
* `vueuse-first` was land-app's `C1`, and reads the same everywhere. `ui-kit-unique-names`,
|
|
6
|
+
* `scss-tokens` and `date-format-enum` are new, and came out of a real code review rather
|
|
7
|
+
* than an outage: a developer shipped a component that duplicated an existing UI-kit one,
|
|
8
|
+
* with colours that were not design tokens. Review caught it; nothing automated did,
|
|
9
|
+
* because the conventions existed only in reviewers\' heads.
|
|
9
10
|
*
|
|
10
|
-
* `
|
|
11
|
-
* mining 400 PR review comments across the
|
|
12
|
-
* typography through the app's declared source,
|
|
11
|
+
* `types-layout`, `typography-source` and `script-attr-order` (2026-09-23) come from the
|
|
12
|
+
* org's Notion frontend-conventions doc and from mining 400 PR review comments across the
|
|
13
|
+
* four apps: entity-folder type organization, typography through the app's declared source,
|
|
14
|
+
* and one canonical <script> attribute order.
|
|
13
15
|
*
|
|
14
16
|
* They ship at `warn` so each consuming app can measure what they find before anyone
|
|
15
17
|
* decides to block on them. Promoting one to `error` is a major version bump.
|
|
16
18
|
*/
|
|
19
|
+
import { componentScopeLines } from '../engine/ast.js';
|
|
17
20
|
import { eachLine, setting } from './shared.js';
|
|
18
21
|
const vueUseFirst = {
|
|
19
|
-
|
|
22
|
+
name: 'vueuse-first',
|
|
20
23
|
title: 'hand-rolled browser API with a VueUse equivalent',
|
|
21
24
|
level: 'warn',
|
|
22
25
|
why: 'VueUse handles the lifecycle, SSR and error paths we otherwise re-implement (and forget to tear down) per call site',
|
|
23
26
|
fix: 'use the VueUse composable named in the message, imported explicitly from @vueuse/core',
|
|
24
27
|
scope: ['app/**/*.{ts,vue}'],
|
|
25
28
|
exclude: ['app/tests/**', 'app/test/**'],
|
|
26
|
-
|
|
29
|
+
async: true,
|
|
30
|
+
// Only lines inside a component lifetime count (`<script setup>`, a `use…` composable, a `setup()`
|
|
31
|
+
// method). A store action, a plugin or a plain helper has no effect scope, so the VueUse composable
|
|
32
|
+
// the message recommends would leak there — those lines are silent by construction.
|
|
33
|
+
fixtures: [
|
|
34
|
+
{ file: 'app/composables/a.ts', bad: 'export const useA = () => {\n window.addEventListener("resize", onResize)\n}', good: 'export const useA = () => {\n useEventListener(window, "resize", onResize)\n}' },
|
|
35
|
+
{ file: 'app/composables/b.ts', bad: 'export function useB() {\n const run = () => setTimeout(tick, 10)\n return { run }\n}' },
|
|
36
|
+
{ file: 'app/components/A.vue', bad: '<script setup lang="ts">\nsetTimeout(tick, 10)\n</script>', good: '<script setup lang="ts">\nuseTimeoutFn(tick, 10)\n</script>' },
|
|
37
|
+
{ file: 'app/components/B.vue', bad: '<script lang="ts">\nexport default defineComponent({\n setup() {\n setInterval(tick, 10)\n },\n})\n</script>' },
|
|
38
|
+
{ file: 'app/stores/quiz/actions.ts', good: 'export const actions = {\n setLazyStep(payload) {\n setTimeout(() => setStep(payload), 300)\n },\n}' },
|
|
39
|
+
{ file: 'app/composables/tools/scroll.ts', good: 'export const scrollIntoViewIfHidden = (el) => {\n new IntersectionObserver(cb).observe(el)\n}' },
|
|
40
|
+
{ file: 'app/composables/tools/sdk.ts', good: 'const init = () => {\n load().catch(() => setTimeout(init, 800))\n}' },
|
|
41
|
+
{ file: 'app/plugins/axios.client.ts', good: 'export default defineNuxtPlugin(() => {\n setTimeout(refresh, 1000)\n})' },
|
|
42
|
+
],
|
|
43
|
+
check: async ({ file, source, code }) => {
|
|
44
|
+
const scoped = await componentScopeLines(file, source);
|
|
27
45
|
const map = [
|
|
28
46
|
[/new\s+IntersectionObserver\s*\(/, 'useIntersectionObserver'],
|
|
29
47
|
[/new\s+ResizeObserver\s*\(/, 'useResizeObserver'],
|
|
@@ -38,6 +56,8 @@ const vueUseFirst = {
|
|
|
38
56
|
[/\b(?:window\s*\.\s*)?setTimeout\s*\(/, 'useTimeoutFn'],
|
|
39
57
|
];
|
|
40
58
|
return eachLine(code, (line, n) => {
|
|
59
|
+
if (!scoped.has(n))
|
|
60
|
+
return null;
|
|
41
61
|
for (const [re, fn] of map) {
|
|
42
62
|
if (re.test(line))
|
|
43
63
|
return { file, line: n, message: `hand-rolled browser API — use ${fn} from @vueuse/core` };
|
|
@@ -47,13 +67,25 @@ const vueUseFirst = {
|
|
|
47
67
|
},
|
|
48
68
|
};
|
|
49
69
|
const uiKitReuseFirst = {
|
|
50
|
-
|
|
70
|
+
name: 'ui-kit-unique-names',
|
|
51
71
|
title: 'component duplicates an existing UI-kit component name',
|
|
52
72
|
level: 'warn',
|
|
53
73
|
once: true,
|
|
54
74
|
why: 'a second component with the same name as a UI-kit one splits the design system in two — the new one drifts on colours, spacing and a11y, and nothing fails. Reviewers catch it by memory or not at all',
|
|
55
75
|
fix: 'import the UI-kit component, or extend it. A new one is justified only when the existing cannot express the case without a breaking prop change — say so in the PR body',
|
|
56
76
|
scope: ['app/components/**/*.vue'],
|
|
77
|
+
// A `once` rule over the whole file list: a single-file case cannot express it.
|
|
78
|
+
world: [
|
|
79
|
+
{
|
|
80
|
+
files: { 'app/components/UI/Buttons/AppButton.vue': '<template />', 'app/components/Pages/Quiz/AppButton.vue': '<template />' },
|
|
81
|
+
expect: 'finding',
|
|
82
|
+
match: /duplicates the UI-kit component/,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
files: { 'app/components/UI/Buttons/AppButton.vue': '<template />', 'app/components/Pages/Quiz/QuizButton.vue': '<template />' },
|
|
86
|
+
expect: 'silence',
|
|
87
|
+
},
|
|
88
|
+
],
|
|
57
89
|
check: ({ allFiles, settings }) => {
|
|
58
90
|
const files = allFiles ?? [];
|
|
59
91
|
const uiKitRoot = settings?.uiKitRoot ?? 'app/components/UI';
|
|
@@ -101,12 +133,26 @@ const onlyStyleBlocks = (source) => {
|
|
|
101
133
|
return out + source.slice(last).replace(/[^\n]/g, ' ');
|
|
102
134
|
};
|
|
103
135
|
const scssColourTokens = {
|
|
104
|
-
|
|
136
|
+
name: 'scss-tokens',
|
|
105
137
|
title: 'literal colour in SCSS instead of a design token',
|
|
106
138
|
level: 'warn',
|
|
107
139
|
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
140
|
fix: 'use a token variable from the palette file; if the colour is genuinely new, add it to the palette first',
|
|
109
141
|
scope: ['app/assets/scss/**/*.scss', 'app/components/**/*.vue'],
|
|
142
|
+
fixtures: [
|
|
143
|
+
{
|
|
144
|
+
file: 'app/assets/scss/components/Card.scss',
|
|
145
|
+
bad: '.card { color: #1f2937; }',
|
|
146
|
+
good: '.card { color: $slate-800; }',
|
|
147
|
+
},
|
|
148
|
+
// In a .vue file only the <style> block is a stylesheet: an SVG fill in the
|
|
149
|
+
// template has no place to put a $token, so it must stay quiet.
|
|
150
|
+
{
|
|
151
|
+
file: 'app/components/UI/Icons/IconLeaf.vue',
|
|
152
|
+
bad: '<template><svg><path fill="currentColor" /></svg></template>\n<style scoped lang="scss">\n.icon { color: #e2e8f0; }\n</style>',
|
|
153
|
+
good: '<template><svg><path fill="#E2E8F0" /></svg></template>\n<style scoped lang="scss">\n.icon { color: $slate-200; }\n</style>',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
110
156
|
check: ({ file, code, settings }) => {
|
|
111
157
|
// Underscore-prefixed partials are DECLARATION files by SCSS convention in every
|
|
112
158
|
// Brighterly app (_colors, _vars, _extends, _mixins, brand variants, …) — they are
|
|
@@ -129,13 +175,33 @@ const scssColourTokens = {
|
|
|
129
175
|
},
|
|
130
176
|
};
|
|
131
177
|
const dateFormatEnum = {
|
|
132
|
-
|
|
178
|
+
name: 'date-format-enum',
|
|
133
179
|
title: 'hand-rolled date format string instead of the shared enum',
|
|
134
180
|
level: 'warn',
|
|
135
181
|
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',
|
|
136
182
|
fix: 'format through the shared DateFormat enum; never hand-roll a format string or use toISOString()',
|
|
137
183
|
scope: ['app/**/*.{ts,vue}'],
|
|
138
184
|
exclude: ['app/tests/**', 'app/test/**'],
|
|
185
|
+
fixtures: [
|
|
186
|
+
{
|
|
187
|
+
file: 'app/a.ts',
|
|
188
|
+
bad: "const d = dayjs().format('YYYY-MM-DD')",
|
|
189
|
+
good: 'const d = dayjs().format(DateFormat.DefaultDate)',
|
|
190
|
+
},
|
|
191
|
+
// The day-extraction misuse fires; a bare toISOString() (an API timestamp) and the
|
|
192
|
+
// epoch duration trick do not.
|
|
193
|
+
{
|
|
194
|
+
file: 'app/b.ts',
|
|
195
|
+
bad: "const day = new Date().toISOString().split('T')[0]",
|
|
196
|
+
good: 'const generatedAt = dayjs().toISOString()\nconst duration = new Date(s * 1000).toISOString().slice(14, 19)',
|
|
197
|
+
},
|
|
198
|
+
// Every letter run must be a real dayjs token — an answer label that happens to fit
|
|
199
|
+
// the char class ('ADHD / ADD' carries a DD) is not a format string.
|
|
200
|
+
{
|
|
201
|
+
file: 'app/c.ts',
|
|
202
|
+
good: "const option = { code: 'ADHD_ADD', label: 'ADHD / ADD' }",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
139
205
|
check: ({ file, code, settings }) => {
|
|
140
206
|
const dateModule = setting({ settings }, 'dateFormatModule', 'the shared DateFormat enum');
|
|
141
207
|
// The module that DEFINES the formats is the one place the literals belong.
|
|
@@ -162,13 +228,21 @@ const dateFormatEnum = {
|
|
|
162
228
|
},
|
|
163
229
|
};
|
|
164
230
|
const typesOrganization = {
|
|
165
|
-
|
|
231
|
+
name: 'types-layout',
|
|
166
232
|
title: 'type file outside the entity-folder structure',
|
|
167
233
|
level: 'warn',
|
|
168
234
|
once: true,
|
|
169
235
|
why: 'types grouped by business entity are findable and own their imports; a root-level types.ts or a mixed-kind file accretes unrelated entities until nobody can say what depends on it (Notion: Types Organization, §1/§5)',
|
|
170
236
|
fix: 'move every type into <typesRoot>/<entity>/{enums,interfaces,types}.ts — enums in enums.ts, interfaces in interfaces.ts, aliases/unions in types.ts',
|
|
171
237
|
scope: ['app/types/**/*.ts'],
|
|
238
|
+
// A `once` rule over the whole file list: a single-file case cannot express it.
|
|
239
|
+
world: [
|
|
240
|
+
{ files: { 'app/types/global-types.ts': 'export type A = 1' }, expect: 'finding', match: /directly under app\/types/ },
|
|
241
|
+
{
|
|
242
|
+
files: { 'app/types/quiz/enums.ts': '', 'app/types/quiz/interfaces.ts': '', 'app/types/quiz/types.ts': '', 'app/types/globals.d.ts': '' },
|
|
243
|
+
expect: 'silence',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
172
246
|
check: ({ allFiles, settings }) => {
|
|
173
247
|
const files = allFiles ?? [];
|
|
174
248
|
const root = settings?.typesRoot ?? 'app/types';
|
|
@@ -194,12 +268,17 @@ const typesOrganization = {
|
|
|
194
268
|
},
|
|
195
269
|
};
|
|
196
270
|
const typographySource = {
|
|
197
|
-
|
|
271
|
+
name: 'typography-source',
|
|
198
272
|
title: 'raw font declaration instead of the app\'s typography source',
|
|
199
273
|
level: 'warn',
|
|
200
274
|
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 та стилі)',
|
|
201
275
|
fix: 'go through the app\'s typography source (see settings.typographySource — a mixin in some apps, a Text/Heading component in others); if the style is genuinely new, add it there first',
|
|
202
276
|
scope: ['app/assets/scss/**/*.scss', 'app/components/**/*.vue'],
|
|
277
|
+
fixtures: {
|
|
278
|
+
file: 'app/assets/scss/components/Card.scss',
|
|
279
|
+
bad: '.card { font-size: 14px; }',
|
|
280
|
+
good: '.card { @include font(14px, 20px); color: $slate-800; }',
|
|
281
|
+
},
|
|
203
282
|
check: ({ file, code, settings }) => {
|
|
204
283
|
const source = setting({ settings }, 'typographySource', 'the app\'s shared typography mixins or text components');
|
|
205
284
|
// The files that DEFINE the typography are the one place raw declarations belong:
|
|
@@ -207,7 +286,7 @@ const typographySource = {
|
|
|
207
286
|
// Brighterly apps, decided 2026-09-24), plus non-underscore fonts/typography files.
|
|
208
287
|
if (/\/(?:_[^/]*|fonts|typography)\.scss$/.test(file))
|
|
209
288
|
return [];
|
|
210
|
-
// @font-face declares a face, not a usage —
|
|
289
|
+
// @font-face declares a face, not a usage — font-face-single-family polices those blocks.
|
|
211
290
|
const stripped = code.replace(/@font-face\s*\{[^}]*\}/g, m => m.replace(/[^\n]/g, ' '));
|
|
212
291
|
return eachLine(stripped, (line, n) => {
|
|
213
292
|
const m = /\bfont(?:-family|-size|-weight)?\s*:/.exec(line);
|
|
@@ -218,12 +297,20 @@ const typographySource = {
|
|
|
218
297
|
},
|
|
219
298
|
};
|
|
220
299
|
const scriptAttrOrder = {
|
|
221
|
-
|
|
300
|
+
name: 'script-attr-order',
|
|
222
301
|
title: 'script tag attribute order differs from the app canonical',
|
|
223
302
|
level: 'warn',
|
|
224
303
|
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',
|
|
225
304
|
fix: 'use the app\'s canonical order everywhere — settings.scriptAttrOrder: \'setup-first\' (default) or \'lang-first\'',
|
|
226
305
|
scope: ['app/**/*.vue'],
|
|
306
|
+
fixtures: {
|
|
307
|
+
// Two script tags in OPPOSITE orders, so the bad fixture violates whichever
|
|
308
|
+
// direction the consuming app declares in settings.scriptAttrOrder. The good
|
|
309
|
+
// fixture carries no lang attribute, so it is clean under either canonical.
|
|
310
|
+
file: 'app/components/A.vue',
|
|
311
|
+
bad: '<script lang="ts" setup>\n</script>\n<script setup lang="ts">\n</script>',
|
|
312
|
+
good: '<template><div /></template>\n<script setup>\nconst a = 1\n</script>',
|
|
313
|
+
},
|
|
227
314
|
check: ({ file, source, settings }) => {
|
|
228
315
|
const canonical = settings?.scriptAttrOrder === 'lang-first' ? 'lang-first' : 'setup-first';
|
|
229
316
|
const spell = (order) => (order === 'lang-first' ? '<script lang="ts" setup>' : '<script setup lang="ts">');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package rule pack — eleven rules by plain name, each carrying its own proof.
|
|
3
|
+
*
|
|
4
|
+
* Every rule reads identically in every Brighterly Nuxt app: none names a funnel, a store or
|
|
5
|
+
* a route. App-specific landmarks arrive through `RuleContext.settings`. Two files by origin:
|
|
6
|
+
* regression.ts — it broke production at least once → a code decision
|
|
7
|
+
* convention.ts — a preference we chose → team consensus
|
|
8
|
+
*
|
|
9
|
+
* App rules live in the app's own config and must not reuse a package name: `run()` fails
|
|
10
|
+
* fast on a duplicate rather than letting a suppression silently cover the wrong rule.
|
|
11
|
+
*/
|
|
12
|
+
import type { Rule } from '../engine/types.js';
|
|
13
|
+
export { eachLine, setting } from './shared.js';
|
|
14
|
+
export type { LineFn } from './shared.js';
|
|
15
|
+
/**
|
|
16
|
+
* Every package rule name, in report order. Hover a member for what the rule checks, at
|
|
17
|
+
* which level, and what to do instead. A test pins this list to `packageRules`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const packageRuleNames: readonly ["no-bare-storage", "inline-scss", "guarded-json-parse", "font-face-single-family", "vueuse-first", "ui-kit-unique-names", "scss-tokens", "date-format-enum", "types-layout", "typography-source", "script-attr-order"];
|
|
20
|
+
/** Union of every name the package ships — for typing app config keys. */
|
|
21
|
+
export type PackageRuleName = typeof packageRuleNames[number];
|
|
22
|
+
/** Every package rule, in report order (the order of `packageRuleNames`). */
|
|
23
|
+
export declare const packageRules: Rule[];
|
|
24
|
+
/**
|
|
25
|
+
* Every 0.5.x id → its 1.0 name. Never delete an entry: a suppression or config key still
|
|
26
|
+
* naming an old id is reported with the new name instead of "does not name any rule".
|
|
27
|
+
*/
|
|
28
|
+
export declare const renamedRules: Record<string, PackageRuleName>;
|
|
@@ -1,53 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* for its own rules.
|
|
2
|
+
* The package rule pack — eleven rules by plain name, each carrying its own proof.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* C — convention: a preference we chose → team consensus
|
|
10
|
-
* L — linter: a built-in ESLint rule → eslint.config.mjs
|
|
4
|
+
* Every rule reads identically in every Brighterly Nuxt app: none names a funnel, a store or
|
|
5
|
+
* a route. App-specific landmarks arrive through `RuleContext.settings`. Two files by origin:
|
|
6
|
+
* regression.ts — it broke production at least once → a code decision
|
|
7
|
+
* convention.ts — a preference we chose → team consensus
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* still fails fast on a duplicate rather than letting an allowlist silently suppress
|
|
15
|
-
* the wrong rule.
|
|
9
|
+
* App rules live in the app's own config and must not reuse a package name: `run()` fails
|
|
10
|
+
* fast on a duplicate rather than letting a suppression silently cover the wrong rule.
|
|
16
11
|
*/
|
|
17
12
|
import { rRules } from './regression.js';
|
|
18
13
|
import { cRules } from './convention.js';
|
|
19
|
-
export { linterOwnedRules } from './linter.js';
|
|
20
|
-
export { commonFixtures, commonSelfTestSkip } from './fixtures.js';
|
|
21
14
|
export { eachLine, setting } from './shared.js';
|
|
22
|
-
/** Every shared rule, in report order. Hover `sharedRuleIds` members for what each checks. */
|
|
23
|
-
export const commonRules = [...rRules, ...cRules];
|
|
24
15
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* IDE — hover any member for what the rule checks, at which level, and what to do
|
|
28
|
-
* instead. A test pins this map to `commonRules`, so it cannot drift.
|
|
16
|
+
* Every package rule name, in report order. Hover a member for what the rule checks, at
|
|
17
|
+
* which level, and what to do instead. A test pins this list to `packageRules`.
|
|
29
18
|
*/
|
|
30
|
-
export const
|
|
19
|
+
export const packageRuleNames = [
|
|
31
20
|
/** `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
|
-
'
|
|
21
|
+
'no-bare-storage',
|
|
33
22
|
/** `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
|
-
'
|
|
23
|
+
'inline-scss',
|
|
35
24
|
/** `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
|
-
'
|
|
25
|
+
'guarded-json-parse',
|
|
37
26
|
/** `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
|
-
'
|
|
27
|
+
'font-face-single-family',
|
|
39
28
|
/** `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
|
-
'
|
|
29
|
+
'vueuse-first',
|
|
41
30
|
/** `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
|
-
'
|
|
31
|
+
'ui-kit-unique-names',
|
|
43
32
|
/** `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
|
-
'
|
|
33
|
+
'scss-tokens',
|
|
45
34
|
/** `warn` — a hand-rolled date format string (`'YYYY-MM-DD'`) or a `toISOString()` whose date part is extracted (`.split('T')[0]`, `.slice(0, 10)`). The date's shape becomes an accident instead of a contract. Fix: the shared enum (`settings.dateFormatModule`). */
|
|
46
|
-
'
|
|
35
|
+
'date-format-enum',
|
|
47
36
|
/** `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
|
-
'
|
|
37
|
+
'types-layout',
|
|
49
38
|
/** `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
|
-
'
|
|
39
|
+
'typography-source',
|
|
51
40
|
/** `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
|
-
'
|
|
41
|
+
'script-attr-order',
|
|
42
|
+
];
|
|
43
|
+
/** Every package rule, in report order (the order of `packageRuleNames`). */
|
|
44
|
+
export const packageRules = [...rRules, ...cRules];
|
|
45
|
+
/**
|
|
46
|
+
* Every 0.5.x id → its 1.0 name. Never delete an entry: a suppression or config key still
|
|
47
|
+
* naming an old id is reported with the new name instead of "does not name any rule".
|
|
48
|
+
*/
|
|
49
|
+
export const renamedRules = {
|
|
50
|
+
'core/R1': 'no-bare-storage',
|
|
51
|
+
'core/R2': 'inline-scss',
|
|
52
|
+
'core/R3': 'guarded-json-parse',
|
|
53
|
+
'core/R4': 'font-face-single-family',
|
|
54
|
+
'core/C1': 'vueuse-first',
|
|
55
|
+
'core/C2': 'ui-kit-unique-names',
|
|
56
|
+
'core/C3': 'scss-tokens',
|
|
57
|
+
'core/C4': 'date-format-enum',
|
|
58
|
+
'core/C5': 'types-layout',
|
|
59
|
+
'core/C6': 'typography-source',
|
|
60
|
+
'core/C7': 'script-attr-order',
|
|
53
61
|
};
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
* Changing one is a code decision — but a code decision for FOUR repos now, so making a
|
|
10
10
|
* rule stricter is a major version bump.
|
|
11
11
|
*/
|
|
12
|
-
import type { Rule } from '../types.js';
|
|
12
|
+
import type { Rule } from '../engine/types.js';
|
|
13
13
|
export declare const rRules: Rule[];
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* Changing one is a code decision — but a code decision for FOUR repos now, so making a
|
|
10
10
|
* rule stricter is a major version bump.
|
|
11
11
|
*/
|
|
12
|
-
import { findJsonParseCalls } from '../ast.js';
|
|
12
|
+
import { findJsonParseCalls } from '../engine/ast.js';
|
|
13
13
|
import { eachLine, setting } from './shared.js';
|
|
14
14
|
const bareStorage = {
|
|
15
|
-
|
|
15
|
+
name: 'no-bare-storage',
|
|
16
16
|
title: 'bare localStorage / sessionStorage access',
|
|
17
17
|
// Target level is `error` (crash-class). Shipped at `warn` since 2026-09-25: the fleet
|
|
18
18
|
// backlog (cs-app ~42 sites, main-app ~28) is real code the team has no bandwidth to
|
|
@@ -23,6 +23,11 @@ const bareStorage = {
|
|
|
23
23
|
fix: 'use the app\'s safe storage wrapper instead of the bare API',
|
|
24
24
|
scope: ['app/**/*.{ts,vue}'],
|
|
25
25
|
exclude: ['app/tests/**', 'app/test/**'],
|
|
26
|
+
fixtures: {
|
|
27
|
+
file: 'app/a.ts',
|
|
28
|
+
bad: 'const t = localStorage.getItem("k")',
|
|
29
|
+
good: 'const t = safeLocalStorage.getItem("k")',
|
|
30
|
+
},
|
|
26
31
|
check: ({ file, code, settings }) => {
|
|
27
32
|
// The wrapper itself is the one place bare access belongs — settings-driven, so no
|
|
28
33
|
// consumer has to allowlist its own wrapper.
|
|
@@ -41,7 +46,7 @@ const bareStorage = {
|
|
|
41
46
|
},
|
|
42
47
|
};
|
|
43
48
|
const scssInStyleBlock = {
|
|
44
|
-
|
|
49
|
+
name: 'inline-scss',
|
|
45
50
|
title: 'SCSS rules inside a Vue <style> block',
|
|
46
51
|
level: 'error',
|
|
47
52
|
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',
|
|
@@ -50,6 +55,11 @@ const scssInStyleBlock = {
|
|
|
50
55
|
// Dev harness pages are dev tooling, not product surfaces — every app that has them
|
|
51
56
|
// (tutor room-preview, cs-app preview) was allowlisting the same thing (2026-09-25).
|
|
52
57
|
exclude: ['app/pages/dev/**'],
|
|
58
|
+
fixtures: {
|
|
59
|
+
file: 'app/components/A.vue',
|
|
60
|
+
bad: '<template><div /></template>\n<style scoped lang="scss">.a { color: red; }</style>',
|
|
61
|
+
good: '<template><div /></template>\n<style scoped lang="scss" src="@scss/components/A.scss" />',
|
|
62
|
+
},
|
|
53
63
|
check: ({ file, source }) => {
|
|
54
64
|
const out = [];
|
|
55
65
|
const re = /<style\b[^>]*>([\s\S]*?)<\/style>/g;
|
|
@@ -73,15 +83,20 @@ const scssInStyleBlock = {
|
|
|
73
83
|
},
|
|
74
84
|
};
|
|
75
85
|
const unguardedJsonParse = {
|
|
76
|
-
|
|
86
|
+
name: 'guarded-json-parse',
|
|
77
87
|
title: 'unguarded JSON.parse',
|
|
78
|
-
// Target level is `error` — see
|
|
88
|
+
// Target level is `error` — see no-bare-storage above; same interim decision (2026-09-25).
|
|
79
89
|
level: 'warn',
|
|
80
90
|
async: true,
|
|
81
91
|
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',
|
|
82
92
|
fix: 'parse through the app\'s guarded helper, or wrap the call in try/catch',
|
|
83
93
|
scope: ['app/**/*.{ts,vue}'],
|
|
84
94
|
exclude: ['app/tests/**', 'app/test/**'],
|
|
95
|
+
fixtures: {
|
|
96
|
+
file: 'app/a.ts',
|
|
97
|
+
bad: 'const v = JSON.parse(raw)',
|
|
98
|
+
good: 'let v = null\ntry { v = JSON.parse(raw) } catch { v = null }',
|
|
99
|
+
},
|
|
85
100
|
check: async ({ file, source, settings }) => {
|
|
86
101
|
const helper = setting({ settings }, 'safeJsonParse', 'a guarded parse helper');
|
|
87
102
|
const calls = await findJsonParseCalls(file, source);
|
|
@@ -91,12 +106,17 @@ const unguardedJsonParse = {
|
|
|
91
106
|
},
|
|
92
107
|
};
|
|
93
108
|
const fontFaceStack = {
|
|
94
|
-
|
|
109
|
+
name: 'font-face-single-family',
|
|
95
110
|
title: '@font-face with a font stack instead of one family name',
|
|
96
111
|
level: 'error',
|
|
97
112
|
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',
|
|
98
113
|
fix: 'keep font-family inside @font-face to the single family name; the fallback stack belongs on the selectors that use the font',
|
|
99
114
|
scope: ['app/assets/scss/**/*.scss'],
|
|
115
|
+
fixtures: {
|
|
116
|
+
file: 'app/assets/scss/fonts.scss',
|
|
117
|
+
bad: '@font-face { font-family: Rubik, sans-serif; src: url(a.woff2); }',
|
|
118
|
+
good: '@font-face { font-family: Rubik; src: url(a.woff2); }',
|
|
119
|
+
},
|
|
100
120
|
check: ({ file, code }) => {
|
|
101
121
|
const out = [];
|
|
102
122
|
for (const m of code.matchAll(/@font-face\s*\{([^}]*)\}/g)) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* through `RuleContext.settings`, which each app fills from `config.settings` — that is why
|
|
6
6
|
* `settings` exists at all, and why the pack needs no factory.
|
|
7
7
|
*/
|
|
8
|
-
import type { Finding, RuleContext } from '../types.js';
|
|
8
|
+
import type { Finding, RuleContext } from '../engine/types.js';
|
|
9
9
|
export type LineFn = (line: string, n: number, lines: string[]) => Finding | Finding[] | null;
|
|
10
10
|
export declare const eachLine: (source: string, fn: LineFn) => Finding[];
|
|
11
11
|
/**
|
package/dist/validate/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface ValidateOptions {
|
|
|
28
28
|
};
|
|
29
29
|
/** Defaults to process.stdout.write. */
|
|
30
30
|
write?: (s: string) => void;
|
|
31
|
-
/** Defaults to process.exit. Injectable so tests can assert the code. */
|
|
31
|
+
/** Defaults to setting `process.exitCode` (never `process.exit()` after output). Injectable so tests can assert the code. */
|
|
32
32
|
exit?: (code: number) => never;
|
|
33
33
|
/** Emit ::group:: markers. Defaults to Boolean(process.env.GITHUB_ACTIONS). */
|
|
34
34
|
ci?: boolean;
|
package/dist/validate/index.js
CHANGED
|
@@ -16,7 +16,7 @@ const NAME_WIDTH = 12;
|
|
|
16
16
|
export const validate = async (steps, options = {}) => {
|
|
17
17
|
const exec = options.exec ?? defaultExec;
|
|
18
18
|
const write = options.write ?? ((s) => { process.stdout.write(s); });
|
|
19
|
-
const exit = options.exit ?? process.
|
|
19
|
+
const exit = options.exit ?? ((code) => { process.exitCode = code; });
|
|
20
20
|
const ci = options.ci ?? Boolean(process.env.GITHUB_ACTIONS);
|
|
21
21
|
const results = [];
|
|
22
22
|
for (const step of steps) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brighterly/lib-fe-codestyle",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/brighterly/lib-fe-codestyle.git"
|
|
9
|
+
"url": "git+https://github.com/brighterly/lib-fe-codestyle.git"
|
|
10
10
|
},
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
@@ -20,20 +20,15 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"exports": {
|
|
23
|
-
"
|
|
24
|
-
"types": "./dist/
|
|
25
|
-
"default": "./dist/
|
|
26
|
-
},
|
|
27
|
-
"./invariants/rules": {
|
|
28
|
-
"types": "./dist/invariants/rules/index.d.ts",
|
|
29
|
-
"default": "./dist/invariants/rules/index.js"
|
|
30
|
-
},
|
|
31
|
-
"./validate": {
|
|
32
|
-
"types": "./dist/validate/index.d.ts",
|
|
33
|
-
"default": "./dist/validate/index.js"
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
34
26
|
},
|
|
35
27
|
"./eslint": "./eslint/index.mjs"
|
|
36
28
|
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"codestyle": "dist/cli.js"
|
|
31
|
+
},
|
|
37
32
|
"scripts": {
|
|
38
33
|
"build": "tsc -p tsconfig.build.json",
|
|
39
34
|
"clean": "rm -rf dist",
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { run } from './run.js';
|
|
2
|
-
export type { RunOptions } from './run.js';
|
|
3
|
-
export { runSelfTest } from './self-test.js';
|
|
4
|
-
export type { Fixture, SelfTestInput, SelfTestResult } from './self-test.js';
|
|
5
|
-
export * from './types.js';
|
|
6
|
-
export * from './util.js';
|
|
7
|
-
export * from './ast.js';
|
package/dist/invariants/index.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convention rules — preferences we chose, with no production incident behind them.
|
|
3
|
-
* Advisory by default (`warn`): nudged, not policed, and droppable by team consensus.
|
|
4
|
-
*
|
|
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
|
-
* real code review rather than an outage: a developer shipped a component that duplicated
|
|
7
|
-
* an existing UI-kit one, with colours that were not design tokens. Review caught it;
|
|
8
|
-
* nothing automated did, because the conventions existed only in reviewers\' heads.
|
|
9
|
-
*
|
|
10
|
-
* `core/C5`-`core/C7` (2026-09-23) come from the org's Notion frontend-conventions doc and from
|
|
11
|
-
* mining 400 PR review comments across the four apps: entity-folder type organization,
|
|
12
|
-
* typography through the app's declared source, and one canonical <script> attribute order.
|
|
13
|
-
*
|
|
14
|
-
* They ship at `warn` so each consuming app can measure what they find before anyone
|
|
15
|
-
* decides to block on them. Promoting one to `error` is a major version bump.
|
|
16
|
-
*/
|
|
17
|
-
import type { Rule } from '../types.js';
|
|
18
|
-
export declare const cRules: Rule[];
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fixtures for the common rule pack — shipped, not test-only.
|
|
3
|
-
*
|
|
4
|
-
* Fixtures travel with their rules. A consuming app runs `--self-test` over the rules it
|
|
5
|
-
* actually loads, so if the pack shipped rules without fixtures every consumer would report
|
|
6
|
-
* "no self-test fixture" for rules it did not write and cannot fix.
|
|
7
|
-
*
|
|
8
|
-
* `bad` must produce at least one finding, `good` none.
|
|
9
|
-
*/
|
|
10
|
-
import type { Fixture } from '../self-test.js';
|
|
11
|
-
export declare const commonFixtures: Record<string, Fixture>;
|
|
12
|
-
/**
|
|
13
|
-
* `core/C2` and `core/C5` are `once` rules over the whole file list — a single-file fixture
|
|
14
|
-
* cannot express them, so they are exempt from the fixture requirement and covered by
|
|
15
|
-
* their own tests instead.
|
|
16
|
-
*/
|
|
17
|
-
export declare const commonSelfTestSkip: string[];
|