refactor(app): migrate to ESM; preload emitted as CJS for sandbox (Phase 5d-3b)

App source (main/preload/worker/renderer) -> ESM; app is now type: module;
__dirname via import.meta.url; worker spawned with type module. Preload is built
as CJS (electron-vite output format) because the sandboxed renderer does not
support ESM preload; main loads ../preload/index.js. Removed dead imports
(nativeImage, readline) and the obsolete scripts/dev.js.

Tests: 4 app tests require->import; app-main-settings rewritten with node:test
mock.module + dynamic import (replacing CJS Module._load mocking); test script
adds --experimental-test-module-mocks. electron-vite build clean, 119/119, and
npm run dev verified: app launches, preload bridges IPC, data loads.
This commit is contained in:
tommy0103
2026-07-09 11:05:00 +08:00
parent 80c125a572
commit 905c10789a
16 changed files with 313 additions and 555 deletions
+13 -12
View File
@@ -1,12 +1,16 @@
const { app, BrowserWindow, ipcMain, clipboard, dialog, nativeImage } = require('electron');
const path = require('path');
const os = require('os');
const fs = require('fs');
const Database = require('better-sqlite3');
const { writeHeartbeat } = require('./indexer');
const { createIndexerService } = require('./indexer-service');
const { createWorkerBuildIndex } = require('./indexer-worker-client');
const { buildRecapExportQuery } = require('./recap-capture-query');
import { app, BrowserWindow, ipcMain, clipboard, dialog, shell } from 'electron';
import path from 'node:path';
import os from 'node:os';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import Database from 'better-sqlite3';
import chokidar from 'chokidar';
import { writeHeartbeat } from './indexer.js';
import { createIndexerService } from './indexer-service.js';
import { createWorkerBuildIndex } from './indexer-worker-client.js';
import { buildRecapExportQuery } from './recap-capture-query.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function detectClaudeDir() {
// macOS / Linux: ~/.claude
@@ -283,7 +287,6 @@ let obeliskWatcher = null;
function startObeliskWatcher() {
if (obeliskWatcher) return obeliskWatcher;
const chokidar = require('chokidar');
if (!fs.existsSync(OBELISK_DIR)) {
fs.mkdirSync(OBELISK_DIR, { recursive: true });
}
@@ -488,7 +491,6 @@ ipcMain.handle('db:getMessageFullText', (_, uuid) => {
if (!jsonlPath || !fs.existsSync(jsonlPath)) return null;
// Scan JSONL for the message UUID and extract full text
const readline = require('readline');
const data = fs.readFileSync(jsonlPath, 'utf-8');
const lines = data.split('\n');
for (const line of lines) {
@@ -809,7 +811,6 @@ ipcMain.handle('settings:browseFolder', async (event) => {
});
ipcMain.handle('settings:revealPath', (_, p) => {
const { shell } = require('electron');
if (fs.existsSync(p)) shell.showItemInFolder(p);
});
+6 -5
View File
@@ -1,6 +1,7 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import chokidarModule from 'chokidar';
const DEFAULT_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
const DEFAULT_DEBOUNCE_MS = 2000;
@@ -38,7 +39,7 @@ function createIndexerService({
if (!name || name.endsWith('.jsonl') || name.endsWith('.json')) onChange(name);
};
for (const root of existingRoots) {
const watcher = (chokidar || require('chokidar')).watch(root, {
const watcher = (chokidar || chokidarModule).watch(root, {
cwd: root,
ignoreInitial: true,
awaitWriteFinish: {
@@ -192,4 +193,4 @@ function createIndexerService({
};
}
module.exports = { createIndexerService };
export { createIndexerService };
+7 -4
View File
@@ -1,5 +1,8 @@
const path = require('path');
const { Worker } = require('worker_threads');
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Worker } from 'node:worker_threads';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function createWorkerBuildIndex({
workerPath = path.join(__dirname, 'indexer-worker.js'),
@@ -16,7 +19,7 @@ function createWorkerBuildIndex({
const ensureWorker = () => {
if (worker) return worker;
worker = new WorkerImpl(workerPath);
worker = new WorkerImpl(workerPath, { type: 'module' });
worker.on('message', (message) => {
const current = pending.get(message.id);
if (!current) return;
@@ -57,4 +60,4 @@ function createWorkerBuildIndex({
return { buildIndex, stop };
}
module.exports = { createWorkerBuildIndex };
export { createWorkerBuildIndex };
+2 -2
View File
@@ -1,5 +1,5 @@
const { parentPort } = require('worker_threads');
const { buildIndex } = require('./indexer');
import { parentPort } from 'node:worker_threads';
import { buildIndex } from './indexer.js';
parentPort.on('message', ({ id, args }) => {
try {
+8 -5
View File
@@ -1,7 +1,10 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const Database = require('better-sqlite3');
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import Database from 'better-sqlite3';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const TEXT_LIMIT = 10000;
const DEFAULT_CLAUDE_DIR = path.join(os.homedir(), '.claude');
@@ -1161,7 +1164,7 @@ function buildIndex({
}
}
module.exports = {
export {
buildIndex,
writeHeartbeat,
openIndexDb,
+2 -2
View File
@@ -1,4 +1,4 @@
const path = require('path');
import path from 'node:path';
function cleanRecapFilename(filename) {
if (!filename) return '';
@@ -15,4 +15,4 @@ function buildRecapExportQuery({ cardIdx = 0, archetype = '', filename = '' } =
return params.toString();
}
module.exports = { buildRecapExportQuery, cleanRecapFilename };
export { buildRecapExportQuery, cleanRecapFilename };
+1 -1
View File
@@ -1,4 +1,4 @@
const { contextBridge, ipcRenderer } = require('electron');
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('obelisk', {
getSessions: (opts) => ipcRenderer.invoke('db:getSessions', opts),