fix: resolve ESLint configuration and TypeScript errors across monorepo

- Add "type": "module" to @tpmjs/eslint-config package.json for ES module support
- Rename eslint.config.js to eslint.config.mjs in apps/web and packages/ui
- Update ESLint configs to ignore build directories (.next, dist, .turbo)
- Extend import/no-internal-modules allowlist for Next.js, testing libs, and React
- Fix unescaped quotes in playground page code elements
- Convert CardDescriptionProps from empty interface to type alias
- Disable jsx-a11y/no-autofocus rule in test files
- Change web app lint script from 'next lint' to 'eslint .'
- Fix TypeScript ref type inference errors in UI package tests by using non-null assertions
- Disable Biome noNonNullAssertion rule for test files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-11-26 13:25:30 +10:00
parent 27f867ff24
commit 54f2c1a2d1
23 changed files with 161 additions and 100 deletions

View file

@ -1,3 +0,0 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
export default reactConfig;

View file

@ -0,0 +1,16 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
export default [
{
ignores: [
'.next/**',
'.turbo/**',
'node_modules/**',
'*.config.js',
'*.config.ts',
'next-env.d.ts',
'eslint.config.mjs'
]
},
...reactConfig,
];

View file

@ -6,7 +6,7 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint .",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"clean": "rm -rf .next .turbo" "clean": "rm -rf .next .turbo"
}, },

View file

@ -167,16 +167,18 @@ export default function PlaygroundPage() {
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-medium">Available Icons</h3> <h3 className="text-xl font-medium">Available Icons</h3>
<div className="grid grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-6"> <div className="grid grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-6">
{([ {(
"check", [
"x", "check",
"chevronDown", "x",
"copy", "chevronDown",
"externalLink", "copy",
"github", "externalLink",
"sun", "github",
"moon", "sun",
] as const).map((icon) => ( "moon",
] as const
).map((icon) => (
<div <div
key={icon} key={icon}
className="flex flex-col items-center gap-2 p-3 rounded-lg hover:bg-surface transition-colors" className="flex flex-col items-center gap-2 p-3 rounded-lg hover:bg-surface transition-colors"
@ -463,28 +465,28 @@ export default function Example() {
<Container size="sm" padding="md"> <Container size="sm" padding="md">
<Card> <Card>
<CardContent className="p-4 text-center"> <CardContent className="p-4 text-center">
<code className="text-sm">size="sm"</code> <code className="text-sm">size=&quot;sm&quot;</code>
</CardContent> </CardContent>
</Card> </Card>
</Container> </Container>
<Container size="md" padding="md"> <Container size="md" padding="md">
<Card> <Card>
<CardContent className="p-4 text-center"> <CardContent className="p-4 text-center">
<code className="text-sm">size="md"</code> <code className="text-sm">size=&quot;md&quot;</code>
</CardContent> </CardContent>
</Card> </Card>
</Container> </Container>
<Container size="lg" padding="md"> <Container size="lg" padding="md">
<Card> <Card>
<CardContent className="p-4 text-center"> <CardContent className="p-4 text-center">
<code className="text-sm">size="lg"</code> <code className="text-sm">size=&quot;lg&quot;</code>
</CardContent> </CardContent>
</Card> </Card>
</Container> </Container>
<Container size="xl" padding="md"> <Container size="xl" padding="md">
<Card> <Card>
<CardContent className="p-4 text-center"> <CardContent className="p-4 text-center">
<code className="text-sm">size="xl"</code> <code className="text-sm">size=&quot;xl&quot;</code>
</CardContent> </CardContent>
</Card> </Card>
</Container> </Container>

View file

@ -37,5 +37,17 @@
"trailingCommas": "es5", "trailingCommas": "es5",
"semicolons": "always" "semicolons": "always"
} }
} },
"overrides": [
{
"include": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off"
}
}
}
}
]
} }

View file

@ -1,32 +1,44 @@
import js from '@eslint/js'; import js from "@eslint/js";
import tseslint from 'typescript-eslint'; import importPlugin from "eslint-plugin-import";
import importPlugin from 'eslint-plugin-import'; import tseslint from "typescript-eslint";
export default tseslint.config( export default tseslint.config(
js.configs.recommended, js.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
{ {
plugins: { plugins: {
import: importPlugin, import: importPlugin,
}, },
rules: { rules: {
// Module boundaries // Module boundaries
'import/no-internal-modules': ['error', { "import/no-internal-modules": [
allow: [ "error",
'@tpmjs/ui/*/[!index]*', {
'@tpmjs/utils/*', allow: [
'@tpmjs/types/*', "@tpmjs/ui/*/[!index]*",
] "@tpmjs/utils/*",
}], "@tpmjs/types/*",
'import/no-restricted-paths': ['error', { "@tpmjs/eslint-config/*",
zones: [ "next/**",
{ target: './packages', from: './apps' }, "./.next/*",
{ target: './packages/ui', from: './packages/utils' }, ],
] },
}], ],
'import/no-anonymous-default-export': 'error', "import/no-restricted-paths": [
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], "error",
'@typescript-eslint/no-explicit-any': 'warn', {
}, zones: [
} { target: "./packages", from: "./apps" },
{ target: "./packages/ui", from: "./packages/utils" },
],
},
],
"import/no-anonymous-default-export": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_" },
],
"@typescript-eslint/no-explicit-any": "warn",
},
},
); );

