AT-based link agregator. Mirror of https://github.com/likeandscribe/frontpage

Use typechecked lint rules

tom.sherman.is f2355153 db863e66

verified
+63 -28
+1 -1
packages/atproto-browser/app/at/_lib/atproto-json.tsx
··· 160 160 <> 161 161 {/* eslint-disable-next-line @next/next/no-img-element */} 162 162 <img 163 - src={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${data.ref}@jpeg`} 163 + src={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${data.ref.toString()}@jpeg`} 164 164 alt="" 165 165 width={200} 166 166 />
+1 -1
packages/atproto-browser/app/at/_lib/collection.tsx
··· 47 47 repo={repo} 48 48 collection={collection} 49 49 pds={pds} 50 - cursor={data.cursor!} 50 + cursor={data.cursor} 51 51 fetchKey={`listCollections/collection:${collection}/cursor:${data.cursor!}`} 52 52 /> 53 53 </Suspense>
+1 -1
packages/atproto-browser/app/at/_lib/uri-bar.tsx
··· 7 7 export function UriBar() { 8 8 const searchParams = useSearchParams(); 9 9 const pathname = usePathname(); 10 - const params = useParams() as { 10 + const params = useParams() satisfies { 11 11 identifier?: string; 12 12 collection?: string; 13 13 rkey?: string;
+1
packages/atproto-browser/app/at/_lib/video-embed-client.tsx
··· 28 28 url.searchParams.set("session_id", sessionId); 29 29 } 30 30 31 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 31 32 return new Request(url, initParams); 32 33 }, 33 34 });
+1 -1
packages/atproto-browser/app/at/_lib/video-embed.tsx
··· 8 8 did: string; 9 9 }; 10 10 11 - export async function VideoEmbed({ cid, did }: VideoEmbedProps) { 11 + export function VideoEmbed({ cid, did }: VideoEmbedProps) { 12 12 return ( 13 13 <Suspense fallback={<VideoEmbedWrapper />}> 14 14 <VideoEmbedInner cid={cid} did={did} />
+1
packages/atproto-browser/lib/link.tsx
··· 7 7 export default function Link(props: ComponentProps<typeof NextLink>) { 8 8 const router = useRouter(); 9 9 function prefetch() { 10 + // eslint-disable-next-line @typescript-eslint/no-base-to-string 10 11 router.prefetch(props.href.toString()); 11 12 } 12 13
+1 -1
packages/atproto-browser/lib/navigation.test.ts
··· 14 14 } 15 15 16 16 vi.mock("next/navigation", () => ({ 17 - redirect: vi.fn((path) => { 17 + redirect: vi.fn((path: string) => { 18 18 throw new RedirectError(path); 19 19 }), 20 20 }));
+2 -1
packages/eslint-config/next.js
··· 21 21 next.flatConfig.coreWebVitals, 22 22 23 23 eslint.configs.recommended, 24 - tseslint.configs.recommended, 24 + tseslint.configs.recommendedTypeChecked, 25 25 reactCompiler.configs.recommended, 26 26 reactHooks.configs["recommended-latest"], 27 27 // @ts-expect-error ··· 45 45 "react/react-in-jsx-scope": "off", 46 46 "react/prop-types": "off", 47 47 "react/no-array-index-key": "error", 48 + "@typescript-eslint/no-unsafe-assignment": "off", 48 49 "@typescript-eslint/no-floating-promises": "error", 49 50 "@typescript-eslint/no-misused-promises": "error", 50 51 "@typescript-eslint/consistent-type-imports": [
+2 -3
packages/frontpage/app/(app)/_components/vote-button.tsx
··· 1 1 "use client"; 2 2 3 3 import { Button } from "@/lib/components/ui/button"; 4 - import { cn } from "@/lib/utils"; 4 + import { cn, exhaustiveCheck } from "@/lib/utils"; 5 5 import { ChevronUpIcon } from "@radix-ui/react-icons"; 6 6 import { useState } from "react"; 7 7 ··· 50 50 } else if (initialState === "unvoted") { 51 51 actualVotes += hasOptimisticallyVoted === true ? 2 : 1; 52 52 } else { 53 - const _exhaustedCheck: never = initialState; 54 - throw new Error(`Invalid state: ${initialState}`); 53 + exhaustiveCheck(initialState, "Invalid state"); 55 54 } 56 55 57 56 return (
+2 -2
packages/frontpage/app/(app)/moderation/page.tsx
··· 64 64 case PostCollection: 65 65 return await moderatePost({ 66 66 rkey: report.subjectRkey!, 67 - authorDid: report.subjectDid! as DID, 67 + authorDid: report.subjectDid as DID, 68 68 cid: report.subjectCid!, 69 69 hide: input.status === "accepted", 70 70 }); ··· 72 72 case CommentCollection: 73 73 return await moderateComment({ 74 74 rkey: report.subjectRkey!, 75 - authorDid: report.subjectDid! as DID, 75 + authorDid: report.subjectDid as DID, 76 76 cid: report.subjectCid!, 77 77 hide: input.status === "accepted", 78 78 });
+2 -2
packages/frontpage/app/api/receive_hook/handlers.ts
··· 9 9 import * as dbVote from "@/lib/data/db/vote"; 10 10 import { getBlueskyProfile } from "@/lib/data/user"; 11 11 import { sendDiscordMessage } from "@/lib/discord"; 12 - import { invariant } from "@/lib/utils"; 12 + import { exhaustiveCheck, invariant } from "@/lib/utils"; 13 13 14 14 type HandlerInput = { 15 15 op: Zod.infer<typeof Operation>; ··· 219 219 break; 220 220 } 221 221 default: 222 - throw new Error(`Unknown collection: ${subject.uri.collection}`); 222 + exhaustiveCheck(subject.uri.collection, "Unknown collection"); 223 223 } 224 224 } else if (op.action === "delete") { 225 225 console.log("deleting vote", rkey);
+2 -1
packages/frontpage/app/api/receive_hook/route.ts
··· 7 7 import { getPdsUrl } from "@/lib/data/atproto/did"; 8 8 import { handleComment, handlePost, handleVote } from "./handlers"; 9 9 import { eq } from "drizzle-orm"; 10 + import { exhaustiveCheck } from "@/lib/utils"; 10 11 11 12 export async function POST(request: Request) { 12 13 const auth = request.headers.get("Authorization"); ··· 52 53 await handleVote({ op, repo, rkey }); 53 54 break; 54 55 default: 55 - throw new Error(`Unknown collection: ${collection}, ${op}`); 56 + exhaustiveCheck(collection, `Unknown collection ${JSON.stringify(op)}`); 56 57 } 57 58 }); 58 59
+4 -1
packages/frontpage/app/blog/[slug]/markdown.tsx
··· 1 + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 + /* eslint-disable @typescript-eslint/no-unsafe-argument */ 3 + /* eslint-disable @typescript-eslint/no-unsafe-return */ 1 4 /* eslint-disable @typescript-eslint/no-explicit-any */ 2 5 import "server-only"; 3 6 import { parse, transform, Tag } from "@markdoc/markdoc"; ··· 28 31 const tree = transform(ast, { 29 32 nodes: { heading: createHeadingSchema() }, 30 33 }); 31 - return render(tree) as any; 34 + return render(tree); 32 35 33 36 function deepRender(value: any): any { 34 37 if (value == null || typeof value !== "object") return value;
+1
packages/frontpage/lib/api-route.ts
··· 23 23 } 24 24 25 25 export function badRequest(message: string, init?: RequestInit): never { 26 + // eslint-disable-next-line @typescript-eslint/only-throw-error 26 27 throw new ResponseError(new Response(message, { ...init, status: 400 })); 27 28 } 28 29
+2
packages/frontpage/lib/api/comment.ts
··· 57 57 } 58 58 } catch (e) { 59 59 await db.deleteComment({ authorDid: user.did, rkey }); 60 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 60 61 throw new DataLayerError(`Failed to create comment: ${e}`); 61 62 } 62 63 } ··· 75 76 after(() => atproto.deleteComment(authorDid, rkey)); 76 77 await db.deleteComment({ authorDid: user.did, rkey }); 77 78 } catch (e) { 79 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 78 80 throw new DataLayerError(`Failed to delete comment: ${e}`); 79 81 } 80 82 }
+2
packages/frontpage/lib/api/post.ts
··· 46 46 return { rkey }; 47 47 } catch (e) { 48 48 await db.deletePost({ authorDid: user.did, rkey }); 49 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 49 50 throw new DataLayerError(`Failed to create post: ${e}`); 50 51 } 51 52 } ··· 61 62 after(() => atproto.deletePost(authorDid, rkey)); 62 63 await db.deletePost({ authorDid: user.did, rkey }); 63 64 } catch (e) { 65 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 64 66 throw new DataLayerError(`Failed to delete post: ${e}`); 65 67 } 66 68 }
+2
packages/frontpage/lib/api/vote.ts
··· 65 65 ); 66 66 } catch (e) { 67 67 await db.deleteVote({ authorDid: user.did, rkey }); 68 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 68 69 throw new DataLayerError(`Failed to create post vote: ${e}`); 69 70 } 70 71 } ··· 79 80 after(() => atproto.deleteVote(authorDid, rkey)); 80 81 await db.deleteVote({ authorDid: user.did, rkey }); 81 82 } catch (e) { 83 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 82 84 throw new DataLayerError(`Failed to delete vote: ${e}`); 83 85 } 84 86 }
+8 -1
packages/frontpage/lib/auth.ts
··· 35 35 const USER_AGENT = "appview/@frontpage.fyi (@tom-sherman.com)"; 36 36 37 37 export const getPrivateJwk = cache(() => 38 - importJWK(JSON.parse(process.env.PRIVATE_JWK!), USER_SESSION_JWT_ALG), 38 + importJWK( 39 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 40 + JSON.parse(process.env.PRIVATE_JWK!), 41 + USER_SESSION_JWT_ALG, 42 + ), 39 43 ); 40 44 41 45 export const getPublicJwk = cache(async () => { 42 46 const jwk = await importJWK( 47 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 43 48 JSON.parse(process.env.PUBLIC_JWK!), 44 49 USER_SESSION_JWT_ALG, 45 50 ); ··· 424 429 const [privateDpopKey, publicDpopKey] = await Promise.all([ 425 430 crypto.subtle.importKey( 426 431 "jwk", 432 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 427 433 JSON.parse(privateJwk), 428 434 { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, 429 435 true, ··· 431 437 ), 432 438 crypto.subtle.importKey( 433 439 "jwk", 440 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 434 441 JSON.parse(publicJwk), 435 442 { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, 436 443 true,
+1 -1
packages/frontpage/lib/components/user-avatar.tsx
··· 17 17 size?: Size; 18 18 }; 19 19 20 - export async function UserAvatar(props: UserAvatarProps) { 20 + export function UserAvatar(props: UserAvatarProps) { 21 21 return ( 22 22 <Suspense fallback={<AvatarFallback size={props.size} />}> 23 23 <AvatarImage {...props} />
+3 -1
packages/frontpage/lib/components/user-hover-card-client.tsx
··· 102 102 did: DID, 103 103 ): Promise<ApiRouteResponse<typeof GetHoverCardContent>> { 104 104 const response = await fetch(`/api/hover-card-content?did=${did}`); 105 - return response.json(); 105 + return response.json() as Promise< 106 + ApiRouteResponse<typeof GetHoverCardContent> 107 + >; 106 108 }
+6 -2
packages/frontpage/lib/data/atproto/identity.ts
··· 61 61 const { Answer } = DnsQueryResponse.parse(await response.json()); 62 62 // Answer[0].data is "\"did=...\"" (with quotes) 63 63 const val = Answer[0]?.data 64 - ? JSON.parse(Answer[0]?.data).split("did=")[1] 64 + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 65 + JSON.parse(Answer[0]?.data).split("did=")[1] 65 66 : null; 66 67 67 - return val ? parseDid(val) : null; 68 + return val 69 + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 70 + parseDid(val) 71 + : null; 68 72 } 69 73 70 74 const getAtprotoFromHttps = unstable_cache(
+1 -1
packages/frontpage/lib/data/db/notification.ts
··· 12 12 export type Cursor = { readonly [tag]: "Cursor" }; 13 13 14 14 function cursorToDate(cursor: Cursor): Date { 15 - // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument 16 16 return new Date(cursor as any); 17 17 } 18 18
+1 -1
packages/frontpage/lib/data/db/shared.ts
··· 27 27 case CommentCollection: { 28 28 const { postAuthor, postRkey } = (await getPostFromComment({ 29 29 rkey: rkey!, 30 - did: author!, 30 + did: author, 31 31 }))!; 32 32 return `/post/${postAuthor}/${postRkey}/${author}/${rkey}/`; 33 33 }
+11 -4
packages/frontpage/lib/infinite-list.tsx
··· 1 1 "use client"; 2 2 3 3 import useSWRInfinite, { unstable_serialize } from "swr/infinite"; 4 - import { createContext, Fragment, type ReactNode, startTransition } from "react"; 4 + import { 5 + createContext, 6 + Fragment, 7 + type ReactNode, 8 + startTransition, 9 + } from "react"; 5 10 import { useInView } from "react-intersection-observer"; 6 11 import { mutate, SWRConfig } from "swr"; 7 12 ··· 40 45 ); 41 46 } 42 47 43 - export const InfiniteListContext = createContext({ 44 - revalidatePage: async (): Promise<void> => { 48 + export const InfiniteListContext = createContext<{ 49 + revalidatePage: () => Promise<void>; 50 + }>({ 51 + revalidatePage: () => { 45 52 throw new Error( 46 53 "Cannot call InfiniteListContext.revalidate when not inside of an InfiniteList", 47 54 ); ··· 87 94 await mutate(data, { 88 95 revalidate: (_data, args) => 89 96 !currentCursor || 90 - // eslint-disable-next-line @typescript-eslint/no-explicit-any 97 + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 91 98 (args as any)[1] === currentCursor, 92 99 }); 93 100 },
+3 -2
packages/frontpage/lib/utils.ts
··· 18 18 } 19 19 } 20 20 21 - export function exhaustiveCheck(value: never): never { 22 - throw new Error(`Unhandled value: ${value}`); 21 + export function exhaustiveCheck(value: never, message?: string): never { 22 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 23 + throw new Error(`Unhandled value (${message}): ${value}`); 23 24 }
+1
packages/unravel/app/page.tsx
··· 4 4 export default function Home() { 5 5 useLayoutEffect(() => { 6 6 // @ts-expect-error UnicornStudio is on the window 7 + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 7 8 UnicornStudio.init(); 8 9 }, []); 9 10