-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy patheslint.config.js
More file actions
198 lines (189 loc) · 8.38 KB
/
Copy patheslint.config.js
File metadata and controls
198 lines (189 loc) · 8.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// eslint.config.js — flat config (ESLint 9+, ESM)
// Architecture/token guard for offlinecv. Minimal ruleset: no style
// bikeshedding, just the structural rules that style_guard.sh checked.
// These same checks run (blocking) in CI via `npm run lint`.
import tseslint from "typescript-eslint";
import reactPlugin from "eslint-plugin-react";
import globals from "globals";
/** Palette colour segments guarded by the token rules. */
const PALETTE_COLOURS =
"red|green|emerald|slate|amber|blue|gray|zinc|stone|orange|yellow|lime|teal|cyan|sky|indigo|violet|purple|fuchsia|pink|rose";
/** Tailwind property prefixes that may carry raw palette colours. */
const PALETTE_PROPS = "bg|text|border|ring|shadow|fill|stroke";
const PALETTE_RE = `(${PALETTE_PROPS})-(${PALETTE_COLOURS})-[0-9]`;
const DARK_RE = `dark:[a-z]+-[a-z]+-[0-9]`;
/** Hardcoded hex colours, e.g. #ef4444, #fff, bg-[#f00], #334155cc.
*
* `#638` is simultaneously a valid 3-digit hex colour and a valid GitHub issue
* reference, and this rule matches `Literal`/`TemplateElement` VALUES — so an
* unanchored `#[0-9a-fA-F]{3,6}` reported every `describe("… (#638)")` in the
* suite as a hardcoded colour. (Comments are neither node type, which is why
* the contrast-ratio docblocks in `CountBadge.tsx`/`Tabs.tsx` were never hit
* and the trap only ever bites test/UI STRINGS.) Same unanchored-substring
* defect as ARBITRARY_TEXT_SIZE_RE below — fixed the same way, positionally.
*
* The two hex shapes live in different positions, and that is what separates
* them from an issue number:
* - 6- or 8-digit form is unambiguous at any length, so it matches anywhere;
* - 3- or 4-digit form only where a colour can actually START — at the
* beginning of the string value (`"#fff"`, `color: "#f00"` as a whole
* value) or straight after Tailwind's arbitrary-value `[` (`bg-[#f00]`).
* An issue reference is always preceded by a space or `(`, so it matches
* neither branch. A string whose ENTIRE value is `#638` is still reported:
* genuinely ambiguous, and a false positive there is cheaper than letting
* `"#fff"` through. */
const HEX_RE = `(?:(?:^|\\[)#[0-9a-fA-F]{3,4}\\b|#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?\\b)`;
/** Arbitrary Tailwind font-size values, e.g. text-[11px], text-[0.6875rem],
* text-[10pt], text-[2vw] or the explicit `text-[length:12px]` form.
*
* Left boundary is load-bearing (#640 review): without it the pattern is a
* bare substring match, so ANY class ending in "text-" fires it — `context-`,
* `subtext-`, a data attribute — and the rule reports a violation on a line
* that has no arbitrary font size at all.
*
* The unit alternation covers every CSS length unit Tailwind will accept here,
* not just the two the #640 sweep happened to use. Arbitrary values that are
* NOT sizes stay legal — `text-[color:var(--x)]` and `text-[#abc]` are colour
* values, governed by the palette/hex rules above, and neither matches. */
const ARBITRARY_TEXT_SIZE_RE = `(^|[\\s:'"\`])text-\\[(length:)?[0-9.]+(px|rem|em|pt|vw|vh|ch|ex|%)\\]`;
/** no-restricted-syntax selectors that catch both string literals and
* template-literal chunks (so cn()/clsx template strings are covered). */
function restrictedSyntaxRules() {
return [
// Raw Tailwind palette colours in class strings
{
selector: `Literal[value=/${PALETTE_RE}/]`,
message:
"Use semantic tokens (bg-surface-card, text-content-primary, border-border-light, text-accent-primary, …) instead of raw Tailwind palette classes.",
},
{
selector: `TemplateElement[value.raw=/${PALETTE_RE}/]`,
message:
"Use semantic tokens (bg-surface-card, text-content-primary, border-border-light, text-accent-primary, …) instead of raw Tailwind palette classes.",
},
// Manual dark: colour variants
{
selector: `Literal[value=/${DARK_RE}/]`,
message:
"Semantic tokens handle dark mode automatically — drop manual dark: colour variants.",
},
{
selector: `TemplateElement[value.raw=/${DARK_RE}/]`,
message:
"Semantic tokens handle dark mode automatically — drop manual dark: colour variants.",
},
// Hardcoded hex colours
{
selector: `Literal[value=/${HEX_RE}/]`,
message:
"No hardcoded hex colours in feature code — use semantic tokens.",
},
{
selector: `TemplateElement[value.raw=/${HEX_RE}/]`,
message:
"No hardcoded hex colours in feature code — use semantic tokens.",
},
// Arbitrary font-size values
{
selector: `Literal[value=/${ARBITRARY_TEXT_SIZE_RE}/]`,
message:
"Use a named type-ramp step (text-4xs, text-3xs, text-2xs, text-xs, text-sm, …) instead of an arbitrary font size.",
},
{
selector: `TemplateElement[value.raw=/${ARBITRARY_TEXT_SIZE_RE}/]`,
message:
"Use a named type-ramp step (text-4xs, text-3xs, text-2xs, text-xs, text-sm, …) instead of an arbitrary font size.",
},
];
}
export default [
// ── Global ignores ──────────────────────────────────────────────────────
{
ignores: [
// Build output, wherever it lands. Anchored `dist/**` only matched
// `vite build`'s root output, so `eslint .` walked the 49 emitted files
// under `packages/core/dist/` (#772) — generated JS, linted against rules
// written for hand-authored source. Unanchored, both are covered.
"**/dist/**",
"node_modules/**",
"*.config.js",
"*.config.ts",
"scripts/**",
"coverage/**",
".claude/**",
// Private repos cloned into this checkout (see .gitignore). `npm run lint`
// is `eslint .`, and flat config does NOT read .gitignore — so without
// these, lint walks into a nested checkout that has its own toolchain and
// its own rules. vitest, coverage and tsconfig.app need no equivalent:
// all three are allowlisted to `src/**` already.
"internal/**",
"extension/**",
],
},
// ── Base block: all src TypeScript ──────────────────────────────────────
{
files: ["src/**/*.{ts,tsx}"],
plugins: {
"@typescript-eslint": tseslint.plugin,
react: reactPlugin,
},
languageOptions: {
parser: tseslint.parser,
globals: {
...globals.browser,
},
},
settings: {
react: {
version: "detect",
},
},
// No broad recommended rulesets — keep minimal to protect the green
// baseline. Only the architecture/token rules below apply.
rules: {},
},
// ── Architecture guard: components + the two lane roots ────────────────
// These rules encode the same checks style_guard.sh runs (non-blocking,
// advisory). Here they are BLOCKING (error) and run in CI.
//
// The scope is every surface that WRITES markup: the component tiers, plus
// the ROOT of each of the two HTML entries the build ships (vite.config.ts
// `rollupOptions.input`). `src/App.tsx` was listed alone, which left its
// peer — `JobsApp` (`/jobs/`) — writing JSX outside every token rule
// (#640 review). Listed as the two files rather than as `src/jobs/**` so
// the scope stays "the entry roots", matching how `src/App.tsx` is named;
// `main.tsx` and the lane hooks/tests hold no markup. `src/lib/**` and
// `src/hooks/**` stay out for the same reason.
{
files: [
"src/components/**/*.{ts,tsx}",
"src/design-system/**/*.{ts,tsx}",
"src/App.tsx",
"src/jobs/JobsApp.tsx",
],
rules: {
// Raw <button> outside the Button primitive is forbidden in feature code.
"react/forbid-elements": [
"error",
{
forbid: [
{
element: "button",
message:
"Use the <Button> primitive from @design-system instead of a raw <button>.",
},
],
},
],
"no-restricted-syntax": ["error", ...restrictedSyntaxRules()],
},
},
// ── Allow raw <button> inside the Button primitive itself ────────────────
// Flat config: later blocks win, so this override is applied last.
{
files: ["src/design-system/primitives/Button.tsx"],
rules: {
"react/forbid-elements": "off",
},
},
];