322 lines
8.3 KiB
JavaScript
322 lines
8.3 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* CDN 静态资源发布脚本
|
||
*
|
||
* 只发布静态资源到 npm,不包含 HTML 文件
|
||
*
|
||
* 安全特性:
|
||
* - 不上传 HTML 文件(避免用户配置泄露)
|
||
* - 不上传 source maps
|
||
* - 不上传敏感配置文件
|
||
*
|
||
* 用法:
|
||
* node scripts/publish-cdn-assets.js [--dry-run] [--skip-build] [--otp=123456]
|
||
*/
|
||
|
||
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'),
|
||
cdnDir: path.resolve(__dirname, '../dist/deploy/cdn'),
|
||
rootPackageJson: path.resolve(__dirname, '../package.json'),
|
||
// 不上传到 CDN 的文件
|
||
excludePatterns: [
|
||
/\.html$/, // HTML 文件
|
||
/\.map$/, // Source maps
|
||
/^init\.json$/, // 初始化配置
|
||
/^stats\.html$/, // 构建分析
|
||
/deploy-info\.json/, // 部署信息
|
||
],
|
||
};
|
||
|
||
// 解析命令行参数
|
||
const args = process.argv.slice(2);
|
||
const isDryRun = args.includes('--dry-run');
|
||
const skipBuild = args.includes('--skip-build');
|
||
const otpArg = args.find(arg => arg.startsWith('--otp='));
|
||
const otp = otpArg ? otpArg.split('=')[1] : null;
|
||
|
||
// 颜色输出
|
||
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 logError(message) {
|
||
log(`✗ ${message}`, 'red');
|
||
}
|
||
|
||
// 执行命令
|
||
function exec(command, options = {}) {
|
||
log(` 执行: ${command}`, 'yellow');
|
||
try {
|
||
execSync(command, { stdio: 'inherit', ...options });
|
||
return true;
|
||
} catch (error) {
|
||
logError(`命令执行失败: ${command}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 读取版本号
|
||
function getVersion() {
|
||
const pkg = JSON.parse(fs.readFileSync(CONFIG.rootPackageJson, 'utf-8'));
|
||
return pkg.version;
|
||
}
|
||
|
||
// 确保目录存在
|
||
function ensureDir(dir) {
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
}
|
||
|
||
// 检查是否应该排除
|
||
function shouldExclude(filename) {
|
||
return CONFIG.excludePatterns.some(pattern => {
|
||
if (pattern instanceof RegExp) {
|
||
return pattern.test(filename);
|
||
}
|
||
return filename === pattern;
|
||
});
|
||
}
|
||
|
||
// 复制文件(排除敏感文件)
|
||
function copyFilteredFiles(src, dest) {
|
||
ensureDir(dest);
|
||
let copied = 0;
|
||
let excluded = 0;
|
||
|
||
function processDir(srcDir, destDir, relativePath = '') {
|
||
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
||
|
||
for (const entry of entries) {
|
||
const srcPath = path.join(srcDir, entry.name);
|
||
const destPath = path.join(destDir, entry.name);
|
||
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||
|
||
if (entry.isDirectory()) {
|
||
ensureDir(destPath);
|
||
processDir(srcPath, destPath, relPath);
|
||
} else {
|
||
if (shouldExclude(entry.name)) {
|
||
excluded++;
|
||
// log(` 排除: ${relPath}`);
|
||
} else {
|
||
fs.copyFileSync(srcPath, destPath);
|
||
copied++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
processDir(src, dest);
|
||
return { copied, excluded };
|
||
}
|
||
|
||
// 生成 npm package.json
|
||
function generateNpmPackageJson(version) {
|
||
return {
|
||
name: CONFIG.packageName,
|
||
version: version,
|
||
description: 'Opentu static assets for CDN deployment (HTML files not included for security)',
|
||
keywords: [
|
||
'aitu',
|
||
'whiteboard',
|
||
'assets',
|
||
'cdn',
|
||
],
|
||
homepage: 'https://opentu.ai',
|
||
repository: {
|
||
type: 'git',
|
||
url: 'https://github.com/ljquan/aitu.git'
|
||
},
|
||
license: 'MIT',
|
||
author: 'ljquan',
|
||
files: ['**/*'],
|
||
publishConfig: {
|
||
access: 'public'
|
||
},
|
||
// 标记这是纯静态资源包
|
||
aituAssets: {
|
||
type: 'cdn-assets',
|
||
htmlIncluded: false,
|
||
note: 'This package contains only static assets (JS, CSS, images, fonts). HTML files are served from your own server for security.'
|
||
}
|
||
};
|
||
}
|
||
|
||
// 生成 README
|
||
function generateReadme(version) {
|
||
return `# Opentu CDN Assets
|
||
|
||
> ⚠️ **安全说明**: 此包仅包含静态资源(JS、CSS、图片、字体),不包含 HTML 文件。HTML 文件应从你自己的服务器提供。
|
||
|
||
## 版本
|
||
|
||
当前版本: **${version}**
|
||
|
||
## CDN 访问
|
||
|
||
### unpkg
|
||
\`\`\`
|
||
https://unpkg.com/${CONFIG.packageName}@${version}/assets/index-xxx.js
|
||
https://unpkg.com/${CONFIG.packageName}@${version}/assets/index-xxx.css
|
||
\`\`\`
|
||
|
||
### jsDelivr
|
||
\`\`\`
|
||
https://cdn.jsdelivr.net/npm/${CONFIG.packageName}@${version}/assets/index-xxx.js
|
||
https://cdn.jsdelivr.net/npm/${CONFIG.packageName}@${version}/assets/index-xxx.css
|
||
\`\`\`
|
||
|
||
## 使用方式
|
||
|
||
1. 从你的服务器提供 HTML 文件
|
||
2. 修改 HTML 中的资源路径指向 CDN
|
||
3. 用户访问你的域名,静态资源从 CDN 加载
|
||
|
||
## 包含的文件
|
||
|
||
- \`/assets/\` - JS 和 CSS 文件
|
||
- \`/icons/\` - 图标文件
|
||
- \`/manifest.json\` - PWA 配置
|
||
|
||
## 不包含的文件
|
||
|
||
- \`*.html\` - HTML 文件(安全考虑)
|
||
- \`*.map\` - Source maps
|
||
|
||
## 许可证
|
||
|
||
MIT License
|
||
`;
|
||
}
|
||
|
||
// 主流程
|
||
async function main() {
|
||
log('\n🚀 CDN 静态资源发布脚本\n', 'cyan');
|
||
|
||
if (isDryRun) {
|
||
log('⚠️ DRY RUN 模式 - 不会实际发布\n', 'yellow');
|
||
}
|
||
|
||
const version = getVersion();
|
||
log(`📦 版本: ${version}`, 'cyan');
|
||
|
||
// 步骤 1: 构建
|
||
if (!skipBuild) {
|
||
logStep('1/4', '构建项目');
|
||
if (!exec('npm run build:web', { cwd: path.resolve(__dirname, '..') })) {
|
||
logError('构建失败');
|
||
process.exit(1);
|
||
}
|
||
logSuccess('构建完成');
|
||
} else {
|
||
logStep('1/4', '跳过构建');
|
||
}
|
||
|
||
// 检查构建产物
|
||
if (!fs.existsSync(CONFIG.distDir)) {
|
||
logError(`构建目录不存在: ${CONFIG.distDir}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
// 步骤 2: 准备 CDN 目录
|
||
logStep('2/4', '准备 CDN 发布目录');
|
||
|
||
if (fs.existsSync(CONFIG.cdnDir)) {
|
||
fs.rmSync(CONFIG.cdnDir, { recursive: true });
|
||
}
|
||
ensureDir(CONFIG.cdnDir);
|
||
|
||
// 复制文件(排除敏感文件)
|
||
const { copied, excluded } = copyFilteredFiles(CONFIG.distDir, CONFIG.cdnDir);
|
||
logSuccess(`复制 ${copied} 个文件,排除 ${excluded} 个敏感文件`);
|
||
|
||
// 步骤 3: 生成 package.json 和 README
|
||
logStep('3/4', '生成 npm 发布文件');
|
||
|
||
const npmPackage = generateNpmPackageJson(version);
|
||
fs.writeFileSync(
|
||
path.join(CONFIG.cdnDir, 'package.json'),
|
||
JSON.stringify(npmPackage, null, 2)
|
||
);
|
||
logSuccess('生成 package.json');
|
||
|
||
const readme = generateReadme(version);
|
||
fs.writeFileSync(path.join(CONFIG.cdnDir, 'README.md'), readme);
|
||
logSuccess('生成 README.md');
|
||
|
||
// 列出排除的文件类型
|
||
log('\n📋 排除的文件类型:', 'cyan');
|
||
log(' - *.html (HTML 文件)');
|
||
log(' - *.map (Source maps)');
|
||
|
||
// 步骤 4: 发布
|
||
logStep('4/4', '发布到 npm');
|
||
|
||
if (isDryRun) {
|
||
log('\n📦 DRY RUN - 将要发布的内容:', 'yellow');
|
||
exec(`ls -la "${CONFIG.cdnDir}"`);
|
||
log('\n📄 package.json:', 'yellow');
|
||
console.log(JSON.stringify(npmPackage, null, 2));
|
||
} else {
|
||
let publishCmd = `cd "${CONFIG.cdnDir}" && npm publish --access public --registry https://registry.npmjs.org`;
|
||
|
||
if (otp) {
|
||
publishCmd += ` --otp=${otp}`;
|
||
log(` 使用 OTP: ${otp.slice(0, 2)}****`);
|
||
}
|
||
|
||
if (!exec(publishCmd)) {
|
||
logError('发布失败');
|
||
if (!otp) {
|
||
log('\n💡 提示:如果启用了 2FA,请使用 --otp=123456 参数', 'yellow');
|
||
}
|
||
process.exit(1);
|
||
}
|
||
logSuccess('发布成功!');
|
||
}
|
||
|
||
// 输出访问链接
|
||
log('\n' + '═'.repeat(50), 'green');
|
||
log('🎉 完成!', 'green');
|
||
log('═'.repeat(50), 'green');
|
||
log('\n📌 CDN 访问地址:', 'cyan');
|
||
log(` unpkg: https://unpkg.com/${CONFIG.packageName}@${version}/`);
|
||
log(` jsdelivr: https://cdn.jsdelivr.net/npm/${CONFIG.packageName}@${version}/`);
|
||
|
||
log('\n📝 下一步:', 'yellow');
|
||
log(' 1. 在你的服务器上部署 HTML 文件');
|
||
log(' 2. 修改 HTML 中的资源路径指向 CDN');
|
||
log(' 3. 或使用 build-hybrid.js 自动处理\n');
|
||
}
|
||
|
||
main().catch(error => {
|
||
logError(`脚本执行失败: ${error.message}`);
|
||
console.error(error);
|
||
process.exit(1);
|
||
});
|