mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
361 lines
14 KiB
TypeScript
361 lines
14 KiB
TypeScript
import { markdownParserAliases } from './config/build-plugins/markdown-parser-exports'
|
|
import { createRequire } from 'node:module'
|
|
import { resolve } from 'node:path'
|
|
import { defineConfig, type UserConfig } from 'electron-vite'
|
|
import type { Plugin } from '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 { createViteCacheRetentionPlugin } from './config/build-plugins/vite-cache-retention'
|
|
import { MAIN_BUILD_ENTRY_POINTS } from './config/build-plugins/main-build-entry-points'
|
|
import { createPlainNodeEntryGuardPlugin } from './config/build-plugins/plain-node-entry-guard'
|
|
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',
|
|
'fflate',
|
|
// Why: Windows NSIS deploys app.asar before external resources; bootstrap must
|
|
// not race the later resources/node_modules copy.
|
|
'zod'
|
|
])
|
|
// 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'
|
|
|
|
const darkBuildSwitches: {
|
|
darkBuildDefines: () => Record<string, string>
|
|
darkBuildMarkers: () => { fileName: string; text: string }[]
|
|
} = createRequire(import.meta.url)('./config/dark-build-switches.cjs')
|
|
const DARK_BUILD_DEFINES = darkBuildSwitches.darkBuildDefines()
|
|
|
|
/** Marks a dark switched-on main build so packaging refuses it. */
|
|
function createDarkBuildMarkerPlugin(): Plugin {
|
|
return {
|
|
name: 'orca-dark-build-marker',
|
|
generateBundle() {
|
|
for (const marker of darkBuildSwitches.darkBuildMarkers()) {
|
|
this.emitFile({ type: 'asset', fileName: marker.fileName, source: marker.text })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 diagnosticFileDescriptor
|
|
let closeSync, openSync, 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
|
|
}
|
|
if (!mainChunk.moduleIds.some((id) => resolve(id) === MAIN_BUILD_ENTRY_POINTS.index)) {
|
|
throw new Error('Main startup must stay in index.js so __dirname resolves app assets')
|
|
}
|
|
|
|
// 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',
|
|
// PR fixtures discard maps; official releases must always retain them.
|
|
...(process.env.ORCA_CI_MAIN_SOURCEMAPS === '0' &&
|
|
orcaBuildIdentity !== 'stable' &&
|
|
orcaBuildIdentity !== 'rc'
|
|
? { sourcemap: false }
|
|
: {}),
|
|
// 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: {
|
|
input: MAIN_BUILD_ENTRY_POINTS,
|
|
// CLI builds replace exported entries; keep their internals separate without moving startup paths.
|
|
preserveEntrySignatures: 'exports-only',
|
|
// 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(),
|
|
createDarkBuildMarkerPlugin()
|
|
]
|
|
}
|
|
},
|
|
// 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,
|
|
...DARK_BUILD_DEFINES
|
|
},
|
|
// 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: {
|
|
// Shared node_modules installs must not share dependency optimization state across worktrees.
|
|
cacheDir: resolve('.cache/vite/renderer'),
|
|
define: {
|
|
ORCA_BUILD_COMMIT: ORCA_BUILD_COMMIT_LITERAL,
|
|
...DARK_BUILD_DEFINES
|
|
},
|
|
resolve: {
|
|
alias: {
|
|
...markdownParserAliases,
|
|
'@renderer': resolve('src/renderer/src'),
|
|
'@': resolve('src/renderer/src')
|
|
}
|
|
},
|
|
plugins: [
|
|
react(),
|
|
tailwindcss(),
|
|
createPdfjsViewerAssetsPlugin(),
|
|
createViteCacheRetentionPlugin()
|
|
],
|
|
worker: {
|
|
format: 'es'
|
|
},
|
|
build: {
|
|
manifest: true,
|
|
reportCompressedSize: false,
|
|
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'),
|
|
space: resolve('src/renderer/space.html'),
|
|
web: resolve('src/renderer/web-index.html')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default defineConfig(({ command }) => {
|
|
if (command !== 'serve') {
|
|
return electronViteConfig
|
|
}
|
|
// Electron Vite also uses Vite's build command for the development main process.
|
|
return {
|
|
...electronViteConfig,
|
|
main: {
|
|
...electronViteConfig.main,
|
|
build: {
|
|
...electronViteConfig.main?.build,
|
|
minify: false,
|
|
sourcemap:
|
|
process.env.ORCA_DEV_MAIN_SOURCEMAPS === '0'
|
|
? false
|
|
: electronViteConfig.main?.build?.sourcemap
|
|
}
|
|
}
|
|
}
|
|
})
|