import fs from 'node:fs'; import path from 'node:path'; import { resolveYomitanExtensionPath as resolveBuiltYomitanExtensionPath } from '../src/core/services/yomitan-extension-paths.js'; // Yomitan bootstrap for CLI scripts that need the app's real tokenizer. Mirrors // what scripts/get_frequency.ts does inline; new scripts should import this. export interface YomitanRuntimeState { yomitanExt: unknown | null; yomitanSession: unknown | null; parserWindow: unknown | null; parserReadyPromise: Promise | null; parserInitPromise: Promise | null; available: boolean; note?: string; } export function withTimeout(promise: Promise, timeoutMs: number, label: string): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`${label} timed out after ${timeoutMs}ms`)); }, timeoutMs); promise .then((value) => { clearTimeout(timer); resolve(value); }) .catch((error) => { clearTimeout(timer); reject(error); }); }); } export function destroyParserWindow(window: unknown): void { if (!window || typeof window !== 'object') { return; } const candidate = window as { isDestroyed?: () => boolean; destroy?: () => void }; if (typeof candidate.isDestroyed !== 'function' || typeof candidate.destroy !== 'function') { return; } if (!candidate.isDestroyed()) { candidate.destroy(); } } export async function loadElectronModule(): Promise { try { const electronImport = await import('electron'); return (electronImport.default ?? electronImport) as typeof import('electron'); } catch { return null; } } async function createYomitanRuntimeState( userDataPath: string, extensionPath?: string, ): Promise { const state: YomitanRuntimeState = { yomitanExt: null, yomitanSession: null, parserWindow: null, parserReadyPromise: null, parserInitPromise: null, available: false, }; const electronImport = await loadElectronModule(); if ( !electronImport || !electronImport.app || typeof electronImport.app.whenReady !== 'function' || !electronImport.session ) { state.note = electronImport ? 'electron runtime not available in this process' : 'electron import failed'; return state; } try { await electronImport.app.whenReady(); const loadYomitanExtension = (await import('../src/core/services/yomitan-extension-loader.js')) .loadYomitanExtension as (options: { userDataPath: string; extensionPath?: string; getYomitanParserWindow: () => unknown; setYomitanParserWindow: (window: unknown) => void; setYomitanParserReadyPromise: (promise: Promise | null) => void; setYomitanParserInitPromise: (promise: Promise | null) => void; setYomitanExtension: (extension: unknown) => void; setYomitanSession: (session: unknown) => void; }) => Promise; const extension = await loadYomitanExtension({ userDataPath, extensionPath, getYomitanParserWindow: () => state.parserWindow, setYomitanParserWindow: (window) => { state.parserWindow = window; }, setYomitanParserReadyPromise: (promise) => { state.parserReadyPromise = promise; }, setYomitanParserInitPromise: (promise) => { state.parserInitPromise = promise; }, setYomitanExtension: (loaded) => { state.yomitanExt = loaded; }, setYomitanSession: (nextSession) => { state.yomitanSession = nextSession; }, }); if (!extension) { state.note = 'yomitan extension is not available'; return state; } state.yomitanExt = extension; state.available = true; return state; } catch (error) { state.note = error instanceof Error ? error.message : 'failed to initialize yomitan extension'; return state; } } export async function createYomitanRuntimeStateWithSearch( userDataPath: string, extensionPath?: string, ): Promise { const resolvedExtensionPath = resolveBuiltYomitanExtensionPath({ explicitPath: extensionPath, cwd: process.cwd(), }); if (resolvedExtensionPath) { try { if (fs.existsSync(path.join(resolvedExtensionPath, 'manifest.json'))) { const state = await createYomitanRuntimeState(userDataPath, resolvedExtensionPath); if (!state.available && !state.note) { state.note = `Failed to load yomitan extension at ${resolvedExtensionPath}`; } return state; } } catch { // fall through to the unconstrained loader below } } // No usable manifest at the resolved path, so let the loader search on its own. return createYomitanRuntimeState(userDataPath); }