build(ts): enable noUncheckedIndexedAccess and isolatedModules

This commit is contained in:
2026-02-20 01:50:09 -08:00
parent 06e8223d63
commit a4532a5fa0
45 changed files with 169 additions and 126 deletions

View File

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

View File

@@ -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'] }]);
});

View File

@@ -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.',
),
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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', () => {

View File

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

View File

@@ -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":');
});

View File

@@ -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/);
});

View File

@@ -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() : '';
}

View File

@@ -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) {

View File

@@ -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'), '');
});

View File

@@ -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}`);

View File

@@ -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(`<span class="${klass}">${escapeHtml(parts[index])}</span>`);
const part = parts[index];
if (part) {
chunks.push(`<span class="${klass}">${escapeHtml(part)}</span>`);
}
if (index < parts.length - 1) {
chunks.push('<br>');

View File

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

View File

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

View File

@@ -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();
}