Crusader_Decomp/psx-map-exporter/viewer/vite.config.js
2026-04-18 16:34:35 +02:00

110 lines
4 KiB
JavaScript

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/<map>/bundle_<hex>/frame_NNN.png
// The exporter writes individual decoded sprite PNGs into
// <projectRoot>/.cache/<mapStem>/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 "<CACHE_ROOT>/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,
},
});