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, '>') .replace(/"/g, '"') .replace(/'/g, ''') } function getLanguage(filename: string): string { const ext = extname(filename).toLowerCase() const langMap: Record = { '.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( Code Browser
Code Browser
Please specify an app name with ?app=<name>
) } const appPath = join(APPS_DIR, appName) try { await stat(appPath) } catch { return c.html( Code Browser
Code Browser
App "{appName}" not found
) } const fullPath = join(appPath, filePath) let stats try { stats = await stat(fullPath) } catch { return c.html( Code Browser
Code Browser
Path "{filePath}" not found in app "{appName}"
) } 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( {`${appName}/${filePath}`}
Code Browser {appName}/{filePath}
⬅️ Back {basename(fullPath)}
{content}