refactor(app): migrate main + preload to TypeScript with typed seams (Phase 5d-3c-ii)

Convert the app's main and preload source from .js to .ts (git mv preserves
history), adding types where they carry value: the core-consumption seam
(BuildIndexOptions/BuildIndexResult, FileInfo), the indexer service/worker
factories, and the preload IPC bridge. Module-to-module specifiers use the
real .ts extension (mirroring scripts/, since Node type-stripping does not
rewrite .js->.ts); the worker's runtime path stays indexer-worker.js because
that is the built output.

Toolchain:
- Add app/tsconfig.json: strict but noImplicitAny:false (the app orchestrates
  the already-strict core; annotating every SQLite-handle helper is low-value
  churn) + allowImportingTsExtensions (safe under noEmit).
- Add @types/better-sqlite3 for the injected binding.
- electron.vite.config.ts inputs -> .ts; refresh the stale CommonJS comment.
- typecheck script runs root + app projects. Root tsconfig excludes the
  app-importing tests (app-*.test.mjs, recap-capture-query.test.mjs) so the
  lenient app files are not dragged into the strict root program; the app
  source is covered by app/tsconfig.json instead. See docs/adr/0005.

Verified: npm run typecheck (root + app) clean; suite 121/121; electron-vite
build emits all 6 main entries + preload with no .ts/node:sqlite residue in
the bundles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tommy0103
2026-07-09 16:25:59 +08:00
co-authored by Claude Opus 4.8
parent 60d47a852e
commit 01a390fa10
21 changed files with 299 additions and 151 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
const require = createRequire(import.meta.url);
import { createIndexerService } from '../app/src/main/indexer-service.js';
import { createIndexerService } from '../app/src/main/indexer-service.ts';
function manualTimers() {
const timers = new Set();
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
import { createWorkerBuildIndex } from '../app/src/main/indexer-worker-client.js';
import { createWorkerBuildIndex } from '../app/src/main/indexer-worker-client.ts';
test('worker build client resolves build results from a worker thread', async () => {
const instances = [];
+1 -1
View File
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
const require = createRequire(import.meta.url);
import { buildIndex } from '../app/src/main/indexer.js';
import { buildIndex } from '../app/src/main/indexer.ts';
const { DatabaseSync } = require('node:sqlite');
class TestDatabase {
+4 -4
View File
@@ -22,7 +22,7 @@ const require = createRequire(import.meta.url);
// resolve each bare specifier exactly as the main module sees it (ESM resolution
// relative to the main module's directory) and mock that URL. Relative deps are
// resolved against the main module URL directly.
const mainUrl = new URL('../app/src/main/index.js', import.meta.url);
const mainUrl = new URL('../app/src/main/index.ts', import.meta.url);
const mainPath = fileURLToPath(mainUrl);
const mainDir = fileURLToPath(new URL('.', mainUrl));
@@ -37,9 +37,9 @@ function esmResolve(specifier) {
const ELECTRON_URL = esmResolve('electron');
const DATABASE_URL = esmResolve('better-sqlite3');
const CHOKIDAR_URL = esmResolve('chokidar');
const INDEXER_URL = new URL('./indexer.js', mainUrl).href;
const INDEXER_SERVICE_URL = new URL('./indexer-service.js', mainUrl).href;
const INDEXER_WORKER_URL = new URL('./indexer-worker-client.js', mainUrl).href;
const INDEXER_URL = new URL('./indexer.ts', mainUrl).href;
const INDEXER_SERVICE_URL = new URL('./indexer-service.ts', mainUrl).href;
const INDEXER_WORKER_URL = new URL('./indexer-worker-client.ts', mainUrl).href;
let importCounter = 0;
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
import { buildRecapExportQuery, cleanRecapFilename } from '../app/src/main/recap-capture-query.js';
import { buildRecapExportQuery, cleanRecapFilename } from '../app/src/main/recap-capture-query.ts';
test('recap export query includes the selected recap filename', () => {
const query = buildRecapExportQuery({