pear-electron
Version:
Pear User-Interface Library for Electron
173 lines (154 loc) • 6.58 kB
JavaScript
/** @typedef {import('pear-interface')} */ /* global Pear */
'use strict'
const fs = require('bare-fs')
const os = require('bare-os')
const path = require('bare-path')
const { spawn } = require('bare-subprocess')
const env = require('bare-env')
const { command } = require('paparam')
const { isLinux, isWindows, isMac, arch } = require('which-runtime')
const { pathToFileURL } = require('url-file-url')
const constants = require('pear-constants')
const plink = require('pear-link')
const Logger = require('pear-logger')
const { stdio } = require('pear-terminal')
const { ERR_INVALID_APPLING, ERR_INVALID_PROJECT_DIR, ERR_INVALID_CONFIG } = require('pear-errors')
// cutover stops replaying & relaying subscriber streams between clients
// set to false to stop run flow from auto cutover, so we can cutover at end of ui init
Pear.constructor.CUTOVER = false
const pear = require('pear-cmd')
const run = require('pear-cmd/run')
const pkg = require('./package.json')
const bin = (name) => {
const kebab = name.toLowerCase().split(' ').join('-')
const cased = kebab.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ')
const app = isMac ? cased + '.app' : kebab + '-app'
const exe = isWindows ? cased + '.exe' : (isMac ? 'Contents/MacOS/' + cased : kebab)
return isWindows ? 'bin\\' + app + '\\' + exe : (isMac ? 'bin/' + app + '/' + exe : 'bin/' + app + '/' + exe)
}
class PearElectron {
constructor () {
if (!Pear.config.assets.ui?.path) {
const info =
Pear.config.options.pre
? { assets: Pear.config.assets }
: { assets: Pear.config.assets, hint: 'set pre: pear-electron/pre to autoset assets.ui' }
throw new ERR_INVALID_CONFIG('pear.assets.ui must be defined for project', info)
}
if (!Pear.config.assets.ui?.name) {
throw new ERR_INVALID_CONFIG('pear.assets.ui.name must be defined for project', { assets: Pear.config.assets })
}
this.ipc = Pear[Pear.constructor.IPC]
this.applink = new URL(Pear.config.applink)
this.LOG = new Logger({ labels: [pkg.name] })
Pear.teardown(() => this.ipc.close())
}
async start (opts = {}) {
this.bin = path.join(Pear.config.assets.ui.path, 'by-arch', require.addon.host, bin(Pear.config.assets.ui.name))
const parsed = pear(Pear.argv.slice(1))
const cmd = command('run', ...run)
let argv = parsed.rest
const { args, indices } = cmd.parse(argv)
let link = Pear.config.link
const { drive, pathname, hash, search } = plink.parse(link)
const { key } = drive
const isPear = link.startsWith('pear://')
const isFile = link.startsWith('file://')
const isPath = isPear === false && isFile === false
const cwd = os.cwd()
let dir = cwd
if (key === null) {
const initial = normalize(pathname)
const base = project(initial, initial)
dir = base.dir
if (dir.length > 1 && dir.endsWith('/')) dir = dir.slice(0, -1)
if (isPath) {
link = pathToFileURL(path.join(dir, base.entrypoint || '/')) + search + hash
argv[indices.args.link] = link
}
}
argv[indices.args.link] = argv[indices.args.link].replace('://', '_||') // for Windows
if ((isLinux || isWindows) && indices.flags.sandbox === undefined) argv.splice(indices.args.link, 0, '--no-sandbox')
if (isLinux && arch === 'x64' && this.ipc) {
const assets = this.ipc.data({ resource: 'assets', link: pkg.pear.assets.ui.link })
assets.on('data', ({ tag, data }) => {
if (tag !== 'assets') return
const bundle = path.join(data[0].path, 'boot.bundle')
const current = fs.readFileSync(bundle)
const patch = Buffer.from(require('./boot.bundle.patch.js'), 'base64')
if (current.equals(patch) === false) fs.writeFileSync(bundle, patch)
})
}
const info = JSON.stringify({
checkout: constants.CHECKOUT,
mount: constants.MOUNT,
bridge: opts.bridge?.addr ?? undefined,
startId: Pear.config.startId,
dir
})
argv = ['run', '--rti', info, ...argv]
const options = {
stdio: args.detach
? ['ignore', 'ignore', 'ignore', 'overlapped']
: ['ignore', 'pipe', 'pipe', 'overlapped'],
cwd,
windowsHide: true,
...{ env: { ...env, NODE_PRESERVE_SYMLINKS: 1 } }
}
let sp = null
if (args.appling) {
this.LOG.info('Spawning UI (appling)')
const { appling } = args
const applingApp = isMac ? appling.split('.app')[0] + '.app' : appling
if (fs.existsSync(applingApp) === false) throw ERR_INVALID_APPLING('Appling does not exist')
if (isMac) sp = spawn('open', [applingApp, '--args', ...argv], options)
else sp = spawn(applingApp, argv, options)
} else {
this.LOG.info('Spawning UI (asset)')
sp = spawn(this.bin, argv, options)
}
sp.on('exit', (code) => {
this.LOG.info('UI exited with code', code)
Pear.exitCode = code
if (!pipe.destroyed) pipe.destroy()
})
const pipe = sp.stdio[3]
if (args.detach) return pipe
const onerr = (data) => {
const str = data.toString()
const ignore = str.indexOf('DevTools listening on ws://') > -1 ||
str.indexOf('NSApplicationDelegate.applicationSupportsSecureRestorableState') > -1 ||
str.indexOf('", source: devtools://devtools/') > -1 ||
str.indexOf('sysctlbyname for kern.hv_vmm_present failed with status -1') > -1 ||
str.indexOf('dev.i915.perf_stream_paranoid=0') > -1 ||
str.indexOf('libva error: vaGetDriverNameByIndex() failed') > -1 ||
str.indexOf('GetVSyncParametersIfAvailable() failed') > -1 ||
str.indexOf('Unsupported pixel format: -1') > -1 ||
(str.indexOf(':ERROR:') > -1 && /:ERROR:.+cache/.test(str))
if (ignore) return
fs.writeSync(2, data)
}
sp.stderr.on('data', onerr)
sp.stdout.pipe(stdio.out)
return pipe
}
}
function project (dir, initial) {
try {
if (JSON.parse(fs.readFileSync(path.join(dir, 'package.json'))).pear) {
return { dir, entrypoint: initial.slice(dir.length) }
}
} catch (err) {
if (err.code !== 'ENOENT' && err.code !== 'EISDIR' && err.code !== 'ENOTDIR') throw err
}
const parent = path.dirname(dir)
if (parent === dir) {
throw ERR_INVALID_PROJECT_DIR(`A valid package.json file with pear field must exist (checked from "${initial}" to "${dir}")`)
}
return project(parent, initial)
}
function normalize (pathname) {
if (isWindows) return path.normalize(pathname.slice(1))
return pathname
}
module.exports = PearElectron