mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
build(ts): enable noUncheckedIndexedAccess and isolatedModules
This commit is contained in:
@@ -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-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-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-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` |
|
||||||
|
|||||||
@@ -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`
|
||||||
@@ -411,7 +411,7 @@ export class AnkiIntegration {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteInfo = notesInfo[0];
|
const noteInfo = notesInfo[0]!;
|
||||||
this.appendKnownWordsFromNoteInfo(noteInfo);
|
this.appendKnownWordsFromNoteInfo(noteInfo);
|
||||||
const fields = this.extractFields(noteInfo.fields);
|
const fields = this.extractFields(noteInfo.fields);
|
||||||
|
|
||||||
@@ -1000,13 +1000,13 @@ export class AnkiIntegration {
|
|||||||
private extractLastSoundTag(value: string): string {
|
private extractLastSoundTag(value: string): string {
|
||||||
const matches = value.match(/\[sound:[^\]]+\]/g);
|
const matches = value.match(/\[sound:[^\]]+\]/g);
|
||||||
if (!matches || matches.length === 0) return '';
|
if (!matches || matches.length === 0) return '';
|
||||||
return matches[matches.length - 1];
|
return matches[matches.length - 1]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractLastImageTag(value: string): string {
|
private extractLastImageTag(value: string): string {
|
||||||
const matches = value.match(/<img\b[^>]*>/gi);
|
const matches = value.match(/<img\b[^>]*>/gi);
|
||||||
if (!matches || matches.length === 0) return '';
|
if (!matches || matches.length === 0) return '';
|
||||||
return matches[matches.length - 1];
|
return matches[matches.length - 1]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractImageTags(value: string): string[] {
|
private extractImageTags(value: string): string[] {
|
||||||
@@ -1472,7 +1472,7 @@ export class AnkiIntegration {
|
|||||||
log.warn('Keep note not found:', keepNoteId);
|
log.warn('Keep note not found:', keepNoteId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keepNoteInfo = keepNotesInfo[0];
|
const keepNoteInfo = keepNotesInfo[0]!;
|
||||||
const mergedFields = await this.computeFieldGroupingMergedFields(
|
const mergedFields = await this.computeFieldGroupingMergedFields(
|
||||||
keepNoteId,
|
keepNoteId,
|
||||||
deleteNoteId,
|
deleteNoteId,
|
||||||
@@ -1539,7 +1539,7 @@ export class AnkiIntegration {
|
|||||||
if (!originalNotesInfo || originalNotesInfo.length === 0) {
|
if (!originalNotesInfo || originalNotesInfo.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const originalNoteInfo = originalNotesInfo[0];
|
const originalNoteInfo = originalNotesInfo[0]!;
|
||||||
const sentenceCardConfig = this.getEffectiveSentenceCardConfig();
|
const sentenceCardConfig = this.getEffectiveSentenceCardConfig();
|
||||||
|
|
||||||
const originalFields = this.extractFields(originalNoteInfo.fields);
|
const originalFields = this.extractFields(originalNoteInfo.fields);
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export class CardCreationService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteInfo = notesInfoResult[0];
|
const noteInfo = notesInfoResult[0]!;
|
||||||
const fields = this.deps.extractFields(noteInfo.fields);
|
const fields = this.deps.extractFields(noteInfo.fields);
|
||||||
const expressionText = fields.expression || fields.word || '';
|
const expressionText = fields.expression || fields.word || '';
|
||||||
const sentenceAudioField = this.getResolvedSentenceAudioFieldName(noteInfo);
|
const sentenceAudioField = this.getResolvedSentenceAudioFieldName(noteInfo);
|
||||||
@@ -366,7 +366,7 @@ export class CardCreationService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteInfo = notesInfoResult[0];
|
const noteInfo = notesInfoResult[0]!;
|
||||||
const fields = this.deps.extractFields(noteInfo.fields);
|
const fields = this.deps.extractFields(noteInfo.fields);
|
||||||
const expressionText = fields.expression || fields.word || '';
|
const expressionText = fields.expression || fields.word || '';
|
||||||
|
|
||||||
@@ -536,7 +536,7 @@ export class CardCreationService {
|
|||||||
const noteInfoResult = await this.deps.client.notesInfo([noteId]);
|
const noteInfoResult = await this.deps.client.notesInfo([noteId]);
|
||||||
const noteInfos = noteInfoResult as CardCreationNoteInfo[];
|
const noteInfos = noteInfoResult as CardCreationNoteInfo[];
|
||||||
if (noteInfos.length > 0) {
|
if (noteInfos.length > 0) {
|
||||||
const createdNoteInfo = noteInfos[0];
|
const createdNoteInfo = noteInfos[0]!;
|
||||||
this.deps.appendKnownWordsFromNoteInfo(createdNoteInfo);
|
this.deps.appendKnownWordsFromNoteInfo(createdNoteInfo);
|
||||||
resolvedSentenceAudioField =
|
resolvedSentenceAudioField =
|
||||||
this.deps.resolveNoteFieldName(createdNoteInfo, audioFieldName) || audioFieldName;
|
this.deps.resolveNoteFieldName(createdNoteInfo, audioFieldName) || audioFieldName;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function findDuplicateNote(
|
|||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
let fieldName = '';
|
let fieldName = '';
|
||||||
for (const name of Object.keys(noteInfo.fields)) {
|
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;
|
fieldName = name;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export class FieldGroupingService {
|
|||||||
this.deps.showOsdNotification('Card not found');
|
this.deps.showOsdNotification('Card not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const noteInfoBeforeUpdate = notesInfo[0];
|
const noteInfoBeforeUpdate = notesInfo[0]!;
|
||||||
const fields = this.deps.extractFields(noteInfoBeforeUpdate.fields);
|
const fields = this.deps.extractFields(noteInfoBeforeUpdate.fields);
|
||||||
const expressionText = fields.expression || fields.word || '';
|
const expressionText = fields.expression || fields.word || '';
|
||||||
if (!expressionText) {
|
if (!expressionText) {
|
||||||
@@ -135,7 +135,7 @@ export class FieldGroupingService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteInfo = refreshedInfo[0];
|
const noteInfo = refreshedInfo[0]!;
|
||||||
|
|
||||||
if (sentenceCardConfig.kikuFieldGrouping === 'auto') {
|
if (sentenceCardConfig.kikuFieldGrouping === 'auto') {
|
||||||
await this.deps.handleFieldGroupingAuto(
|
await this.deps.handleFieldGroupingAuto(
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export class KnownWordCacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (decks.length === 1) {
|
if (decks.length === 1) {
|
||||||
return `deck:"${escapeAnkiSearchValue(decks[0])}"`;
|
return `deck:"${escapeAnkiSearchValue(decks[0]!)}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deckQueries = decks.map((deck) => `deck:"${escapeAnkiSearchValue(deck)}"`);
|
const deckQueries = decks.map((deck) => `deck:"${escapeAnkiSearchValue(deck)}"`);
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
|
|
||||||
for (let i = 0; i < argv.length; i += 1) {
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
if (!arg.startsWith('--')) continue;
|
if (!arg || !arg.startsWith('--')) continue;
|
||||||
|
|
||||||
if (arg === '--background') args.background = true;
|
if (arg === '--background') args.background = true;
|
||||||
else if (arg === '--start') args.start = true;
|
else if (arg === '--start') args.start = true;
|
||||||
|
|||||||
@@ -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 {
|
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]!);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ function pickBestSearchResult(
|
|||||||
return titles.includes(normalizedTarget);
|
return titles.includes(normalizedTarget);
|
||||||
});
|
});
|
||||||
|
|
||||||
const selected = exact ?? candidates[0];
|
const selected = exact ?? candidates[0]!;
|
||||||
const selectedTitle =
|
const selectedTitle =
|
||||||
selected.title?.english || selected.title?.romaji || selected.title?.native || title;
|
selected.title?.english || selected.title?.romaji || selected.title?.native || title;
|
||||||
return { id: selected.id, title: selectedTitle };
|
return { id: selected.id, title: selectedTitle };
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ test('refreshKnownWords throws when integration is unavailable', async () => {
|
|||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
async () => {
|
async () => {
|
||||||
await registered.refreshKnownWords();
|
await registered.refreshKnownWords!();
|
||||||
},
|
},
|
||||||
{ message: 'AnkiConnect integration not enabled' },
|
{ 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);
|
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.deepEqual(state.patches, [false]);
|
||||||
assert.equal(destroyed, 1);
|
assert.equal(destroyed, 1);
|
||||||
@@ -188,8 +188,8 @@ test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () =
|
|||||||
deleteDuplicate: true,
|
deleteDuplicate: true,
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
};
|
};
|
||||||
registered.clearAnkiHistory();
|
registered.clearAnkiHistory!();
|
||||||
registered.respondFieldGrouping(choice);
|
registered.respondFieldGrouping!(choice);
|
||||||
|
|
||||||
options.getSubtitleTimingTracker = originalGetTracker;
|
options.getSubtitleTimingTracker = originalGetTracker;
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () =
|
|||||||
test('buildKikuMergePreview returns guard error when integration is missing', async () => {
|
test('buildKikuMergePreview returns guard error when integration is missing', async () => {
|
||||||
const { registered } = createHarness();
|
const { registered } = createHarness();
|
||||||
|
|
||||||
const result = await registered.buildKikuMergePreview({
|
const result = await registered.buildKikuMergePreview!({
|
||||||
keepNoteId: 1,
|
keepNoteId: 1,
|
||||||
deleteNoteId: 2,
|
deleteNoteId: 2,
|
||||||
deleteDuplicate: false,
|
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,
|
keepNoteId: 3,
|
||||||
deleteNoteId: 4,
|
deleteNoteId: 4,
|
||||||
deleteDuplicate: true,
|
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 () => {
|
test('searchJimakuEntries caps results and onDownloadedSubtitle sends sub-add to mpv', async () => {
|
||||||
const { registered, state } = createHarness();
|
const { registered, state } = createHarness();
|
||||||
|
|
||||||
const searchResult = await registered.searchJimakuEntries({ query: 'test' });
|
const searchResult = await registered.searchJimakuEntries!({ query: 'test' });
|
||||||
assert.deepEqual(state.fetchCalls, [
|
assert.deepEqual(state.fetchCalls, [
|
||||||
{
|
{
|
||||||
endpoint: '/api/entries/search',
|
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 { ok: boolean }).ok, true);
|
||||||
assert.equal((searchResult as { data: unknown[] }).data.length, 2);
|
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'] }]);
|
assert.deepEqual(state.sentCommands, [{ command: ['sub-add', '/tmp/subtitle.ass', 'select'] }]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -209,30 +209,31 @@ test('runAppReadyRuntime aggregates multiple critical anki mapping errors', asyn
|
|||||||
|
|
||||||
await runAppReadyRuntime(deps);
|
await runAppReadyRuntime(deps);
|
||||||
|
|
||||||
|
const firstErrorSet = capturedErrors[0]!;
|
||||||
assert.equal(capturedErrors.length, 1);
|
assert.equal(capturedErrors.length, 1);
|
||||||
assert.equal(capturedErrors[0].length, 5);
|
assert.equal(firstErrorSet.length, 5);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
capturedErrors[0].includes(
|
firstErrorSet.includes(
|
||||||
'ankiConnect.fields.audio must be a non-empty string when ankiConnect is enabled.',
|
'ankiConnect.fields.audio must be a non-empty string when ankiConnect is enabled.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
capturedErrors[0].includes(
|
firstErrorSet.includes(
|
||||||
'ankiConnect.fields.image must be a non-empty string when ankiConnect is enabled.',
|
'ankiConnect.fields.image must be a non-empty string when ankiConnect is enabled.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
capturedErrors[0].includes(
|
firstErrorSet.includes(
|
||||||
'ankiConnect.fields.sentence must be a non-empty string when ankiConnect is enabled.',
|
'ankiConnect.fields.sentence must be a non-empty string when ankiConnect is enabled.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
capturedErrors[0].includes(
|
firstErrorSet.includes(
|
||||||
'ankiConnect.fields.miscInfo must be a non-empty string when ankiConnect is enabled.',
|
'ankiConnect.fields.miscInfo must be a non-empty string when ankiConnect is enabled.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
capturedErrors[0].includes(
|
firstErrorSet.includes(
|
||||||
'ankiConnect.fields.translation must be a non-empty string when ankiConnect is enabled.',
|
'ankiConnect.fields.translation must be a non-empty string when ankiConnect is enabled.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ testIfSqlite('persists and retrieves minimum immersion tracking fields', async (
|
|||||||
|
|
||||||
const summaries = await tracker.getSessionSummaries(10);
|
const summaries = await tracker.getSessionSummaries(10);
|
||||||
assert.ok(summaries.length >= 1);
|
assert.ok(summaries.length >= 1);
|
||||||
assert.ok(summaries[0].linesSeen >= 1);
|
assert.ok(summaries[0]!.linesSeen >= 1);
|
||||||
assert.ok(summaries[0].cardsMined >= 2);
|
assert.ok(summaries[0]!.cardsMined >= 2);
|
||||||
|
|
||||||
tracker.destroy();
|
tracker.destroy();
|
||||||
|
|
||||||
@@ -376,8 +376,8 @@ testIfSqlite('monthly rollups are grouped by calendar month', async () => {
|
|||||||
const videoRows = rows.filter((row) => row.videoId === 1);
|
const videoRows = rows.filter((row) => row.videoId === 1);
|
||||||
|
|
||||||
assert.equal(videoRows.length, 2);
|
assert.equal(videoRows.length, 2);
|
||||||
assert.equal(videoRows[0].rollupDayOrMonth, 202602);
|
assert.equal(videoRows[0]!.rollupDayOrMonth, 202602);
|
||||||
assert.equal(videoRows[1].rollupDayOrMonth, 202601);
|
assert.equal(videoRows[1]!.rollupDayOrMonth, 202601);
|
||||||
} finally {
|
} finally {
|
||||||
tracker?.destroy();
|
tracker?.destroy();
|
||||||
cleanupDbPath(dbPath);
|
cleanupDbPath(dbPath);
|
||||||
|
|||||||
@@ -1443,7 +1443,7 @@ export class ImmersionTrackerService {
|
|||||||
const parsed = new URL(mediaPath);
|
const parsed = new URL(mediaPath);
|
||||||
const parts = parsed.pathname.split('/').filter(Boolean);
|
const parts = parsed.pathname.split('/').filter(Boolean);
|
||||||
if (parts.length > 0) {
|
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(leaf.replace(/\.[^/.]+$/, ''));
|
||||||
}
|
}
|
||||||
return this.normalizeText(parsed.hostname) || 'unknown';
|
return this.normalizeText(parsed.hostname) || 'unknown';
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export { createFrequencyDictionaryLookup } from './frequency-dictionary';
|
|||||||
export { createJlptVocabularyLookup } from './jlpt-vocab';
|
export { createJlptVocabularyLookup } from './jlpt-vocab';
|
||||||
export {
|
export {
|
||||||
getIgnoredPos1Entries,
|
getIgnoredPos1Entries,
|
||||||
JlptIgnoredPos1Entry,
|
|
||||||
JLPT_EXCLUDED_TERMS,
|
JLPT_EXCLUDED_TERMS,
|
||||||
JLPT_IGNORED_MECAB_POS1,
|
JLPT_IGNORED_MECAB_POS1,
|
||||||
JLPT_IGNORED_MECAB_POS1_ENTRIES,
|
JLPT_IGNORED_MECAB_POS1_ENTRIES,
|
||||||
@@ -43,6 +42,7 @@ export {
|
|||||||
shouldIgnoreJlptByTerm,
|
shouldIgnoreJlptByTerm,
|
||||||
shouldIgnoreJlptForMecabPos1,
|
shouldIgnoreJlptForMecabPos1,
|
||||||
} from './jlpt-token-filter';
|
} from './jlpt-token-filter';
|
||||||
|
export type { JlptIgnoredPos1Entry } from './jlpt-token-filter';
|
||||||
export { loadYomitanExtension } from './yomitan-extension-loader';
|
export { loadYomitanExtension } from './yomitan-extension-loader';
|
||||||
export {
|
export {
|
||||||
getJimakuLanguagePreference,
|
getJimakuLanguagePreference,
|
||||||
@@ -72,8 +72,6 @@ export {
|
|||||||
export {
|
export {
|
||||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
MpvIpcClient,
|
MpvIpcClient,
|
||||||
MpvRuntimeClientLike,
|
|
||||||
MpvTrackProperty,
|
|
||||||
playNextSubtitleRuntime,
|
playNextSubtitleRuntime,
|
||||||
replayCurrentSubtitleRuntime,
|
replayCurrentSubtitleRuntime,
|
||||||
resolveCurrentAudioStreamIndex,
|
resolveCurrentAudioStreamIndex,
|
||||||
@@ -81,6 +79,7 @@ export {
|
|||||||
setMpvSubVisibilityRuntime,
|
setMpvSubVisibilityRuntime,
|
||||||
showMpvOsdRuntime,
|
showMpvOsdRuntime,
|
||||||
} from './mpv';
|
} from './mpv';
|
||||||
|
export type { MpvRuntimeClientLike, MpvTrackProperty } from './mpv';
|
||||||
export {
|
export {
|
||||||
applyMpvSubtitleRenderMetricsPatch,
|
applyMpvSubtitleRenderMetricsPatch,
|
||||||
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ test('start posts capabilities on socket connect', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
service.start();
|
service.start();
|
||||||
sockets[0].emit('open');
|
sockets[0]!.emit('open');
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
assert.equal(fetchCalls.length, 1);
|
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);
|
assert.equal(service.isConnected(), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,9 +97,9 @@ test('socket headers include jellyfin authorization metadata', () => {
|
|||||||
|
|
||||||
service.start();
|
service.start();
|
||||||
assert.equal(seenHeaders.length, 1);
|
assert.equal(seenHeaders.length, 1);
|
||||||
assert.ok(seenHeaders[0].Authorization.includes('Client="SubMiner"'));
|
assert.ok(seenHeaders[0]!['Authorization']!.includes('Client="SubMiner"'));
|
||||||
assert.ok(seenHeaders[0].Authorization.includes('DeviceId="device-auth"'));
|
assert.ok(seenHeaders[0]!['Authorization']!.includes('DeviceId="device-auth"'));
|
||||||
assert.ok(seenHeaders[0]['X-Emby-Authorization']);
|
assert.ok(seenHeaders[0]!['X-Emby-Authorization']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => {
|
test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => {
|
||||||
@@ -124,7 +124,7 @@ test('dispatches inbound Play, Playstate, and GeneralCommand messages', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
service.start();
|
service.start();
|
||||||
const socket = sockets[0];
|
const socket = sockets[0]!;
|
||||||
socket.emit('message', JSON.stringify({ MessageType: 'Play', Data: { ItemId: 'movie-1' } }));
|
socket.emit('message', JSON.stringify({ MessageType: 'Play', Data: { ItemId: 'movie-1' } }));
|
||||||
socket.emit(
|
socket.emit(
|
||||||
'message',
|
'message',
|
||||||
@@ -174,13 +174,13 @@ test('schedules reconnect with bounded exponential backoff', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
service.start();
|
service.start();
|
||||||
sockets[0].emit('close');
|
sockets[0]!.emit('close');
|
||||||
pendingTimers.shift()?.();
|
pendingTimers.shift()?.();
|
||||||
sockets[1].emit('close');
|
sockets[1]!.emit('close');
|
||||||
pendingTimers.shift()?.();
|
pendingTimers.shift()?.();
|
||||||
sockets[2].emit('close');
|
sockets[2]!.emit('close');
|
||||||
pendingTimers.shift()?.();
|
pendingTimers.shift()?.();
|
||||||
sockets[3].emit('close');
|
sockets[3]!.emit('close');
|
||||||
|
|
||||||
assert.deepEqual(delays, [100, 200, 400, 400]);
|
assert.deepEqual(delays, [100, 200, 400, 400]);
|
||||||
assert.equal(sockets.length, 4);
|
assert.equal(sockets.length, 4);
|
||||||
@@ -216,7 +216,7 @@ test('Jellyfin remote stop prevents further reconnect/network activity', () => {
|
|||||||
|
|
||||||
service.start();
|
service.start();
|
||||||
assert.equal(sockets.length, 1);
|
assert.equal(sockets.length, 1);
|
||||||
sockets[0].emit('close');
|
sockets[0]!.emit('close');
|
||||||
assert.equal(pendingTimers.length, 1);
|
assert.equal(pendingTimers.length, 1);
|
||||||
|
|
||||||
service.stop();
|
service.stop();
|
||||||
@@ -252,7 +252,7 @@ test('reportProgress posts timeline payload and treats failure as non-fatal', as
|
|||||||
});
|
});
|
||||||
|
|
||||||
service.start();
|
service.start();
|
||||||
sockets[0].emit('open');
|
sockets[0]!.emit('open');
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
const expectedPayload = buildJellyfinTimelinePayload({
|
const expectedPayload = buildJellyfinTimelinePayload({
|
||||||
@@ -310,7 +310,7 @@ test('advertiseNow validates server registration using Sessions endpoint', async
|
|||||||
});
|
});
|
||||||
|
|
||||||
service.start();
|
service.start();
|
||||||
sockets[0].emit('open');
|
sockets[0]!.emit('open');
|
||||||
const ok = await service.advertiseNow();
|
const ok = await service.advertiseNow();
|
||||||
assert.equal(ok, true);
|
assert.equal(ok, true);
|
||||||
assert.ok(calls.some((url) => url.endsWith('/Sessions')));
|
assert.ok(calls.some((url) => url.endsWith('/Sessions')));
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ test('listItems supports search and formats title', async () => {
|
|||||||
limit: 25,
|
limit: 25,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert.equal(items[0].title, 'Space Show S01E02 Pilot');
|
assert.equal(items[0]!.title, 'Space Show S01E02 Pilot');
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
@@ -338,14 +338,14 @@ test('listSubtitleTracks returns all subtitle streams with delivery urls', async
|
|||||||
[2, 3, 4],
|
[2, 3, 4],
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
tracks[0].deliveryUrl,
|
tracks[0]!.deliveryUrl,
|
||||||
'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/2/Stream.srt?api_key=token',
|
'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/2/Stream.srt?api_key=token',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
tracks[1].deliveryUrl,
|
tracks[1]!.deliveryUrl,
|
||||||
'http://jellyfin.local/Videos/movie-1/ms-1/Subtitles/3/Stream.srt?api_key=token',
|
'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 {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
@@ -556,9 +556,9 @@ test('listSubtitleTracks falls back from PlaybackInfo to item media sources', as
|
|||||||
);
|
);
|
||||||
assert.equal(requestCount, 2);
|
assert.equal(requestCount, 2);
|
||||||
assert.equal(tracks.length, 1);
|
assert.equal(tracks.length, 1);
|
||||||
assert.equal(tracks[0].index, 11);
|
assert.equal(tracks[0]!.index, 11);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
tracks[0].deliveryUrl,
|
tracks[0]!.deliveryUrl,
|
||||||
'http://jellyfin.local/Videos/movie-fallback/ms-fallback/Subtitles/11/Stream.srt?api_key=token',
|
'http://jellyfin.local/Videos/movie-fallback/ms-fallback/Subtitles/11/Stream.srt?api_key=token',
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -177,8 +177,8 @@ test('splitMpvMessagesFromBuffer parses complete lines and preserves partial buf
|
|||||||
|
|
||||||
assert.equal(parsed.messages.length, 2);
|
assert.equal(parsed.messages.length, 2);
|
||||||
assert.equal(parsed.nextBuffer, '{"partial"');
|
assert.equal(parsed.nextBuffer, '{"partial"');
|
||||||
assert.equal(parsed.messages[0].event, 'shutdown');
|
assert.equal(parsed.messages[0]!.event, 'shutdown');
|
||||||
assert.equal(parsed.messages[1].name, 'media-title');
|
assert.equal(parsed.messages[1]!.name, 'media-title');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => {
|
test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => {
|
||||||
@@ -189,7 +189,7 @@ test('splitMpvMessagesFromBuffer reports invalid JSON lines', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(errors.length, 1);
|
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', () => {
|
test('visibility and boolean parsers handle text values', () => {
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ test('scheduleMpvReconnect clears existing timer and increments attempt', () =>
|
|||||||
|
|
||||||
assert.equal(nextAttempt, 4);
|
assert.equal(nextAttempt, 4);
|
||||||
assert.equal(cleared.length, 1);
|
assert.equal(cleared.length, 1);
|
||||||
assert.equal(cleared[0], existing);
|
assert.equal(cleared[0]!, existing);
|
||||||
assert.equal(setTimers.length, 1);
|
assert.equal(setTimers.length, 1);
|
||||||
assert.equal(calls.length, 1);
|
assert.equal(calls.length, 1);
|
||||||
assert.equal(calls[0].attempt, 4);
|
assert.equal(calls[0]!.attempt, 4);
|
||||||
assert.equal(calls[0].delay, getMpvReconnectDelay(3, true));
|
assert.equal(calls[0]!.delay, getMpvReconnectDelay(3, true));
|
||||||
assert.equal(connected, 1);
|
assert.equal(connected, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ test('MpvIpcClient handles sub-text property change and broadcasts tokenized sub
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(events.length, 1);
|
assert.equal(events.length, 1);
|
||||||
assert.equal(events[0].text, '字幕');
|
assert.equal(events[0]!.text, '字幕');
|
||||||
assert.equal(events[0].isOverlayVisible, false);
|
assert.equal(events[0]!.isOverlayVisible, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
||||||
@@ -70,8 +70,8 @@ test('MpvIpcClient parses JSON line protocol in processBuffer', () => {
|
|||||||
(client as any).processBuffer();
|
(client as any).processBuffer();
|
||||||
|
|
||||||
assert.equal(seen.length, 2);
|
assert.equal(seen.length, 2);
|
||||||
assert.equal(seen[0].name, 'path');
|
assert.equal(seen[0]!.name, 'path');
|
||||||
assert.equal(seen[1].request_id, 1);
|
assert.equal(seen[1]!.request_id, 1);
|
||||||
assert.equal((client as any).buffer, '{"partial":');
|
assert.equal((client as any).buffer, '{"partial":');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -83,5 +83,5 @@ test('overlay measurement store rate-limits invalid payload warnings', () => {
|
|||||||
now = 11_000;
|
now = 11_000;
|
||||||
store.report({ layer: 'visible' });
|
store.report({ layer: 'visible' });
|
||||||
assert.equal(warnings.length, 1);
|
assert.equal(warnings.length, 1);
|
||||||
assert.match(warnings[0], /Dropped 3 invalid measurement payload/);
|
assert.match(warnings[0]!, /Dropped 3 invalid measurement payload/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const VIDEO_EXTENSIONS = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
function getPathExtension(pathValue: string): string {
|
function getPathExtension(pathValue: string): string {
|
||||||
const normalized = pathValue.split(/[?#]/, 1)[0];
|
const normalized = pathValue.split(/[?#]/, 1)[0] ?? '';
|
||||||
const dot = normalized.lastIndexOf('.');
|
const dot = normalized.lastIndexOf('.');
|
||||||
return dot >= 0 ? normalized.slice(dot).toLowerCase() : '';
|
return dot >= 0 ? normalized.slice(dot).toLowerCase() : '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function shortcutMatchesInputForLocalFallback(
|
|||||||
const parts = normalized.split('+').filter(Boolean);
|
const parts = normalized.split('+').filter(Boolean);
|
||||||
if (parts.length === 0) return false;
|
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 modifierTokens = new Set(parts.slice(0, -1));
|
||||||
const allowedModifiers = new Set(['shift', 'alt', 'meta', 'control', 'commandorcontrol']);
|
const allowedModifiers = new Set(['shift', 'alt', 'meta', 'control', 'commandorcontrol']);
|
||||||
for (const token of modifierTokens) {
|
for (const token of modifierTokens) {
|
||||||
|
|||||||
@@ -339,6 +339,6 @@ test('runSubsyncManual resolves string sid values from mpv stream properties', a
|
|||||||
assert.equal(syncOutputIndex >= 0, true);
|
assert.equal(syncOutputIndex >= 0, true);
|
||||||
const outputPath = ffArgs[syncOutputIndex + 1];
|
const outputPath = ffArgs[syncOutputIndex + 1];
|
||||||
assert.equal(typeof outputPath, 'string');
|
assert.equal(typeof outputPath, 'string');
|
||||||
assert.ok(outputPath.length > 0);
|
assert.ok(outputPath!.length > 0);
|
||||||
assert.equal(fs.readFileSync(outputPath, 'utf8'), '');
|
assert.equal(fs.readFileSync(outputPath!, 'utf8'), '');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export function cycleSecondarySubMode(deps: CycleSecondarySubModeDeps): void {
|
|||||||
|
|
||||||
const currentMode = deps.getSecondarySubMode();
|
const currentMode = deps.getSecondarySubMode();
|
||||||
const currentIndex = SECONDARY_SUB_CYCLE.indexOf(currentMode);
|
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.setSecondarySubMode(nextMode);
|
||||||
deps.broadcastSecondarySubMode(nextMode);
|
deps.broadcastSecondarySubMode(nextMode);
|
||||||
deps.showMpvOsd(`Secondary subtitle: ${nextMode}`);
|
deps.showMpvOsd(`Secondary subtitle: ${nextMode}`);
|
||||||
|
|||||||
@@ -82,8 +82,9 @@ export function serializeSubtitleMarkup(
|
|||||||
const klass = computeWordClass(token, options);
|
const klass = computeWordClass(token, options);
|
||||||
const parts = token.surface.split('\n');
|
const parts = token.surface.split('\n');
|
||||||
for (let index = 0; index < parts.length; index += 1) {
|
for (let index = 0; index < parts.length; index += 1) {
|
||||||
if (parts[index]) {
|
const part = parts[index];
|
||||||
chunks.push(`<span class="${klass}">${escapeHtml(parts[index])}</span>`);
|
if (part) {
|
||||||
|
chunks.push(`<span class="${klass}">${escapeHtml(part)}</span>`);
|
||||||
}
|
}
|
||||||
if (index < parts.length - 1) {
|
if (index < parts.length - 1) {
|
||||||
chunks.push('<br>');
|
chunks.push('<br>');
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class Texthooker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.server = http.createServer((req, res) => {
|
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 filePath = path.join(texthookerPath, urlPath === '/' ? 'index.html' : urlPath);
|
||||||
|
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ function isRepeatedKanaSfx(text: string): boolean {
|
|||||||
let hasAdjacentRepeat = false;
|
let hasAdjacentRepeat = false;
|
||||||
|
|
||||||
for (let i = 0; i < chars.length; i += 1) {
|
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);
|
counts.set(char, (counts.get(char) ?? 0) + 1);
|
||||||
if (i > 0 && chars[i] === chars[i - 1]) {
|
if (i > 0 && chars[i] === chars[i - 1]) {
|
||||||
hasAdjacentRepeat = true;
|
hasAdjacentRepeat = true;
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ export function openYomitanSettingsWindow(options: OpenYomitanSettingsWindowOpti
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!settingsWindow.isDestroyed()) {
|
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.webContents.invalidate();
|
||||||
settingsWindow.show();
|
settingsWindow.show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function getRetryAfter(headers: http.IncomingHttpHeaders): number | undefined {
|
|||||||
const value = headers['x-ratelimit-reset-after'];
|
const value = headers['x-ratelimit-reset-after'];
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
const raw = Array.isArray(value) ? value[0] : value;
|
const raw = Array.isArray(value) ? value[0] : value;
|
||||||
const parsed = Number.parseFloat(raw);
|
const parsed = Number.parseFloat(raw!);
|
||||||
if (!Number.isFinite(parsed)) return undefined;
|
if (!Number.isFinite(parsed)) return undefined;
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
@@ -146,8 +146,8 @@ function matchEpisodeFromName(name: string): {
|
|||||||
const seasonEpisode = name.match(/S(\d{1,2})E(\d{1,3})/i);
|
const seasonEpisode = name.match(/S(\d{1,2})E(\d{1,3})/i);
|
||||||
if (seasonEpisode && seasonEpisode.index !== undefined) {
|
if (seasonEpisode && seasonEpisode.index !== undefined) {
|
||||||
return {
|
return {
|
||||||
season: Number.parseInt(seasonEpisode[1], 10),
|
season: Number.parseInt(seasonEpisode[1]!, 10),
|
||||||
episode: Number.parseInt(seasonEpisode[2], 10),
|
episode: Number.parseInt(seasonEpisode[2]!, 10),
|
||||||
index: seasonEpisode.index,
|
index: seasonEpisode.index,
|
||||||
confidence: 'high',
|
confidence: 'high',
|
||||||
};
|
};
|
||||||
@@ -156,8 +156,8 @@ function matchEpisodeFromName(name: string): {
|
|||||||
const alt = name.match(/(\d{1,2})x(\d{1,3})/i);
|
const alt = name.match(/(\d{1,2})x(\d{1,3})/i);
|
||||||
if (alt && alt.index !== undefined) {
|
if (alt && alt.index !== undefined) {
|
||||||
return {
|
return {
|
||||||
season: Number.parseInt(alt[1], 10),
|
season: Number.parseInt(alt[1]!, 10),
|
||||||
episode: Number.parseInt(alt[2], 10),
|
episode: Number.parseInt(alt[2]!, 10),
|
||||||
index: alt.index,
|
index: alt.index,
|
||||||
confidence: 'high',
|
confidence: 'high',
|
||||||
};
|
};
|
||||||
@@ -167,7 +167,7 @@ function matchEpisodeFromName(name: string): {
|
|||||||
if (epOnly && epOnly.index !== undefined) {
|
if (epOnly && epOnly.index !== undefined) {
|
||||||
return {
|
return {
|
||||||
season: null,
|
season: null,
|
||||||
episode: Number.parseInt(epOnly[1], 10),
|
episode: Number.parseInt(epOnly[1]!, 10),
|
||||||
index: epOnly.index,
|
index: epOnly.index,
|
||||||
confidence: 'medium',
|
confidence: 'medium',
|
||||||
};
|
};
|
||||||
@@ -177,7 +177,7 @@ function matchEpisodeFromName(name: string): {
|
|||||||
if (numeric && numeric.index !== undefined) {
|
if (numeric && numeric.index !== undefined) {
|
||||||
return {
|
return {
|
||||||
season: null,
|
season: null,
|
||||||
episode: Number.parseInt(numeric[1], 10),
|
episode: Number.parseInt(numeric[1]!, 10),
|
||||||
index: numeric.index,
|
index: numeric.index,
|
||||||
confidence: 'medium',
|
confidence: 'medium',
|
||||||
};
|
};
|
||||||
@@ -190,7 +190,7 @@ function detectSeasonFromDir(mediaPath: string): number | null {
|
|||||||
const parent = path.basename(path.dirname(mediaPath));
|
const parent = path.basename(path.dirname(mediaPath));
|
||||||
const match = parent.match(/(?:Season|S)\s*(\d{1,2})/i);
|
const match = parent.match(/(?:Season|S)\s*(\d{1,2})/i);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const parsed = Number.parseInt(match[1], 10);
|
const parsed = Number.parseInt(match[1]!, 10);
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ test('manual anilist setup submission forwards access token to callback consumer
|
|||||||
const handled = handleSubmission('subminer://anilist-setup?access_token=abc123');
|
const handled = handleSubmission('subminer://anilist-setup?access_token=abc123');
|
||||||
assert.equal(handled, true);
|
assert.equal(handled, true);
|
||||||
assert.equal(consumed.length, 1);
|
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', () => {
|
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.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', () => {
|
test('anilist setup did-finish-load handler triggers fallback on blank page', () => {
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ test('createLaunchMpvIdleForJellyfinPlaybackHandler builds expected mpv args', (
|
|||||||
|
|
||||||
launch();
|
launch();
|
||||||
assert.equal(spawnedArgs.length, 1);
|
assert.equal(spawnedArgs.length, 1);
|
||||||
assert.ok(spawnedArgs[0].includes('--idle=yes'));
|
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]!.some((arg) => arg.includes('--input-ipc-server=/tmp/subminer.sock')));
|
||||||
assert.ok(logs.some((entry) => entry.includes('Launched mpv for Jellyfin playback')));
|
assert.ok(logs.some((entry) => entry.includes('Launched mpv for Jellyfin playback')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ test('tray menu template contains expected entries and handlers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(template.length, 7);
|
assert.equal(template.length, 7);
|
||||||
template[0].click?.();
|
template[0]!.click?.();
|
||||||
template[5].type === 'separator' ? calls.push('separator') : calls.push('bad');
|
template[5]!.type === 'separator' ? calls.push('separator') : calls.push('bad');
|
||||||
template[6].click?.();
|
template[6]!.click?.();
|
||||||
assert.deepEqual(calls, ['overlay', 'separator', 'quit']);
|
assert.deepEqual(calls, ['overlay', 'separator', 'quit']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export class MediaGenerator {
|
|||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
const { format, quality = 92, maxWidth, maxHeight } = options;
|
const { format, quality = 92, maxWidth, maxHeight } = options;
|
||||||
const ext = format === 'webp' ? 'webp' : format === 'png' ? 'png' : 'jpg';
|
const ext = format === 'webp' ? 'webp' : format === 'png' ? 'png' : 'jpg';
|
||||||
const codecMap: Record<string, string> = {
|
const codecMap: Record<'jpg' | 'png' | 'webp', string> = {
|
||||||
jpg: 'mjpeg',
|
jpg: 'mjpeg',
|
||||||
png: 'png',
|
png: 'png',
|
||||||
webp: 'webp',
|
webp: 'webp',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ test('handleError logs context and recovers overlay state', () => {
|
|||||||
assert.equal(dismissed, 1);
|
assert.equal(dismissed, 1);
|
||||||
assert.equal(restored, 1);
|
assert.equal(restored, 1);
|
||||||
assert.equal(shown.length, 1);
|
assert.equal(shown.length, 1);
|
||||||
assert.match(shown[0], /recovered/i);
|
assert.match(shown[0]!, /recovered/i);
|
||||||
assert.equal(payloads.length, 1);
|
assert.equal(payloads.length, 1);
|
||||||
|
|
||||||
const payload = payloads[0] as {
|
const payload = payloads[0] as {
|
||||||
|
|||||||
@@ -126,12 +126,12 @@ export function createMouseHandlers(
|
|||||||
if (!probeChar || isBoundary(probeChar)) return null;
|
if (!probeChar || isBoundary(probeChar)) return null;
|
||||||
|
|
||||||
let start = probeIndex;
|
let start = probeIndex;
|
||||||
while (start > 0 && !isBoundary(text[start - 1])) {
|
while (start > 0 && !isBoundary(text[start - 1] ?? '')) {
|
||||||
start -= 1;
|
start -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let end = probeIndex + 1;
|
let end = probeIndex + 1;
|
||||||
while (end < text.length && !isBoundary(text[end])) {
|
while (end < text.length && !isBoundary(text[end] ?? '')) {
|
||||||
end += 1;
|
end += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export function createJimakuModal(
|
|||||||
if (index < 0 || index >= ctx.state.jimakuEntries.length) return;
|
if (index < 0 || index >= ctx.state.jimakuEntries.length) return;
|
||||||
|
|
||||||
ctx.state.selectedEntryIndex = index;
|
ctx.state.selectedEntryIndex = index;
|
||||||
ctx.state.currentEntryId = ctx.state.jimakuEntries[index].id;
|
ctx.state.currentEntryId = ctx.state.jimakuEntries[index]!.id;
|
||||||
renderEntries();
|
renderEntries();
|
||||||
|
|
||||||
if (ctx.state.currentEntryId !== null) {
|
if (ctx.state.currentEntryId !== null) {
|
||||||
@@ -223,7 +223,7 @@ export function createJimakuModal(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = ctx.state.jimakuFiles[index];
|
const file = ctx.state.jimakuFiles[index]!;
|
||||||
setJimakuStatus('Downloading subtitle...');
|
setJimakuStatus('Downloading subtitle...');
|
||||||
|
|
||||||
const result: JimakuDownloadResult = await window.electronAPI.jimakuDownloadFile({
|
const result: JimakuDownloadResult = await window.electronAPI.jimakuDownloadFile({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function createRuntimeOptionsModal(
|
|||||||
if (ctx.state.runtimeOptionSelectedIndex >= ctx.state.runtimeOptions.length) {
|
if (ctx.state.runtimeOptionSelectedIndex >= ctx.state.runtimeOptions.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ctx.state.runtimeOptions[ctx.state.runtimeOptionSelectedIndex];
|
return ctx.state.runtimeOptions[ctx.state.runtimeOptionSelectedIndex] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRuntimeOptionsList(): void {
|
function renderRuntimeOptionsList(): void {
|
||||||
@@ -114,11 +114,11 @@ export function createRuntimeOptionsModal(
|
|||||||
? (safeIndex + 1) % option.allowedValues.length
|
? (safeIndex + 1) % option.allowedValues.length
|
||||||
: (safeIndex - 1 + option.allowedValues.length) % 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();
|
renderRuntimeOptionsList();
|
||||||
setRuntimeOptionsStatus(
|
setRuntimeOptionsStatus(`Selected ${option.label}: ${formatRuntimeOptionValue(nextValue)}`);
|
||||||
`Selected ${option.label}: ${formatRuntimeOptionValue(option.allowedValues[nextIndex])}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applySelectedRuntimeOption(): Promise<void> {
|
async function applySelectedRuntimeOption(): Promise<void> {
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ function createSectionNode(
|
|||||||
list.className = 'session-help-item-list';
|
list.className = 'session-help-item-list';
|
||||||
|
|
||||||
section.rows.forEach((row, rowIndex) => {
|
section.rows.forEach((row, rowIndex) => {
|
||||||
const globalIndex = globalIndexMap[sectionIndex] + rowIndex;
|
const globalIndex = (globalIndexMap[sectionIndex] ?? 0) + rowIndex;
|
||||||
const button = createShortcutRow(row, globalIndex);
|
const button = createShortcutRow(row, globalIndex);
|
||||||
list.appendChild(button);
|
list.appendChild(button);
|
||||||
});
|
});
|
||||||
@@ -406,6 +406,7 @@ export function createSessionHelpModal(
|
|||||||
item.tabIndex = idx === next ? 0 : -1;
|
item.tabIndex = idx === next ? 0 : -1;
|
||||||
});
|
});
|
||||||
const activeItem = items[next];
|
const activeItem = items[next];
|
||||||
|
if (!activeItem) return;
|
||||||
activeItem.focus({ preventScroll: true });
|
activeItem.focus({ preventScroll: true });
|
||||||
activeItem.scrollIntoView({
|
activeItem.scrollIntoView({
|
||||||
block: 'nearest',
|
block: 'nearest',
|
||||||
@@ -562,7 +563,7 @@ export function createSessionHelpModal(
|
|||||||
|
|
||||||
const shortcutSections = buildOverlayShortcutSections(shortcuts);
|
const shortcutSections = buildOverlayShortcutSections(shortcuts);
|
||||||
if (shortcutSections.length > 0) {
|
if (shortcutSections.length > 0) {
|
||||||
shortcutSections[0].title = 'Overlay shortcuts (configurable)';
|
shortcutSections[0]!.title = 'Overlay shortcuts (configurable)';
|
||||||
}
|
}
|
||||||
const colorSection = buildColorSection(styleConfig ?? {});
|
const colorSection = buildColorSection(styleConfig ?? {});
|
||||||
helpSections = [...bindingSections, ...shortcutSections, colorSection];
|
helpSections = [...bindingSections, ...shortcutSections, colorSection];
|
||||||
|
|||||||
@@ -145,10 +145,11 @@ function renderWithTokens(
|
|||||||
if (surface.includes('\n')) {
|
if (surface.includes('\n')) {
|
||||||
const parts = surface.split('\n');
|
const parts = surface.split('\n');
|
||||||
for (let i = 0; i < parts.length; i += 1) {
|
for (let i = 0; i < parts.length; i += 1) {
|
||||||
if (parts[i]) {
|
const part = parts[i];
|
||||||
|
if (part) {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.className = computeWordClass(token, resolvedFrequencyRenderSettings);
|
span.className = computeWordClass(token, resolvedFrequencyRenderSettings);
|
||||||
span.textContent = parts[i];
|
span.textContent = part;
|
||||||
if (token.reading) span.dataset.reading = token.reading;
|
if (token.reading) span.dataset.reading = token.reading;
|
||||||
if (token.headword) span.dataset.headword = token.headword;
|
if (token.headword) span.dataset.headword = token.headword;
|
||||||
fragment.appendChild(span);
|
fragment.appendChild(span);
|
||||||
@@ -277,7 +278,7 @@ function renderPlainTextPreserveLineBreaks(root: ParentNode, text: string): void
|
|||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i += 1) {
|
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) {
|
if (i < lines.length - 1) {
|
||||||
fragment.appendChild(document.createElement('br'));
|
fragment.appendChild(document.createElement('br'));
|
||||||
}
|
}
|
||||||
@@ -361,8 +362,9 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
|
|
||||||
const lines = normalized.split('\n');
|
const lines = normalized.split('\n');
|
||||||
for (let i = 0; i < lines.length; i += 1) {
|
for (let i = 0; i < lines.length; i += 1) {
|
||||||
if (lines[i]) {
|
const line = lines[i];
|
||||||
ctx.dom.secondarySubRoot.appendChild(document.createTextNode(lines[i]));
|
if (line) {
|
||||||
|
ctx.dom.secondarySubRoot.appendChild(document.createTextNode(line));
|
||||||
}
|
}
|
||||||
if (i < lines.length - 1) {
|
if (i < lines.length - 1) {
|
||||||
ctx.dom.secondarySubRoot.appendChild(document.createElement('br'));
|
ctx.dom.secondarySubRoot.appendChild(document.createElement('br'));
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function setPathValue(target: Record<string, unknown>, path: string, value: unkn
|
|||||||
const parts = path.split('.');
|
const parts = path.split('.');
|
||||||
let current = target;
|
let current = target;
|
||||||
for (let i = 0; i < parts.length; i += 1) {
|
for (let i = 0; i < parts.length; i += 1) {
|
||||||
const part = parts[i];
|
const part = parts[i]!;
|
||||||
const isLeaf = i === parts.length - 1;
|
const isLeaf = i === parts.length - 1;
|
||||||
if (isLeaf) {
|
if (isLeaf) {
|
||||||
current[part] = value;
|
current[part] = value;
|
||||||
@@ -188,7 +188,7 @@ export class RuntimeOptionsManager {
|
|||||||
direction === 1
|
direction === 1
|
||||||
? (safeIndex + 1) % values.length
|
? (safeIndex + 1) % values.length
|
||||||
: (safeIndex - 1 + values.length) % values.length;
|
: (safeIndex - 1 + values.length) % values.length;
|
||||||
return this.setOptionValue(id, values[nextIndex]);
|
return this.setOptionValue(id, values[nextIndex]!);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEffectiveAnkiConnectConfig(baseConfig?: AnkiConnectConfig): AnkiConnectConfig {
|
getEffectiveAnkiConnectConfig(baseConfig?: AnkiConnectConfig): AnkiConnectConfig {
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3)
|
|||||||
let sentenceWordCount = 0;
|
let sentenceWordCount = 0;
|
||||||
for (let i = start; i < endExclusive; i++) {
|
for (let i = start; i < endExclusive; i++) {
|
||||||
const token = markedTokens[i];
|
const token = markedTokens[i];
|
||||||
|
if (!token) continue;
|
||||||
if (!isSentenceBoundaryToken(token) && token.surface.trim().length > 0) {
|
if (!isSentenceBoundaryToken(token) && token.surface.trim().length > 0) {
|
||||||
sentenceWordCount += 1;
|
sentenceWordCount += 1;
|
||||||
}
|
}
|
||||||
@@ -317,8 +318,8 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sentenceWordCount >= minimumSentenceWords && sentenceCandidates.length === 1) {
|
if (sentenceWordCount >= minimumSentenceWords && sentenceCandidates.length === 1) {
|
||||||
markedTokens[sentenceCandidates[0]] = {
|
markedTokens[sentenceCandidates[0]!] = {
|
||||||
...markedTokens[sentenceCandidates[0]],
|
...markedTokens[sentenceCandidates[0]!]!,
|
||||||
isNPlusOneTarget: true,
|
isNPlusOneTarget: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -326,6 +327,7 @@ export function markNPlusOneTargets(tokens: MergedToken[], minSentenceWords = 3)
|
|||||||
|
|
||||||
for (let i = 0; i < markedTokens.length; i++) {
|
for (let i = 0; i < markedTokens.length; i++) {
|
||||||
const token = markedTokens[i];
|
const token = markedTokens[i];
|
||||||
|
if (!token) continue;
|
||||||
if (isSentenceBoundaryToken(token)) {
|
if (isSentenceBoundaryToken(token)) {
|
||||||
markSentence(sentenceStart, i);
|
markSentence(sentenceStart, i);
|
||||||
sentenceStart = i + 1;
|
sentenceStart = i + 1;
|
||||||
|
|||||||
@@ -177,10 +177,10 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
|||||||
if (result && result !== 'not-found') {
|
if (result && result !== 'not-found') {
|
||||||
const parts = result.split(',');
|
const parts = result.split(',');
|
||||||
if (parts.length === 4) {
|
if (parts.length === 4) {
|
||||||
const x = parseInt(parts[0], 10);
|
const x = parseInt(parts[0]!, 10);
|
||||||
const y = parseInt(parts[1], 10);
|
const y = parseInt(parts[1]!, 10);
|
||||||
const width = parseInt(parts[2], 10);
|
const width = parseInt(parts[2]!, 10);
|
||||||
const height = parseInt(parts[3], 10);
|
const height = parseInt(parts[3]!, 10);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Number.isFinite(x) &&
|
Number.isFinite(x) &&
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ export function parseX11WindowGeometry(winInfo: string): {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
x: parseInt(xMatch[1], 10),
|
x: parseInt(xMatch[1]!, 10),
|
||||||
y: parseInt(yMatch[1], 10),
|
y: parseInt(yMatch[1]!, 10),
|
||||||
width: parseInt(widthMatch[1], 10),
|
width: parseInt(widthMatch[1]!, 10),
|
||||||
height: parseInt(heightMatch[1], 10),
|
height: parseInt(heightMatch[1]!, 10),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export function parseX11WindowPid(raw: string): number | null {
|
|||||||
if (!pidMatch) {
|
if (!pidMatch) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const pid = Number.parseInt(pidMatch[1], 10);
|
const pid = Number.parseInt(pidMatch[1]!, 10);
|
||||||
return Number.isInteger(pid) ? pid : null;
|
return Number.isInteger(pid) ? pid : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"lib": ["ES2022", "DOM"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user