pnpm run build ran every step in one serial chain, including the two noEmit aggregate programs (tests, scripts, benchmarks, website) that no artifact step consumes, so the Host aggregate check alone delayed tsdown by about 66 s. - build:src (scripts/build.ts --scope src) builds every artifact in the existing order and writes the client build record; it replaces the removed --artifacts-only flag and the libraries CLI mode. - build:test (--scope test) type-checks only the Host and Client aggregate programs with tsc -p, concurrently, without compiling references or checking prerequisites. - build runs both as one dependency graph through run-gates' new runAggregate: each aggregate check starts once its face's packages are compiled, and Client compilation stays after the Host bundle that writes the Typert /remote declarations. DSH_GATE_CONCURRENCY=1 serializes it; DSH_GATE_FAIL_FAST=1 stops the remaining steps at the first failure. Passing steps print one PASS line (DSH_GATE_VERBOSE=1 shows their output). - PR artifact builds (e2e, release, preview, Node 22 compatibility) and the Python builder (--build-src) use build:src; dev:web and dev:desktop use build:src. test:*, bench, typecheck and the required consumer/Windows lanes keep the complete build. - lint, lint:fix, doc-typecheck and the Typert contracts gate prepare with host-libraries (Host package compilation + Host bundle) instead of build:lib:host; their own checks never read the aggregate root or the Desktop bundle. Parallel and serial build produce byte-identical lib/, Web dist and client build record; a test-only type error fails build and build:test while build:src passes. Refs #5852
131 lines
5.8 KiB
TypeScript
131 lines
5.8 KiB
TypeScript
/** Build and launch the unpackaged Electron shell against the current workspace. */
|
|
|
|
import { spawn, execFileSync } from 'node:child_process'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { join, resolve } from 'node:path'
|
|
import { parseArgs } from 'node:util'
|
|
import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts'
|
|
import type { DesktopRelease } from '../src/release.ts'
|
|
import { developmentRuntimeDirectory, resolveDesktopBuildTarget } from './desktop-build-paths.mjs'
|
|
import { prepareDevelopmentProject } from './development-project.ts'
|
|
import { prepareDevelopmentApp } from './development-app.ts'
|
|
import { preparePrimaryRuntime } from './prepare-primary-runtime.ts'
|
|
|
|
const APP_ROOT = resolve(import.meta.dirname, '..')
|
|
const REPOSITORY_ROOT = resolve(APP_ROOT, '..', '..')
|
|
const BUILD_ROOT = join(APP_ROOT, '.desktop-build')
|
|
const DEVELOPMENT_ROOT = join(BUILD_ROOT, 'development')
|
|
|
|
interface PackageManifest {
|
|
readonly version?: string
|
|
}
|
|
|
|
function packageVersion(path: string, subject: string): string {
|
|
const manifest = JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
|
|
if (typeof manifest.version !== 'string') throw new Error(`desktop development: ${subject} has no version`)
|
|
return manifest.version
|
|
}
|
|
|
|
function debugPort(name: string, fallback: number): number {
|
|
const value = process.env[name]
|
|
if (value === undefined || value === '') return fallback
|
|
const port = Number(value)
|
|
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
|
|
throw new Error(`desktop development: ${name} must be an integer from 1 through 65535`)
|
|
}
|
|
return port
|
|
}
|
|
|
|
async function run(command: string, args: readonly string[], cwd: string, environment = process.env): Promise<void> {
|
|
await new Promise<void>((resolvePromise, reject) => {
|
|
const child = spawn(command, args, { cwd, env: environment, stdio: 'inherit' })
|
|
child.once('error', reject)
|
|
child.once('exit', (code, signal) => {
|
|
if (code === 0) resolvePromise()
|
|
else reject(new Error(`desktop development: ${args.join(' ')} exited with ${String(code ?? signal)}`))
|
|
})
|
|
})
|
|
}
|
|
|
|
async function runPackageScript(script: string, cwd: string): Promise<void> {
|
|
const packageManager = process.env.npm_execpath
|
|
if (packageManager === undefined || packageManager === '') {
|
|
throw new Error('desktop development: invoke this launcher through pnpm run dev:desktop or start:desktop')
|
|
}
|
|
await run(process.execPath, [packageManager, 'run', script], cwd)
|
|
}
|
|
|
|
async function launchElectron(): Promise<void> {
|
|
const require = createRequire(import.meta.url)
|
|
const electron: unknown = require('electron')
|
|
if (typeof electron !== 'string') throw new Error('desktop development: electron executable is unavailable')
|
|
const mainPort = debugPort('DSH_DESKTOP_MAIN_INSPECT_PORT', 9229)
|
|
const rendererPort = debugPort('DSH_DESKTOP_RENDERER_DEBUG_PORT', 9222)
|
|
const hostPort = debugPort('DSH_DESKTOP_HOST_INSPECT_PORT', 9230)
|
|
const home = resolve(process.env.DSH_HOME ?? join(DEVELOPMENT_ROOT, 'home'))
|
|
const userData = resolve(process.env.DSH_DESKTOP_USER_DATA_DIR ?? join(DEVELOPMENT_ROOT, 'electron-user-data'))
|
|
const environment: NodeJS.ProcessEnv = {
|
|
...process.env,
|
|
DSH_HOME: home,
|
|
DSH_DESKTOP_PRIMARY_RUNTIME_DIR: process.env.DSH_DESKTOP_PRIMARY_RUNTIME_DIR ?? developmentRuntimeDirectory(),
|
|
DSH_DESKTOP_HOST_INSPECT_PORT: String(hostPort),
|
|
DSH_DESKTOP_OPEN_DEVTOOLS: process.env.DSH_DESKTOP_OPEN_DEVTOOLS ?? '1',
|
|
ELECTRON_ENABLE_LOGGING: process.env.ELECTRON_ENABLE_LOGGING ?? '1',
|
|
}
|
|
console.log(`desktop development: DSH_HOME=${home}`)
|
|
console.log(`desktop development: userData=${userData}`)
|
|
console.log(`desktop development: inspectors main=${String(mainPort)}, renderer=${String(rendererPort)}, host=${String(hostPort)}`)
|
|
if (process.platform === 'darwin') {
|
|
const executable = prepareDevelopmentApp({ electron, appRoot: APP_ROOT, directory: DEVELOPMENT_ROOT, home, userData,
|
|
mainPort, rendererPort, hostPort, openDevtools: environment.DSH_DESKTOP_OPEN_DEVTOOLS! })
|
|
await run(executable, [], APP_ROOT, environment)
|
|
return
|
|
}
|
|
await run(electron, [
|
|
`--inspect=127.0.0.1:${String(mainPort)}`,
|
|
`--remote-debugging-port=${String(rendererPort)}`,
|
|
`--user-data-dir=${userData}`,
|
|
APP_ROOT,
|
|
], APP_ROOT, environment)
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const { values } = parseArgs({ options: { 'skip-build': { type: 'boolean', default: false } } })
|
|
if (!values['skip-build']) {
|
|
await runPackageScript('build:src', REPOSITORY_ROOT)
|
|
await runPackageScript('build', APP_ROOT)
|
|
}
|
|
for (const path of [
|
|
join(APP_ROOT, 'lib', 'main.js'),
|
|
join(REPOSITORY_ROOT, 'apps', 'desktop-host', 'lib', 'index.js'),
|
|
]) {
|
|
if (!existsSync(path)) throw new Error(`desktop development: missing built artifact ${path}`)
|
|
}
|
|
const version = packageVersion(join(APP_ROOT, 'package.json'), 'desktop package')
|
|
const pnpmVersion = packageVersion(join(APP_ROOT, 'node_modules', 'pnpm', 'package.json'), 'pnpm package')
|
|
const release: DesktopRelease = {
|
|
schemaVersion: 1,
|
|
version,
|
|
hostProtocolVersion: DESKTOP_HOST_PROTOCOL_VERSION,
|
|
nodeVersion: execFileSync(createRequire(import.meta.url)('electron') as string, ['-p', 'process.versions.node'],
|
|
{ encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } }).trim(),
|
|
pnpmVersion,
|
|
}
|
|
prepareDevelopmentProject({
|
|
projectDir: join(DEVELOPMENT_ROOT, 'project'),
|
|
cliDir: join(REPOSITORY_ROOT, 'apps', 'cli'),
|
|
hostDir: join(REPOSITORY_ROOT, 'apps', 'desktop-host'),
|
|
dependencyDir: join(REPOSITORY_ROOT, 'node_modules', '.pnpm', 'node_modules'),
|
|
release,
|
|
target: resolveDesktopBuildTarget(),
|
|
})
|
|
await preparePrimaryRuntime()
|
|
await launchElectron()
|
|
}
|
|
|
|
await main().catch((error: unknown) => {
|
|
console.error(error instanceof Error ? error.message : error)
|
|
process.exitCode = 1
|
|
})
|