Initial TrueGrowth source import
This commit is contained in:
321
scripts/publish-cdn-assets.js
Normal file
321
scripts/publish-cdn-assets.js
Normal file
@@ -0,0 +1,321 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user