From 1354283b20ac401d50d87280913228293765d803 Mon Sep 17 00:00:00 2001 From: Simo Date: Thu, 30 Oct 2025 11:40:01 +0200 Subject: [PATCH 1/7] wip Signed-off-by: Simo --- AGENT-REFERENCE.md | 212 +++++++++++++++++++++++++++++++++++++ AGENTS.md | 228 +++++++++++++++++++++++++++++++--------- scripts/lint-to-json.js | 63 +++++++++++ 3 files changed, 453 insertions(+), 50 deletions(-) create mode 100644 AGENT-REFERENCE.md create mode 100644 scripts/lint-to-json.js diff --git a/AGENT-REFERENCE.md b/AGENT-REFERENCE.md new file mode 100644 index 0000000000..b38702bce6 --- /dev/null +++ b/AGENT-REFERENCE.md @@ -0,0 +1,212 @@ +````markdown +# Lint-First Refactor Guide (Next.js 16 + React 19.2) + +This document is the single source of truth for how we fix ESLint issues and make **small, safe** improvements while adopting React 19.2 and Next.js 16 features. + +--- + +## Principles + +- **Preserve behavior.** Changes must not alter UI or API outputs. +- **Prefer deletion to indirection.** Remove unused code first. +- **Minimize Effects.** If something can be expressed as derived data or an event handler, do that instead. +- **Server-first.** Default to Server Components; opt into Client Components only when browser APIs or interactivity require it. +- **Document intent.** When you suppress a rule, add a one-line reason. + +--- + +## React 19.2: What we use + +- **Effect Events (`useEffectEvent`)** — Extract event-like logic out of `useEffect` so the effect’s dependencies stay minimal. Lint v6 understands that Effect Events are not dependencies. + Docs: https://react.dev/reference/react/useEffectEvent + Release notes: https://react.dev/blog/2025/10/01/react-19-2 ← see sections on `useEffectEvent` and `eslint-plugin-react-hooks v6`. + +- **“You might not need an Effect”** — Most state derivations and event responses should not be in Effects. + Guide: https://react.dev/learn/you-might-not-need-an-effect + +- **Server rendering + caching primitives** — `cache()` and `cacheSignal()` may appear in RSC utilities. Use sparingly and only where obvious. + `cacheSignal`: https://react.dev/reference/react/cacheSignal + +**Notes** +- When converting code to `useEffectEvent`, call the event only from within Effects. Don’t pass it as a prop or store it; treat it like an effect-local event. See the release post for constraints. + https://react.dev/blog/2025/10/01/react-19-2 + +--- + +## Next.js 16: What we use + +- **Cache Components & `"use cache"`** (opt‑in) + - Enable in `next.config.ts`: `cacheComponents: true`. + - Apply `"use cache"` to routes/components/functions that return stable, serializable results safe to cache. + - Prefer **no change** unless the gain is obvious and inputs are pure. + Blog: https://nextjs.org/blog/next-16 + Directive docs: https://nextjs.org/docs/app/api-reference/directives/use-cache + Config: https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents + +- **Next.js Devtools MCP** (required tool) + - We have `next-devtools-mcp` installed. Use it for project metadata, page rendering info, unified logs, and error surfacing. + Guide: https://nextjs.org/docs/app/guides/mcp + +- **Server & Client Components** + - Default to Server Components; use `"use client"` only for stateful interactive UI or browser-only APIs. + Docs: https://nextjs.org/docs/app/getting-started/server-and-client-components + +**Optional (case-by-case)** +- Updated `revalidateTag` and `updateTag` APIs exist; do not change unless the file already uses these. See release blog for details: https://nextjs.org/blog/next-16 + +--- + +## Playbook: Fixing Common ESLint Issues + +### 1) `unused-imports/no-unused-vars` +- **Remove** the symbol if truly unused. +- If it’s a placeholder or doc-only type, **rename with a leading underscore** (e.g., `_WarpcastFrameResponse`) to satisfy our rule. +- For public exported types used elsewhere, **do not rename**. Keep the export and add: + ```ts + // eslint-disable-next-line unused-imports/no-unused-vars -- exported API, referenced outside this file + export type AllowlistWalletPool = { ... } +```` + +* Prefer `import type {...}` and `export type` for type-only symbols. + +### 2) `react-hooks/exhaustive-deps` + +**Step A: Remove unnecessary Effects** + +* If the Effect only derives state from props/state, compute it during render or with `useMemo`. + +**Step B: Extract event-like logic with `useEffectEvent`** + +* **Before** + + ```ts + useEffect(() => { + socket.on('connected', () => notify(theme)); + return () => socket.off('connected'); + }, [roomId]) // linter asks for `theme` + ``` +* **After** + + ```ts + const onConnected = useEffectEvent(() => notify(theme)); + useEffect(() => { + socket.on('connected', onConnected); + return () => socket.off('connected', onConnected); + }, [roomId]); // correct and stable + ``` +* Add real reactive inputs (IDs, query, flags) to the dependencies. Event payload values belong in the Effect Event. + Docs: [https://react.dev/reference/react/useEffectEvent](https://react.dev/reference/react/useEffectEvent) + +**Step C: Router/search params patterns** + +* Prefer server-side defaults (redirects in Server Components or default params) instead of client `useEffect` to push/replace. +* If you must react to URL changes client-side, it’s fine for the Effect to depend on `searchParams`. Keep navigation calls inside an Effect Event to avoid dependency bloat. + +--- + +## When to consider `"use cache"` (Cache Components) + +Add **only** when all are true: + +* The code runs on the server and returns **pure, serializable** data. +* Cache keys are clear from the inputs (no hidden header/cookie dependence). +* There is a real win (expensive read, stable result). + +**Examples** + +* Route handler assembling a public, static OG image response: OK to cache if inputs are explicit. +* Server utility fetching immutable docs by ID: OK to cache at function level. + +**How** + +```ts +// At file or function top: +'use cache' + +export async function getStableThing(id: string) { ... } +``` + +Docs: [https://nextjs.org/docs/app/api-reference/directives/use-cache](https://nextjs.org/docs/app/api-reference/directives/use-cache) + +Avoid `"use cache: private"` and `"use cache: remote"` unless you clearly match their semantics. Docs: + +* Private: [https://nextjs.org/docs/app/api-reference/directives/use-cache-private](https://nextjs.org/docs/app/api-reference/directives/use-cache-private) +* Remote: [https://nextjs.org/docs/app/api-reference/directives/use-cache-remote](https://nextjs.org/docs/app/api-reference/directives/use-cache-remote) + +--- + +## Live Testing Checklist (per file) + +1. **Dev server**: `npm run dev`. +2. **Next Devtools MCP**: + + * `get_errors`: ensure zero hydration/runtime errors. + * `get_page_metadata`: confirm which route(s) use the changed file and whether they render as Server/Client. + * `get_logs`: scan for warnings after interacting with the page. +3. **Manual route check**: open the affected URL(s), hard refresh, interact with UI paths that exercise the changes. +4. **Effects**: if you changed an Effect, test the dependency that previously caused re-runs (e.g., change theme, switch roomId) to confirm no reconnection loop. +5. **Caching (if added)**: reload twice and verify stable behavior; look for improved render time or logged cache hits. + +--- + +## Examples tailored to our repo warnings + +> Use these patterns when the agent receives the file + lint lines below. + +* `app/api/farcaster/route.ts` — `'WarpcastFrameResponse' is defined but never used` + + * Remove type or rename to `_WarpcastFrameResponse` if intentionally kept. If it’s part of a public API consumed elsewhere, keep the name and add a one-line rule suppression with a reason. + +* `components/6529Gradient/6529Gradient.tsx` — missing deps on `searchParams`, `router`, `nftsRaw` + + * First check if the logic can be derived or moved to an event handler (no Effect). + * If subscribing or navigating on mount, wrap notification/navigation in `useEffectEvent` and keep the Effect deps minimal and correct (e.g., `[criticalId]`). See `useEffectEvent` guidance above. + +* `components/about/AboutPrimaryAddress.tsx` — missing dep `populateData` + + * If `populateData` is an event-like function (e.g., fetch + setState), convert it to an Effect Event and reference it from the Effect; keep deps to the wiring inputs. + +* `allowlist-tool.types.ts` — several unused exported types + + * Check for cross-file references. If none, delete or underscore-prefix. If used, keep exports and add targeted inline disables with a short reason. + +--- + +## Links (authoritative) + +* **React 19.2 Release** (Activity, useEffectEvent, cacheSignal, hooks lint v6): + [https://react.dev/blog/2025/10/01/react-19-2](https://react.dev/blog/2025/10/01/react-19-2) + +* **Effect Events**: + [https://react.dev/reference/react/useEffectEvent](https://react.dev/reference/react/useEffectEvent) + +* **You might not need an Effect**: + [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect) + +* **cacheSignal**: + [https://react.dev/reference/react/cacheSignal](https://react.dev/reference/react/cacheSignal) + +* **Next.js 16 Release (Cache Components, Devtools MCP)**: + [https://nextjs.org/blog/next-16](https://nextjs.org/blog/next-16) + +* **Next.js MCP**: + [https://nextjs.org/docs/app/guides/mcp](https://nextjs.org/docs/app/guides/mcp) + +* **Cache Components & Directives**: + [https://nextjs.org/docs/app/api-reference/directives/use-cache](https://nextjs.org/docs/app/api-reference/directives/use-cache) + [https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) + +* **Server & Client Components**: + [https://nextjs.org/docs/app/getting-started/server-and-client-components](https://nextjs.org/docs/app/getting-started/server-and-client-components) + +``` + +--- + +- React 19.2 introduces **`useEffectEvent`** to split event-like logic from Effects; the **hooks linter v6** understands that Effect Events are not dependencies. This prevents unnecessary reconnections/re-subscriptions when unrelated values (like theme) change. :contentReference[oaicite:1]{index=1} +- The **“You might not need an Effect”** doc emphasizes removing Effects used for pure derivations or event handling to reduce bugs and improve performance. :contentReference[oaicite:2]{index=2} +- Next.js 16 ships **Cache Components** (opt-in) centered on the `"use cache"` directive; enable via `cacheComponents: true`. Use only when safe and inputs are serializable. :contentReference[oaicite:3]{index=3} +- **Next.js Devtools MCP** exposes project/runtime insights to AI agents (errors, logs, page metadata, server actions). It’s designed for exactly this workflow. :contentReference[oaicite:4]{index=4} +``` + +[1]: https://react.dev/blog/2025/10/01/react-19-2 "React 19.2 – React" diff --git a/AGENTS.md b/AGENTS.md index 64ef479b2d..00d75a5d79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,34 +1,46 @@ -# AGENTS.md – Codex Configuration for Next.js Frontend +Awesome—let’s turn your **AGENTS.md** into a “single source of truth” that bakes in Next.js 16, React 19.2 (including `useEffectEvent`), Next DevTools MCP, and your modernization-first lint policy. Below is a **drop‑in replacement** you can paste over your current file. -## Setup +--- + +# AGENTS.md – Agent Playbook for Next.js Frontend + +This document is the contract for any coding agent (e.g. codex‑cli) working in this repo. It describes **how to run checks**, **what “good” looks like**, and the **modernization rules** tied to our stack (Next.js 16 + React 19.2). + +--- + +## Quickstart + +### Setup ```bash npm install ``` -## Test +### Test ```bash npm run test ``` -## Lint & Format +### Lint & Format ```bash npm run lint ``` -## Type Check +### Type Check ```bash npm run type-check ``` -## Programmatic Checks +> **Note on Next.js 16 & ESLint:** Starting with Next 16, `next lint` is removed. Use the ESLint CLI driven by `eslint-config-next` (flat config). Remove any `eslint` options from `next.config.*`. ([Next.js][1]) + +--- + +## Programmatic Checks (must pass before completing any task) -Before completing any coding task, ensure the following commands succeed. For a -quicker check when your changes are small, you may run -`npm run test:cov:changed` to execute tests only for files changed since `main`: +Run all of the following (unless you are only editing docs or non-code, in which case tests may be skipped): ```bash npm run test @@ -36,44 +48,120 @@ npm run lint npm run type-check ``` -Note: When changing files like readme.md, agents.md or other documentation or in general tasks that aren't supposed to have tests, then there's no need to run tests for that task. +For small changes, you may run a faster coverage subset: -- `npm run test`: Executes all Jest tests and checks code coverage. The command will fail if: - - Any Jest test fails. - - Any file modified since diverging from the `main` branch (including uncommitted changes) has less than 80% line coverage. -- `npm run test:cov:changed`: Runs Jest only on files changed since `main`, still enforcing the 80% coverage threshold. -- `npm run lint`: Ensure code adheres to linting rules. -- `npm run type-check`: Verify TypeScript type checking passes with `tsc --noEmit -p tsconfig.json`. +```bash +npm run test:cov:changed +``` -If `npm run test` fails due to low coverage on a modified file, write meaningful tests that verify the file's functionality and bring its coverage to at least 80%. If a Jest test fails, debug and fix the underlying code or the test itself, ensuring the code behaves as expected and tests accurately reflect its intended functionality. Repeat this process until `npm run test` passes. +* `npm run test`: Executes all Jest tests and enforces **≥ 80% line coverage for files changed since `main`**. Fails if tests fail or coverage threshold is not met. +* `npm run test:cov:changed`: Jest on changed files only; still enforces the 80% threshold. +* `npm run lint`: Code must satisfy ESLint (Next’s Core Web Vitals + React Hooks). +* `npm run type-check`: Must pass `tsc --noEmit`. -## Codex Workspace +If tests fail due to coverage, write meaningful tests until coverage ≥ 80%. If a test fails functionally, fix root cause (code or test) and re‑run until green. -Use the `/codex/` directory as the shared source of truth for planning and ticket execution. +--- -- Start every workstream by reviewing `codex/STATE.md` and keeping its ticket rows in sync with the matching files under `codex/tickets/`. -- Author new tickets with the provided template, maintain alphabetical YAML front matter, and log timestamped updates as work progresses. -- Capture broader planning artefacts in `codex/plans/` and evergreen documentation in `codex/docs/`, following the conventions spelled out in `codex/agents.md` and `codex/docs/README.md`. -- Link pull requests back to their tickets and mirror merged PR references in both the ticket log and `STATE.md` so the board stays auditable. -- Never edit tickets marked **Done**; open a fresh ticket if new scope emerges. +## MCP: Enable Next.js DevTools for Agents (highly recommended) -## Coding Conventions +Enable the **Next DevTools MCP server** so agents can query live routes, errors, logs, and Server Actions from a running `next dev`: + +```jsonc +// .mcp.json (project root) +{ + "mcpServers": { + "next-devtools": { + "command": "npx", + "args": ["-y", "next-devtools-mcp@latest"] + } + } +} +``` + +* When `next dev` is running, `next-devtools-mcp` auto‑discovers and connects to the app. +* Available tools include: `get_errors`, `get_logs`, `get_page_metadata`, `get_project_metadata`, and `get_server_action_by_id`. Agents should use them **before** changing code when fixes might affect routing, hydration, or Server Actions. ([Next.js][2]) + +--- + +## Agent Operating Principles + +1. **Fix with modernization** (no “make the warning go away”). Don’t add `// eslint-disable` unless explicitly instructed. Prefer refactors aligned with **React 19.2**, **React Compiler**, and **Next.js 16** conventions. ([React][3]) +2. **Prefer Server over Client** where possible. Data reads: Server Components with inline fetches. Mutations: Server Functions / Server Actions (`'use server'`). Avoid client Effects for data fetching unless truly needed. ([Next.js][4]) +3. **Effects are last resort.** If there’s no external system, remove the Effect and compute during render. If you must listen to external events but need the latest props/state without re‑running the Effect, use **`useEffectEvent`**. ([React][5]) +4. **Use framework APIs:** internal links → ``, images → `next/image`, and adopt Next’s ESLint rules (Core Web Vitals). ([Next.js][1]) +5. **Cache explicitly where it helps.** With Next 16, caching is **opt‑in** via the `"use cache"` directive and related Cache Components features (see below). ([Next.js][6]) +6. **Commit small, surgical diffs.** If you uncover a broader refactor, open a follow‑up ticket rather than ballooning a lint‑fix PR. + +--- + +## Next.js 16: What this means for agents + +* **Proxy instead of Middleware:** `middleware.ts` is **renamed to** `proxy.ts` (Node runtime). If you touch request‑boundary logic, ensure the file and exported function are named `proxy`. Legacy `middleware.ts` still exists for edge‑only cases but our default is `proxy.ts`. ([Next.js][6]) +* **ESLint changes:** `next lint` removed; use ESLint CLI with `eslint-config-next` flat config. ([Next.js][1]) +* **React Compiler (stable):** Supported in Next 16. It **auto‑memoizes components**, reducing the need for manual `useMemo`/`useCallback`. You may see lints originating from the compiler surfaced via `eslint-plugin-react-hooks`. Consider enabling the compiler in `next.config.ts` when CI is green: + + ```ts + // next.config.ts + export default { reactCompiler: true } + ``` + + ([Next.js][6]) +* **Cache Components / `"use cache"`:** Caching is explicit. You can place `"use cache"` at the top of a Server Component, route, or function to opt‑in caching; configure `cacheComponents: true` in `next.config.ts` as needed. Prefer tagging/expiration APIs over ad‑hoc hacks. ([Next.js][7]) +* **Turbopack default:** Dev and build use Turbopack by default in v16—don’t pass `--turbopack`. ([Next.js][8]) + +--- + +## React 19.2: Effects guidance for agents + +* **Remove unnecessary Effects.** If the Effect’s only job is to derive or sync internal state, calculate during render or use `useMemo` if truly expensive (the **Compiler** may remove that need). ([React][5]) +* **Use `useEffectEvent`** for non‑reactive logic inside Effects so you can read the latest props/state without turning them into dependencies or causing needless re‑runs. Keep the Effect’s dependency array minimal and stable. ([React][3]) +* **Let lints guide you.** `eslint-plugin-react-hooks` v6+ ships flat-config presets and **compiler‑powered** rules; don’t suppress—refactor. ([React][9]) -- Use TypeScript and React functional components with hooks. -- Follow existing code style and naming conventions. -- Adhere to clean code standards as measured by SonarQube. -- Place tests in `__tests__` directories or alongside components as `ComponentName.test.tsx`. -- Mock external dependencies and APIs in tests. -- When parsing Seize URLs or similar app-specific links, do not fall back to placeholder origins (e.g., `https://example.com`); fail fast if the configured base origin is unavailable. +**Example (pattern to prefer):** + +```tsx +// BEFORE: re-runs on theme change, reconnects unnecessarily +useEffect(() => { + const c = connect(roomId) + c.on('connected', () => showToast('Connected!', theme)) + return () => c.disconnect() +}, [roomId, theme]) + +// AFTER: stable effect with an Effect Event +import { useEffectEvent } from 'react' +const onConnected = useEffectEvent(() => showToast('Connected!', theme)) +useEffect(() => { + const c = connect(roomId) + c.on('connected', onConnected) + return () => c.disconnect() +}, [roomId]) +``` + +--- + +## Lint Rules → Modern Fixes (cheat‑sheet) + +* **`react-hooks/exhaustive-deps`** + + * If the Effect only derives state → **remove the Effect** and compute during render. + * If listening to an external system and you need fresh props/state → wrap non‑reactive logic in **`useEffectEvent`**. ([React][5]) + +* **`@next/next/no-img-element`** → replace `` with `` from `next/image`. ([Next.js][1]) + +* **`@next/next/no-html-link-for-pages`** → use `` for internal navigation. ([Next.js][1]) + +* **Data fetching in client Effects** → move reads to **Server Components**; mutations go through **Server Functions / Server Actions** (`'use server'`). ([Next.js][4]) + +* **Request boundary logic** touching legacy `middleware.ts` → **rename to `proxy.ts`** and export `proxy`. ([Next.js][6]) + +--- ## Next.js Directory Structure -All production routes now live under the Next.js `app/` router. -The legacy `pages/` directory has been fully migrated, so add any new routes -under `app/`. +All production routes live under the App Router (`app/`). Add new routes there. -Routes in `app/` should export a `generateMetadata` function using the helper -`getAppMetadata`: +Routes in `app/` should export `generateMetadata` using our helper: ```ts import { getAppMetadata } from "@/components/providers/metadata"; @@ -84,22 +172,62 @@ export async function generateMetadata(): Promise { } ``` +If you add or modify `proxy.ts`, keep it at the root (or `src/`) alongside `app/`/`pages/` and export `proxy`. ([Next.js][10]) + +--- + +## Coding Conventions + +* TypeScript + React functional components with hooks. +* Follow existing code style and naming conventions. +* Maintain clean code standards (measured by SonarQube). +* Tests live in `__tests__/` or `ComponentName.test.tsx`. +* Mock external dependencies and APIs in tests. +* When parsing Seize URLs (or similar), **do not** fall back to placeholder origins; fail fast if base origin is unavailable. + +--- + +## Codex Workspace + +Use the `/codex/` directory as the source of truth for planning and ticket execution. + +* Review `codex/STATE.md` at the start of every workstream; keep tickets in sync with `codex/tickets/`. +* Author new tickets with the provided template, keep alphabetical YAML front matter, and log timestamped updates. +* Put broader plans in `codex/plans/` and evergreen docs in `codex/docs/`, following `codex/agents.md` and `codex/docs/README.md`. +* Link PRs back to tickets and mirror merged PRs in both the ticket log and `STATE.md`. +* Never edit tickets marked **Done**; open a new ticket for new scope. + +--- + ## Commit Guidelines -- Follow [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`) -- Do not squash commits; maintain a clear history -- Each commit should represent a single logical change -- Add a Developer Certificate of Origin (DCO) signature to every commit message footer: +* Use **Conventional Commits** (`feat:`, `fix:`, etc.). +* **Do not squash**; keep a clear history. +* One logical change per commit. +* **DCO required** on every commit: + + ``` + Signed-off-by: Your Full Name + ``` + +--- + +### Why the policy - - **Must use your real GitHub-connected identity** (no generic/placeholder emails like `codex@openai.com`) - - Format: - ``` - Signed-off-by: Your Full Name - ``` - - Example: +* **Next DevTools MCP** gives agents live, app‑specific context (routes, errors, actions) for accurate fixes. ([Next.js][2]) +* **React 19.2 + Hooks v6** align lints with modern patterns, including `useEffectEvent` and compiler‑powered guidance. ([React][3]) +* **Next 16** introduces explicit caching (`"use cache"`), a clearer network boundary (`proxy.ts`), and stable React Compiler support, so “lint fixes” often become meaningful improvements. ([Next.js][6]) - ``` - feat: add user authentication middleware - Signed-off-by: Jane Developer <12345+jane-dev@users.noreply.github.com> - ``` +[1]: https://nextjs.org/docs/app/api-reference/config/eslint "Configuration: ESLint | Next.js" +[2]: https://nextjs.org/docs/app/guides/mcp "Guides: Next.js MCP Server | Next.js" +[3]: https://react.dev/reference/react/experimental_useEffectEvent "useEffectEvent – React" +[4]: https://nextjs.org/docs/app/getting-started/server-and-client-components?utm_source=chatgpt.com "Getting Started: Server and Client Components" +[5]: https://react.dev/learn/you-might-not-need-an-effect "You Might Not Need an Effect – React" +[6]: https://nextjs.org/blog/next-16?utm_source=chatgpt.com "Next.js 16" +[7]: https://nextjs.org/docs/app/api-reference/directives/use-cache?utm_source=chatgpt.com "Directives: use cache" +[8]: https://nextjs.org/docs/app/guides/upgrading/version-16?utm_source=chatgpt.com "Upgrading: Version 16" +[9]: https://react.dev/blog/2025/10/01/react-19-2?utm_source=chatgpt.com "React 19.2" +[10]: https://nextjs.org/docs/app/getting-started/proxy?utm_source=chatgpt.com "Getting Started: Proxy" +[11]: https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler?utm_source=chatgpt.com "next.config.js: reactCompiler" +[12]: https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents?utm_source=chatgpt.com "next.config.js: cacheComponents" diff --git a/scripts/lint-to-json.js b/scripts/lint-to-json.js new file mode 100644 index 0000000000..e7ccec2e6c --- /dev/null +++ b/scripts/lint-to-json.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * Transform lint.txt output into lint.json summary. + * + * lint.json will be an array of objects with: + * - key: absolute path to the file. + * - item: combined warning lines exactly as in lint.txt. + * - count: number of warnings captured for that path. + * + * Usage: node scripts/lint-to-json.js [inputPath] [outputPath] + * Defaults: lint.txt -> lint.json relative to repo root. + */ +import fs from "node:fs"; +import path from "node:path"; + +const [inputArg, outputArg] = process.argv.slice(2); +const inputPath = inputArg ?? path.resolve(process.cwd(), "lint.txt"); +const outputPath = outputArg ?? path.resolve(process.cwd(), "lint.json"); + +const fileContents = fs.readFileSync(inputPath, "utf8"); +const lines = fileContents.split(/\r?\n/); + +const entries = []; +let currentPath = null; +let currentWarnings = []; + +const flushCurrent = () => { + if (!currentPath) return; + + const warningText = currentWarnings.join("\n\n"); + entries.push({ + key: currentPath, + item: warningText, + prompt: null, + }); + + currentPath = null; + currentWarnings = []; +}; + +for (const line of lines) { + if (!line.trim()) { + continue; + } + + if (/^\/.+/.test(line)) { + flushCurrent(); + currentPath = line.trim(); + continue; + } + + if (currentPath && line.startsWith(" ")) { + currentWarnings.push(line); + continue; + } + + // Non-path, non-warning lines (tool noise) are ignored. +} + +flushCurrent(); + +const jsonOutput = `${JSON.stringify(entries, null, 2)}\n`; +fs.writeFileSync(outputPath, jsonOutput, "utf8"); From 9c683273b62002284774db2f7270b1a87a8fbfd8 Mon Sep 17 00:00:00 2001 From: Simo Date: Thu, 30 Oct 2025 13:25:03 +0200 Subject: [PATCH 2/7] wip Signed-off-by: Simo --- app/api/farcaster/route.ts | 8 -- app/api/open-graph/compound/service.ts | 2 +- app/api/open-graph/utils.ts | 2 +- app/api/wikimedia-card/route.ts | 9 +- app/nextgen/[[...view]]/NextGenPageClient.tsx | 2 +- .../[app-wallet-address]/page.client.tsx | 2 +- .../app-wallets/import-wallet/page.client.tsx | 2 +- components/6529Gradient/6529Gradient.tsx | 52 +++++++++--- components/about/AboutPrimaryAddress.tsx | 85 ++++++++++--------- .../allowlist-tool/allowlist-tool.types.ts | 64 -------------- components/app-wallets/AppWallet.tsx | 4 +- components/app-wallets/AppWalletModal.tsx | 57 +++++++------ components/app-wallets/AppWalletsContext.tsx | 37 ++++++-- scripts/lint-to-json.js | 18 ++-- 14 files changed, 169 insertions(+), 175 deletions(-) diff --git a/app/api/farcaster/route.ts b/app/api/farcaster/route.ts index bdbd04a812..b557a1ffb4 100644 --- a/app/api/farcaster/route.ts +++ b/app/api/farcaster/route.ts @@ -242,14 +242,6 @@ type WarpcastChannelResponse = { }; }; -type WarpcastFrameResponse = { - readonly result?: { - readonly frame?: { - readonly url?: string; - }; - }; -}; - const mapWarpcastUser = ( data: WarpcastUserResponse | null, canonicalUrl: string diff --git a/app/api/open-graph/compound/service.ts b/app/api/open-graph/compound/service.ts index 6861db45f2..c055e7df0b 100644 --- a/app/api/open-graph/compound/service.ts +++ b/app/api/open-graph/compound/service.ts @@ -798,7 +798,7 @@ async function fetchV3Account(address: Address): Promise<{ return { v3Positions: v3Positions .filter((position) => position.hasPosition) - .map(({ hasPosition, ...rest }) => rest), + .map(({ hasPosition: _ignored, ...rest }) => rest), }; } diff --git a/app/api/open-graph/utils.ts b/app/api/open-graph/utils.ts index b5ffbfac64..94d833a67f 100644 --- a/app/api/open-graph/utils.ts +++ b/app/api/open-graph/utils.ts @@ -674,7 +674,7 @@ export function buildResponse( url: URL, html: string, contentType: string | null, - finalUrl?: string + _finalUrl?: string ): LinkPreviewResponse { diff --git a/app/api/wikimedia-card/route.ts b/app/api/wikimedia-card/route.ts index 7c5054a775..c0aa7a22bf 100644 --- a/app/api/wikimedia-card/route.ts +++ b/app/api/wikimedia-card/route.ts @@ -617,10 +617,7 @@ const buildSummaryCard = async ( } }; -const buildCommonsCard = async ( - target: CommonsFileTarget, - languages: readonly string[] -): Promise => { +const buildCommonsCard = async (target: CommonsFileTarget): Promise => { const fileTitle = target.fileName.startsWith("File:") ? target.fileName : `File:${target.fileName}`; const canonicalUrl = `https://commons.wikimedia.org/wiki/${encodeURIComponent(fileTitle)}`; const pageUrl = target.fragment ? `${canonicalUrl}#${target.fragment.raw}` : canonicalUrl; @@ -917,7 +914,7 @@ const buildWikidataCard = async ( const imageClaim = claims["P18"]?.[0]?.mainsnak?.datavalue; if (imageClaim && imageClaim.type === "string" && typeof imageClaim.value === "string") { const fileName = imageClaim.value.startsWith("File:") ? imageClaim.value : `File:${imageClaim.value}`; - const commonsData = await buildCommonsCard({ type: "commons-file", fileName }, languages); + const commonsData = await buildCommonsCard({ type: "commons-file", fileName }); if (commonsData.kind === "commons-file" && commonsData.thumbnail) { image = commonsData.thumbnail; } @@ -971,7 +968,7 @@ const buildCard = async ( case "summary": return buildSummaryCard(target, languages); case "commons-file": - return buildCommonsCard(target, languages); + return buildCommonsCard(target); case "wikidata": return buildWikidataCard(target, languages); } diff --git a/app/nextgen/[[...view]]/NextGenPageClient.tsx b/app/nextgen/[[...view]]/NextGenPageClient.tsx index d105054eb2..c9b8704919 100644 --- a/app/nextgen/[[...view]]/NextGenPageClient.tsx +++ b/app/nextgen/[[...view]]/NextGenPageClient.tsx @@ -28,7 +28,7 @@ export default function NextGenPageClient({ useEffect(() => { setTitle("NextGen " + (view ?? "")); - }, [view]); + }, [setTitle, view]); const updateView = (newView?: NextgenView) => { setView(newView); diff --git a/app/tools/app-wallets/[app-wallet-address]/page.client.tsx b/app/tools/app-wallets/[app-wallet-address]/page.client.tsx index c07caeb26c..19e475eaed 100644 --- a/app/tools/app-wallets/[app-wallet-address]/page.client.tsx +++ b/app/tools/app-wallets/[app-wallet-address]/page.client.tsx @@ -13,7 +13,7 @@ export default function AppWalletPage(props: { readonly address: string }) { useEffect(() => { setTitle(`${formatAddress(address)} | App Wallets | 6529.io`); - }, [setTitle]); + }, [address, setTitle]); return (
diff --git a/app/tools/app-wallets/import-wallet/page.client.tsx b/app/tools/app-wallets/import-wallet/page.client.tsx index 03f97872d0..48d2d53f08 100644 --- a/app/tools/app-wallets/import-wallet/page.client.tsx +++ b/app/tools/app-wallets/import-wallet/page.client.tsx @@ -4,7 +4,7 @@ import AppWalletImport from "@/components/app-wallets/AppWalletImport"; import { useSetTitle } from "@/contexts/TitleContext"; import styles from "@/styles/Home.module.scss"; -export default function AppWalletImportPage(props: any) { +export default function AppWalletImportPage() { useSetTitle("Import App Wallet | Tools"); return ( diff --git a/components/6529Gradient/6529Gradient.tsx b/components/6529Gradient/6529Gradient.tsx index b7706dc811..3169eca923 100644 --- a/components/6529Gradient/6529Gradient.tsx +++ b/components/6529Gradient/6529Gradient.tsx @@ -51,19 +51,47 @@ export default function GradientsComponent() { const [sort, setSort] = useState(Sort.ID); useEffect(() => { - const sortParam = (searchParams?.get("sort") as Sort) || Sort.ID; - const dirParam = - (searchParams?.get("sort_dir")?.toUpperCase() as SortDirection) || - SortDirection.ASC; + const rawSort = searchParams.get("sort")?.toLowerCase() as Sort | undefined; + const nextSort = rawSort === Sort.TDH ? Sort.TDH : Sort.ID; - setSort(sortParam); - setSortDir(dirParam); + const rawSortDir = searchParams + .get("sort_dir") + ?.toUpperCase() as SortDirection | undefined; + const nextSortDir = + rawSortDir === SortDirection.DESC ? SortDirection.DESC : SortDirection.ASC; + + setSort((current) => (current === nextSort ? current : nextSort)); + setSortDir((current) => + current === nextSortDir ? current : nextSortDir + ); + }, [searchParams]); + + useEffect(() => { + let isMounted = true; const url = `${publicEnv.API_ENDPOINT}/api/nfts/gradients?page_size=101`; - fetchAllPages(url).then((raw: GradientNFT[]) => { - setNftsRaw(raw); - setNftsLoaded(true); - }); + fetchAllPages(url) + .then((raw: GradientNFT[]) => { + if (!isMounted) { + return; + } + setNftsRaw(raw); + }) + .catch(() => { + if (!isMounted) { + return; + } + setNftsRaw([]); + }) + .finally(() => { + if (isMounted) { + setNftsLoaded(true); + } + }); + + return () => { + isMounted = false; + }; }, []); useEffect(() => { @@ -74,7 +102,7 @@ export default function GradientsComponent() { router.replace(`/6529-gradient?${params.toString()}`, { scroll: false, }); - }, [sort, sortDir]); + }, [router, sort, sortDir]); useEffect(() => { if (!nftsLoaded) return; @@ -94,7 +122,7 @@ export default function GradientsComponent() { } setNfts(sorted); - }, [sort, sortDir, nftsLoaded]); + }, [nftsLoaded, nftsRaw, sort, sortDir]); function printNft(nft: GradientNFT) { return ( diff --git a/components/about/AboutPrimaryAddress.tsx b/components/about/AboutPrimaryAddress.tsx index 3498d943aa..e2ab1e13c9 100644 --- a/components/about/AboutPrimaryAddress.tsx +++ b/components/about/AboutPrimaryAddress.tsx @@ -1,7 +1,7 @@ "use client"; import csvParser from "csv-parser"; import Link from "next/link"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Col, Container, Row } from "react-bootstrap"; interface PrimaryAddressData { @@ -20,6 +20,47 @@ export default function AboutPrimaryAddress() { verticalAlign: "middle", }; + const populateData = useCallback( + (body: Blob) => { + const reader = new FileReader(); + + reader.onload = () => { + const data = reader.result; + const results: PrimaryAddressData[] = []; + + if (!data) { + return; + } + + const parser = csvParser({ headers: false }) + .on("data", (row: any) => { + const r = { + profile_id: row["0"], + handle: row["1"], + current_primary: row["2"], + new_primary: row["3"], + }; + results.push(r); + }) + .on("end", () => { + results.sort((a, b) => { + return a.handle.localeCompare(b.handle); + }); + setData(results); + }) + .on("error", (err: any) => { + console.error(err); + }); + + parser.write(data); + parser.end(); + }; + + reader.readAsText(body); + }, + [setData], + ); + useEffect(() => { const filePath = "/primary_address.csv"; fetch(filePath) @@ -28,45 +69,11 @@ export default function AboutPrimaryAddress() { if (body) { populateData(body); } + }) + .catch((error) => { + console.error(error); }); - }, []); - - function populateData(body: Blob) { - const reader = new FileReader(); - - reader.onload = async () => { - const data = reader.result; - const results: PrimaryAddressData[] = []; - - const parser = csvParser({ headers: false }) - .on("data", (row: any) => { - const r = { - profile_id: row["0"], - handle: row["1"], - current_primary: row["2"], - new_primary: row["3"], - }; - results.push(r); - }) - .on("end", () => { - setResults(results); - }) - .on("error", (err: any) => { - console.error(err); - }); - - parser.write(data); - parser.end(); - }; - reader.readAsText(body); - } - - function setResults(results: PrimaryAddressData[]) { - results.sort((a, b) => { - return a.handle.localeCompare(b.handle); - }); - setData(results); - } + }, [populateData]); return ( diff --git a/components/allowlist-tool/allowlist-tool.types.ts b/components/allowlist-tool/allowlist-tool.types.ts index 39ed6d141b..a659a2523d 100644 --- a/components/allowlist-tool/allowlist-tool.types.ts +++ b/components/allowlist-tool/allowlist-tool.types.ts @@ -46,14 +46,6 @@ export interface AllowlistCustomTokenPool { readonly tokensCount: number; } -interface AllowlistWalletPool { - readonly id: string; - readonly allowlistId: string; - readonly name: string; - readonly description: string; - readonly walletsCount: number; -} - interface AllowlistPhase { readonly id: string; readonly allowlistId: string; @@ -161,33 +153,12 @@ export type Mutable = Omit & { -readonly [P in K]: T[P]; }; -interface AllowlistOperationDescription { - readonly code: AllowlistOperationCode; - readonly title: string; - readonly description: string; -} - export enum AllowlistRunStatus { PENDING = "PENDING", CLAIMED = "CLAIMED", FAILED = "FAILED", } -enum AllowlistToolEntity { - TRANSFER_POOLS = "TRANSFER_POOLS", - TRANSFER_POOL = "TRANSFER_POOL", - TOKEN_POOLS = "TOKEN_POOLS", - TOKEN_POOL = "TOKEN_POOL", - CUSTOM_TOKEN_POOLS = "CUSTOM_TOKEN_POOLS", - CUSTOM_TOKEN_POOL = "CUSTOM_TOKEN_POOL", - WALLET_POOLS = "WALLET_POOLS", - WALLET_POOL = "WALLET_POOL", - PHASES = "PHASES", - PHASE = "PHASE", - COMPONENT = "COMPONENT", - ITEM = "ITEM", -} - export interface AllowlistResult { readonly id: string; readonly wallet: string; @@ -197,41 +168,6 @@ export interface AllowlistResult { readonly amount: number; } -interface AllowlistToolOperationsGrouped { - transferPools: { - operations: AllowlistOperation[]; - pools: Record; - }; - tokenPools: { - operations: AllowlistOperation[]; - pools: Record; - }; - customTokenPools: { - operations: AllowlistOperation[]; - pools: Record; - }; - walletPools: { - operations: AllowlistOperation[]; - pools: Record; - }; - phases: { - operations: AllowlistOperation[]; - phases: Record< - string, - { - operations: AllowlistOperation[]; - components: Record< - string, - { - operations: AllowlistOperation[]; - items: Record; - } - >; - } - >; - }; -} - export interface DistributionPlanSearchContractMetadataResult { readonly id: string; readonly address: string; diff --git a/components/app-wallets/AppWallet.tsx b/components/app-wallets/AppWallet.tsx index ad79cfa7b3..3ab54d9afc 100644 --- a/components/app-wallets/AppWallet.tsx +++ b/components/app-wallets/AppWallet.tsx @@ -119,7 +119,7 @@ export default function AppWalletComponent( url: result.uri, dialogTitle: "Share or Save File", }); - } catch (e) { + } catch (_error) { alert("Unable to write file"); } }; @@ -154,7 +154,7 @@ export default function AppWalletComponent( }); } }, - [account.address] + [account.address, deleteAppWallet, router, setToast] ); if (fetchingAppWallets) { diff --git a/components/app-wallets/AppWalletModal.tsx b/components/app-wallets/AppWalletModal.tsx index 0274f52918..a56eb9b9c2 100644 --- a/components/app-wallets/AppWalletModal.tsx +++ b/components/app-wallets/AppWalletModal.tsx @@ -39,6 +39,7 @@ export function CreateAppWalletModal( onHide: (isSuccess?: boolean) => void; }> ) { + const { show, import: importData, onHide } = props; const { createAppWallet, importAppWallet } = useAppWallets(); const { setToast } = useAuth(); const [walletName, setWalletName] = useState(""); @@ -50,12 +51,15 @@ export function CreateAppWalletModal( const timeoutRef = useRef(null); - const handleHide = (isSuccess?: boolean) => { - setWalletName(""); - setWalletPass(""); - setError(""); - props.onHide(isSuccess); - }; + const handleHide = useCallback( + (isSuccess?: boolean) => { + setWalletName(""); + setWalletPass(""); + setError(""); + onHide(isSuccess); + }, + [onHide] + ); const handleCreate = useCallback(async () => { if (walletPass.length < SEED_MIN_PASS_LENGTH) { @@ -85,10 +89,10 @@ export function CreateAppWalletModal( handleHide(true); } setIsAdding(false); - }, [walletName, walletPass, isAdding]); + }, [createAppWallet, handleHide, setToast, walletName, walletPass]); const handleImport = useCallback(async () => { - if (!props.import) return; + if (!importData) return; if (walletPass.length < SEED_MIN_PASS_LENGTH) { showAppWalletError( @@ -106,9 +110,9 @@ export function CreateAppWalletModal( const success = await importAppWallet( walletName, walletPass, - props.import.address, - props.import.mnemonic, - props.import.privateKey + importData.address, + importData.mnemonic, + importData.privateKey ); if (!success) { setToast({ @@ -120,18 +124,18 @@ export function CreateAppWalletModal( handleHide(true); } setIsAdding(false); - }, [walletName, walletPass, isAdding]); + }, [handleHide, importAppWallet, importData, setToast, walletName, walletPass]); return ( handleHide()} backdrop keyboard={false} centered> - {props.import ? `Import` : `Create New`} Wallet + {importData ? `Import` : `Create New`} Wallet @@ -199,7 +203,7 @@ export function CreateAppWalletModal( - {props.import ? ( + {importData ? (