mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* feat(layout): add the dark layout.apply channel and route pane splits through main behind ORCA_LAYOUT_OWNER A build made with ORCA_LAYOUT_OWNER=main registers layout.apply: main applies the window's layout command to the stored partition through the shared applyLayoutCommand, minting new ids. A user split then waits for main's answer and mounts the pane id main minted. Every other build substitutes 'window', so neither branch is compiled in. * fix(layout): write only the records a layout.apply command touched A load-then-save rewrite also normalizes records the command never touched. The command's effect is now the diff between the save before and after it, written onto the stored partition record by record. A split whose main reply says a paired server holds the tab splits as today. * fix(layout): write a load-supplied record whole and check that patched partitions reload to the command's result * fix(layout): leave a split to today's path only when the workspace's row names a paired server The split switch point asked which host to route to with a helper that falls back to the focused server, so a focused server could claim a workspace whose row names no owner. Use the same owner test the tab mirror uses. * test(layout): type the split owner mock without a cast
325 lines
13 KiB
TypeScript
325 lines
13 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 { MAIN_BUILD_ENTRY_POINTS } from './config/build-plugins/main-build-entry-points'
|
|
import { createPlainNodeEntryGuardPlugin } from './config/build-plugins/plain-node-entry-guard'
|
|
import packageJson from './package.json' with { type: 'json' }
|
|
import { resolveBuildSourceCommit } from './config/scripts/build-source-commit.mjs'
|
|
|
|
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'
|
|
|
|
// Why: the dark layout-owner path (src/shared/layout-owner-switch.ts) is compiled in only for a
|
|
// build that asks for it; every other build substitutes 'window', so its branches fold away.
|
|
const ORCA_LAYOUT_OWNER_LITERAL = JSON.stringify(
|
|
process.env.ORCA_LAYOUT_OWNER === 'main' ? 'main' : 'window'
|
|
)
|
|
|
|
// Why: official skill installs pin to the commit the build came from, so they match the
|
|
// bundled skills on every channel. Main runs the installer; the renderer prints the command.
|
|
const ORCA_BUILD_COMMIT_LITERAL = JSON.stringify(resolveBuildSourceCommit())
|
|
|
|
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: MAIN_BUILD_ENTRY_POINTS,
|
|
// 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,
|
|
ORCA_BUILD_COMMIT: ORCA_BUILD_COMMIT_LITERAL,
|
|
ORCA_LAYOUT_OWNER: ORCA_LAYOUT_OWNER_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: {
|
|
define: {
|
|
ORCA_BUILD_COMMIT: ORCA_BUILD_COMMIT_LITERAL,
|
|
ORCA_LAYOUT_OWNER: ORCA_LAYOUT_OWNER_LITERAL
|
|
},
|
|
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)
|