fix(lint): close type-check gaps in client scripts and tooling

Remove unused import flagged by CI lint, then harden the toolchain so
such issues are caught before push:

- lint-staged now also matches .tsx/.jsx (previously .tsx files skipped
  the pre-commit eslint pass, which is how the error reached CI)
- eslint ignores generated client bundles (gitignored, not worth linting)
- typecheck now also runs the client tsconfig; the hand-written browser
  source was excluded from the root config and never type-checked
- consolidate the window global augmentations (showToast,
  parseJsonResponseOrThrow) into a single client globals.d.ts; the inline
  declare-global blocks failed (non-module files) and masked real errors

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-23 10:38:01 +02:00
parent 6bfaa4dec7
commit 4db9fc1b8a
6 changed files with 48 additions and 37 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ import prettier from "eslint-config-prettier";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
export default tseslint.config( export default tseslint.config(
{ ignores: ["dist/", "coverage/"] }, { ignores: ["dist/", "coverage/", "src/scripts/generated/"] },
...tseslint.configs.recommended, ...tseslint.configs.recommended,
prettier, prettier,
{ {
+3 -2
View File
@@ -16,11 +16,12 @@
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit && npm run typecheck:client",
"typecheck:client": "tsc -p src/scripts/client/tsconfig.json --noEmit",
"prepare": "husky && npm run build:client" "prepare": "husky && npm run build:client"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,js}": [ "*.{ts,tsx,js,jsx}": [
"eslint --fix", "eslint --fix",
"prettier --write" "prettier --write"
], ],
+12 -4
View File
@@ -12,7 +12,6 @@ import {
updateFeedInList, updateFeedInList,
removeFeedFromList, removeFeedFromList,
removeFeedsFromListBulk, removeFeedsFromListBulk,
deleteKeysWithConcurrency,
purgeFeedKeysStep, purgeFeedKeysStep,
} from "./helpers"; } from "./helpers";
@@ -45,7 +44,12 @@ const updateFeedSchema = z.object({
}); });
const senderFilterSchema = z.object({ const senderFilterSchema = z.object({
action: z.enum(["allow_sender", "allow_domain", "block_sender", "block_domain"]), action: z.enum([
"allow_sender",
"allow_domain",
"block_sender",
"block_domain",
]),
value: z.string().min(1), value: z.string().min(1),
}); });
@@ -668,7 +672,9 @@ feedsRouter.post("/bulk-delete", async (c) => {
const deletedFeedIds = await removeFeedsFromListBulk(emailStorage, okIds); const deletedFeedIds = await removeFeedsFromListBulk(emailStorage, okIds);
if (deletedFeedIds.length > 0) { if (deletedFeedIds.length > 0) {
await bumpCounters(emailStorage, { feeds_deleted: deletedFeedIds.length }); await bumpCounters(emailStorage, {
feeds_deleted: deletedFeedIds.length,
});
} }
const removed = new Set(deletedFeedIds); const removed = new Set(deletedFeedIds);
@@ -720,7 +726,9 @@ feedsRouter.post("/bulk-delete", async (c) => {
const deletedFeedIds = await removeFeedsFromListBulk(emailStorage, okIds); const deletedFeedIds = await removeFeedsFromListBulk(emailStorage, okIds);
if (deletedFeedIds.length > 0) { if (deletedFeedIds.length > 0) {
await bumpCounters(emailStorage, { feeds_deleted: deletedFeedIds.length }); await bumpCounters(emailStorage, {
feeds_deleted: deletedFeedIds.length,
});
} }
return c.redirect( return c.redirect(
-15
View File
@@ -341,21 +341,6 @@ function refreshFeedRowCache(): void {
updateFeedSelectionState(); updateFeedSelectionState();
} }
interface ToastHandle {
update?: (msg: string, opts?: Record<string, unknown>) => void;
dismiss?: () => void;
}
declare global {
interface Window {
showToast?: (msg: string, opts?: Record<string, unknown>) => ToastHandle;
parseJsonResponseOrThrow?: (
res: Response,
opts?: Record<string, unknown>,
) => Promise<Record<string, unknown>>;
}
}
function setupFeedDeleteButtons(): void { function setupFeedDeleteButtons(): void {
const buttons = Array.from( const buttons = Array.from(
document.querySelectorAll<HTMLButtonElement>( document.querySelectorAll<HTMLButtonElement>(
-15
View File
@@ -321,21 +321,6 @@ function refreshEmailRowCache(): void {
updateEmailSelectionState(); updateEmailSelectionState();
} }
interface ToastHandle {
update?: (msg: string, opts?: Record<string, unknown>) => void;
dismiss?: () => void;
}
declare global {
interface Window {
showToast?: (msg: string, opts?: Record<string, unknown>) => ToastHandle;
parseJsonResponseOrThrow?: (
res: Response,
opts?: Record<string, unknown>,
) => Promise<Record<string, unknown>>;
}
}
function setupEmailDeleteButtons(): void { function setupEmailDeleteButtons(): void {
Array.from( Array.from(
document.querySelectorAll<HTMLButtonElement>( document.querySelectorAll<HTMLButtonElement>(
+32
View File
@@ -0,0 +1,32 @@
// Ambient declarations for browser globals injected at runtime via inline
// <script> bundles (see src/scripts/toast.ts and src/scripts/httpErrors.ts).
// Those helpers attach themselves to window rather than being importable, so the
// client TypeScript needs them declared here to type-check.
interface ToastOptions {
type?: "info" | "success" | "warning" | "error" | (string & {});
loading?: boolean;
duration?: number;
}
interface ToastHandle {
dismiss: () => void;
update: (message: string, opts?: ToastOptions) => void;
}
interface ParseJsonOptions {
prefix?: string;
allowText?: boolean;
}
declare global {
interface Window {
showToast: (message: string, opts?: ToastOptions) => ToastHandle;
parseJsonResponseOrThrow: (
res: Response,
opts?: ParseJsonOptions,
) => Promise<Record<string, unknown>>;
}
}
export {};