UNPKG

pear-electron

Version:

Pear User-Interface Library for Electron

179 lines (166 loc) 7.2 kB
/** @typedef {import('pear-interface')} */ /* global Pear */ 'use strict' const fs = require('bare-fs') const os = require('bare-os') const tty = require('bare-tty') const path = require('bare-path') const Pipe = require('bare-pipe') const { spawn } = require('bare-subprocess') const env = require('bare-env') const { command } = require('paparam') const { isLinux, isWindows, isMac } = require('which-runtime') const { pathToFileURL } = require('url-file-url') const constants = require('pear-api/constants') const parseLink = require('pear-api/parse-link') const Logger = require('pear-api/logger') const { ERR_INVALID_INPUT, ERR_INVALID_APPLING } = require('pear-api/errors') const { ansi, byteSize } = require('pear-api/terminal') const run = require('pear-api/cmd/run') const pear = require('pear-api/cmd') const bootstrap = require('./bootstrap') const EXEC = isWindows ? 'pear-runtime-app\\Pear Runtime.exe' : isLinux ? 'pear-runtime-app/pear-runtime' : 'Pear Runtime.app/Contents/MacOS/Pear Runtime' class PearElectron { constructor () { this.stderr = null this.ipc = Pear[Pear.constructor.IPC] this.arch = '/node_modules/pear-electron/by-arch/' + require.addon.host this.prebuilds = '/node_modules/pear-electron/prebuilds/' + require.addon.host this.boot = '/node_modules/pear-electron/boot.bundle' this.applink = new URL(Pear.config.applink) this.LOG = new Logger({ labels: ['runtime-bootstrap'] }) Pear.teardown(() => this.ipc.close()) } #outs () { if (this.LOG.INF === false) { return { stats ({ upload, download, peers }) { const dl = download.total + download.speed === 0 ? '' : `[${ansi.down} ${byteSize(download.total)} - ${byteSize(download.speed)}/s ] ` const ul = upload.total + upload.speed === 0 ? '' : `[${ansi.up} ${byteSize(upload.total)} - ${byteSize(upload.speed)}/s ] ` return { output: 'status', message: `Bootstrapping Runtime [ Peers: ${peers} ] ${dl}${ul}` } } } } return { dumping: ({ link, dir, list }) => list > -1 ? '' : { message: ['Bootstrapping runtime from peers', 'from: ' + link, 'into: ' + dir] }, file: ({ key }) => key, complete: () => 'Bootstrap complete', error: (err) => `Bootstrap Failure (code: ${err.code || 'none'}) ${err.stack}` } } async start (opts = {}) { if (this.applink.protocol === 'pear:') { const base = path.join(Pear.config.storage, 'pear-runtimes') await bootstrap({ id: Pear.id, link: Pear.config.applink, only: [this.arch, this.prebuilds, this.boot], dir: base, force: true, log: (msg) => this.LOG.info('runtime-bootstrap', msg) }, this.#outs()) this.bin = path.join(base, 'node_modules', 'pear-electron', 'by-arch', require.addon.host, 'bin', EXEC) } else { this.bin = path.join(this.applink.pathname, 'node_modules', 'pear-electron', 'by-arch', require.addon.host, 'bin', EXEC) } 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 } = parseLink(link) const entry = isWindows ? path.normalize(pathname.slice(1)) : pathname 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 let base = null if (key === null) { try { dir = fs.statSync(entry).isDirectory() ? entry : path.dirname(entry) } catch { /* ignore */ } base = project(dir, pathname, cwd) dir = base.dir if (dir.length > 1 && dir.endsWith('/')) dir = dir.slice(0, -1) if (isPath) { link = pathToFileURL(path.join(dir, base.entrypoint || '/')).pathname } } if (isPath) argv[indices.args.link] = 'file://' + (base.entrypoint || '/') 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') const info = JSON.stringify({ checkout: constants.CHECKOUT, mount: constants.MOUNT, bridge: opts.bridge?.addr ?? undefined, startId: Pear.config.startId, dir }) argv = [require.resolve('./boot.bundle'), '--rti', info, ...argv] const stdio = args.detach ? ['ignore', 'ignore', 'ignore', 'overlapped'] : ['ignore', 'inherit', 'pipe', 'overlapped'] const options = { stdio, cwd, windowsHide: true, ...{ env: { ...env, NODE_PRESERVE_SYMLINKS: 1 } } } const sp = spawn(this.bin, argv, options) if (args.appling) { const { appling } = args const applingApp = isMac ? appling.split('.app')[0] + '.app' : appling try { fs.statSync(applingApp) } catch { throw ERR_INVALID_APPLING('Appling does not exist') } if (isMac) spawn('open', [applingApp, '--args', ...argv], options).unref() else spawn(applingApp, argv, options).unref() } sp.on('exit', (code) => { Pear.exit(code) }) const pipe = sp.stdio[3] if (args.detach) return pipe this.stderr = tty.isTTY(2) ? new tty.WriteStream(2) : new Pipe(2) 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 this.stderr.write(data) } sp.stderr.on('data', onerr) return pipe } } function project (dir, origin, cwd) { try { if (JSON.parse(fs.readFileSync(path.join(dir, 'package.json'))).pear) { return { dir, origin, entrypoint: isWindows ? path.normalize(origin.slice(1)).slice(dir.length) : origin.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 || parent.startsWith(cwd) === false) { const normalizedOrigin = !isWindows ? origin : path.normalize(origin.slice(1)) const cwdIsOrigin = path.relative(cwd, normalizedOrigin).length === 0 const condition = cwdIsOrigin ? `at "${cwd}"` : normalizedOrigin.includes(cwd) ? `from "${normalizedOrigin}" up to "${cwd}"` : `at "${normalizedOrigin}"` throw ERR_INVALID_INPUT(`A valid package.json file with pear field must exist ${condition}`) } return project(parent, origin, cwd) } module.exports = PearElectron