UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

576 lines 20.7 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var server_exports = {}; __export(server_exports, { clearAllMappings: () => clearAllMappings, clearMappingsForSimulator: () => clearMappingsForSimulator, getSimulatorMappings: () => getSimulatorMappings, setPendingMapping: () => setPendingMapping, setRouteMode: () => setRouteMode, setSimulatorMapping: () => setSimulatorMapping, startDaemon: () => startDaemon }); module.exports = __toCommonJS(server_exports); var http = __toESM(require("node:http"), 1); var fs = __toESM(require("node:fs"), 1); var os = __toESM(require("node:os"), 1); var path = __toESM(require("node:path"), 1); var import_registry = require("./registry.cjs"); var import_ipc = require("./ipc.cjs"); var import_proxy = require("./proxy.cjs"); var import_picker = require("./picker.cjs"); var import_picocolors = __toESM(require("picocolors"), 1); const debugLogPath = path.join(os.homedir(), ".one", "daemon-debug.log"); function debugLog(msg) { fs.appendFileSync(debugLogPath, `${(/* @__PURE__ */new Date()).toISOString()} ${msg} `); } const serverAppNames = /* @__PURE__ */new Map(); async function getAppNameForServer(root) { const cached = serverAppNames.get(root); if (cached) return cached; try { const appJsonPath = path.join(root, "app.json"); if (fs.existsSync(appJsonPath)) { const content = JSON.parse(fs.readFileSync(appJsonPath, "utf-8")); const name = content.expo?.name || content.name; if (name) { serverAppNames.set(root, name); return name; } } for (const ext of ["ts", "js"]) { const configPath = path.join(root, `app.config.${ext}`); if (fs.existsSync(configPath)) { const content = fs.readFileSync(configPath, "utf-8"); const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/); if (nameMatch) { const name = nameMatch[1]; serverAppNames.set(root, name); return name; } } } const dirName = path.basename(root); if (dirName) { serverAppNames.set(root, dirName); return dirName; } } catch (err) { debugLog(`Failed to get app name for ${root}: ${err}`); } return null; } const clientMappings = /* @__PURE__ */new Map(); const MAPPING_TTL_MS = 36e5; setInterval(() => { const now = Date.now(); let cleaned = 0; for (const [key, info] of clientMappings) { if (now - info.lastUsed > MAPPING_TTL_MS) { clientMappings.delete(key); cleaned++; } } if (cleaned > 0) { debugLog(`Cleaned ${cleaned} stale mappings`); } }, 3e4); function getRouteIdFromCookies(req) { const cookieHeader = req.headers.cookie; if (!cookieHeader) return null; const match = cookieHeader.match(/one-route-id=([^;]+)/); return match ? match[1] : null; } const pendingMappings = /* @__PURE__ */new Map(); function setPendingMapping(serverId, simulatorUdid) { pendingMappings.set(serverId, simulatorUdid); debugLog(`Pending mapping: next request to server ${serverId} will map to sim ${simulatorUdid}`); } function clearMappingsForSimulator(simulatorUdid) { let count = 0; for (const [key, info] of clientMappings) { if (info.simulatorUdid === simulatorUdid) { clientMappings.delete(key); count++; } } debugLog(`Cleared ${count} mappings for simulator ${simulatorUdid}`); } function clearAllMappings() { const count = clientMappings.size; clientMappings.clear(); debugLog(`Cleared all ${count} client mappings`); } function getSimulatorMappings() { const result = /* @__PURE__ */new Map(); for (const [_key, info] of clientMappings) { if (info.simulatorUdid) { result.set(info.simulatorUdid, info.serverId); } } return result; } function setSimulatorMapping(simulatorUdid, serverId) { const key = `tui:${simulatorUdid}`; clientMappings.set(key, { serverId, simulatorUdid, matchedBy: "tui", lastUsed: Date.now() }); debugLog(`TUI set mapping: sim=${simulatorUdid} -> server=${serverId}`); } async function matchUserAgentToServer(headers, servers) { const userAgent = headers["user-agent"]; if (!userAgent || typeof userAgent !== "string") return null; const uaAppName = userAgent.split("/")[0]; if (!uaAppName) return null; debugLog(`Trying to match user-agent app "${uaAppName}" to servers`); for (const server of servers) { const appName = await getAppNameForServer(server.root); if (!appName) continue; const normalizedUa = uaAppName.toLowerCase().replace(/\s+/g, ""); const normalizedApp = appName.toLowerCase().replace(/\s+/g, ""); debugLog(` Comparing "${normalizedUa}" to server app "${normalizedApp}"`); if (normalizedUa === normalizedApp || normalizedUa.includes(normalizedApp) || normalizedApp.includes(normalizedUa)) { debugLog(` Matched! ${uaAppName} -> ${server.root}`); return server; } } return null; } function isGenericExpoAgent(ua) { return ua.startsWith("Expo/") || ua.startsWith("Exponent/"); } function extractAppNameFromUA(ua) { const firstPart = ua.split(" ")[0] || ""; const appName = firstPart.split("/")[0]; return appName || null; } const recentConnections = /* @__PURE__ */new Map(); const CONNECTION_MEMORY_MS = 5e3; function getPrimaryIdentifier(headers) { const userAgent = headers["user-agent"] || ""; if (!isGenericExpoAgent(userAgent)) { const appName = extractAppNameFromUA(userAgent); if (appName) { return `app:${appName}`; } } const easClientId = headers["eas-client-id"]; if (easClientId && typeof easClientId === "string") { return `eas:${easClientId}`; } if (userAgent) { return `ua:${userAgent}`; } return null; } function lookupClient(headers) { const identifier = getPrimaryIdentifier(headers); if (!identifier) { return { info: null, identifier: null }; } const info = clientMappings.get(identifier); if (info) { info.lastUsed = Date.now(); } return { info: info || null, identifier }; } function saveClientMapping(identifier, serverId, simulatorUdid, matchedBy) { clientMappings.set(identifier, { serverId, simulatorUdid, matchedBy, lastUsed: Date.now() }); debugLog(`Saved mapping: ${identifier} -> server=${serverId}, sim=${simulatorUdid || "unknown"}, via=${matchedBy}`); } const DEFAULT_PORT = 8081; let routeModeOverride = null; function setRouteMode(mode) { routeModeOverride = mode; } let activeDaemonState = null; async function inferSimulator(clientInfo) { if (clientInfo?.simulatorUdid) return clientInfo.simulatorUdid; const simulators = await (0, import_picker.getBootedSimulators)(); const existingMappings = getSimulatorMappings(); const unmappedSims = simulators.filter(s => !existingMappings.has(s.udid)); return unmappedSims[0]?.udid || simulators[0]?.udid; } async function resolveServer(state, headers, servers, bundleId) { const { info: clientInfo, identifier } = lookupClient(headers); debugLog(`resolveServer: identifier=${identifier}, clientInfo=${JSON.stringify(clientInfo)}`); const learnMapping = async (server, matchedBy) => { if (identifier && !clientInfo?.simulatorUdid) { const simUdid = await inferSimulator(clientInfo); if (simUdid) { saveClientMapping(identifier, server.id, simUdid, matchedBy); return true; } } return false; }; if (servers.length === 1) { const server = servers[0]; const learned = await learnMapping(server, "auto"); debugLog(`Single server: ${server.root}`); return { server, learned }; } if (pendingMappings.size > 0 && identifier) { for (const [serverId, simUdid] of pendingMappings) { const server = (0, import_registry.findServerById)(state, serverId); if (server && servers.some(s => s.id === serverId)) { debugLog(`TUI pending mapping: ${server.root}, sim=${simUdid}`); saveClientMapping(identifier, serverId, simUdid, "tui"); pendingMappings.delete(serverId); return { server, learned: true }; } } } if (clientInfo?.simulatorUdid) { const simRoute = (0, import_registry.getRoute)(state, `sim:${clientInfo.simulatorUdid}`); if (simRoute) { const server = (0, import_registry.findServerById)(state, simRoute.serverId); if (server) { debugLog(`TUI cable route: sim=${clientInfo.simulatorUdid} -> ${server.root}`); return { server, learned: false }; } } } if (clientInfo?.serverId) { const server = (0, import_registry.findServerById)(state, clientInfo.serverId); if (server) { debugLog(`Cached mapping: ${identifier} -> ${server.root}`); return { server, learned: false }; } } const userAgent = headers["user-agent"] || ""; if (!isGenericExpoAgent(userAgent)) { const matchedServer = await matchUserAgentToServer(headers, servers); if (matchedServer) { debugLog(`UA match: ${extractAppNameFromUA(userAgent)} -> ${matchedServer.root}`); await learnMapping(matchedServer, "user-agent"); return { server: matchedServer, learned: true }; } } const routeKey = bundleId || "default"; const fallbackRoute = (0, import_registry.getRoute)(state, bundleId || "") || (0, import_registry.getRoute)(state, "default"); if (fallbackRoute) { const server = (0, import_registry.findServerById)(state, fallbackRoute.serverId); if (server) { debugLog(`Fallback route: ${server.root}`); await learnMapping(server, "auto"); return { server, learned: true }; } } const mostRecent = [...servers].sort((a, b) => b.registeredAt - a.registeredAt)[0]; debugLog(`Most recent fallback: ${mostRecent.root}`); (0, import_registry.setRoute)(state, routeKey, mostRecent.id); await learnMapping(mostRecent, "auto"); return { server: mostRecent, learned: true }; } function proxyAndTouch(req, res, server) { const pendingSimId = pendingMappings.get(server.id); if (pendingSimId) { const identifier = getPrimaryIdentifier(req.headers); if (identifier) { saveClientMapping(identifier, server.id, pendingSimId, "tui"); pendingMappings.delete(server.id); } } if (activeDaemonState) { (0, import_registry.touchServer)(activeDaemonState, server.id); } (0, import_proxy.proxyHttpRequest)(req, res, server); } async function startDaemon(options = {}) { const port = options.port || DEFAULT_PORT; const host = options.host || "0.0.0.0"; const quiet = options.quiet || false; const log = quiet ? (..._args) => {} : console.log; const state = (0, import_registry.createRegistry)(); activeDaemonState = state; const persistedServers = (0, import_ipc.readServerFiles)(); for (const ps of persistedServers) { const alive = await (0, import_registry.checkServerAlive)({ port: ps.port }); if (alive) { (0, import_registry.registerServer)(state, { port: ps.port, bundleId: ps.bundleId, root: ps.root }); log(import_picocolors.default.cyan(`[daemon] Recovered server: ${ps.bundleId} \u2192 :${ps.port}`)); } } const ipcServer = (0, import_ipc.createIPCServer)(state, id => { const server = (0, import_registry.findServerById)(state, id); if (server) { const shortRoot = server.root.replace(process.env.HOME || "", "~"); log(import_picocolors.default.green(`[daemon] Server registered: ${server.bundleId} \u2192 :${server.port} (${shortRoot})`)); } }, id => { log(import_picocolors.default.yellow(`[daemon] Server unregistered: ${id}`)); }); const httpServer = http.createServer(async (req, res) => { debugLog(`${req.method} ${req.url}`); if (req.url?.startsWith("/__daemon")) { await handleDaemonEndpoint(req, res, state); return; } const url = new URL(req.url || "/", `http://${req.headers.host}`); const bundleId = url.searchParams.get("app"); const servers = bundleId ? (0, import_registry.findServersByBundleId)(state, bundleId) : (0, import_registry.getAllServers)(state); if (servers.length === 0) { res.writeHead(404); res.end(bundleId ? `No server for app: ${bundleId}` : "No servers registered"); return; } const { server } = await resolveServer(state, req.headers, servers, bundleId); res.setHeader("Set-Cookie", `one-route-id=${server.id}; Path=/; Max-Age=3600`); const remotePort = req.socket?.remotePort; if (remotePort) { recentConnections.set(remotePort, { serverId: server.id, timestamp: Date.now() }); debugLog(`HTTP: port ${remotePort} -> ${server.root}`); } proxyAndTouch(req, res, server); }); httpServer.on("upgrade", async (req, rawSocket, head) => { const socket = rawSocket; const url = new URL(req.url || "/", `http://${req.headers.host}`); const bundleId = url.searchParams.get("app"); const servers = bundleId ? (0, import_registry.findServersByBundleId)(state, bundleId) : (0, import_registry.getAllServers)(state); if (servers.length === 0) { socket.end("HTTP/1.1 404 Not Found\r\n\r\n"); return; } let server; const routeIdFromCookie = getRouteIdFromCookies(req); if (routeIdFromCookie) { server = (0, import_registry.findServerById)(state, routeIdFromCookie); if (server && servers.some(s => s.id === server.id)) { debugLog(`WebSocket: cookie route -> ${server.root}`); } else { server = void 0; } } if (!server) { const remotePort = req.socket?.remotePort; if (remotePort) { const recent = recentConnections.get(remotePort); if (recent && Date.now() - recent.timestamp < CONNECTION_MEMORY_MS) { server = (0, import_registry.findServerById)(state, recent.serverId); if (server) { debugLog(`WebSocket: port ${remotePort} matched to ${server.root}`); } } } } if (!server) { const result = await resolveServer(state, req.headers, servers, bundleId); server = result.server; debugLog(`WebSocket: fallback -> ${server.root}`); } (0, import_registry.touchServer)(state, server.id); (0, import_proxy.proxyWebSocket)(req, socket, head, server); }); httpServer.listen(port, host, () => { log(import_picocolors.default.cyan("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550")); log(import_picocolors.default.cyan(" one daemon")); log(import_picocolors.default.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550")); log(` Listening on ${import_picocolors.default.green(`http://${host === "0.0.0.0" ? "localhost" : host}:${port}`)}`); log(` IPC socket: ${import_picocolors.default.dim((0, import_ipc.getSocketPath)())}`); log(""); log(import_picocolors.default.dim(" Waiting for dev servers to register...")); log(import_picocolors.default.dim(" Run 'one dev' in your project directories")); log(""); }); const HEALTH_CHECK_INTERVAL = 5e3; const healthCheckInterval = setInterval(async () => { const prunedCount = await (0, import_registry.pruneDeadServers)(state, server => { log(import_picocolors.default.yellow(`[daemon] Pruned dead server: ${server.bundleId} (port ${server.port})`)); }); if (prunedCount > 0) { log(import_picocolors.default.dim(`[daemon] Pruned ${prunedCount} dead server(s)`)); } }, HEALTH_CHECK_INTERVAL); const shutdown = () => { log(import_picocolors.default.yellow("\n[daemon] Shutting down...")); clearInterval(healthCheckInterval); httpServer.close(); ipcServer.close(); (0, import_ipc.cleanupSocket)(); process.exit(0); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); return { httpServer, ipcServer, state, shutdown, healthCheckInterval }; } async function handleDaemonEndpoint(req, res, state) { const url = new URL(req.url || "/", `http://${req.headers.host}`); if (url.pathname === "/__daemon/status") { const servers = (0, import_registry.getAllServers)(state); const simulators = await (0, import_picker.getBootedSimulators)(); const simMappings = getSimulatorMappings(); const simulatorRoutes = {}; for (const [udid, serverId] of simMappings) { simulatorRoutes[udid] = serverId; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ servers: servers.map(s => ({ id: s.id, port: s.port, bundleId: s.bundleId, root: s.root })), simulators, simulatorRoutes, routeMode: routeModeOverride || "most-recent" }, null, 2)); return; } if (url.pathname === "/__daemon/route" && req.method === "POST") { const bundleId = url.searchParams.get("bundleId"); const serverId = url.searchParams.get("serverId"); if (!bundleId || !serverId) { res.writeHead(400); res.end("Missing bundleId or serverId"); return; } const server = (0, import_registry.findServerById)(state, serverId); if (!server) { res.writeHead(404); res.end("Server not found"); return; } (0, import_registry.setRoute)(state, bundleId, serverId); (0, import_picker.resolvePendingPicker)(bundleId, serverId); res.writeHead(200); res.end("Route set"); return; } if (url.pathname === "/__daemon/simulator-route" && req.method === "POST") { const simulatorUdid = url.searchParams.get("simulatorUdid"); const serverId = url.searchParams.get("serverId"); if (!simulatorUdid || !serverId) { res.writeHead(400); res.end("Missing simulatorUdid or serverId"); return; } const server = (0, import_registry.findServerById)(state, serverId); if (!server) { res.writeHead(404); res.end("Server not found"); return; } setSimulatorMapping(simulatorUdid, serverId); setPendingMapping(serverId, simulatorUdid); (0, import_registry.setRoute)(state, `sim:${simulatorUdid}`, serverId); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); return; } if (url.pathname === "/__daemon/simulator-route" && req.method === "DELETE") { const simulatorUdid = url.searchParams.get("simulatorUdid"); if (!simulatorUdid) { res.writeHead(400); res.end("Missing simulatorUdid"); return; } clearMappingsForSimulator(simulatorUdid); (0, import_registry.clearRoute)(state, `sim:${simulatorUdid}`); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); return; } res.writeHead(404); res.end("Not found"); }