apps.ts
This commit is contained in:
parent
fa459c45eb
commit
aaf6ce8361
8
src/pages/index.tsx
Normal file
8
src/pages/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { runningApps } from '../server/apps'
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<>
|
||||||
|
<h1>🐾 Running Apps</h1>
|
||||||
|
{runningApps().map(app => <h2><a href={`http://localhost:${app.port}`}>{app.port}: {app.name}</a></h2>)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
150
src/server/apps.ts
Normal file
150
src/server/apps.ts
Normal file
|
|
@ -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<string, RunningApp>()
|
||||||
|
|
||||||
|
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<Uint8Array> | 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()
|
||||||
|
}
|
||||||
|
|
@ -1,167 +1,9 @@
|
||||||
import type { Subprocess } from 'bun'
|
|
||||||
import { Hype } from 'hype'
|
import { Hype } from 'hype'
|
||||||
import { existsSync, readdirSync, readFileSync, watch } from 'fs'
|
import { initApps } from './apps'
|
||||||
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<string, RunningApp>()
|
|
||||||
|
|
||||||
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<Uint8Array> | 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new Hype()
|
const app = new Hype()
|
||||||
|
|
||||||
app.get('/', c => {
|
console.log('🐾 Toes!')
|
||||||
return c.html(
|
initApps()
|
||||||
<>
|
|
||||||
<h1>🐾 Running Apps</h1>
|
|
||||||
{getRunningApps().map(app => <h2><a href={`http://localhost:${app.port}`}>{app.port}: {app.name}</a></h2>)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
startup()
|
|
||||||
|
|
||||||
export { getRunningApps, stopApp }
|
|
||||||
export default app.defaults
|
export default app.defaults
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user