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:
@@ -24,6 +24,13 @@ export default defineConfig({
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Electron sandbox does not support ESM preload — emit CJS index.js
|
||||
// (main loads ../preload/index.js) even though the project is ESM.
|
||||
output: { format: 'cjs', entryFileNames: '[name].js' },
|
||||
},
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
plugins: [vue()],
|
||||
|
||||
+3
-5
@@ -46,16 +46,13 @@
|
||||
"out/**"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"**/node_modules/better-sqlite3/**"
|
||||
"node_modules/better-sqlite3/**/*"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "../scripts/schema.sql",
|
||||
"to": "scripts/schema.sql"
|
||||
}
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/better-sqlite3/**/*"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -70,5 +67,6 @@
|
||||
"vite": "^6.0.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
const { spawn } = require('child_process');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
|
||||
const appDir = path.join(__dirname, '..');
|
||||
const binExt = process.platform === 'win32' ? '.cmd' : '';
|
||||
const viteBin = path.join(appDir, 'node_modules', '.bin', `vite${binExt}`);
|
||||
const electronBin = path.join(appDir, 'node_modules', '.bin', `electron${binExt}`);
|
||||
const DEFAULT_DEV_PORT = Number(process.env.OBELISK_DEV_SERVER_PORT || 5173);
|
||||
|
||||
let viteProcess = null;
|
||||
let electronProcess = null;
|
||||
let shuttingDown = false;
|
||||
|
||||
function spawnLocal(command, args, extraEnv = {}) {
|
||||
return spawn(command, args, {
|
||||
cwd: appDir,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, ...extraEnv },
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
}
|
||||
|
||||
function waitForDevServer(url, timeoutMs = 20000) {
|
||||
const started = Date.now();
|
||||
return new Promise((resolve, reject) => {
|
||||
const poll = () => {
|
||||
const req = http.get(url, (res) => {
|
||||
res.resume();
|
||||
if (res.statusCode >= 200 && res.statusCode < 400) {
|
||||
resolve();
|
||||
} else {
|
||||
if (Date.now() - started >= timeoutMs) {
|
||||
reject(new Error(`Timed out waiting for ${url}`));
|
||||
} else {
|
||||
setTimeout(poll, 250);
|
||||
}
|
||||
}
|
||||
});
|
||||
req.on('error', () => {
|
||||
if (Date.now() - started >= timeoutMs) {
|
||||
reject(new Error(`Timed out waiting for ${url}`));
|
||||
return;
|
||||
}
|
||||
setTimeout(poll, 250);
|
||||
});
|
||||
req.setTimeout(1000, () => {
|
||||
req.destroy();
|
||||
});
|
||||
};
|
||||
poll();
|
||||
});
|
||||
}
|
||||
|
||||
function isDevServerRunning(url) {
|
||||
return new Promise((resolve) => {
|
||||
const req = http.get(url, (res) => {
|
||||
res.resume();
|
||||
resolve(res.statusCode >= 200 && res.statusCode < 400);
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
req.setTimeout(1000, () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isPortAvailable(port) {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
server.once('error', () => resolve(false));
|
||||
server.once('listening', () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
server.listen(port, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
async function findAvailablePort(startPort) {
|
||||
for (let port = startPort; port < startPort + 20; port++) {
|
||||
if (await isPortAvailable(port)) return port;
|
||||
}
|
||||
throw new Error(`No available port found from ${startPort} to ${startPort + 19}`);
|
||||
}
|
||||
|
||||
function stopChild(child) {
|
||||
if (!child || child.killed) return;
|
||||
child.kill(process.platform === 'win32' ? undefined : 'SIGTERM');
|
||||
}
|
||||
|
||||
function shutdown(exitCode = 0) {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
stopChild(electronProcess);
|
||||
stopChild(viteProcess);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let devPort = DEFAULT_DEV_PORT;
|
||||
let devUrl = `http://127.0.0.1:${devPort}`;
|
||||
const existingServer = await isDevServerRunning(devUrl);
|
||||
if (existingServer) {
|
||||
console.log(`Using existing Vite dev server at ${devUrl}`);
|
||||
} else {
|
||||
devPort = await findAvailablePort(DEFAULT_DEV_PORT);
|
||||
devUrl = `http://127.0.0.1:${devPort}`;
|
||||
viteProcess = spawnLocal(viteBin, ['renderer', '--host', '127.0.0.1', '--port', String(devPort), '--strictPort']);
|
||||
viteProcess.on('exit', (code, signal) => {
|
||||
if (!shuttingDown && !electronProcess) shutdown(code || (signal ? 1 : 0));
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForDevServer(devUrl);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
shutdown(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
electronProcess = spawnLocal(electronBin, ['.', '--dev', ...process.argv.slice(2)], {
|
||||
OBELISK_DEV_SERVER_URL: devUrl,
|
||||
});
|
||||
electronProcess.on('exit', (code, signal) => {
|
||||
shutdown(code || (signal ? 1 : 0));
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => shutdown(0));
|
||||
process.on('SIGTERM', () => shutdown(0));
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
shutdown(1);
|
||||
});
|
||||
+13
-12
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('obelisk', {
|
||||
getSessions: (opts) => ipcRenderer.invoke('db:getSessions', opts),
|
||||
|
||||
Reference in New Issue
Block a user