Capture the five recurring code-review patterns from the recent PR series
(#3591–#3596) as enforceable rules so they don't have to be raised again
in review.
## Conventions
- Prefer MUI native props over `sx` (e.g. color={...}, mt={...}, width={...}).
- Use the full theme path for colors: theme.palette.text.secondary, never
the "text.secondary" shorthand string. The shorthand is invisible to
grep, which makes future audits and refactors miss callsites.
- No hardcoded numeric or string literals — use LAYOUT, typographyLevels,
theme.shape.borderRadius, theme.palette tokens.
- Use useTheme() inside components; don't import { theme } from
@/Utils/Theme/Theme (factory output, not a live mode-aware instance).
- Pair runtime tuples with derived types so they can't drift.
## Files
- docs/frontend-conventions.md — long-form doc with before/after examples
drawn from the actual review fixes, plus a pre-PR checklist.
- CLAUDE.md — short summary in the Code Conventions section pointing to
the doc, so it's loaded into agent context at session start.
## ESLint enforcement
- client/.eslintrc.cjs — four no-restricted-syntax selectors for each rule:
- color="…" / bgcolor="…" / borderColor="…" with a dotted shorthand value
- sx={{ key: ... }} where key has a native equivalent, only on hoistable
components (Box, Stack, Typography, Grid, Container, Paper, Card*,
Divider, Link, Chip) — Dialog/Drawer/Menu etc. only accept sx.
- import { theme } from "@/Utils/Theme/Theme"
- Typography fontSize={13} (the body default — drop the prop).
- TypeScript files are now actually parsed: added @typescript-eslint/parser
and @typescript-eslint/eslint-plugin and a TS override that extends the
React plugin configs. Several @typescript-eslint/recommended rules
disabled to avoid noise on the existing codebase.
- client/package.json — lint script now covers .ts/.tsx; new lint-strict
script for CI use once the codebase is conformant; --max-warnings 0
removed from `lint` so existing-code warnings don't block the workflow.
## Surface
Running `npm run lint` after this change reports ~280 warnings against
the existing codebase, almost all from rule #1 (sx-vs-native). These are
intentionally warnings, not errors — they surface the work without
blocking. Each warning links to the rule number in the conventions doc
so the reasoning is one click away.
159 lines
4.7 KiB
JavaScript
159 lines
4.7 KiB
JavaScript
// MUI sx keys that have a native (system) prop equivalent.
|
|
// Flagged when found inside an sx={{ ... }} object literal — see frontend-conventions.md rule #1.
|
|
const SX_NATIVE_EQUIVALENT_KEYS = [
|
|
"color",
|
|
"bgcolor",
|
|
"backgroundColor",
|
|
"borderRadius",
|
|
"border",
|
|
"borderTop",
|
|
"borderBottom",
|
|
"borderLeft",
|
|
"borderRight",
|
|
"width",
|
|
"height",
|
|
"minHeight",
|
|
"maxWidth",
|
|
"minWidth",
|
|
"maxHeight",
|
|
"p",
|
|
"pt",
|
|
"pb",
|
|
"px",
|
|
"py",
|
|
"pl",
|
|
"pr",
|
|
"m",
|
|
"mt",
|
|
"mb",
|
|
"mx",
|
|
"my",
|
|
"ml",
|
|
"mr",
|
|
"display",
|
|
"textAlign",
|
|
"fontWeight",
|
|
"fontSize",
|
|
"lineHeight",
|
|
"gap",
|
|
];
|
|
|
|
// MUI components that accept system props as top-level attributes.
|
|
// sx={{ color, p, mt, ... }} on these can be hoisted into native props.
|
|
// Dialog*, Drawer*, Menu*, Tab*, Toolbar etc. only accept sx — don't flag those.
|
|
const SX_HOISTABLE_COMPONENTS = [
|
|
"Box",
|
|
"Stack",
|
|
"Typography",
|
|
"Grid",
|
|
"Container",
|
|
"Paper",
|
|
"Card",
|
|
"CardContent",
|
|
"CardActions",
|
|
"CardHeader",
|
|
"Divider",
|
|
"Link",
|
|
"Chip",
|
|
];
|
|
|
|
const sxKeySelector = SX_HOISTABLE_COMPONENTS.flatMap((component) =>
|
|
SX_NATIVE_EQUIVALENT_KEYS.map(
|
|
(k) =>
|
|
`JSXOpeningElement[name.name='${component}'] > JSXAttribute[name.name='sx'] > JSXExpressionContainer > ObjectExpression > Property[key.name='${k}']`
|
|
)
|
|
).join(", ");
|
|
|
|
const FRONTEND_CONVENTION_RULES = {
|
|
// Rule #2 — color/bgcolor/borderColor as a string shorthand like "text.secondary".
|
|
// Catches: <Typography color="text.secondary" /> — should be color={theme.palette.text.secondary}.
|
|
"no-restricted-syntax": [
|
|
"warn",
|
|
{
|
|
selector:
|
|
"JSXAttribute[name.name=/^(color|bgcolor|borderColor)$/] > Literal[value=/\\./]",
|
|
message:
|
|
'Use the full theme path instead of a string shorthand. e.g. color={theme.palette.text.secondary} not color="text.secondary". See docs/frontend-conventions.md rule #2.',
|
|
},
|
|
{
|
|
// Rule #1 — sx={{ key: ... }} where key has a native prop equivalent.
|
|
selector: sxKeySelector,
|
|
message:
|
|
"Hoist this key out of `sx` onto the component as a native MUI prop (e.g. color={...}, mt={...}, width={...}). See docs/frontend-conventions.md rule #1.",
|
|
},
|
|
{
|
|
// Rule #4 — importing the singleton theme from the Theme module.
|
|
// Catches: import { theme } from "@/Utils/Theme/Theme" — should be useTheme() inside the component.
|
|
selector:
|
|
"ImportDeclaration[source.value='@/Utils/Theme/Theme'] > ImportSpecifier[imported.name='theme']",
|
|
message:
|
|
"Don't import { theme } from @/Utils/Theme/Theme — use the useTheme() hook inside the component. See docs/frontend-conventions.md rule #4.",
|
|
},
|
|
{
|
|
// Rule #3 (subset) — Typography fontSize={13} is the body default; drop it.
|
|
selector:
|
|
"JSXOpeningElement[name.name='Typography'] JSXAttribute[name.name='fontSize'] > JSXExpressionContainer > Literal[value=13]",
|
|
message:
|
|
"fontSize={13} is the Typography body default — drop the prop. For other sizes use typographyLevels tokens. See docs/frontend-conventions.md rule #3.",
|
|
},
|
|
],
|
|
};
|
|
|
|
module.exports = {
|
|
root: true,
|
|
env: { browser: true, es2020: true },
|
|
extends: [
|
|
"eslint:recommended",
|
|
"plugin:react/recommended",
|
|
"plugin:react/jsx-runtime",
|
|
"plugin:react-hooks/recommended",
|
|
],
|
|
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
|
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
|
settings: { react: { version: "18.2" } },
|
|
plugins: ["react-refresh"],
|
|
rules: {
|
|
"react/jsx-no-target-blank": "off",
|
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
|
"react/no-unescaped-entities": "off",
|
|
...FRONTEND_CONVENTION_RULES,
|
|
},
|
|
globals: {
|
|
__APP_VERSION__: "readonly",
|
|
process: "readonly",
|
|
},
|
|
overrides: [
|
|
{
|
|
files: ["**/*.ts", "**/*.tsx"],
|
|
parser: "@typescript-eslint/parser",
|
|
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
|
extends: [
|
|
"eslint:recommended",
|
|
"plugin:@typescript-eslint/recommended",
|
|
"plugin:react/recommended",
|
|
"plugin:react/jsx-runtime",
|
|
"plugin:react-hooks/recommended",
|
|
],
|
|
plugins: ["@typescript-eslint", "react-refresh"],
|
|
rules: {
|
|
"react/jsx-no-target-blank": "off",
|
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
|
"react/no-unescaped-entities": "off",
|
|
"no-unused-vars": "off",
|
|
"@typescript-eslint/no-unused-vars": [
|
|
"warn",
|
|
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
],
|
|
// TS handles these; eslint duplicates create noise on existing files.
|
|
"@typescript-eslint/no-explicit-any": "off",
|
|
"@typescript-eslint/ban-ts-comment": "off",
|
|
"@typescript-eslint/ban-types": "off",
|
|
"@typescript-eslint/no-empty-function": "off",
|
|
"@typescript-eslint/no-empty-interface": "off",
|
|
"@typescript-eslint/no-non-null-assertion": "off",
|
|
...FRONTEND_CONVENTION_RULES,
|
|
},
|
|
},
|
|
],
|
|
};
|