Files
TrueGrowth/scripts/build-hybrid.js

345 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 混合部署构建脚本
*
* 构建产物分离:
* 1. HTML 文件 → 自有服务器(保护用户信息)
* 2. 静态资源 → npm CDN节约流量
*
* 用法:
* node scripts/build-hybrid.js [--version x.x.x] [--cdn unpkg|jsdelivr]
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// 配置
const CONFIG = {
packageName: 'aitu-app',
distDir: path.resolve(__dirname, '../dist/apps/web'),
// 输出目录
outputServer: path.resolve(__dirname, '../dist/deploy/server'), // HTML 文件
outputCDN: path.resolve(__dirname, '../dist/deploy/cdn'), // 静态资源
// CDN URL 模板
cdnTemplates: {
unpkg: 'https://unpkg.com/aitu-app@{version}',
jsdelivr: 'https://cdn.jsdelivr.net/npm/aitu-app@{version}',
},
// 需要保留在服务器的文件(不上传到 CDN
serverOnlyFiles: [
'index.html',
'sw-debug.html',
'cdn-debug.html',
'versions.html',
'iframe-test.html',
// 配置文件也留在服务器
],
// 不上传到 CDN 的文件模式
excludeFromCDN: [
/\.html$/,
/^init\.json$/,
/\.map$/, // source maps
],
};
// 解析命令行参数
const args = process.argv.slice(2);
const versionArg = args.find(arg => arg.startsWith('--version='));
const cdnArg = args.find(arg => arg.startsWith('--cdn='));
const skipBuild = args.includes('--skip-build');
const cdnProvider = cdnArg ? cdnArg.split('=')[1] : 'unpkg';
// 颜色输出
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
red: '\x1b[31m',
cyan: '\x1b[36m',
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function logStep(step, message) {
log(`\n[${step}] ${message}`, 'blue');
}
function logSuccess(message) {
log(`${message}`, 'green');
}
function logWarning(message) {
log(`${message}`, 'yellow');
}
function logError(message) {
log(`${message}`, 'red');
}
// 读取版本号
function getVersion() {
if (versionArg) {
return versionArg.split('=')[1];
}
const versionPath = path.resolve(__dirname, '../apps/web/public/version.json');
if (fs.existsSync(versionPath)) {
const versionJson = JSON.parse(fs.readFileSync(versionPath, 'utf-8'));
return versionJson.version;
}
const pkgPath = path.resolve(__dirname, '../package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
return pkg.version;
}
// 确保目录存在
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
// 复制文件
function copyFile(src, dest) {
ensureDir(path.dirname(dest));
fs.copyFileSync(src, dest);
}
// 递归复制目录
function copyDir(src, dest, filter = () => true) {
ensureDir(dest);
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDir(srcPath, destPath, filter);
} else if (filter(entry.name, srcPath)) {
copyFile(srcPath, destPath);
}
}
}
// 检查文件是否应该上传到 CDN
function shouldUploadToCDN(filename) {
for (const pattern of CONFIG.excludeFromCDN) {
if (pattern instanceof RegExp) {
if (pattern.test(filename)) return false;
} else if (filename === pattern) {
return false;
}
}
return true;
}
// 检查文件是否应该保留在服务器
function shouldKeepOnServer(filename) {
return CONFIG.serverOnlyFiles.some(f => filename === f || filename.endsWith(f));
}
// 替换 HTML 中的资源路径
function replaceAssetPaths(htmlContent, cdnBaseUrl) {
// 替换相对路径的 JS 引用
// ./assets/xxx.js → CDN_URL/assets/xxx.js
let result = htmlContent;
// 替换 script src
result = result.replace(
/src=["'](\.\/)?assets\//g,
`src="${cdnBaseUrl}/assets/`
);
// 替换 link href (CSS)
result = result.replace(
/href=["'](\.\/)?assets\//g,
`href="${cdnBaseUrl}/assets/`
);
// 替换 favicon 和图标
result = result.replace(
/href=["'](\.\/)?icons\//g,
`href="${cdnBaseUrl}/icons/`
);
// 替换 manifest
result = result.replace(
/href=["'](\.\/)?manifest\.json/g,
`href="${cdnBaseUrl}/manifest.json`
);
// 保留 sw.js 在本地Service Worker 必须同源)
// 不替换 sw.js 的路径
// 替换 cdn-config.js 路径
result = result.replace(
/src=["'](\.\/)?cdn-config\.js/g,
`src="${cdnBaseUrl}/cdn-config.js`
);
return result;
}
// 主流程
async function main() {
log('\n🚀 混合部署构建脚本\n', 'cyan');
const version = getVersion();
const cdnBaseUrl = CONFIG.cdnTemplates[cdnProvider].replace('{version}', version);
log(`📦 版本: ${version}`, 'cyan');
log(`🌐 CDN: ${cdnProvider} (${cdnBaseUrl})`, 'cyan');
// 步骤 1: 构建项目
if (!skipBuild) {
logStep('1/5', '构建项目');
try {
execSync('npm run build:web', { stdio: 'inherit', cwd: path.resolve(__dirname, '..') });
logSuccess('构建完成');
} catch (error) {
logError('构建失败');
process.exit(1);
}
} else {
logStep('1/5', '跳过构建(使用现有产物)');
}
// 检查构建产物
if (!fs.existsSync(CONFIG.distDir)) {
logError(`构建目录不存在: ${CONFIG.distDir}`);
process.exit(1);
}
// 步骤 2: 清理输出目录
logStep('2/5', '清理输出目录');
if (fs.existsSync(CONFIG.outputServer)) {
fs.rmSync(CONFIG.outputServer, { recursive: true });
}
if (fs.existsSync(CONFIG.outputCDN)) {
fs.rmSync(CONFIG.outputCDN, { recursive: true });
}
ensureDir(CONFIG.outputServer);
ensureDir(CONFIG.outputCDN);
logSuccess('输出目录已清理');
// 步骤 3: 分离文件
logStep('3/5', '分离 HTML 和静态资源');
let serverFileCount = 0;
let cdnFileCount = 0;
// 遍历构建产物
function processDir(dir, relativePath = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(dir, entry.name);
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
processDir(srcPath, relPath);
} else {
const filename = entry.name;
// 决定文件去向
if (shouldKeepOnServer(filename)) {
// HTML 和敏感文件 → 服务器
const destPath = path.join(CONFIG.outputServer, relPath);
copyFile(srcPath, destPath);
serverFileCount++;
log(` → 服务器: ${relPath}`);
}
if (shouldUploadToCDN(filename)) {
// 静态资源 → CDN
const destPath = path.join(CONFIG.outputCDN, relPath);
copyFile(srcPath, destPath);
cdnFileCount++;
}
// Service Worker 需要同时存在于服务器
if (filename === 'sw.js') {
const destPath = path.join(CONFIG.outputServer, relPath);
copyFile(srcPath, destPath);
serverFileCount++;
log(` → 服务器: ${relPath} (Service Worker 必须同源)`);
}
}
}
}
processDir(CONFIG.distDir);
logSuccess(`服务器文件: ${serverFileCount} 个, CDN 文件: ${cdnFileCount}`);
// 步骤 4: 修改 HTML 中的资源路径
logStep('4/5', '修改 HTML 资源路径指向 CDN');
const htmlFiles = fs.readdirSync(CONFIG.outputServer).filter(f => f.endsWith('.html'));
for (const htmlFile of htmlFiles) {
const htmlPath = path.join(CONFIG.outputServer, htmlFile);
let content = fs.readFileSync(htmlPath, 'utf-8');
// 替换资源路径
content = replaceAssetPaths(content, cdnBaseUrl);
// 添加 CDN 版本注释
content = content.replace(
'</head>',
` <!-- CDN: ${cdnProvider} v${version} -->\n </head>`
);
fs.writeFileSync(htmlPath, content);
log(`${htmlFile}`);
}
logSuccess('HTML 资源路径已更新');
// 步骤 5: 生成部署信息
logStep('5/5', '生成部署信息');
const deployInfo = {
version,
buildTime: new Date().toISOString(),
cdnProvider,
cdnBaseUrl,
serverFiles: fs.readdirSync(CONFIG.outputServer),
cdnFileCount,
};
fs.writeFileSync(
path.join(CONFIG.outputServer, 'deploy-info.json'),
JSON.stringify(deployInfo, null, 2)
);
// 输出摘要
log('\n' + '═'.repeat(50), 'cyan');
log('📋 部署摘要', 'cyan');
log('═'.repeat(50), 'cyan');
log(`\n📁 服务器文件目录: ${CONFIG.outputServer}`);
log(` 包含: ${htmlFiles.join(', ')}, sw.js`);
log(`\n📁 CDN 文件目录: ${CONFIG.outputCDN}`);
log(` 包含: ${cdnFileCount} 个静态资源文件`);
log(`\n🌐 CDN 地址: ${cdnBaseUrl}`);
log('\n📝 下一步操作:', 'yellow');
log(' 1. 发布 CDN 文件到 npm:');
log(` cd ${CONFIG.outputCDN} && npm publish`);
log(' 2. 部署服务器文件:');
log(`${CONFIG.outputServer} 中的文件部署到你的服务器`);
log('\n✅ 构建完成!\n', 'green');
}
main().catch(error => {
logError(`脚本执行失败: ${error.message}`);
console.error(error);
process.exit(1);
});