View file

@ -1,19 +1,17 @@
{ {
"name": "@tpmjs/eslint-config", "name": "@tpmjs/eslint-config",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"main": "./base.js", "type": "module",
"files": [ "main": "./base.js",
"base.js", "files": ["base.js", "react.js"],
"react.js" "dependencies": {
], "@eslint/js": "^9.18.0",
"dependencies": { "eslint": "^9.18.0",
"@eslint/js": "^9.18.0", "eslint-plugin-import": "^2.31.0",
"eslint": "^9.18.0", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-react": "^7.37.3",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react": "^7.37.3", "typescript-eslint": "^8.19.1"
"eslint-plugin-react-hooks": "^5.1.0", }
"typescript-eslint": "^8.19.1"
}
} }

View file

@ -1,3 +0,0 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
export default reactConfig;

View file

@ -0,0 +1,28 @@
import reactConfig from '@tpmjs/eslint-config/react.js';
export default [
{
ignores: ['dist/**', 'node_modules/**', 'eslint.config.mjs']
},
...reactConfig,
{
rules: {
'import/no-internal-modules': ['error', {
allow: [
'@tpmjs/ui/*/[!index]*',
'@tpmjs/utils/*',
'@tpmjs/types/*',
'@tpmjs/eslint-config/*',
'@testing-library/**',
'react/*',
]
}],
}
},
{
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
rules: {
'jsx-a11y/no-autofocus': 'off',
}
}
];

View file

@ -242,7 +242,7 @@ describe("Badge", () => {
let ref: HTMLDivElement | null = null; let ref: HTMLDivElement | null = null;
render( render(
<Badge <Badge
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -250,7 +250,7 @@ describe("Badge", () => {
</Badge>, </Badge>,
); );
expect(ref).toBeInstanceOf(HTMLDivElement); expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref?.tagName).toBe("DIV"); expect(ref!.tagName).toBe("DIV");
}); });
}); });

View file

@ -233,7 +233,7 @@ describe("Button", () => {
let ref: HTMLButtonElement | null = null; let ref: HTMLButtonElement | null = null;
render( render(
<Button <Button
ref={(el: HTMLButtonElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -241,7 +241,7 @@ describe("Button", () => {
</Button>, </Button>,
); );
expect(ref).toBeInstanceOf(HTMLButtonElement); expect(ref).toBeInstanceOf(HTMLButtonElement);
expect(ref?.tagName).toBe("BUTTON"); expect(ref!.tagName).toBe("BUTTON");
}); });
}); });

View file

