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<HTMLInputElement> {}` -> `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<any>` -> `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 <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-06-02 00:11:52 -05:00
parent 778fe1d321
commit 1c9deea3fd
13 changed files with 99 additions and 27 deletions

View file

@ -1,13 +1,8 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"incremental": false "incremental": false
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],

View file

@ -16,6 +16,7 @@
"zod": "^3.24.0" "zod": "^3.24.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.0",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.0" "typescript": "^5.7.0"

View file

@ -1,14 +1,10 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"declaration": true, "incremental": false,
"declarationMap": true, "allowImportingTsExtensions": true
"sourceMap": true,
"incremental": false
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]

View file

@ -4,7 +4,7 @@ import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {

View file

@ -22,6 +22,18 @@ export type DocumentCardShape = TLBaseShape<
} }
>; >;
/**
* Register this shape with tldraw so `BaseBoxShapeUtil<DocumentCardShape>` 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 { function truncatePreview(text: string, max = 100): string {
const t = text.trim(); const t = text.trim();
if (t.length <= max) return t; if (t.length <= max) return t;
@ -67,7 +79,7 @@ function DocumentCardBody({ shape }: { shape: DocumentCardShape }) {
); );
} }
export class DocumentCardShapeUtil extends BaseBoxShapeUtil<any> { export class DocumentCardShapeUtil extends BaseBoxShapeUtil<DocumentCardShape> {
static override type = "document-card"; static override type = "document-card";
static override props = { static override props = {

View file

@ -24,6 +24,12 @@ export type ProjectCardShape = TLBaseShape<
} }
>; >;
declare module "@tldraw/tlschema" {
interface TLGlobalShapePropsMap {
"project-card": ProjectCardShape["props"];
}
}
function clampProgress(n: number): number { function clampProgress(n: number): number {
if (Number.isNaN(n)) return 0; if (Number.isNaN(n)) return 0;
return Math.min(100, Math.max(0, n)); return Math.min(100, Math.max(0, n));
@ -105,7 +111,7 @@ function ProjectCardBody({ shape }: { shape: ProjectCardShape }) {
); );
} }
export class ProjectCardShapeUtil extends BaseBoxShapeUtil<any> { export class ProjectCardShapeUtil extends BaseBoxShapeUtil<ProjectCardShape> {
static override type = "project-card"; static override type = "project-card";
static override props = { static override props = {

View file

@ -25,6 +25,12 @@ export type TaskCardShape = TLBaseShape<
} }
>; >;
declare module "@tldraw/tlschema" {
interface TLGlobalShapePropsMap {
"task-card": TaskCardShape["props"];
}
}
const STATUS_DOT: Record<TaskCardShape["props"]["status"], string> = { const STATUS_DOT: Record<TaskCardShape["props"]["status"], string> = {
open: "bg-zinc-400 dark:bg-zinc-500", open: "bg-zinc-400 dark:bg-zinc-500",
in_progress: "bg-blue-500 dark:bg-blue-400", in_progress: "bg-blue-500 dark:bg-blue-400",
@ -122,7 +128,7 @@ function TaskCardBody({ shape }: { shape: TaskCardShape }) {
); );
} }
export class TaskCardShapeUtil extends BaseBoxShapeUtil<any> { export class TaskCardShapeUtil extends BaseBoxShapeUtil<TaskCardShape> {
static override type = "task-card"; static override type = "task-card";
static override props = { static override props = {

View file

@ -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;

View file

@ -6,7 +6,7 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint .",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
@ -77,7 +77,9 @@
"zustand": "^5.0.0" "zustand": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tldraw/tlschema": "4.5.4",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",

View file

@ -1,5 +1,6 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import type { PgTable } from "drizzle-orm/pg-core";
import { import {
objects, objects,
forms, forms,
@ -16,6 +17,11 @@ type Db = typeof defaultDb;
* routers when a mutation targets a specific row by id. Throws NOT_FOUND if the * 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 * row either doesn't exist or lives in a different workspace, so callers can't
* use the error code to probe IDs across tenants. * 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< export async function assertRowInWorkspace<
T extends { id: typeof objects.id; workspaceId: typeof objects.workspaceId }, T extends { id: typeof objects.id; workspaceId: typeof objects.workspaceId },
@ -28,7 +34,7 @@ export async function assertRowInWorkspace<
}): Promise<void> { }): Promise<void> {
const [row] = await args.db const [row] = await args.db
.select({ id: args.table.id }) .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))) .where(and(eq(args.table.id, args.rowId), eq(args.table.workspaceId, args.workspaceId)))
.limit(1); .limit(1);
if (!row) { if (!row) {

View file

@ -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 { export function generateId(): string {
return randomUUID(); return globalThis.crypto.randomUUID();
} }

View file

@ -4,12 +4,12 @@ slug: fix-lint-and-shared-types
title: Fix pnpm lint (Next 16 deprecation) and packages/shared Node-API leak title: Fix pnpm lint (Next 16 deprecation) and packages/shared Node-API leak
plan_slug: daily-driver-finish plan_slug: daily-driver-finish
epic_slug: shipping-the-shell epic_slug: shipping-the-shell
status: ready status: done
priority: P0 priority: P0
tenant_id: global tenant_id: global
owner: unassigned owner: unassigned
cursor_todo_id: null cursor_todo_id: null
updated_at: "2026-06-01" updated_at: "2026-06-02"
--- ---
# Task summary # Task summary
@ -48,11 +48,26 @@ Verify by grepping for other Node imports in `packages/shared/` while you're in
## Subtasks ## Subtasks
- [ ] Run the Next.js codemod and verify `apps/web/eslint.config.{js,mjs}` is created. - [x] Create `apps/web/eslint.config.mjs` using `FlatCompat` from `@eslint/eslintrc` (the codemod assumes an existing config; we had none).
- [ ] Update `apps/web/package.json` `lint` script. - [x] Update `apps/web/package.json` `lint` script from `next lint` to `eslint .`.
- [ ] Replace `crypto.randomUUID` with `globalThis.crypto.randomUUID` in `packages/shared/src/utils/id.ts`. - [x] 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. - [x] Grep `packages/shared/src/` for other Node-only imports — none found beyond `crypto`.
- [ ] Verify `pnpm lint && pnpm type-check` exits 0. - [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<HTMLInputElement> {}` with `type InputProps = React.InputHTMLAttributes<HTMLInputElement>`.
- `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<any>` with the properly-typed `BaseBoxShapeUtil<DocumentCardShape>` / 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 ## Owner or assignee

View file

@ -73,6 +73,9 @@ importers:
specifier: ^3.24.0 specifier: ^3.24.0
version: 3.25.76 version: 3.25.76
devDependencies: devDependencies:
'@types/node':
specifier: ^22.10.0
version: 22.19.15
tsup: tsup:
specifier: ^8.3.5 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) 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 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)) 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: devDependencies:
'@eslint/eslintrc':
specifier: ^3.3.5
version: 3.3.5
'@tailwindcss/typography': '@tailwindcss/typography':
specifier: ^0.5.16 specifier: ^0.5.16
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) 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': '@types/node':
specifier: ^22.10.0 specifier: ^22.10.0
version: 22.19.15 version: 22.19.15