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'
}