CI
This commit is contained in:
+74
-65
@@ -1,12 +1,23 @@
|
||||
/**
|
||||
* Pear-side Holesail client using the **real `holesail` package**.
|
||||
* Local Holesail client helper for the peardock UI.
|
||||
*
|
||||
* Architecture (Holepunch pear-electron):
|
||||
* - Bare main (`index.js`) runs `holesailBareControl.cjs` with real `require('holesail')`
|
||||
* (require.addon works there). Exposes http://127.0.0.1:<port> + token in app storage.
|
||||
* - UI renderer never loads bare-tcp; it POSTs to that control API.
|
||||
* - Node (tests / non-Pear): in-process `require('holesail')`.
|
||||
* Architecture:
|
||||
* - Electron/Pear main runs `holesailBareControl.cjs` (real `require('holesail')`)
|
||||
* and writes http://127.0.0.1:<port> + token into app storage.
|
||||
* - UI talks to that control API over fetch — never loads bare-tcp in the renderer.
|
||||
* - Node tests: optional in-process `require('holesail')`.
|
||||
*
|
||||
* Electron packaged client: NEVER use dynamic `import('fs')` / `import('path')`
|
||||
* etc. inside the esbuild CJS GUI bundle. Node ESM `import()` in the renderer
|
||||
* crashes Chromium (exit 5 → grey frozen window). Static imports become
|
||||
* `require()` in the bundle and work with nodeIntegration.
|
||||
*/
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import net from 'net'
|
||||
import { createRequire } from 'module'
|
||||
import { exec } from 'child_process'
|
||||
|
||||
/** @type {Map<string, { info: object, localPort: number, host: string, url: string, via?: string }>} */
|
||||
const localClients = new Map()
|
||||
|
||||
@@ -18,32 +29,52 @@ let holesailLoadError = null
|
||||
let controlCache = null
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
* Lazy-load npm packages without static-importing natives into every UI load.
|
||||
* Prefer ambient require (Electron CJS bundle); fall back to createRequire (pear ESM).
|
||||
* @param {string} id
|
||||
*/
|
||||
function isPearGui() {
|
||||
return Boolean(globalThis.Pear?.config)
|
||||
function nodeRequire(id) {
|
||||
try {
|
||||
if (typeof require === 'function') return require(id)
|
||||
} catch {
|
||||
// not resolvable
|
||||
}
|
||||
try {
|
||||
const metaUrl =
|
||||
typeof import.meta !== 'undefined' &&
|
||||
import.meta &&
|
||||
typeof import.meta.url === 'string' &&
|
||||
import.meta.url.startsWith('file:')
|
||||
? import.meta.url
|
||||
: null
|
||||
if (metaUrl) return createRequire(metaUrl)(id)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return createRequire(path.join(process.cwd(), 'package.json'))(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Bare control endpoint written by index.js.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPearGui() {
|
||||
// pear run + Electron peardock-client both set Pear.config.storage
|
||||
return Boolean(globalThis.Pear?.config?.storage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve control endpoint written by main process.
|
||||
* @returns {Promise<{ baseUrl: string, token: string } | null>}
|
||||
*/
|
||||
async function getControlEndpoint() {
|
||||
if (controlCache) return controlCache
|
||||
|
||||
const Pear = globalThis.Pear
|
||||
const storage = Pear?.config?.storage
|
||||
const storage = globalThis.Pear?.config?.storage
|
||||
if (!storage) return null
|
||||
|
||||
try {
|
||||
const fs = await import('fs')
|
||||
const path = await import('path')
|
||||
const join = path.join || path.default?.join
|
||||
const readFileSync = fs.readFileSync || fs.default?.readFileSync
|
||||
if (typeof join !== 'function' || typeof readFileSync !== 'function') return null
|
||||
|
||||
const p = join(storage, STATE_FILE)
|
||||
const raw = readFileSync(p, 'utf8')
|
||||
const p = path.join(storage, STATE_FILE)
|
||||
const raw = fs.readFileSync(p, 'utf8')
|
||||
const meta = JSON.parse(raw)
|
||||
if (!meta?.baseUrl || !meta?.token) return null
|
||||
controlCache = { baseUrl: meta.baseUrl, token: meta.token }
|
||||
@@ -54,7 +85,7 @@ async function getControlEndpoint() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll storage briefly — Bare main may still be writing the state file.
|
||||
* Poll storage briefly — main may still be writing the state file.
|
||||
* @param {number} [ms]
|
||||
*/
|
||||
async function waitForControlEndpoint(ms = 8000) {
|
||||
@@ -62,7 +93,6 @@ async function waitForControlEndpoint(ms = 8000) {
|
||||
while (Date.now() - start < ms) {
|
||||
const ep = await getControlEndpoint()
|
||||
if (ep) {
|
||||
// Verify health
|
||||
try {
|
||||
const res = await fetch(`${ep.baseUrl}/health`, { method: 'GET' })
|
||||
if (res.ok) return ep
|
||||
@@ -85,7 +115,7 @@ async function controlRpc(method, body, timeoutMs = 55000) {
|
||||
const ep = await waitForControlEndpoint()
|
||||
if (!ep) {
|
||||
throw new Error(
|
||||
'Holesail local control service not found. Restart peardock (Bare main starts the control API).'
|
||||
'Holesail local control service not found. Restart peardock so the main process can start the control API.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,35 +149,19 @@ async function controlRpc(method, body, timeoutMs = 55000) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the real holesail constructor in-process (Node / Bare with require.addon).
|
||||
* Load the real holesail constructor in-process (Node tests only).
|
||||
* @returns {Promise<Function>}
|
||||
*/
|
||||
export async function loadHolesailCtor() {
|
||||
if (HolesailCtor) return HolesailCtor
|
||||
if (holesailLoadError) throw holesailLoadError
|
||||
try {
|
||||
try {
|
||||
const { createRequire } = await import('module')
|
||||
const require = createRequire(import.meta.url)
|
||||
const Ctor = require('holesail')
|
||||
if (typeof Ctor === 'function') {
|
||||
HolesailCtor = Ctor
|
||||
return HolesailCtor
|
||||
}
|
||||
} catch {
|
||||
// createRequire missing
|
||||
}
|
||||
const mod = await import('holesail')
|
||||
const Ctor =
|
||||
(typeof mod === 'function' && mod) ||
|
||||
mod?.default ||
|
||||
mod?.Holesail ||
|
||||
(mod?.default && mod.default.default) ||
|
||||
null
|
||||
if (typeof Ctor !== 'function') {
|
||||
const Ctor = nodeRequire('holesail')
|
||||
const Fn = typeof Ctor === 'function' ? Ctor : Ctor?.default
|
||||
if (typeof Fn !== 'function') {
|
||||
throw new Error('holesail package loaded but no constructor export was found')
|
||||
}
|
||||
HolesailCtor = Ctor
|
||||
HolesailCtor = Fn
|
||||
return HolesailCtor
|
||||
} catch (err) {
|
||||
holesailLoadError = err
|
||||
@@ -160,16 +174,13 @@ export async function loadHolesailCtor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a free TCP port on 127.0.0.1 (Node / in-process path only).
|
||||
* Pick a free TCP port on 127.0.0.1 (in-process path only).
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
export async function findFreePort() {
|
||||
try {
|
||||
const net = await import('net')
|
||||
const createServer = net.createServer || net.default?.createServer
|
||||
if (typeof createServer !== 'function') throw new Error('net.createServer missing')
|
||||
return await new Promise((resolve, reject) => {
|
||||
const s = createServer()
|
||||
const s = net.createServer()
|
||||
s.listen(0, '127.0.0.1', () => {
|
||||
const addr = s.address()
|
||||
const port = typeof addr === 'object' && addr ? addr.port : 0
|
||||
@@ -183,7 +194,7 @@ export async function findFreePort() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to an hs:// URL and listen locally (real holesail).
|
||||
* Connect to an hs:// URL and listen locally.
|
||||
* @param {string} urlOrKey
|
||||
* @param {{ localPort?: number, host?: string, openBrowser?: boolean }} [opts]
|
||||
*/
|
||||
@@ -195,7 +206,7 @@ export async function connectLocalHolesail(urlOrKey, opts = {}) {
|
||||
return localClients.get(url)
|
||||
}
|
||||
|
||||
// Pear GUI → Bare main control HTTP (real holesail there)
|
||||
// Pear GUI / Electron client → main control HTTP
|
||||
if (isPearGui()) {
|
||||
const result = await controlRpc('/connect', {
|
||||
url,
|
||||
@@ -298,20 +309,18 @@ function tryOpenBrowser(href) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
import('child_process')
|
||||
.then((cp) => {
|
||||
const exec = cp.exec || cp.default?.exec
|
||||
if (typeof exec !== 'function') return
|
||||
const platform = globalThis.process?.platform || ''
|
||||
const cmd =
|
||||
platform === 'darwin'
|
||||
? `open "${href}"`
|
||||
: platform === 'win32'
|
||||
? `start "" "${href}"`
|
||||
: `xdg-open "${href}"`
|
||||
exec(cmd)
|
||||
})
|
||||
.catch(() => {})
|
||||
try {
|
||||
const platform = globalThis.process?.platform || ''
|
||||
const cmd =
|
||||
platform === 'darwin'
|
||||
? `open "${href}"`
|
||||
: platform === 'win32'
|
||||
? `start "" "${href}"`
|
||||
: `xdg-open "${href}"`
|
||||
exec(cmd)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
Reference in New Issue
Block a user