From 3ec5b8d1e5eeab47b4bd5215205a631c35f4948f Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:57:48 -0700 Subject: [PATCH] wip --- bin/apps.tsx | 2 +- bin/share.ts | 2 +- bin/unshare.ts | 2 +- src/dns.ts | 2 +- src/helpers.tsx | 2 +- src/server.tsx | 5 +- src/{webapp.ts => webapp/server.ts} | 76 ++++++++++++++++------------- src/webapp/utils.ts | 9 ++++ src/webapp/worker.ts | 26 ++++++++++ 9 files changed, 85 insertions(+), 41 deletions(-) rename src/{webapp.ts => webapp/server.ts} (55%) create mode 100644 src/webapp/utils.ts create mode 100644 src/webapp/worker.ts diff --git a/bin/apps.tsx b/bin/apps.tsx index 3a0ecb8..b4e2e80 100644 --- a/bin/apps.tsx +++ b/bin/apps.tsx @@ -1,7 +1,7 @@ // Show the webapps hosted on this NOSEputer. import { $ } from "bun" -import { apps } from "@/webapp" +import { apps } from "@/webapp/server" const devMode = process.env.NODE_ENV !== "production" diff --git a/bin/share.ts b/bin/share.ts index b6d7a36..af60100 100644 --- a/bin/share.ts +++ b/bin/share.ts @@ -1,6 +1,6 @@ // Share a webapp with the public internet. -import { apps } from "@/webapp" +import { apps } from "@/webapp/server" import { connectSneaker, sneakers, sneakerUrl } from "@/sneaker" export default async function (app: string) { diff --git a/bin/unshare.ts b/bin/unshare.ts index 5f3f07b..4110a7b 100644 --- a/bin/unshare.ts +++ b/bin/unshare.ts @@ -1,6 +1,6 @@ // Stop sharing a webapp with the public internet. -import { apps } from "@/webapp" +import { apps } from "@/webapp/server" import { disconnectSneaker, sneakers } from "@/sneaker" export default async function (app: string) { diff --git a/src/dns.ts b/src/dns.ts index 3347b4b..8ec42cc 100644 --- a/src/dns.ts +++ b/src/dns.ts @@ -2,7 +2,7 @@ // Publishes webapps as subdomains on your local network import { watch } from "fs" -import { apps } from "./webapp" +import { apps } from "./webapp/server" import { expectDir } from "./utils" import { NOSE_DIR } from "./config" import { expectShellCmd } from "./utils" diff --git a/src/helpers.tsx b/src/helpers.tsx index 7cc9e70..96ba07b 100644 --- a/src/helpers.tsx +++ b/src/helpers.tsx @@ -8,7 +8,7 @@ // import { css } from "@nose" import { Hono } from "hono" -import { type Handler, toResponse } from "./webapp" +import { type Handler, toResponse } from "./webapp/server" // // command helpers diff --git a/src/server.tsx b/src/server.tsx index 203ea7e..5daff01 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -7,9 +7,9 @@ import color from "kleur" import type { Message } from "./shared/types" import { rewriteJsImports } from "./build" -import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN, DEFAULT_PROJECT } from "./config" +import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN, BUN_BIN, DEFAULT_PROJECT } from "./config" import { transpile, isFile, tilde, isDir } from "./utils" -import { serveApp, apps, initWebapps } from "./webapp" +import { serveApp, apps, initWebapps } from "./webapp/server" import { commands, commandPath, loadCommandModule } from "./commands" import { runCommandFn } from "./shell" import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket" @@ -241,6 +241,7 @@ initWebapps() initSneakers() console.log(color.cyan(NOSE_ICON)) +console.log(color.blue(" BUN_BIN:"), color.yellow(tilde(BUN_BIN))) console.log(color.blue(" NOSE_BIN:"), color.yellow(tilde(NOSE_BIN))) console.log(color.blue(" NOSE_DATA:"), color.yellow(tilde(NOSE_DATA))) console.log(color.blue(" NOSE_DIR:"), color.yellow(tilde(NOSE_DIR))) diff --git a/src/webapp.ts b/src/webapp/server.ts similarity index 55% rename from src/webapp.ts rename to src/webapp/server.ts index 77fba38..ba4cec9 100644 --- a/src/webapp.ts +++ b/src/webapp/server.ts @@ -5,36 +5,34 @@ import type { Child } from "hono/jsx" import { type Context, Hono } from "hono" import { join } from "path" import { readdirSync, watch } from "fs" -import { sendAll } from "./websocket" -import { expectDir } from "./utils" -import { NOSE_DIR } from "./config" -import { isFile, isDir, mtimeEpoch } from "./utils" -import { importUrl } from "./shared/utils" +import { sendAll } from "../websocket" +import { expectDir } from "../utils" +import { NOSE_DIR, BUN_BIN } from "../config" +import { isFile, isDir } from "../utils" export type Handler = (r: Context) => string | Child | Response | Promise export type App = Hono | Handler +const processes = new Map }>() + export function initWebapps() { startWatcher() } export async function serveApp(c: Context, subdomain: string): Promise { - const app = await findApp(subdomain) - const path = appDir(subdomain) + const port = await startApp(subdomain) + if (!port) return c.text(`App not found: ${subdomain}`, 404) - if (!path) return c.text(`App not found: ${subdomain}`, 404) + const staticPath = join(appDir(subdomain)!, "pub", c.req.path === "/" ? "/index.html" : c.req.path) + if (isFile(staticPath)) return serveStatic(staticPath) - const staticPath = join(path, "pub", c.req.path === "/" ? "/index.html" : c.req.path) + const res = await fetch(`http://localhost:${port}${c.req.path}`, { + method: c.req.method, + headers: c.req.raw.headers, + body: c.req.raw.body, + }) - if (isFile(staticPath)) - return serveStatic(staticPath) - - if (!app) return c.text(`App not found: ${subdomain}`, 404) - - if (app instanceof Hono) - return app.fetch(c.req.raw) - else - return toResponse(await app(c)) + return new Response(res.body, { status: res.status, headers: res.headers }) } export function apps(): string[] { @@ -58,24 +56,23 @@ export function appDir(name: string): string | undefined { return join(NOSE_DIR, name) } -async function findApp(name: string): Promise { - const paths = [ - join(name, "index.ts"), - join(name, "index.tsx") - ] +async function startApp(name: string): Promise { + const existing = processes.get(name) + if (existing) return existing.port - for (const path of paths) { - const app = await loadApp(join(NOSE_DIR, path)) - if (app) return app - } -} -async function loadApp(path: string): Promise { - if (!await Bun.file(path).exists()) return + const port = String(4000 + processes.size) + const proc = Bun.spawn({ + cmd: [BUN_BIN, "run", "src/webapp/worker.ts", name], + env: { PORT: port }, + stdout: "inherit", + stderr: "inherit", + }) - const mod = await importUrl(path, await mtimeEpoch(path)) - if (mod?.default) - return mod.default as App + processes.set(name, { port, proc }) + proc.exited.then(() => processes.delete(name)) + + return port } export async function toResponse(source: string | Child | Response): Promise { @@ -100,6 +97,14 @@ function serveStatic(path: string): Response { }) } +async function shutdown() { + for (const [name, { port, proc }] of processes) { + console.log(`Shutting down ${name}`) + try { proc.kill() } catch { } + } + process.exit(0) +} + let wwwWatcher function startWatcher() { if (!expectDir(NOSE_DIR)) return @@ -110,4 +115,7 @@ function startWatcher() { if (/^.+\/index\.tsx?$/.test(filename)) sendAll({ type: "apps", data: apps() }) }) -} \ No newline at end of file +} + +process.on("SIGINT", shutdown) +process.on("SIGTERM", shutdown) diff --git a/src/webapp/utils.ts b/src/webapp/utils.ts new file mode 100644 index 0000000..857c22e --- /dev/null +++ b/src/webapp/utils.ts @@ -0,0 +1,9 @@ +import { join } from "path" +import { NOSE_DIR } from "../config" + +export async function appPath(appName: string): Promise { + const ts = join(NOSE_DIR, appName, "index.ts") + const tsx = join(NOSE_DIR, appName, "index.tsx") + + return [ts, tsx].find(async file => await Bun.file(file).exists()) +} \ No newline at end of file diff --git a/src/webapp/worker.ts b/src/webapp/worker.ts new file mode 100644 index 0000000..352a1d9 --- /dev/null +++ b/src/webapp/worker.ts @@ -0,0 +1,26 @@ +//// +// This is the child process that runs a single webapp. + +import { Hono } from "hono" +import { appPath } from "./utils" + +const appName = Bun.argv[2] +if (!appName) { + console.log("usage: bun run webapp-worker ") + process.exit(1) +} + +const path = await appPath(appName) +if (!path) throw `Can't find app: ${appName}` + +const mod = await import(path) + +const handler = mod.default +if (typeof handler !== "function") throw `no default export in ${appName}` + +const app = new Hono() +app.all("*", handler) +const port = Number(process.env.PORT || 4000) +Bun.serve({ port, fetch: app.fetch }) + +console.log(`[child:${appName}] listening on localhost:${port}`) \ No newline at end of file