From 1c9deea3fdcabadd90558e51772fc67d98a873c3 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 2 Jun 2026 00:11:52 -0500 Subject: [PATCH] chore(lint+types): green pnpm lint && pnpm type-check from a clean clone Path-A first task: get the repo's two repo-wide quality gates passing. Both were failing from a clean clone in ways that were silently hiding each other. Headline fixes: * apps/web: add an eslint 9 flat config (eslint.config.mjs) using FlatCompat against next/core-web-vitals + next/typescript, and switch the `lint` script from `next lint` to `eslint .`. Previously `next lint` fell into its interactive setup prompt because there was no config at all in apps/web, which made `pnpm lint` permanently fail before any rule ever ran. * packages/shared/src/utils/id.ts: replace `randomUUID` from `node:crypto` with `globalThis.crypto.randomUUID`. `@tasks/shared` is forbidden from using Node-only APIs (per AGENTS.md / repo-overview.mdc) because it has to be importable from the browser bundle. Adjacent fixes pulled in to make the gates actually green: * apps/mcp-server/tsconfig.json: drop vestigial rootDir / declaration* / outDir / sourceMap (build is via tsup, not tsc emit) and add allowImportingTsExtensions. The MCP server uses `.ts`-extension re-export shims (db.ts / schema.ts / shared-types.ts) so tsup can inline workspace .ts sources into the bundle. * apps/collab-server/tsconfig.json: same simplification. * apps/mcp-server/package.json: add @types/node so `process.env` in packages/database/src/client.ts (transitively pulled into the MCP server's type-check) resolves. * apps/web/components/ui/input.tsx: empty `interface InputProps extends React.InputHTMLAttributes {}` -> `type` alias. * apps/web/server/lib/workspace-guard.ts: `from(args.table as any)` -> `as unknown as PgTable` with a comment. Standard drizzle escape hatch for structural generic tables. * apps/web/components/whiteboard/shapes/{document,project,task}-card.tsx: `BaseBoxShapeUtil` -> `BaseBoxShapeUtil<{Shape}>` plus inline `declare module "@tldraw/tlschema"` augmentation of TLGlobalShapePropsMap. Required adding @tldraw/tlschema as a direct devDep of apps/web so the augmentation target resolves; previously it was only present transitively under tldraw's own deps. Result: `pnpm lint && pnpm type-check` exits 0 across all 6 packages. 16 unused-import / exhaustive-deps warnings remain; they're pre-existing housekeeping and out of scope for this task. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-fix-lint-and-shared-types.md (status: done). Co-authored-by: Cursor --- apps/collab-server/tsconfig.json | 5 ---- apps/mcp-server/package.json | 1 + apps/mcp-server/tsconfig.json | 8 ++--- apps/web/components/ui/input.tsx | 2 +- .../whiteboard/shapes/document-card.tsx | 14 ++++++++- .../whiteboard/shapes/project-card.tsx | 8 ++++- .../whiteboard/shapes/task-card.tsx | 8 ++++- apps/web/eslint.config.mjs | 19 ++++++++++++ apps/web/package.json | 4 ++- apps/web/server/lib/workspace-guard.ts | 8 ++++- packages/shared/src/utils/id.ts | 11 +++++-- .../Task-fix-lint-and-shared-types.md | 29 ++++++++++++++----- pnpm-lock.yaml | 9 ++++++ 13 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 apps/web/eslint.config.mjs diff --git a/apps/collab-server/tsconfig.json b/apps/collab-server/tsconfig.json index 8f1da89..2017f24 100644 --- a/apps/collab-server/tsconfig.json +++ b/apps/collab-server/tsconfig.json @@ -1,13 +1,8 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", "module": "ESNext", "moduleResolution": "bundler", - "declaration": true, - "declarationMap": true, - "sourceMap": true, "incremental": false }, "include": ["src/**/*.ts"], diff --git a/apps/mcp-server/package.json b/apps/mcp-server/package.json index 023529a..471a032 100644 --- a/apps/mcp-server/package.json +++ b/apps/mcp-server/package.json @@ -16,6 +16,7 @@ "zod": "^3.24.0" }, "devDependencies": { + "@types/node": "^22.10.0", "tsup": "^8.3.5", "tsx": "^4.19.2", "typescript": "^5.7.0" diff --git a/apps/mcp-server/tsconfig.json b/apps/mcp-server/tsconfig.json index 8f1da89..aeb0724 100644 --- a/apps/mcp-server/tsconfig.json +++ b/apps/mcp-server/tsconfig.json @@ -1,14 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", "module": "ESNext", "moduleResolution": "bundler", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "incremental": false + "incremental": false, + "allowImportingTsExtensions": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"] diff --git a/apps/web/components/ui/input.tsx b/apps/web/components/ui/input.tsx index 2e9d2d6..9a8a367 100644 --- a/apps/web/components/ui/input.tsx +++ b/apps/web/components/ui/input.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -export interface InputProps extends React.InputHTMLAttributes {} +export type InputProps = React.InputHTMLAttributes; const Input = React.forwardRef( ({ className, type, ...props }, ref) => { diff --git a/apps/web/components/whiteboard/shapes/document-card.tsx b/apps/web/components/whiteboard/shapes/document-card.tsx index d2a6686..5062b97 100644 --- a/apps/web/components/whiteboard/shapes/document-card.tsx +++ b/apps/web/components/whiteboard/shapes/document-card.tsx @@ -22,6 +22,18 @@ export type DocumentCardShape = TLBaseShape< } >; +/** + * Register this shape with tldraw so `BaseBoxShapeUtil` accepts + * it. tldraw's `TLBaseBoxShape = ExtractShapeByProps<{ w, h }>` extracts from + * `TLShape`, which only knows about built-in shapes unless we add an entry to + * `TLGlobalShapePropsMap` here. + */ +declare module "@tldraw/tlschema" { + interface TLGlobalShapePropsMap { + "document-card": DocumentCardShape["props"]; + } +} + function truncatePreview(text: string, max = 100): string { const t = text.trim(); if (t.length <= max) return t; @@ -67,7 +79,7 @@ function DocumentCardBody({ shape }: { shape: DocumentCardShape }) { ); } -export class DocumentCardShapeUtil extends BaseBoxShapeUtil { +export class DocumentCardShapeUtil extends BaseBoxShapeUtil { static override type = "document-card"; static override props = { diff --git a/apps/web/components/whiteboard/shapes/project-card.tsx b/apps/web/components/whiteboard/shapes/project-card.tsx index 874da05..ea5b522 100644 --- a/apps/web/components/whiteboard/shapes/project-card.tsx +++ b/apps/web/components/whiteboard/shapes/project-card.tsx @@ -24,6 +24,12 @@ export type ProjectCardShape = TLBaseShape< } >; +declare module "@tldraw/tlschema" { + interface TLGlobalShapePropsMap { + "project-card": ProjectCardShape["props"]; + } +} + function clampProgress(n: number): number { if (Number.isNaN(n)) return 0; return Math.min(100, Math.max(0, n)); @@ -105,7 +111,7 @@ function ProjectCardBody({ shape }: { shape: ProjectCardShape }) { ); } -export class ProjectCardShapeUtil extends BaseBoxShapeUtil { +export class ProjectCardShapeUtil extends BaseBoxShapeUtil { static override type = "project-card"; static override props = { diff --git a/apps/web/components/whiteboard/shapes/task-card.tsx b/apps/web/components/whiteboard/shapes/task-card.tsx index 0377663..811ea25 100644 --- a/apps/web/components/whiteboard/shapes/task-card.tsx +++ b/apps/web/components/whiteboard/shapes/task-card.tsx @@ -25,6 +25,12 @@ export type TaskCardShape = TLBaseShape< } >; +declare module "@tldraw/tlschema" { + interface TLGlobalShapePropsMap { + "task-card": TaskCardShape["props"]; + } +} + const STATUS_DOT: Record = { open: "bg-zinc-400 dark:bg-zinc-500", in_progress: "bg-blue-500 dark:bg-blue-400", @@ -122,7 +128,7 @@ function TaskCardBody({ shape }: { shape: TaskCardShape }) { ); } -export class TaskCardShapeUtil extends BaseBoxShapeUtil { +export class TaskCardShapeUtil extends BaseBoxShapeUtil { static override type = "task-card"; static override props = { diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs new file mode 100644 index 0000000..a4902a2 --- /dev/null +++ b/apps/web/eslint.config.mjs @@ -0,0 +1,19 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const config = [ + { + ignores: [".next/**", "node_modules/**", "dist/**", "next-env.d.ts"], + }, + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default config; diff --git a/apps/web/package.json b/apps/web/package.json index 09fa1a6..3f0ee5b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint .", "type-check": "tsc --noEmit" }, "dependencies": { @@ -77,7 +77,9 @@ "zustand": "^5.0.0" }, "devDependencies": { + "@eslint/eslintrc": "^3.3.5", "@tailwindcss/typography": "^0.5.16", + "@tldraw/tlschema": "4.5.4", "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", diff --git a/apps/web/server/lib/workspace-guard.ts b/apps/web/server/lib/workspace-guard.ts index a69d2fe..c7de83d 100644 --- a/apps/web/server/lib/workspace-guard.ts +++ b/apps/web/server/lib/workspace-guard.ts @@ -1,5 +1,6 @@ import { TRPCError } from "@trpc/server"; import { and, eq } from "drizzle-orm"; +import type { PgTable } from "drizzle-orm/pg-core"; import { objects, forms, @@ -16,6 +17,11 @@ type Db = typeof defaultDb; * routers when a mutation targets a specific row by id. Throws NOT_FOUND if the * row either doesn't exist or lives in a different workspace, so callers can't * use the error code to probe IDs across tenants. + * + * The `table` parameter is constrained structurally to having `id` and + * `workspaceId` columns; the `as PgTable` cast inside the body is the + * standard drizzle escape hatch when a structural generic table can't be + * proven equivalent to drizzle's nominal `PgTable` shape. */ export async function assertRowInWorkspace< T extends { id: typeof objects.id; workspaceId: typeof objects.workspaceId }, @@ -28,7 +34,7 @@ export async function assertRowInWorkspace< }): Promise { const [row] = await args.db .select({ id: args.table.id }) - .from(args.table as any) + .from(args.table as unknown as PgTable) .where(and(eq(args.table.id, args.rowId), eq(args.table.workspaceId, args.workspaceId))) .limit(1); if (!row) { diff --git a/packages/shared/src/utils/id.ts b/packages/shared/src/utils/id.ts index c819ac6..33c01a9 100644 --- a/packages/shared/src/utils/id.ts +++ b/packages/shared/src/utils/id.ts @@ -1,5 +1,10 @@ -import { randomUUID } from "crypto"; - +/** + * Web-Crypto based UUID v4 generator. We use `globalThis.crypto.randomUUID` + * rather than Node's `node:crypto` because `@tasks/shared` is meant to be + * importable from both browser and server bundles (see `AGENTS.md` §4 and + * `.cursor/rules/repo-overview.mdc`). The Web Crypto API is available in + * Node >= 20 and every modern browser, so this works in both worlds. + */ export function generateId(): string { - return randomUUID(); + return globalThis.crypto.randomUUID(); } diff --git a/plans/Plan-daily-driver-finish/Epic-shipping-the-shell/Task-fix-lint-and-shared-types.md b/plans/Plan-daily-driver-finish/Epic-shipping-the-shell/Task-fix-lint-and-shared-types.md index 322f18c..30547f3 100644 --- a/plans/Plan-daily-driver-finish/Epic-shipping-the-shell/Task-fix-lint-and-shared-types.md +++ b/plans/Plan-daily-driver-finish/Epic-shipping-the-shell/Task-fix-lint-and-shared-types.md @@ -4,12 +4,12 @@ slug: fix-lint-and-shared-types title: Fix pnpm lint (Next 16 deprecation) and packages/shared Node-API leak plan_slug: daily-driver-finish epic_slug: shipping-the-shell -status: ready +status: done priority: P0 tenant_id: global owner: unassigned cursor_todo_id: null -updated_at: "2026-06-01" +updated_at: "2026-06-02" --- # Task summary @@ -48,11 +48,26 @@ Verify by grepping for other Node imports in `packages/shared/` while you're in ## Subtasks -- [ ] Run the Next.js codemod and verify `apps/web/eslint.config.{js,mjs}` is created. -- [ ] Update `apps/web/package.json` `lint` script. -- [ ] Replace `crypto.randomUUID` with `globalThis.crypto.randomUUID` in `packages/shared/src/utils/id.ts`. -- [ ] Grep `packages/shared/src/` for other Node-only imports and fix. -- [ ] Verify `pnpm lint && pnpm type-check` exits 0. +- [x] Create `apps/web/eslint.config.mjs` using `FlatCompat` from `@eslint/eslintrc` (the codemod assumes an existing config; we had none). +- [x] Update `apps/web/package.json` `lint` script from `next lint` to `eslint .`. +- [x] Replace `crypto.randomUUID` with `globalThis.crypto.randomUUID` in `packages/shared/src/utils/id.ts`. +- [x] Grep `packages/shared/src/` for other Node-only imports — none found beyond `crypto`. +- [x] Verify `pnpm lint && pnpm type-check` exits 0. + +### Adjacent fixes pulled in to make `lint && type-check` actually pass + +Lint failing on the interactive prompt was hiding several real type/lint errors. Once lint was running, the following pre-existing issues had to be resolved before `pnpm lint` exited 0: + +- `apps/mcp-server/tsconfig.json`: removed vestigial `rootDir`/`declaration`/`outDir`/`sourceMap`/`declarationMap` and added `allowImportingTsExtensions: true`. The MCP server uses `.ts`-extension re-export shims (see `apps/mcp-server/src/db.ts`, `schema.ts`, `shared-types.ts`) to let tsup bundle workspace `.ts` sources at build time. With `rootDir` set, cross-workspace imports tripped TS6059; with `noEmit` inherited from root, the other fields were unused. +- `apps/collab-server/tsconfig.json`: same simplification. +- `apps/mcp-server/package.json`: added `@types/node` so `process.env` in `packages/database/src/client.ts` (pulled in transitively) resolves. +- `apps/web/components/ui/input.tsx`: replaced empty `interface InputProps extends React.InputHTMLAttributes {}` with `type InputProps = React.InputHTMLAttributes`. +- `apps/web/server/lib/workspace-guard.ts`: replaced `as any` on the `from(...)` call with `as unknown as PgTable` and documented why drizzle's structural generic needs the escape hatch. +- Three whiteboard shapes (`document-card`, `project-card`, `task-card`): replaced `BaseBoxShapeUtil` with the properly-typed `BaseBoxShapeUtil` / etc., plus module augmentation on `TLGlobalShapePropsMap` from `@tldraw/tlschema` so tldraw's `TLBaseBoxShape` extracts our custom shapes. Required adding `@tldraw/tlschema` as a direct devDep of `apps/web` so the `declare module` could resolve. + +### Warnings remaining (out of scope, not blocking lint pass) + +`pnpm lint` reports 16 warnings — all pre-existing unused imports, unused vars, and `react-hooks/exhaustive-deps` notes. Errors are gone; these are housekeeping for a future task. ## Owner or assignee diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20cd9a8..525397a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ importers: specifier: ^3.24.0 version: 3.25.76 devDependencies: + '@types/node': + specifier: ^22.10.0 + version: 22.19.15 tsup: specifier: ^8.3.5 version: 8.5.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) @@ -281,9 +284,15 @@ importers: specifier: ^5.0.0 version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: + '@eslint/eslintrc': + specifier: ^3.3.5 + version: 3.3.5 '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) + '@tldraw/tlschema': + specifier: 4.5.4 + version: 4.5.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@types/node': specifier: ^22.10.0 version: 22.19.15