toes/apps/code/20260130-000000/src/server/index.tsx
Chris Wanstrath ebf3ffc3af spicy
2026-01-30 16:16:59 -08:00

328 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Hype } from '@because/hype'
import { define, stylesToCSS } from '@because/forge'
import { readdir, stat } from 'fs/promises'
import { readFileSync } from 'fs'
import { join, extname, basename } from 'path'
const APPS_DIR = process.env.APPS_DIR!
const app = new Hype({ prettyHTML: false })
// Styles
const Container = define('CodeBrowserContainer', {
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
padding: '20px',
maxWidth: '1200px',
margin: '0 auto',
})
const Header = define('Header', {
marginBottom: '20px',
paddingBottom: '10px',
borderBottom: '2px solid #333',
})
const Title = define('Title', {
margin: 0,
fontSize: '24px',
fontWeight: 'bold',
})
const AppName = define('AppName', {
color: '#666',
fontSize: '18px',
marginTop: '5px',
})
const FileList = define('FileList', {
listStyle: 'none',
padding: 0,
margin: '20px 0',
border: '1px solid #ddd',
borderRadius: '4px',
overflow: 'hidden',
})
const FileItem = define('FileItem', {
padding: '10px 15px',
borderBottom: '1px solid #eee',
states: {
':last-child': {
borderBottom: 'none',
},
':hover': {
backgroundColor: '#f5f5f5',
},
}
})
const FileLink = define('FileLink', {
base: 'a',
textDecoration: 'none',
color: '#0066cc',
display: 'flex',
alignItems: 'center',
gap: '8px',
states: {
':hover': {
textDecoration: 'underline',
},
}
})
const CodeBlock = define('CodeBlock', {
margin: '20px 0',
border: '1px solid #ddd',
borderRadius: '4px',
overflow: 'auto',
selectors: {
'& pre': {
margin: 0,
padding: '15px',
overflow: 'auto',
whiteSpace: 'pre',
},
'& pre code': {
whiteSpace: 'pre',
fontFamily: 'monospace',
},
},
})
const CodeHeader = define('CodeHeader', {
padding: '10px 15px',
backgroundColor: '#f5f5f5',
borderBottom: '1px solid #ddd',
fontWeight: 'bold',
fontSize: '14px',
})
const Error = define('Error', {
color: '#d32f2f',
padding: '20px',
backgroundColor: '#ffebee',
borderRadius: '4px',
margin: '20px 0',
})
const BackLink = define('BackLink', {
base: 'a',
textDecoration: 'none',
color: '#0066cc',
display: 'inline-flex',
alignItems: 'center',
gap: '5px',
marginBottom: '15px',
states: {
':hover': {
textDecoration: 'underline',
},
},
})
app.get('/styles.css', c => c.text(stylesToCSS(), 200, {
'Content-Type': 'text/css; charset=utf-8',
}))
async function listFiles(appPath: string, subPath: string = '') {
const fullPath = join(appPath, subPath)
const entries = await readdir(fullPath, { withFileTypes: true })
const items = await Promise.all(
entries.map(async entry => {
const itemPath = join(fullPath, entry.name)
const stats = await stat(itemPath)
return {
name: entry.name,
isDirectory: entry.isDirectory(),
path: subPath ? `${subPath}/${entry.name}` : entry.name,
}
})
)
return items.sort((a, b) => {
if (a.isDirectory !== b.isDirectory) {
return a.isDirectory ? -1 : 1
}
return a.name.localeCompare(b.name)
})
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
function getLanguage(filename: string): string {
const ext = extname(filename).toLowerCase()
const langMap: Record<string, string> = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.json': 'json',
'.css': 'css',
'.html': 'html',
'.md': 'markdown',
'.sh': 'bash',
'.yml': 'yaml',
'.yaml': 'yaml',
}
return langMap[ext] || 'plaintext'
}
app.get('/', async c => {
const appName = c.req.query('app')
const filePath = c.req.query('file') || ''
if (!appName) {
return c.html(
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Code Browser</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<Container>
<Header>
<Title>Code Browser</Title>
</Header>
<Error>Please specify an app name with ?app=&lt;name&gt;</Error>
</Container>
</body>
</html>
)
}
const appPath = join(APPS_DIR, appName)
try {
await stat(appPath)
} catch {
return c.html(
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Code Browser</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<Container>
<Header>
<Title>Code Browser</Title>
</Header>
<Error>App "{appName}" not found</Error>
</Container>
</body>
</html>
)
}
const fullPath = join(appPath, filePath)
let stats
try {
stats = await stat(fullPath)
} catch {
return c.html(
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Code Browser</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<Container>
<Header>
<Title>Code Browser</Title>
</Header>
<Error>Path "{filePath}" not found in app "{appName}"</Error>
</Container>
</body>
</html>
)
}
if (stats.isFile()) {
const content = readFileSync(fullPath, 'utf-8')
const language = getLanguage(basename(fullPath))
const parentPath = filePath.split('/').slice(0, -1).join('/')
return c.html(
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{`${appName}/${filePath}`}</title>
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
</head>
<body>
<Container>
<Header>
<Title>Code Browser</Title>
<AppName>{appName}/{filePath}</AppName>
</Header>
<BackLink href={`/?app=${appName}${parentPath ? `&file=${parentPath}` : ''}`}>
Back
</BackLink>
<CodeBlock>
<CodeHeader>{basename(fullPath)}</CodeHeader>
<pre><code class={`language-${language}`}>{content}</code></pre>
</CodeBlock>
</Container>
<script dangerouslySetInnerHTML={{ __html: 'hljs.highlightAll();' }} />
</body>
</html>
)
}
const files = await listFiles(appPath, filePath)
const parentPath = filePath.split('/').slice(0, -1).join('/')
return c.html(
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{`${appName}${filePath ? `/${filePath}` : ''}`}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<Container>
<Header>
<Title>Code Browser</Title>
<AppName>{appName}{filePath ? `/${filePath}` : ''}</AppName>
</Header>
{filePath && (
<BackLink href={`/?app=${appName}${parentPath ? `&file=${parentPath}` : ''}`}>
Back
</BackLink>
)}
<FileList>
{files.map(file => (
<FileItem>
<FileLink href={`/?app=${appName}&file=${file.path}`}>
{file.isDirectory ? '📁' : '📄'}
<span>{file.name}</span>
</FileLink>
</FileItem>
))}
</FileList>
</Container>
</body>
</html>
)
})
export default app.defaults