chore: release v1.0.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env node
|
||||
// dev-proxy.js — 本地开发反向代理
|
||||
// localhost:3000 → 营销站 :3001 / Flutter Web :8888
|
||||
// /app/* 路由到 Flutter,其余路由到 Eleventy
|
||||
// 自动把 Flutter index.html 里的 base href="/" 改写为 /app/,使静态资源请求正确走代理
|
||||
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
|
||||
const PROXY_PORT = parseInt(process.env.PROXY_PORT || '3000', 10);
|
||||
const ELEVENTY_PORT = parseInt(process.env.ELEVENTY_PORT || '3001', 10);
|
||||
const FLUTTER_PORT = parseInt(process.env.FLUTTER_PORT || '9090', 10);
|
||||
const BACKEND_PORT = parseInt(process.env.BACKEND_PORT || '8080', 10);
|
||||
|
||||
function isFlutter(url) {
|
||||
return url === '/app' || url.startsWith('/app/');
|
||||
}
|
||||
|
||||
function isBackend(url) {
|
||||
return url.startsWith('/api/') || url === '/health' || url.startsWith('/images/');
|
||||
}
|
||||
|
||||
function stripApp(url) {
|
||||
return url.slice(4) || '/';
|
||||
}
|
||||
|
||||
function proxyHttp(req, res, port, path) {
|
||||
const opts = {
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path,
|
||||
method: req.method,
|
||||
headers: { ...req.headers, host: `localhost:${port}` },
|
||||
};
|
||||
|
||||
const pr = http.request(opts, (upstream) => {
|
||||
const ct = upstream.headers['content-type'] || '';
|
||||
|
||||
if (port === FLUTTER_PORT && ct.includes('text/html')) {
|
||||
let body = '';
|
||||
upstream.setEncoding('utf8');
|
||||
upstream.on('data', (c) => { body += c; });
|
||||
upstream.on('end', () => {
|
||||
// rewrite base href so Flutter asset requests stay under /app/
|
||||
body = body.replace(/(<base\s+href=")[^"]*(")/i, '$1/app/$2');
|
||||
const headers = { ...upstream.headers };
|
||||
headers['content-length'] = Buffer.byteLength(body);
|
||||
delete headers['transfer-encoding'];
|
||||
res.writeHead(upstream.statusCode, headers);
|
||||
res.end(body);
|
||||
});
|
||||
} else {
|
||||
res.writeHead(upstream.statusCode, upstream.headers);
|
||||
upstream.pipe(res, { end: true });
|
||||
}
|
||||
});
|
||||
|
||||
pr.on('error', () => res.writeHead(502).end('upstream error'));
|
||||
req.pipe(pr, { end: true });
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (isFlutter(req.url)) {
|
||||
proxyHttp(req, res, FLUTTER_PORT, stripApp(req.url));
|
||||
} else if (isBackend(req.url)) {
|
||||
proxyHttp(req, res, BACKEND_PORT, req.url);
|
||||
} else {
|
||||
proxyHttp(req, res, ELEVENTY_PORT, req.url);
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket 透传(Eleventy 热重载 / Flutter DevTools)
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
const port = isFlutter(req.url) ? FLUTTER_PORT
|
||||
: isBackend(req.url) ? BACKEND_PORT
|
||||
: ELEVENTY_PORT;
|
||||
const path = isFlutter(req.url) ? stripApp(req.url) : req.url;
|
||||
|
||||
const conn = net.createConnection(port, 'localhost', () => {
|
||||
conn.write(`${req.method} ${path} HTTP/1.1\r\n`);
|
||||
for (const [k, v] of Object.entries(req.headers)) {
|
||||
conn.write(`${k}: ${v}\r\n`);
|
||||
}
|
||||
conn.write('\r\n');
|
||||
if (head && head.length) conn.write(head);
|
||||
socket.pipe(conn);
|
||||
conn.pipe(socket);
|
||||
});
|
||||
|
||||
conn.on('error', () => socket.destroy());
|
||||
socket.on('error', () => conn.destroy());
|
||||
});
|
||||
|
||||
server.listen(PROXY_PORT, () => {
|
||||
console.log(`[proxy] http://localhost:${PROXY_PORT}`);
|
||||
console.log(` / → Eleventy :${ELEVENTY_PORT}`);
|
||||
console.log(` /app/ → Flutter Web :${FLUTTER_PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user