99 lines
3.2 KiB
JavaScript
99 lines
3.2 KiB
JavaScript
// Local development server that mirrors the Vercel deployment.
|
|||
|
|
//
|
||
|
|
// `npx serve` cannot proxy, and enka.network sends no CORS headers, so the
|
||
|
|
// Enka import button would always fail locally without this. Production uses
|
||
|
|
// the /api/enka/:uid rewrite in vercel.json instead.
|
||
|
|
//
|
||
|
|
// Usage: npm run dev:proxy (or node scripts/dev-server.cjs)
|
||
|
|
const http = require('node:http');
|
||
|
|
const fs = require('node:fs');
|
||
|
|
const path = require('node:path');
|
||
|
|
|
||
|
|
const root = path.resolve(__dirname, '..');
|
||
|
|
const port = Number(process.env.PORT) || 5173;
|
||
|
|
const ENKA_ORIGIN = 'https://enka.network';
|
||
|
|
const ENKA_PATH = /^\/api\/enka\/(\d{1,12})\/?$/;
|
||
|
|
|
||
|
|
const MIME_TYPES = {
|
||
|
|
'.html': 'text/html; charset=utf-8',
|
||
|
|
'.js': 'text/javascript; charset=utf-8',
|
||
|
|
'.mjs': 'text/javascript; charset=utf-8',
|
||
|
|
'.cjs': 'text/javascript; charset=utf-8',
|
||
|
|
'.css': 'text/css; charset=utf-8',
|
||
|
|
'.json': 'application/json; charset=utf-8',
|
||
|
|
'.svg': 'image/svg+xml',
|
||
|
|
'.png': 'image/png',
|
||
|
|
'.jpg': 'image/jpeg',
|
||
|
|
'.jpeg': 'image/jpeg',
|
||
|
|
'.webp': 'image/webp',
|
||
|
|
'.gif': 'image/gif',
|
||
|
|
'.ico': 'image/x-icon',
|
||
|
|
'.woff': 'font/woff',
|
||
|
|
'.woff2': 'font/woff2',
|
||
|
|
'.ttf': 'font/ttf',
|
||
|
|
'.txt': 'text/plain; charset=utf-8',
|
||
|
|
'.md': 'text/markdown; charset=utf-8'
|
||
|
|
};
|
||
|
|
|
||
|
|
function sendJson(res, status, payload) {
|
||
|
|
res.writeHead(status, {
|
||
|
|
'Content-Type': 'application/json; charset=utf-8',
|
||
|
|
'Cache-Control': 'no-store'
|
||
|
|
});
|
||
|
|
res.end(JSON.stringify(payload));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function proxyEnka(uid, res) {
|
||
|
|
try {
|
||
|
|
const upstream = await fetch(`${ENKA_ORIGIN}/api/hsr/uid/${uid}`, {
|
||
|
|
headers: { 'User-Agent': 'GaCy-Srtool-dev' }
|
||
|
|
});
|
||
|
|
const body = await upstream.text();
|
||
|
|
res.writeHead(upstream.status, {
|
||
|
|
'Content-Type': 'application/json; charset=utf-8',
|
||
|
|
'Cache-Control': 'no-store'
|
||
|
|
});
|
||
|
|
res.end(body);
|
||
|
|
} catch (err) {
|
||
|
|
sendJson(res, 502, { error: 'enka_proxy_failed', message: err.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function serveStatic(pathname, res) {
|
||
|
|
const decoded = decodeURIComponent(pathname);
|
||
|
|
const relative = decoded === '/' ? 'index.html' : decoded.replace(/^\/+/, '');
|
||
|
|
const target = path.resolve(root, relative);
|
||
|
|
if (!target.startsWith(root + path.sep) && target !== path.join(root, 'index.html')) {
|
||
|
|
sendJson(res, 403, { error: 'forbidden' });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
fs.stat(target, (err, stats) => {
|
||
|
|
if (err || !stats.isFile()) {
|
||
|
|
sendJson(res, 404, { error: 'not_found', path: relative });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const type = MIME_TYPES[path.extname(target).toLowerCase()] || 'application/octet-stream';
|
||
|
|
res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-store' });
|
||
|
|
fs.createReadStream(target).pipe(res);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const server = http.createServer((req, res) => {
|
||
|
|
const requestUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
||
|
|
const enkaMatch = requestUrl.pathname.match(ENKA_PATH);
|
||
|
|
if (enkaMatch) {
|
||
|
|
proxyEnka(enkaMatch[1], res);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (requestUrl.pathname.startsWith('/api/')) {
|
||
|
|
sendJson(res, 404, { error: 'not_found', path: requestUrl.pathname });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
serveStatic(requestUrl.pathname, res);
|
||
|
|
});
|
||
|
|
|
||
|
|
server.listen(port, () => {
|
||
|
|
console.log(`GaCy-Srtool dev server: http://localhost:${port}`);
|
||
|
|
console.log(`Enka proxy: http://localhost:${port}/api/enka/<uid>`);
|
||
|
|
});
|