File size: 3,660 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | import fs from 'node:fs/promises'
import path from 'node:path'
const CACHEABLE_ASSET_RE = /^\/assets\//
const MIME_TYPES: Record<string, string> = {
'.css': 'text/css; charset=utf-8',
'.gif': 'image/gif',
'.html': 'text/html; charset=utf-8',
'.ico': 'image/x-icon',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.map': 'application/json; charset=utf-8',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.txt': 'text/plain; charset=utf-8',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
}
export async function handleStaticH5Request(req: Request, url: URL): Promise<Response | null> {
if (req.method !== 'GET' && req.method !== 'HEAD') {
return null
}
const distDir = await resolveH5DistDir()
if (!distDir) {
return null
}
const filePath = await resolveStaticFilePath(distDir, url.pathname)
if (!filePath) {
return null
}
const headers = new Headers({
'Content-Type': contentTypeForPath(filePath),
'Cache-Control': CACHEABLE_ASSET_RE.test(url.pathname)
? 'public, max-age=31536000, immutable'
: 'no-store',
})
if (req.method === 'HEAD') {
const stat = await fs.stat(filePath)
headers.set('Content-Length', String(stat.size))
return new Response(null, { status: 200, headers })
}
return new Response(Bun.file(filePath), { status: 200, headers })
}
async function resolveH5DistDir(): Promise<string | null> {
const candidates = [
process.env.CLAUDE_H5_DIST_DIR,
process.env.CLAUDE_APP_ROOT
? path.resolve(process.env.CLAUDE_APP_ROOT, '..', 'Resources', '_up_', 'dist')
: undefined,
process.env.CLAUDE_APP_ROOT
? path.resolve(process.env.CLAUDE_APP_ROOT, '..', 'Resources', 'dist')
: undefined,
process.env.CLAUDE_APP_ROOT
? path.resolve(process.env.CLAUDE_APP_ROOT, 'dist')
: undefined,
path.resolve(process.cwd(), 'desktop', 'dist'),
path.resolve(process.cwd(), 'dist'),
].filter((candidate): candidate is string => !!candidate)
for (const candidate of candidates) {
try {
const stat = await fs.stat(path.join(candidate, 'index.html'))
if (stat.isFile()) {
return path.resolve(candidate)
}
} catch {
// Try the next candidate.
}
}
return null
}
async function resolveStaticFilePath(distDir: string, pathname: string): Promise<string | null> {
const requested = containedPath(distDir, pathname)
if (!requested) {
return null
}
const direct = await fileIfExists(requested)
if (direct) {
return direct
}
const nestedIndex = await fileIfExists(path.join(requested, 'index.html'))
if (nestedIndex) {
return nestedIndex
}
if (path.extname(requested)) {
return null
}
return fileIfExists(path.join(distDir, 'index.html'))
}
function containedPath(root: string, pathname: string): string | null {
let decoded: string
try {
decoded = decodeURIComponent(pathname)
} catch {
return null
}
const relativePath = decoded.replace(/^\/+/, '') || 'index.html'
const candidate = path.resolve(root, relativePath)
const relativeToRoot = path.relative(root, candidate)
if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
return null
}
return candidate
}
async function fileIfExists(filePath: string): Promise<string | null> {
try {
const stat = await fs.stat(filePath)
return stat.isFile() ? filePath : null
} catch {
return null
}
}
function contentTypeForPath(filePath: string): string {
return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream'
}
|