@ -74,7 +74,7 @@ describe("Card", () => {
); );
const card = screen.getByTestId("card"); const card = screen.getByTestId("card");
expect(card.className).toContain("border-dotted"); expect(card.className).toContain("border-dotted");
expect(card.className).toContain("border-2"); expect(card.className).toContain("border-2");
expect(card.className).toContain("bg-transparent"); expect(card.className).toContain("bg-transparent");
}); });
@ -409,7 +409,7 @@ describe("Card", () => {
let ref: HTMLDivElement | null = null; let ref: HTMLDivElement | null = null;
render( render(
<Card <Card
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -417,7 +417,7 @@ describe("Card", () => {
</Card>, </Card>,
); );
expect(ref).toBeInstanceOf(HTMLDivElement); expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref?.tagName).toBe("DIV"); expect(ref!.tagName).toBe("DIV");
}); });
it("forwards ref to CardHeader element", () => { it("forwards ref to CardHeader element", () => {
@ -425,7 +425,7 @@ describe("Card", () => {
render( render(
<Card> <Card>
<CardHeader <CardHeader
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -442,7 +442,7 @@ describe("Card", () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle <CardTitle
ref={(el: HTMLHeadingElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -452,7 +452,7 @@ describe("Card", () => {
</Card>, </Card>,
); );
expect(ref).toBeInstanceOf(HTMLHeadingElement); expect(ref).toBeInstanceOf(HTMLHeadingElement);
expect(ref?.tagName).toBe("H3"); expect(ref!.tagName).toBe("H3");
}); });
it("forwards ref to CardDescription element", () => { it("forwards ref to CardDescription element", () => {
@ -461,7 +461,7 @@ describe("Card", () => {
<Card> <Card>
<CardHeader> <CardHeader>
<CardDescription <CardDescription
ref={(el: HTMLParagraphElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -478,7 +478,7 @@ describe("Card", () => {
render( render(
<Card> <Card>
<CardContent <CardContent
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -494,7 +494,7 @@ describe("Card", () => {
render( render(
<Card> <Card>
<CardFooter <CardFooter
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >

View file

@ -42,8 +42,7 @@ export interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
/** /**
* CardDescription component props * CardDescription component props
*/ */
export interface CardDescriptionProps export type CardDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
extends HTMLAttributes<HTMLParagraphElement> {}
/** /**
* CardContent component props * CardContent component props

View file

@ -287,13 +287,13 @@ describe("CodeBlock", () => {
render( render(
<CodeBlock <CodeBlock
code="code" code="code"
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
/>, />,
); );
expect(ref).toBeInstanceOf(HTMLDivElement); expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref?.querySelector("code")).toBeInTheDocument(); expect(ref!.querySelector("code")).toBeInTheDocument();
}); });
}); });

View file

@ -215,7 +215,7 @@ describe("Container", () => {
let ref: HTMLDivElement | null = null; let ref: HTMLDivElement | null = null;
render( render(
<Container <Container
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -223,7 +223,7 @@ describe("Container", () => {
</Container>, </Container>,
); );
expect(ref).toBeInstanceOf(HTMLDivElement); expect(ref).toBeInstanceOf(HTMLDivElement);
expect(ref?.tagName).toBe("DIV"); expect(ref!.tagName).toBe("DIV");
}); });
}); });

View file

@ -258,7 +258,7 @@ describe("GridContainer", () => {
let ref: HTMLDivElement | null = null; let ref: HTMLDivElement | null = null;
render( render(
<GridContainer <GridContainer
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >

View file

@ -249,13 +249,13 @@ describe("Header", () => {
let ref: HTMLElement | null = null; let ref: HTMLElement | null = null;
render( render(
<Header <Header
ref={(el: HTMLElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
/>, />,
); );
expect(ref).toBeInstanceOf(HTMLElement); expect(ref).toBeInstanceOf(HTMLElement);
expect(ref?.tagName).toBe("HEADER"); expect(ref!.tagName).toBe("HEADER");
}); });
}); });

View file

@ -179,7 +179,7 @@ describe("Icon", () => {
/>, />,
); );
expect(ref).toBeInstanceOf(SVGSVGElement); expect(ref).toBeInstanceOf(SVGSVGElement);
expect(ref?.tagName).toBe("svg"); expect(ref!.tagName).toBe("svg");
}); });
}); });

View file

@ -311,26 +311,26 @@ describe("Input", () => {
let ref: HTMLInputElement | null = null; let ref: HTMLInputElement | null = null;
render( render(
<Input <Input
ref={(el: HTMLInputElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
/>, />,
); );
expect(ref).toBeInstanceOf(HTMLInputElement); expect(ref).toBeInstanceOf(HTMLInputElement);
expect(ref?.tagName).toBe("INPUT"); expect(ref!.tagName).toBe("INPUT");
}); });
it("can focus input through ref", () => { it("can focus input through ref", () => {
let ref: HTMLInputElement | null = null; let ref: HTMLInputElement | null = null;
render( render(
<Input <Input
ref={(el: HTMLInputElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
data-testid="input" data-testid="input"
/>, />,
); );
ref?.focus(); ref!.focus();
expect(screen.getByTestId("input")).toHaveFocus(); expect(screen.getByTestId("input")).toHaveFocus();
}); });
}); });

View file

@ -208,7 +208,7 @@ describe("Label", () => {
</Label>, </Label>,
); );
expect(ref).toBeInstanceOf(HTMLLabelElement); expect(ref).toBeInstanceOf(HTMLLabelElement);
expect(ref?.tagName).toBe("LABEL"); expect(ref!.tagName).toBe("LABEL");
}); });
}); });

View file

@ -221,7 +221,7 @@ describe("ProgressBar", () => {
render( render(
<ProgressBar <ProgressBar
value={50} value={50}
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
/>, />,

View file

@ -284,7 +284,7 @@ describe("Section", () => {
let ref: HTMLElement | null = null; let ref: HTMLElement | null = null;
render( render(
<Section <Section
ref={(el: HTMLElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
> >
@ -292,7 +292,7 @@ describe("Section", () => {
</Section>, </Section>,
); );
expect(ref).toBeInstanceOf(HTMLElement); expect(ref).toBeInstanceOf(HTMLElement);
expect(ref?.tagName).toBe("SECTION"); expect(ref!.tagName).toBe("SECTION");
}); });
}); });

View file

@ -370,7 +370,7 @@ describe("Tabs", () => {
tabs={mockTabs} tabs={mockTabs}
activeTab="all" activeTab="all"
onTabChange={handleChange} onTabChange={handleChange}
ref={(el: HTMLDivElement | null) => { ref={(el) => {
ref = el; ref = el;
}} }}
/>, />,