@brighterly/lib-fe-codestyle 0.5.1 → 1.0.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/dist/invariants/ast.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export declare const findSharedMutables: (file: string, source: string) => Promi
|
|
|
71
71
|
}[]>;
|
|
72
72
|
/** One attribute or binding, normalised so a rule need not know the node types. */
|
|
73
73
|
export interface TemplateAttr {
|
|
74
|
-
/** Attribute name without the `:` / `v-bind:` prefix — `data-qa`, `class`, `title
|
|
74
|
+
/** Attribute name without the `:` / `v-bind:` prefix — `data-qa`, `class`, `title`; `v-if` for the condition. */
|
|
75
75
|
name: string;
|
|
76
76
|
/** Static value, or the raw expression source for a binding. Null for a bare attribute. */
|
|
77
77
|
value: string | null;
|
|
@@ -95,11 +95,6 @@ export interface TemplateEl {
|
|
|
95
95
|
/** True when this element carries a `v-for`. */
|
|
96
96
|
vFor: boolean;
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Root elements of a .vue file's <template>. Empty array when the file has no
|
|
100
|
-
* template or the template does not parse — a rule must treat that as "nothing
|
|
101
|
-
* to say", never as "compliant".
|
|
102
|
-
*/
|
|
103
98
|
export declare const templateElements: (file: string, source: string) => Promise<TemplateEl[]>;
|
|
104
99
|
/** Every element with the given tag, at any depth, in document order. */
|
|
105
100
|
export declare const elementsByTag: (roots: TemplateEl[], tag: string) => TemplateEl[];
|
package/dist/invariants/ast.js
CHANGED
|
@@ -248,6 +248,12 @@ const normalise = (node) => {
|
|
|
248
248
|
vFor = true;
|
|
249
249
|
continue;
|
|
250
250
|
}
|
|
251
|
+
// The condition is exposed as a bound attribute so a rule can read `v-if` through
|
|
252
|
+
// the AST instead of a tag regex, which an attribute holding `=>` or `>` defeats.
|
|
253
|
+
if (p.name === 'if') {
|
|
254
|
+
attrs.push({ name: 'v-if', value: p.exp?.content ?? p.exp?.loc?.source ?? null, bound: true, line });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
251
257
|
if (p.name === 'slot') {
|
|
252
258
|
// `#title` → arg.content 'title'; `#[slotName]` → dynamic, keep the source
|
|
253
259
|
slot = p.arg ? String(p.arg.content ?? p.arg.loc?.source ?? '') : 'default';
|
|
@@ -271,7 +277,18 @@ const normalise = (node) => {
|
|
|
271
277
|
* template or the template does not parse — a rule must treat that as "nothing
|
|
272
278
|
* to say", never as "compliant".
|
|
273
279
|
*/
|
|
274
|
-
|
|
280
|
+
/** One parse per file and source per run: several registry rules read the same Views. */
|
|
281
|
+
const templateCache = new Map();
|
|
282
|
+
export const templateElements = (file, source) => {
|
|
283
|
+
const key = `${file}\0${source}`;
|
|
284
|
+
let hit = templateCache.get(key);
|
|
285
|
+
if (!hit) {
|
|
286
|
+
hit = parseTemplate(file, source);
|
|
287
|
+
templateCache.set(key, hit);
|
|
288
|
+
}
|
|
289
|
+
return hit;
|
|
290
|
+
};
|
|
291
|
+
const parseTemplate = async (file, source) => {
|
|
275
292
|
if (!file.endsWith('.vue'))
|
|
276
293
|
return [];
|
|
277
294
|
const { parse } = await sfc();
|
|
@@ -80,6 +80,26 @@ const uiKitReuseFirst = {
|
|
|
80
80
|
return out;
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Blank everything outside `<style>` bodies, line count preserved. Colour tokens are a
|
|
85
|
+
* stylesheet concern: an SVG `fill="#E2E8F0"` in an inline-icon template has no place to
|
|
86
|
+
* put a `$token`, so flagging it prescribes an impossible fix (74 of 81 .vue findings
|
|
87
|
+
* fleet-wide were exactly that, 2026-10-02).
|
|
88
|
+
*/
|
|
89
|
+
const onlyStyleBlocks = (source) => {
|
|
90
|
+
let out = '';
|
|
91
|
+
let last = 0;
|
|
92
|
+
const re = /<style\b[^>]*>([\s\S]*?)<\/style>/g;
|
|
93
|
+
let m;
|
|
94
|
+
while ((m = re.exec(source)) !== null) {
|
|
95
|
+
const body = m[1] ?? '';
|
|
96
|
+
const bodyStart = m.index + m[0].length - body.length - '</style>'.length;
|
|
97
|
+
out += source.slice(last, bodyStart).replace(/[^\n]/g, ' ');
|
|
98
|
+
out += body;
|
|
99
|
+
last = bodyStart + body.length;
|
|
100
|
+
}
|
|
101
|
+
return out + source.slice(last).replace(/[^\n]/g, ' ');
|
|
102
|
+
};
|
|
83
103
|
const scssColourTokens = {
|
|
84
104
|
id: 'core/C3',
|
|
85
105
|
title: 'literal colour in SCSS instead of a design token',
|
|
@@ -96,7 +116,8 @@ const scssColourTokens = {
|
|
|
96
116
|
const palette = setting({ settings }, 'scssTokensFile', 'app/assets/scss/_colors.scss');
|
|
97
117
|
if (file === palette)
|
|
98
118
|
return [];
|
|
99
|
-
|
|
119
|
+
const scanned = file.endsWith('.vue') ? onlyStyleBlocks(code) : code;
|
|
120
|
+
return eachLine(scanned, (line, n) => {
|
|
100
121
|
// `rgba($token, .5)` is correct usage — only a NUMERIC first argument is a literal.
|
|
101
122
|
const hex = /#[0-9a-fA-F]{3,8}\b/.exec(line);
|
|
102
123
|
const numericRgb = /\brgba?\(\s*\d[^)]*\)/.exec(line);
|
|
@@ -122,12 +143,19 @@ const dateFormatEnum = {
|
|
|
122
143
|
return [];
|
|
123
144
|
// A date pattern: only Y/M/D/H/m/s and separators, and carrying a real token.
|
|
124
145
|
const FORMAT = /(['"`])((?=[^'"`]*(?:YYYY|DD|MM|HH|mm|ss))[YMDHhmsAaZ\s:/.,-]{5,})\1/;
|
|
146
|
+
// Every letter RUN must be a real dayjs token: 'ADHD / ADD' passes the char class
|
|
147
|
+
// (A, D and H are all tokens) and the DD lookahead, but neither 'ADHD' nor 'ADD'
|
|
148
|
+
// is a token — it is an answer label, not a format (found live in cs-app, 2026-10-02).
|
|
149
|
+
const TOKENS = new Set(['YYYY', 'YY', 'Y', 'MMMM', 'MMM', 'MM', 'M', 'DD', 'D', 'Do', 'dddd', 'ddd', 'dd', 'd', 'HH', 'H', 'hh', 'h', 'mm', 'm', 'ss', 's', 'SSS', 'A', 'a', 'Z', 'ZZ']);
|
|
150
|
+
const isFormat = (s) => (s.match(/[A-Za-z]+/g) ?? []).every(run => TOKENS.has(run));
|
|
125
151
|
return eachLine(code, (line, n) => {
|
|
126
|
-
|
|
127
|
-
|
|
152
|
+
// Only the day-extraction misuse shifts the calendar day. A bare toISOString()
|
|
153
|
+
// (an API timestamp) and the epoch duration trick (.slice(14, 19)) are correct uses.
|
|
154
|
+
if (/\.toISOString\s*\(\s*\)\s*(?:\.split\s*\(\s*['"`]T['"`]\s*\)|\.(?:slice|substring)\s*\(\s*0\s*,\s*10\s*\))/.test(line)) {
|
|
155
|
+
return { file, line: n, message: `toISOString() is always UTC — extracting its date part shifts the calendar day for any visitor west of Greenwich; format through ${dateModule}` };
|
|
128
156
|
}
|
|
129
157
|
const m = FORMAT.exec(line);
|
|
130
|
-
if (!m)
|
|
158
|
+
if (!m || !isFormat(m[2] ?? ''))
|
|
131
159
|
return null;
|
|
132
160
|
return { file, line: n, message: `hand-rolled date format ${m[1]}${m[2]}${m[1]} — use a member of ${dateModule}` };
|
|
133
161
|
});
|
|
@@ -33,16 +33,40 @@ export const commonFixtures = {
|
|
|
33
33
|
bad: 'window.addEventListener("resize", onResize)',
|
|
34
34
|
good: 'useEventListener(window, "resize", onResize)',
|
|
35
35
|
},
|
|
36
|
-
'core/C3':
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
file:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
'core/C3': [
|
|
37
|
+
{
|
|
38
|
+
file: 'app/assets/scss/components/Card.scss',
|
|
39
|
+
bad: '.card { color: #1f2937; }',
|
|
40
|
+
good: '.card { color: $slate-800; }',
|
|
41
|
+
},
|
|
42
|
+
// In a .vue file only the <style> block is a stylesheet: an SVG fill in the
|
|
43
|
+
// template has no place to put a $token, so it must stay quiet.
|
|
44
|
+
{
|
|
45
|
+
file: 'app/components/UI/Icons/IconLeaf.vue',
|
|
46
|
+
bad: '<template><svg><path fill="currentColor" /></svg></template>\n<style scoped lang="scss">\n.icon { color: #e2e8f0; }\n</style>',
|
|
47
|
+
good: '<template><svg><path fill="#E2E8F0" /></svg></template>\n<style scoped lang="scss">\n.icon { color: $slate-200; }\n</style>',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
'core/C4': [
|
|
51
|
+
{
|
|
52
|
+
file: 'app/a.ts',
|
|
53
|
+
bad: "const d = dayjs().format('YYYY-MM-DD')",
|
|
54
|
+
good: 'const d = dayjs().format(DateFormat.DefaultDate)',
|
|
55
|
+
},
|
|
56
|
+
// The day-extraction misuse fires; a bare toISOString() (an API timestamp) and the
|
|
57
|
+
// epoch duration trick do not.
|
|
58
|
+
{
|
|
59
|
+
file: 'app/b.ts',
|
|
60
|
+
bad: "const day = new Date().toISOString().split('T')[0]",
|
|
61
|
+
good: 'const generatedAt = dayjs().toISOString()\nconst duration = new Date(s * 1000).toISOString().slice(14, 19)',
|
|
62
|
+
},
|
|
63
|
+
// Every letter run must be a real dayjs token — an answer label that happens to fit
|
|
64
|
+
// the char class ('ADHD / ADD' carries a DD) is not a format string.
|
|
65
|
+
{
|
|
66
|
+
file: 'app/c.ts',
|
|
67
|
+
good: "const option = { code: 'ADHD_ADD', label: 'ADHD / ADD' }",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
46
70
|
'core/C6': {
|
|
47
71
|
file: 'app/assets/scss/components/Card.scss',
|
|
48
72
|
bad: '.card { font-size: 14px; }',
|
|
@@ -42,7 +42,7 @@ export declare const sharedRuleIds: {
|
|
|
42
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
44
|
readonly 'core/C3': "core/C3";
|
|
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`). */
|
|
45
|
+
/** `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
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
48
|
readonly 'core/C5': "core/C5";
|
|
@@ -42,7 +42,7 @@ export const sharedRuleIds = {
|
|
|
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
|
-
/** `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`). */
|
|
45
|
+
/** `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
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',
|
package/package.json
CHANGED