Compare commits
4 Commits
36c7913b6c
...
520606ccb9
| Author | SHA1 | Date | |
|---|---|---|---|
| 520606ccb9 | |||
| 26409010a8 | |||
| 236e8ff38e | |||
| 4aebd6a087 |
|
|
@ -57,6 +57,8 @@ toes open
|
|||
|
||||
Your app is now running at `http://my-app.toes.local`.
|
||||
|
||||
> **Tip:** Add `.toes` to your `.gitignore`. This file tracks local sync state and shouldn't be committed.
|
||||
|
||||
---
|
||||
|
||||
## Creating an App
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { App as SharedApp, AppState } from '@types'
|
|||
import type { ToesEvent, ToesEventInput, ToesEventType } from '../shared/events'
|
||||
import type { Subprocess } from 'bun'
|
||||
import { DEFAULT_EMOJI } from '@types'
|
||||
import { buildAppUrl } from '@urls'
|
||||
import { buildAppUrl, toSubdomain } from '@urls'
|
||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from 'fs'
|
||||
import { hostname } from 'os'
|
||||
import { join, resolve } from 'path'
|
||||
|
|
@ -60,6 +60,9 @@ export const allApps = (): App[] =>
|
|||
export const getApp = (dir: string): App | undefined =>
|
||||
_apps.get(dir)
|
||||
|
||||
export const getAppBySubdomain = (subdomain: string): App | undefined =>
|
||||
_apps.get(subdomain) ?? allApps().find(a => toSubdomain(a.name) === subdomain)
|
||||
|
||||
export const runApps = () =>
|
||||
allAppDirs().filter(isApp).forEach(startApp)
|
||||
|
||||
|
|
@ -644,9 +647,12 @@ async function runApp(dir: string, port: number) {
|
|||
// Load env vars from TOES_DIR/env/
|
||||
const appEnv = loadAppEnv(dir, TOES_DIR)
|
||||
|
||||
const dataDir = join(process.env.DATA_DIR ?? '.', 'toes', dir)
|
||||
mkdirSync(dataDir, { recursive: true })
|
||||
|
||||
const proc = Bun.spawn(['bun', 'run', 'toes'], {
|
||||
cwd,
|
||||
env: { ...process.env, ...appEnv, PORT: String(port), NO_AUTOPORT: 'true', APP_URL: buildAppUrl(dir, TOES_URL), APPS_DIR, DATA_DIR: join(process.env.DATA_DIR ?? '.', 'toes', dir), TOES_DIR, TOES_URL },
|
||||
env: { ...process.env, ...appEnv, PORT: String(port), NO_AUTOPORT: 'true', APP_URL: buildAppUrl(dir, TOES_URL), APPS_DIR, DATA_DIR: dataDir, TOES_DIR, TOES_URL },
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Subprocess } from 'bun'
|
||||
import { toSubdomain } from '@urls'
|
||||
import { networkInterfaces } from 'os'
|
||||
import { hostLog } from './tui'
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ export function publishApp(name: string) {
|
|||
return
|
||||
}
|
||||
|
||||
const hostname = `${name}.toes.local`
|
||||
const hostname = `${toSubdomain(name)}.toes.local`
|
||||
|
||||
try {
|
||||
const proc = Bun.spawn(['avahi-publish', '-a', hostname, '-R', ip], {
|
||||
|
|
@ -67,7 +68,7 @@ export function unpublishApp(name: string) {
|
|||
|
||||
proc.kill()
|
||||
_publishers.delete(name)
|
||||
hostLog(`mDNS: unpublished ${name}.toes.local`)
|
||||
hostLog(`mDNS: unpublished ${toSubdomain(name)}.toes.local`)
|
||||
}
|
||||
|
||||
export function unpublishAll() {
|
||||
|
|
@ -75,7 +76,7 @@ export function unpublishAll() {
|
|||
|
||||
for (const [name, proc] of _publishers) {
|
||||
proc.kill()
|
||||
hostLog(`mDNS: unpublished ${name}.toes.local`)
|
||||
hostLog(`mDNS: unpublished ${toSubdomain(name)}.toes.local`)
|
||||
}
|
||||
_publishers.clear()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Server, ServerWebSocket } from 'bun'
|
||||
import { getApp } from '$apps'
|
||||
import { getAppBySubdomain } from '$apps'
|
||||
|
||||
export type { WsData }
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ export function extractSubdomain(host: string): string | null {
|
|||
}
|
||||
|
||||
export async function proxySubdomain(subdomain: string, req: Request): Promise<Response> {
|
||||
const app = getApp(subdomain)
|
||||
const app = getAppBySubdomain(subdomain)
|
||||
|
||||
if (!app || app.state !== 'running' || !app.port) {
|
||||
return new Response(`App "${subdomain}" not found or not running`, { status: 502 })
|
||||
|
|
@ -66,7 +66,7 @@ export async function proxySubdomain(subdomain: string, req: Request): Promise<R
|
|||
}
|
||||
|
||||
export function proxyWebSocket(subdomain: string, req: Request, server: Server<WsData>): Response | undefined {
|
||||
const app = getApp(subdomain)
|
||||
const app = getAppBySubdomain(subdomain)
|
||||
|
||||
if (!app || app.state !== 'running' || !app.port) {
|
||||
return new Response(`App "${subdomain}" not found or not running`, { status: 502 })
|
||||
|
|
|
|||
44
src/shared/urls.test.ts
Normal file
44
src/shared/urls.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
import { buildAppUrl, toSubdomain } from './urls'
|
||||
|
||||
describe('toSubdomain', () => {
|
||||
test('passes through clean names', () => {
|
||||
expect(toSubdomain('clock')).toBe('clock')
|
||||
expect(toSubdomain('my-app')).toBe('my-app')
|
||||
})
|
||||
|
||||
test('replaces dots with hyphens', () => {
|
||||
expect(toSubdomain('mcp.txt')).toBe('mcp-txt')
|
||||
expect(toSubdomain('my.cool.app')).toBe('my-cool-app')
|
||||
})
|
||||
|
||||
test('replaces underscores with hyphens', () => {
|
||||
expect(toSubdomain('my_app')).toBe('my-app')
|
||||
})
|
||||
|
||||
test('collapses multiple hyphens', () => {
|
||||
expect(toSubdomain('my--app')).toBe('my-app')
|
||||
expect(toSubdomain('a..b')).toBe('a-b')
|
||||
})
|
||||
|
||||
test('strips leading and trailing hyphens', () => {
|
||||
expect(toSubdomain('.hidden')).toBe('hidden')
|
||||
expect(toSubdomain('trailing.')).toBe('trailing')
|
||||
expect(toSubdomain('-dashed-')).toBe('dashed')
|
||||
})
|
||||
|
||||
test('lowercases', () => {
|
||||
expect(toSubdomain('MyApp')).toBe('myapp')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildAppUrl', () => {
|
||||
test('prepends sanitized name as subdomain', () => {
|
||||
expect(buildAppUrl('clock', 'http://toes.local')).toBe('http://clock.toes.local')
|
||||
expect(buildAppUrl('mcp.txt', 'http://toes.local')).toBe('http://mcp-txt.toes.local')
|
||||
})
|
||||
|
||||
test('works with localhost and port', () => {
|
||||
expect(buildAppUrl('my-app', 'http://localhost:3000')).toBe('http://my-app.localhost:3000')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
export const toSubdomain = (name: string): string =>
|
||||
name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
||||
|
||||
export function buildAppUrl(appName: string, baseUrl: string): string {
|
||||
const url = new URL(baseUrl)
|
||||
url.hostname = `${appName}.${url.hostname}`
|
||||
url.hostname = `${toSubdomain(appName)}.${url.hostname}`
|
||||
return url.origin
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user