From 2f1467fb14c449868542c298371edc7fd2367e56 Mon Sep 17 00:00:00 2001 From: Rob Simmons Date: Tue, 4 Aug 2026 16:33:40 -0400 Subject: [PATCH 1/2] chore: add eslint and tighter typescript rules, and adapt codebase to them Update src/lib/server/vscodeServer.ts Co-authored-by: Wojciech Nawrocki <13901751+Vtec234@users.noreply.github.com> --- collab-server/src/server.ts | 2 +- eslint.config.mjs | 4 +--- .../[userName]/[projectName]/preview/[...relPath]/route.ts | 2 +- src/app/admin/actions.ts | 6 +++--- src/app/admin/components/HealthMonitor.tsx | 2 +- src/app/api/auth-route/file/route.ts | 4 ++-- src/app/api/auth-route/vs/route.ts | 2 +- src/app/api/setup-events/route.ts | 2 +- src/app/components/AvatarIcon.tsx | 2 +- src/app/setup/actions.ts | 2 +- src/app/setup/page.tsx | 2 +- src/lib/server/collabServer.ts | 2 +- src/lib/server/dirToken.ts | 4 ++-- src/lib/server/editorSessions.ts | 4 ++-- src/lib/server/seed.ts | 2 +- src/lib/server/user.ts | 2 +- src/lib/server/util.ts | 2 +- src/proxy.ts | 2 +- tsconfig.json | 3 ++- vscode-workbench/src/collabServer.ts | 2 +- vscode-workbench/src/extension.ts | 2 +- vscode-workbench/src/panel.ts | 2 +- vscode-workbench/src/remoteSelections.ts | 4 ++-- 23 files changed, 30 insertions(+), 31 deletions(-) diff --git a/collab-server/src/server.ts b/collab-server/src/server.ts index bf0e1bc3..635ee7ce 100644 --- a/collab-server/src/server.ts +++ b/collab-server/src/server.ts @@ -13,7 +13,7 @@ if (process.argv.length !== 3) { process.exit(1) } -const projectDir = process.argv[2] +const projectDir = process.argv[2]! const socketPath = path.join(process.cwd(), 'collab.sock') const dbPath = path.join(process.cwd(), 'collab.db') diff --git a/eslint.config.mjs b/eslint.config.mjs index 81c37c04..bd3b5647 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -51,8 +51,7 @@ const eslintConfig = defineConfig([ checksVoidReturn: { arguments: false, attributes: false }, }, ], - '@typescript-eslint/no-unnecessary-condition': 'off', // actively misleading until we turn the typescript option noUncheckedIndexedAccess on - '@typescript-eslint/no-unnecessary-type-assertion': 'off', // actively misleading until we turn the typescript option noUncheckedIndexedAccess on + '@typescript-eslint/no-unnecessary-condition': 'error', '@typescript-eslint/no-unused-vars': [ 'error', { @@ -62,7 +61,6 @@ const eslintConfig = defineConfig([ enableAutofixRemoval: { imports: true }, }, ], - '@typescript-eslint/no-unsafe-return': 'off', // noisy, temporarily disabled '@typescript-eslint/require-await': 'off', // actively misleading in `'use server'` modules '@typescript-eslint/restrict-template-expressions': 'off', // always allow `${x}` regardless of x's type '@typescript-eslint/use-unknown-in-catch-callback-variable': 'error', // complements how strict works in typescript for chained promises diff --git a/src/app/[userName]/[projectName]/preview/[...relPath]/route.ts b/src/app/[userName]/[projectName]/preview/[...relPath]/route.ts index f19a1bc9..7612407a 100644 --- a/src/app/[userName]/[projectName]/preview/[...relPath]/route.ts +++ b/src/app/[userName]/[projectName]/preview/[...relPath]/route.ts @@ -24,7 +24,7 @@ type Params = z.infer * Access control is currently the same as for editing the project. */ export async function GET(_req: Request, { params }: { params: Promise }) { const parsed = zParams.safeParse(await params) - if (!parsed.success) return new Response(parsed.error.issues[0].message, { status: 400 }) + if (!parsed.success) return new Response(parsed.error.issues[0]!.message, { status: 400 }) const { userName, projectName, relPath: relPathSegs } = parsed.data const session = await requireAuth() diff --git a/src/app/admin/actions.ts b/src/app/admin/actions.ts index c6b74fac..b9008ad0 100644 --- a/src/app/admin/actions.ts +++ b/src/app/admin/actions.ts @@ -127,7 +127,7 @@ function parseMeminfo(): Record { const result: Record = {} for (const line of text.split('\n')) { const m = line.match(/^(\w+):\s+(\d+)/) - if (m) result[m[1]] = parseInt(m[2], 10) * 1024 // kB -> bytes + if (m) result[m[1]!] = parseInt(m[2]!, 10) * 1024 // kB -> bytes } return result } catch { @@ -151,7 +151,7 @@ export async function fetchHealth(): Promise { const dfOut = execFileSync('df', ['-h', getDataDir()], { encoding: 'utf8' }) const lines = dfOut.trim().split('\n') if (lines.length < 2) throw new Error('no dataVolumeDisk information') - const parts = lines[1].split(/\s+/) + const parts = lines[1]!.split(/\s+/) dataVolumeDisk = { total: parts[1] ?? '?', used: parts[2] ?? '?', @@ -170,7 +170,7 @@ export async function fetchHealth(): Promise { try { const text = fs.readFileSync('/proc/loadavg', 'utf8') const parts = text.split(' ') - loadAvg = [parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2])] + loadAvg = [parseFloat(parts[0]!), parseFloat(parts[1]!), parseFloat(parts[2]!)] } catch { loadAvg = [0, 0, 0] } diff --git a/src/app/admin/components/HealthMonitor.tsx b/src/app/admin/components/HealthMonitor.tsx index ecce0c04..0253fbb8 100644 --- a/src/app/admin/components/HealthMonitor.tsx +++ b/src/app/admin/components/HealthMonitor.tsx @@ -25,7 +25,7 @@ export function HealthMonitor() { return (

