dev.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. /** Build and launch the unpackaged Electron shell against the current workspace. */
  2. import { spawn, execFileSync } from 'node:child_process'
  3. import { existsSync, readFileSync } from 'node:fs'
  4. import { createRequire } from 'node:module'
  5. import { join, resolve } from 'node:path'
  6. import { parseArgs } from 'node:util'
  7. import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts'
  8. import type { DesktopRelease } from '../src/release.ts'
  9. import { prepareDevelopmentProject } from './development-project.ts'
  10. import { preparePrimaryRuntime } from './prepare-primary-runtime.ts'
  11. const APP_ROOT = resolve(import.meta.dirname, '..')
  12. const REPOSITORY_ROOT = resolve(APP_ROOT, '..', '..')
  13. const BUILD_ROOT = join(APP_ROOT, '.desktop-build')
  14. const DEVELOPMENT_ROOT = join(BUILD_ROOT, 'development')
  15. interface PackageManifest {
  16. readonly version?: string
  17. }
  18. function packageVersion(path: string, subject: string): string {
  19. const manifest = JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
  20. if (typeof manifest.version !== 'string') throw new Error(`desktop development: ${subject} has no version`)
  21. return manifest.version
  22. }
  23. function debugPort(name: string, fallback: number): number {
  24. const value = process.env[name]
  25. if (value === undefined || value === '') return fallback
  26. const port = Number(value)
  27. if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
  28. throw new Error(`desktop development: ${name} must be an integer from 1 through 65535`)
  29. }
  30. return port
  31. }
  32. async function run(command: string, args: readonly string[], cwd: string, environment = process.env): Promise<void> {
  33. await new Promise<void>((resolvePromise, reject) => {
  34. const child = spawn(command, args, { cwd, env: environment, stdio: 'inherit' })
  35. child.once('error', reject)
  36. child.once('exit', (code, signal) => {
  37. if (code === 0) resolvePromise()
  38. else reject(new Error(`desktop development: ${args.join(' ')} exited with ${String(code ?? signal)}`))
  39. })
  40. })
  41. }
  42. async function runPackageScript(script: string, cwd: string): Promise<void> {
  43. const packageManager = process.env.npm_execpath
  44. if (packageManager === undefined || packageManager === '') {
  45. throw new Error('desktop development: invoke this launcher through pnpm run dev:desktop or start:desktop')
  46. }
  47. await run(process.execPath, [packageManager, 'run', script], cwd)
  48. }
  49. async function launchElectron(): Promise<void> {
  50. const require = createRequire(import.meta.url)
  51. const electron: unknown = require('electron')
  52. if (typeof electron !== 'string') throw new Error('desktop development: electron executable is unavailable')
  53. const mainPort = debugPort('DSH_DESKTOP_MAIN_INSPECT_PORT', 9229)
  54. const rendererPort = debugPort('DSH_DESKTOP_RENDERER_DEBUG_PORT', 9222)
  55. const hostPort = debugPort('DSH_DESKTOP_HOST_INSPECT_PORT', 9230)
  56. const home = resolve(process.env.DSH_HOME ?? join(DEVELOPMENT_ROOT, 'home'))
  57. const userData = join(DEVELOPMENT_ROOT, 'electron-user-data')
  58. const environment: NodeJS.ProcessEnv = {
  59. ...process.env,
  60. DSH_HOME: home,
  61. DSH_DESKTOP_HOST_INSPECT_PORT: String(hostPort),
  62. DSH_DESKTOP_OPEN_DEVTOOLS: process.env.DSH_DESKTOP_OPEN_DEVTOOLS ?? '1',
  63. ELECTRON_ENABLE_LOGGING: process.env.ELECTRON_ENABLE_LOGGING ?? '1',
  64. }
  65. console.log(`desktop development: DSH_HOME=${home}`)
  66. console.log(`desktop development: inspectors main=${String(mainPort)}, renderer=${String(rendererPort)}, host=${String(hostPort)}`)
  67. await run(electron, [
  68. `--inspect=127.0.0.1:${String(mainPort)}`,
  69. `--remote-debugging-port=${String(rendererPort)}`,
  70. `--user-data-dir=${userData}`,
  71. APP_ROOT,
  72. ], APP_ROOT, environment)
  73. }
  74. async function main(): Promise<void> {
  75. const { values } = parseArgs({ options: { 'skip-build': { type: 'boolean', default: false } } })
  76. if (!values['skip-build']) {
  77. await runPackageScript('build', REPOSITORY_ROOT)
  78. await runPackageScript('build', APP_ROOT)
  79. }
  80. for (const path of [
  81. join(APP_ROOT, 'lib', 'main.js'),
  82. join(REPOSITORY_ROOT, 'apps', 'desktop-host', 'lib', 'index.js'),
  83. ]) {
  84. if (!existsSync(path)) throw new Error(`desktop development: missing built artifact ${path}`)
  85. }
  86. const version = packageVersion(join(APP_ROOT, 'package.json'), 'desktop package')
  87. const pnpmVersion = packageVersion(join(APP_ROOT, 'node_modules', 'pnpm', 'package.json'), 'pnpm package')
  88. const release: DesktopRelease = {
  89. schemaVersion: 1,
  90. version,
  91. hostProtocolVersion: DESKTOP_HOST_PROTOCOL_VERSION,
  92. nodeVersion: execFileSync(createRequire(import.meta.url)('electron') as string, ['-p', 'process.versions.node'],
  93. { encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } }).trim(),
  94. pnpmVersion,
  95. }
  96. prepareDevelopmentProject({
  97. projectDir: join(DEVELOPMENT_ROOT, 'project'),
  98. cliDir: join(REPOSITORY_ROOT, 'apps', 'cli'),
  99. hostDir: join(REPOSITORY_ROOT, 'apps', 'desktop-host'),
  100. dependencyDir: join(REPOSITORY_ROOT, 'node_modules', '.pnpm', 'node_modules'),
  101. release,
  102. })
  103. await preparePrimaryRuntime()
  104. await launchElectron()
  105. }
  106. await main().catch((error: unknown) => {
  107. console.error(error instanceof Error ? error.message : error)
  108. process.exitCode = 1
  109. })