Files
TrueGrowth/scripts/publish-cdn-assets.js

322 lines
8.3 KiB
JavaScript
Raw Permalink 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
/**
* 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);
});