| … | … | @@ -11,7 +11,27 @@ import path from 'path' |
| 11 | 11 | import os from 'os' |
| 12 | 12 | import process from 'process' |
| 13 | 13 | import Pipe from 'bare-pipe' |
| 14 | | import { startDaemon } from './daemon/daemon.js' |
| 14 | |
| 15 | // keet-daemon is loaded LAZILY (see loadKeetDaemon) — never as a top-level |
| 16 | // import. A static `import './daemon/daemon.js'` is evaluated before main() |
| 17 | // runs flattenIfNeeded(), so on a fresh pgh install (content-addressed |
| 18 | // symlinked node_modules) keet-core's Bundlebee resolver can't find its |
| 19 | // modules — it expects a flat npm layout, and Bare caches by realpath. The |
| 20 | // daemon must be imported only AFTER node_modules is flattened. |
| 21 | let startDaemon = null |
| 22 | let _keetDaemonLoaded = false |
| 23 | async function loadKeetDaemon () { |
| 24 | if (_keetDaemonLoaded) return startDaemon |
| 25 | _keetDaemonLoaded = true |
| 26 | try { |
| 27 | const mod = await import('./daemon/daemon.js') |
| 28 | startDaemon = mod.startDaemon |
| 29 | } catch (err) { |
| 30 | console.error('[launcher] keet-daemon module not loadable:', err.message) |
| 31 | if (err.code) console.error('[launcher] code:', err.code) |
| 32 | } |
| 33 | return startDaemon |
| 34 | } |
| 15 | 35 | |
| 16 | 36 | const isPear = typeof Pear !== 'undefined' |
| 17 | 37 | const DAEMON_SOCKET = '/tmp/keet-daemon.sock' |
| … | … | @@ -110,6 +130,13 @@ async function ensureDaemon () { |
| 110 | 130 | // Remove stale socket if it exists |
| 111 | 131 | try { fs.unlinkSync(DAEMON_SOCKET) } catch {} |
| 112 | 132 | |
| 133 | // Daemon module is loaded lazily after the flatten; if it failed to load |
| 134 | // there's nothing to start. |
| 135 | if (!startDaemon) { |
| 136 | console.error('[launcher] keet-daemon module not loaded — starting without daemon') |
| 137 | return null |
| 138 | } |
| 139 | |
| 113 | 140 | // Start daemon in-process |
| 114 | 141 | console.log('[launcher] Starting keet-daemon (in-process)...') |
| 115 | 142 | try { |
| … | … | @@ -187,6 +214,53 @@ function startBackgroundUpdate () { |
| 187 | 214 | } |
| 188 | 215 | } |
| 189 | 216 | |
| 217 | // Replace top-level symlinks in node_modules with real copies. Idempotent — |
| 218 | // no-op if already flat. Required because keet-core 4.x's Bundlebee resolver |
| 219 | // expects a flat npm layout, while pgh's content-addressed materializer |
| 220 | // installs node_modules/<pkg> as symlinks into node_modules/.pgh/<hash>/. |
| 221 | // Without this, keet-daemon throws "failed to find module: |
| 222 | // /node_modules/<pkg>/index.js" on first bundle load (room invite / chat send). |
| 223 | // Mirrors the proven hyper-tps workaround. |
| 224 | async function flattenIfNeeded () { |
| 225 | const nodeModulesDir = path.join(getAppDir(), 'node_modules') |
| 226 | if (!fs.existsSync(nodeModulesDir)) return |
| 227 | |
| 228 | // Quick check: any top-level (or scoped) symlink left? |
| 229 | let needsFlatten = false |
| 230 | for (const name of fs.readdirSync(nodeModulesDir)) { |
| 231 | if (name === '.pgh') continue |
| 232 | const p = path.join(nodeModulesDir, name) |
| 233 | try { |
| 234 | const st = fs.lstatSync(p) |
| 235 | if (st.isSymbolicLink()) { needsFlatten = true; break } |
| 236 | if (name.startsWith('@') && st.isDirectory()) { |
| 237 | for (const sub of fs.readdirSync(p)) { |
| 238 | if (fs.lstatSync(path.join(p, sub)).isSymbolicLink()) { needsFlatten = true; break } |
| 239 | } |
| 240 | } |
| 241 | } catch {} |
| 242 | if (needsFlatten) break |
| 243 | } |
| 244 | if (!needsFlatten) { |
| 245 | console.log('[launcher] node_modules already flat — skipping flatten') |
| 246 | return |
| 247 | } |
| 248 | |
| 249 | console.log('[launcher] Flattening node_modules (pgh symlinks → real copies)…') |
| 250 | const scriptPath = path.join(getAppDir(), 'flatten-node-modules.cjs') |
| 251 | if (!fs.existsSync(scriptPath)) { |
| 252 | console.error('[launcher] flatten script missing at', scriptPath) |
| 253 | return |
| 254 | } |
| 255 | await new Promise((resolve, reject) => { |
| 256 | const child = subprocess.spawn('node', [scriptPath, nodeModulesDir], { stdio: 'inherit' }) |
| 257 | child.on('exit', code => code === 0 ? resolve() : reject(new Error('flatten exit ' + code))) |
| 258 | child.on('error', reject) |
| 259 | }).catch(err => { |
| 260 | console.error('[launcher] flatten failed (continuing anyway):', err.message) |
| 261 | }) |
| 262 | } |
| 263 | |
| 190 | 264 | let daemonRef = null |
| 191 | 265 | |
| 192 | 266 | async function main () { |
| … | … | @@ -201,6 +275,12 @@ async function main () { |
| 201 | 275 | console.log(`[launcher] godot-keet ${appVersion}`) |
| 202 | 276 | console.log(`[launcher] Platform: ${platformDir}`) |
| 203 | 277 | |
| 278 | // keet-core's Bundlebee needs a flat node_modules; pgh installs it as |
| 279 | // content-addressed symlinks. Flatten first, THEN load the daemon module |
| 280 | // (its keet-core import resolves only against the flat layout). |
| 281 | await flattenIfNeeded() |
| 282 | await loadKeetDaemon() |
| 283 | |
| 204 | 284 | // Start daemon in-process (or detect existing external daemon) |
| 205 | 285 | daemonRef = await ensureDaemon() |
| 206 | 286 | |