From 9d0141538cbed8eeff1675443ff1852b02d3c8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fatih=20Kadir=20Ak=C4=B1n?= Date: Tue, 3 Feb 2026 13:12:56 +0300 Subject: [PATCH] chore(eslint): update eslint config and downgrade strict rules --- eslint.config.mjs | 28 +++++ .../prompts.chat/scripts/generate-docs.ts | 1 + .../src/cli/components/PromptList.tsx | 2 +- packages/prompts.chat/src/parser/index.ts | 2 +- .../kids/layout/level-content-wrapper.tsx | 65 +++++----- src/components/prompts/prompt-card.tsx | 6 +- src/components/prompts/prompt-connections.tsx | 1 + src/components/prompts/prompt-form.tsx | 12 +- src/components/prompts/related-prompts.tsx | 2 +- src/components/prompts/run-prompt-button.tsx | 1 + src/components/prompts/skill-diff-viewer.tsx | 2 +- src/components/prompts/skill-editor.tsx | 4 +- .../prompts/structured-format-warning.tsx | 2 +- src/components/ui/code-view.tsx | 10 +- src/components/ui/json-tree-view.tsx | 116 +++++++++--------- src/lib/auth/index.ts | 1 - src/pages/api/mcp.ts | 2 +- 17 files changed, 139 insertions(+), 118 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d1..8455f605 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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; diff --git a/packages/prompts.chat/scripts/generate-docs.ts b/packages/prompts.chat/scripts/generate-docs.ts index dcc08171..871356a8 100644 --- a/packages/prompts.chat/scripts/generate-docs.ts +++ b/packages/prompts.chat/scripts/generate-docs.ts @@ -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]; diff --git a/packages/prompts.chat/src/cli/components/PromptList.tsx b/packages/prompts.chat/src/cli/components/PromptList.tsx index f8227204..dfea156e 100644 --- a/packages/prompts.chat/src/cli/components/PromptList.tsx +++ b/packages/prompts.chat/src/cli/components/PromptList.tsx @@ -220,7 +220,7 @@ export function PromptList({ return ( Error: {error} - Press 'r' to retry, 'q' to quit + Press 'r' to retry, 'q' to quit ); } diff --git a/packages/prompts.chat/src/parser/index.ts b/packages/prompts.chat/src/parser/index.ts index e810bd80..0319768a 100644 --- a/packages/prompts.chat/src/parser/index.ts +++ b/packages/prompts.chat/src/parser/index.ts @@ -55,7 +55,7 @@ function parseSimpleYaml(content: string): Record { 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 = ''; diff --git a/src/components/kids/layout/level-content-wrapper.tsx b/src/components/kids/layout/level-content-wrapper.tsx index 91925bee..845cfbb1 100644 --- a/src/components/kids/layout/level-content-wrapper.tsx +++ b/src/components/kids/layout/level-content-wrapper.tsx @@ -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>({}); - + + // 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 = {}; @@ -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 (
{/* Content area */} diff --git a/src/components/prompts/prompt-card.tsx b/src/components/prompts/prompt-card.tsx index ecd777e2..bfd09e80 100644 --- a/src/components/prompts/prompt-card.tsx +++ b/src/components/prompts/prompt-card.tsx @@ -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(null); - const [isVisible, setIsVisible] = useState(false); + const [_isVisible, setIsVisible] = useState(false); // Autoplay video when visible in viewport useEffect(() => { diff --git a/src/components/prompts/prompt-connections.tsx b/src/components/prompts/prompt-connections.tsx index fc382c6a..b802f8ad 100644 --- a/src/components/prompts/prompt-connections.tsx +++ b/src/components/prompts/prompt-connections.tsx @@ -794,6 +794,7 @@ function FlowGraph({ nodes, edges, currentPromptId, currentUserId, isAdmin, onNo return (
+ {/* eslint-disable-next-line react-hooks/refs -- Container dimensions needed for tooltip positioning */} {hoveredNode && (() => { // Calculate position with viewport awareness const tooltipWidth = 320; diff --git a/src/components/prompts/prompt-form.tsx b/src/components/prompts/prompt-form.tsx index 999220cf..e27147d7 100644 --- a/src/components/prompts/prompt-form.tsx +++ b/src/components/prompts/prompt-form.tsx @@ -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>; @@ -1324,6 +1317,7 @@ export function PromptForm({ categories, tags, initialData, initialContributors
{/* Code output content */}
+ {/* eslint-disable-next-line react/jsx-no-comment-textnodes -- Intentional code preview text */}
// Code generated by skill...
export function handler() {'{'}
return "...";
diff --git a/src/components/prompts/related-prompts.tsx b/src/components/prompts/related-prompts.tsx index 7ed0e48a..f98c7f04 100644 --- a/src/components/prompts/related-prompts.tsx +++ b/src/components/prompts/related-prompts.tsx @@ -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"; diff --git a/src/components/prompts/run-prompt-button.tsx b/src/components/prompts/run-prompt-button.tsx index 2d9bdb62..63cf3e17 100644 --- a/src/components/prompts/run-prompt-button.tsx +++ b/src/components/prompts/run-prompt-button.tsx @@ -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); diff --git a/src/components/prompts/skill-diff-viewer.tsx b/src/components/prompts/skill-diff-viewer.tsx index c30299d9..59040cb1 100644 --- a/src/components/prompts/skill-diff-viewer.tsx +++ b/src/components/prompts/skill-diff-viewer.tsx @@ -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"; diff --git a/src/components/prompts/skill-editor.tsx b/src/components/prompts/skill-editor.tsx index 882cd0c9..40e16968 100644 --- a/src/components/prompts/skill-editor.tsx +++ b/src/components/prompts/skill-editor.tsx @@ -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 ; }; diff --git a/src/components/prompts/structured-format-warning.tsx b/src/components/prompts/structured-format-warning.tsx index 04274484..4a0e1ab6 100644 --- a/src/components/prompts/structured-format-warning.tsx +++ b/src/components/prompts/structured-format-warning.tsx @@ -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"; diff --git a/src/components/ui/code-view.tsx b/src/components/ui/code-view.tsx index 8808c7bf..af849a06 100644 --- a/src/components/ui/code-view.tsx +++ b/src/components/ui/code-view.tsx @@ -101,12 +101,12 @@ export function CodeView({ content, language = "json", className, maxLines, font
)} {viewMode === "tree" && isValidJson ? ( - ) : (
 {
+  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).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>(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).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 (
     
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 ( - ); } diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index a0041979..d7d8c58f 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -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 diff --git a/src/pages/api/mcp.ts b/src/pages/api/mcp.ts index bb16d0fa..5c3c4dca 100644 --- a/src/pages/api/mcp.ts +++ b/src/pages/api/mcp.ts @@ -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;