Files
Max-Cocos-Demo/extensions/max-studio/source/hotupdate/version_generator.ts
2025-10-28 21:55:41 +08:00

185 lines
6.2 KiB
TypeScript

import fs, { existsSync } from "fs";
import { ensureDirSync, copyFileSync } from "fs-extra";
import { createHash } from "crypto";
import path from "path";
import { CfgUtils } from "./cfgutils";
export interface GenPackageConfig {
pkgName: string;
files: string[];
version?: string;
}
class Manifest {
packageUrl: string = "";
remoteManifestUrl: string = "";
remoteVersionUrl: string = "";
version: string = "";
assets: object = {};
searchPaths: string[] = [];
}
class VersionManifest {
packageUrl: string = "";
remoteManifestUrl: string = "";
remoteVersionUrl: string = "";
version: string = "";
}
function readDir(buildDir: string, destination: string, dir: string, obj: any) {
try {
const stat = fs.statSync(dir);
if (stat.isFile()) {
const subpath = dir;
const size = stat.size;
const md5 = createHash('md5').update(fs.readFileSync(subpath)).digest('hex');
const compressed = path.extname(subpath).toLowerCase() === '.zip';
let relv = path.relative(buildDir, subpath);
relv = relv.replace(/\\/g, '/');
relv = encodeURI(relv);
obj[relv] = {
"size": size,
"md5": md5
};
if (compressed) {
obj[relv].compressed = true;
}
const index = relv.lastIndexOf("/");
const subdir = relv.substring(0, index);
mkdirSync(path.join(destination, subdir));
copyFileSync(subpath, path.join(destination, relv));
} else {
const subpaths = fs.readdirSync(dir);
for (let i = 0; i < subpaths.length; i++) {
if (subpaths[i][0] === '.') {
continue;
}
const subpath = path.join(dir, subpaths[i]);
const stat = fs.statSync(subpath);
if (stat.isDirectory()) {
readDir(buildDir, destination, subpath, obj);
} else if (stat.isFile()) {
const size = stat.size;
const md5 = createHash('md5').update(fs.readFileSync(subpath)).digest('hex');
const compressed = path.extname(subpath).toLowerCase() === '.zip';
let relv = path.relative(buildDir, subpath);
relv = relv.replace(/\\/g, '/');
relv = encodeURI(relv);
obj[relv] = {
"size": size,
"md5": md5
};
if (compressed) {
obj[relv].compressed = true;
}
const index = relv.lastIndexOf("/");
const subdir = relv.substring(0, index);
mkdirSync(path.join(destination, subdir));
copyFileSync(subpath, path.join(destination, relv));
}
}
}
} catch (error) {
console.error(error);
}
}
function mkdirSync(path: string) {
try {
ensureDirSync(path);
} catch (error: any) {
if (error.code != 'EEXIST') throw error;
}
}
function versionCompare(versionA: string, versionB: string) {
const vA = versionA.split('.');
const vB = versionB.split('.');
for (let i = 0; i < vA.length; ++i) {
const a = parseInt(vA[i]);
const b = parseInt(vB[i] || '0');
if (a === b) {
continue;
} else {
return a - b;
}
}
return vB.length > vA.length ? -1 : 0;
}
function clearDir(dir: string) {
try {
if (existsSync(dir)) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
clearDir(filePath);
fs.rmdirSync(filePath);
} else {
fs.unlinkSync(filePath);
}
}
}
} catch (err) {
console.error('清理目录失败:', err);
}
}
export function generator(buildDir: string, output: string, cfg: GenPackageConfig) {
const destination = path.join(output,cfg.pkgName);
const manifest = new Manifest();
const version = new VersionManifest();
const cfgData = CfgUtils.getCfgData(cfg.pkgName);
if (cfg.version) {
cfgData.version = cfg.version;
} else {
const versionParts = cfgData.version.split('.');
const patch = parseInt(versionParts[2] || '0') + 1;
cfgData.version = `${versionParts[0]}.${versionParts[1]}.${patch}`;
}
manifest.version = cfgData.version;
manifest.packageUrl = `${cfg.pkgName}/${cfgData.version}`;
manifest.remoteManifestUrl = `${cfg.pkgName}/project.manifest`;
manifest.remoteVersionUrl = `${cfg.pkgName}/version.manifest`;
version.version = cfgData.version;
// 清理输出目录
clearDir(destination);
ensureDirSync(destination);
// 处理文件
for (const file of cfg.files) {
const filePath = path.join(buildDir, file);
if (existsSync(filePath)) {
readDir(buildDir, destination, filePath, manifest.assets);
}
}
// 生成 manifest 文件到输出目录
fs.writeFileSync(path.join(destination, 'project.manifest'), JSON.stringify(manifest, null, 2));
fs.writeFileSync(path.join(destination, 'version.manifest'), JSON.stringify(version, null, 2));
// 将 manifest 和 version 文件也存储到 buildDir + "/assets/" + pkgName 目录下
const buildAssetsDir = path.join(buildDir, "assets", cfg.pkgName);
ensureDirSync(buildAssetsDir);
fs.writeFileSync(path.join(buildAssetsDir, 'project.manifest'), JSON.stringify(manifest, null, 2));
fs.writeFileSync(path.join(buildAssetsDir, 'version.manifest'), JSON.stringify(version, null, 2));
console.log(`Manifest文件已生成到构建目录: ${buildAssetsDir}`);
console.log(`Manifest文件已生成到输出目录: ${destination}`);
// 保存配置
CfgUtils.savaConfig(cfg.pkgName, cfgData);
console.log(`热更新包生成完成: ${cfg.pkgName} v${cfgData.version}`);
}