diff --git a/docs/subagents/INDEX.md b/docs/subagents/INDEX.md index 9569d6e..b5c9f8e 100644 --- a/docs/subagents/INDEX.md +++ b/docs/subagents/INDEX.md @@ -17,3 +17,4 @@ Read first. Keep concise. | `codex-preserve-linebreaks-20260220T063538Z-s4nd` | `codex-preserve-linebreaks` | `Add config option to preserve subtitle line breaks in visible overlay rendering` | `completed` | `docs/subagents/agents/codex-preserve-linebreaks-20260220T063538Z-s4nd.md` | `2026-02-20T06:42:51Z` | | `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` | | `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` | +| `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` | diff --git a/docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md b/docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md new file mode 100644 index 0000000..98aeeff --- /dev/null +++ b/docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md @@ -0,0 +1,30 @@ +# Agent Log: codex-tsconfig-modernize-20260220T093035Z-68qb + +- alias: `codex-tsconfig-modernize` +- mission: `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` +- status: `completed` +- started_utc: `2026-02-20T09:31:11Z` +- last_update_utc: `2026-02-20T09:46:26Z` +- planned_files: + - `tsconfig.json` + - `src/**/*.ts` (targeted type-safety fixes) + - `docs/subagents/INDEX.md` + - `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` +- assumptions: + - User wants requested config adopted for root project tsconfig. + - Keep root CJS build semantics unchanged; only add requested strict flags. +- phase: + - `handoff` +- notes: + - Read `docs/subagents/INDEX.md` and `docs/subagents/collaboration.md`. + - Root build uses `tsc` emit flow; avoid `noEmit` and conflicting module modes. + - Applied and kept: `lib` includes `DOM.Iterable`, `moduleDetection: force`, `noImplicitOverride: true`. + - Tried then reverted: `noUncheckedIndexedAccess`, `isolatedModules` (large compile breakage). + - Validation: `bun run tsc --noEmit` passed. + - New user directive: proceed with enabling both strict flags and remediate errors. + - Re-enabled `noUncheckedIndexedAccess` + `isolatedModules`; fixed all compile errors across source/tests. + - Final validation: `bun run tsc --noEmit` and `bun run test:fast` both pass. +- touched_files: + - `tsconfig.json` + - `docs/subagents/INDEX.md` + - `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` diff --git a/src/anki-integration.ts b/src/anki-integration.ts index 2444b9d..297be01 100644 --- a/src/anki-integration.ts +++ b/src/anki-integration.ts @@ -411,7 +411,7 @@ export class AnkiIntegration { return; } - const noteInfo = notesInfo[0]; + const noteInfo = notesInfo[0]!; this.appendKnownWordsFromNoteInfo(noteInfo); const fields = this.extractFields(noteInfo.fields); @@ -1000,13 +1000,13 @@ export class AnkiIntegration { private extractLastSoundTag(value: string): string { const matches = value.match(/\[sound:[^\]]+\]/g); if (!matches || matches.length === 0) return ''; - return matches[matches.length - 1]; + return matches[matches.length - 1]!; } private extractLastImageTag(value: string): string { const matches = value.match(/]*>/gi); if (!matches || matches.length === 0) return ''; - return matches[matches.length - 1]; + return matches[matches.length - 1]!; } private extractImageTags(value: string): string[] { @@ -1472,7 +1472,7 @@ export class AnkiIntegration { log.warn('Keep note not found:', keepNoteId); return; } - const keepNoteInfo = keepNotesInfo[0]; + const keepNoteInfo = keepNotesInfo[0]!; const mergedFields = await this.computeFieldGroupingMergedFields( keepNoteId, deleteNoteId, @@ -1539,7 +1539,7 @@ export class AnkiIntegration { if (!originalNotesInfo || originalNotesInfo.length === 0) { return false; } - const originalNoteInfo = originalNotesInfo[0]; + const originalNoteInfo = originalNotesInfo[0]!; const sentenceCardConfig = this.getEffectiveSentenceCardConfig(); const originalFields = this.extractFields(originalNoteInfo.fields); diff --git a/src/anki-integration/card-creation.ts b/src/anki-integration/card-creation.ts index 03cc4bc..d81d9fd 100644 --- a/src/anki-integration/card-creation.ts +++ b/src/anki-integration/card-creation.ts @@ -199,7 +199,7 @@ export class CardCreationService { return; } - const noteInfo = notesInfoResult[0]; + const noteInfo = notesInfoResult[0]!; const fields = this.deps.extractFields(noteInfo.fields); const expressionText = fields.expression || fields.word || ''; const sentenceAudioField = this.getResolvedSentenceAudioFieldName(noteInfo); @@ -366,7 +366,7 @@ export class CardCreationService { return; } - const noteInfo = notesInfoResult[0]; + const noteInfo = notesInfoResult[0]!; const fields = this.deps.extractFields(noteInfo.fields); const expressionText = fields.expression || fields.word || ''; @@ -536,7 +536,7 @@ export class CardCreationService { const noteInfoResult = await this.deps.client.notesInfo([noteId]); const noteInfos = noteInfoResult as CardCreationNoteInfo[]; if (noteInfos.length > 0) { - const createdNoteInfo = noteInfos[0]; + const createdNoteInfo = noteInfos[0]!; this.deps.appendKnownWordsFromNoteInfo(createdNoteInfo); resolvedSentenceAudioField = this.deps.resolveNoteFieldName(createdNoteInfo, audioFieldName) || audioFieldName; diff --git a/src/anki-integration/duplicate.ts b/src/anki-integration/duplicate.ts index 1848dbd..23b33cb 100644 --- a/src/anki-integration/duplicate.ts +++ b/src/anki-integration/duplicate.ts @@ -23,7 +23,7 @@ export async function findDuplicateNote( ): Promise { let fieldName = ''; for (const name of Object.keys(noteInfo.fields)) { - if (['word', 'expression'].includes(name.toLowerCase()) && noteInfo.fields[name].value) { + if (['word', 'expression'].includes(name.toLowerCase()) && noteInfo.fields[name]?.value) { fieldName = name; break; } diff --git a/src/anki-integration/field-grouping.ts b/src/anki-integration/field-grouping.ts index 1d32f5e..becb2f2 100644 --- a/src/anki-integration/field-grouping.ts +++ b/src/anki-integration/field-grouping.ts @@ -100,7 +100,7 @@ export class FieldGroupingService { this.deps.showOsdNotification('Card not found'); return; } - const noteInfoBeforeUpdate = notesInfo[0]; + const noteInfoBeforeUpdate = notesInfo[0]!; const fields = this.deps.extractFields(noteInfoBeforeUpdate.fields); const expressionText = fields.expression || fields.word || ''; if (!expressionText) { @@ -135,7 +135,7 @@ export class FieldGroupingService { return; } - const noteInfo = refreshedInfo[0]; + const noteInfo = refreshedInfo[0]!; if (sentenceCardConfig.kikuFieldGrouping === 'auto') { await this.deps.handleFieldGroupingAuto( diff --git a/src/anki-integration/known-word-cache.ts b/src/anki-integration/known-word-cache.ts index 323eece..b693fb8 100644 --- a/src/anki-integration/known-word-cache.ts +++ b/src/anki-integration/known-word-cache.ts @@ -236,7 +236,7 @@ export class KnownWordCacheManager { } if (decks.length === 1) { - return `deck:"${escapeAnkiSearchValue(decks[0])}"`; + return `deck:"${escapeAnkiSearchValue(decks[0]!)}"`; } const deckQueries = decks.map((deck) => `deck:"${escapeAnkiSearchValue(deck)}"`); diff --git a/src/cli/args.ts b/src/cli/args.ts index 5d5a548..a031d54 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -115,7 +115,7 @@ export function parseArgs(argv: string[]): CliArgs { for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; - if (!arg.startsWith('--')) continue; + if (!arg || !arg.startsWith('--')) continue; if (arg === '--background') args.background = true; else if (arg === '--start') args.start = true; diff --git a/src/config/path-resolution.ts b/src/config/path-resolution.ts index 50cc9b7..62ba79e 100644 --- a/src/config/path-resolution.ts +++ b/src/config/path-resolution.ts @@ -54,7 +54,7 @@ export function resolveConfigDir(options: ConfigPathOptions): string { } } - return path.join(baseDirs[0], getDefaultAppName(options)); + return path.join(baseDirs[0]!, getDefaultAppName(options)); } export function resolveConfigFilePath(options: ConfigPathOptions): string { @@ -72,5 +72,5 @@ export function resolveConfigFilePath(options: ConfigPathOptions): string { } } - return path.join(baseDirs[0], getDefaultAppName(options), DEFAULT_FILE_NAMES[0]); + return path.join(baseDirs[0]!, getDefaultAppName(options), DEFAULT_FILE_NAMES[0]!); } diff --git a/src/core/services/anilist/anilist-updater.ts b/src/core/services/anilist/anilist-updater.ts index 2dd1b13..4012651 100644 --- a/src/core/services/anilist/anilist-updater.ts +++ b/src/core/services/anilist/anilist-updater.ts @@ -168,7 +168,7 @@ function pickBestSearchResult( return titles.includes(normalizedTarget); }); - const selected = exact ?? candidates[0]; + const selected = exact ?? candidates[0]!; const selectedTitle = selected.title?.english || selected.title?.romaji || selected.title?.native || title; return { id: selected.id, title: selectedTitle }; diff --git a/src/core/services/anki-jimaku.test.ts b/src/core/services/anki-jimaku.test.ts index 32ef750..8d6c143 100644 --- a/src/core/services/anki-jimaku.test.ts +++ b/src/core/services/anki-jimaku.test.ts @@ -129,7 +129,7 @@ test('refreshKnownWords throws when integration is unavailable', async () => { await assert.rejects( async () => { - await registered.refreshKnownWords(); + await registered.refreshKnownWords!(); }, { message: 'AnkiConnect integration not enabled' }, ); @@ -144,7 +144,7 @@ test('refreshKnownWords delegates to integration', async () => { }, }; - await registered.refreshKnownWords(); + await registered.refreshKnownWords!(); assert.equal(refreshed, 1); }); @@ -158,7 +158,7 @@ test('setAnkiConnectEnabled disables active integration and broadcasts changes', }, }; - registered.setAnkiConnectEnabled(false); + registered.setAnkiConnectEnabled!(false); assert.deepEqual(state.patches, [false]); assert.equal(destroyed, 1); @@ -188,8 +188,8 @@ test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () = deleteDuplicate: true, cancelled: false, }; - registered.clearAnkiHistory(); - registered.respondFieldGrouping(choice); + registered.clearAnkiHistory!(); + registered.respondFieldGrouping!(choice); options.getSubtitleTimingTracker = originalGetTracker; @@ -201,7 +201,7 @@ test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () = test('buildKikuMergePreview returns guard error when integration is missing', async () => { const { registered } = createHarness(); - const result = await registered.buildKikuMergePreview({ + const result = await registered.buildKikuMergePreview!({ keepNoteId: 1, deleteNoteId: 2, deleteDuplicate: false, @@ -227,7 +227,7 @@ test('buildKikuMergePreview delegates to integration when available', async () = }, }; - const result = await registered.buildKikuMergePreview({ + const result = await registered.buildKikuMergePreview!({ keepNoteId: 3, deleteNoteId: 4, deleteDuplicate: true, @@ -240,7 +240,7 @@ test('buildKikuMergePreview delegates to integration when available', async () = test('searchJimakuEntries caps results and onDownloadedSubtitle sends sub-add to mpv', async () => { const { registered, state } = createHarness(); - const searchResult = await registered.searchJimakuEntries({ query: 'test' }); + const searchResult = await registered.searchJimakuEntries!({ query: 'test' }); assert.deepEqual(state.fetchCalls, [ { endpoint: '/api/entries/search', @@ -250,6 +250,6 @@ test('searchJimakuEntries caps results and onDownloadedSubtitle sends sub-add to assert.equal((searchResult as { ok: boolean }).ok, true); assert.equal((searchResult as { data: unknown[] }).data.length, 2); - registered.onDownloadedSubtitle('/tmp/subtitle.ass'); + registered.onDownloadedSubtitle!('/tmp/subtitle.ass'); assert.deepEqual(state.sentCommands, [{ command: ['sub-add', '/tmp/subtitle.ass', 'select'] }]); }); diff --git a/src/core/services/app-ready.test.ts b/src/core/services/app-ready.test.ts index ffe723a..eee41cb 100644 --- a/src/core/services/app-ready.test.ts +++ b/src/core/services/app-ready.test.ts @@ -209,30 +209,31 @@ test('runAppReadyRuntime aggregates multiple critical anki mapping errors', asyn await runAppReadyRuntime(deps); + const firstErrorSet = capturedErrors[0]!; assert.equal(capturedErrors.length, 1); - assert.equal(capturedErrors[0].length, 5); + assert.equal(firstErrorSet.length, 5); assert.ok( - capturedErrors[0].includes( + firstErrorSet.includes( 'ankiConnect.fields.audio must be a non-empty string when ankiConnect is enabled.', ), ); assert.ok( - capturedErrors[0].includes( + firstErrorSet.includes( 'ankiConnect.fields.image must be a non-empty string when ankiConnect is enabled.', ), ); assert.ok( - capturedErrors[0].includes( + firstErrorSet.includes( 'ankiConnect.fields.sentence must be a non-empty string when ankiConnect is enabled.', ), ); assert.ok( - capturedErrors[0].includes( + firstErrorSet.includes( 'ankiConnect.fields.miscInfo must be a non-empty string when ankiConnect is enabled.', ), ); assert.ok( - capturedErrors[0].includes( + firstErrorSet.includes( 'ankiConnect.fields.translation must be a non-empty string when ankiConnect is enabled.', ), ); diff --git a/src/core/services/immersion-tracker-service.test.ts b/src/core/services/immersion-tracker-service.test.ts index 891f51c..13eba46 100644 --- a/src/core/services/immersion-tracker-service.test.ts +++ b/src/core/services/immersion-tracker-service.test.ts @@ -124,8 +124,8 @@ testIfSqlite('persists and retrieves minimum immersion tracking fields', async ( const summaries = await tracker.getSessionSummaries(10); assert.ok(summaries.length >= 1); - assert.ok(summaries[0].linesSeen >= 1); - assert.ok(summaries[0].cardsMined >= 2); + assert.ok(summaries[0]!.linesSeen >= 1); + assert.ok(summaries[0]!.cardsMined >= 2); tracker.destroy(); @@ -376,8 +376,8 @@ testIfSqlite('monthly rollups are grouped by calendar month', async () => { const videoRows = rows.filter((row) => row.videoId === 1); assert.equal(videoRows.length, 2); - assert.equal(videoRows[0].rollupDayOrMonth, 202602); - assert.equal(videoRows[1].rollupDayOrMonth, 202601); + assert.equal(videoRows[0]!.rollupDayOrMonth, 202602); + assert.equal(videoRows[1]!.rollupDayOrMonth, 202601); } finally { tracker?.destroy(); cleanupDbPath(dbPath); diff --git a/src/core/services/immersion-tracker-service.ts b/src/core/services/immersion-tracker-service.ts index efa3332..56b6e38 100644 --- a/src/core/services/immersion-tracker-service.ts +++ b/src/core/services/immersion-tracker-service.ts @@ -1443,7 +1443,7 @@ export class ImmersionTrackerService { const parsed = new URL(mediaPath); const parts = parsed.pathname.split('/').filter(Boolean); if (parts.length > 0) { - const leaf = decodeURIComponent(parts[parts.length - 1]); + const leaf = decodeURIComponent(parts[parts.length - 1]!); return this.normalizeText(leaf.replace(/\.[^/.]+$/, '')); } return this.normalizeText(parsed.hostname) || 'unknown'; diff --git a/src/core/services/index.ts b/src/core/services/index.ts index 5dd66e0..5bcf1c7 100644 --- a/src/core/services/index.ts +++ b/src/core/services/index.ts @@ -35,7 +35,6 @@ export { createFrequencyDictionaryLookup } from './frequency-dictionary'; export { createJlptVocabularyLookup } from './jlpt-vocab'; export { getIgnoredPos1Entries, - JlptIgnoredPos1Entry, JLPT_EXCLUDED_TERMS, JLPT_IGNORED_MECAB_POS1, JLPT_IGNORED_MECAB_POS1_ENTRIES, @@ -43,6 +42,7 @@ export { shouldIgnoreJlptByTerm, shouldIgnoreJlptForMecabPos1, } from './jlpt-token-filter'; +export type { JlptIgnoredPos1Entry } from './jlpt-token-filter'; export { loadYomitanExtension } from './yomitan-extension-loader'; export { getJimakuLanguagePreference, @@ -72,8 +72,6 @@ export { export { MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY, MpvIpcClient, - MpvRuntimeClientLike, - MpvTrackProperty, playNextSubtitleRuntime, replayCurrentSubtitleRuntime, resolveCurrentAudioStreamIndex, @@ -81,6 +79,7 @@ export { setMpvSubVisibilityRuntime, showMpvOsdRuntime, } from './mpv'; +export type { MpvRuntimeClientLike, MpvTrackProperty } from './mpv'; export { applyMpvSubtitleRenderMetricsPatch, DEFAULT_MPV_SUBTITLE_RENDER_METRICS, diff --git a/src/core/services/jellyfin-remote.test.ts b/src/core/services/jellyfin-remote.test.ts index 4765644..071bada 100644 --- a/src/core/services/jellyfin-remote.test.ts +++ b/src/core/services/jellyfin-remote.test.ts @@ -70,11 +70,11 @@ test('start posts capabilities on socket connect', async () => { }); service.start(); - sockets[0].emit('open'); + sockets[0]!.emit('open'); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(fetchCalls.length, 1); - assert.equal(fetchCalls[0].input, 'http://jellyfin.local:8096/Sessions/Capabilities/Full'); + assert.equal(fetchCalls[0]!.input, 'http://jellyfin.local:8096/Sessions/Capabilities/Full'); assert.equal(service.isConnected(), true); }); @@ -97,9 +97,9 @@ test('socket headers include jellyfin authorization metadata', () => { service.start(); assert.equal(seenHeaders.length, 1); - assert.ok(seenHeaders[0].Authorization.includes('Client="SubMiner"')); - assert.ok(seenHeaders[0].Authorization.includes('DeviceId="device-auth"')); - assert.ok(seenHeaders[0]['X-Emby-Authorization']); + assert.ok(seenHeaders[0]!['Authorization']!.includes('Client="SubMiner"')); + assert.ok(seenHeaders[0]!['Authorization']!.includes('DeviceId="device-auth"')); + assert.ok(seenHeaders[0]!['X-Emby-Authorization']); }); test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => { @@ -124,7 +124,7 @@ test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => { }); service.start(); - const socket = sockets[0]; + const socket = sockets[0]!; socket.emit('message', JSON.stringify({ MessageType: 'Play', Data: { ItemId: 'movie-1' } })); socket.emit( 'message', @@ -174,13 +174,13 @@ test('schedules reconnect with bounded exponential backoff', () => { }); service.start(); - sockets[0].emit('close'); + sockets[0]!.emit('close'); pendingTimers.shift()?.(); - sockets[1].emit('close'); + sockets[1]!.emit('close'); pendingTimers.shift()?.(); - sockets[2].emit('close'); + sockets[2]!.emit('close'); pendingTimers.shift()?.(); - sockets[3].emit('close'); + sockets[3]!.emit('close'); assert.deepEqual(delays, [100, 200, 400, 400]); assert.equal(sockets.length, 4); @@ -216,7 +216,7 @@ test('Jellyfin remote stop prevents further reconnect/network activity', () => { service.start(); assert.equal(sockets.length, 1); - sockets[0].emit('close'); + sockets[0]!.emit('close'); assert.equal(pendingTimers.length, 1); service.stop(); @@ -252,7 +252,7 @@ test('reportProgress posts timeline payload and treats failure as non-fatal', as }); service.start(); - sockets[0].emit('open'); + sockets[0]!.emit('open'); await new Promise((resolve) => setTimeout(resolve, 0)); const expectedPayload = buildJellyfinTimelinePayload({ @@ -310,7 +310,7 @@ test('advertiseNow validates server registration using Sessions endpoint', async }); service.start(); - sockets[0].emit('open'); + sockets[0]!.emit('open'); const ok = await service.advertiseNow(); assert.equal(ok, true); assert.ok(calls.some((url) => url.endsWith('/Sessions'))); diff --git a/src/core/services/jellyfin.test.ts b/src/core/services/jellyfin.test.ts index 8252320..1c84bf9 100644 --- a/src/core/services/jellyfin.test.ts +++ b/src/core/services/jellyfin.test.ts @@ -119,7 +119,7 @@ test('listItems supports search and formats title', async () => { limit: 25, }, ); - assert.equal(items[0].title, 'Space Show S01E02 Pilot'); + assert.equal(items[0]!.title, 'Space Show S01E02 Pilot'); } finally { globalThis.fetch = originalFetch; } @@ -338,14 +338,14 @@ test('listSubtitleTracks returns all subtitle streams with delivery urls', async [2, 3, 4], ); assert.equal( - tracks[0].deliveryUrl, + tracks[0]!.deliveryUrl, 'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/2/Stream.srt?api_key=token', ); assert.equal( - tracks[1].deliveryUrl, + tracks[1]!.deliveryUrl, 'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/3/Stream.srt?api_key=token', ); - assert.equal(tracks[2].deliveryUrl, 'https://cdn.example.com/subs.srt'); + assert.equal(tracks[2]!.deliveryUrl, 'https://cdn.example.com/subs.srt'); } finally { globalThis.fetch = originalFetch; } @@ -556,9 +556,9 @@ test('listSubtitleTracks falls back from PlaybackInfo to item media sources', as ); assert.equal(requestCount, 2); assert.equal(tracks.length, 1); - assert.equal(tracks[0].index, 11); + assert.equal(tracks[0]!.index, 11); assert.equal( - tracks[0].deliveryUrl, + tracks[0]!.deliveryUrl, 'http://jellyfin.local/Videos/movie-fallback/ms-fallback/Subtitles/11/Stream.srt?api_key=token', ); } finally { diff --git a/src/core/services/mpv-protocol.test.ts b/src/core/services/mpv-protocol.test.ts index 14097f2..110685d 100644 --- a/src/core/services/mpv-protocol.test.ts +++ b/src/core/services/mpv-protocol.test.ts @@ -177,8 +177,8 @@ test('splitMpvMessagesFromBuffer parses complete lines and preserves partial buf assert.equal(parsed.messages.length, 2); assert.equal(parsed.nextBuffer, '{"partial"'); - assert.equal(parsed.messages[0].event, 'shutdown'); - assert.equal(parsed.messages[1].name, 'media-title'); + assert.equal(parsed.messages[0]!.event, 'shutdown'); + assert.equal(parsed.messages[1]!.name, 'media-title'); }); test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => { @@ -189,7 +189,7 @@ test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => { }); assert.equal(errors.length, 1); - assert.equal(errors[0].line, '{invalid}'); + assert.equal(errors[0]!.line, '{invalid}'); }); test('visibility and boolean parsers handle text values', () => { diff --git a/src/core/services/mpv-transport.test.ts b/src/core/services/mpv-transport.test.ts index 1f496c7..b602b61 100644 --- a/src/core/services/mpv-transport.test.ts +++ b/src/core/services/mpv-transport.test.ts @@ -84,11 +84,11 @@ test('scheduleMpvReconnect clears existing timer and increments attempt', () => assert.equal(nextAttempt, 4); assert.equal(cleared.length, 1); - assert.equal(cleared[0], existing); + assert.equal(cleared[0]!, existing); assert.equal(setTimers.length, 1); assert.equal(calls.length, 1); - assert.equal(calls[0].attempt, 4); - assert.equal(calls[0].delay, getMpvReconnectDelay(3, true)); + assert.equal(calls[0]!.attempt, 4); + assert.equal(calls[0]!.delay, getMpvReconnectDelay(3, true)); assert.equal(connected, 1); }); diff --git a/src/core/services/mpv.test.ts b/src/core/services/mpv.test.ts index feffc6c..98816ce 100644 --- a/src/core/services/mpv.test.ts +++ b/src/core/services/mpv.test.ts @@ -54,8 +54,8 @@ test('MpvIpcClient handles sub-text property change and broadcasts tokenized sub }); assert.equal(events.length, 1); - assert.equal(events[0].text, '字幕'); - assert.equal(events[0].isOverlayVisible, false); + assert.equal(events[0]!.text, '字幕'); + assert.equal(events[0]!.isOverlayVisible, false); }); test('MpvIpcClient parses JSON line protocol in processBuffer', () => { @@ -70,8 +70,8 @@ test('MpvIpcClient parses JSON line protocol in processBuffer', () => { (client as any).processBuffer(); assert.equal(seen.length, 2); - assert.equal(seen[0].name, 'path'); - assert.equal(seen[1].request_id, 1); + assert.equal(seen[0]!.name, 'path'); + assert.equal(seen[1]!.request_id, 1); assert.equal((client as any).buffer, '{"partial":'); }); diff --git a/src/core/services/overlay-content-measurement.test.ts b/src/core/services/overlay-content-measurement.test.ts index f01a27a..43e0fa4 100644 --- a/src/core/services/overlay-content-measurement.test.ts +++ b/src/core/services/overlay-content-measurement.test.ts @@ -83,5 +83,5 @@ test('overlay measurement store rate-limits invalid payload warnings', () => { now = 11_000; store.report({ layer: 'visible' }); assert.equal(warnings.length, 1); - assert.match(warnings[0], /Dropped 3 invalid measurement payload/); + assert.match(warnings[0]!, /Dropped 3 invalid measurement payload/); }); diff --git a/src/core/services/overlay-drop.ts b/src/core/services/overlay-drop.ts index 93b57c4..8698307 100644 --- a/src/core/services/overlay-drop.ts +++ b/src/core/services/overlay-drop.ts @@ -23,7 +23,7 @@ const VIDEO_EXTENSIONS = new Set([ ]); function getPathExtension(pathValue: string): string { - const normalized = pathValue.split(/[?#]/, 1)[0]; + const normalized = pathValue.split(/[?#]/, 1)[0] ?? ''; const dot = normalized.lastIndexOf('.'); return dot >= 0 ? normalized.slice(dot).toLowerCase() : ''; } diff --git a/src/core/services/shortcut-fallback.ts b/src/core/services/shortcut-fallback.ts index 61fa002..d89a567 100644 --- a/src/core/services/shortcut-fallback.ts +++ b/src/core/services/shortcut-fallback.ts @@ -26,7 +26,7 @@ export function shortcutMatchesInputForLocalFallback( const parts = normalized.split('+').filter(Boolean); if (parts.length === 0) return false; - const keyToken = parts[parts.length - 1]; + const keyToken = parts[parts.length - 1]!; const modifierTokens = new Set(parts.slice(0, -1)); const allowedModifiers = new Set(['shift', 'alt', 'meta', 'control', 'commandorcontrol']); for (const token of modifierTokens) { diff --git a/src/core/services/subsync.test.ts b/src/core/services/subsync.test.ts index e8b1739..d33be08 100644 --- a/src/core/services/subsync.test.ts +++ b/src/core/services/subsync.test.ts @@ -339,6 +339,6 @@ test('runSubsyncManual resolves string sid values from mpv stream properties', a assert.equal(syncOutputIndex >= 0, true); const outputPath = ffArgs[syncOutputIndex + 1]; assert.equal(typeof outputPath, 'string'); - assert.ok(outputPath.length > 0); - assert.equal(fs.readFileSync(outputPath, 'utf8'), ''); + assert.ok(outputPath!.length > 0); + assert.equal(fs.readFileSync(outputPath!, 'utf8'), ''); }); diff --git a/src/core/services/subtitle-position.ts b/src/core/services/subtitle-position.ts index 89d5a15..9974367 100644 --- a/src/core/services/subtitle-position.ts +++ b/src/core/services/subtitle-position.ts @@ -28,7 +28,8 @@ export function cycleSecondarySubMode(deps: CycleSecondarySubModeDeps): void { const currentMode = deps.getSecondarySubMode(); const currentIndex = SECONDARY_SUB_CYCLE.indexOf(currentMode); - const nextMode = SECONDARY_SUB_CYCLE[(currentIndex + 1) % SECONDARY_SUB_CYCLE.length]; + const safeIndex = currentIndex >= 0 ? currentIndex : 0; + const nextMode = SECONDARY_SUB_CYCLE[(safeIndex + 1) % SECONDARY_SUB_CYCLE.length]!; deps.setSecondarySubMode(nextMode); deps.broadcastSecondarySubMode(nextMode); deps.showMpvOsd(`Secondary subtitle: ${nextMode}`); diff --git a/src/core/services/subtitle-ws.ts b/src/core/services/subtitle-ws.ts index 869b59e..f416b4f 100644 --- a/src/core/services/subtitle-ws.ts +++ b/src/core/services/subtitle-ws.ts @@ -82,8 +82,9 @@ export function serializeSubtitleMarkup( const klass = computeWordClass(token, options); const parts = token.surface.split('\n'); for (let index = 0; index < parts.length; index += 1) { - if (parts[index]) { - chunks.push(`${escapeHtml(parts[index])}`); + const part = parts[index]; + if (part) { + chunks.push(`${escapeHtml(part)}`); } if (index < parts.length - 1) { chunks.push('
'); diff --git a/src/core/services/texthooker.ts b/src/core/services/texthooker.ts index 19f78e0..6d9e6ff 100644 --- a/src/core/services/texthooker.ts +++ b/src/core/services/texthooker.ts @@ -20,7 +20,7 @@ export class Texthooker { } this.server = http.createServer((req, res) => { - const urlPath = (req.url || '/').split('?')[0]; + const urlPath = (req.url || '/').split('?')[0] ?? '/'; const filePath = path.join(texthookerPath, urlPath === '/' ? 'index.html' : urlPath); const ext = path.extname(filePath); diff --git a/src/core/services/tokenizer.ts b/src/core/services/tokenizer.ts index b88a0d2..c1d0644 100644 --- a/src/core/services/tokenizer.ts +++ b/src/core/services/tokenizer.ts @@ -377,7 +377,7 @@ function isRepeatedKanaSfx(text: string): boolean { let hasAdjacentRepeat = false; for (let i = 0; i < chars.length; i += 1) { - const char = chars[i]; + const char = chars[i]!; counts.set(char, (counts.get(char) ?? 0) + 1); if (i > 0 && chars[i] === chars[i - 1]) { hasAdjacentRepeat = true; diff --git a/src/core/services/yomitan-settings.ts b/src/core/services/yomitan-settings.ts index a01da2e..ae86c1b 100644 --- a/src/core/services/yomitan-settings.ts +++ b/src/core/services/yomitan-settings.ts @@ -73,7 +73,8 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti setTimeout(() => { if (!settingsWindow.isDestroyed()) { - settingsWindow.setSize(settingsWindow.getSize()[0], settingsWindow.getSize()[1]); + const [width = 0, height = 0] = settingsWindow.getSize(); + settingsWindow.setSize(width, height); settingsWindow.webContents.invalidate(); settingsWindow.show(); } diff --git a/src/jimaku/utils.ts b/src/jimaku/utils.ts index 67ec7a5..ac1b382 100644 --- a/src/jimaku/utils.ts +++ b/src/jimaku/utils.ts @@ -51,7 +51,7 @@ function getRetryAfter(headers: http.IncomingHttpHeaders): number | undefined { const value = headers['x-ratelimit-reset-after']; if (!value) return undefined; const raw = Array.isArray(value) ? value[0] : value; - const parsed = Number.parseFloat(raw); + const parsed = Number.parseFloat(raw!); if (!Number.isFinite(parsed)) return undefined; return parsed; } @@ -146,8 +146,8 @@ function matchEpisodeFromName(name: string): { const seasonEpisode = name.match(/S(\d{1,2})E(\d{1,3})/i); if (seasonEpisode && seasonEpisode.index !== undefined) { return { - season: Number.parseInt(seasonEpisode[1], 10), - episode: Number.parseInt(seasonEpisode[2], 10), + season: Number.parseInt(seasonEpisode[1]!, 10), + episode: Number.parseInt(seasonEpisode[2]!, 10), index: seasonEpisode.index, confidence: 'high', }; @@ -156,8 +156,8 @@ function matchEpisodeFromName(name: string): { const alt = name.match(/(\d{1,2})x(\d{1,3})/i); if (alt && alt.index !== undefined) { return { - season: Number.parseInt(alt[1], 10), - episode: Number.parseInt(alt[2], 10), + season: Number.parseInt(alt[1]!, 10), + episode: Number.parseInt(alt[2]!, 10), index: alt.index, confidence: 'high', }; @@ -167,7 +167,7 @@ function matchEpisodeFromName(name: string): { if (epOnly && epOnly.index !== undefined) { return { season: null, - episode: Number.parseInt(epOnly[1], 10), + episode: Number.parseInt(epOnly[1]!, 10), index: epOnly.index, confidence: 'medium', }; @@ -177,7 +177,7 @@ function matchEpisodeFromName(name: string): { if (numeric && numeric.index !== undefined) { return { season: null, - episode: Number.parseInt(numeric[1], 10), + episode: Number.parseInt(numeric[1]!, 10), index: numeric.index, confidence: 'medium', }; @@ -190,7 +190,7 @@ function detectSeasonFromDir(mediaPath: string): number | null { const parent = path.basename(path.dirname(mediaPath)); const match = parent.match(/(?:Season|S)\s*(\d{1,2})/i); if (!match) return null; - const parsed = Number.parseInt(match[1], 10); + const parsed = Number.parseInt(match[1]!, 10); return Number.isFinite(parsed) ? parsed : null; } diff --git a/src/main/runtime/anilist-setup-window.test.ts b/src/main/runtime/anilist-setup-window.test.ts index 127d550..96b37d3 100644 --- a/src/main/runtime/anilist-setup-window.test.ts +++ b/src/main/runtime/anilist-setup-window.test.ts @@ -29,7 +29,7 @@ test('manual anilist setup submission forwards access token to callback consumer const handled = handleSubmission('subminer://anilist-setup?access_token=abc123'); assert.equal(handled, true); assert.equal(consumed.length, 1); - assert.ok(consumed[0].includes('https://anilist.subminer.moe/#access_token=abc123')); + assert.ok(consumed[0]!.includes('https://anilist.subminer.moe/#access_token=abc123')); }); test('maybe focus anilist setup window focuses existing window', () => { @@ -179,7 +179,7 @@ test('anilist setup did-fail-load handler forwards details', () => { }); assert.equal(seen.length, 1); - assert.equal(seen[0].errorCode, -3); + assert.equal(seen[0]!.errorCode, -3); }); test('anilist setup did-finish-load handler triggers fallback on blank page', () => { diff --git a/src/main/runtime/jellyfin-remote-connection.test.ts b/src/main/runtime/jellyfin-remote-connection.test.ts index 3fe4502..f51cf6c 100644 --- a/src/main/runtime/jellyfin-remote-connection.test.ts +++ b/src/main/runtime/jellyfin-remote-connection.test.ts @@ -49,8 +49,8 @@ test('createLaunchMpvIdleForJellyfinPlaybackHandler builds expected mpv args', ( launch(); assert.equal(spawnedArgs.length, 1); - assert.ok(spawnedArgs[0].includes('--idle=yes')); - assert.ok(spawnedArgs[0].some((arg) => arg.includes('--input-ipc-server=/tmp/subminer.sock'))); + assert.ok(spawnedArgs[0]!.includes('--idle=yes')); + assert.ok(spawnedArgs[0]!.some((arg) => arg.includes('--input-ipc-server=/tmp/subminer.sock'))); assert.ok(logs.some((entry) => entry.includes('Launched mpv for Jellyfin playback'))); }); diff --git a/src/main/runtime/tray-runtime.test.ts b/src/main/runtime/tray-runtime.test.ts index 2e87262..6d9821c 100644 --- a/src/main/runtime/tray-runtime.test.ts +++ b/src/main/runtime/tray-runtime.test.ts @@ -38,8 +38,8 @@ test('tray menu template contains expected entries and handlers', () => { }); assert.equal(template.length, 7); - template[0].click?.(); - template[5].type === 'separator' ? calls.push('separator') : calls.push('bad'); - template[6].click?.(); + template[0]!.click?.(); + template[5]!.type === 'separator' ? calls.push('separator') : calls.push('bad'); + template[6]!.click?.(); assert.deepEqual(calls, ['overlay', 'separator', 'quit']); }); diff --git a/src/media-generator.ts b/src/media-generator.ts index ef5dab6..8268b27 100644 --- a/src/media-generator.ts +++ b/src/media-generator.ts @@ -180,7 +180,7 @@ export class MediaGenerator { ): Promise { const { format, quality = 92, maxWidth, maxHeight } = options; const ext = format === 'webp' ? 'webp' : format === 'png' ? 'png' : 'jpg'; - const codecMap: Record = { + const codecMap: Record<'jpg' | 'png' | 'webp', string> = { jpg: 'mjpeg', png: 'png', webp: 'webp', diff --git a/src/renderer/error-recovery.test.ts b/src/renderer/error-recovery.test.ts index c8b86cb..30bcae0 100644 --- a/src/renderer/error-recovery.test.ts +++ b/src/renderer/error-recovery.test.ts @@ -41,7 +41,7 @@ test('handleError logs context and recovers overlay state', () => { assert.equal(dismissed, 1); assert.equal(restored, 1); assert.equal(shown.length, 1); - assert.match(shown[0], /recovered/i); + assert.match(shown[0]!, /recovered/i); assert.equal(payloads.length, 1); const payload = payloads[0] as { diff --git a/src/renderer/handlers/mouse.ts b/src/renderer/handlers/mouse.ts index f6c79e6..e5d7d50 100644 --- a/src/renderer/handlers/mouse.ts +++ b/src/renderer/handlers/mouse.ts @@ -126,12 +126,12 @@ export function createMouseHandlers( if (!probeChar || isBoundary(probeChar)) return null; let start = probeIndex; - while (start > 0 && !isBoundary(text[start - 1])) { + while (start > 0 && !isBoundary(text[start - 1] ?? '')) { start -= 1; } let end = probeIndex + 1; - while (end < text.length && !isBoundary(text[end])) { + while (end < text.length && !isBoundary(text[end] ?? '')) { end += 1; } diff --git a/src/renderer/modals/jimaku.ts b/src/renderer/modals/jimaku.ts index a28d71d..f105eed 100644 --- a/src/renderer/modals/jimaku.ts +++ b/src/renderer/modals/jimaku.ts @@ -204,7 +204,7 @@ export function createJimakuModal( if (index < 0 || index >= ctx.state.jimakuEntries.length) return; ctx.state.selectedEntryIndex = index; - ctx.state.currentEntryId = ctx.state.jimakuEntries[index].id; + ctx.state.currentEntryId = ctx.state.jimakuEntries[index]!.id; renderEntries(); if (ctx.state.currentEntryId !== null) { @@ -223,7 +223,7 @@ export function createJimakuModal( return; } - const file = ctx.state.jimakuFiles[index]; + const file = ctx.state.jimakuFiles[index]!; setJimakuStatus('Downloading subtitle...'); const result: JimakuDownloadResult = await window.electronAPI.jimakuDownloadFile({ diff --git a/src/renderer/modals/runtime-options.ts b/src/renderer/modals/runtime-options.ts index a7a2564..b069674 100644 --- a/src/renderer/modals/runtime-options.ts +++ b/src/renderer/modals/runtime-options.ts @@ -30,7 +30,7 @@ export function createRuntimeOptionsModal( if (ctx.state.runtimeOptionSelectedIndex >= ctx.state.runtimeOptions.length) { return null; } - return ctx.state.runtimeOptions[ctx.state.runtimeOptionSelectedIndex]; + return ctx.state.runtimeOptions[ctx.state.runtimeOptionSelectedIndex] ?? null; } function renderRuntimeOptionsList(): void { @@ -114,11 +114,11 @@ export function createRuntimeOptionsModal( ? (safeIndex + 1) % option.allowedValues.length : (safeIndex - 1 + option.allowedValues.length) % option.allowedValues.length; - ctx.state.runtimeOptionDraftValues.set(option.id, option.allowedValues[nextIndex]); + const nextValue = option.allowedValues[nextIndex]; + if (nextValue === undefined) return; + ctx.state.runtimeOptionDraftValues.set(option.id, nextValue); renderRuntimeOptionsList(); - setRuntimeOptionsStatus( - `Selected ${option.label}: ${formatRuntimeOptionValue(option.allowedValues[nextIndex])}`, - ); + setRuntimeOptionsStatus(`Selected ${option.label}: ${formatRuntimeOptionValue(nextValue)}`); } async function applySelectedRuntimeOption(): Promise { diff --git a/src/renderer/modals/session-help.ts b/src/renderer/modals/session-help.ts index b517a81..3df690a 100644 --- a/src/renderer/modals/session-help.ts +++ b/src/renderer/modals/session-help.ts @@ -357,7 +357,7 @@ function createSectionNode( list.className = 'session-help-item-list'; section.rows.forEach((row, rowIndex) => { - const globalIndex = globalIndexMap[sectionIndex] + rowIndex; + const globalIndex = (globalIndexMap[sectionIndex] ?? 0) + rowIndex; const button = createShortcutRow(row, globalIndex); list.appendChild(button); }); @@ -406,6 +406,7 @@ export function createSessionHelpModal( item.tabIndex = idx === next ? 0 : -1; }); const activeItem = items[next]; + if (!activeItem) return; activeItem.focus({ preventScroll: true }); activeItem.scrollIntoView({ block: 'nearest', @@ -562,7 +563,7 @@ export function createSessionHelpModal( const shortcutSections = buildOverlayShortcutSections(shortcuts); if (shortcutSections.length > 0) { - shortcutSections[0].title = 'Overlay shortcuts (configurable)'; + shortcutSections[0]!.title = 'Overlay shortcuts (configurable)'; } const colorSection = buildColorSection(styleConfig ?? {}); helpSections = [...bindingSections, ...shortcutSections, colorSection]; diff --git a/src/renderer/subtitle-render.ts b/src/renderer/subtitle-render.ts index 43491f2..41abf36 100644 --- a/src/renderer/subtitle-render.ts +++ b/src/renderer/subtitle-render.ts @@ -145,10 +145,11 @@ function renderWithTokens( if (surface.includes('\n')) { const parts = surface.split('\n'); for (let i = 0; i < parts.length; i += 1) { - if (parts[i]) { + const part = parts[i]; + if (part) { const span = document.createElement('span'); span.className = computeWordClass(token, resolvedFrequencyRenderSettings); - span.textContent = parts[i]; + span.textContent = part; if (token.reading) span.dataset.reading = token.reading; if (token.headword) span.dataset.headword = token.headword; fragment.appendChild(span); @@ -277,7 +278,7 @@ function renderPlainTextPreserveLineBreaks(root: ParentNode, text: string): void const fragment = document.createDocumentFragment(); for (let i = 0; i < lines.length; i += 1) { - fragment.appendChild(document.createTextNode(lines[i])); + fragment.appendChild(document.createTextNode(lines[i] ?? '')); if (i < lines.length - 1) { fragment.appendChild(document.createElement('br')); } @@ -361,8 +362,9 @@ export function createSubtitleRenderer(ctx: RendererContext) { const lines = normalized.split('\n'); for (let i = 0; i < lines.length; i += 1) { - if (lines[i]) { - ctx.dom.secondarySubRoot.appendChild(document.createTextNode(lines[i])); + const line = lines[i]; + if (line) { + ctx.dom.secondarySubRoot.appendChild(document.createTextNode(line)); } if (i < lines.length - 1) { ctx.dom.secondarySubRoot.appendChild(document.createElement('br')); diff --git a/src/runtime-options.ts b/src/runtime-options.ts index 02d64f6..0b1ce70 100644 --- a/src/runtime-options.ts +++ b/src/runtime-options.ts @@ -47,7 +47,7 @@ function setPathValue(target: Record, path: string, value: unkn const parts = path.split('.'); let current = target; for (let i = 0; i < parts.length; i += 1) { - const part = parts[i]; + const part = parts[i]!; const isLeaf = i === parts.length - 1; if (isLeaf) { current[part] = value; @@ -188,7 +188,7 @@ export class RuntimeOptionsManager { direction === 1 ? (safeIndex + 1) % values.length : (safeIndex - 1 + values.length) % values.length; - return this.setOptionValue(id, values[nextIndex]); + return this.setOptionValue(id, values[nextIndex]!); } getEffectiveAnkiConnectConfig(baseConfig?: AnkiConnectConfig): AnkiConnectConfig { diff --git a/src/token-merger.ts b/src/token-merger.ts index 54a89a3..8d7d40f 100644 --- a/src/token-merger.ts +++ b/src/token-merger.ts @@ -307,6 +307,7 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3) let sentenceWordCount = 0; for (let i = start; i < endExclusive; i++) { const token = markedTokens[i]; + if (!token) continue; if (!isSentenceBoundaryToken(token) && token.surface.trim().length > 0) { sentenceWordCount += 1; } @@ -317,8 +318,8 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3) } if (sentenceWordCount >= minimumSentenceWords && sentenceCandidates.length === 1) { - markedTokens[sentenceCandidates[0]] = { - ...markedTokens[sentenceCandidates[0]], + markedTokens[sentenceCandidates[0]!] = { + ...markedTokens[sentenceCandidates[0]!]!, isNPlusOneTarget: true, }; } @@ -326,6 +327,7 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3) for (let i = 0; i < markedTokens.length; i++) { const token = markedTokens[i]; + if (!token) continue; if (isSentenceBoundaryToken(token)) { markSentence(sentenceStart, i); sentenceStart = i + 1; diff --git a/src/window-trackers/macos-tracker.ts b/src/window-trackers/macos-tracker.ts index 6fd2928..13b9272 100644 --- a/src/window-trackers/macos-tracker.ts +++ b/src/window-trackers/macos-tracker.ts @@ -177,10 +177,10 @@ export class MacOSWindowTracker extends BaseWindowTracker { if (result && result !== 'not-found') { const parts = result.split(','); if (parts.length === 4) { - const x = parseInt(parts[0], 10); - const y = parseInt(parts[1], 10); - const width = parseInt(parts[2], 10); - const height = parseInt(parts[3], 10); + const x = parseInt(parts[0]!, 10); + const y = parseInt(parts[1]!, 10); + const width = parseInt(parts[2]!, 10); + const height = parseInt(parts[3]!, 10); if ( Number.isFinite(x) && diff --git a/src/window-trackers/x11-tracker.ts b/src/window-trackers/x11-tracker.ts index 339d8cc..43274d0 100644 --- a/src/window-trackers/x11-tracker.ts +++ b/src/window-trackers/x11-tracker.ts @@ -47,10 +47,10 @@ export function parseX11WindowGeometry(winInfo: string): { return null; } return { - x: parseInt(xMatch[1], 10), - y: parseInt(yMatch[1], 10), - width: parseInt(widthMatch[1], 10), - height: parseInt(heightMatch[1], 10), + x: parseInt(xMatch[1]!, 10), + y: parseInt(yMatch[1]!, 10), + width: parseInt(widthMatch[1]!, 10), + height: parseInt(heightMatch[1]!, 10), }; } @@ -59,7 +59,7 @@ export function parseX11WindowPid(raw: string): number | null { if (!pidMatch) { return null; } - const pid = Number.parseInt(pidMatch[1], 10); + const pid = Number.parseInt(pidMatch[1]!, 10); return Number.isInteger(pid) ? pid : null; } diff --git a/tsconfig.json b/tsconfig.json index 352ec68..2a5c86c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,14 @@ "compilerOptions": { "target": "ES2022", "module": "commonjs", - "lib": ["ES2022", "DOM"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "outDir": "./dist", "rootDir": "./src", "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "moduleDetection": "force", + "isolatedModules": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true,