This commit is contained in:
Chris Wanstrath 2025-10-10 14:57:48 -07:00
parent 0b2f73eafa
commit 3ec5b8d1e5
9 changed files with 85 additions and 41 deletions

View File

@ -1,7 +1,7 @@
// Show the webapps hosted on this NOSEputer. // Show the webapps hosted on this NOSEputer.
import { $ } from "bun" import { $ } from "bun"
import { apps } from "@/webapp" import { apps } from "@/webapp/server"
const devMode = process.env.NODE_ENV !== "production" const devMode = process.env.NODE_ENV !== "production"

View File

@ -1,6 +1,6 @@
// Share a webapp with the public internet. // Share a webapp with the public internet.
import { apps } from "@/webapp" import { apps } from "@/webapp/server"
import { connectSneaker, sneakers, sneakerUrl } from "@/sneaker" import { connectSneaker, sneakers, sneakerUrl } from "@/sneaker"
export default async function (app: string) { export default async function (app: string) {

View File

@ -1,6 +1,6 @@
// Stop sharing a webapp with the public internet. // Stop sharing a webapp with the public internet.
import { apps } from "@/webapp" import { apps } from "@/webapp/server"
import { disconnectSneaker, sneakers } from "@/sneaker" import { disconnectSneaker, sneakers } from "@/sneaker"
export default async function (app: string) { export default async function (app: string) {

View File

@ -2,7 +2,7 @@
// Publishes webapps as subdomains on your local network // Publishes webapps as subdomains on your local network
import { watch } from "fs" import { watch } from "fs"
import { apps } from "./webapp" import { apps } from "./webapp/server"
import { expectDir } from "./utils" import { expectDir } from "./utils"
import { NOSE_DIR } from "./config" import { NOSE_DIR } from "./config"
import { expectShellCmd } from "./utils" import { expectShellCmd } from "./utils"

View File

@ -8,7 +8,7 @@
// import { css } from "@nose" // import { css } from "@nose"
import { Hono } from "hono" import { Hono } from "hono"
import { type Handler, toResponse } from "./webapp" import { type Handler, toResponse } from "./webapp/server"
// //
// command helpers // command helpers

View File

@ -7,9 +7,9 @@ import color from "kleur"
import type { Message } from "./shared/types" import type { Message } from "./shared/types"
import { rewriteJsImports } from "./build" 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 { 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 { commands, commandPath, loadCommandModule } from "./commands"
import { runCommandFn } from "./shell" import { runCommandFn } from "./shell"
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket" import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
@ -241,6 +241,7 @@ initWebapps()
initSneakers() initSneakers()
console.log(color.cyan(NOSE_ICON)) 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_BIN:"), color.yellow(tilde(NOSE_BIN)))
console.log(color.blue(" NOSE_DATA:"), color.yellow(tilde(NOSE_DATA))) console.log(color.blue(" NOSE_DATA:"), color.yellow(tilde(NOSE_DATA)))
console.log(color.blue(" NOSE_DIR:"), color.yellow(tilde(NOSE_DIR))) console.log(color.blue(" NOSE_DIR:"), color.yellow(tilde(NOSE_DIR)))

View File

@ -5,36 +5,34 @@ import type { Child } from "hono/jsx"
import { type Context, Hono } from "hono" import { type Context, Hono } from "hono"
import { join } from "path" import { join } from "path"
import { readdirSync, watch } from "fs" import { readdirSync, watch } from "fs"
import { sendAll } from "./websocket" import { sendAll } from "../websocket"
import { expectDir } from "./utils" import { expectDir } from "../utils"
import { NOSE_DIR } from "./config" import { NOSE_DIR, BUN_BIN } from "../config"
import { isFile, isDir, mtimeEpoch } from "./utils" import { isFile, isDir } from "../utils"
import { importUrl } from "./shared/utils"
export type Handler = (r: Context) => string | Child | Response | Promise<Response> export type Handler = (r: Context) => string | Child | Response | Promise<Response>
export type App = Hono | Handler export type App = Hono | Handler
const processes = new Map<string, { port: string, proc: ReturnType<typeof Bun.spawn> }>()
export function initWebapps() { export function initWebapps() {
startWatcher() startWatcher()
} }
export async function serveApp(c: Context, subdomain: string): Promise<Response> { export async function serveApp(c: Context, subdomain: string): Promise<Response> {
const app = await findApp(subdomain) const port = await startApp(subdomain)
const path = appDir(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 new Response(res.body, { status: res.status, headers: res.headers })
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))
} }
export function apps(): string[] { export function apps(): string[] {
@ -58,24 +56,23 @@ export function appDir(name: string): string | undefined {
return join(NOSE_DIR, name) return join(NOSE_DIR, name)
} }
async function findApp(name: string): Promise<App | undefined> { async function startApp(name: string): Promise<string | undefined> {
const paths = [ const existing = processes.get(name)
join(name, "index.ts"), if (existing) return existing.port
join(name, "index.tsx")
]
for (const path of paths) {
const app = await loadApp(join(NOSE_DIR, path))
if (app) return app
}
}
async function loadApp(path: string): Promise<App | undefined> { const port = String(4000 + processes.size)
if (!await Bun.file(path).exists()) return 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)) processes.set(name, { port, proc })
if (mod?.default) proc.exited.then(() => processes.delete(name))
return mod.default as App
return port
} }
export async function toResponse(source: string | Child | Response): Promise<Response> { export async function toResponse(source: string | Child | Response): Promise<Response> {
@ -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 let wwwWatcher
function startWatcher() { function startWatcher() {
if (!expectDir(NOSE_DIR)) return if (!expectDir(NOSE_DIR)) return
@ -110,4 +115,7 @@ function startWatcher() {
if (/^.+\/index\.tsx?$/.test(filename)) if (/^.+\/index\.tsx?$/.test(filename))
sendAll({ type: "apps", data: apps() }) sendAll({ type: "apps", data: apps() })
}) })
} }
process.on("SIGINT", shutdown)
process.on("SIGTERM", shutdown)

9
src/webapp/utils.ts Normal file
View File

@ -0,0 +1,9 @@
import { join } from "path"
import { NOSE_DIR } from "../config"
export async function appPath(appName: string): Promise<string | undefined> {
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())
}

26
src/webapp/worker.ts Normal file
View File

@ -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 <app-name>")
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}`)