code app
This commit is contained in:
parent
f12854fc04
commit
664861a0fb
1
apps/code/.npmrc
Normal file
1
apps/code/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
registry=https://npm.nose.space
|
||||
43
apps/code/bun.lock
Normal file
43
apps/code/bun.lock
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "code",
|
||||
"dependencies": {
|
||||
"@because/forge": "*",
|
||||
"@because/howl": "*",
|
||||
"@because/hype": "*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.9.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@because/forge": ["@because/forge@0.0.1", "https://npm.nose.space/@because/forge/-/forge-0.0.1.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-QS5CK51gcWma91i4uECWe4HPJeNHcE+Af4SQHOcfEovyzOEa7VOTAjei+jIWr2i+abGWqQCEC9wIuFgPgyr2Bg=="],
|
||||
|
||||
"@because/howl": ["@because/howl@0.0.2", "https://npm.nose.space/@because/howl/-/howl-0.0.2.tgz", { "dependencies": { "lucide-static": "^0.555.0" }, "peerDependencies": { "@because/forge": "*", "typescript": "^5" } }, "sha512-Z4okzEa282LKkBk9DQwEUU6FT+PeThfQ6iQAY41LIEjs8B2kfXRZnbWLs7tgpwCfYORxb0RO4Hr7KiyEqnfTvQ=="],
|
||||
|
||||
"@because/hype": ["@because/hype@0.0.1", "https://npm.nose.space/@because/hype/-/hype-0.0.1.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-i92DNUXJOwt3J8dN1x8sh7i86blelcTCk8XDpwD839Ic8oe710lkDSVXJ7xYZb/i8YtzGhRg+L6eXDhaRiU2Pw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.8", "https://npm.nose.space/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.1.0", "https://npm.nose.space/@types/node/-/node-25.1.0.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.8", "https://npm.nose.space/bun-types/-/bun-types-1.3.8.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||
|
||||
"hono": ["hono@4.11.7", "https://npm.nose.space/hono/-/hono-4.11.7.tgz", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "https://npm.nose.space/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"lucide-static": ["lucide-static@0.555.0", "https://npm.nose.space/lucide-static/-/lucide-static-0.555.0.tgz", {}, "sha512-FMMaYYsEYsUA6xlEzIMoKEV3oGnxIIvAN+AtLmYXvlTJptJTveJjVBQwvtA/zZLrD6KLEu89G95dQYlhivw5jQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "https://npm.nose.space/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "https://npm.nose.space/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
}
|
||||
}
|
||||
1
apps/code/index.tsx
Normal file
1
apps/code/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './src/server'
|
||||
25
apps/code/package.json
Normal file
25
apps/code/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "code",
|
||||
"module": "index.tsx",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"toes": "bun run --watch index.tsx",
|
||||
"start": "bun toes",
|
||||
"dev": "bun run --hot index.tsx"
|
||||
},
|
||||
"toes": {
|
||||
"icon": "🖥️"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@because/hype": "*",
|
||||
"@because/forge": "*",
|
||||
"@because/howl": "*"
|
||||
}
|
||||
}
|
||||
1
apps/code/src/pages/index.tsx
Normal file
1
apps/code/src/pages/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default () => <h1>code</h1>
|
||||
327
apps/code/src/server/index.tsx
Normal file
327
apps/code/src/server/index.tsx
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
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, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
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=<name></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
|
||||
29
apps/code/tsconfig.json
Normal file
29
apps/code/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"$*": ["src/server/*"],
|
||||
"#*": ["src/client/*"],
|
||||
"@*": ["src/shared/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user