fuzzyworld

nuts / lava / files / vite.config.ts

Branch lava · default branch · all branches

vite.config.ts

4.5 KB · history · blame

1import { defineConfig, type Plugin } from 'vite'
2import { qrcode } from 'vite-plugin-qrcode'
3import basicSsl from '@vitejs/plugin-basic-ssl'
4import type { IncomingMessage, ServerResponse } from 'node:http'
5import { execFileSync } from 'node:child_process'
6import { readdirSync, readFileSync, existsSync, mkdirSync } from 'node:fs'
7import { join, dirname } from 'node:path'
8import { createRequire } from 'node:module'
9import { tmpdir } from 'node:os'
10
11const require = createRequire(import.meta.url)
12// eslint-disable-next-line @typescript-eslint/no-require-imports
13const ffmpeg: string = require('ffmpeg-static')
14
15const SOUNDS_SRC = new URL('./src/sounds', import.meta.url).pathname
16
17// The specific files sound.ts references (category prefix only, 000–002 appended)
18const SOUND_CATEGORIES = [
19 'impactWood_medium',
20 'impactGlass_medium',
21 'impactMetal_heavy',
22 'impactMining',
23 'impactSoft_heavy',
24 'impactSoft_medium',
25 'impactPunch_heavy',
26 'impactTin_medium',
27]
28
29function oggToMp3(src: string, dest: string): void {
30 mkdirSync(dirname(dest), { recursive: true })
31 execFileSync(ffmpeg, ['-i', src, '-q:a', '4', '-y', dest], { stdio: 'pipe' })
32}
33
34function soundFiles(): string[] {
35 return SOUND_CATEGORIES.flatMap(cat =>
36 [0, 1, 2].map(v => `${cat}_${String(v).padStart(3, '0')}`)
37 )
38}
39
40function sounds(): Plugin {
41 // Temp dir for dev-server cache
42 const cache = join(tmpdir(), 'nuts-sounds')
43 mkdirSync(cache, { recursive: true })
44
45 return {
46 name: 'sounds',
47
48 // Build: convert + emit each MP3 as an asset
49 async generateBundle() {
50 for (const name of soundFiles()) {
51 const src = join(SOUNDS_SRC, `${name}.ogg`)
52 if (!existsSync(src)) continue
53 const tmp = join(cache, `${name}.mp3`)
54 if (!existsSync(tmp)) oggToMp3(src, tmp)
55 this.emitFile({
56 type: 'asset',
57 fileName: `sounds/${name}.mp3`,
58 source: readFileSync(tmp),
59 })
60 }
61 },
62
63 // Dev: convert on first request, then serve from cache
64 configureServer(server) {
65 server.middlewares.use('/sounds/', (req: IncomingMessage, res: ServerResponse) => {
66 const mp3Name = (req.url ?? '').replace(/^\//, '')
67 if (!mp3Name.endsWith('.mp3')) { res.writeHead(404).end(); return }
68 const oggName = mp3Name.replace(/\.mp3$/, '.ogg')
69 const src = join(SOUNDS_SRC, oggName)
70 const dest = join(cache, mp3Name)
71 if (!existsSync(src)) { res.writeHead(404).end(); return }
72 try {
73 if (!existsSync(dest)) oggToMp3(src, dest)
74 const data = readFileSync(dest)
75 res.writeHead(200, { 'Content-Type': 'audio/mpeg', 'Content-Length': data.length })
76 res.end(data)
77 } catch (e) {
78 res.writeHead(500).end(String(e))
79 }
80 })
81 },
82 }
83}
84
85function remoteLog(): Plugin {
86 return {
87 name: 'remote-log',
88 configureServer(server) {
89 server.middlewares.use(
90 '/__log',
91 (req: IncomingMessage, res: ServerResponse) => {
92 if (req.method !== 'POST') {
93 res.writeHead(405).end()
94 return
95 }
96 let body = ''
97 req.setEncoding('utf8')
98 req.on('data', (chunk: string) => { body += chunk })
99 req.on('end', () => {
100 try {
101 const { level, msg } = JSON.parse(body) as { level: string; msg: string }
102 const prefix = '\x1b[36m[web]\x1b[0m'
103 if (level === 'error') {
104 server.config.logger.error(`${prefix} ${msg}`)
105 } else {
106 server.config.logger.info(`${prefix} ${msg}`)
107 }
108 } catch { /* malformed – ignore */ }
109 res.writeHead(204).end()
110 })
111 }
112 )
113 },
114 }
115}
116
117export default defineConfig({
118 base: './',
119 build: {
120 outDir: 'dist',
121 },
122 server: {
123 host: true, // listen on 0.0.0.0 so the phone can reach it
124 },
125 plugins: [sounds(), remoteLog(), basicSsl(), qrcode()],
126})