one
Version:
One is a new React Framework that makes Vite serve both native and web.
386 lines (385 loc) • 17.3 kB
JavaScript
import * as http from "node:http";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import {
createRegistry,
findServersByBundleId,
findServerById,
getAllServers,
getRoute,
setRoute,
clearRoute,
touchServer,
pruneDeadServers,
checkServerAlive,
registerServer
} from "./registry";
import { createIPCServer, getSocketPath, cleanupSocket, readServerFiles } from "./ipc";
import { proxyHttpRequest, proxyWebSocket } from "./proxy";
import { getBootedSimulators, resolvePendingPicker } from "./picker";
import colors from "picocolors";
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")), name = content.expo?.name || content.name;
if (name)
return serverAppNames.set(root, name), name;
}
const appConfigTsPath = path.join(root, "app.config.ts");
if (fs.existsSync(appConfigTsPath)) {
const nameMatch = fs.readFileSync(appConfigTsPath, "utf-8").match(/name:\s*['"]([^'"]+)['"]/);
if (nameMatch) {
const name = nameMatch[1];
return serverAppNames.set(root, name), name;
}
}
const appConfigJsPath = path.join(root, "app.config.js");
if (fs.existsSync(appConfigJsPath)) {
const nameMatch = fs.readFileSync(appConfigJsPath, "utf-8").match(/name:\s*['"]([^'"]+)['"]/);
if (nameMatch) {
const name = nameMatch[1];
return serverAppNames.set(root, name), name;
}
}
const dirName = path.basename(root);
if (dirName)
return serverAppNames.set(root, dirName), dirName;
} catch (err) {
debugLog(`Failed to get app name for ${root}: ${err}`);
}
return null;
}
const clientMappings = /* @__PURE__ */ new Map(), MAPPING_TTL_MS = 36e5;
setInterval(() => {
const now = Date.now();
let cleaned = 0;
for (const [key, info] of clientMappings)
now - info.lastUsed > MAPPING_TTL_MS && (clientMappings.delete(key), cleaned++);
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)
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)
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, ""), normalizedApp = appName.toLowerCase().replace(/\s+/g, "");
if (debugLog(` Comparing "${normalizedUa}" to server app "${normalizedApp}"`), normalizedUa === normalizedApp || normalizedUa.includes(normalizedApp) || normalizedApp.includes(normalizedUa))
return debugLog(` Matched! ${uaAppName} -> ${server.root}`), server;
}
return null;
}
function isGenericExpoAgent(ua) {
return ua.startsWith("Expo/") || ua.startsWith("Exponent/");
}
function extractAppNameFromUA(ua) {
return (ua.split(" ")[0] || "").split("/")[0] || null;
}
const recentConnections = /* @__PURE__ */ new Map(), 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"];
return easClientId && typeof easClientId == "string" ? `eas:${easClientId}` : userAgent ? `ua:${userAgent}` : null;
}
function lookupClient(headers) {
const identifier = getPrimaryIdentifier(headers);
if (!identifier)
return { info: null, identifier: null };
const info = clientMappings.get(identifier);
return info && (info.lastUsed = Date.now()), { 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 getBootedSimulators(), existingMappings = getSimulatorMappings();
return simulators.filter((s) => !existingMappings.has(s.udid))[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)
return saveClientMapping(identifier, server.id, simUdid, matchedBy), !0;
}
return !1;
};
if (servers.length === 1) {
const server = servers[0], learned = await learnMapping(server, "auto");
return debugLog(`Single server: ${server.root}`), { server, learned };
}
if (pendingMappings.size > 0 && identifier)
for (const [serverId, simUdid] of pendingMappings) {
const server = findServerById(state, serverId);
if (server && servers.some((s) => s.id === serverId))
return debugLog(`TUI pending mapping: ${server.root}, sim=${simUdid}`), saveClientMapping(identifier, serverId, simUdid, "tui"), pendingMappings.delete(serverId), { server, learned: !0 };
}
if (clientInfo?.simulatorUdid) {
const simRoute = getRoute(state, `sim:${clientInfo.simulatorUdid}`);
if (simRoute) {
const server = findServerById(state, simRoute.serverId);
if (server)
return debugLog(`TUI cable route: sim=${clientInfo.simulatorUdid} -> ${server.root}`), { server, learned: !1 };
}
}
if (clientInfo?.serverId) {
const server = findServerById(state, clientInfo.serverId);
if (server)
return debugLog(`Cached mapping: ${identifier} -> ${server.root}`), { server, learned: !1 };
}
const userAgent = headers["user-agent"] || "";
if (!isGenericExpoAgent(userAgent)) {
const matchedServer = await matchUserAgentToServer(headers, servers);
if (matchedServer)
return debugLog(`UA match: ${extractAppNameFromUA(userAgent)} -> ${matchedServer.root}`), await learnMapping(matchedServer, "user-agent"), { server: matchedServer, learned: !0 };
}
const routeKey = bundleId || "default", fallbackRoute = getRoute(state, bundleId || "") || getRoute(state, "default");
if (fallbackRoute) {
const server = findServerById(state, fallbackRoute.serverId);
if (server)
return debugLog(`Fallback route: ${server.root}`), await learnMapping(server, "auto"), { server, learned: !0 };
}
const mostRecent = [...servers].sort((a, b) => b.registeredAt - a.registeredAt)[0];
return debugLog(`Most recent fallback: ${mostRecent.root}`), setRoute(state, routeKey, mostRecent.id), await learnMapping(mostRecent, "auto"), { server: mostRecent, learned: !0 };
}
function proxyAndTouch(req, res, server) {
const pendingSimId = pendingMappings.get(server.id);
if (pendingSimId) {
const identifier = getPrimaryIdentifier(req.headers);
identifier && (saveClientMapping(identifier, server.id, pendingSimId, "tui"), pendingMappings.delete(server.id));
}
activeDaemonState && touchServer(activeDaemonState, server.id), proxyHttpRequest(req, res, server);
}
async function startDaemon(options = {}) {
const port = options.port || DEFAULT_PORT, host = options.host || "0.0.0.0", log = options.quiet || !1 ? (..._args) => {
} : console.log, state = createRegistry();
activeDaemonState = state;
const persistedServers = readServerFiles();
for (const ps of persistedServers)
await checkServerAlive({ port: ps.port }) && (registerServer(state, {
port: ps.port,
bundleId: ps.bundleId,
root: ps.root
}), log(colors.cyan(`[daemon] Recovered server: ${ps.bundleId} \u2192 :${ps.port}`)));
const ipcServer = createIPCServer(
state,
(id) => {
const server = findServerById(state, id);
if (server) {
const shortRoot = server.root.replace(process.env.HOME || "", "~");
log(
colors.green(
`[daemon] Server registered: ${server.bundleId} \u2192 :${server.port} (${shortRoot})`
)
);
}
},
(id) => {
log(colors.yellow(`[daemon] Server unregistered: ${id}`));
}
), httpServer = http.createServer(async (req, res) => {
if (debugLog(`${req.method} ${req.url}`), req.url?.startsWith("/__daemon")) {
await handleDaemonEndpoint(req, res, state);
return;
}
const bundleId = new URL(req.url || "/", `http://${req.headers.host}`).searchParams.get("app"), servers = bundleId ? findServersByBundleId(state, bundleId) : 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;
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, bundleId = new URL(req.url || "/", `http://${req.headers.host}`).searchParams.get("app"), servers = bundleId ? findServersByBundleId(state, bundleId) : getAllServers(state);
if (servers.length === 0) {
socket.end(`HTTP/1.1 404 Not Found\r
\r
`);
return;
}
let server;
const routeIdFromCookie = getRouteIdFromCookies(req);
if (routeIdFromCookie && (server = findServerById(state, routeIdFromCookie), server && servers.some((s) => s.id === server.id) ? debugLog(`WebSocket: cookie route -> ${server.root}`) : server = void 0), !server) {
const remotePort = req.socket?.remotePort;
if (remotePort) {
const recent = recentConnections.get(remotePort);
recent && Date.now() - recent.timestamp < CONNECTION_MEMORY_MS && (server = findServerById(state, recent.serverId), server && debugLog(`WebSocket: port ${remotePort} matched to ${server.root}`));
}
}
server || (server = (await resolveServer(state, req.headers, servers, bundleId)).server, debugLog(`WebSocket: fallback -> ${server.root}`)), touchServer(state, server.id), proxyWebSocket(req, socket, head, server);
}), httpServer.listen(port, host, () => {
log(colors.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(colors.cyan(" one daemon")), log(colors.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 ${colors.green(`http://${host === "0.0.0.0" ? "localhost" : host}:${port}`)}`
), log(` IPC socket: ${colors.dim(getSocketPath())}`), log(""), log(colors.dim(" Waiting for dev servers to register...")), log(colors.dim(" Run 'one dev' in your project directories")), log("");
});
const healthCheckInterval = setInterval(async () => {
const prunedCount = await pruneDeadServers(state, (server) => {
log(
colors.yellow(
`[daemon] Pruned dead server: ${server.bundleId} (port ${server.port})`
)
);
});
prunedCount > 0 && log(colors.dim(`[daemon] Pruned ${prunedCount} dead server(s)`));
}, 5e3), shutdown = () => {
log(colors.yellow(`
[daemon] Shutting down...`)), clearInterval(healthCheckInterval), httpServer.close(), ipcServer.close(), cleanupSocket(), process.exit(0);
};
return process.on("SIGINT", shutdown), process.on("SIGTERM", shutdown), {
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 = getAllServers(state), simulators = await getBootedSimulators(), simMappings = getSimulatorMappings(), 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"), serverId = url.searchParams.get("serverId");
if (!bundleId || !serverId) {
res.writeHead(400), res.end("Missing bundleId or serverId");
return;
}
if (!findServerById(state, serverId)) {
res.writeHead(404), res.end("Server not found");
return;
}
setRoute(state, bundleId, serverId), 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"), serverId = url.searchParams.get("serverId");
if (!simulatorUdid || !serverId) {
res.writeHead(400), res.end("Missing simulatorUdid or serverId");
return;
}
if (!findServerById(state, serverId)) {
res.writeHead(404), res.end("Server not found");
return;
}
setSimulatorMapping(simulatorUdid, serverId), setPendingMapping(serverId, simulatorUdid), setRoute(state, `sim:${simulatorUdid}`, serverId), res.writeHead(200, { "Content-Type": "application/json" }), res.end(JSON.stringify({ ok: !0 }));
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), clearRoute(state, `sim:${simulatorUdid}`), res.writeHead(200, { "Content-Type": "application/json" }), res.end(JSON.stringify({ ok: !0 }));
return;
}
res.writeHead(404), res.end("Not found");
}
export {
clearAllMappings,
clearMappingsForSimulator,
getSimulatorMappings,
setPendingMapping,
setRouteMode,
setSimulatorMapping,
startDaemon
};
//# sourceMappingURL=server.js.map