edgeone部署支持
This commit is contained in:
@@ -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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user