diff --git a/src/core/services/yomitan-extension-copy.ts b/src/core/services/yomitan-extension-copy.ts new file mode 100644 index 0000000..8c90f32 --- /dev/null +++ b/src/core/services/yomitan-extension-copy.ts @@ -0,0 +1,53 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const YOMITAN_SYNC_SCRIPT_PATHS = [ + path.join('js', 'app', 'popup.js'), + path.join('js', 'display', 'popup-main.js'), + path.join('js', 'display', 'display.js'), + path.join('js', 'display', 'display-audio.js'), +]; + +function readManifestVersion(manifestPath: string): string | null { + try { + const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { version?: unknown }; + return typeof parsed.version === 'string' ? parsed.version : null; + } catch { + return null; + } +} + +function areFilesEqual(sourcePath: string, targetPath: string): boolean { + if (!fs.existsSync(sourcePath) || !fs.existsSync(targetPath)) return false; + try { + return fs.readFileSync(sourcePath).equals(fs.readFileSync(targetPath)); + } catch { + return false; + } +} + +export function shouldCopyYomitanExtension(sourceDir: string, targetDir: string): boolean { + if (!fs.existsSync(targetDir)) { + return true; + } + + const sourceManifest = path.join(sourceDir, 'manifest.json'); + const targetManifest = path.join(targetDir, 'manifest.json'); + if (!fs.existsSync(sourceManifest) || !fs.existsSync(targetManifest)) { + return true; + } + + const sourceVersion = readManifestVersion(sourceManifest); + const targetVersion = readManifestVersion(targetManifest); + if (sourceVersion === null || targetVersion === null || sourceVersion !== targetVersion) { + return true; + } + + for (const relativePath of YOMITAN_SYNC_SCRIPT_PATHS) { + if (!areFilesEqual(path.join(sourceDir, relativePath), path.join(targetDir, relativePath))) { + return true; + } + } + + return false; +} diff --git a/src/core/services/yomitan-extension-loader.test.ts b/src/core/services/yomitan-extension-loader.test.ts new file mode 100644 index 0000000..aaf645e --- /dev/null +++ b/src/core/services/yomitan-extension-loader.test.ts @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { shouldCopyYomitanExtension } from './yomitan-extension-copy'; + +function writeFile(filePath: string, content: string): void { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf-8'); +} + +test('shouldCopyYomitanExtension detects popup runtime script drift', () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'yomitan-copy-test-')); + const sourceDir = path.join(tempRoot, 'source'); + const targetDir = path.join(tempRoot, 'target'); + + writeFile(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); + writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); + + writeFile(path.join(sourceDir, 'js', 'app', 'popup.js'), 'same-popup-script'); + writeFile(path.join(targetDir, 'js', 'app', 'popup.js'), 'same-popup-script'); + + writeFile(path.join(sourceDir, 'js', 'display', 'popup-main.js'), 'source-popup-main'); + writeFile(path.join(targetDir, 'js', 'display', 'popup-main.js'), 'target-popup-main'); + + assert.equal(shouldCopyYomitanExtension(sourceDir, targetDir), true); +}); + +test('shouldCopyYomitanExtension skips copy when versions and watched scripts match', () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'yomitan-copy-test-')); + const sourceDir = path.join(tempRoot, 'source'); + const targetDir = path.join(tempRoot, 'target'); + + writeFile(path.join(sourceDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); + writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify({ version: '1.0.0' })); + + writeFile(path.join(sourceDir, 'js', 'app', 'popup.js'), 'same-popup-script'); + writeFile(path.join(targetDir, 'js', 'app', 'popup.js'), 'same-popup-script'); + + writeFile(path.join(sourceDir, 'js', 'display', 'popup-main.js'), 'same-popup-main'); + writeFile(path.join(targetDir, 'js', 'display', 'popup-main.js'), 'same-popup-main'); + + writeFile(path.join(sourceDir, 'js', 'display', 'display.js'), 'same-display'); + writeFile(path.join(targetDir, 'js', 'display', 'display.js'), 'same-display'); + + writeFile(path.join(sourceDir, 'js', 'display', 'display-audio.js'), 'same-display-audio'); + writeFile(path.join(targetDir, 'js', 'display', 'display-audio.js'), 'same-display-audio'); + + assert.equal(shouldCopyYomitanExtension(sourceDir, targetDir), false); +}); diff --git a/src/core/services/yomitan-extension-loader.ts b/src/core/services/yomitan-extension-loader.ts index 59b6fd5..fcde4dc 100644 --- a/src/core/services/yomitan-extension-loader.ts +++ b/src/core/services/yomitan-extension-loader.ts @@ -2,6 +2,7 @@ import { BrowserWindow, Extension, session } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; import { createLogger } from '../../logger'; +import { shouldCopyYomitanExtension } from './yomitan-extension-copy'; const logger = createLogger('main:yomitan-extension-loader'); @@ -22,27 +23,7 @@ function ensureExtensionCopy(sourceDir: string, userDataPath: string): string { const extensionsRoot = path.join(userDataPath, 'extensions'); const targetDir = path.join(extensionsRoot, 'yomitan'); - const sourceManifest = path.join(sourceDir, 'manifest.json'); - const targetManifest = path.join(targetDir, 'manifest.json'); - - let shouldCopy = !fs.existsSync(targetDir); - if (!shouldCopy && fs.existsSync(sourceManifest) && fs.existsSync(targetManifest)) { - try { - const sourceVersion = ( - JSON.parse(fs.readFileSync(sourceManifest, 'utf-8')) as { - version: string; - } - ).version; - const targetVersion = ( - JSON.parse(fs.readFileSync(targetManifest, 'utf-8')) as { - version: string; - } - ).version; - shouldCopy = sourceVersion !== targetVersion; - } catch { - shouldCopy = true; - } - } + const shouldCopy = shouldCopyYomitanExtension(sourceDir, targetDir); if (shouldCopy) { fs.mkdirSync(extensionsRoot, { recursive: true });