File size: 4,063 Bytes
e1bb50d
 
 
e18c81d
e1bb50d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2a78c4
e1bb50d
 
 
 
 
 
 
a260d88
 
e1bb50d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2a78c4
 
 
 
 
 
 
e1bb50d
 
 
 
 
 
 
 
 
 
d530cd8
e1bb50d
d530cd8
e1bb50d
d530cd8
 
e1bb50d
 
 
d530cd8
 
 
 
 
 
 
 
 
 
e1bb50d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Static file serving for the Friend VRM frontend.
 *
 * Serves the built Vite/React frontend from src/components/friend/frontend/dist/ at /friend/*.
 * All asset files (VRM, FBX, VRMA, VMD, MP3) are in the dist root and served
 * under /friend/<filename>.
 */
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const CACHEABLE_RE = /^\/friend\/assets\//
const MIME_TYPES: Record<string, string> = {
  '.vrm': 'application/octet-stream',
  '.fbx': 'application/octet-stream',
  '.vrma': 'application/octet-stream',
  '.vmd': 'application/octet-stream',
  '.mp3': 'audio/mpeg',
  '.css': 'text/css; charset=utf-8',
  '.js': 'text/javascript; charset=utf-8',
  '.mjs': 'text/javascript; charset=utf-8',
  '.html': 'text/html; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.png': 'image/png',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
  '.woff': 'font/woff',
  '.woff2': 'font/woff2',
  '.onnx': 'application/octet-stream',
  '.wasm': 'application/wasm',
}

export async function handleFriendStaticRequest(req: Request, url: URL): Promise<Response | null> {
  if (req.method !== 'GET' && req.method !== 'HEAD') {
    return null
  }

  // Only handle /friend/* paths
  if (!url.pathname.startsWith('/friend/') && url.pathname !== '/friend') {
    return null
  }

  const distDir = await resolveFriendDistDir()
  if (!distDir) {
    return null
  }

  // Map /friend/* to dist root
  // /friend/              → index.html
  // /friend/model1.vrm    → dist/model1.vrm
  // /friend/assets/foo.js → dist/assets/foo.js
  const relativePath = url.pathname.replace(/^\/friend\/?/, '') || 'index.html'
  const filePath = await resolveFriendFilePath(distDir, relativePath)
  if (!filePath) {
    return null
  }

  const headers = new Headers({
    'Content-Type': contentTypeForPath(filePath),
    'Cache-Control': CACHEABLE_RE.test(url.pathname)
      ? 'public, max-age=31536000, immutable'
      : 'no-store',
  })

  // Enable cross-origin isolation for SharedArrayBuffer support,
  // needed by onnxruntime-web WASM threading (used by @ricky0123/vad-web).
  if (relativePath === 'index.html') {
    headers.set('Cross-Origin-Opener-Policy', 'same-origin')
    headers.set('Cross-Origin-Embedder-Policy', 'require-corp')
  }

  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 resolveFriendDistDir(): Promise<string | null> {
  // Method 1: relative to source file (works in dev mode / server subprocess)
  const _srcDir = path.dirname(fileURLToPath(import.meta.url))
  const candidate1 = path.resolve(_srcDir, '..', '..', 'src', 'components', 'friend', 'frontend', 'dist')
  try {
    const stat = await fs.stat(path.join(candidate1, 'index.html'))
    if (stat.isFile()) return candidate1
  } catch {
    // Not found
  }

  // Method 2: relative to cwd (works in compiled binary where import.meta.url is virtual)
  const candidate2 = path.resolve(process.cwd(), 'src', 'components', 'friend', 'frontend', 'dist')
  try {
    const stat = await fs.stat(path.join(candidate2, 'index.html'))
    if (stat.isFile()) return candidate2
  } catch {
    // Not found
  }

  return null
}

async function resolveFriendFilePath(distDir: string, relativePath: string): Promise<string | null> {
  if (!relativePath) return null

  const decoded = decodeURIComponent(relativePath)
  const candidate = path.resolve(distDir, decoded)
  const relativeToRoot = path.relative(distDir, candidate)
  if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
    return null
  }

  try {
    const stat = await fs.stat(candidate)
    return stat.isFile() ? candidate : null
  } catch {
    return null
  }
}

function contentTypeForPath(filePath: string): string {
  return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream'
}