Files
hermes-agent/apps/desktop/scripts/bundle-electron-main.mjs
kshitijk4poor 3fc37481eb refactor(desktop): one receipt reader, one output list, no native double copy on renderer reuse
rendererCurrent and productCurrent share hostReceipt() for the schema/product/host checks. The stamped outputs come from bundleElectronMain's own electronOutputs list, so a renamed or added bundle can never be copied stale from the previous dist. The reuse copy skips node_modules/ and native/, which copyNativeTree recopies from nativeDeps right after, and a --typecheck build never reuses a renderer that skipped tsc.
2026-10-08 15:20:58 -07:00

103 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process'
import { mkdirSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { parseArgs } from 'node:util'
import { isMain, workspaceTool } from '../../../scripts/build/frontend-common.mjs'
import { environmentDefaultsBanner } from './bundle-env.mjs'
const repoRoot = resolve(import.meta.dirname, '../../..')
// Evaluate the one identity module in a fresh process. Its CJS cache and the
// caller's environment must not carry a previous variant into this artifact.
function productIdentity(source, stamp) {
const variant = stamp.updateMechanism === 'microsoft-store' ? 'store'
: stamp.payload === 'bootstrap' ? '' : stamp.payload
if (!['', 'bundled', 'light', 'store'].includes(variant)) {
throw new Error(`Invalid desktop stamp payload: ${stamp.payload}`)
}
return execFileSync(process.execPath, ['-e', 'console.log(JSON.stringify(require(process.argv[1])))',
join(source, 'apps/desktop/product-identity.cjs')], {
env: { ...process.env, HERMES_DESKTOP_VARIANT: variant, HERMES_PAYLOAD_TAG: stamp.tag || '',
// Commit builds stamp source='commit-build'; the display name carries
// the short SHA (see product-identity.cjs). Tagged builds pass ''.
HERMES_BUILD_COMMIT: stamp.source === 'commit-build' ? (stamp.commit || '') : '' },
encoding: 'utf8',
}).trim()
}
/** Every file bundleElectronMain writes: the only outputs that bake the install stamp. */
export const electronOutputs = Object.freeze({
main: 'electron-main.mjs', preload: 'electron-preload.js', previewGuestPreload: 'preview-guest-preload.js',
})
/** Bundle main/preload using only the prepared workspace's compiler. */
export async function bundleElectronMain({ source, out, stamp, dev = false }) {
source = resolve(source)
out = resolve(out)
const { build } = await import(pathToFileURL(workspaceTool(source, 'apps/desktop', 'esbuild')).href)
// Defaults must run before bundled modules resolve paths or onboarding flags.
// Dev bundles leave the environment alone so source-tree resolution keeps working.
const envBanner = dev ? '' : environmentDefaultsBanner(process.env.HERMES_BUNDLE_ENV_JSON || '{}')
const define = {}
let bakedStamp = null
if (!dev) {
if (!stamp) throw new Error('A prepared install stamp is required')
const raw = readFileSync(stamp, 'utf8')
const metadata = JSON.parse(raw)
// The baked bytes, and the clock the caller must record: re-reading the stamp
// after this point would report a restamp the output never saw.
bakedStamp = metadata.builtAt ?? null
define['process.env.HERMES_DESKTOP_IS_PACKAGED'] = JSON.stringify(true)
define.__HERMES_INSTALL_STAMP__ = raw
define.__HERMES_PRODUCT_IDENTITY__ = productIdentity(source, metadata)
}
mkdirSync(out, { recursive: true })
const common = {
absWorkingDir: join(source, 'apps/desktop'),
bundle: true,
platform: 'node',
target: 'node20',
external: ['electron', 'node-pty', 'get-windows', 'fs'],
define,
logLevel: 'info',
}
await build({
...common,
// entry.ts picks the Linux ozone platform (native Wayland/WSLg) and
// relaunches before main.ts loads; Electron reads it only from argv.
entryPoints: [join(source, 'apps/desktop/electron/entry.ts')],
format: 'esm',
outfile: join(out, electronOutputs.main),
banner: { js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);" + envBanner },
})
await build({
...common,
entryPoints: [join(source, 'apps/desktop/electron/preload.ts')],
format: 'cjs',
outfile: join(out, electronOutputs.preload),
})
// Preview-pane <webview> guest preload; main.ts hands this path to the
// preview webview via will-attach-webview.
await build({
...common,
entryPoints: [join(source, 'apps/desktop/electron/preview-guest-preload-entry.ts')],
format: 'cjs',
outfile: join(out, electronOutputs.previewGuestPreload),
})
return { stampClock: bakedStamp }
}
if (isMain(import.meta.url)) {
const { values } = parseArgs({ options: {
source: { type: 'string', default: repoRoot },
out: { type: 'string' }, stamp: { type: 'string' }, dev: { type: 'boolean' },
} })
await bundleElectronMain({
...values,
out: values.out || join(values.source, 'apps/desktop/dist'),
stamp: values.stamp || join(values.source, 'apps/desktop/build/install-stamp.json'),
})
}