edgeone部署支持

This commit is contained in:
mtvpls
2026-06-18 11:25:07 +08:00
parent 5e78189e69
commit 8997228be4
7 changed files with 4818 additions and 43 deletions
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env node
import { createServer } from 'node:http';
import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
const root = process.cwd();
const assetsDir = path.join(root, '.edgeone', 'assets');
const handlerPath = path.join(root, '.edgeone', 'cloud-functions', 'ssr-node', 'handler.js');
const defaultPort = Number(process.env.PORT || process.env.EDGEONE_LOCAL_PORT || 8088);
function loadDotenv(file) {
if (!existsSync(file)) return;
const content = readFileSync(file, 'utf-8');
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const normalized = line.startsWith('export ') ? line.slice(7).trim() : line;
const idx = normalized.indexOf('=');
if (idx <= 0) continue;
const key = normalized.slice(0, idx).trim();
let value = normalized.slice(idx + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
if (!(key in process.env)) process.env[key] = value;
}
}
loadDotenv(path.join(root, '.env'));
loadDotenv(path.join(root, '.env.local'));
process.env.NODE_ENV ||= 'production';
process.env.BUILD_TARGET = 'edgeone';
process.env.EDGEONE_PAGES = '1';
if (!existsSync(handlerPath)) {
console.error('未找到 EdgeOne SSR handler', handlerPath);
console.error('请先运行:pnpm build:edgeone');
process.exit(1);
}
const { default: edgeoneHandler } = await import(pathToFileURL(handlerPath).href);
const mime = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.map': 'application/json; charset=utf-8',
'.txt': 'text/plain; charset=utf-8',
'.xml': 'application/xml; charset=utf-8',
'.jar': 'application/java-archive',
};
function safeAssetPath(urlPath) {
let decoded;
try {
decoded = decodeURIComponent(urlPath.split('?')[0]);
} catch {
return null;
}
const relative = decoded.replace(/^\/+/, '') || 'index.html';
const full = path.normalize(path.join(assetsDir, relative));
if (!full.startsWith(assetsDir)) return null;
if (existsSync(full) && statSync(full).isFile()) return full;
if (existsSync(full + '.html') && statSync(full + '.html').isFile()) return full + '.html';
const indexFile = path.join(full, 'index.html');
if (existsSync(indexFile) && statSync(indexFile).isFile()) return indexFile;
return null;
}
function sendStatic(req, res, file) {
const ext = path.extname(file).toLowerCase();
res.statusCode = 200;
res.setHeader('content-type', mime[ext] || 'application/octet-stream');
if (req.url?.startsWith('/_next/static/')) {
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
}
createReadStream(file).pipe(res);
}
function toEdgeOneRequest(req) {
const headers = {};
for (const [key, value] of Object.entries(req.headers)) {
headers[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value ?? '');
}
headers['x-forwarded-proto'] ||= 'http';
headers.host ||= `localhost:${defaultPort}`;
return {
method: req.method || 'GET',
url: req.url || '/',
headers,
body: ['GET', 'HEAD'].includes(req.method || 'GET') ? undefined : req,
on: req.on.bind(req),
read: req.read.bind(req),
};
}
async function sendFetchResponse(res, response) {
res.statusCode = response.status;
response.headers.forEach((value, key) => res.setHeader(key, value));
if (!response.body) {
res.end();
return;
}
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!res.write(Buffer.from(value))) {
await new Promise((resolve) => res.once('drain', resolve));
}
}
res.end();
} catch (error) {
res.destroy(error);
}
}
const server = createServer(async (req, res) => {
try {
const pathname = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`).pathname;
const asset = safeAssetPath(pathname);
if (asset) return sendStatic(req, res, asset);
const request = toEdgeOneRequest(req);
const response = await edgeoneHandler(request, {
env: process.env,
waitUntil: (promise) => Promise.resolve(promise).catch((err) => console.error('[waitUntil]', err)),
});
await sendFetchResponse(res, response);
} catch (error) {
console.error('[edgeone-local-preview]', error);
res.statusCode = 500;
res.setHeader('content-type', 'text/plain; charset=utf-8');
res.end(error instanceof Error ? error.stack || error.message : String(error));
}
});
server.listen(defaultPort, () => {
console.log(`EdgeOne local preview (no login) running at http://localhost:${defaultPort}`);
});