mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-06 19:57:26 -08:00
build: expand typecheck coverage beyond src
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build (TypeScript check)
|
- name: Build (TypeScript check)
|
||||||
# Keep explicit typecheck for fast fail before full build/bundle.
|
# Keep explicit typecheck for fast fail before full build/bundle.
|
||||||
run: bun run tsc --noEmit
|
run: bun run typecheck
|
||||||
|
|
||||||
- name: Test suite (source)
|
- name: Test suite (source)
|
||||||
run: bun run test:fast
|
run: bun run test:fast
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
id: TASK-90
|
||||||
|
title: Expand TypeScript typecheck coverage beyond src
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-06 08:18'
|
||||||
|
updated_date: '2026-03-06 08:23'
|
||||||
|
labels:
|
||||||
|
- tooling
|
||||||
|
- typescript
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- /home/sudacode/projects/japanese/SubMiner/tsconfig.json
|
||||||
|
- /home/sudacode/projects/japanese/SubMiner/package.json
|
||||||
|
- /home/sudacode/projects/japanese/SubMiner/launcher
|
||||||
|
- /home/sudacode/projects/japanese/SubMiner/scripts
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Bring all repository TypeScript entrypoints outside src/ into the enforced typecheck gate so CI and local checks cover launcher/ and script files, then resolve any surfaced diagnostics.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 TypeScript typecheck covers repository TypeScript entrypoints outside src/ that should be maintained in this repo, including launcher/ and script files.
|
||||||
|
- [x] #2 The enforced typecheck command used by CI and local development passes with the expanded coverage.
|
||||||
|
- [x] #3 Any diagnostics surfaced by the expanded coverage are fixed without weakening existing strictness for src/.
|
||||||
|
- [x] #4 Relevant documentation or command wiring is updated if the typecheck entrypoint changes.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Added a dedicated repo-wide typecheck config at tsconfig.typecheck.json and wired package.json/CI to use `bun run typecheck` for launcher and scripts coverage without changing the existing src build config. Fixed the strict-null/indexing diagnostics surfaced in launcher/* and scripts/*, keeping src strictness intact. Verified with `bun run typecheck`, `bun run tsc --noEmit`, and `bun run test:launcher:src` (47 passing, plugin start gate OK).
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -166,8 +166,10 @@ test('buildSubminerScriptOpts includes aniskip payload fields', () => {
|
|||||||
assert.match(opts, /subminer-aniskip_intro_end=62/);
|
assert.match(opts, /subminer-aniskip_intro_end=62/);
|
||||||
assert.match(opts, /subminer-aniskip_lookup_status=ready/);
|
assert.match(opts, /subminer-aniskip_lookup_status=ready/);
|
||||||
assert.ok(payloadMatch !== null);
|
assert.ok(payloadMatch !== null);
|
||||||
assert.equal(payloadMatch[1].includes('%'), false);
|
const encodedPayload = payloadMatch[1];
|
||||||
const payloadJson = Buffer.from(payloadMatch[1], 'base64url').toString('utf-8');
|
assert.ok(encodedPayload !== undefined);
|
||||||
|
assert.equal(encodedPayload.includes('%'), false);
|
||||||
|
const payloadJson = Buffer.from(encodedPayload, 'base64url').toString('utf-8');
|
||||||
const payload = JSON.parse(payloadJson);
|
const payload = JSON.parse(payloadJson);
|
||||||
assert.equal(payload.found, true);
|
assert.equal(payload.found, true);
|
||||||
const first = payload.results?.[0];
|
const first = payload.results?.[0];
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ interface AniSkipPayloadResponse {
|
|||||||
results?: unknown;
|
results?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROMAN_SEASON_ALIASES: Record<number, readonly string[]> = {
|
||||||
|
2: [' ii ', ' second season ', ' 2nd season '],
|
||||||
|
3: [' iii ', ' third season ', ' 3rd season '],
|
||||||
|
4: [' iv ', ' fourth season ', ' 4th season '],
|
||||||
|
5: [' v ', ' fifth season ', ' 5th season '],
|
||||||
|
};
|
||||||
|
|
||||||
const MAL_PREFIX_API = 'https://myanimelist.net/search/prefix.json?type=anime&keyword=';
|
const MAL_PREFIX_API = 'https://myanimelist.net/search/prefix.json?type=anime&keyword=';
|
||||||
const ANISKIP_PAYLOAD_API = 'https://api.aniskip.com/v1/skip-times/';
|
const ANISKIP_PAYLOAD_API = 'https://api.aniskip.com/v1/skip-times/';
|
||||||
const MAL_USER_AGENT = 'SubMiner-launcher/ani-skip';
|
const MAL_USER_AGENT = 'SubMiner-launcher/ani-skip';
|
||||||
@@ -188,14 +195,7 @@ function seasonSignalScore(requestedSeason: number | null, candidateTitle: strin
|
|||||||
return 40;
|
return 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
const romanAliases = {
|
const aliases = ROMAN_SEASON_ALIASES[season] ?? [];
|
||||||
2: [' ii ', ' second season ', ' 2nd season '],
|
|
||||||
3: [' iii ', ' third season ', ' 3rd season '],
|
|
||||||
4: [' iv ', ' fourth season ', ' 4th season '],
|
|
||||||
5: [' v ', ' fifth season ', ' 5th season '],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const aliases = romanAliases[season] ?? [];
|
|
||||||
return aliases.some((alias) => normalized.includes(alias))
|
return aliases.some((alias) => normalized.includes(alias))
|
||||||
? 40
|
? 40
|
||||||
: hasAnySequelMarker(candidateTitle)
|
: hasAnySequelMarker(candidateTitle)
|
||||||
|
|||||||
@@ -284,8 +284,10 @@ export function parseEpisodePathFromDisplay(
|
|||||||
const normalized = display.trim().replace(/\s+/g, ' ');
|
const normalized = display.trim().replace(/\s+/g, ' ');
|
||||||
const match = normalized.match(/^(.*?)\s+S(\d{1,2})E\d{1,3}\b/i);
|
const match = normalized.match(/^(.*?)\s+S(\d{1,2})E\d{1,3}\b/i);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const seriesName = match[1].trim();
|
const seriesName = match[1]?.trim();
|
||||||
const seasonNumber = Number.parseInt(match[2], 10);
|
const seasonText = match[2];
|
||||||
|
if (!seriesName || !seasonText) return null;
|
||||||
|
const seasonNumber = Number.parseInt(seasonText, 10);
|
||||||
if (!seriesName || !Number.isFinite(seasonNumber) || seasonNumber < 0) return null;
|
if (!seriesName || !Number.isFinite(seasonNumber) || seasonNumber < 0) return null;
|
||||||
return { seriesName, seasonNumber };
|
return { seriesName, seasonNumber };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,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;
|
||||||
|
if (!raw) return undefined;
|
||||||
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;
|
||||||
@@ -72,9 +73,14 @@ export 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) {
|
||||||
|
const seasonText = seasonEpisode[1];
|
||||||
|
const episodeText = seasonEpisode[2];
|
||||||
|
if (!seasonText || !episodeText) {
|
||||||
|
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
season: Number.parseInt(seasonEpisode[1], 10),
|
season: Number.parseInt(seasonText, 10),
|
||||||
episode: Number.parseInt(seasonEpisode[2], 10),
|
episode: Number.parseInt(episodeText, 10),
|
||||||
index: seasonEpisode.index,
|
index: seasonEpisode.index,
|
||||||
confidence: 'high',
|
confidence: 'high',
|
||||||
};
|
};
|
||||||
@@ -82,9 +88,14 @@ export 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) {
|
||||||
|
const seasonText = alt[1];
|
||||||
|
const episodeText = alt[2];
|
||||||
|
if (!seasonText || !episodeText) {
|
||||||
|
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
season: Number.parseInt(alt[1], 10),
|
season: Number.parseInt(seasonText, 10),
|
||||||
episode: Number.parseInt(alt[2], 10),
|
episode: Number.parseInt(episodeText, 10),
|
||||||
index: alt.index,
|
index: alt.index,
|
||||||
confidence: 'high',
|
confidence: 'high',
|
||||||
};
|
};
|
||||||
@@ -92,9 +103,13 @@ export function matchEpisodeFromName(name: string): {
|
|||||||
|
|
||||||
const epOnly = name.match(/(?:^|[\s._-])E(?:P)?(\d{1,3})(?:\b|[\s._-])/i);
|
const epOnly = name.match(/(?:^|[\s._-])E(?:P)?(\d{1,3})(?:\b|[\s._-])/i);
|
||||||
if (epOnly && epOnly.index !== undefined) {
|
if (epOnly && epOnly.index !== undefined) {
|
||||||
|
const episodeText = epOnly[1];
|
||||||
|
if (!episodeText) {
|
||||||
|
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
season: null,
|
season: null,
|
||||||
episode: Number.parseInt(epOnly[1], 10),
|
episode: Number.parseInt(episodeText, 10),
|
||||||
index: epOnly.index,
|
index: epOnly.index,
|
||||||
confidence: 'medium',
|
confidence: 'medium',
|
||||||
};
|
};
|
||||||
@@ -102,9 +117,13 @@ export function matchEpisodeFromName(name: string): {
|
|||||||
|
|
||||||
const numeric = name.match(/(?:^|[-–—]\s*)(\d{1,3})\s*[-–—]/);
|
const numeric = name.match(/(?:^|[-–—]\s*)(\d{1,3})\s*[-–—]/);
|
||||||
if (numeric && numeric.index !== undefined) {
|
if (numeric && numeric.index !== undefined) {
|
||||||
|
const episodeText = numeric[1];
|
||||||
|
if (!episodeText) {
|
||||||
|
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
season: null,
|
season: null,
|
||||||
episode: Number.parseInt(numeric[1], 10),
|
episode: Number.parseInt(episodeText, 10),
|
||||||
index: numeric.index,
|
index: numeric.index,
|
||||||
confidence: 'medium',
|
confidence: 'medium',
|
||||||
};
|
};
|
||||||
@@ -117,7 +136,9 @@ 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 seasonText = match[1];
|
||||||
|
if (!seasonText) return null;
|
||||||
|
const parsed = Number.parseInt(seasonText, 10);
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ export async function startMpv(
|
|||||||
appPath: string,
|
appPath: string,
|
||||||
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
|
preloadedSubtitles?: { primaryPath?: string; secondaryPath?: string },
|
||||||
options?: { startPaused?: boolean },
|
options?: { startPaused?: boolean },
|
||||||
): void {
|
): Promise<void> {
|
||||||
if (targetKind === 'file' && (!fs.existsSync(target) || !fs.statSync(target).isFile())) {
|
if (targetKind === 'file' && (!fs.existsSync(target) || !fs.statSync(target).isFile())) {
|
||||||
fail(`Video file not found: ${target}`);
|
fail(`Video file not found: ${target}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ export function pickLibrary(
|
|||||||
iconPath: ensureIcon(session, lib.id) || undefined,
|
iconPath: ensureIcon(session, lib.id) || undefined,
|
||||||
}));
|
}));
|
||||||
const idx = showRofiIconMenu(entries, 'Jellyfin Library', initialQuery, themePath);
|
const idx = showRofiIconMenu(entries, 'Jellyfin Library', initialQuery, themePath);
|
||||||
return idx >= 0 ? visibleLibraries[idx].id : '';
|
const selected = idx >= 0 ? visibleLibraries[idx] : undefined;
|
||||||
|
return selected?.id ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = visibleLibraries.map((lib) => `${lib.id}\t${lib.name} [${lib.kind}]`);
|
const lines = visibleLibraries.map((lib) => `${lib.id}\t${lib.name} [${lib.kind}]`);
|
||||||
@@ -244,7 +245,8 @@ export function pickItem(
|
|||||||
iconPath: ensureIcon(session, item.id) || undefined,
|
iconPath: ensureIcon(session, item.id) || undefined,
|
||||||
}));
|
}));
|
||||||
const idx = showRofiIconMenu(entries, 'Jellyfin Item', initialQuery, themePath);
|
const idx = showRofiIconMenu(entries, 'Jellyfin Item', initialQuery, themePath);
|
||||||
return idx >= 0 ? visibleItems[idx].id : '';
|
const selected = idx >= 0 ? visibleItems[idx] : undefined;
|
||||||
|
return selected?.id ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = visibleItems.map((item) => `${item.id}\t${item.display}`);
|
const lines = visibleItems.map((item) => `${item.id}\t${item.display}`);
|
||||||
@@ -281,7 +283,8 @@ export function pickGroup(
|
|||||||
iconPath: ensureIcon(session, group.id) || undefined,
|
iconPath: ensureIcon(session, group.id) || undefined,
|
||||||
}));
|
}));
|
||||||
const idx = showRofiIconMenu(entries, 'Jellyfin Anime/Folder', initialQuery, themePath);
|
const idx = showRofiIconMenu(entries, 'Jellyfin Anime/Folder', initialQuery, themePath);
|
||||||
return idx >= 0 ? visibleGroups[idx].id : '';
|
const selected = idx >= 0 ? visibleGroups[idx] : undefined;
|
||||||
|
return selected?.id ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = visibleGroups.map((group) => `${group.id}\t${group.display}`);
|
const lines = visibleGroups.map((group) => `${group.id}\t${group.display}`);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function pickBestCandidate(candidates: SubtitleCandidate[]): SubtitleCandidate |
|
|||||||
if (srtA !== srtB) return srtB - srtA;
|
if (srtA !== srtB) return srtB - srtA;
|
||||||
return b.size - a.size;
|
return b.size - a.size;
|
||||||
});
|
});
|
||||||
return scored[0];
|
return scored[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanSubtitleCandidates(
|
function scanSubtitleCandidates(
|
||||||
@@ -120,7 +120,7 @@ function findAudioFile(tempDir: string, preferredExt: string): string | null {
|
|||||||
const preferred = audioFiles.find((entry) => entry.ext === `.${preferredExt.toLowerCase()}`);
|
const preferred = audioFiles.find((entry) => entry.ext === `.${preferredExt.toLowerCase()}`);
|
||||||
if (preferred) return preferred.path;
|
if (preferred) return preferred.path;
|
||||||
audioFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
audioFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||||
return audioFiles[0].path;
|
return audioFiles[0]?.path ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runWhisper(
|
async function runWhisper(
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -5,11 +5,13 @@
|
|||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"main": "dist/main-entry.js",
|
"main": "dist/main-entry.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
||||||
|
"typecheck:watch": "tsc --watch --preserveWatchOutput -p tsconfig.typecheck.json",
|
||||||
"get-frequency": "bun run scripts/get_frequency.ts --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
|
"get-frequency": "bun run scripts/get_frequency.ts --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
|
||||||
"get-frequency:electron": "bun build scripts/get_frequency.ts --format=cjs --target=node --outfile dist/scripts/get_frequency.js --external electron && electron dist/scripts/get_frequency.js --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
|
"get-frequency:electron": "bun build scripts/get_frequency.ts --format=cjs --target=node --outfile dist/scripts/get_frequency.js --external electron && electron dist/scripts/get_frequency.js --pretty --color-top-x 10000 --yomitan-user-data ~/.config/SubMiner --colorized-line",
|
||||||
"test-yomitan-parser": "bun run scripts/test-yomitan-parser.ts",
|
"test-yomitan-parser": "bun run scripts/test-yomitan-parser.ts",
|
||||||
"test-yomitan-parser:electron": "bun build scripts/test-yomitan-parser.ts --format=cjs --target=node --outfile dist/scripts/test-yomitan-parser.js --external electron && electron dist/scripts/test-yomitan-parser.js",
|
"test-yomitan-parser:electron": "bun build scripts/test-yomitan-parser.ts --format=cjs --target=node --outfile dist/scripts/test-yomitan-parser.js --external electron && electron dist/scripts/test-yomitan-parser.js",
|
||||||
"build": "tsc && bun run build:renderer && cp src/renderer/index.html src/renderer/style.css dist/renderer/ && cp -r src/renderer/fonts dist/renderer/ && bash scripts/build-macos-helper.sh",
|
"build": "tsc -p tsconfig.json && bun run build:renderer && cp src/renderer/index.html src/renderer/style.css dist/renderer/ && cp -r src/renderer/fonts dist/renderer/ && bash scripts/build-macos-helper.sh",
|
||||||
"build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap",
|
"build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
@@ -24,6 +26,15 @@
|
|||||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
|
||||||
|
"test:immersion:sqlite:src": "bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts",
|
||||||
|
"test:immersion:sqlite:dist": "node --experimental-sqlite --test dist/core/services/immersion-tracker-service.test.js dist/core/services/immersion-tracker/storage-session.test.js",
|
||||||
|
"test:immersion:sqlite": "bun run tsc && bun run test:immersion:sqlite:dist",
|
||||||
|
"test:src": "node scripts/run-test-lane.mjs bun-src-full",
|
||||||
|
"test:launcher:unit:src": "node scripts/run-test-lane.mjs bun-launcher-unit",
|
||||||
|
"test:launcher:env:src": "bun run test:launcher:smoke:src && bun run test:plugin:src",
|
||||||
|
"test:env": "bun run test:launcher:env:src && bun run test:immersion:sqlite:src",
|
||||||
|
"test:node:compat": "bun run tsc && node --experimental-sqlite --test dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/overlay-manager.test.js dist/main/config-validation.test.js dist/main/runtime/registry.test.js dist/main/runtime/startup-config.test.js",
|
||||||
|
"test:full": "bun run test:src && bun run test:launcher:unit:src && bun run test:node:compat",
|
||||||
"test": "bun run test:fast",
|
"test": "bun run test:fast",
|
||||||
"test:config": "bun run test:config:src",
|
"test:config": "bun run test:config:src",
|
||||||
"test:launcher": "bun run test:launcher:src",
|
"test:launcher": "bun run test:launcher:src",
|
||||||
@@ -38,16 +49,7 @@
|
|||||||
"build:appimage": "bun run build && electron-builder --linux AppImage",
|
"build:appimage": "bun run build && electron-builder --linux AppImage",
|
||||||
"build:mac": "bun run build && electron-builder --mac dmg zip",
|
"build:mac": "bun run build && electron-builder --mac dmg zip",
|
||||||
"build:mac:unsigned": "bun run build && env -u APPLE_ID -u APPLE_APP_SPECIFIC_PASSWORD -u APPLE_TEAM_ID -u CSC_LINK -u CSC_KEY_PASSWORD CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dmg zip",
|
"build:mac:unsigned": "bun run build && env -u APPLE_ID -u APPLE_APP_SPECIFIC_PASSWORD -u APPLE_TEAM_ID -u CSC_LINK -u CSC_KEY_PASSWORD CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dmg zip",
|
||||||
"build:mac:zip": "bun run build && electron-builder --mac zip",
|
"build:mac:zip": "bun run build && electron-builder --mac zip"
|
||||||
"test:immersion:sqlite:src": "bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts",
|
|
||||||
"test:immersion:sqlite:dist": "node --experimental-sqlite --test dist/core/services/immersion-tracker-service.test.js dist/core/services/immersion-tracker/storage-session.test.js",
|
|
||||||
"test:immersion:sqlite": "bun run tsc && bun run test:immersion:sqlite:dist",
|
|
||||||
"test:src": "node scripts/run-test-lane.mjs bun-src-full",
|
|
||||||
"test:launcher:unit:src": "node scripts/run-test-lane.mjs bun-launcher-unit",
|
|
||||||
"test:launcher:env:src": "bun run test:launcher:smoke:src && bun run test:plugin:src",
|
|
||||||
"test:env": "bun run test:launcher:env:src && bun run test:immersion:sqlite:src",
|
|
||||||
"test:node:compat": "bun run tsc && node --experimental-sqlite --test dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/overlay-manager.test.js dist/main/config-validation.test.js dist/main/runtime/registry.test.js dist/main/runtime/startup-config.test.js",
|
|
||||||
"test:full": "bun run test:src && bun run test:launcher:unit:src && bun run test:node:compat"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"anki",
|
"anki",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface CliOptions {
|
|||||||
emitDiagnostics: boolean;
|
emitDiagnostics: boolean;
|
||||||
mecabCommand?: string;
|
mecabCommand?: string;
|
||||||
mecabDictionaryPath?: string;
|
mecabDictionaryPath?: string;
|
||||||
forceMecabOnly?: boolean;
|
forceMecabOnly: boolean;
|
||||||
yomitanExtensionPath?: string;
|
yomitanExtensionPath?: string;
|
||||||
yomitanUserDataPath?: string;
|
yomitanUserDataPath?: string;
|
||||||
emitColoredLine: boolean;
|
emitColoredLine: boolean;
|
||||||
@@ -678,7 +678,7 @@ function getBandColor(
|
|||||||
}
|
}
|
||||||
const normalizedBand = Math.ceil((safeRank / topX) * bandedColors.length);
|
const normalizedBand = Math.ceil((safeRank / topX) * bandedColors.length);
|
||||||
const band = Math.min(bandedColors.length, Math.max(1, normalizedBand));
|
const band = Math.min(bandedColors.length, Math.max(1, normalizedBand));
|
||||||
return bandedColors[band - 1];
|
return bandedColors[band - 1] ?? colorSingle;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTokenColor(token: MergedToken, args: CliOptions): string {
|
function getTokenColor(token: MergedToken, args: CliOptions): string {
|
||||||
@@ -845,7 +845,26 @@ async function main(): Promise<void> {
|
|||||||
? simplifyTokenWithVerbose(token, getFrequencyRank)
|
? simplifyTokenWithVerbose(token, getFrequencyRank)
|
||||||
: simplifyToken(token),
|
: simplifyToken(token),
|
||||||
) ?? null;
|
) ?? null;
|
||||||
const diagnostics = {
|
const diagnostics: {
|
||||||
|
yomitan: {
|
||||||
|
available: boolean;
|
||||||
|
loaded: boolean;
|
||||||
|
forceMecabOnly: boolean;
|
||||||
|
note: string | null;
|
||||||
|
};
|
||||||
|
mecab: {
|
||||||
|
command: string;
|
||||||
|
dictionaryPath: string | null;
|
||||||
|
available: boolean;
|
||||||
|
status?: 'ok' | 'no-tokens';
|
||||||
|
note?: string;
|
||||||
|
};
|
||||||
|
tokenizer: {
|
||||||
|
sourceHint: 'none' | 'yomitan-merged' | 'mecab-merge';
|
||||||
|
mergedTokenCount: number;
|
||||||
|
totalTokenCount: number;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
yomitan: {
|
yomitan: {
|
||||||
available: Boolean(yomitanState?.available),
|
available: Boolean(yomitanState?.available),
|
||||||
loaded: useYomitan,
|
loaded: useYomitan,
|
||||||
@@ -864,11 +883,11 @@ async function main(): Promise<void> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (tokens === null) {
|
if (tokens === null) {
|
||||||
diagnostics.mecab['status'] = 'no-tokens';
|
diagnostics.mecab.status = 'no-tokens';
|
||||||
diagnostics.mecab['note'] =
|
diagnostics.mecab.note =
|
||||||
'MeCab returned no parseable tokens. This is often caused by a missing/invalid MeCab dictionary path.';
|
'MeCab returned no parseable tokens. This is often caused by a missing/invalid MeCab dictionary path.';
|
||||||
} else {
|
} else {
|
||||||
diagnostics.mecab['status'] = 'ok';
|
diagnostics.mecab.status = 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = {
|
const output = {
|
||||||
|
|||||||
@@ -348,7 +348,11 @@ function findSelectedCandidateIndexes(
|
|||||||
const mergedSignatures = mergedTokens.map(mergedTokenSignature);
|
const mergedSignatures = mergedTokens.map(mergedTokenSignature);
|
||||||
const selected: number[] = [];
|
const selected: number[] = [];
|
||||||
for (let i = 0; i < candidates.length; i += 1) {
|
for (let i = 0; i < candidates.length; i += 1) {
|
||||||
const candidateSignatures = candidates[i].tokens.map(candidateTokenSignature);
|
const candidate = candidates[i];
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const candidateSignatures = candidate.tokens.map(candidateTokenSignature);
|
||||||
if (candidateSignatures.length !== mergedSignatures.length) {
|
if (candidateSignatures.length !== mergedSignatures.length) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -490,6 +494,9 @@ function renderTextOutput(payload: Record<string, unknown>): void {
|
|||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < finalTokens.length; i += 1) {
|
for (let i = 0; i < finalTokens.length; i += 1) {
|
||||||
const token = finalTokens[i];
|
const token = finalTokens[i];
|
||||||
|
if (!token) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
` [${i}] ${token.surface} -> ${token.headword} (${token.reading}) [${token.startPos}, ${token.endPos})\n`,
|
` [${i}] ${token.surface} -> ${token.headword} (${token.reading}) [${token.startPos}, ${token.endPos})\n`,
|
||||||
);
|
);
|
||||||
@@ -505,6 +512,9 @@ function renderTextOutput(payload: Record<string, unknown>): void {
|
|||||||
|
|
||||||
for (let i = 0; i < candidates.length; i += 1) {
|
for (let i = 0; i < candidates.length; i += 1) {
|
||||||
const candidate = candidates[i];
|
const candidate = candidates[i];
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
` [${i}] source=${String(candidate.source)} index=${String(candidate.index)} selectedByTokenizer=${String(candidate.selectedByTokenizer)} tokenCount=${String(candidate.tokenCount)}\n`,
|
` [${i}] source=${String(candidate.source)} index=${String(candidate.index)} selectedByTokenizer=${String(candidate.selectedByTokenizer)} tokenCount=${String(candidate.tokenCount)}\n`,
|
||||||
);
|
);
|
||||||
@@ -514,6 +524,9 @@ function renderTextOutput(payload: Record<string, unknown>): void {
|
|||||||
}
|
}
|
||||||
for (let j = 0; j < tokens.length; j += 1) {
|
for (let j = 0; j < tokens.length; j += 1) {
|
||||||
const token = tokens[j];
|
const token = tokens[j];
|
||||||
|
if (!token) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
` - ${token.surface} -> ${token.headword} (${token.reading}) [${token.startPos}, ${token.endPos})\n`,
|
` - ${token.surface} -> ${token.headword} (${token.reading}) [${token.startPos}, ${token.endPos})\n`,
|
||||||
);
|
);
|
||||||
|
|||||||
12
tsconfig.typecheck.json
Normal file
12
tsconfig.typecheck.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"noEmit": true,
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"sourceMap": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "launcher/**/*.ts", "scripts/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "vendor"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user