mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging. Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan. Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews. Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
364 lines
15 KiB
TypeScript
364 lines
15 KiB
TypeScript
import { markdownParserAliases } from './config/build-plugins/markdown-parser-exports'
|
|
import { isBuiltin } from 'node:module'
|
|
import { resolve } from 'node:path'
|
|
import { defineConfig, type UserConfig } from 'electron-vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { createBootstrapFatalExitBanner } from './config/build-plugins/bootstrap-fatal-exit-banner'
|
|
import { createPdfjsViewerAssetsPlugin } from './config/build-plugins/pdfjs-viewer-assets'
|
|
import { createProcessHostDevRebuildPlugin } from './config/build-plugins/process-host-dev-rebuild'
|
|
import {
|
|
CLI_MAIN_ENTRY_NAMES,
|
|
createPlainNodeEntryGuardPlugin
|
|
} from './config/build-plugins/plain-node-entry-guard'
|
|
import { ORCAD_LOCAL_SERVE_SELECTION_ENTRY } from './src/shared/orcad-local-serve-selection'
|
|
import packageJson from './package.json' with { type: 'json' }
|
|
|
|
const BUNDLED_MAIN_DEPENDENCIES = new Set([
|
|
'stream-json',
|
|
'stream-chain',
|
|
'@xterm/headless',
|
|
'@xterm/addon-serialize',
|
|
'tldts',
|
|
'smol-toml',
|
|
// Why: Windows NSIS deploys app.asar before external resources; bootstrap must
|
|
// not race the later resources/node_modules copy.
|
|
'zod'
|
|
])
|
|
const EXTERNAL_MAIN_DEPENDENCIES = Object.keys(packageJson.dependencies).filter(
|
|
(dependency) => !BUNDLED_MAIN_DEPENDENCIES.has(dependency)
|
|
)
|
|
|
|
function isExternalMainModule(source: string): boolean {
|
|
if (isBuiltin(source) || source === 'electron' || source.startsWith('electron/')) {
|
|
return true
|
|
}
|
|
return EXTERNAL_MAIN_DEPENDENCIES.some(
|
|
(dependency) => source === dependency || source.startsWith(`${dependency}/`)
|
|
)
|
|
}
|
|
|
|
// Why: the telemetry transport is gated by two compile-time constants that
|
|
// only the official CI release workflow sets. Contributor / `pnpm dev` /
|
|
// third-party rebuilds must substitute literal `null` at these sites so
|
|
// `IS_OFFICIAL_BUILD` in `src/main/telemetry/client.ts` evaluates `false`
|
|
// at module load and the track() wrapper short-circuits to console-mirror.
|
|
// The substitution happens at compile time — there is no runtime env-var
|
|
// fallback — so a curious contributor cannot spoof transmission with a
|
|
// shell export.
|
|
//
|
|
// CI injects real values via GitHub Actions secrets
|
|
// (ORCA_BUILD_IDENTITY='stable' | 'rc', ORCA_POSTHOG_WRITE_KEY=phc_...);
|
|
// every other build path resolves these env vars to undefined, which the
|
|
// JSON.stringify below folds to the literal `null`. Ambient declarations
|
|
// for the two constants live in `src/types/build-constants.d.ts`.
|
|
const orcaBuildIdentity = process.env.ORCA_BUILD_IDENTITY
|
|
const ORCA_BUILD_IDENTITY_LITERAL =
|
|
orcaBuildIdentity === 'stable' || orcaBuildIdentity === 'rc'
|
|
? JSON.stringify(orcaBuildIdentity)
|
|
: 'null'
|
|
const orcaPostHogWriteKey = process.env.ORCA_POSTHOG_WRITE_KEY
|
|
const ORCA_POSTHOG_WRITE_KEY_LITERAL =
|
|
typeof orcaPostHogWriteKey === 'string' && orcaPostHogWriteKey.length > 0
|
|
? JSON.stringify(orcaPostHogWriteKey)
|
|
: 'null'
|
|
const orcaDiagnosticsTokenUrl = process.env.ORCA_DIAGNOSTICS_TOKEN_URL
|
|
const ORCA_DIAGNOSTICS_TOKEN_URL_LITERAL =
|
|
typeof orcaDiagnosticsTokenUrl === 'string' && orcaDiagnosticsTokenUrl.length > 0
|
|
? JSON.stringify(orcaDiagnosticsTokenUrl)
|
|
: 'null'
|
|
|
|
function createStartupDiagnosticsBanner(chunkName: string): string {
|
|
return `
|
|
;(() => {
|
|
const env = typeof process !== 'undefined' ? process.env : undefined
|
|
const mode = env?.ORCA_STARTUP_DIAGNOSTICS
|
|
if (mode !== '1' && mode !== 'trace') {
|
|
return
|
|
}
|
|
const safeJson = (value) => {
|
|
try {
|
|
return JSON.stringify(value)
|
|
} catch {
|
|
return '"<unserializable>"'
|
|
}
|
|
}
|
|
let closeSync
|
|
let diagnosticFileDescriptor
|
|
let openSync
|
|
let writeSync
|
|
try {
|
|
const fs = require('node:fs')
|
|
closeSync = fs.closeSync
|
|
openSync = fs.openSync
|
|
writeSync = fs.writeSync
|
|
} catch {
|
|
closeSync = undefined
|
|
openSync = undefined
|
|
writeSync = undefined
|
|
}
|
|
const diagnosticFile = env?.ORCA_STARTUP_DIAGNOSTICS_FILE
|
|
if (typeof diagnosticFile === 'string' && diagnosticFile.length > 0 && typeof openSync === 'function') {
|
|
try {
|
|
diagnosticFileDescriptor = openSync(diagnosticFile, 'a', 0o600)
|
|
} catch {
|
|
diagnosticFileDescriptor = undefined
|
|
}
|
|
}
|
|
const writeLine = (message) => {
|
|
try {
|
|
const line = message.endsWith('\\n') ? message : message + '\\n'
|
|
if (typeof writeSync === 'function') {
|
|
writeSync(2, line)
|
|
if (typeof diagnosticFileDescriptor === 'number') {
|
|
writeSync(diagnosticFileDescriptor, line)
|
|
}
|
|
}
|
|
} catch {
|
|
// Diagnostics must never affect startup.
|
|
}
|
|
}
|
|
const chunkName = ${JSON.stringify(chunkName)}
|
|
writeLine('[bootstrap] bundle-enter chunk=' + safeJson(chunkName) + ' pid=' + process.pid + ' ppid=' + process.ppid + ' execPath=' + safeJson(process.execPath) + ' argv=' + safeJson(process.argv) + ' electronRunAsNode=' + safeJson(env?.ELECTRON_RUN_AS_NODE ?? null))
|
|
if (!globalThis.__ORCA_BOOTSTRAP_EXIT_LOG_INSTALLED__) {
|
|
globalThis.__ORCA_BOOTSTRAP_EXIT_LOG_INSTALLED__ = true
|
|
process.once('exit', (code) => {
|
|
writeLine('[bootstrap] process-exit code=' + code)
|
|
if (typeof closeSync === 'function' && typeof diagnosticFileDescriptor === 'number') {
|
|
try {
|
|
closeSync(diagnosticFileDescriptor)
|
|
} catch {
|
|
// Diagnostics must never affect shutdown.
|
|
}
|
|
}
|
|
})
|
|
process.on('uncaughtExceptionMonitor', (error, origin) => {
|
|
const message = error && typeof error === 'object' && 'stack' in error ? error.stack : error
|
|
writeLine('[bootstrap] uncaught-exception origin=' + safeJson(origin) + ' error=' + safeJson(String(message)))
|
|
})
|
|
process.on('unhandledRejection', (reason) => {
|
|
const message = reason && typeof reason === 'object' && 'stack' in reason ? reason.stack : reason
|
|
writeLine('[bootstrap] unhandled-rejection error=' + safeJson(String(message)))
|
|
})
|
|
}
|
|
if (mode === 'trace' && !globalThis.__ORCA_BOOTSTRAP_REQUIRE_TRACE_INSTALLED__) {
|
|
globalThis.__ORCA_BOOTSTRAP_REQUIRE_TRACE_INSTALLED__ = true
|
|
try {
|
|
const Module = require('node:module')
|
|
const originalLoad = Module._load
|
|
const parsedTraceLimit = Number(env?.ORCA_STARTUP_DIAGNOSTICS_TRACE_LIMIT ?? 20000)
|
|
const traceLimit = Number.isFinite(parsedTraceLimit) && parsedTraceLimit > 0 ? parsedTraceLimit : 20000
|
|
let traceLineCount = 0
|
|
let traceLimitReported = false
|
|
const writeTraceLine = (message) => {
|
|
if (traceLineCount >= traceLimit) {
|
|
if (!traceLimitReported) {
|
|
traceLimitReported = true
|
|
writeLine('[bootstrap] require-trace-limit-reached limit=' + safeJson(traceLimit))
|
|
}
|
|
return
|
|
}
|
|
traceLineCount += 1
|
|
writeLine(message)
|
|
}
|
|
Module._load = function (request, parent, isMain) {
|
|
const parentName = parent && parent.filename ? parent.filename : null
|
|
writeTraceLine('[bootstrap] require-start request=' + safeJson(request) + ' parent=' + safeJson(parentName) + ' isMain=' + safeJson(Boolean(isMain)))
|
|
try {
|
|
const result = Reflect.apply(originalLoad, this, arguments)
|
|
writeTraceLine('[bootstrap] require-ok request=' + safeJson(request))
|
|
return result
|
|
} catch (error) {
|
|
const message = error && typeof error === 'object' && 'stack' in error ? error.stack : error
|
|
writeTraceLine('[bootstrap] require-error request=' + safeJson(request) + ' error=' + safeJson(String(message)))
|
|
throw error
|
|
}
|
|
}
|
|
} catch (error) {
|
|
writeLine('[bootstrap] require-trace-install-error error=' + safeJson(String(error)))
|
|
}
|
|
}
|
|
})();
|
|
`
|
|
}
|
|
|
|
function createMainBootstrapPlugin() {
|
|
return {
|
|
name: 'orca-main-bootstrap',
|
|
generateBundle(_options, bundle) {
|
|
const mainChunk = bundle['index.js']
|
|
if (!mainChunk || mainChunk.type !== 'chunk') {
|
|
return
|
|
}
|
|
|
|
// Why: source guards and diagnostics run after Rollup's generated require
|
|
// prelude, too late to handle a missing bootstrap dependency.
|
|
mainChunk.code =
|
|
createBootstrapFatalExitBanner() +
|
|
createStartupDiagnosticsBanner(mainChunk.fileName) +
|
|
mainChunk.code
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Diagnostic escape hatch: an unminified main bundle so a V8 CPU profile of the
|
|
* main process attributes self time to real function names. Release builds never
|
|
* set this, and `pnpm build` does not read it.
|
|
*/
|
|
const MAIN_MINIFY: 'oxc' | false = process.env.ORCA_UNMINIFIED_MAIN === '1' ? false : 'oxc'
|
|
|
|
export const electronViteConfig: UserConfig = {
|
|
main: {
|
|
build: {
|
|
// Why: 'esbuild' makes rolldown disable its own minifier and re-print every
|
|
// chunk through esbuild, which is undeclared here and only resolves via
|
|
// pnpm hoisting. 'oxc' is rolldown's in-process minifier.
|
|
minify: MAIN_MINIFY,
|
|
// Why: 'hidden' emits .js.map with no sourceMappingURL, so the shipped
|
|
// bundle never references maps that packaging strips out. Release CI
|
|
// uploads them so minified crash traces stay decodable.
|
|
sourcemap: 'hidden',
|
|
// Why: daemon-entry.js is asar-unpacked so child_process.fork() can
|
|
// execute it from disk. Node's module resolution from the unpacked
|
|
// directory cannot reach into app.asar; startup-critical pure JS must
|
|
// also survive a partially copied Windows resources tree.
|
|
externalizeDeps: {
|
|
exclude: [...BUNDLED_MAIN_DEPENDENCIES]
|
|
},
|
|
rollupOptions: {
|
|
// Why: native dependencies must resolve from packaged node_modules,
|
|
// while the unpacked daemon needs its pure-JS xterm graph bundled.
|
|
external: isExternalMainModule,
|
|
input: {
|
|
index: resolve('src/main/index.ts'),
|
|
// Why: sandboxed webview preloads cannot load Rollup helper chunks.
|
|
'browser-window-close-preload': resolve('src/preload/browser-window-close.ts'),
|
|
'doc-preview-link-preload': resolve('src/preload/doc-preview-link.ts'),
|
|
'daemon-entry': resolve('src/main/daemon/daemon-entry.ts'),
|
|
'plugin-host-entry': resolve('src/main/plugins/plugin-host-entry.ts'),
|
|
'computer-sidecar': resolve('src/main/computer/sidecar-entry.ts'),
|
|
'stt-worker': resolve('src/main/speech/stt-worker.ts'),
|
|
'warp-theme-parser-worker': resolve('src/main/warp-themes/warp-theme-parser-worker.ts'),
|
|
'foreign-sqlite-reader-entry': resolve(
|
|
'src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry.ts'
|
|
),
|
|
'session-scanner-service-entry': resolve(
|
|
'src/main/ai-vault/session-scanner-service-entry.ts'
|
|
),
|
|
'wsl-transcript-fs-process-entry': resolve(
|
|
'src/main/native-chat/wsl-transcript-fs-process-entry.ts'
|
|
),
|
|
// Why: libuv spawns processes inline on the calling loop, so the port
|
|
// scan's probe commands run on a worker thread instead of the UI one.
|
|
'port-scan-command-worker-entry': resolve(
|
|
'src/main/ports/port-scan-command-worker-entry.ts'
|
|
),
|
|
// Why: the Claude/Codex/OpenCode usage scans walk whole history
|
|
// corpora and read SQLite synchronously; a worker thread keeps that
|
|
// off the main-process event loop.
|
|
'usage-scan-worker-entry': resolve('src/main/usage/usage-scan-worker-entry.ts'),
|
|
// Why: a first account setup can merge a large history tree with sync fs calls.
|
|
'claude-profile-setup-worker-entry': resolve(
|
|
'src/main/claude-accounts/claude-profile-setup-worker-entry.ts'
|
|
),
|
|
'profile-state-backup-worker-entry': resolve(
|
|
'src/main/persistence/profile-state/profile-state-backup-worker-entry.ts'
|
|
),
|
|
'profile-state-writer-worker-entry': resolve(
|
|
'src/main/persistence/profile-state/profile-state-writer-worker-entry.ts'
|
|
),
|
|
// Why: forked with ELECTRON_RUN_AS_NODE so @parcel/watcher faults
|
|
// can't take down the main process (issue #7547).
|
|
'parcel-watcher-process-entry': resolve('src/main/ipc/parcel-watcher-process-entry.ts'),
|
|
// Why: `orca serve` runs it under ELECTRON_RUN_AS_NODE so the CLI never bundles orcad prep.
|
|
[ORCAD_LOCAL_SERVE_SELECTION_ENTRY]: resolve(
|
|
'src/main/orcad/orcad-local-serve-selection-entry.ts'
|
|
),
|
|
// Why: a worker thread survives the macOS 26 AppKit main-thread deadlock
|
|
// without paying for another Electron process.
|
|
'main-thread-hang-watchdog-entry': resolve(
|
|
'src/main/hang-watchdog/main-thread-hang-watchdog-entry.ts'
|
|
),
|
|
...Object.fromEntries(
|
|
CLI_MAIN_ENTRY_NAMES.map((module) => [module, resolve(`src/main/${module}.ts`)])
|
|
)
|
|
},
|
|
// Why: Rolldown's SSR default is ESM, but Electron and sidecar launchers
|
|
// consume these stable CommonJS paths.
|
|
output: {
|
|
format: 'cjs',
|
|
entryFileNames: '[name].js',
|
|
chunkFileNames: 'chunks/[name]-[hash].js'
|
|
},
|
|
plugins: [
|
|
createMainBootstrapPlugin(),
|
|
createPlainNodeEntryGuardPlugin(),
|
|
createProcessHostDevRebuildPlugin()
|
|
]
|
|
}
|
|
},
|
|
// Why: compile-time substitution for the telemetry gate. See the block
|
|
// above for the full rationale.
|
|
define: {
|
|
ORCA_BUILD_IDENTITY: ORCA_BUILD_IDENTITY_LITERAL,
|
|
ORCA_POSTHOG_WRITE_KEY: ORCA_POSTHOG_WRITE_KEY_LITERAL,
|
|
ORCA_DIAGNOSTICS_TOKEN_URL: ORCA_DIAGNOSTICS_TOKEN_URL_LITERAL
|
|
},
|
|
// Why: @xterm/headless declares "exports": null in package.json, which
|
|
// prevents Vite's default resolver from finding the CJS entry. Point
|
|
// directly at the published main file so the bundler can inline it.
|
|
resolve: {
|
|
alias: {
|
|
'@xterm/headless': resolve('node_modules/@xterm/headless/lib-headless/xterm-headless.js'),
|
|
'@xterm/addon-serialize': resolve(
|
|
'node_modules/@xterm/addon-serialize/lib/addon-serialize.js'
|
|
)
|
|
}
|
|
}
|
|
},
|
|
preload: {
|
|
build: {
|
|
externalizeDeps: {
|
|
exclude: ['zod']
|
|
}
|
|
}
|
|
},
|
|
renderer: {
|
|
resolve: {
|
|
alias: {
|
|
...markdownParserAliases,
|
|
'@renderer': resolve('src/renderer/src'),
|
|
'@': resolve('src/renderer/src')
|
|
}
|
|
},
|
|
plugins: [react(), tailwindcss(), createPdfjsViewerAssetsPlugin()],
|
|
worker: {
|
|
format: 'es'
|
|
},
|
|
build: {
|
|
manifest: true,
|
|
modulePreload: { polyfill: true },
|
|
minify: 'oxc',
|
|
target: 'es2020',
|
|
// Why: the pop-out dashboard is a second top-level window with its own
|
|
// React root. It gets its own HTML entry so it can boot independently of
|
|
// the main window while reusing the same preload/window.api. `index` must
|
|
// stay listed — overriding input otherwise drops electron-vite's default
|
|
// renderer entry.
|
|
rollupOptions: {
|
|
// Why: shared chunks must never import an HTML entry whose module mounts
|
|
// a different React root.
|
|
preserveEntrySignatures: 'strict',
|
|
input: {
|
|
index: resolve('src/renderer/index.html'),
|
|
popout: resolve('src/renderer/popout.html'),
|
|
web: resolve('src/renderer/web-index.html')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default defineConfig(electronViteConfig)
|