chore(eslint): update eslint config and downgrade strict rules

This commit is contained in:
Fatih Kadir Akın
2026-02-03 13:12:56 +03:00
parent 72fb2c1662
commit 9d0141538c
17 changed files with 139 additions and 118 deletions

View File

@@ -12,7 +12,35 @@ const eslintConfig = defineConfig([
"out/**",
"build/**",
"next-env.d.ts",
// Compiled outputs
"packages/*/dist/**",
// Packages with their own ESLint config
"packages/raycast-extension/**",
// Scripts - may use CommonJS
"scripts/**",
// Prisma scripts
"prisma/**",
]),
// Downgrade strict rules to warnings for gradual adoption
{
rules: {
// React hooks compiler rules - many false positives in complex state patterns
"react-hooks/set-state-in-effect": "warn",
"react-hooks/immutability": "warn",
"react-hooks/refs": "warn",
"react-hooks/preserve-manual-memoization": "warn",
// JSX entity escaping - affects many existing components
"react/no-unescaped-entities": "warn",
// Function type - affects test mocks
"@typescript-eslint/no-unsafe-function-type": "warn",
// Display name - affects anonymous components
"react/display-name": "warn",
// HTML links - sometimes needed for external/special navigation
"@next/next/no-html-link-for-pages": "warn",
// Children as props - used in some component patterns
"react/no-children-prop": "warn",
},
},
]);
export default eslintConfig;

View File

