it's alive

This commit is contained in:
Chris Wanstrath 2025-09-15 15:46:44 -07:00
parent 465ed69fc3
commit 45286920de
6 changed files with 99 additions and 16 deletions

View File

@ -1,7 +1,7 @@
import { text } from "./other" import { text } from "./other"
import { css } from "@utils" import { css } from "@utils"
export default (c: Context) => { export default () => {
return <> return <>
{css` {css`
body { body {

29
nose/app/routes.tsx Normal file
View File

@ -0,0 +1,29 @@
import { routes } from "@utils"
export default routes({
"GET /": index,
"GET /pets": pets
})
function index() {
return <>
<h1>Hi world!</h1>
<p>Welcome to my personal web page.</p>
<p>If you are looking for information on pets, click here:</p>
<p><a href="/pets">PETS</a></p>
</>
}
function pets(c: Context) {
return c.html(<>
<ul>
<li>dogs</li>
<li>cats</li>
<li>iguanas</li>
<li>hamsters</li>
<li>snakes</li>
<li>chickens</li>
<li>...even goats!</li>
</ul>
</>)
}

View File

@ -1,11 +1,11 @@
{ {
"name": "pluto", "name": "pluto",
"module": "index.ts", "module": "src/server.tsx",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "bun src/server.ts", "start": "bun src/server.tsx",
"dev": "bun --hot src/server.ts" "dev": "bun --hot src/server.tsx"
}, },
"alias": { "alias": {
"@utils": "./src/utils.tsx", "@utils": "./src/utils.tsx",

View File

@ -5,7 +5,7 @@ import color from "kleur"
import { NOSE_ICON, NOSE_DIR, NOSE_BIN, NOSE_APP } from "./config" import { NOSE_ICON, NOSE_DIR, NOSE_BIN, NOSE_APP } from "./config"
import { transpile, isFile } from "./utils" import { transpile, isFile } from "./utils"
import { serveApp } from "./webapp" import { apps, serveApp } from "./webapp"
// //
// Hono setup // Hono setup
@ -43,12 +43,29 @@ app.get("/js/:path{.+}", async c => {
// app routes // app routes
// //
app.get("*", async c => { app.use("*", async (c, next) => {
const url = new URL(c.req.url) const url = new URL(c.req.url)
const domains = url.hostname.split(".") const domains = url.hostname.split(".")
const subdomain = domains.length > 1 ? domains[0]! : "none" const subdomain = domains.length > 1 ? domains[0]! : ""
return await serveApp(c, subdomain) if (subdomain) {
const app = serveApp(c, subdomain)
return await app
}
return next()
})
app.get("/", c => {
const url = new URL(c.req.url)
const domain = url.hostname
const port = url.port
return c.html(<>
<h1>apps</h1>
<ul>{apps().map(app => <li><a href={`http://${app}.${domain}:${port}`}>{app}</a></li>)}
</ul>
</>)
}) })
// //

View File

@ -1,5 +1,7 @@
import { Hono } from "hono"
import { statSync } from "node:fs" import { statSync } from "node:fs"
import { stat } from "node:fs/promises" import { stat } from "node:fs/promises"
import { type Handler, toResponse } from "./webapp"
// Is the given `path` a file? // Is the given `path` a file?
export function isFile(path: string): boolean { export function isFile(path: string): boolean {
@ -26,7 +28,6 @@ export function randomID(): string {
return Math.random().toString(36).slice(2, 10) return Math.random().toString(36).slice(2, 10)
} }
const transpiler = new Bun.Transpiler({ loader: 'tsx' }) const transpiler = new Bun.Transpiler({ loader: 'tsx' })
const transpileCache: Record<string, string> = {} const transpileCache: Record<string, string> = {}
@ -65,3 +66,25 @@ export function js(strings: TemplateStringsArray, ...values: any[]) {
}, '') }, '')
}} /> }} />
} }
// for defining routes in your NOSE webapp
// example:
// export default routes({
// "GET /": index,
// "GET /pets": pets
// })
export function routes(def: Record<string, Handler>): Hono {
const app = new Hono
for (const key in def) {
const parts = key.split(" ") // GET /path
const method = parts[0] || "GET"
const path = parts[1] || "/"
console.log(method, path, def[key])
//@ts-ignore
app.on(method, path, async c => toResponse(await def[key](c)))
}
return app
}

View File

@ -1,18 +1,32 @@
import type { Context } from "hono" import { type Context, Hono } from "hono"
import type { Child } from "hono/jsx" import type { Child } from "hono/jsx"
import { renderToString } from "hono/jsx/dom/server"
import { join } from "node:path" import { join } from "node:path"
import { readdirSync } from "node:fs"
import { NOSE_APP } from "./config" import { NOSE_APP } from "./config"
import { isFile } from "./utils" import { isFile } from "./utils"
type App = (r: Context) => string | Child | Response export type Handler = (r: Context) => string | Child | Response | Promise<Response>
export type App = Hono | Handler
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 app = await findApp(subdomain)
if (app) if (!app) return c.text(`App Not Found: ${subdomain}`, 404)
return await toResponse(app(c))
return c.text(`App Not Found: ${subdomain}`, 404) if (app instanceof Hono)
return app.fetch(c.req.raw)
else
return toResponse(app(c))
}
export function apps(): string[] {
const apps: string[] = []
for (const entry of readdirSync(NOSE_APP))
apps.push(entry.replace(/\.tsx?/, ""))
return apps
} }
async function findApp(name: string): Promise<App | undefined> { async function findApp(name: string): Promise<App | undefined> {
@ -43,13 +57,13 @@ async function loadApp(path: string): Promise<App | undefined> {
return mod.default as App return mod.default as App
} }
async function toResponse(source: string | Child | Response): Promise<Response> { export function toResponse(source: string | Child | Response): Response {
if (source instanceof Response) if (source instanceof Response)
return source return source
else if (typeof source === "string") else if (typeof source === "string")
return new Response(source) return new Response(source)
else else
return new Response(await source?.toString(), { return new Response(renderToString(source), {
headers: { headers: {
"Content-Type": "text/html; charset=utf-8" "Content-Type": "text/html; charset=utf-8"
} }