@edgeone/nuxt-pages
Version:
A professional deployment package that seamlessly deploys your Nuxt 3/4 applications to Tencent Cloud EdgeOne platform with optimized performance and intelligent caching.
511 lines (446 loc) • 15.9 kB
JavaScript
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { readFileSync, existsSync, statSync } from 'fs';
import { extname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Static assets directory
const ASSET_DIR = resolve(__dirname, '../assets');
// MIME type mapping
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain',
'.xml': 'application/xml'
};
/**
* Get the MIME type of a file
*/
function getMimeType(filePath) {
const ext = extname(filePath).toLowerCase();
return MIME_TYPES[ext] || 'application/octet-stream';
}
/**
* 解析IPX参数
*/
function parseIPXParams(paramString) {
const params = {};
if (!paramString || paramString === '_') {
return params;
}
// IPX参数格式: w_800&h_600&q_80&f_webp
const paramPairs = paramString.split('&');
for (const pair of paramPairs) {
if (pair.includes('_')) {
const [key, value] = pair.split('_', 2);
switch (key) {
case 'w':
params.width = parseInt(value);
break;
case 'h':
params.height = parseInt(value);
break;
case 's':
// 尺寸格式: s_800x600
if (value.includes('x')) {
const [w, h] = value.split('x');
params.width = parseInt(w);
params.height = parseInt(h);
}
break;
case 'q':
params.quality = parseInt(value);
break;
case 'f':
params.format = value;
break;
case 'fit':
params.fit = value;
break;
case 'b':
case 'bg':
params.background = value;
break;
case 'blur':
params.blur = parseInt(value);
break;
}
}
}
return params;
}
/**
* 处理IPX图片请求 - 简化版本,直接调用Nitro的IPX处理器
*/
async function processIPXImage(ipxPath) {
try {
const app = await getNitroApp();
const fullPath = `/_ipx/${ipxPath}`;
// 检查本地文件是否存在,如果是本地文件且存在,直接处理
if (!ipxPath.includes('http')) {
const pathParts = ipxPath.split('/');
let filePath = '';
let params = {};
// 找到实际的文件路径(跳过参数)
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i];
if (part.includes('.')) {
// 找到文件扩展名,这是文件路径的开始
filePath = pathParts.slice(i).join('/');
// 前面的部分是参数
if (i > 0) {
const paramString = pathParts.slice(0, i).join('&');
params = parseIPXParams(paramString);
}
break;
}
}
const localFilePath = resolve(ASSET_DIR, filePath);
// console.log(`Checking local file: ${localFilePath}`);
// console.log(`File exists: ${existsSync(localFilePath)}`);
// 如果是本地文件且存在,直接返回文件内容(暂时跳过IPX处理)
if (existsSync(localFilePath)) {
const fileContent = readFileSync(localFilePath);
const mimeType = getMimeType(localFilePath);
return {
statusCode: 200,
headers: {
'Content-Type': mimeType,
'Content-Length': fileContent.length.toString(),
'Cache-Control': 'public, max-age=31536000',
'from-server': 'true'
},
body: fileContent
};
}
}
const response = await app.localCall({
url: fullPath,
method: 'GET',
headers: {
'accept': 'image/*'
},
body: ''
});
if (!response || response.status !== 200) {
console.log('IPX processing failed, status code:', response?.status);
return null;
}
// 处理响应体
let responseBody;
if (response.body) {
if (Buffer.isBuffer(response.body)) {
responseBody = response.body;
} else if (typeof response.body === 'string') {
// 对于图片数据,使用latin1编码保持二进制完整性
responseBody = Buffer.from(response.body, 'latin1');
} else if (response.body && typeof response.body.getReader === 'function') {
// 处理ReadableStream
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(Buffer.from(value));
}
responseBody = Buffer.concat(chunks);
} else if (response.body instanceof Uint8Array) {
responseBody = Buffer.from(response.body);
} else {
return null;
}
} else if (response._data) {
if (Buffer.isBuffer(response._data)) {
responseBody = response._data;
} else if (response._data instanceof Uint8Array) {
responseBody = Buffer.from(response._data);
} else if (typeof response._data === 'string') {
responseBody = Buffer.from(response._data, 'latin1');
} else {
console.log('Unknown _data format:', typeof response._data);
return null;
}
} else {
console.log('No image data found in IPX response');
return null;
}
if (!responseBody || responseBody.length === 0) {
console.log('Image data is empty after IPX processing');
return null;
}
// 获取内容类型
let contentType = 'image/jpeg';
if (response.headers) {
const headers = response.headers instanceof Headers ? response.headers : new Headers(response.headers);
contentType = headers.get('content-type') || contentType;
}
// 验证图片数据完整性
const isValidImage = responseBody.length > 0 && (
responseBody[0] === 0xFF || // JPEG
(responseBody[0] === 0x89 && responseBody[1] === 0x50) || // PNG
(responseBody[0] === 0x47 && responseBody[1] === 0x49) // GIF
);
// console.log(`IPX processing successful: size=${responseBody.length}bytes, type=${contentType}, valid image=${isValidImage}`);
// console.log(`Image header bytes: ${responseBody.slice(0, 10).toString('hex')}`);
return {
statusCode: 200,
headers: {
'Content-Type': contentType,
'Content-Length': responseBody.length.toString(),
'Cache-Control': 'public, max-age=31536000', // 1年缓存
'Accept-Ranges': 'bytes',
'Access-Control-Allow-Origin': '*',
'from-server': 'true'
},
body: responseBody
};
} catch (error) {
console.error('IPX processing error:', error);
console.error('Error stack:', error.stack);
return null;
}
}
/**
* Handle static file requests
*/
async function handleStaticFile(url) {
try {
// Remove query parameters
let cleanUrl = url.split('?')[0];
// Handle IPX image processing paths from @nuxt/image
// 直接使用IPX处理图片,而不是重定向
if (cleanUrl.startsWith('/_ipx/')) {
const ipxPath = cleanUrl.slice(6); // Remove '/_ipx/'
return processIPXImage(ipxPath);
}
// Handle EdgeOne SSR functions IPX paths
// 直接使用IPX处理图片
if(cleanUrl.includes('-ssr-functions/_ipx/')) {
// 提取IPX路径部分
const ipxIndex = cleanUrl.indexOf('_ipx/');
if (ipxIndex !== -1) {
const ipxPath = cleanUrl.slice(ipxIndex + 5); // Remove '_ipx/'
return processIPXImage(ipxPath);
}
return null;
}
const possiblePaths = [];
// Direct file path
const directPath = resolve(ASSET_DIR, cleanUrl.startsWith('/') ? cleanUrl.slice(1) : cleanUrl);
possiblePaths.push(directPath);
// Try each possible path
for (const filePath of possiblePaths) {
// Security check: ensure file is within asset directory
if (!filePath.startsWith(ASSET_DIR)) {
continue;
}
if (existsSync(filePath) && statSync(filePath).isFile()) {
const content = readFileSync(filePath);
const mimeType = getMimeType(filePath);
return {
statusCode: 200,
headers: {
'Content-Type': mimeType,
'Content-Length': content.length.toString(),
'Cache-Control': 'public, max-age=31536000' // 1 year cache
},
body: content
};
}
}
} catch (error) {
console.error('Static file error:', error);
}
return null;
}
/**
* Lazy load Nitro application
*/
let nitroApp = null;
async function getNitroApp() {
if (!nitroApp) {
// Set environment variables to prevent automatic server startup
process.env.NITRO_PORT = '';
process.env.PORT = '';
// Set correct static assets path
process.env.NITRO_PUBLIC_DIR = ASSET_DIR;
const { j: useNitroApp } = await import('./chunks/nitro/nitro.mjs');
nitroApp = useNitroApp();
// 检查IPX配置
const runtimeConfig = nitroApp.hooks.callHook ? await nitroApp.hooks.callHook('render:route', { url: '/' }).catch(() => null) : null;
console.log('Nitro application initialized');
}
return nitroApp;
}
/**
* Handle HTTP response
*/
function handleResponse(response, event) {
if (!response) {
return {
statusCode: 500,
headers: { 'Content-Type': 'text/plain' },
body: 'Internal Server Error'
};
}
const headers = {};
// Ensure response.headers is a Headers object
if (!(response.headers instanceof Headers)) {
response.headers = new Headers(response.headers || {});
}
// Correctly iterate over Headers object (using entries() method)
for (const [key, value] of response.headers.entries()) {
headers[key] = value;
}
// Check if Content-Type already exists (case-insensitive)
const hasContentType = response.headers.has('content-type');
// Only set default value if Content-Type is missing
if (!hasContentType) {
headers['Content-Type'] = 'text/html; charset=utf-8';
}
headers['from-server'] = 'true';
// Handle set-cookie header (special handling, as there may be multiple values)
if (response.headers.has('set-cookie')) {
const cookieArr = response.headers.getSetCookie();
headers['set-cookie'] = Array.isArray(cookieArr) ? cookieArr : [cookieArr];
}
return {
statusCode: response.status || response.statusCode || 200,
headers,
body: response.body || response._data || ''
};
}
/**
* EdgeOne function handler
*/
export async function handler(event, context) {
try {
const url = event.path || '/';
const method = event.httpMethod || event.method || 'GET';
const headers = event.headers || {};
const body = event.body || '';
// First try to handle static assets
if (method === 'GET') {
const staticResponse = await handleStaticFile(url);
if (staticResponse) {
return staticResponse;
}
}
// Handle dynamic requests
const app = await getNitroApp();
try {
const response = await app.localCall({
url,
method,
headers,
body
});
return handleResponse(response, event);
} catch (nitroError) {
// Handle Nitro static file read errors (prerender files not found)
// Check error and its cause property (H3Error may wrap actual error in cause)
const actualError = nitroError?.cause || nitroError;
const errorPath = actualError?.path || nitroError?.path;
const errorCode = actualError?.code || nitroError?.code;
// If error is due to prerender static file not found, try dynamic rendering
if (errorCode === 'ENOENT' &&
errorPath &&
(errorPath.includes('/assets/') || errorPath.includes('assets/')) &&
(errorPath.includes('/index.html') || errorPath.includes('index.html'))) {
console.warn(`Prerender file not found: ${errorPath}, falling back to dynamic rendering for ${url}`);
// If static file handling has been tried but file not found, should perform dynamic rendering
// Nitro should be able to handle dynamic routes, but if it still tries to read static files,
// it may be due to configuration issues. We throw an error directly to let user know to build or check configuration
throw new Error(`Prerender route ${url} not found. Make sure to run build first or configure routeRules correctly. Original error: ${actualError?.message || nitroError?.message}`);
}
// Other errors are thrown directly
throw nitroError;
}
} catch (error) {
console.error('EdgeOne handler error:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'text/plain' },
body: `Internal Server Error: ${error.message}`
};
}
}
import('http').then(async (http) => {
const { createServer } = http;
// Dynamically import stream module to handle ReadableStream
await import('stream').then(({ Readable, pipeline }) => {
const server = createServer(async (req, res) => {
try {
const event = {
path: req.url,
httpMethod: req.method,
headers: req.headers,
body: ''
};
if (req.method !== 'GET' && req.method !== 'HEAD') {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
event.body = Buffer.concat(chunks).toString();
}
const result = await handler(event, {});
res.statusCode = result.statusCode;
Object.entries(result.headers).forEach(([key, value]) => {
if(key === 'set-cookie') {
res.setHeader('set-cookie', Array.isArray(value) ? value[0].split(',') : value);
} else {
res.setHeader(key, value);
}
});
// Handle response body: support Buffer, string, and ReadableStream
if (Buffer.isBuffer(result.body)) {
res.end(result.body);
} else if (result.body && typeof result.body === 'object' && typeof result.body.getReader === 'function') {
// Detect ReadableStream (Web Streams API)
try {
const nodeStream = Readable.fromWeb(result.body);
nodeStream.pipe(res);
} catch (streamError) {
console.error('Stream conversion error:', streamError);
// If conversion fails, try to read the entire stream
const reader = result.body.getReader();
const chunks = [];
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(Buffer.from(value));
}
res.end(Buffer.concat(chunks));
} catch (readError) {
console.error('Stream read error:', readError);
res.end();
}
}
} else {
// Handle string or other types
res.end(result.body || '');
}
} catch (error) {
console.error('Local server error:', error);
res.statusCode = 500;
res.setHeader('Content-Type', 'text/plain');
res.end(`Server Error: ${error.message}`);
}
});
const port = process.env.DEV_PORT || 9000;
server.listen(port, () => {
console.log(`EdgeOne development server running at http://localhost:${port}`);
console.log(`Static assets served from: ${ASSET_DIR}`);
});
});
});