From aaf6ce83613192fec022d0adab1a528279098d73 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:05:58 -0800 Subject: [PATCH] apps.ts --- src/pages/index.tsx | 8 +++ src/server/apps.ts | 150 +++++++++++++++++++++++++++++++++++++++ src/server/index.tsx | 164 +------------------------------------------ 3 files changed, 161 insertions(+), 161 deletions(-) create mode 100644 src/pages/index.tsx create mode 100644 src/server/apps.ts diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..fcd0541 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,8 @@ +import { runningApps } from '../server/apps' + +export default () => ( + <> +

🐾 Running Apps

+ {runningApps().map(app =>

{app.port}: {app.name}

)} + +) \ No newline at end of file diff --git a/src/server/apps.ts b/src/server/apps.ts new file mode 100644 index 0000000..eb8afc7 --- /dev/null +++ b/src/server/apps.ts @@ -0,0 +1,150 @@ +import type { Subprocess } from 'bun' +import { existsSync, readdirSync, readFileSync, watch } from 'fs' +import { join } from 'path' + +const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps') + +type RunningApp = { + name: string + port: number + proc: Subprocess +} + +const _runningApps = new Map() + +const err = (app: string, ...msg: string[]) => + console.error('🐾', `${app}:`, ...msg) + +const info = (app: string, ...msg: string[]) => + console.log('🐾', `${app}:`, ...msg) + +const log = (app: string, ...msg: string[]) => + console.log(`<${app}>`, ...msg) + +export const appNames = () => { + return readdirSync(APPS_DIR, { withFileTypes: true }) + .filter(e => e.isDirectory()) + .map(e => e.name) + .sort() +} + +let NEXT_PORT = 3001 +const getPort = () => NEXT_PORT++ + +export const runApps = () => { + for (const dir of appNames()) { + if (!isApp(dir)) continue + const port = getPort() + runApp(dir, port) + } +} + +const isApp = (dir: string): boolean => { + try { + const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8') + const json = JSON.parse(file) + return !!json.scripts?.toes + } catch (e) { + return false + } +} + +const loadApp = (dir: string) => { + try { + const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8') + const json = JSON.parse(file) + + if (json.scripts?.toes) { + return json + } else { + err(dir, 'No `bun toes` script in package.json') + return {} + } + } catch (e) { + err(dir, 'No package.json') + return {} + } +} + +const runApp = async (dir: string, port: number) => { + const pkg = loadApp(dir) + if (!pkg.scripts?.toes) return + + const cwd = join(APPS_DIR, dir) + + const needsInstall = !existsSync(join(cwd, 'node_modules')) + if (needsInstall) info(dir, 'Installing dependencies...') + const install = Bun.spawn(['bun', 'install'], { cwd, stdout: 'pipe', stderr: 'pipe' }) + await install.exited + + info(dir, `Starting on port ${port}...`) + + const proc = Bun.spawn(['bun', 'run', 'toes'], { + cwd, + env: { ...process.env, PORT: String(port) }, + stdout: 'pipe', + stderr: 'pipe', + }) + + _runningApps.set(dir, { name: dir, port, proc }) + + const streamOutput = async (stream: ReadableStream | null, isErr: boolean) => { + if (!stream) return + const reader = stream.getReader() + const decoder = new TextDecoder() + while (true) { + const { done, value } = await reader.read() + if (done) break + const text = decoder.decode(value).trimEnd() + if (text) { + //isErr ? err(dir, text) : info(dir, text) + log(dir, text) + } + } + } + + streamOutput(proc.stdout, false) + streamOutput(proc.stderr, true) + + // Handle process exit + proc.exited.then(code => { + if (code !== 0) + err(dir, `Exited with code ${code}`) + else + info(dir, 'Stopped') + _runningApps.delete(dir) + }) +} + +export const runningApps = () => + Array.from(_runningApps.values()) + .map(({ name, port }) => ({ name, port })) + .sort((a, b) => a.port - b.port) + +const stopApp = (dir: string) => { + const app = _runningApps.get(dir) + if (app) { + info(dir, 'Stopping...') + app.proc.kill() + } +} + +const watchAppsDir = () => { + watch(APPS_DIR, (event, filename) => { + console.log('apps dir') + if (!filename) return + + if (isApp(filename) && !_runningApps.has(filename)) { + const port = getPort() + runApp(filename, port) + } + + if (_runningApps.has(filename) && !isApp(filename)) + stopApp(filename) + }) +} + +export const initApps = () => { + runApps() + watchAppsDir() +} \ No newline at end of file diff --git a/src/server/index.tsx b/src/server/index.tsx index 2dba9cf..ecabf38 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -1,167 +1,9 @@ -import type { Subprocess } from 'bun' import { Hype } from 'hype' -import { existsSync, readdirSync, readFileSync, watch } from 'fs' -import { join } from 'path' - -const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps') - -type RunningApp = { - name: string - port: number - proc: Subprocess -} - -const runningApps = new Map() - -const err = (app: string, ...msg: string[]) => - console.error('🐾', `${app}:`, ...msg) - -const info = (app: string, ...msg: string[]) => - console.log('🐾', `${app}:`, ...msg) - -const log = (app: string, ...msg: string[]) => - console.log(`<${app}>`, ...msg) - -const appNames = () => { - return readdirSync(APPS_DIR, { withFileTypes: true }) - .filter(e => e.isDirectory()) - .map(e => e.name) - .sort() -} - -let NEXT_PORT = 3001 -const getPort = () => NEXT_PORT++ - -const runApps = () => { - for (const dir of appNames()) { - if (!isApp(dir)) continue - const port = getPort() - runApp(dir, port) - } -} - -const isApp = (dir: string): boolean => { - try { - const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8') - const json = JSON.parse(file) - return !!json.scripts?.toes - } catch (e) { - return false - } -} - -const loadApp = (dir: string) => { - try { - const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8') - const json = JSON.parse(file) - - if (json.scripts?.toes) { - return json - } else { - err(dir, 'No `bun toes` script in package.json') - return {} - } - } catch (e) { - err(dir, 'No package.json') - return {} - } -} - -const runApp = async (dir: string, port: number) => { - const pkg = loadApp(dir) - if (!pkg.scripts?.toes) return - - const cwd = join(APPS_DIR, dir) - - const needsInstall = !existsSync(join(cwd, 'node_modules')) - if (needsInstall) info(dir, 'Installing dependencies...') - const install = Bun.spawn(['bun', 'install'], { cwd, stdout: 'pipe', stderr: 'pipe' }) - await install.exited - - info(dir, `Starting on port ${port}...`) - - const proc = Bun.spawn(['bun', 'run', 'toes'], { - cwd, - env: { ...process.env, PORT: String(port) }, - stdout: 'pipe', - stderr: 'pipe', - }) - - runningApps.set(dir, { name: dir, port, proc }) - - const streamOutput = async (stream: ReadableStream | null, isErr: boolean) => { - if (!stream) return - const reader = stream.getReader() - const decoder = new TextDecoder() - while (true) { - const { done, value } = await reader.read() - if (done) break - const text = decoder.decode(value).trimEnd() - if (text) { - //isErr ? err(dir, text) : info(dir, text) - log(dir, text) - } - } - } - - streamOutput(proc.stdout, false) - streamOutput(proc.stderr, true) - - // Handle process exit - proc.exited.then(code => { - if (code !== 0) - err(dir, `Exited with code ${code}`) - else - info(dir, 'Stopped') - runningApps.delete(dir) - }) -} - -const getRunningApps = () => - Array.from(runningApps.values()).map(({ name, port }) => ({ name, port })) - -const stopApp = (dir: string) => { - const app = runningApps.get(dir) - if (app) { - info(dir, 'Stopping...') - app.proc.kill() - } -} - -const watchAppsDir = () => { - watch(APPS_DIR, (event, filename) => { - console.log('apps dir') - if (!filename) return - - if (isApp(filename) && !runningApps.has(filename)) { - const port = getPort() - runApp(filename, port) - } - - if (runningApps.has(filename) && !isApp(filename)) - stopApp(filename) - }) -} - -const startup = () => { - console.log('🐾 Toes!') - - runApps() - watchAppsDir() -} +import { initApps } from './apps' const app = new Hype() -app.get('/', c => { - return c.html( - <> -

🐾 Running Apps

- {getRunningApps().map(app =>

{app.port}: {app.name}

)} - - ) -}) +console.log('🐾 Toes!') +initApps() -startup() - -export { getRunningApps, stopApp } export default app.defaults