litellm/ui/litellm-dashboard/scripts/eslint-rules/filename-pascal-case.mjs
ryan-crabbe-berri 67fce87b16
chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules (#34341)
* chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules

Wires up five error-level ESLint rules on the dashboard, grandfathering every
current offender into eslint-suppressions.json so the gate only bites new code
and ratchets down as files are fixed

- local/filename-pascal-case: new local rule requiring PascalCase .tsx names,
  exempting Next.js reserved files (page, layout, route, ...) and test/spec files
  (239 grandfathered)
- max-lines: 800 lines over src/**, excluding tests, src/data, and generated
  schema.d.ts (20 grandfathered)
- local/no-complex-jsx-arrow: new local rule flagging inline JSX arrow handlers
  with block bodies over two statements; each failure is a small extract-to-named
  -handler refactor (65 grandfathered)
- prefer-const: flipped from off to error (103 grandfathered)
- no-restricted-imports: added antd to the phase-out ban alongside tremor, and
  pointed both messages at shadcn/ui primitives (405 antd import sites grandfathered)

Both new local rules ship with RuleTester coverage

* fix(ui): preserve secondary extensions in filename-pascal-case suggestion

The suggestion text built the rename from only the head segment, so a
multi-dot file like my-component.utils.tsx was told to become
MyComponent.tsx instead of MyComponent.utils.tsx. Rebuild it from the
PascalCased head plus the untouched remaining segments, and add tests
covering multi-dot filenames and the hyphenated Next.js reserved names
(global-error, apple-icon, opengraph-image, twitter-image)
2026-07-22 19:34:35 -07:00

59 lines
1.4 KiB
JavaScript

import { basename } from "path";
const NEXT_RESERVED = new Set([
"page",
"layout",
"route",
"template",
"default",
"loading",
"error",
"global-error",
"not-found",
"middleware",
"instrumentation",
"sitemap",
"robots",
"manifest",
"icon",
"apple-icon",
"favicon",
"opengraph-image",
"twitter-image",
]);
const PASCAL_CASE = /^[A-Z][A-Za-z0-9]*$/;
const rule = {
meta: {
type: "suggestion",
docs: {
description: "Require PascalCase filenames for .tsx modules; exempt Next.js reserved files and test/spec files.",
},
schema: [],
messages: {
notPascalCase: "Filename '{{name}}' should be PascalCase (e.g. '{{suggestion}}.tsx').",
},
},
create(context) {
const filename = context.filename;
const stem = basename(filename).replace(/\.tsx$/, "");
const [head, ...rest] = stem.split(".");
if (rest.includes("test") || rest.includes("spec")) return {};
if (NEXT_RESERVED.has(head)) return {};
if (PASCAL_CASE.test(head)) return {};
const pascalHead = head
.split(/[-_]/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("");
const suggestion = [pascalHead, ...rest].join(".");
return {
Program(node) {
context.report({ node, messageId: "notPascalCase", data: { name: `${stem}.tsx`, suggestion } });
},
};
},
};
export default rule;