import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import path from 'node:path'; import fs from 'node:fs/promises'; const RENDER_ROOT = path.resolve(__dirname, '..', '.output-render'); const CACHE_ROOT = path.resolve(__dirname, '..', '.cache'); // Tiny dev plugin that exposes the .output-render directory at /render and // serves a /api/index endpoint enumerating maps and variants. Keeps the app // dependency-free of any external server. function renderRootPlugin() { return { name: 'psx-render-root', configureServer(server) { server.middlewares.use('/render', async (req, res, next) => { try { const url = decodeURIComponent(req.url.split('?')[0]); const filePath = path.join(RENDER_ROOT, url); if (!filePath.startsWith(RENDER_ROOT)) { res.statusCode = 403; res.end('Forbidden'); return; } const stat = await fs.stat(filePath); if (stat.isDirectory()) { const entries = await fs.readdir(filePath, { withFileTypes: true }); res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(entries.map((e) => ({ name: e.name, isDir: e.isDirectory() })))); return; } const ext = path.extname(filePath).toLowerCase(); const mime = ext === '.png' ? 'image/png' : ext === '.json' ? 'application/json' : ext === '.log' ? 'text/plain' : 'application/octet-stream'; res.setHeader('Content-Type', mime); res.setHeader('Cache-Control', 'no-cache'); const data = await fs.readFile(filePath); res.end(data); } catch (error) { if (error.code === 'ENOENT') { res.statusCode = 404; res.end('Not found'); return; } next(error); } }); server.middlewares.use('/api/index', async (req, res) => { try { const indexPath = path.join(RENDER_ROOT, 'index.json'); const data = await fs.readFile(indexPath, 'utf8'); res.setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'no-cache'); res.end(data); } catch (error) { res.statusCode = 500; res.end(JSON.stringify({ error: error.message })); } }); // Serve per-map sprite cache: /sprites//bundle_/frame_NNN.png // The exporter writes individual decoded sprite PNGs into // /.cache//sprites/, sharing the cache between // both the auto and region01 variants of the same map (bundle decoding // is invariant to the record-set choice). server.middlewares.use('/sprites', async (req, res, next) => { try { const url = decodeURIComponent(req.url.split('?')[0]); // url like "/L2/bundle_00085c40/frame_000.png" // map to "/L2/sprites/bundle_00085c40/frame_000.png" const segments = url.split('/').filter(Boolean); if (segments.length < 2) { res.statusCode = 404; res.end('Not found'); return; } const [mapStem, ...rest] = segments; const filePath = path.join(CACHE_ROOT, mapStem, 'sprites', ...rest); if (!filePath.startsWith(CACHE_ROOT)) { res.statusCode = 403; res.end('Forbidden'); return; } const data = await fs.readFile(filePath); res.setHeader('Content-Type', 'image/png'); res.setHeader('Cache-Control', 'no-cache'); res.end(data); } catch (error) { if (error.code === 'ENOENT') { res.statusCode = 404; res.end('Not found'); return; } next(error); } }); }, }; } export default defineConfig({ plugins: [vue(), renderRootPlugin()], server: { port: 5180, strictPort: false, }, });