System health

-

Failed to load: {String(healthError)}

+

Failed to load: {healthError.message}

) } diff --git a/src/app/api/auth-route/file/route.ts b/src/app/api/auth-route/file/route.ts index 4c591b33..185dce12 100644 --- a/src/app/api/auth-route/file/route.ts +++ b/src/app/api/auth-route/file/route.ts @@ -14,11 +14,11 @@ export async function GET(req: Request) { // we can expect no trailing slash. const match = uri.match(/^\/_file\/([^/]+)\/(.*[^/])$/) if (!match) forbidden() - const rootDir = verifySignedDirToken(match[1]) + const rootDir = verifySignedDirToken(match[1]!) if (!rootDir) forbidden() const realRootDir = await fs.realpath(rootDir).catch(() => null) if (!realRootDir) forbidden() - const filePath = path.resolve(realRootDir, match[2]) + const filePath = path.resolve(realRootDir, match[2]!) // Ensure the absolute path with symlinks resolved lives under `realRootDir`. // If resolution fails, we pass `filePath` through - Nginx will 404 it. const realFilePath = await fs.realpath(filePath).catch(() => filePath) diff --git a/src/app/api/auth-route/vs/route.ts b/src/app/api/auth-route/vs/route.ts index e6964867..c042b533 100644 --- a/src/app/api/auth-route/vs/route.ts +++ b/src/app/api/auth-route/vs/route.ts @@ -10,7 +10,7 @@ export async function GET(req: Request) { const uri = req.headers.get('x-auth-uri') ?? '' const match = uri.match(/^\/_vs\/([^/]+)\/.*$/) if (!match) forbidden() - const sessionId = match[1] + const sessionId = match[1]! const userSession = await requireAuth() const socketPath = getEditorSessionManager().socketPathForViewer(userSession.user.id, sessionId) if (!socketPath) forbidden() diff --git a/src/app/api/setup-events/route.ts b/src/app/api/setup-events/route.ts index abc4d701..87bdd6fc 100644 --- a/src/app/api/setup-events/route.ts +++ b/src/app/api/setup-events/route.ts @@ -11,7 +11,7 @@ export async function GET() { interval = setInterval(() => { const st = getSeedState() while (cursor < st.events.length) { - const event = st.events[cursor++] + const event = st.events[cursor++]! send(event) if (event.type === 'done' || event.type === 'error') { clearInterval(interval) diff --git a/src/app/components/AvatarIcon.tsx b/src/app/components/AvatarIcon.tsx index 5fc28fc8..28365c02 100644 --- a/src/app/components/AvatarIcon.tsx +++ b/src/app/components/AvatarIcon.tsx @@ -8,7 +8,7 @@ export default function AvatarIcon({ user }: { user: Pick ) : ( - {user.name[0].toUpperCase()} + {user.name[0]!.toUpperCase()} )} ) diff --git a/src/app/setup/actions.ts b/src/app/setup/actions.ts index a48ec65c..7a8dedad 100644 --- a/src/app/setup/actions.ts +++ b/src/app/setup/actions.ts @@ -21,7 +21,7 @@ export async function saveSetupConfig(formData: FormData): Promise { - if (phase !== 'seeding') return + if (phase !== 'seeding') return undefined const source = new EventSource('/api/setup-events') source.onmessage = event => { let data: SeedEvent diff --git a/src/lib/server/collabServer.ts b/src/lib/server/collabServer.ts index 7968075f..b2afcae8 100644 --- a/src/lib/server/collabServer.ts +++ b/src/lib/server/collabServer.ts @@ -121,7 +121,7 @@ export class CollabServerHandle implements AsyncDisposable { if (this.disposing) return this.disposing this.disposing = (async () => { if (this.starting) { - await this.starting!.catch(() => {}) + await this.starting.catch(() => {}) if (this.proc) { await new Promise(resolve => { this.proc!.once('close', () => { diff --git a/src/lib/server/dirToken.ts b/src/lib/server/dirToken.ts index 4ec29934..78da0fde 100644 --- a/src/lib/server/dirToken.ts +++ b/src/lib/server/dirToken.ts @@ -46,11 +46,11 @@ export function verifySignedDirToken(token: string): string | null { if (parts.length !== 3) return null const [rootDirB64, exp, sig] = parts const expected = hmac(`${rootDirB64}.${exp}`) - if (sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { + if (sig!.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig!), Buffer.from(expected))) { return null } if (Number(exp) <= Date.now()) return null - return Buffer.from(rootDirB64, 'base64url').toString() + return Buffer.from(rootDirB64!, 'base64url').toString() } /** URL at which Nginx serves {@link relPath}, a path relative to {@link token}'s root. */ diff --git a/src/lib/server/editorSessions.ts b/src/lib/server/editorSessions.ts index cb7fd71c..bd9c52f3 100644 --- a/src/lib/server/editorSessions.ts +++ b/src/lib/server/editorSessions.ts @@ -247,8 +247,8 @@ export async function initEditorSessions() { for (const servers of m['vscServers'].values()) { for (const s of servers) Object.setPrototypeOf(s, VscodeServerHandle.prototype) } - await m['mounts'].forEach(mount => Object.setPrototypeOf(mount, ProjectMountHandle.prototype)) - await m['collabServers'].forEach(collab => Object.setPrototypeOf(collab, CollabServerHandle.prototype)) + await m['mounts'].forEach(mount => Object.setPrototypeOf(mount, ProjectMountHandle.prototype) as unknown) + await m['collabServers'].forEach(collab => Object.setPrototypeOf(collab, CollabServerHandle.prototype) as unknown) } } diff --git a/src/lib/server/seed.ts b/src/lib/server/seed.ts index 9c7886dd..4e38a7dd 100644 --- a/src/lib/server/seed.ts +++ b/src/lib/server/seed.ts @@ -48,7 +48,7 @@ export function startSeed(leanVersion: string | undefined): ActionResponse { // Git reads `$HOME/.config/git/config` as the global config. const name = user.displayName?.trim() || user.name - const email = user.email?.trim() + const email = user.email.trim() const userBlock = ['[user]'] if (name) userBlock.push(`\tname = ${name}`) if (email) userBlock.push(`\temail = ${email}`) diff --git a/src/lib/server/util.ts b/src/lib/server/util.ts index 916081c4..31ab17a2 100644 --- a/src/lib/server/util.ts +++ b/src/lib/server/util.ts @@ -18,7 +18,7 @@ export function serverAction( ): (raw: z.input) => Promise> { return async raw => { const parsed = schema.safeParse(raw) - if (!parsed.success) return { error: parsed.error.issues[0].message } + if (!parsed.success) return { error: parsed.error.issues[0]!.message } return handler(parsed.data) } } diff --git a/src/proxy.ts b/src/proxy.ts index 3885514f..d4fc39e5 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -2,7 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { getConfig } from './lib/server/config' -export async function proxy(request: NextRequest) { +export function proxy(request: NextRequest) { const cfg = getConfig() if (!cfg.isSetupComplete) { const path = request.nextUrl.pathname diff --git a/tsconfig.json b/tsconfig.json index 6df57a49..c88ece4d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,8 @@ }, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, - "noImplicitReturns": true + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true }, // Subfolders of `.next/` duplicated since without that `next dev` tries to rewrite this config. "include": [ diff --git a/vscode-workbench/src/collabServer.ts b/vscode-workbench/src/collabServer.ts index c7366718..b54d677d 100644 --- a/vscode-workbench/src/collabServer.ts +++ b/vscode-workbench/src/collabServer.ts @@ -89,7 +89,7 @@ export async function connectToCollabServer( } const color = AWARENESS_CURSOR_COLORS.reduce( (acc, c) => (counts.get(acc)! <= counts.get(c)! ? acc : c), - AWARENESS_CURSOR_COLORS[0], + AWARENESS_CURSOR_COLORS[0]!, ) awarenessProvider.setAwarenessField(AWARENESS_USER_KEY, { diff --git a/vscode-workbench/src/extension.ts b/vscode-workbench/src/extension.ts index e81a4183..8b8bd448 100644 --- a/vscode-workbench/src/extension.ts +++ b/vscode-workbench/src/extension.ts @@ -35,7 +35,7 @@ async function ensureProjectFolderOpen(mdata: WorkspaceMetadata, log: vs.LogOutp const expected = bwrapProjectDir(mdata.project.name) if ( vs.workspace.workspaceFolders?.length === 1 && - path.resolve(vs.workspace.workspaceFolders[0].uri.fsPath) === path.resolve(expected) + path.resolve(vs.workspace.workspaceFolders[0]!.uri.fsPath) === path.resolve(expected) ) return true diff --git a/vscode-workbench/src/panel.ts b/vscode-workbench/src/panel.ts index d62125c8..5736e2f7 100644 --- a/vscode-workbench/src/panel.ts +++ b/vscode-workbench/src/panel.ts @@ -70,7 +70,7 @@ export class WorkbenchPanelProvider implements vs.TreeDataProvider, v if (!sels) continue filePath = sels.filePath if (0 < sels.selections.length) { - const active = sels.selections[0].active + const active = sels.selections[0]!.active const pos = new vs.Position(active.line, active.character) sel = new vs.Selection(pos, pos) break diff --git a/vscode-workbench/src/remoteSelections.ts b/vscode-workbench/src/remoteSelections.ts index e4b3a85d..bf7883be 100644 --- a/vscode-workbench/src/remoteSelections.ts +++ b/vscode-workbench/src/remoteSelections.ts @@ -117,10 +117,10 @@ export class RemoteSelectionDecorator implements vs.Disposable { const decos = this.decorationsFor(clientId, user.color) const beforeRanges: vs.DecorationOptions[] = [] const afterRanges: vs.DecorationOptions[] = [] - if (selection?.filePath === filePath) { + if (selection.filePath === filePath) { for (const s of selection.selections) { const range = new vs.Range(s.anchor.line, s.anchor.character, s.active.line, s.active.character) - const opts = { range, hoverMessage: user?.name } + const opts = { range, hoverMessage: user.name } // Is `active` at the start or the end of the selection? if ( s.active.line < s.anchor.line || From 3b4e0efebbe5e1f517685567749c1f18cba56630 Mon Sep 17 00:00:00 2001 From: Rob Simmons Date: Sat, 8 Aug 2026 22:29:23 -0400 Subject: [PATCH 2/2] remove `undefined` from return --- src/app/setup/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index c8f2a0f1..f25c5673 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -63,7 +63,7 @@ export default function Setup() { // Stream seed events whenever we're in the seeding phase. useEffect(() => { - if (phase !== 'seeding') return undefined + if (phase !== 'seeding') return const source = new EventSource('/api/setup-events') source.onmessage = event => { let data: SeedEvent