@@ -39,6 +39,7 @@ function getJSDocComment(node: ts.Node, sourceFile: ts.SourceFile): { descriptio
const examples: string[] = [];
let description: string | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeScript AST doesn't expose jsDoc in types
const jsDocNodes = (node as any).jsDoc as ts.JSDoc[] | undefined;
if (jsDocNodes && jsDocNodes.length > 0) {
const jsDoc = jsDocNodes[0];

View File

@@ -220,7 +220,7 @@ export function PromptList({
return (
<Box flexDirection="column" height={terminalHeight} padding={1}>
<Text color="red">Error: {error}</Text>
<Text dimColor>Press 'r' to retry, 'q' to quit</Text>
<Text dimColor>Press &apos;r&apos; to retry, &apos;q&apos; to quit</Text>
</Box>
);
}

View File

@@ -55,7 +55,7 @@ function parseSimpleYaml(content: string): Record<string, unknown> {
const lines = content.split('\n');
let currentKey: string | null = null;
let currentValue: unknown = null;
const _currentValue: unknown = null; // Placeholder for future use
let inArray = false;
let inMultiline = false;
let multilineContent = '';

View File

@@ -16,21 +16,24 @@ interface LevelContentWrapperProps {
levelNumber: string;
}
export function LevelContentWrapper({ children, levelSlug, levelNumber }: LevelContentWrapperProps) {
export function LevelContentWrapper({ children, levelSlug, levelNumber: _levelNumber }: LevelContentWrapperProps) {
const t = useTranslations("kids");
const setLevelSlug = useSetLevelSlug();
const {
currentSection,
setCurrentSection,
completedSections,
const {
currentSection,
setCurrentSection,
completedSections: _completedSections,
markSectionComplete,
isSectionComplete,
isSectionComplete: _isSectionComplete,
sectionRequiresCompletion,
} = useSectionNavigation();
// Track section completion state from localStorage
const [sectionCompletionState, setSectionCompletionState] = useState<Record<number, boolean>>({});
// Track the highest section the user has visited (moved before early returns)
const [highestVisitedSection, setHighestVisitedSection] = useState(0);
// Check localStorage for section completion on mount and when section changes
const checkSectionCompletion = useCallback(() => {
const newState: Record<number, boolean> = {};
@@ -39,27 +42,40 @@ export function LevelContentWrapper({ children, levelSlug, levelNumber }: LevelC
}
setSectionCompletionState(newState);
}, [levelSlug]);
useEffect(() => {
checkSectionCompletion();
// Re-check periodically to catch component completions
const interval = setInterval(checkSectionCompletion, 500);
return () => clearInterval(interval);
}, [checkSectionCompletion, currentSection]);
// Set the level slug in context when component mounts
useEffect(() => {
setLevelSlug(levelSlug);
// Track level view
const level = getLevelBySlug(levelSlug);
if (level) {
analyticsKids.viewLevel(levelSlug, level.world);
}
return () => setLevelSlug(""); // Clear when unmounting
}, [levelSlug, setLevelSlug]);
// Update highest visited when current section changes
useEffect(() => {
setHighestVisitedSection(prev => Math.max(prev, currentSection));
}, [currentSection]);
// Reset to first section and visited state when level changes
useEffect(() => {
setCurrentSection(0);
setHighestVisitedSection(0);
setSectionCompletionState({});
// eslint-disable-next-line react-hooks/exhaustive-deps -- setCurrentSection is stable
}, [levelSlug]);
// Extract Section components from children
const sections: ReactElement[] = [];
let hasExplicitSections = false;
@@ -104,19 +120,11 @@ export function LevelContentWrapper({ children, levelSlug, levelNumber }: LevelC
const totalSections = sections.length;
const isFirstSection = currentSection === 0;
const isLastSection = currentSection === totalSections - 1;
// Check if current section is complete (from localStorage) OR doesn't require completion
const currentSectionRequiresCompletion = sectionRequiresCompletion(currentSection);
const isCurrentSectionComplete = !currentSectionRequiresCompletion || sectionCompletionState[currentSection] || false;
// Track the highest section the user has visited
const [highestVisitedSection, setHighestVisitedSection] = useState(0);
// Update highest visited when current section changes
useEffect(() => {
setHighestVisitedSection(prev => Math.max(prev, currentSection));
}, [currentSection]);
// Can navigate to a section if it's:
// 1. The current section
// 2. A previously visited section (but NOT future sections)
@@ -139,27 +147,20 @@ export function LevelContentWrapper({ children, levelSlug, levelNumber }: LevelC
setCurrentSection((prev) => prev - 1);
}
};
const handleDotClick = (targetSection: number) => {
if (canNavigateToSection(targetSection)) {
setCurrentSection(targetSection);
}
};
// Mark section as complete manually (for sections without interactive elements)
const handleMarkComplete = () => {
const _handleMarkComplete = () => {
markSectionCompleted(levelSlug, currentSection);
markSectionComplete(currentSection);
checkSectionCompletion();
};
// Reset to first section and visited state when level changes
useEffect(() => {
setCurrentSection(0);
setHighestVisitedSection(0);
setSectionCompletionState({});
}, [levelSlug]);
return (
<div className="h-full flex flex-col">
{/* Content area */}

View File

@@ -2,9 +2,7 @@
import { useState, useRef, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { useTranslations, useLocale } from "next-intl";
import { formatDistanceToNow } from "@/lib/date";
import { getPromptUrl } from "@/lib/urls";
import { ArrowBigUp, Lock, Copy, ImageIcon, Download, Play, BadgeCheck, Volume2, Link2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
@@ -91,7 +89,7 @@ export interface PromptCardProps {
export function PromptCard({ prompt, showPinButton = false, isPinned = false }: PromptCardProps) {
const t = useTranslations("prompts");
const tCommon = useTranslations("common");
const locale = useLocale();
const _locale = useLocale();
const outgoingCount = prompt._count?.outgoingConnections || 0;
const incomingCount = prompt._count?.incomingConnections || 0;
const isFlowStart = outgoingCount > 0 && incomingCount === 0;
@@ -104,7 +102,7 @@ export function PromptCard({ prompt, showPinButton = false, isPinned = false }:
const isVideo = prompt.type === "VIDEO";
const hasMediaBackground = prompt.type === "IMAGE" || isVideo || (isStructuredInput && !!prompt.mediaUrl && !isAudio);
const videoRef = useRef<HTMLVideoElement>(null);
const [isVisible, setIsVisible] = useState(false);
const [_isVisible, setIsVisible] = useState(false);
// Autoplay video when visible in viewport
useEffect(() => {

View File

@@ -794,6 +794,7 @@ function FlowGraph({ nodes, edges, currentPromptId, currentUserId, isAdmin, onNo
return (
<div ref={containerRef} className="relative">
<svg ref={svgRef} className="w-full" />
{/* eslint-disable-next-line react-hooks/refs -- Container dimensions needed for tooltip positioning */}
{hoveredNode && (() => {
// Calculate position with viewport awareness
const tooltipWidth = 320;

View File

@@ -6,7 +6,7 @@ import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Loader2, Upload, X, ArrowDown, Play, Image as ImageIcon, Video, Volume2, Paperclip, Search, Sparkles, BookOpen, ExternalLink, ChevronDown, Settings2 } from "lucide-react";
import { Loader2, Upload, X, ArrowDown, Image as ImageIcon, Video, Volume2, Paperclip, Search, Sparkles, BookOpen, ExternalLink, ChevronDown, Settings2 } from "lucide-react";
import Link from "next/link";
import { VariableToolbar } from "./variable-toolbar";
import { VariableWarning } from "./variable-warning";
@@ -21,17 +21,10 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import {
parseSkillFiles,
serializeSkillFiles,
getLanguageFromFilename,
validateFilename,
suggestFilename,
generateSkillContentWithFrontmatter,
updateSkillFrontmatter,
validateSkillFrontmatter,
DEFAULT_SKILL_FILE,
DEFAULT_SKILL_CONTENT,
type SkillFile,
} from "@/lib/skill-files";
import {
Form,
@@ -61,7 +54,7 @@ import { toast } from "sonner";
import { prettifyJson } from "@/lib/format";
import { analyticsPrompt } from "@/lib/analytics";
import { getPromptUrl } from "@/lib/urls";
import { AI_MODELS, getModelsByProvider, type PromptMCPConfig } from "@/lib/works-best-with";
import { AI_MODELS, getModelsByProvider } from "@/lib/works-best-with";
interface MediaFieldProps {
form: ReturnType<typeof useForm<PromptFormValues>>;
@@ -1324,6 +1317,7 @@ export function PromptForm({ categories, tags, initialData, initialContributors
</div>
{/* Code output content */}
<div className="p-4 text-xs space-y-1" style={{ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace' }}>
{/* eslint-disable-next-line react/jsx-no-comment-textnodes -- Intentional code preview text */}
<div><span className="text-[#6a9955]">// Code generated by skill...</span></div>
<div><span className="text-[#c586c0]">export</span> <span className="text-[#569cd6]">function</span> <span className="text-[#dcdcaa]">handler</span><span className="text-[#d4d4d4]">()</span> <span className="text-[#d4d4d4]">{'{'}</span></div>
<div><span className="text-[#d4d4d4]"> </span><span className="text-[#c586c0]">return</span> <span className="text-[#ce9178]">&quot;...&quot;</span><span className="text-[#d4d4d4]">;</span></div>

View File

@@ -1,6 +1,6 @@
import Link from "next/link";
import { useTranslations } from "next-intl";
import { Sparkles, ArrowBigUp } from "lucide-react";
import { ArrowBigUp } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { getPromptUrl } from "@/lib/urls";

View File

@@ -316,6 +316,7 @@ export function RunPromptButton({
if (url.startsWith("http://") || url.startsWith("https://")) {
window.open(url, "_blank");
} else {
// eslint-disable-next-line react-hooks/immutability -- Valid browser navigation for custom URL schemes
window.location.href = url;
}
analyticsPrompt.run(promptId, platform.name);

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useCallback, useMemo, useEffect } from "react";
import { useState, useCallback, useMemo } from "react";
import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
import { DiffEditor } from "@monaco-editor/react";

View File

@@ -32,7 +32,6 @@ import {
validateFilename,
suggestFilename,
DEFAULT_SKILL_FILE,
DEFAULT_SKILL_CONTENT,
type SkillFile,
} from "@/lib/skill-files";
@@ -357,6 +356,7 @@ export function SkillEditor({ value, onChange, className }: SkillEditorProps) {
// Only update if the value changed externally
if (value !== currentSerialized) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- Intentional sync from external prop
setFiles(parsed);
// Ensure active file exists
if (!parsed.some((f) => f.filename === activeFile)) {
@@ -372,7 +372,7 @@ export function SkillEditor({ value, onChange, className }: SkillEditorProps) {
// File icon based on extension
const getFileIcon = (filename: string) => {
const ext = filename.split(".").pop()?.toLowerCase();
const _ext = filename.split(".").pop()?.toLowerCase();
// Could add more specific icons here
return <File className="h-4 w-4 text-muted-foreground" />;
};

View File

@@ -2,7 +2,7 @@
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import { AlertTriangle, Braces, FileCode } from "lucide-react";
import { Braces, FileCode } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";

View File

@@ -101,12 +101,12 @@ export function CodeView({ content, language = "json", className, maxLines, font
</div>
)}
{viewMode === "tree" && isValidJson ? (
<JsonTreeViewWrapper
content={content}
className={className}
<JsonTreeViewWrapper
content={content}
className={className}
fontSize={fontSize}
onExpandAll={expandAllRef}
onCollapseAll={collapseAllRef}
onExpandAllRef={expandAllRef}
onCollapseAllRef={collapseAllRef}
/>
) : (
<pre suppressHydrationWarning className={cn("font-mono bg-muted rounded p-2", preview ? "overflow-hidden" : "overflow-y-auto max-h-[500px]", {

View File

@@ -2,10 +2,8 @@
import { useState, useMemo, useCallback, useEffect } from "react";
import type React from "react";
import { ChevronRight, ChevronDown, ChevronsDown, ChevronsUp } from "lucide-react";
import { ChevronRight, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { useTranslations } from "next-intl";
interface JsonNode {
key: string | null;
@@ -14,17 +12,46 @@ interface JsonNode {
path: string;
}
// Pure helper function - defined outside component to avoid recreation
const getNodeType = (value: unknown): JsonNode["type"] => {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
if (typeof value === "object") return "object";
return typeof value as "string" | "number" | "boolean";
};
// Recursive helper function - defined outside component
const collectExpandablePaths = (value: unknown, path: string, maxDepth: number, depth: number = 0): string[] => {
const paths: string[] = [];
const type = getNodeType(value);
if ((type === "object" || type === "array") && depth < maxDepth) {
paths.push(path);
if (type === "array") {
(value as unknown[]).forEach((item, index) => {
paths.push(...collectExpandablePaths(item, `${path}.${index}`, maxDepth, depth + 1));
});
} else {
Object.entries(value as Record<string, unknown>).forEach(([k, v]) => {
paths.push(...collectExpandablePaths(v, `${path}.${k}`, maxDepth, depth + 1));
});
}
}
return paths;
};
interface JsonTreeViewProps {
data: unknown;
className?: string;
fontSize?: "xs" | "sm" | "base";
maxDepth?: number;
onExpandAll?: React.MutableRefObject<(() => void) | undefined>;
onCollapseAll?: React.MutableRefObject<(() => void) | undefined>;
onExpandAllRef?: React.MutableRefObject<(() => void) | undefined>;
onCollapseAllRef?: React.MutableRefObject<(() => void) | undefined>;
}
function JsonTreeView({ data, className, fontSize = "xs", maxDepth = 10, onExpandAll, onCollapseAll }: JsonTreeViewProps) {
const t = useTranslations("common");
function JsonTreeView({ data, className, fontSize = "xs", maxDepth = 10, onExpandAllRef, onCollapseAllRef }: JsonTreeViewProps) {
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set(["root"]));
const togglePath = (path: string) => {
@@ -39,38 +66,9 @@ function JsonTreeView({ data, className, fontSize = "xs", maxDepth = 10, onExpan
});
};
const getNodeType = (value: unknown): JsonNode["type"] => {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
if (typeof value === "object") return "object";
return typeof value as "string" | "number" | "boolean";
};
// Collect all expandable paths recursively
const collectExpandablePaths = useCallback((value: unknown, path: string, depth: number = 0): string[] => {
const paths: string[] = [];
const type = getNodeType(value);
if ((type === "object" || type === "array") && depth < maxDepth) {
paths.push(path);
if (type === "array") {
(value as unknown[]).forEach((item, index) => {
paths.push(...collectExpandablePaths(item, `${path}.${index}`, depth + 1));
});
} else {
Object.entries(value as Record<string, unknown>).forEach(([k, v]) => {
paths.push(...collectExpandablePaths(v, `${path}.${k}`, depth + 1));
});
}
}
return paths;
}, [maxDepth, getNodeType]);
const allExpandablePaths = useMemo(() => {
return collectExpandablePaths(data, "root");
}, [data, collectExpandablePaths]);
return collectExpandablePaths(data, "root", maxDepth);
}, [data, maxDepth]);
const expandAll = useCallback(() => {
setExpandedPaths(new Set(allExpandablePaths));
@@ -109,7 +107,7 @@ function JsonTreeView({ data, className, fontSize = "xs", maxDepth = 10, onExpan
}
};
const renderNode = (node: JsonNode, depth: number = 0, isLast: boolean = true): React.ReactNode => {
const renderNode = (node: JsonNode, depth: number = 0, _isLast: boolean = true): React.ReactNode => {
const { key, value, type, path } = node;
const isExpanded = expandedPaths.has(path);
const isComplex = type === "object" || type === "array";
@@ -239,13 +237,13 @@ function JsonTreeView({ data, className, fontSize = "xs", maxDepth = 10, onExpan
// Expose expand/collapse functions via useEffect
useEffect(() => {
if (onExpandAll) {
onExpandAll.current = expandAll;
if (onExpandAllRef) {
onExpandAllRef.current = expandAll;
}
if (onCollapseAll) {
onCollapseAll.current = collapseAll;
if (onCollapseAllRef) {
onCollapseAllRef.current = collapseAll;
}
}, [expandAll, collapseAll, onExpandAll, onCollapseAll]);
}, [expandAll, collapseAll, onExpandAllRef, onCollapseAllRef]);
return (
<div
@@ -264,18 +262,18 @@ function JsonTreeView({ data, className, fontSize = "xs", maxDepth = 10, onExpan
);
}
export function JsonTreeViewWrapper({
content,
className,
export function JsonTreeViewWrapper({
content,
className,
fontSize = "xs",
onExpandAll,
onCollapseAll
}: {
content: string;
className?: string;
onExpandAllRef,
onCollapseAllRef
}: {
content: string;
className?: string;
fontSize?: "xs" | "sm" | "base";
onExpandAll?: React.MutableRefObject<(() => void) | undefined>;
onCollapseAll?: React.MutableRefObject<(() => void) | undefined>;
onExpandAllRef?: React.MutableRefObject<(() => void) | undefined>;
onCollapseAllRef?: React.MutableRefObject<(() => void) | undefined>;
}) {
const parsedData = useMemo(() => {
try {
@@ -294,12 +292,12 @@ export function JsonTreeViewWrapper({
}
return (
<JsonTreeView
data={parsedData}
className={className}
<JsonTreeView
data={parsedData}
className={className}
fontSize={fontSize}
onExpandAll={onExpandAll}
onCollapseAll={onCollapseAll}
onExpandAllRef={onExpandAllRef}
onCollapseAllRef={onCollapseAllRef}
/>
);
}

View File

@@ -3,7 +3,6 @@ import { PrismaAdapter } from "@auth/prisma-adapter";
import { db } from "@/lib/db";
import { getConfig } from "@/lib/config";
import { initializePlugins, getAuthPlugin } from "@/lib/plugins";
import type { User } from "@prisma/client";
import type { Adapter, AdapterUser } from "next-auth/adapters";
// Initialize plugins before use

View File

@@ -11,7 +11,7 @@ import { z } from "zod";
import { db } from "@/lib/db";
import { isValidApiKeyFormat } from "@/lib/api-key";
import { improvePrompt } from "@/lib/ai/improve-prompt";
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE, DEFAULT_SKILL_CONTENT } from "@/lib/skill-files";
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE } from "@/lib/skill-files";
interface AuthenticatedUser {
id: string;